├── .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 | [](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 |
--------------------------------------------------------------------------------