├── .gitignore ├── README.md ├── docs ├── basic.md ├── constants.md ├── functions.md ├── memory.md ├── statements.md ├── stdio.md ├── stdlib.md └── variables.md ├── examples ├── arrays.calc ├── breakinggates.calc ├── fizzbuzz.calc └── turingmachine.calc ├── pycalc ├── __init__.py ├── interpreter │ └── interpret.py ├── lex │ ├── __init__.py │ └── tokenizer.py ├── stack │ └── builder.py └── tokentypes │ ├── __init__.py │ ├── tokens.py │ └── types.py ├── repl.py ├── std ├── __init__.py ├── stdio.py ├── stdlibrary.py ├── stdmem.py └── stdstatements.py └── tests ├── __init__.py ├── __main__.py └── testcases.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | 4 | hi.py 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![wakatime](https://wakatime.com/badge/user/b4a2ea9a-a721-41c8-b704-79b9b8cec646/project/3e68a432-deec-4dab-ba8a-a5f424e91eed.svg)](https://wakatime.com/badge/user/b4a2ea9a-a721-41c8-b704-79b9b8cec646/project/3e68a432-deec-4dab-ba8a-a5f424e91eed) 2 | # pycalc 3 | Simple calculator on python, written in academic purposes. TURING-COMPLETE. Uses Sorting Station Algorithm for building reverse polish notation stack. Supports all kinds of operations python supports (except bool operations like or, not, etc. but they will be implemented as a functions of std-library), functions defining, variables declarations, etc. 4 | 5 | # How to install? 6 | ```bash 7 | $ git clone https://github.com/fakefloordiv/pycalc && cd pycalc 8 | ``` 9 | 10 | # How to run it? 11 | For code running, we have repl.py. It has such options: 12 | - No options (interactive shell) 13 | - -e, --execute: execute expression from command line 14 | - -s, --script: execute code from file (with .calc extension) 15 | 16 | For example: 17 | ```bash 18 | $ python3 repl.py 19 | ``` 20 | 21 | Or: 22 | ```bash 23 | $ python3 repl.py -e "40+2" 24 | 42 25 | ``` 26 | 27 | Even: 28 | ```bash 29 | $ python3 repl.py -s examples/fizzbuzz.calc 30 | ``` 31 | 32 | # How to use it? 33 | I personally allow you to use: integers, floats, constants, and functions (including defining). For example: 34 | ``` 35 | f(x,y)=x+y 36 | 40 + rt(25, 5) - pi + 0.14 / .14 << f(1,2) 37 | ``` 38 | 39 | # Documentation 40 | See documentation in `docs/` folder. 41 | -------------------------------------------------------------------------------- /docs/basic.md: -------------------------------------------------------------------------------- 1 | # Operations 2 | There are 14 operators (and 3 internal): 3 | - `+` add 4 | - `-` subtract 5 | - `/` divide 6 | - `//` floordiv (same as usual divide, but returns integer without floating part) 7 | - `*` multuply 8 | - `**` power 9 | - `%` modulo 10 | - `<<` left bitshift 11 | - `>>` right bitshift 12 | - `&` bitwise and 13 | - `|` bitwise or 14 | - `^` bitwise xor 15 | - `==` is equal (returns 0 if not, 1 if equal) 16 | - `!=` not equal (inverted equal) 17 | 18 | # Unary operations 19 | Unary is also supported.
20 | For example: `2+-2 == 0`
21 | Or even: `-+--++--+-++--+++-+1 == -1` 22 | 23 | # Numbers 24 | There are 3 types of numbers: 25 | - integers (example: `42`) 26 | - floats (example: `0.42`, or even `.5` (that is simply `0.5`); exponenta is not supported) 27 | - hexdecimals (example: `0x5f3759df`, just an alternative notation of integers) 28 | 29 | # Strings 30 | Strings are usual strings, except only double quotes are allowed. 31 | `"Hello, world!"` is valid. `'Hello, world!'` is not 32 | 33 | Strings are just strings from python. Of course, string methods aren't allowed 34 | (just like any other methods), but all the other operations (like concatenation) 35 | are available 36 | -------------------------------------------------------------------------------- /docs/constants.md: -------------------------------------------------------------------------------- 1 | ## `pi` 2 | Contains a value of pi -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | ### Functions declaring 3 | To define a function, simply type `() = `. 4 | For example: `f(x,y)=x+y` 5 | 6 | Also, multi-lined functions are allowed. Rules of expression lines break: 7 | - Expression is inside of parenthesis 8 | - Or there is an operator in the end of previous line 9 | 10 | For example: 11 | ``` 12 | f(x, y) = 13 | a = x + y; 14 | println(a) 15 | ``` 16 | 17 | Or: 18 | 19 | ``` 20 | sum(mem) = reduce( 21 | (x, y) = x + y, 22 | mem 23 | ) 24 | ``` 25 | 26 | ### Multiple expressions in functions 27 | Wait, what? You said function body is a single expression! 28 | Well, yes. The problem is function body is a single expression __for interpreter__. You, as human, can write multiple expressions in function body as well. For this, you need to separate them with semicolon. Result of the last one expression is function result 29 | For example: 30 | ``` 31 | f(x)=x+5;x 32 | ``` 33 | How do you think, what will `f(1)` return? Correct, value of `x`. `x+5` does nothing, so result of this expression will be removed from the stack after semicolon 34 | 35 | ### Higher-order functions 36 | PyCalc supports even this. Higher-order function is a function that returns another function. 37 | For example: 38 | ``` 39 | f(a) = y(b) = a * b 40 | mulBy5 = f(5) 41 | mulBy5(5) 42 | ``` 43 | This example will return `25` 44 | 45 | ### Function calls 46 | To call a function, simply type `()`. 47 | For example: `root(25, 2)` 48 | 49 | Function may have \[0, +∞) arguments, separated by comma. Function call has maximal priority (just like power) 50 | 51 | ### Lambda 52 | Lambda is simply function without name. It can be defined directly as an argument for some function. Or just be a value for some variable. 53 | 54 | Semantic: 55 | ``` 56 | (x, y) = x + y 57 | ``` 58 | Example: 59 | ``` 60 | map((x)=x**2, range(0, 10)) 61 | ``` 62 | 63 | Or even: 64 | ``` 65 | sqpow = (x)=x**2 66 | sqpow(2) 67 | ``` -------------------------------------------------------------------------------- /docs/memory.md: -------------------------------------------------------------------------------- 1 | ## `malloc` 2 | Semantic: 3 | ``` 4 | malloc(int) 5 | ``` 6 | Returns: array with given length filled by zeroes 7 | 8 | Example: 9 | ``` 10 | mem = malloc(15) 11 | println(mem) 12 | ``` 13 | This example will print an array with length of 15 filled by zeroes 14 | 15 | --- 16 | 17 | ## `mallocfor` 18 | Semantic: 19 | ``` 20 | mallocfor(values...) 21 | ``` 22 | Returns: memory filled with values instead of zeroes. Size of memory equals to number of given values 23 | 24 | Example: 25 | ``` 26 | mem = mallocfor(1, 2, 3, 4, 5) 27 | println(mem) 28 | ``` 29 | This is hello world 30 | 31 | --- 32 | 33 | ## `get` 34 | Semantic: 35 | ``` 36 | get(mem, position) 37 | ``` 38 | Returns: value by given index 39 | 40 | Example: 41 | ``` 42 | mem = range(0, 8) 43 | get(mem, 4) == 5 44 | ``` 45 | This example will make sure that element with index 4 equals 5 (indexes are as usually, starts with 0) 46 | 47 | --- 48 | 49 | ## `set` 50 | Semantic: 51 | ``` 52 | set(mem, position, value) 53 | ``` 54 | Returns: 0 if everything is fine, -1 if position is out of bounds 55 | 56 | Example: 57 | ``` 58 | mem = malloc(15) 59 | set(mem, 0x1, 0x15) 60 | get(mem, 0x1) == 0x15 61 | ``` 62 | This example will make sure that after we set a value 0x15 with index 1, it really equals 0x15 63 | 64 | --- 65 | 66 | ## `len` 67 | Semantic: 68 | ``` 69 | len(mem) 70 | ``` 71 | Returns: length of allocated memory 72 | 73 | Example: 74 | ``` 75 | len(malloc(15)) == 15 76 | ``` 77 | This example will make sure that newly allocated memory length is 15 78 | -------------------------------------------------------------------------------- /docs/statements.md: -------------------------------------------------------------------------------- 1 | There are no statements. But there are functions that can replace them 2 | 3 | ## `if` 4 | Semantic: 5 | ``` 6 | if(condition, if[, else]) 7 | ``` 8 | Returns: value returned from if or else function 9 | 10 | Example: 11 | ``` 12 | a = 5 13 | if(a == 5, ()=println(chr(97)), ()=println(chr(65))) 14 | if(a != 5, ()=println(chr(97)), ()=println(chr(65))) 15 | ``` 16 | This example prints a or A, depending on whether true or false condition is 17 | 18 | --- 19 | 20 | ## `branch` 21 | Semantic: 22 | ``` 23 | branch(condition, callback[, ...[, callback]]) 24 | ``` 25 | Returns: result of first true callback 26 | 27 | Example: 28 | ``` 29 | a = 6 30 | b = 7 31 | branch(a == 5, ()=1, b == 7, ()=2, ()=3) == 2 32 | ``` 33 | This example will make sure that second callback will be called as b == 7 is true. In other case, the last one callback will be called 34 | 35 | --- 36 | 37 | ## `map` 38 | Semantic: 39 | ``` 40 | map(func(x), mem) 41 | ``` 42 | Returns: array with new values 43 | 44 | Example: 45 | ``` 46 | map(println, mem) 47 | ``` 48 | Here map is the same as in python. So this example will just print all the values from `mem` (that is a result of `malloc()`) 49 | 50 | --- 51 | 52 | ## `reduce` 53 | Semantic: 54 | ``` 55 | reduce(func(x,y), mem) 56 | ``` 57 | Returns: final value 58 | 59 | Example: 60 | ``` 61 | reduce((x,y)=x+y, range(0,5)) 62 | ``` 63 | Result of this expression is a sum of set of numbers \[0,5) 64 | 65 | ## `filter` 66 | Semantic: 67 | ``` 68 | filter(func(x), mem) 69 | ``` 70 | Returns: array with only values that satisfy a filter callback 71 | 72 | Example: 73 | ``` 74 | filter((x)=x>5, range(0,10)) 75 | ``` 76 | Result of this expression is array that contains ONLY numbers that are more than 5 77 | -------------------------------------------------------------------------------- /docs/stdio.md: -------------------------------------------------------------------------------- 1 | ## `print` 2 | Semantic: 3 | ``` 4 | print(values...) 5 | ``` 6 | Returns: 0 7 | 8 | Example: 9 | ``` 10 | print(1, 2, 3) 11 | ``` 12 | This example will print `123` WITHOUT newline in the end 13 | 14 | --- 15 | 16 | ## `println` 17 | Semantic: 18 | ``` 19 | println(values...) 20 | ``` 21 | Returns: 0 22 | 23 | Example: 24 | ``` 25 | println(1, 2, 3) 26 | ``` 27 | This example will print `123` WITH newline in the end 28 | 29 | --- 30 | 31 | ## `input` 32 | Semantic: 33 | ``` 34 | input([text]) 35 | ``` 36 | Returns: array with entered text in bytes 37 | 38 | Example: 39 | ``` 40 | text = input("How are you? ") 41 | println(text, "fine") 42 | ``` 43 | This will wait for input, after that will print back everything you typed 44 | 45 | --- 46 | 47 | ## `chr` 48 | Semantic: 49 | ``` 50 | chr(int) 51 | ``` 52 | Returns: ascii-symbol 53 | 54 | Example: 55 | ``` 56 | println(chr(97)) 57 | ``` 58 | This example will print not number 97 (as it could be without `chr()`), but `a`, that in ascii has code 97 59 | 60 | --- 61 | 62 | ## `ord` 63 | Semantic: 64 | ``` 65 | ord(char) 66 | ``` 67 | Returns: code of given ascii-symbol 68 | 69 | Example: 70 | ``` 71 | ord(chr(97)) == 97 72 | ``` 73 | In this example we make sure that `a` has code 97 74 | -------------------------------------------------------------------------------- /docs/stdlib.md: -------------------------------------------------------------------------------- 1 | ## `rt` 2 | Semantic: 3 | ``` 4 | rt(value, base) 5 | ``` 6 | Returns: root of value by base 7 | 8 | Example: 9 | ``` 10 | rt(25, 2) == 5 11 | ``` 12 | 13 | --- 14 | 15 | ## `sqrt` 16 | Semantic: 17 | ``` 18 | sqrt(value) 19 | ``` 20 | Returns: square root of value 21 | 22 | Example: 23 | ``` 24 | sqrt(25) == 5 25 | ``` 26 | 27 | --- 28 | 29 | ## `cbrt` 30 | Semantic: 31 | ``` 32 | cbrt(value) 33 | ``` 34 | Returns: cube root of value 35 | 36 | Example: 37 | ``` 38 | cbrt(9) == 3 39 | ``` 40 | 41 | --- 42 | 43 | ## `int` 44 | Semantic: 45 | ``` 46 | int(value[, base]) 47 | ``` 48 | Returns: integer 49 | 50 | Examples: 51 | ``` 52 | int("15") == 15 53 | int("5f", 16) 54 | ``` 55 | First example parses a string to integer. Second does the same but for hex-decimal 56 | 57 | --- 58 | 59 | ## `float` 60 | Semantic: 61 | ``` 62 | float(value) 63 | ``` 64 | Returns: float 65 | 66 | Examples: 67 | ``` 68 | float("3.14") == 3.14 69 | float("inf") 70 | float(".5") == .5 71 | ``` 72 | 73 | --- 74 | 75 | ## `str` 76 | Semantic: 77 | ``` 78 | str(15.5) 79 | ``` 80 | Returns: string 81 | 82 | Examples: 83 | ``` 84 | str(.5) == "0.5" 85 | ``` 86 | 87 | --- 88 | 89 | ## `strjoin` 90 | Semantic: 91 | ``` 92 | strjoin(separator, mem) 93 | ``` 94 | Returns: single string 95 | 96 | Examples: 97 | ``` 98 | strjoin(".", "123") == "1.2.3" 99 | ``` 100 | 101 | --- 102 | 103 | ## `range` 104 | Semantic: 105 | ``` 106 | range(begin[, end[, step]]) 107 | ``` 108 | Returns: array with values from `begin` to `end`. `end` is optional, if not set [0, begin) range will be returned 109 | 110 | Example: 111 | ``` 112 | range(1, 7) 113 | range(5) 114 | range(2, 10, 2) 115 | ``` 116 | 117 | --- 118 | 119 | ## `inv` 120 | Semantic: 121 | ``` 122 | inv(value) 123 | ``` 124 | Returns: inverted (bitwise not) value 125 | 126 | Example: 127 | ``` 128 | inv(5) == -6 129 | ``` 130 | -------------------------------------------------------------------------------- /docs/variables.md: -------------------------------------------------------------------------------- 1 | ## Variables declaring 2 | To declare a variable, simply type: 3 | ``` 4 | = 5 | ``` 6 | For example: 7 | ``` 8 | my_constant = (0x5f3759df + 42) >> 8 9 | ``` 10 | 11 | ## Get variable value 12 | To get a value of variable, simply type its name.
13 | For example: 14 | ``` 15 | my_constant + 5 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/arrays.calc: -------------------------------------------------------------------------------- 1 | uint8 = 2 2 | uint16 = 4 3 | uint32 = 8 4 | uint64 = 16 5 | 6 | arrMake(size, bytesize) = malloc(size*bytesize) 7 | 8 | arrGet(arr, index, bytesize) = 9 | reduce( 10 | (x, y) = x + (y<<8), 11 | slice(arr, index*bytesize, index*bytesize+bytesize) 12 | ) 13 | 14 | arrSet(arr, index, bytesize, value) = 15 | index=index*bytesize; 16 | map( 17 | (x) = set(arr, index+x, value >> (x<<3) & 0xff), 18 | range(0, bytesize) 19 | ); 20 | 0 21 | arrMap(arr, func, bytesize) = 22 | map( 23 | (x) = func(arrGet(arr, x, bytesize)), 24 | range(0, len(arr)//bytesize) 25 | ); 26 | 0 27 | 28 | mem = arrMake(4, 4) 29 | arrSet(mem, 0, 4, 32754) 30 | arrSet(mem, 1, 4, 167) 31 | arrSet(mem, 3, 4, 12765) 32 | println(mem) 33 | println(arrGet(mem, 0, 4)) 34 | arrMap(mem, (x)=print(x, " "), 4) 35 | println() 36 | -------------------------------------------------------------------------------- /examples/breakinggates.calc: -------------------------------------------------------------------------------- 1 | src = input("1: ") 2 | modified = input("2: ") 3 | 4 | count(string, char) = 5 | len(filter( 6 | (x) = x==char, 7 | string 8 | )) 9 | 10 | extra = 0 11 | map( 12 | (x) = 13 | if(count(src, x) != count(modified, x), () = extra=x), 14 | modified 15 | ) 16 | println(extra) -------------------------------------------------------------------------------- /examples/fizzbuzz.calc: -------------------------------------------------------------------------------- 1 | limit_raw = input("Limit: ") 2 | limit = 0; 3 | get(map( 4 | (x) = 5 | limit = (limit*10) + (x-48), 6 | limit_raw 7 | ), 8 | len(limit_raw)-1 9 | ) 10 | 11 | map( 12 | (x) = 13 | branch( 14 | x % 15 == 0, () = print("fizzbuzz "), 15 | x % 3 == 0, () = print("fizz "), 16 | x % 5 == 0, () = print("buzz "), 17 | () = print(x, " ") 18 | ), 19 | range(1, limit+1) 20 | ) 21 | println() -------------------------------------------------------------------------------- /examples/turingmachine.calc: -------------------------------------------------------------------------------- 1 | rulesMap(func, rules) = 2 | map( 3 | (x) = func(slice(rules, x, x+5)), 4 | range(0, len(rules), 5) 5 | ) 6 | 7 | step(tape, rules, state, position) = 8 | char = get(tape, position); 9 | selectedRule = 0; 10 | rulesMap( 11 | (rule) = if( 12 | (get(rule, 0) == state) + (get(rule, 1) == char) == 2, 13 | () = selectedRule = rule 14 | ), 15 | rules 16 | ); 17 | set(tape, position, get(selectedRule, 3)); 18 | moveHead = get(selectedRule, 4); 19 | branch( 20 | moveHead == 2, 21 | () = position = position + 1, 22 | moveHead == 1, 23 | () = position = position - 1, 24 | ); 25 | mallocfor(get(selectedRule, 2), position) 26 | 27 | 28 | rules = mallocfor( 29 | 1, ord("0"), 1, ord("1"), 2, 30 | 1, ord("1"), 1, ord("0"), 2, 31 | 1, ord("*"), 2, ord("*"), 1 32 | ) 33 | tape = map(ord, "010011001*") 34 | println("tape was: ", strjoin("", map(chr, tape))) 35 | 36 | state = 1 37 | position = 0 38 | while( 39 | () = state != 2, 40 | () = 41 | result = step(tape, rules, state, position); 42 | state = get(result, 0); 43 | position = get(result, 1) 44 | ) 45 | 46 | println("tape became: ", strjoin("", map(chr, tape))) 47 | -------------------------------------------------------------------------------- /pycalc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flrdv/pycalc/2cbd30cb3787f4596ca9a3cd80a35bf3221ea43e/pycalc/__init__.py -------------------------------------------------------------------------------- /pycalc/interpreter/interpret.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from functools import reduce 3 | from abc import ABC, abstractmethod 4 | from typing import Optional, Tuple, Union, List 5 | 6 | from pycalc.lex import tokenizer as _tokenizer 7 | from pycalc.stack import builder 8 | from pycalc.tokentypes.tokens import Token, Tokens, Function 9 | from pycalc.tokentypes.types import (TokenKind, TokenType, Stack, Namespace, Number, 10 | NamespaceValue, ArgumentsError, NameNotFoundError, 11 | InvalidSyntaxError, ExternalFunctionError, 12 | PyCalcError, NoCodeError) 13 | 14 | 15 | Value = Union[Number, Function] 16 | 17 | 18 | class NamespaceStack(Stack[dict]): 19 | def add_namespaces(self, *namespaces: Namespace): 20 | for namespace in namespaces: 21 | self.append(namespace) 22 | 23 | def add_namespace(self, namespace: Namespace): 24 | self.append(namespace) 25 | 26 | def with_add_namespace(self, namespace: Namespace) -> "NamespaceStack": 27 | self.add_namespace(namespace) 28 | return self 29 | 30 | def get(self, var: str) -> NamespaceValue: 31 | for namespace in self[::-1]: 32 | if var in namespace: 33 | return namespace[var] 34 | 35 | raise NameNotFoundError(var, (-1, -1)) 36 | 37 | def set(self, key: str, value: NamespaceValue): 38 | for namespace in self[::-1]: 39 | if key in namespace: 40 | namespace[key] = value 41 | break 42 | else: 43 | self.top[key] = value 44 | 45 | def copy(self) -> "NamespaceStack": 46 | return NamespaceStack(super().copy()) 47 | 48 | def __enter__(self): 49 | pass 50 | 51 | def __exit__(self, exc_type, exc_val, exc_tb): 52 | self.pop() 53 | 54 | 55 | class ABCInterpreter(ABC): 56 | @abstractmethod 57 | def interpret(self, code: str, namespace: Namespace) -> Value: 58 | """ 59 | Receives expression as a string and basic namespace. 60 | This namespace will be in the beginning of the namespaces stack 61 | Returns the last one element in a stack (if multiple left SyntaxError 62 | will be raised) 63 | """ 64 | 65 | 66 | class Interpreter(ABCInterpreter): 67 | unary_executors = { 68 | TokenType.UN_POS: operator.pos, 69 | TokenType.UN_NEG: operator.neg, 70 | } 71 | executors = { 72 | TokenType.OP_ADD: operator.add, 73 | TokenType.OP_SUB: operator.sub, 74 | 75 | TokenType.OP_DIV: operator.truediv, 76 | TokenType.OP_FLOORDIV: operator.floordiv, # it's me! 77 | TokenType.OP_MUL: operator.mul, 78 | TokenType.OP_MOD: operator.mod, 79 | TokenType.OP_LSHIFT: operator.lshift, 80 | TokenType.OP_RSHIFT: operator.rshift, 81 | TokenType.OP_BITWISE_AND: operator.and_, 82 | TokenType.OP_BITWISE_OR: operator.or_, 83 | TokenType.OP_BITWISE_XOR: operator.xor, 84 | 85 | TokenType.OP_DOT: getattr, 86 | TokenType.OP_EQEQ: operator.eq, 87 | TokenType.OP_NOTEQ: operator.ne, 88 | TokenType.OP_GT: operator.gt, 89 | TokenType.OP_GE: operator.ge, 90 | TokenType.OP_LT: operator.lt, 91 | TokenType.OP_LE: operator.le, 92 | 93 | TokenType.OP_POW: operator.pow, 94 | } 95 | 96 | def __init__(self, 97 | tokenize: Optional[_tokenizer.ABCTokenizer] = None, 98 | stackbuilder: Optional[builder.ABCBuilder] = None, 99 | ): 100 | self.tokenizer = tokenize or _tokenizer.Tokenizer() 101 | self.stackbuilder = stackbuilder or builder.SortingStationBuilder() 102 | 103 | def interpret(self, code: str, namespace: Namespace) -> Value: 104 | """ 105 | Currently parses only one-line expressions 106 | """ 107 | 108 | tokens = self.tokenizer.tokenize(code) 109 | stacks = self.stackbuilder.build(tokens) 110 | namespaces = NamespaceStack() 111 | # empty namespace especially for global namespace 112 | # because default one must not be overridden by 113 | # global namespace of code 114 | namespaces.add_namespaces(namespace, {}) 115 | 116 | return self._interpreter(stacks, namespaces) 117 | 118 | def _interpreter(self, exprs: List[Stack[Token]], namespaces: NamespaceStack) -> Value: 119 | if not exprs: 120 | raise NoCodeError 121 | 122 | return list(map(lambda expr: self._interpret_line(expr, namespaces), exprs))[-1] 123 | 124 | def _interpret_line(self, expression: Stack[Token], namespaces: NamespaceStack) -> Value: 125 | stack: Stack[Token] = Stack() 126 | 127 | for i, token in enumerate(expression): 128 | if token.kind in (TokenKind.NUMBER, TokenKind.STRING) \ 129 | or token.type == TokenType.IDENTIFIER: 130 | stack.append(token) 131 | elif token.type == TokenType.VAR: 132 | try: 133 | stack.append(self._token(namespaces.get(token.value), token.pos)) 134 | except NameNotFoundError as exc: 135 | raise NameNotFoundError(str(exc), token.pos) from None 136 | 137 | elif token.kind == TokenKind.UNARY_OPERATOR: 138 | stack.append(self._token( 139 | self.unary_executors[token.type](stack.pop().value), # noqa 140 | token.pos 141 | )) 142 | 143 | elif token.type == TokenType.OP_SEMICOLON: 144 | if len(stack) > 1: 145 | raise SyntaxError("multiple values left in stack") 146 | 147 | stack.pop() 148 | elif token.type == TokenType.OP_EQ: 149 | right, left = stack.pop(), stack.pop() 150 | namespaces.set(left.value, right.value) 151 | stack.append(right) 152 | 153 | elif token.kind == TokenKind.OPERATOR: 154 | right, left = stack.pop(), stack.pop() 155 | stack.append(self._token( 156 | self.executors[token.type](left.value, right.value), 157 | token.pos 158 | )) 159 | elif token.type == TokenType.FUNCCALL: 160 | try: 161 | func = namespaces.get(token.value.name) 162 | except NameNotFoundError as exc: 163 | raise NameNotFoundError(str(exc), token.pos) from None 164 | 165 | stack, args = self._get_func_args(token.value.argscount, stack) 166 | 167 | try: 168 | call_result = func(*(arg.value for arg in args)) 169 | except ArgumentsError as exc: 170 | raise ArgumentsError(str(exc), token.pos) from None 171 | except PyCalcError as exc: 172 | raise exc from None 173 | except Exception as exc: 174 | raise ExternalFunctionError(str(exc), token.pos) 175 | 176 | stack.append(self._token(call_result, token.pos)) 177 | elif token.type == TokenType.FUNCDEF: 178 | func = self._spawn_function( 179 | namespace=namespaces.copy(), 180 | name=token.value.name, 181 | fargs=[tok.value for tok in token.value.args], 182 | body=token.value.body 183 | ) 184 | 185 | if token.value.name: 186 | # lambdas have no name, so their token.value.name 187 | # is just an empty string 188 | namespaces.set(token.value.name, func) 189 | 190 | stack.append(Token( 191 | kind=TokenKind.FUNC, 192 | typeof=TokenType.FUNC, 193 | value=func, 194 | pos=token.pos 195 | )) 196 | else: 197 | raise InvalidSyntaxError( 198 | f"unknown token: {token.type.name}({token.value})", 199 | token.pos 200 | ) 201 | 202 | result = stack.pop() 203 | 204 | if stack: 205 | raise InvalidSyntaxError("multiple values left in stack", stack[0].pos) 206 | 207 | return result.value 208 | 209 | def _spawn_function(self, 210 | namespace: NamespaceStack, 211 | name: str, 212 | fargs: List[str], 213 | body: Stack[Token]) -> Function: 214 | def real_function(*args) -> Number: 215 | if not fargs and args: 216 | raise ArgumentsError("function takes no arguments", (-1, -1)) 217 | elif len(fargs) != len(args): 218 | text = ( 219 | "not enough arguments", 220 | "too much arguments" 221 | )[len(fargs) < len(args)] 222 | 223 | raise ArgumentsError( 224 | f"{text}: expected {len(fargs)}, got {len(args)}", 225 | (-1, -1) 226 | ) 227 | 228 | args_namespace = self._get_args_namespace(fargs, args) 229 | 230 | with namespace.with_add_namespace(args_namespace): 231 | return self._interpret_line(body, namespace) 232 | 233 | return Function( 234 | name=f"{name or ''}({','.join(fargs)})", 235 | target=real_function 236 | ) 237 | 238 | @staticmethod 239 | def _token(num: Number, pos: Tuple[int, int]) -> Token: 240 | if isinstance(num, int): 241 | return Token( 242 | kind=TokenKind.NUMBER, 243 | typeof=TokenType.INTEGER, 244 | value=int(num), 245 | pos=pos 246 | ) 247 | elif isinstance(num, float): 248 | return Token( 249 | kind=TokenKind.NUMBER, 250 | typeof=TokenType.FLOAT, 251 | value=num, 252 | pos=pos 253 | ) 254 | else: 255 | return Token( 256 | kind=TokenKind.OTHER, 257 | typeof=TokenType.OTHER, 258 | value=num, 259 | pos=pos 260 | ) 261 | 262 | @staticmethod 263 | def _get_func_args(argscount: int, stack: Stack[Token]) -> Tuple[Stack[Token], Tokens]: 264 | if not argscount: 265 | return stack, [] 266 | 267 | return stack[:-argscount], stack[-argscount:] 268 | 269 | @staticmethod 270 | def _get_args_namespace(fargs, args) -> Namespace: 271 | return dict(zip(fargs, args)) 272 | 273 | @staticmethod 274 | def _merge_namespaces(*namespaces: Namespace): 275 | return reduce(lambda a, b: {**a, **b}, namespaces) 276 | -------------------------------------------------------------------------------- /pycalc/lex/__init__.py: -------------------------------------------------------------------------------- 1 | from . import tokenizer 2 | -------------------------------------------------------------------------------- /pycalc/lex/tokenizer.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import string 3 | from functools import reduce 4 | from abc import ABC, abstractmethod 5 | from typing import List, Iterator, Tuple, Callable 6 | 7 | from pycalc.tokentypes.tokens import Lexeme, Lexemes, Token, Tokens, FuncDef 8 | from pycalc.tokentypes.types import (LexemeType, TokenType, TokenKind, OPERATORS_TABLE, 9 | OPERATORS_CHARS, UNARY_OPERATORS, Stack, 10 | InvalidSyntaxError) 11 | 12 | 13 | class _LexerState(enum.IntEnum): 14 | ANY = 1 15 | OPERATOR = 2 16 | NOT_OPERATOR = 3 17 | STRING = 4 18 | STRING_BACKSLASH = 5 19 | 20 | 21 | class _ParserState(enum.IntEnum): 22 | ARG = 1 23 | ARG_COMMA = 3 24 | EQ = 5 25 | FUNCNAME = 6 26 | OTHER = 7 27 | 28 | 29 | class ABCTokenizer(ABC): 30 | @abstractmethod 31 | def tokenize(self, data: str) -> Tokens: 32 | """ 33 | Tokenizer is 2 in 1: lexer+parser. Lexer is just a generator 34 | that yields lexemes (strings that are single language piece). 35 | Parser parses unary, marks identifiers (for example, variable 36 | names), function calls (counts arguments function call provides) 37 | and function defines (creates FuncDef token with all the args 38 | and name function takes; also OP_EQ is ignored in this case) 39 | """ 40 | 41 | 42 | def tokenize(data: str) -> List[Tokens]: 43 | return Tokenizer().tokenize(data) 44 | 45 | 46 | class Tokenizer(ABCTokenizer): 47 | def tokenize(self, data: str) -> List[Tokens]: 48 | if not data: 49 | return [] 50 | 51 | lexemes: Lexemes = [] 52 | lineno = 0 53 | 54 | for element, is_op, pos in self._lex(data): 55 | if is_op: 56 | op, unaries = self._parse_ops(element, lineno, pos) 57 | lexemes.append(op) 58 | lexemes.extend(unaries) 59 | else: 60 | if element == "\n": 61 | lineno += 1 62 | 63 | lexemes.append(self._parse_lexeme(element, lineno, pos)) 64 | 65 | output: List[Tokens] = [] 66 | 67 | for line in self._split_lines(lexemes): 68 | unary = self._parse_unary(list(map( 69 | self._lexeme2token, line 70 | ))) 71 | output.append(self._mark_identifiers(unary)) 72 | 73 | return output 74 | 75 | def _lex(self, data: str) -> Iterator[Tuple[str, bool, int]]: 76 | buff: List[str] = [] 77 | state = _LexerState.ANY 78 | pos = 0 79 | lineno = 0 80 | 81 | for i, char in enumerate(data): 82 | char_state = self._get_lexer_state_for_char(char) 83 | 84 | if state == _LexerState.ANY: 85 | state = char_state 86 | 87 | if state == _LexerState.STRING: 88 | if char == "\"" and buff: 89 | buff.append(char) 90 | yield "".join(buff), False, pos-len(buff)+1 91 | buff.clear() 92 | state = _LexerState.ANY 93 | elif char == "\\": 94 | state = _LexerState.STRING_BACKSLASH 95 | buff.append(char) 96 | else: 97 | buff.append(char) 98 | elif state == _LexerState.STRING_BACKSLASH: 99 | buff.append(char) 100 | state = _LexerState.STRING 101 | elif char == " ": 102 | if buff: 103 | yield "".join(buff), state == _LexerState.OPERATOR, pos 104 | buff.clear() 105 | elif char == "\n": 106 | if buff: 107 | yield "".join(buff), state == _LexerState.OPERATOR, pos 108 | buff.clear() 109 | 110 | state = _LexerState.ANY 111 | pos = 0 112 | lineno += 1 113 | yield "\n", False, pos 114 | elif char in "()": 115 | if buff: 116 | yield "".join(buff), state == _LexerState.OPERATOR, pos-len(buff) 117 | buff.clear() 118 | 119 | yield char, False, pos 120 | elif char == ".": 121 | if i == len(data)-1: 122 | raise InvalidSyntaxError( 123 | "unexpected dot in the end of the expression", 124 | (lineno, i) 125 | ) 126 | 127 | if data[i+1] in string.digits: 128 | buff.append(char) 129 | else: 130 | if buff: 131 | yield "".join(buff), state == _LexerState.OPERATOR, i-len(buff) 132 | buff.clear() 133 | 134 | yield char, True, i 135 | 136 | state = _LexerState.NOT_OPERATOR 137 | elif state != char_state: 138 | if buff: 139 | yield "".join(buff), state == _LexerState.OPERATOR, pos 140 | buff.clear() 141 | 142 | buff.append(char) 143 | state = char_state 144 | else: 145 | buff.append(char) 146 | 147 | pos += 1 148 | 149 | if buff: 150 | yield "".join(buff), state == _LexerState.OPERATOR, pos+1 151 | 152 | @staticmethod 153 | def _get_lexer_state_for_char(char: str) -> _LexerState: 154 | if char in OPERATORS_CHARS: 155 | return _LexerState.OPERATOR 156 | elif char == "\"": 157 | return _LexerState.STRING 158 | 159 | return _LexerState.NOT_OPERATOR 160 | 161 | @staticmethod 162 | def _split_lines(lexemes: Lexemes) -> List[Lexemes]: 163 | output: List[Lexemes] = [[]] 164 | parens = 0 165 | 166 | for i, lexeme in enumerate(lexemes): 167 | if lexeme.type == LexemeType.LPAREN: 168 | parens += 1 169 | output[-1].append(lexeme) 170 | elif lexeme.type == LexemeType.RPAREN: 171 | if not parens: 172 | raise InvalidSyntaxError("unexpected closing parenthesis", lexeme.pos) 173 | 174 | parens -= 1 175 | output[-1].append(lexeme) 176 | elif lexeme.type == LexemeType.EOL: 177 | if i > 0 and (lexemes[i-1].type == LexemeType.OPERATOR or parens): 178 | continue 179 | 180 | output.append([]) 181 | else: 182 | output[-1].append(lexeme) 183 | 184 | return list(filter(bool, output)) 185 | 186 | def _parse_unary(self, tokens: Tokens) -> Tokens: 187 | output: Tokens = [] 188 | buffer: Tokens = [] 189 | 190 | if tokens[0].kind == TokenKind.OPERATOR: 191 | for i, token in enumerate(tokens): 192 | if token.kind != TokenKind.OPERATOR: 193 | tokens = tokens[i:] 194 | break 195 | 196 | buffer.append(token) 197 | 198 | unary = self._calculate_final_unary(buffer) 199 | output.append(Token( 200 | kind=TokenKind.UNARY_OPERATOR, 201 | typeof=unary, 202 | value="+" if unary == TokenType.UN_POS else "-", 203 | pos=(tokens[0].pos[0], 0) 204 | )) 205 | buffer.clear() 206 | else: 207 | output.append(tokens[0]) 208 | tokens = tokens[1:] 209 | 210 | for i, token in enumerate(tokens): 211 | if buffer: 212 | if token.kind == TokenKind.OPERATOR: 213 | buffer.append(token) 214 | else: 215 | output.append(buffer[0]) 216 | 217 | if buffer[1:]: 218 | unary = self._calculate_final_unary(buffer[1:]) 219 | output.append(Token( 220 | kind=TokenKind.UNARY_OPERATOR, 221 | typeof=unary, 222 | value="+" if unary == TokenType.UN_POS else "-", 223 | pos=buffer[-1].pos 224 | )) 225 | 226 | buffer.clear() 227 | output.append(token) 228 | elif token.kind == TokenKind.OPERATOR: 229 | buffer.append(token) 230 | else: 231 | output.append(token) 232 | 233 | if buffer: 234 | if len(buffer) == 1 and buffer[0].type == TokenType.OP_SEMICOLON: 235 | output.append(buffer.pop()) 236 | else: 237 | raise InvalidSyntaxError( 238 | "unexpected operator in the end of the expression", 239 | buffer[-1].pos 240 | ) 241 | 242 | return output 243 | 244 | @staticmethod 245 | def _calculate_final_unary(ops: Tokens) -> TokenType: 246 | if not ops: 247 | raise ValueError("_calculate_final_query(): ops are empty") 248 | 249 | subs = 0 250 | 251 | for i, token in enumerate(ops): 252 | if token.value not in UNARY_OPERATORS: 253 | raise InvalidSyntaxError(f"illegal unary: {token.value}", token.pos) 254 | 255 | subs += token.value == '-' 256 | 257 | # hehe, pretty tricky, isn't it? 258 | return TokenType.UN_NEG if subs & 1 else TokenType.UN_POS 259 | 260 | def _parse_ops(self, raw_op: str, lineno: int, pos: int) -> Tuple[Lexeme, Lexemes]: 261 | """ 262 | Splits a string of operators into actual and 263 | unary operators 264 | """ 265 | 266 | op_len = len(max(OPERATORS_TABLE.keys(), key=len)) 267 | 268 | while op_len > 0: 269 | op = raw_op[:op_len] 270 | 271 | if op not in OPERATORS_TABLE: 272 | op_len -= 1 273 | continue 274 | 275 | oper = self._get_op_lexeme(op, lineno, pos) 276 | unaries = map( 277 | lambda op_: self._get_op_lexeme(op_, lineno, pos+op_len), 278 | raw_op[op_len:] 279 | ) 280 | 281 | return oper, list(unaries) 282 | 283 | raise InvalidSyntaxError( 284 | f"illegal operator: {raw_op[0]}", 285 | (lineno, pos) 286 | ) 287 | 288 | def _mark_identifiers(self, tokens: Tokens) -> Tokens: 289 | output = [] 290 | state = _ParserState.OTHER 291 | empty_stack = Stack() 292 | funcdef = FuncDef("", [], empty_stack) 293 | prev_eq_pos = None 294 | 295 | for i, token in enumerate(tokens[1:]): 296 | if token.type == TokenType.VAR and tokens[i].type == TokenType.OP_DOT: 297 | token.type = TokenType.IDENTIFIER 298 | 299 | for i, token in enumerate(tokens[::-1]): 300 | if state == _ParserState.OTHER: 301 | if token.type == TokenType.OP_EQ: 302 | state = _ParserState.EQ 303 | prev_eq_pos = token.pos 304 | else: 305 | output.append(token) 306 | elif state == _ParserState.EQ: 307 | if token.type == TokenType.VAR: 308 | token.type = TokenType.IDENTIFIER 309 | state = _ParserState.OTHER 310 | output.append(Token( 311 | kind=TokenKind.OPERATOR, 312 | typeof=TokenType.OP_EQ, 313 | value="=", 314 | pos=prev_eq_pos, 315 | )) 316 | output.append(token) 317 | elif token.type == TokenType.RPAREN: 318 | state = _ParserState.ARG 319 | else: 320 | raise InvalidSyntaxError( 321 | f"cannot assign to {repr(token.value)}", 322 | token.pos 323 | ) 324 | elif state == _ParserState.ARG: 325 | if token.type == TokenType.OP_COMMA: 326 | raise InvalidSyntaxError("double comma", token.pos) 327 | elif token.type == TokenType.LPAREN: 328 | state = _ParserState.FUNCNAME 329 | continue 330 | elif token.type != TokenType.VAR: 331 | raise InvalidSyntaxError( 332 | f"illegal argument identifier: {repr(token.value)}", 333 | token.pos 334 | ) 335 | 336 | funcdef.args.append(token) 337 | token.type = TokenType.IDENTIFIER 338 | state = _ParserState.ARG_COMMA 339 | elif state == _ParserState.ARG_COMMA: 340 | if token.type == TokenType.LPAREN: 341 | state = _ParserState.FUNCNAME 342 | elif token.type != TokenType.OP_COMMA: 343 | raise InvalidSyntaxError( 344 | f"expected comma, got {repr(token.value)}", 345 | token.pos 346 | ) 347 | else: 348 | state = _ParserState.ARG 349 | elif state == _ParserState.FUNCNAME: 350 | if token.type not in (TokenType.IDENTIFIER, TokenType.VAR): 351 | funcdef.name = "" 352 | 353 | if token.type == TokenType.OP_EQ: 354 | state = _ParserState.EQ 355 | else: 356 | state = _ParserState.OTHER 357 | else: 358 | funcdef.name = token.value 359 | state = _ParserState.OTHER 360 | 361 | line, column = token.pos 362 | column += bool(funcdef.name) 363 | 364 | funcdef.args.reverse() 365 | output.append(Token( 366 | kind=TokenKind.FUNC, 367 | typeof=TokenType.FUNCDEF, 368 | value=funcdef, 369 | pos=(line, column) 370 | )) 371 | 372 | if token.type not in (TokenType.IDENTIFIER, TokenType.VAR, TokenType.OP_EQ): 373 | output.append(token) 374 | 375 | funcdef = FuncDef("", [], empty_stack) 376 | 377 | return self._fill_funcbodies(output[::-1]) 378 | 379 | def _fill_funcbodies(self, tokens: Tokens) -> Tokens: 380 | output: Tokens = [] 381 | bodybuff = [] 382 | lparens = 0 383 | 384 | for token in tokens: 385 | if bodybuff: 386 | if token.type == TokenType.LPAREN: 387 | lparens += 1 388 | bodybuff.append(token) 389 | elif lparens and token.type == TokenType.RPAREN: 390 | lparens -= 1 391 | bodybuff.append(token) 392 | elif not lparens and token.type in (TokenType.OP_COMMA, TokenType.RPAREN): 393 | bodybuff[0].value.body = self._fill_funcbodies(bodybuff[1:]) 394 | output.append(bodybuff[0]) 395 | output.append(token) 396 | bodybuff.clear() 397 | else: 398 | bodybuff.append(token) 399 | else: 400 | if token.type == TokenType.FUNCDEF: 401 | bodybuff.append(token) 402 | else: 403 | output.append(token) 404 | 405 | if bodybuff: 406 | bodybuff[0].value.body = self._fill_funcbodies(bodybuff[1:]) 407 | output.append(bodybuff[0]) 408 | 409 | return output 410 | 411 | @staticmethod 412 | def _get_op_lexeme(op: str, lineno: int, pos: int) -> Lexeme: 413 | return Lexeme( 414 | typeof=LexemeType.OPERATOR, 415 | value=op, 416 | pos=(lineno, pos) 417 | ) 418 | 419 | def _parse_lexeme(self, raw_lexeme: str, lineno: int, pos: int) -> Lexeme: 420 | get_lexeme = self._lexeme_getter(raw_lexeme, lineno, pos) 421 | 422 | if raw_lexeme.startswith("0x"): 423 | if len(raw_lexeme) == 2: 424 | raise InvalidSyntaxError( 425 | "invalid hexdecimal value: 0x", 426 | (lineno, pos) 427 | ) 428 | 429 | return get_lexeme(LexemeType.HEXNUMBER) 430 | elif raw_lexeme[0] in string.digits + ".": 431 | if len(raw_lexeme) == raw_lexeme.count(".") or raw_lexeme.count(".") > 1: 432 | raise InvalidSyntaxError( 433 | f"invalid float: {raw_lexeme}", 434 | (lineno, pos) 435 | ) 436 | 437 | if "." in raw_lexeme: 438 | return get_lexeme(LexemeType.FLOAT) 439 | 440 | return get_lexeme(LexemeType.NUMBER) 441 | elif raw_lexeme == "(": 442 | return get_lexeme(LexemeType.LPAREN) 443 | elif raw_lexeme == ")": 444 | return get_lexeme(LexemeType.RPAREN) 445 | elif raw_lexeme == "\n": 446 | return get_lexeme(LexemeType.EOL) 447 | elif raw_lexeme[0] == raw_lexeme[-1] == "\"": 448 | return get_lexeme(LexemeType.STRING) 449 | 450 | return get_lexeme(LexemeType.LITERAL) 451 | 452 | @staticmethod 453 | def _lexeme_getter(value: str, lineno: int, pos: int) -> Callable[[LexemeType], Lexeme]: 454 | def getter(typeof: LexemeType) -> Lexeme: 455 | return Lexeme( 456 | typeof=typeof, 457 | value=value, 458 | pos=(lineno, pos) 459 | ) 460 | 461 | return getter 462 | 463 | @staticmethod 464 | def _lexeme2token(lexeme: Lexeme) -> Token: 465 | parentheses = { 466 | LexemeType.LPAREN: TokenType.LPAREN, 467 | LexemeType.RPAREN: TokenType.RPAREN 468 | } 469 | 470 | if lexeme.type == LexemeType.NUMBER: 471 | return Token( 472 | kind=TokenKind.NUMBER, 473 | typeof=TokenType.INTEGER, 474 | value=int(lexeme.value), 475 | pos=lexeme.pos 476 | ) 477 | elif lexeme.type == LexemeType.FLOAT: 478 | return Token( 479 | kind=TokenKind.NUMBER, 480 | typeof=TokenType.FLOAT, 481 | value=float(lexeme.value), 482 | pos=lexeme.pos 483 | ) 484 | elif lexeme.type == LexemeType.HEXNUMBER: 485 | return Token( 486 | kind=TokenKind.NUMBER, 487 | typeof=TokenType.INTEGER, 488 | value=int(lexeme.value[2:], 16), 489 | pos=lexeme.pos 490 | ) 491 | elif lexeme.type == LexemeType.LITERAL: 492 | return Token( 493 | kind=TokenKind.LITERAL, 494 | typeof=TokenType.VAR, 495 | value=lexeme.value, 496 | pos=lexeme.pos 497 | ) 498 | elif lexeme.type == LexemeType.OPERATOR: 499 | return Token( 500 | kind=TokenKind.OPERATOR, 501 | typeof=OPERATORS_TABLE[lexeme.value], 502 | value=lexeme.value, 503 | pos=lexeme.pos 504 | ) 505 | elif lexeme.type in parentheses: 506 | return Token( 507 | kind=TokenKind.PAREN, 508 | typeof=parentheses[lexeme.type], 509 | value=lexeme.value, 510 | pos=lexeme.pos 511 | ) 512 | elif lexeme.type == LexemeType.STRING: 513 | return Token( 514 | kind=TokenKind.STRING, 515 | typeof=TokenType.STRING, 516 | value=_prepare_string(lexeme.value[1:-1]), 517 | pos=lexeme.pos 518 | ) 519 | 520 | raise InvalidSyntaxError("unexpected lexeme type: " + lexeme.type.name, lexeme.pos) 521 | 522 | 523 | def _prepare_string(string_val: str) -> str: 524 | replacements = { 525 | "\\\"": "\"", 526 | "\\n": "\n", 527 | "\\r": "\r", 528 | "\\t": "\t", 529 | "\\b": "\b", 530 | "\\f": "\f", 531 | "\\v": "\v", 532 | "\\0": "\0", 533 | "\\\\": "\\" 534 | } 535 | 536 | return reduce(lambda a, b: a.replace(*b), replacements.items(), string_val) 537 | -------------------------------------------------------------------------------- /pycalc/stack/builder.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Iterator, List, Tuple 3 | 4 | from pycalc.tokentypes.tokens import Token, Tokens, Func, FuncDef 5 | from pycalc.tokentypes.types import (PRIORITIES_TABLE, TokenKind, TokenType, 6 | Stack, InvalidSyntaxError, UnknownTokenError) 7 | 8 | 9 | class ABCBuilder(ABC): 10 | @abstractmethod 11 | def build(self, tokens: List[Tokens]) -> List[Stack[Token]]: 12 | """ 13 | Builder receives tokens directly from tokenizer. These tokens 14 | already must be parsed into: 15 | - Identifiers 16 | - Variables 17 | - Unary tokens 18 | - Function calls and defines 19 | """ 20 | 21 | 22 | class SortingStationBuilder(ABCBuilder): 23 | """ 24 | This is a reference implementation of Sorting Station Algorithm 25 | """ 26 | 27 | def build(self, tokens: List[Tokens]) -> List[Stack[Token]]: 28 | return list(map(self._build_line, tokens)) 29 | 30 | def _build_line(self, tokens: Tokens) -> Stack: 31 | output: Stack[Token] = Stack() 32 | divider = self._expr_divider(tokens) 33 | 34 | for expr, semicolon_pos in divider: 35 | stack: Stack[Token] = Stack() 36 | args_counters = self._count_args(expr)[::-1] 37 | 38 | for i, token in enumerate(expr): 39 | if token.kind in (TokenKind.NUMBER, TokenKind.STRING)\ 40 | or token.type == TokenType.IDENTIFIER: 41 | output.append(token) 42 | elif token.type == TokenType.VAR: 43 | if i < len(expr)-1 and expr[i+1].type == TokenType.LPAREN: 44 | # it's a function! 45 | stack.append(self._get_func(token, args_counters.pop())) 46 | else: 47 | output.append(token) 48 | elif token.type == TokenType.FUNCDEF: 49 | output.append(Token( 50 | kind=token.kind, 51 | typeof=token.type, 52 | value=FuncDef( 53 | name=token.value.name, 54 | args=token.value.args, 55 | body=self._build_line(token.value.body) 56 | ), 57 | pos=token.pos 58 | )) 59 | elif token.type == TokenType.OP_COMMA: 60 | if not stack: 61 | raise InvalidSyntaxError( 62 | "missing left parenthesis or comma", 63 | token.pos 64 | ) 65 | 66 | try: 67 | while stack.top.type != TokenType.LPAREN: 68 | output.append(stack.pop()) 69 | except IndexError: 70 | raise InvalidSyntaxError( 71 | "missing left parenthesis or comma", 72 | output[-1].pos 73 | ) from None 74 | elif token.kind in (TokenKind.OPERATOR, TokenKind.UNARY_OPERATOR): 75 | priority = PRIORITIES_TABLE 76 | token_priority = priority[token.type] 77 | 78 | while stack and ( 79 | stack.top.kind in (TokenKind.OPERATOR, TokenKind.UNARY_OPERATOR, TokenKind.FUNC) 80 | and 81 | token_priority <= priority[stack.top.type] 82 | and 83 | stack.top.type != TokenType.OP_POW 84 | ): 85 | output.append(stack.pop()) 86 | 87 | stack.append(token) 88 | elif token.type == TokenType.LPAREN: 89 | stack.append(token) 90 | elif token.type == TokenType.RPAREN: 91 | if not stack: 92 | raise InvalidSyntaxError( 93 | "missing opening parenthesis", 94 | token.pos 95 | ) 96 | 97 | try: 98 | while stack.top.type != TokenType.LPAREN: 99 | output.append(stack.pop()) 100 | except IndexError: 101 | raise InvalidSyntaxError( 102 | "missing opening parenthesis", 103 | output[-1].pos 104 | ) from None 105 | 106 | stack.pop() 107 | 108 | if stack and stack.top.type == TokenType.FUNCNAME: 109 | # it's a function! 110 | output.append(self._get_func(stack.pop(), args_counters.pop())) 111 | else: 112 | raise UnknownTokenError(f"unknown token: {token}", token.pos) 113 | 114 | while stack: 115 | if stack.top.type == TokenType.LPAREN: 116 | raise InvalidSyntaxError("missing closing parenthesis", stack.top.pos) 117 | 118 | output.append(stack.pop()) 119 | 120 | output.append(Token( 121 | kind=TokenKind.OPERATOR, 122 | typeof=TokenType.OP_SEMICOLON, 123 | value=";", 124 | pos=semicolon_pos 125 | )) 126 | 127 | return output[:-1] # remove trailing semicolon 128 | 129 | def _count_args(self, tokens: Tokens) -> List[int]: 130 | result = [] 131 | 132 | for funccall in self.__find_funccalls(tokens): 133 | result.append(0) 134 | waitforcomma = False 135 | parens = 0 136 | 137 | for token in funccall: 138 | if parens: 139 | if token.type == TokenType.LPAREN: 140 | parens += 1 141 | elif token.type == TokenType.RPAREN: 142 | parens -= 1 143 | 144 | continue 145 | elif token.type == TokenType.LPAREN: 146 | parens += 1 147 | result[-1] += not waitforcomma 148 | waitforcomma = True 149 | elif waitforcomma: 150 | waitforcomma = token.type != TokenType.OP_COMMA 151 | else: 152 | result[-1] += 1 153 | waitforcomma = True 154 | 155 | return result 156 | 157 | def __find_funccalls(self, tokens: Tokens) -> List[Tokens]: 158 | funcs: List[Tokens] = [] 159 | parens = 0 160 | 161 | for i, token in enumerate(tokens[1:], start=1): 162 | if parens: 163 | if token.type == TokenType.LPAREN: 164 | parens += 1 165 | elif token.type == TokenType.RPAREN: 166 | parens -= 1 167 | 168 | if not parens: 169 | funcs.extend(self.__find_funccalls(funcs[-1])) 170 | continue 171 | 172 | funcs[-1].append(token) 173 | elif token.type == TokenType.LPAREN and tokens[i - 1].type == TokenType.VAR: 174 | parens = 1 175 | funcs.append([]) 176 | 177 | return funcs 178 | 179 | @staticmethod 180 | def _expr_divider(expr: Tokens) -> Iterator[Tuple[Tokens, Tuple[int, int]]]: 181 | """ 182 | Yields expression and semicolon index 183 | """ 184 | 185 | border = 0 186 | 187 | for i, token in enumerate(expr): 188 | if token.type == TokenType.OP_SEMICOLON: 189 | yield expr[border:i], token.pos 190 | border = i + 1 191 | 192 | # semicolon anyway cannot be in the end of the expression, 193 | # in case it is, error will be raised even before this func 194 | yield expr[border:], -1 195 | 196 | @staticmethod 197 | def _get_func(token: Token, argscount: int) -> Token: 198 | return Token( 199 | kind=TokenKind.FUNC, 200 | typeof=TokenType.FUNCCALL, 201 | value=Func( 202 | name=token.value, 203 | argscount=argscount 204 | ), 205 | pos=token.pos 206 | ) 207 | -------------------------------------------------------------------------------- /pycalc/tokentypes/__init__.py: -------------------------------------------------------------------------------- 1 | from . import tokens, types 2 | -------------------------------------------------------------------------------- /pycalc/tokentypes/tokens.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union, Callable, Tuple 2 | 3 | from . import types 4 | 5 | Lexemes = List["Lexeme"] 6 | Tokens = List["Token"] 7 | TokenValue = Union[int, float, str, "Func", "FuncDef", "Function"] 8 | 9 | 10 | class Lexeme: 11 | """ 12 | A class that contains a raw piece of input stream. 13 | It may be: number, literal, operator, lbrace, rbrace 14 | """ 15 | 16 | def __init__(self, typeof: types.LexemeType, value: str, pos: Tuple[int, int]): 17 | self.type = typeof 18 | self.value = value 19 | self.pos = pos 20 | 21 | def __str__(self): 22 | return f"{self.type.name}({repr(self.value)})" 23 | 24 | __repr__ = __str__ 25 | 26 | 27 | class Token: 28 | def __init__(self, 29 | kind: types.TokenKind, 30 | typeof: types.TokenType, 31 | value: TokenValue, 32 | pos: Tuple[int, int] 33 | ): 34 | self.kind = kind 35 | self.type = typeof 36 | self.value = value 37 | self.pos = pos 38 | 39 | def __str__(self): 40 | return f"{self.kind.name}:{self.type.name}:{self.pos[1]}({repr(self.value)})" 41 | 42 | __repr__ = __str__ 43 | 44 | 45 | class Func: 46 | """ 47 | Func just represents some information about function call 48 | """ 49 | 50 | def __init__(self, name: str, argscount: int): 51 | self.name = name 52 | self.argscount = argscount 53 | 54 | def __str__(self): 55 | return f"Func(name={repr(self.name)}, argscount={self.argscount})" 56 | 57 | __repr__ = __str__ 58 | 59 | 60 | class FuncDef: 61 | """ 62 | FuncDef represents function defining 63 | """ 64 | 65 | def __init__(self, name: str, args: Tokens, body: types.Stack): 66 | self.name = name 67 | self.args = args 68 | self.body = body 69 | 70 | def __str__(self): 71 | return f"FuncDef(name={repr(self.name)}, " \ 72 | f"args=({','.join(arg.value for arg in self.args)}), " \ 73 | f"body={self.body})" 74 | 75 | __repr__ = __str__ 76 | 77 | 78 | class Function: 79 | def __init__(self, name: str, target: Callable): 80 | self.name = name 81 | self.target = target 82 | 83 | @property 84 | def __call__(self): 85 | return self.target 86 | 87 | def __str__(self): 88 | return self.name 89 | 90 | __repr__ = __str__ 91 | -------------------------------------------------------------------------------- /pycalc/tokentypes/types.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from operator import add 3 | from functools import reduce 4 | from string import ascii_letters 5 | from typing import Union, Dict, Callable, Tuple, List, TypeVar 6 | 7 | 8 | Number = Union[int, float] 9 | NamespaceValue = Union[Number, Callable] 10 | Namespace = Dict[str, NamespaceValue] 11 | 12 | UNARY_OPERATORS = {"+", "-"} 13 | ALLOWED_LITERALS = ascii_letters + "_" 14 | 15 | T = TypeVar("T") 16 | 17 | 18 | class Stack(List[T]): 19 | @property 20 | def top(self): 21 | return self[-1] 22 | 23 | 24 | class LexemeType(enum.IntEnum): 25 | UNKNOWN = 0 26 | NUMBER = 1 27 | HEXNUMBER = 2 28 | FLOAT = 3 29 | LITERAL = 4 30 | OPERATOR = 5 31 | LPAREN = 6 # ( 32 | RPAREN = 7 # ) 33 | DOT = 8 34 | COMMA = 9 35 | EOL = 10 36 | STRING = 11 37 | 38 | 39 | class TokenKind(enum.IntEnum): 40 | NUMBER = 0 41 | LITERAL = 1 42 | OPERATOR = 2 43 | UNARY_OPERATOR = 3 44 | PAREN = 4 45 | FUNC = 5 46 | STRING = 6 47 | OTHER = 7 48 | 49 | 50 | class TokenType(enum.IntEnum): 51 | FLOAT = 0 52 | INTEGER = 1 53 | OP_EQ = 2 54 | OP_EQEQ = 3 55 | OP_NOTEQ = 4 56 | OP_ADD = 5 57 | OP_SUB = 6 58 | OP_DIV = 7 59 | OP_MUL = 8 60 | OP_POW = 9 61 | OP_LSHIFT = 10 62 | OP_RSHIFT = 11 63 | OP_BITWISE_AND = 12 64 | OP_BITWISE_OR = 13 65 | OP_BITWISE_XOR = 14 66 | OP_MOD = 15 67 | OP_FLOORDIV = 16 68 | OP_SEMICOLON = 17 69 | OP_COMMA = 18 70 | OP_GT = 19 71 | OP_GE = 20 72 | OP_LT = 21 73 | OP_LE = 22 74 | OP_DOT = 35 75 | UN_POS = 23 76 | UN_NEG = 24 77 | LPAREN = 25 78 | RPAREN = 26 79 | VAR = 27 80 | IDENTIFIER = 28 81 | FUNCCALL = 29 82 | FUNCDEF = 30 83 | FUNCNAME = 31 84 | FUNC = 32 85 | STRING = 33 86 | OTHER = 34 87 | 88 | 89 | OPERATORS_TABLE = { 90 | "+": TokenType.OP_ADD, 91 | "-": TokenType.OP_SUB, 92 | "/": TokenType.OP_DIV, 93 | "//": TokenType.OP_FLOORDIV, 94 | "*": TokenType.OP_MUL, 95 | "**": TokenType.OP_POW, 96 | "%": TokenType.OP_MOD, 97 | "<<": TokenType.OP_LSHIFT, 98 | ">>": TokenType.OP_RSHIFT, 99 | "&": TokenType.OP_BITWISE_AND, 100 | "|": TokenType.OP_BITWISE_OR, 101 | "^": TokenType.OP_BITWISE_XOR, 102 | 103 | "==": TokenType.OP_EQEQ, 104 | "!=": TokenType.OP_NOTEQ, 105 | ">": TokenType.OP_GT, 106 | ">=": TokenType.OP_GE, 107 | "<": TokenType.OP_LT, 108 | "<=": TokenType.OP_LE, 109 | 110 | ".": TokenType.OP_DOT, 111 | 112 | ";": TokenType.OP_SEMICOLON, 113 | "=": TokenType.OP_EQ, 114 | ",": TokenType.OP_COMMA 115 | } 116 | 117 | OPERATORS_CHARS = set(reduce(add, OPERATORS_TABLE.keys())) 118 | 119 | 120 | class Priorities(enum.IntEnum): 121 | NONE = 0 122 | MINIMAL = 1 123 | MEDIUM = 2 124 | HIGH = 3 125 | MAXIMAL = 4 126 | 127 | 128 | PRIORITIES_TABLE = { 129 | TokenType.OP_ADD: Priorities.MINIMAL, 130 | TokenType.OP_SUB: Priorities.MINIMAL, 131 | 132 | TokenType.OP_DIV: Priorities.MEDIUM, 133 | TokenType.OP_FLOORDIV: Priorities.MEDIUM, 134 | TokenType.OP_MUL: Priorities.MEDIUM, 135 | TokenType.OP_MOD: Priorities.MEDIUM, 136 | TokenType.OP_LSHIFT: Priorities.MEDIUM, 137 | TokenType.OP_RSHIFT: Priorities.MEDIUM, 138 | TokenType.OP_BITWISE_AND: Priorities.MEDIUM, 139 | TokenType.OP_BITWISE_OR: Priorities.MEDIUM, 140 | TokenType.OP_BITWISE_XOR: Priorities.MEDIUM, 141 | 142 | TokenType.UN_POS: Priorities.HIGH, 143 | TokenType.UN_NEG: Priorities.HIGH, 144 | 145 | TokenType.OP_POW: Priorities.MAXIMAL, 146 | TokenType.FUNCCALL: Priorities.MAXIMAL, 147 | TokenType.FUNCDEF: Priorities.MAXIMAL, 148 | TokenType.OP_DOT: Priorities.MAXIMAL, 149 | 150 | TokenType.OP_EQ: Priorities.NONE, 151 | TokenType.OP_EQEQ: Priorities.NONE, 152 | TokenType.OP_NOTEQ: Priorities.NONE, 153 | TokenType.OP_GT: Priorities.NONE, 154 | TokenType.OP_GE: Priorities.NONE, 155 | TokenType.OP_LT: Priorities.NONE, 156 | TokenType.OP_LE: Priorities.NONE, 157 | TokenType.OP_COMMA: Priorities.NONE, 158 | TokenType.OP_SEMICOLON: Priorities.NONE, 159 | } 160 | 161 | 162 | class PyCalcError(Exception): 163 | def __init__(self, message: str, pos: Tuple[int, int]): 164 | self.pos = pos 165 | super().__init__(message) 166 | 167 | 168 | class InvalidSyntaxError(PyCalcError): 169 | pass 170 | 171 | 172 | class ArgumentsError(PyCalcError): 173 | pass 174 | 175 | 176 | class NameNotFoundError(PyCalcError): 177 | pass 178 | 179 | 180 | class UnknownTokenError(PyCalcError): 181 | pass 182 | 183 | 184 | class ExternalFunctionError(PyCalcError): 185 | pass 186 | 187 | 188 | class NoCodeError(Exception): 189 | pass 190 | -------------------------------------------------------------------------------- /repl.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from sys import argv, stdin as _stdin, stdout as _stdout 3 | 4 | from std.stdlibrary import stdnamespace 5 | from pycalc.interpreter import interpret 6 | from pycalc.tokentypes.types import PyCalcError, NoCodeError 7 | 8 | PROMPT = ">> " 9 | 10 | 11 | def _format_exc( 12 | code: str, 13 | exc: PyCalcError, 14 | file: str = "") -> str: 15 | lineno, pos = exc.pos 16 | line = code.split("\n")[lineno] 17 | 18 | return f"{line}\n" + \ 19 | " " * pos + "^\n" \ 20 | f"{file}:{lineno+1}:{pos+1}: " \ 21 | f"{exc.__class__.__name__}: {exc}" 22 | 23 | 24 | class InteractiveShell: 25 | def __init__(self, 26 | prompt: str = PROMPT, 27 | interpreter: Optional[interpret.ABCInterpreter] = None 28 | ): 29 | self.prompt = prompt 30 | self.interpreter = interpreter or interpret.Interpreter() 31 | 32 | def session(self, stdin=_stdin, stdout=_stdout): 33 | while True: 34 | print(end=self.prompt, file=stdout) 35 | 36 | try: 37 | expression = stdin.readline().strip() 38 | except KeyboardInterrupt: 39 | return 40 | 41 | if not expression: 42 | continue 43 | 44 | try: 45 | print(self.interpreter.interpret(expression, stdnamespace), 46 | file=stdout) 47 | except PyCalcError as exc: 48 | print(_format_exc(expression, exc, file=""), 49 | file=stdout) 50 | except Exception as exc: 51 | print(f":1:?: internal interpreter error: {exc.__class__.__name__}: {exc}", 52 | file=stdout) 53 | 54 | 55 | def interactive_mode(): 56 | interpreter = interpret.Interpreter() 57 | shell = InteractiveShell( 58 | prompt=PROMPT, 59 | interpreter=interpreter 60 | ) 61 | shell.session() 62 | 63 | 64 | def expr_exec_mode(expr: str): 65 | try: 66 | print(interpret.Interpreter().interpret(expr, stdnamespace)) 67 | except PyCalcError as exc: 68 | print(_format_exc(expr, exc, file="")) 69 | except NoCodeError: 70 | pass 71 | except Exception as exc: 72 | print(f":1:?: internal interpreter error: {exc.__class__.__name__}({repr(exc)})") 73 | 74 | 75 | def script_exec_mode(filename: str): 76 | if not filename.endswith(".calc"): 77 | print("unsupported file extension:", filename) 78 | return 79 | 80 | try: 81 | with open(filename) as fd: 82 | code = fd.read() 83 | except FileNotFoundError: 84 | print("file not found:", filename) 85 | return 86 | 87 | interpreter = interpret.Interpreter() 88 | 89 | try: 90 | interpreter.interpret(code, stdnamespace) 91 | except PyCalcError as exc: 92 | print(_format_exc(code, exc, file=fd.name)) 93 | except NoCodeError: 94 | pass 95 | except Exception as exc: 96 | print(f"{fd.name}:?:?: internal interpreter error:") 97 | raise exc 98 | 99 | 100 | if __name__ == '__main__': 101 | options = { 102 | "-e": expr_exec_mode, 103 | "--execute": expr_exec_mode, 104 | "-s": script_exec_mode, 105 | "--script": script_exec_mode, 106 | } 107 | 108 | if len(argv) > 1 and (argv[1] not in options or len(argv) != 3): 109 | print("Invalid options:", " ".join(argv[1:])) 110 | print("Available options:") 111 | print("\t-e, --execute : execute expression right from a command line") 112 | print("\t-s, --script .calc: execute program from a file") 113 | elif len(argv) == 3: 114 | option, value = argv[1:] 115 | options[option](value) 116 | else: 117 | interactive_mode() 118 | -------------------------------------------------------------------------------- /std/__init__.py: -------------------------------------------------------------------------------- 1 | from . import stdlibrary 2 | -------------------------------------------------------------------------------- /std/stdio.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | def print_(*values) -> int: 5 | print(*values, sep="", end="") 6 | 7 | return 0 8 | 9 | 10 | def println_(*values) -> int: 11 | print(*values, sep="", end="\n") 12 | 13 | return 0 14 | 15 | 16 | def print_mem(mem: List) -> int: 17 | print(*mem, sep="", end="") 18 | 19 | return 0 20 | 21 | 22 | def println_mem(mem: List) -> int: 23 | print(*mem, sep="", end="\n") 24 | 25 | return 0 26 | -------------------------------------------------------------------------------- /std/stdlibrary.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from functools import reduce 3 | from typing import Callable, Iterable 4 | 5 | from . import stdmem, stdstatements, stdio 6 | 7 | 8 | def _as_list(func: Callable) -> Callable[[Callable, Iterable], list]: 9 | def decorator(a: Callable, b: Iterable) -> list: 10 | return list(func(a, b)) 11 | 12 | return decorator 13 | 14 | 15 | stdnamespace = { 16 | "rt": lambda a, b: a ** (1/b), 17 | "sqrt": lambda a: a ** (1/2), 18 | "cbrt": lambda a: a ** (1/3), 19 | "int": int, 20 | "float": float, 21 | "str": str, 22 | "strjoin": str.join, 23 | "range": range, 24 | "inv": lambda a: ~a, 25 | "pi": pi, 26 | 27 | "write": lambda target, value: target.write(value), 28 | "print": stdio.print_, 29 | "println": stdio.println_, 30 | "input": input, 31 | "chr": chr, 32 | "ord": ord, 33 | 34 | "malloc": stdmem.mem_alloc, 35 | "mallocfor": stdmem.mem_allocfor, 36 | "get": stdmem.mem_get, 37 | "set": stdmem.mem_set, 38 | "slice": stdmem.slice_, 39 | "len": len, 40 | 41 | "map": _as_list(map), 42 | "filter": _as_list(filter), 43 | "reduce": reduce, 44 | "while": stdstatements.while_, 45 | "if": stdstatements.if_else, 46 | "branch": stdstatements.branch, 47 | 48 | "nop": lambda: 0, 49 | "call": lambda func: func(), 50 | } 51 | -------------------------------------------------------------------------------- /std/stdmem.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | def mem_alloc(size: int) -> List[int]: 5 | return [0] * size 6 | 7 | 8 | def mem_allocfor(*values: int) -> List[int]: 9 | return list(values) 10 | 11 | 12 | def mem_get(mem: List[int], offset: int) -> int: 13 | if 0 > offset >= len(mem): 14 | return -1 15 | 16 | return mem[offset] 17 | 18 | 19 | def mem_set(mem: List[int], offset: int, value: int) -> int: 20 | if 0 > offset or offset >= len(mem) or 0 > value > 255: 21 | return -1 22 | 23 | mem[offset] = value 24 | return 0 25 | 26 | 27 | def slice_(mem: List[int], begin: int, end: int) -> List[int]: 28 | return mem[begin:end] 29 | -------------------------------------------------------------------------------- /std/stdstatements.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | from typing import Callable, Optional, Union 3 | 4 | from pycalc.tokentypes.types import Number, ArgumentsError 5 | 6 | 7 | def if_else( 8 | condition: Number, 9 | if_cb: Callable, 10 | else_cb: Optional[Callable] = None) -> Number: 11 | if else_cb is None: 12 | return _if(condition, if_cb) 13 | 14 | return if_cb() if condition else else_cb() 15 | 16 | 17 | def _if(condition: Number, cb: Callable) -> int: 18 | return cb() if condition else 0 19 | 20 | 21 | def while_(condition: Callable, body: Callable) -> int: 22 | while condition(): 23 | body() 24 | 25 | return 0 26 | 27 | 28 | def branch(*values: Union[Number, Callable]) -> int: 29 | """ 30 | This is also kind of if, but a bit better 31 | """ 32 | 33 | if len(values) < 2 or callable(values[0]): 34 | raise ArgumentsError("invalid arguments") 35 | 36 | pairs = zip( 37 | islice(values, None, None, 2), 38 | islice(values, 1, None, 2) 39 | ) 40 | 41 | for cond, callback in pairs: 42 | if cond: 43 | return callback() 44 | 45 | if len(values) % 2: 46 | return values[-1]() 47 | 48 | return 0 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest import TestSuite 2 | 3 | from .testcases import evaluation_tests 4 | 5 | 6 | full_suite = TestSuite() 7 | full_suite.addTest(evaluation_tests) 8 | -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from . import full_suite 4 | 5 | 6 | if __name__ == "__main__": 7 | runner = unittest.TextTestRunner(verbosity=2) 8 | runner.run(full_suite) 9 | -------------------------------------------------------------------------------- /tests/testcases.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | from unittest import TestCase, TestSuite, makeSuite 3 | 4 | from std.stdlibrary import stdnamespace 5 | from pycalc.tokentypes.tokens import Function 6 | from pycalc.interpreter.interpret import Interpreter 7 | from pycalc.tokentypes.types import InvalidSyntaxError 8 | 9 | 10 | interpreter = Interpreter() 11 | evaluate = lambda code: interpreter.interpret(code, stdnamespace) 12 | 13 | 14 | class TestNumbers(TestCase): 15 | def test_integer(self): 16 | self.assertEqual(evaluate("100"), 100) 17 | 18 | def test_float(self): 19 | self.assertEqual(evaluate("0.1"), 0.1) 20 | self.assertEqual(evaluate(".1"), 0.1) 21 | 22 | def test_hexdecimal(self): 23 | self.assertEqual(evaluate("0x175ffa14"), 0x175ffa14) 24 | 25 | 26 | class TestBasicOperations(TestCase): 27 | def test_addition(self): 28 | self.assertEqual(evaluate("1+1"), 2) 29 | 30 | def test_subtraction(self): 31 | self.assertEqual(evaluate("1-1"), 0) 32 | 33 | def test_multiplication(self): 34 | self.assertEqual(evaluate("1*1"), 1) 35 | 36 | def test_division(self): 37 | self.assertEqual(evaluate("1/2"), .5) 38 | 39 | def test_floordivision(self): 40 | self.assertEqual(evaluate("3//2"), 1) 41 | 42 | def test_modulo(self): 43 | self.assertEqual(evaluate("7%2"), 1) 44 | 45 | def test_lshift(self): 46 | self.assertEqual(evaluate("1<<5"), 32) 47 | 48 | def test_rshift(self): 49 | self.assertEqual(evaluate("128>>5"), 4) 50 | 51 | def test_bitwise_and(self): 52 | self.assertEqual(evaluate("32 & 64"), 0) 53 | 54 | def test_bitwise_or(self): 55 | self.assertEqual(evaluate("81 | 82"), 83) 56 | 57 | def test_bitwise_xor(self): 58 | self.assertEqual(evaluate("54^87"), 97) 59 | 60 | def test_exponentiation(self): 61 | self.assertEqual(evaluate("2**3"), 8) 62 | 63 | def test_unary_addition(self): 64 | self.assertEqual(evaluate("+1"), 1) 65 | 66 | def test_unary_subtraction(self): 67 | self.assertEqual(evaluate("-1"), -1) 68 | 69 | def test_unary_subtraction_multiple(self): 70 | self.assertEqual(evaluate("--1"), 1) 71 | self.assertEqual(evaluate("---1"), -1) 72 | 73 | def test_equality(self): 74 | self.assertEqual(evaluate("2==2"), 1) 75 | self.assertEqual(evaluate("2!=2"), 0) 76 | 77 | def test_less_than(self): 78 | self.assertEqual(evaluate("1<2"), 1) 79 | self.assertEqual(evaluate("2<1"), 0) 80 | 81 | def test_less_equal(self): 82 | self.assertEqual(evaluate("2<=3"), 1) 83 | self.assertEqual(evaluate("2<=2"), 1) 84 | self.assertEqual(evaluate("2<=1"), 0) 85 | 86 | def test_more_than(self): 87 | self.assertEqual(evaluate("2>1"), 1) 88 | self.assertEqual(evaluate("1>2"), 0) 89 | 90 | def test_more_equal(self): 91 | self.assertEqual(evaluate("2>=1"), 1) 92 | self.assertEqual(evaluate("2>=2"), 1) 93 | self.assertEqual(evaluate("2>=3"), 0) 94 | 95 | 96 | class TestOperatorsPriority(TestCase): 97 | def test_addition_multiplication(self): 98 | self.assertEqual(evaluate("2+2*2"), 6) 99 | 100 | def test_addition_division(self): 101 | self.assertEqual(evaluate("2+2/2"), 3) 102 | 103 | def test_addition_exponentiation(self): 104 | self.assertEqual(evaluate("1+2**3"), 9) 105 | 106 | def test_subtraction_addition(self): 107 | self.assertEqual(evaluate("1-2+3"), 2) 108 | 109 | def test_subtraction_subtraction(self): 110 | self.assertEqual(evaluate("1-2-3"), -4) 111 | 112 | def test_subtraction_multiplication(self): 113 | self.assertEqual(evaluate("2-2*2"), -2) 114 | 115 | def test_subtraction_division(self): 116 | self.assertEqual(evaluate("2-2/2"), 1) 117 | 118 | def test_subtraction_exponentiation(self): 119 | self.assertEqual(evaluate("1-2**3"), -7) 120 | 121 | def test_multiplicaion_exponentiation(self): 122 | self.assertEqual(evaluate("2*10**2"), 200) 123 | 124 | def test_division_exponentiation(self): 125 | self.assertEqual(evaluate("1/10**2"), 0.01) 126 | 127 | def test_exponentiation_right_associativity(self): 128 | self.assertEqual(evaluate("2**3**2"), 512) 129 | 130 | def test_exponentiation_unary_subtraction(self): 131 | self.assertEqual(evaluate("2**-3"), 0.125) 132 | 133 | def test_unary_subtraction_exponentiation(self): 134 | self.assertEqual(evaluate("-2**2"), -4) 135 | 136 | 137 | class TestVariables(TestCase): 138 | def test_get_pi(self): 139 | self.assertEqual(evaluate("pi"), pi) 140 | 141 | def test_negotate_pi(self): 142 | self.assertEqual(evaluate("-pi"), -pi) 143 | 144 | def test_expression_with_constant(self): 145 | self.assertEqual(evaluate("pi+2.0-3"), pi + 2 - 3) 146 | self.assertEqual(evaluate("2.0+pi-3"), 2 + pi - 3) 147 | self.assertEqual(evaluate("2.0-3+pi"), 2 - 3 + pi) 148 | 149 | def test_declare_var(self): 150 | self.assertEqual(evaluate("a=5+5"), 10) 151 | 152 | def test_get_declared_var(self): 153 | self.assertEqual(evaluate("a=10 \n a"), 10) 154 | 155 | 156 | class TestFunctions(TestCase): 157 | def test_funccall(self): 158 | self.assertEqual(evaluate("rt(25, 2)"), 5) 159 | 160 | def test_nested_funccall(self): 161 | self.assertEqual(evaluate("rt(rt(625, 2), 2)"), 5) 162 | 163 | def test_expr_in_funccall(self): 164 | self.assertEqual(evaluate("rt(20+5, 1.0+1.0)"), 5) 165 | 166 | def test_funcdef(self): 167 | func_a = evaluate("a()=5") 168 | self.assertIsInstance(func_a, Function) 169 | self.assertEqual(func_a.name, "a()") 170 | 171 | func_b = evaluate("b(x)=x+1") 172 | self.assertIsInstance(func_b, Function) 173 | self.assertEqual(func_b.name, "b(x)") 174 | 175 | func_c = evaluate("c(x,y)=x*y") 176 | self.assertIsInstance(func_c, Function) 177 | self.assertEqual(func_c.name, "c(x,y)") 178 | 179 | def test_def_func_call(self): 180 | self.assertEqual(evaluate("f(x,y)=x*y \n f(2,5)"), 10) 181 | 182 | def test_def_func_argexpr(self): 183 | self.assertEqual(evaluate("f(x,y)=x*y \n f(2+5, 3*2)"), 42) 184 | 185 | def test_funcdef_argexpr(self): 186 | with self.assertRaises(InvalidSyntaxError): 187 | evaluate("f(x+1)=x+2") 188 | 189 | with self.assertRaises(InvalidSyntaxError): 190 | evaluate("f(1)=2") 191 | 192 | def test_funcdef_missed_brace(self): 193 | with self.assertRaises(InvalidSyntaxError): 194 | evaluate("f(x=2") 195 | 196 | with self.assertRaises(InvalidSyntaxError): 197 | evaluate("fx)=2") 198 | 199 | def test_funcdef_no_body(self): 200 | with self.assertRaises(InvalidSyntaxError): 201 | evaluate("f(x)=") 202 | 203 | 204 | class TestLambdas(TestCase): 205 | def test_assign_to_var(self): 206 | self.assertEqual(evaluate("a=(x)=x+1 \n a(1)"), 2) 207 | 208 | def test_lambda_as_argument(self): 209 | self.assertEqual(evaluate(""" 210 | sum(mem)=reduce((x,y)=x+y, mem) 211 | range(begin, end) = i=begin-1; map((x)=i=i+1;x+i, malloc(end-begin)) 212 | sum(range(0,5)) 213 | """), 10) 214 | 215 | def test_missing_brace_in_arglambda(self): 216 | with self.assertRaises(InvalidSyntaxError): 217 | evaluate("sum(mem)=reduce(x,y)=x+y, mem)") 218 | 219 | with self.assertRaises(InvalidSyntaxError): 220 | evaluate("sum(mem)=reduce((x,y=x+y, mem)") 221 | 222 | def test_missing_brace_in_vardecl_lambda(self): 223 | with self.assertRaises(InvalidSyntaxError): 224 | evaluate("a=(x=x+1") 225 | 226 | with self.assertRaises(InvalidSyntaxError): 227 | evaluate("a=x)=x+1") 228 | 229 | 230 | evaluation_tests = TestSuite() 231 | evaluation_tests.addTest(makeSuite(TestNumbers)) 232 | evaluation_tests.addTest(makeSuite(TestBasicOperations)) 233 | evaluation_tests.addTest(makeSuite(TestOperatorsPriority)) 234 | evaluation_tests.addTest(makeSuite(TestVariables)) 235 | evaluation_tests.addTest(makeSuite(TestFunctions)) 236 | evaluation_tests.addTest(makeSuite(TestLambdas)) 237 | --------------------------------------------------------------------------------