├── lint.sh
├── check.sh
├── requirements.txt
├── example.txt
├── compile.sh
├── run_tests.sh
├── fib.nd
├── benchmark.py
├── .github
└── workflows
│ └── python-app.yml
├── cli.py
├── grammar_static.py
├── wasm
└── wat2wasm.js
├── grammar.py
├── .gitignore
├── README.md
├── tests.py
├── compiler.py
└── interpreter.py
/lint.sh:
--------------------------------------------------------------------------------
1 | python3 -m black .
2 |
--------------------------------------------------------------------------------
/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python3 -m mypy .
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | lark==1.1.7
2 | mypy==1.4.1
3 |
--------------------------------------------------------------------------------
/example.txt:
--------------------------------------------------------------------------------
1 | # this file is used for integration tests e.g. read()
2 |
--------------------------------------------------------------------------------
/compile.sh:
--------------------------------------------------------------------------------
1 | # compile and run the test program
2 | python compiler.py | node wasm/wat2wasm.js
3 |
--------------------------------------------------------------------------------
/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | echo python3 --version
5 | pip3 install -r requirements.txt
6 | # TODO fix types
7 | # python3 -m mypy cli.py interpreter.py grammar.py
8 | python3 tests.py
9 |
--------------------------------------------------------------------------------
/fib.nd:
--------------------------------------------------------------------------------
1 | for (i = 0; i < 21; i = i + 1)
2 | # recursive (slow)
3 | fun fib(x)
4 | if (x == 0 or x == 1)
5 | return x;
6 | fi
7 | return fib(x - 1) + fib(x - 2);
8 | nuf
9 | log(fib(i));
10 | rof
11 |
--------------------------------------------------------------------------------
/benchmark.py:
--------------------------------------------------------------------------------
1 | from interpreter import interpret
2 |
3 | program = """
4 | for (i = 0; i < 20; i = i + 1)
5 | # gets called 35400 times
6 | fun fib(x)
7 | if (x == 0 or x == 1)
8 | return x;
9 | fi
10 | return fib(x - 1) + fib(x - 2);
11 | nuf
12 | fib(i);
13 | rof
14 | """
15 |
16 | interpret(program, opts={"debug": False, "profile": True})
17 |
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Set up Python 3.10
20 | uses: actions/setup-python@v3
21 | with:
22 | python-version: "3.10"
23 | - name: Run tests
24 | run: ./run_tests.sh
25 |
--------------------------------------------------------------------------------
/cli.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import readline
3 | from interpreter import interpret, eval_program, get_context, get_root
4 |
5 |
6 | def repl():
7 | readline.parse_and_bind('"\e[A": history-search-backward')
8 | readline.parse_and_bind('"\e[B": history-search-forward')
9 |
10 | lines = []
11 | prompt = "> "
12 |
13 | root_context = get_context({"debug": False, "profile": False})
14 |
15 | while True:
16 | try:
17 | line = input(prompt)
18 | lines.append(line)
19 | inner_value = eval_program(get_root(line), context=root_context).value
20 | print(f"{inner_value if inner_value is not None else 'nil'}")
21 | except KeyboardInterrupt:
22 | quit()
23 | except Exception as e:
24 | print(f"Error: {e}")
25 |
26 |
27 | if len(sys.argv) == 1:
28 | repl()
29 | quit()
30 |
31 | if sys.argv[1] == "--profile":
32 | with open(sys.argv[2]) as f:
33 | interpret(f.read(), opts={"debug": False, "profile": True})
34 | else:
35 | with open(sys.argv[1]) as f:
36 | interpret(f.read(), opts={"debug": False})
37 |
--------------------------------------------------------------------------------
/grammar_static.py:
--------------------------------------------------------------------------------
1 | # similar to grammar.py but for a static typed version of nodots
2 | GRAMMAR = r"""
3 | program : fun_stmt*
4 |
5 | declaration : fun_stmt | return_stmt | if_stmt | expression_stmt
6 |
7 | fun_stmt : "fn" function "nf"
8 | return_stmt : "return" expression_stmt
9 | if_stmt : "if" "(" expression ")" declaration* "fi"
10 | expression_stmt : expression? ";"
11 |
12 | expression : type? identifier "=" expression | equality
13 | equality : comparison ( ( "!=" | "==" ) comparison )*
14 | comparison : term ( ( ">" | ">=" | "<" | "<=" ) term )*
15 | term : factor ( ( "-" | "+" ) factor )*
16 | factor : call ( ( "/" | "*" ) call )*
17 | call : primary ( "(" arguments? ")" )*
18 | primary : NUMBER -> number | identifier
19 |
20 | function : identifier "(" parameters? ")" "->" type declaration*
21 | parameters : type identifier ( "," type identifier )*
22 | arguments : expression ( "," expression )*
23 |
24 | type : "i32" | "f64"
25 | identifier : CNAME
26 |
27 | %import common.ESCAPED_STRING
28 | %import common.LETTER
29 | %import common.DIGIT
30 | %import common.NUMBER
31 | %import common.CNAME
32 |
33 | %import common.WS
34 | %ignore WS
35 | %import common.SH_COMMENT
36 | %ignore SH_COMMENT
37 | """
38 |
--------------------------------------------------------------------------------
/wasm/wat2wasm.js:
--------------------------------------------------------------------------------
1 | const wabtmodule = require('./wabtmodule')
2 |
3 | const compile = (source) => {
4 | wabtmodule().then(function (wabt) {
5 | const features = {}
6 | const module = wabt.parseWat('test.wast', source, features);
7 | module.resolveNames();
8 | module.validate(features);
9 | const binaryOutput = module.toBinary({ log: true, write_debug_names: true });
10 | const outputLog = binaryOutput.log;
11 | const binaryBuffer = binaryOutput.buffer;
12 |
13 | // send debug details to stderr
14 | console.error({ binaryOutput, outputLog, binaryBuffer })
15 |
16 | // test that fib works!
17 | WebAssembly.instantiate(binaryBuffer, {})
18 | .then(result => {
19 | const func = result.instance.exports.fib;
20 | console.log('Calling with:', 25)
21 | const t = performance.now();
22 | console.log('Result:', func(25));
23 | console.log('Time:', performance.now() - t);
24 | })
25 | .catch(error => {
26 | console.error('Error loading WebAssembly module:', error);
27 | });
28 | })
29 | }
30 |
31 | let source = '';
32 | process.stdin.setEncoding('utf-8');
33 | process.stdin.on('data', (chunk) => {
34 | source += chunk;
35 | });
36 | process.stdin.on('end', () => {
37 | compile(source)
38 | });
39 | process.stdin.on('error', (err) => {
40 | console.error(err);
41 | });
42 |
--------------------------------------------------------------------------------
/grammar.py:
--------------------------------------------------------------------------------
1 | GRAMMAR = r"""
2 | program : declaration*
3 |
4 | declaration : fun_decl | statement
5 | fun_decl : "fun" function "nuf"
6 |
7 | statement : expression_stmt | return_stmt | if_stmt | for_stmt | break_stmt | continue_stmt
8 | expression_stmt : expression? ";"
9 | return_stmt : "return" expression? ";"
10 | if_stmt : "if" "(" expression ")" declaration* "fi"
11 | for_stmt : "for" "(" expression_stmt expression_stmt expression ")" declaration* "rof"
12 | break_stmt : "break" ";"
13 | continue_stmt : "continue" ";"
14 |
15 | expression : assignment
16 | assignment : identifier "=" assignment | logic_or
17 | logic_or : logic_and ( "or" logic_and )*
18 | logic_and : equality ( "and" equality )*
19 | equality : comparison ( ( "!=" | "==" ) comparison )*
20 | comparison : term ( ( ">" | ">=" | "<" | "<=" ) term )*
21 | term : factor ( ( "-" | "+" ) factor )*
22 | factor : unary ( ( "/" | "*" ) unary )*
23 | unary : ( "!" | "-" ) unary | call
24 | call : primary ( "(" arguments? ")" )*
25 | primary : "true" -> true | "false" -> false | "nil" -> nil
26 | | NUMBER -> number | ESCAPED_STRING -> string
27 | | identifier | "(" expression ")"
28 |
29 | function : identifier "(" parameters? ")" declaration*
30 | parameters : identifier ( "," identifier )*
31 | arguments : expression ( "," expression )*
32 |
33 | identifier : CNAME
34 |
35 | %import common.ESCAPED_STRING
36 | %import common.LETTER
37 | %import common.DIGIT
38 | %import common.NUMBER
39 | %import common.CNAME
40 |
41 | %import common.WS
42 | %ignore WS
43 | %import common.SH_COMMENT
44 | %ignore SH_COMMENT
45 | """
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # VSCode
2 | .vscode
3 |
4 | # Lol
5 | .DS_Store
6 |
7 | # Byte-compiled / optimized / DLL files
8 | __pycache__/
9 | *.py[cod]
10 | *$py.class
11 |
12 | # C extensions
13 | *.so
14 |
15 | # Distribution / packaging
16 | .Python
17 | build/
18 | develop-eggs/
19 | dist/
20 | downloads/
21 | eggs/
22 | .eggs/
23 | lib/
24 | lib64/
25 | parts/
26 | sdist/
27 | var/
28 | wheels/
29 | pip-wheel-metadata/
30 | share/python-wheels/
31 | *.egg-info/
32 | .installed.cfg
33 | *.egg
34 | MANIFEST
35 |
36 | # PyInstaller
37 | # Usually these files are written by a python script from a template
38 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
39 | *.manifest
40 | *.spec
41 |
42 | # Installer logs
43 | pip-log.txt
44 | pip-delete-this-directory.txt
45 |
46 | # Unit test / coverage reports
47 | htmlcov/
48 | .tox/
49 | .nox/
50 | .coverage
51 | .coverage.*
52 | .cache
53 | nosetests.xml
54 | coverage.xml
55 | *.cover
56 | *.py,cover
57 | .hypothesis/
58 | .pytest_cache/
59 |
60 | # Translations
61 | *.mo
62 | *.pot
63 |
64 | # Django stuff:
65 | *.log
66 | local_settings.py
67 | db.sqlite3
68 | db.sqlite3-journal
69 |
70 | # Flask stuff:
71 | instance/
72 | .webassets-cache
73 |
74 | # Scrapy stuff:
75 | .scrapy
76 |
77 | # Sphinx documentation
78 | docs/_build/
79 |
80 | # PyBuilder
81 | target/
82 |
83 | # Jupyter Notebook
84 | .ipynb_checkpoints
85 |
86 | # IPython
87 | profile_default/
88 | ipython_config.py
89 |
90 | # pyenv
91 | .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
101 | __pypackages__/
102 |
103 | # Celery stuff
104 | celerybeat-schedule
105 | celerybeat.pid
106 |
107 | # SageMath parsed files
108 | *.sage.py
109 |
110 | # Environments
111 | .env
112 | .venv
113 | env/
114 | venv/
115 | ENV/
116 | env.bak/
117 | venv.bak/
118 |
119 | # Spyder project settings
120 | .spyderproject
121 | .spyproject
122 |
123 | # Rope project settings
124 | .ropeproject
125 |
126 | # mkdocs documentation
127 | /site
128 |
129 | # mypy
130 | .mypy_cache/
131 | .dmypy.json
132 | dmypy.json
133 |
134 | # Pyre type checker
135 | .pyre/
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/healeycodes/nodots-lang/actions/workflows/python-app.yml)
2 |
3 | # nodots lang
4 | > My blog posts:
5 | > - [Adding a Line Profiler to My Language](https://healeycodes.com/adding-a-line-profiler-to-my-language)
6 | > - [A Custom WebAssembly Compiler](https://healeycodes.com/a-custom-webassembly-compiler)
7 | > - [Profiling and Optimizing an Interpreter](https://healeycodes.com/profiling-and-optimizing-an-interpreter)
8 | > - [Adding For Loops to an Interpreter](https://healeycodes.com/adding-for-loops-to-an-interpreter)
9 |
10 |
11 |
12 | A small programming language without any dots called **nodots**. There are two versions of this language; static types and a custom WebAssembly compiler (w/ type checking), and dynamic types with a tree-walk interpreter. Both use [Lark](https://lark-parser.readthedocs.io/en/latest/index.html) for parsing.
13 |
14 | Source files typically have the `.nd` file extension.
15 |
16 |
17 |
18 | ## WebAssembly Compiler (static types)
19 |
20 | `compiler.py` is a WebAssembly compiler (w/ type checking) that outputs WebAssembly Text. See `grammar_static.py` for the grammar.
21 |
22 | This version is more experimental than the interpreter but you can compile and run an example program with:
23 |
24 | ```text
25 | pip3 install -r requirements.txt
26 | ./compile.sh
27 | ```
28 |
29 | The example program is a naive algorithm that calculates the n-th Fibonacci number. It requires ~243k function calls and runs 4000x quicker in the compiled version.
30 |
31 | ```text
32 | fn fib(i32 n) -> i32
33 | if (n == 0)
34 | return 0;
35 | fi
36 | if (n == 1)
37 | return 1;
38 | fi
39 | return fib(n - 1) + fib(n - 2);
40 | nf
41 | ```
42 |
43 | The binary of this program is 134 bytes when encoded in base64. This is much smaller than: a Python runtime, the Lark parsing library, and a 1k LOC interpreter!
44 |
45 |
46 |
47 | ## Interpreter (dynamic types)
48 |
49 | `interpreter.py` is a tree-walk interpreter. See `grammar.py` for the grammar.
50 |
51 | Here's an example program (see `tests.py` for more examples).
52 |
53 | ```python
54 | # booleans
55 | (true or false) and !true;
56 |
57 | # numbers!
58 | (2 * 2) / 4 == 0;
59 | 1 < 2;
60 | -(8 * 8) > -65;
61 |
62 | # strings!
63 | log("Hello, World!");
64 |
65 | # variables!
66 | some_var = 2;
67 |
68 | # lists!
69 | some_list = list(-1, 3, 4);
70 | at(some_list, 0); # -1
71 | mut(some_list, 0, -2); # as in _mutate_
72 | at(some_list, 0); # -2
73 | join(list(1), list(2)); # [1, 2]
74 | len(list(1, 2, 3)); # 3
75 |
76 | # dictionaries!
77 | some_dict = dict("a", 2, "b", 3); # (k, v, k, v, ...)
78 | mut(some_dict, "a", "hi!");
79 | at(some_dict, "a"); # "hi!"
80 |
81 | # (also)
82 | keys(some_dict);
83 | vals(some_dict);
84 |
85 | # loops!
86 | sum = 0;
87 | for (i = 0; i < 5; i = i + 1)
88 | sum = sum + i;
89 | rof
90 | log(sum);
91 |
92 | # functions!
93 | fun some_func(b, c)
94 | return b * c;
95 | nuf
96 | some_func(some_var, 5);
97 |
98 | # naive fibonacci
99 | fun fib(x)
100 | if (x == 0 or x == 1)
101 | return x;
102 | fi
103 | return fib(x - 1) + fib(x - 2);
104 | nuf
105 | fib(10);
106 |
107 | # closures!
108 | fun closure()
109 | z = 0;
110 | fun inner()
111 | z = z + 1;
112 | return z;
113 | nuf
114 | return inner;
115 | nuf
116 | closure()(); # 1
117 |
118 | # i/o!
119 | write("./foo", "bar");
120 | data = read_all("./foo");
121 |
122 | fun read_function(chunk)
123 | log(chunk);
124 | nuf
125 | read("./foo", read_function);
126 | ```
127 |
128 | ### Install
129 |
130 | `pip3 install -r requirements.txt`
131 |
132 | ### Run
133 |
134 | `python3 cli.py sourcefile`
135 |
136 | ### Line Profiler
137 |
138 | `python3 cli.py --profile sourcefile`
139 |
140 | ### Tests
141 |
142 | `./test.sh`
143 |
144 | - Mypy type checking
145 | - Tests programs
146 |
--------------------------------------------------------------------------------
/tests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import signal
3 | import subprocess
4 | import time
5 | from interpreter import NilValue, interpret
6 |
7 |
8 | def rm_file(fp):
9 | try:
10 | os.remove(fp)
11 | except OSError:
12 | pass
13 |
14 |
15 | kitchen_sink_example = """
16 | # booleans
17 | (true or false) and !true;
18 |
19 | # numbers!
20 | (2 * 2) / 4 == 0;
21 | 1 < 2;
22 | -(8 * 8) > -65;
23 |
24 | # strings!
25 | log("Hello, World!");
26 |
27 | # variables!
28 | some_var = 2;
29 |
30 | # lists!
31 | some_list = list(-1, 3, 4);
32 | at(some_list, 0); # -1
33 | mut(some_list, 0, -2); # as in _mutate_
34 | at(some_list, 0); # -2
35 |
36 | # dictionaries!
37 | some_dict = dict("a", 2);
38 | mut(some_dict, "a", "hi!");
39 | at(some_dict, "a"); # "hi!"
40 |
41 | # (also)
42 | keys(some_dict);
43 | vals(some_dict);
44 |
45 | # loops!
46 | sum = 0;
47 | for (i = 0; i < 5; i = i + 1)
48 | sum = sum + i;
49 | rof
50 | log(sum);
51 |
52 | # functions!
53 | fun some_func(b, c)
54 | return b * c;
55 | nuf
56 | some_func(some_var, 5);
57 |
58 | # naive fibonacci
59 | fun fib(x)
60 | if (x == 0 or x == 1)
61 | return x;
62 | fi
63 | return fib(x - 1) + fib(x - 2);
64 | nuf
65 | fib(10);
66 |
67 | # closures!
68 | fun closure()
69 | z = 0;
70 | fun inner()
71 | z = z + 1;
72 | return z;
73 | nuf
74 | return inner;
75 | nuf
76 | closure()(); # 1
77 | """
78 |
79 |
80 | def assert_or_log(a, b):
81 | try:
82 | assert a == b
83 | except AssertionError as e:
84 | print(f"{a} != {b}")
85 | print("tests failed!")
86 | quit(1)
87 |
88 |
89 | # example programs
90 | assert_or_log(interpret(kitchen_sink_example).value, 1)
91 |
92 | # basic types
93 | assert_or_log(interpret("1;").value, 1)
94 | assert_or_log(interpret("-1.5;").value, -1.5)
95 | assert_or_log(interpret('"1";').value, "1")
96 | assert_or_log(interpret("true;").value, True)
97 | assert_or_log(interpret("false;").value, False)
98 | assert_or_log(interpret("nil;").value, None)
99 | assert_or_log(interpret("nil == nil;").value, True)
100 |
101 | # dicts
102 | assert_or_log(interpret('dict("a", "b");').value["a"].value, "b")
103 | assert_or_log(interpret('dict("1", "b");').value["1"].value, "b")
104 | assert_or_log(
105 | str(interpret('dict("a");')),
106 | "1:1 [error] dict expects an even number of args e.g. `k, v, k, v`, got: ['(StringValue: a)']",
107 | )
108 | assert_or_log(
109 | interpret(
110 | """
111 | a = dict("_", "b");
112 | mut(a, "_", "c");
113 | at(a, "_");
114 | """
115 | ).value,
116 | "c",
117 | )
118 | assert_or_log(
119 | interpret(
120 | """
121 | a = dict();
122 | at(a, "missing");
123 | """
124 | ).value,
125 | None,
126 | )
127 | assert_or_log(
128 | interpret(
129 | """
130 | a = dict("b", 2);
131 | keys(a);
132 | """
133 | )
134 | .value[0]
135 | .value,
136 | "b",
137 | )
138 | assert_or_log(
139 | interpret(
140 | """
141 | a = dict("b", 2);
142 | vals(a);
143 | """
144 | )
145 | .value[0]
146 | .value,
147 | 2,
148 | )
149 |
150 | # lists
151 | assert_or_log(interpret('list("a");').value[0].value, "a")
152 | assert_or_log(
153 | interpret(
154 | """
155 | a = list("b");
156 | mut(a, 0, "c");
157 | at(a, 0);
158 | """
159 | ).value,
160 | "c",
161 | )
162 | assert_or_log(
163 | str(
164 | interpret(
165 | """
166 | a = list();
167 | at(a, 1);
168 | """
169 | )
170 | ),
171 | "3:1 [error] list index out of bounds, len: 0 got: 1.0",
172 | )
173 |
174 | # logic
175 | assert_or_log(interpret("true and true;").value, True)
176 | assert_or_log(interpret("true and false;").value, False)
177 | assert_or_log(interpret("true or false;").value, True)
178 | assert_or_log(interpret("false or false;").value, False)
179 | assert_or_log(interpret("(false or false) or true;").value, True)
180 | assert_or_log(interpret("(false or false) and true;").value, False)
181 | assert_or_log(interpret("a = nil; if (true) a = 2; fi a;").value, 2)
182 | assert_or_log(interpret("a = nil; if (false or true) a = 2; fi a;").value, 2)
183 | assert_or_log(
184 | interpret(
185 | """
186 | fun truth()
187 | return true;
188 | nuf a = nil;
189 | if (truth())
190 | a = 2;
191 | fi
192 | a;
193 | """
194 | ).value,
195 | 2,
196 | )
197 | assert_or_log(isinstance(interpret("a = nil; if (false) a = 2; fi a;"), NilValue), True)
198 |
199 | # compare
200 | assert_or_log(interpret("1 == 1;").value, True)
201 | assert_or_log(interpret("1 != 1;").value, False)
202 | assert_or_log(interpret("1 >= 1;").value, True)
203 | assert_or_log(interpret("1 <= 1;").value, True)
204 | assert_or_log(interpret("2 < 3;").value, True)
205 | assert_or_log(interpret("3 > 2;").value, True)
206 |
207 | # expressions
208 | assert_or_log(isinstance(interpret(";"), NilValue), True)
209 | assert_or_log(interpret("-1;").value, -1)
210 | assert_or_log(isinstance(interpret("-1;;"), NilValue), True)
211 | assert_or_log(interpret("(1);").value, 1)
212 | assert_or_log(interpret("-(1);").value, -1)
213 | assert_or_log(interpret("(1 + 1);").value, 2)
214 | assert_or_log(interpret("(1 - 1);").value, 0)
215 | assert_or_log(interpret("(2 * 2);").value, 4)
216 | assert_or_log(interpret("(2 * 2) * (7 + 8);").value, 60)
217 | assert_or_log(interpret("(1 / 1);").value, 1)
218 | assert_or_log(interpret("a = (6 * 6); a;").value, 36)
219 | assert_or_log(
220 | interpret(
221 | """
222 | sum = 0;
223 | for (i = 0; i < 5; i = i + 1)
224 | sum = sum + i;
225 | rof
226 | sum;
227 | """
228 | ).value,
229 | 10,
230 | )
231 | assert_or_log(
232 | interpret(
233 | """
234 | a = 0;
235 | for (i = 0; i < 5; i = i + 1)
236 | a = a + 1;
237 | break;
238 | rof
239 | a;
240 | """
241 | ).value,
242 | 1,
243 | )
244 | assert_or_log(
245 | interpret(
246 | """
247 | a = 0;
248 | for (i = 0; i < 5; i = i + 1)
249 | continue;
250 | a = 1;
251 | rof
252 | a;
253 | """
254 | ).value,
255 | 0,
256 | )
257 |
258 | # scope and functions
259 | assert_or_log(interpret("a = 1; a;").value, 1)
260 | assert_or_log(interpret("a = 1; a = 2; a;").value, 2)
261 | assert_or_log(interpret("fun a() 1; 2; 3; 4; nuf a();").value, None)
262 | assert_or_log(interpret("fun a() 1; 2; return 3; 4; nuf a();").value, 3)
263 | assert_or_log(interpret("fun a() 1; 2; return 3; 0 / 0; nuf a();").value, 3)
264 | assert_or_log(interpret("a = 1; fun alter() a = 2; nuf alter(); a;").value, 2)
265 | assert_or_log(interpret("fun a(b, c) return b * c; nuf -a(3, 4);").value, -12)
266 | assert_or_log(interpret("fun a(b, c) return b * c; nuf a(3, 4) * a(3, 4);").value, 144)
267 | assert_or_log(type(interpret("fun a() 1; nuf a; b = a; b;").value).__name__, "function")
268 | assert_or_log(isinstance(interpret("fun a() 1; nuf"), NilValue), True)
269 | assert_or_log(
270 | interpret(
271 | """
272 | fun a()
273 | fun inner(b)
274 | return b * b;
275 | nuf
276 | return inner;
277 | nuf
278 | a()(3);
279 | """
280 | ).value,
281 | 9,
282 | )
283 | assert_or_log(
284 | interpret(
285 | """
286 | fun fib(x)
287 | if (x == 0 or x == 1)
288 | return x;
289 | fi
290 | return fib(x - 1) + fib(x - 2);
291 | nuf
292 | fib(10);
293 | """
294 | ).value,
295 | 55.0,
296 | )
297 | assert_or_log(
298 | isinstance(
299 | interpret(
300 | """
301 | fun _()
302 | return;
303 | nuf
304 | _();
305 | """
306 | ),
307 | NilValue,
308 | ),
309 | True,
310 | )
311 | assert_or_log(
312 | interpret(
313 | """
314 | j = 0;
315 | fun early_return()
316 | for (i = 0; i < 5; i = i + 1)
317 | j = j + 1;
318 | return;
319 | rof
320 | nuf
321 | early_return();
322 | j;
323 | """
324 | ).value,
325 | 1,
326 | )
327 |
328 | # builtins
329 | assert_or_log(interpret("len(list(1, 2));").value, 2)
330 | assert_or_log(interpret('len("ab");').value, 2)
331 | assert_or_log(interpret('join("a", "b");').value, "ab")
332 | assert_or_log(interpret('at(join(list("a"), list("b")), 0);').value, "a")
333 | assert_or_log(interpret('at(join(list("a"), list("b")), 1);').value, "b")
334 |
335 | # errors
336 | assert_or_log(str(interpret("(0 / 0);")), "1:2 [error] cannot divide by zero")
337 | assert_or_log(
338 | str(interpret("a = 1; fun alter() b = 2; nuf alter(); b;")),
339 | "1:40 [error] unknown variable 'b'",
340 | )
341 | assert_or_log(str(interpret("a;")), "1:1 [error] unknown variable 'a'")
342 | assert_or_log(
343 | str(interpret("b = 7; b();")),
344 | "1:8 [error] [(NumberValue: 7.0)] only functions are callable",
345 | )
346 | assert_or_log(
347 | str(interpret("if (1) fi")),
348 | "1:1 [error] [(NumberValue: 1.0)] if expressions expect a boolean",
349 | )
350 | assert_or_log(
351 | str(
352 | interpret(
353 | """
354 | a = 1000;
355 | fun decr()
356 | a = a - 1;
357 | decr();
358 | nuf
359 | decr();
360 | """
361 | )
362 | ),
363 | "5:7 [error] maximum recursion depth exceeded",
364 | )
365 | assert_or_log(
366 | str(interpret("for (i = 0; i < 5; i = i + 1) rof i;")),
367 | "1:35 [error] unknown variable 'i'",
368 | )
369 | assert_or_log(
370 | str(interpret("break;")),
371 | "1:1 [error] can't use 'break' outside of for loop body",
372 | )
373 | assert_or_log(
374 | str(interpret("continue;")),
375 | "1:1 [error] can't use 'continue' outside of for loop body",
376 | )
377 | assert_or_log(
378 | str(interpret("for (i = 0; i < 5; i = i + 1) fun b() break; nuf b(); rof")),
379 | "1:39 [error] can't use 'break' outside of for loop body",
380 | )
381 | assert_or_log(
382 | str(interpret("for (i = 0; i < 5; i = i + 1) fun b() continue; nuf b(); rof")),
383 | "1:39 [error] can't use 'continue' outside of for loop body",
384 | )
385 | assert_or_log(
386 | str(interpret("read();")),
387 | "1:1 [error] read() expects two args [string, function], got []",
388 | )
389 | assert_or_log(
390 | str(interpret("write();")),
391 | "1:1 [error] write() expects two args [string, string | number], got []",
392 | )
393 |
394 | # i/o
395 | assert_or_log(
396 | str(
397 | interpret(
398 | """
399 | data = nil;
400 | fun read_function(chunk)
401 | # first call: the entire file
402 | # second call: an empty string
403 | if (chunk != "")
404 | data = chunk;
405 | fi
406 | nuf;
407 | read("./example.txt", read_function);
408 | data;"""
409 | ).value
410 | ),
411 | "# this file is used for integration tests e.g. read()\n",
412 | )
413 |
414 | # write str
415 | write_path_1 = "./_test_write.txt"
416 | data_1 = "1"
417 | rm_file(write_path_1)
418 | assert_or_log(
419 | interpret(f'write("{write_path_1}", "{data_1}");').value,
420 | None,
421 | )
422 | with open(write_path_1, "r") as f:
423 | assert f.read() == data_1
424 | rm_file(write_path_1)
425 |
426 | # write num
427 | write_path_2 = "./_test_write.txt"
428 | data = 1
429 | rm_file(write_path_2)
430 | assert_or_log(
431 | interpret(f'write("{write_path_2}", "{data}");').value,
432 | None,
433 | )
434 | with open(write_path_2, "r") as f:
435 | assert f.read() == str(data)
436 | rm_file(write_path_2)
437 |
438 | # stdlib
439 | assert_or_log(
440 | str(
441 | interpret(
442 | """
443 | data = read_all("./example.txt");
444 | data;
445 | """
446 | ).value
447 | ),
448 | "# this file is used for integration tests e.g. read()\n",
449 | )
450 |
451 | # repl
452 | repl_process = subprocess.Popen(
453 | ["python3", "./cli.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
454 | )
455 | repl_process.stdin.write(b"1;\n") # type: ignore
456 | repl_process.stdin.flush() # type: ignore
457 | time.sleep(0.25) # would prefer not to sleep..
458 | repl_process.send_signal(signal.SIGINT)
459 | assert_or_log(repl_process.stdout.read(), b"> 1.0\n> ") # type: ignore
460 |
461 |
462 | print("tests passed!")
463 |
--------------------------------------------------------------------------------
/compiler.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List, Tuple, Type, Union
2 | from lark import Lark, Tree, Token
3 | from grammar_static import GRAMMAR
4 |
5 | parser = Lark(
6 | GRAMMAR,
7 | start="program",
8 | parser="lalr",
9 | keep_all_tokens=True,
10 | propagate_positions=True,
11 | )
12 |
13 |
14 | class I32:
15 | def __str__(self):
16 | return "i32"
17 |
18 |
19 | class F64:
20 | def __str__(self):
21 | return "f64"
22 |
23 |
24 | Params = List[Tuple[str, Union[Type[I32], Type[F64]]]]
25 |
26 |
27 | def format_params(params: Params):
28 | return "(" + (", ".join(map(lambda p: f"{p[1]} {p[0]}", params))) + ")"
29 |
30 |
31 | class Func:
32 | def __init__(
33 | self,
34 | params: Params,
35 | ntype: Union[Type[I32], Type[F64]],
36 | ):
37 | self.params = params
38 | self.ntype = ntype
39 |
40 | def __str__(self):
41 | return f"func ({format_params(self.params)}) -> {self.ntype}"
42 |
43 |
44 | def ntype_to_class(ntype: str):
45 | if ntype == "i32":
46 | return I32()
47 | elif ntype == "f64":
48 | return F64()
49 | raise Exception(f"unknown ntype {ntype}")
50 |
51 |
52 | Ntype = Union[Type[I32], Type[F64], Type[Func]]
53 |
54 |
55 | class Context:
56 | scope: Dict[str, Ntype] = {}
57 | func_return_ntype: None | Ntype = None
58 | wat = "(module\n"
59 |
60 | def write(self, code: str):
61 | self.wat += f"{code}"
62 |
63 | def finish(self):
64 | self.wat += ")\n"
65 | return self.wat
66 |
67 |
68 | def visit_functions(node: Tree, context):
69 | for child in node.children:
70 | if isinstance(child, Token):
71 | continue
72 | if child.data == "fun_stmt":
73 | visit_fun_stmt_for_types(child, context)
74 | else:
75 | visit_functions(child, context)
76 |
77 |
78 | def visit_declaration(node: Tree, context: Context):
79 | for child in node.children:
80 | if child.data == "expression_stmt":
81 | visit_expression_stmt(child, context)
82 | elif child.data == "fun_stmt":
83 | visit_fun_stmt(child, context)
84 | elif child.data == "return_stmt":
85 | visit_return_stmt(child, context)
86 | elif child.data == "if_stmt":
87 | visit_if_stmt(child, context)
88 |
89 |
90 | def visit_expression_stmt(node: Tree, context: Context):
91 | for child in node.children:
92 | if child == ";":
93 | continue
94 | if child.data == "expression":
95 | visit_expression(child, context)
96 |
97 |
98 | def visit_expression(node: Tree, context: Context) -> Ntype:
99 | line, col = node.meta.line, node.meta.column
100 | if node.children[0].data == "type":
101 | identifier = node.children[1].children[0].value
102 | if identifier in context.scope:
103 | raise Exception(f"can't redeclare identifier: {identifier} ({line}:{col})")
104 |
105 | context.write(f"(local ${identifier} {node.children[0].children[0]})\n")
106 | expr_ntype = visit_expression(node.children[3], context)
107 | context.write(f"(local.set ${identifier})\n")
108 |
109 | context.scope[identifier] = expr_ntype
110 | return expr_ntype
111 | elif node.children[0].data == "identifier":
112 | identifier = node.children[0].children[0].value
113 | if identifier not in context.scope:
114 | raise Exception(f"unknown identifier: {identifier} ({line}:{col})")
115 | ntype = context.scope[identifier]
116 |
117 | expr_ntype = visit_expression(node.children[2], context)
118 | context.write(f"(local.set ${identifier})\n")
119 |
120 | if type(ntype) != type(expr_ntype):
121 | raise Exception(
122 | f"type error {identifier}: expected {ntype} got {expr_ntype} ({line}:{col})"
123 | )
124 | return expr_ntype
125 |
126 | return visit_equality(node.children[0], context)
127 |
128 |
129 | def visit_equality(node: Tree, context: Context) -> Ntype:
130 | if len(node.children) == 1:
131 | return visit_comparison(node.children[0], context)
132 | line, col = node.meta.line, node.meta.column
133 |
134 | op = "eq" if node.children[1] == "==" else "ne"
135 | left_nytpe = visit_comparison(node.children[0], context)
136 | right_nytpe = visit_comparison(node.children[2], context)
137 |
138 | if type(left_nytpe) != type(right_nytpe):
139 | raise Exception(
140 | f"type error {node.children[1]}: mismatched types got {left_nytpe} and {right_nytpe} ({line}:{col})"
141 | )
142 | context.write(f"({left_nytpe}.{op})\n")
143 | return left_nytpe
144 |
145 |
146 | def visit_comparison(node: Tree, context: Context) -> Ntype:
147 | if len(node.children) == 1:
148 | return visit_term(node.children[0], context)
149 | line, col = node.meta.line, node.meta.column
150 |
151 | if node.children[1] == "<":
152 | op = "lt"
153 | elif node.children[1] == "<=":
154 | op = "le"
155 | elif node.children[1] == ">":
156 | op = "gt"
157 | elif node.children[1] == ">=":
158 | op = "ge"
159 |
160 | left_nytpe = visit_term(node.children[0], context)
161 | right_nytpe = visit_term(node.children[2], context)
162 |
163 | if type(left_nytpe) != type(right_nytpe):
164 | raise Exception(
165 | f"type error {node.children[1]}: mismatched types got {left_nytpe} and {right_nytpe} ({line}:{col})"
166 | )
167 | context.write(f"({left_nytpe}.{op})\n")
168 | return left_nytpe
169 |
170 |
171 | def visit_term(node: Tree, context: Context) -> Ntype:
172 | if len(node.children) == 1:
173 | return visit_factor(node.children[0], context)
174 | line, col = node.meta.line, node.meta.column
175 |
176 | op = "add" if node.children[1] == "+" else "sub"
177 | left_nytpe = visit_factor(node.children[0], context)
178 | right_nytpe = visit_factor(node.children[2], context)
179 |
180 | if type(left_nytpe) != type(right_nytpe):
181 | raise Exception(
182 | f"type error {node.children[1]}: mismatched types got {left_nytpe} and {right_nytpe} ({line}:{col})"
183 | )
184 | context.write(f"({left_nytpe}.{op})\n")
185 | return left_nytpe
186 |
187 |
188 | def visit_factor(node: Tree, context: Context) -> Ntype:
189 | if len(node.children) == 1:
190 | return visit_call(node.children[0], context)
191 | line, col = node.meta.line, node.meta.column
192 |
193 | op = "mul" if node.children[1] == "*" else "div"
194 | left_nytpe = visit_call(node.children[0], context)
195 | right_nytpe = visit_call(node.children[2], context)
196 |
197 | if type(left_nytpe) != type(right_nytpe):
198 | raise Exception(
199 | f"type error {node.children[1]}: mismatched types got {left_nytpe} and {right_nytpe} ({line}:{col})"
200 | )
201 | context.write(f"({left_nytpe}.{op})\n")
202 | return left_nytpe
203 |
204 |
205 | def visit_call(node: Tree, context: Context) -> Ntype:
206 | if len(node.children) == 1:
207 | return visit_primary(node.children[0], context)
208 | line, col = node.meta.line, node.meta.column
209 |
210 | identifier = node.children[0].children[0].children[0]
211 | if identifier not in context.scope:
212 | raise Exception(f"unknown function: {identifier} ({line}:{col})")
213 |
214 | func = context.scope[identifier]
215 | if type(func) != Func:
216 | raise Exception(f"can only call functions: {identifier} ({line}:{col})")
217 |
218 | args = []
219 | if not isinstance(node.children[2], Token):
220 | args = list(
221 | filter(lambda arg: not isinstance(arg, Token), node.children[2].children)
222 | )
223 |
224 | if len(func.params) != len(args):
225 | raise Exception(
226 | f"type error {identifier}: expected {len(func.params)} args got {len(args)} ({line}:{col})"
227 | )
228 |
229 | context.write(f"(call ${identifier} ")
230 | for i, arg in enumerate(args):
231 | if isinstance(arg, Token):
232 | continue
233 | arg_ntype = visit_expression(arg, context)
234 | if type(arg_ntype) != type(func.params[i][1]):
235 | raise Exception(
236 | f"type error {identifier}: expected {format_params(func.params)} got {arg_ntype} at pos {i} ({line}:{col})"
237 | )
238 | context.write(")\n")
239 | return func.ntype
240 |
241 |
242 | def visit_primary(node: Tree, context: Context) -> Ntype:
243 | line, col = node.meta.line, node.meta.column
244 | inner = node.children[0]
245 | if isinstance(inner, Token):
246 | if "." in inner:
247 | context.write(f"(f64.const {inner})\n")
248 | return F64()
249 | else:
250 | context.write(f"(i32.const {inner})\n")
251 | return I32()
252 | if inner.data == "identifier":
253 | identifier = inner.children[0]
254 | if identifier in context.scope:
255 | context.write(f"(local.get ${identifier})\n")
256 | return context.scope[identifier]
257 | raise Exception(f"unknown identifier: {identifier} ({line}:{col})")
258 | raise Exception("unreachable")
259 |
260 |
261 | def visit_fun_stmt_for_types(node: Tree, context: Context):
262 | line, col = node.meta.line, node.meta.column
263 | func_parts = node.children[1].children
264 | identifier = func_parts[0].children[0].value
265 | if identifier in context.scope:
266 | raise Exception(f"can't redeclare identifier: {identifier} ({line}:{col})")
267 |
268 | ntype: None | Tree = None
269 | args: None | Tree = None
270 | i = 0
271 | while i < len(func_parts):
272 | if func_parts[i] == "(" and func_parts[i + 1] != ")":
273 | args = func_parts[i + 1].children
274 | if func_parts[i] == "->":
275 | ntype = ntype_to_class(func_parts[i + 1].children[0].value)
276 | i += 1
277 |
278 | if not ntype:
279 | raise Exception(f"missing ntype for function {identifier} ({line}:{col})")
280 |
281 | params: Params = []
282 | if args:
283 | param_parts = list(filter(lambda x: x != ",", func_parts[2].children))
284 | for i in range(0, len(param_parts), 2):
285 | param_ntype = ntype_to_class(param_parts[i].children[0])
286 | param_id = param_parts[i + 1].children[0]
287 | params.append((param_id, param_ntype))
288 | context.scope[identifier] = Func(params, ntype)
289 |
290 |
291 | def visit_fun_stmt(node: Tree, context: Context):
292 | line, col = node.meta.line, node.meta.column
293 | func_parts = node.children[1].children
294 | identifier = func_parts[0].children[0].value
295 |
296 | if identifier not in context.scope:
297 | raise Exception(
298 | f"could't find function (this really shouldn't happen): {identifier} ({line}:{col})"
299 | )
300 | func: Func = context.scope[identifier]
301 | if type(func) != Func:
302 | raise Exception(
303 | f"expected func to be of type Func (this really shouldn't happen): {identifier} ({line}:{col})"
304 | )
305 |
306 | bodies_idx = 0
307 | while bodies_idx < len(func_parts):
308 | if func_parts[bodies_idx] == "->":
309 | bodies_idx += 2
310 | break
311 | bodies_idx += 1
312 | func_bodies = func_parts[bodies_idx:]
313 |
314 | if context.func_return_ntype is not None:
315 | raise Exception(f"nesting functions isn't allowed: {identifier} ({line}:{col})")
316 | context.func_return_ntype = func.ntype
317 |
318 | func_scope = {}
319 | for param in func.params:
320 | func_scope[param[0]] = param[1]
321 | for id, maybe_func in context.scope.items():
322 | if type(maybe_func) == Func:
323 | func_scope[id] = maybe_func
324 |
325 | prev_scope = context.scope
326 | context.scope = func_scope
327 | wat_params = " ".join(map(lambda p: f"(param ${p[0]} {p[1]})", func.params))
328 | context.write(
329 | f'\n(func ${identifier} (export "{identifier}") {wat_params} (result {func.ntype})\n'
330 | )
331 | for func_body in func_bodies:
332 | visit_declaration(func_body, context)
333 |
334 | # write a default value to avoid type errors
335 | # functions without a return statement return the default value (e.g. `0`)
336 | context.write(f"({func.ntype}.const 0)")
337 |
338 | context.write(")\n\n")
339 | context.scope = prev_scope
340 |
341 | context.func_return_ntype = None
342 |
343 |
344 | def visit_return_stmt(node: Tree, context: Context):
345 | line, col = node.meta.line, node.meta.column
346 | for child in node.children[1].children:
347 | if child == ";":
348 | continue
349 | if child.data == "expression":
350 | ntype = visit_expression(child, context)
351 | if context.func_return_ntype is None:
352 | raise Exception(f"can't return outside of functions ({line}:{col})")
353 | if type(ntype) != type(context.func_return_ntype):
354 | raise Exception(
355 | f"type error return: expected {context.func_return_ntype} got {ntype} ({line}:{col})"
356 | )
357 | context.write("(return)\n")
358 |
359 |
360 | def visit_if_stmt(node: Tree, context: Context):
361 | line, col = node.meta.line, node.meta.column
362 | ntype = visit_expression(node.children[2], context)
363 | if type(ntype) != I32:
364 | raise Exception(f"type error if: expected {I32()} got {ntype} ({line}:{col})")
365 | context.write(
366 | """(if
367 | (then\n"""
368 | )
369 | for i in range(3, len(node.children)):
370 | if isinstance(node.children[i], Token):
371 | continue
372 | visit_declaration(node.children[i], context)
373 | context.write(")\n)\n")
374 |
375 |
376 | def compile(source: str, context: Context):
377 | root = parser.parse(source)
378 | visit_functions(root, context)
379 | visit_declaration(root, context)
380 |
381 |
382 | if __name__ == "__main__":
383 | source = """
384 | fn fib(i32 n) -> i32
385 | if (n == 0)
386 | return 0;
387 | fi
388 | if (n == 1)
389 | return 1;
390 | fi
391 | return fib(n - 1) + fib(n - 2);
392 | nf
393 | """
394 | context = Context()
395 | compile(source, context)
396 | context.finish()
397 | print(context.wat)
398 |
--------------------------------------------------------------------------------
/interpreter.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | import time
3 | from typing import Any, Dict, List, Optional, Tuple, TypedDict
4 | import typing
5 | from lark import Lark, Tree as LarkTree, Token as LarkToken
6 | from grammar import GRAMMAR
7 |
8 | parser = Lark(
9 | GRAMMAR,
10 | start="program",
11 | parser="lalr",
12 | keep_all_tokens=True,
13 | propagate_positions=True,
14 | )
15 |
16 | Meta = typing.NamedTuple("Meta", [("line", int), ("column", int)])
17 |
18 |
19 | def format_number(seconds: float) -> str:
20 | if seconds >= 1:
21 | return f"{round(seconds, 1)}s"
22 | elif seconds >= 0.001:
23 | return f"{int(seconds * 1000)}ms"
24 | return f"{int(seconds * 1000 * 1000)}µs"
25 |
26 |
27 | class Tree:
28 | kind = "tree"
29 |
30 | def __init__(self, data: str, meta: Meta, children: List[Tree | Token]) -> None:
31 | self.data = data
32 | self.meta = meta
33 | self.children = children
34 |
35 | def __str__(self) -> str:
36 | return f"{self.data} (node)"
37 |
38 |
39 | class Token:
40 | kind = "token"
41 | data = "token"
42 | children: List[Any] = []
43 |
44 | def __init__(self, value: str, meta: Meta) -> None:
45 | self.value = value
46 | self.meta = meta
47 |
48 | def __eq__(self, other) -> bool:
49 | return self.value == other
50 |
51 | def __str__(self) -> str:
52 | return self.value
53 |
54 |
55 | def print_tree(node: Tree | Token, depth=0):
56 | print(depth * "-" + f" {node}")
57 | if node.kind == "tree":
58 | for child in node.children:
59 | print_tree(child, depth + 1)
60 |
61 |
62 | def build_nodots_tree(children: List[LarkTree | LarkToken]) -> List[Tree | Token]:
63 | return [
64 | Tree(
65 | str(child.data),
66 | Meta(child.meta.line, child.meta.column),
67 | build_nodots_tree(child.children),
68 | )
69 | if isinstance(child, LarkTree)
70 | else Token(child.value, Meta(child.line, child.column)) # type: ignore
71 | for child in children
72 | ]
73 |
74 |
75 | class LanguageError(Exception):
76 | def __init__(self, line: int, column: int, message: str):
77 | self.line = line
78 | self.column = column
79 | self.message = message
80 |
81 | def __str__(self) -> str:
82 | return f"{self.line}:{self.column} [error] {self.message}"
83 |
84 |
85 | class CallsDict(TypedDict):
86 | calls: List[Tuple[int, float]]
87 |
88 |
89 | class Context:
90 | def __init__(
91 | self,
92 | parent,
93 | opts={"debug": False, "profile": False},
94 | line_durations: Optional[CallsDict] = None,
95 | ):
96 | self._opts = opts
97 | self.parent = parent
98 | self.children: List[Context] = []
99 | self.debug = opts["debug"]
100 | self.profile = opts["profile"]
101 | self.lookup = {}
102 | self.line_durations: CallsDict = line_durations or {"calls": []}
103 |
104 | def set(self, key, value):
105 | if self.debug:
106 | print(f"set: {key}, {value}")
107 | cur = self
108 | while cur:
109 | if key in cur.lookup:
110 | cur.lookup[key] = value
111 | return
112 | cur = cur.parent
113 | self.lookup[key] = value
114 |
115 | def get(self, line, column, key) -> Value:
116 | cur = self
117 | while cur:
118 | if key in cur.lookup:
119 | return cur.lookup[key]
120 | cur = cur.parent
121 | raise LanguageError(line, column, f"unknown variable '{key}'")
122 |
123 | def get_child_context(self):
124 | child = Context(self, self._opts, self.line_durations)
125 | self.children.append(child)
126 | return child
127 |
128 | def track_call(self, line, duration):
129 | if self.profile:
130 | self.line_durations["calls"].append((line, duration))
131 |
132 | def print_line_profile(self, source: str):
133 | line_durations: Dict[int, List[float]] = {}
134 | for ln, dur in self.line_durations["calls"]:
135 | if ln in line_durations:
136 | line_durations[ln].append(dur)
137 | else:
138 | line_durations[ln] = [dur]
139 |
140 | # convert raw durations into statistics
141 | line_info: Dict[int, List[str]] = {}
142 | for ln, line in enumerate(source.splitlines()):
143 | if ln in line_durations:
144 | line_info[ln] = [
145 | # ncalls
146 | f"x{len(line_durations[ln])}",
147 | # tottime
148 | f"{format_number(sum(line_durations[ln]))}",
149 | # percall
150 | f"{format_number((sum(line_durations[ln]) / len(line_durations[ln])))}",
151 | ]
152 |
153 | # configure padding/lining up columns
154 | padding = 2
155 | max_line = max([len(line) for line in source.splitlines()])
156 | max_digits = (
157 | max(
158 | [
159 | max([len(f"{digits}") for digits in info])
160 | for info in line_info.values()
161 | ]
162 | )
163 | + 3 # column padding
164 | )
165 |
166 | # iterate source code, printing the line and (if any) its statistics
167 | print(" " * (max_line + padding), "ncalls ", "tottime ", "percall ")
168 | for i, line in enumerate(source.splitlines()):
169 | output = line
170 | ln = i + 1
171 | if ln in line_info:
172 | output += " " * (max_line - len(line) + padding)
173 | ncalls = line_info[ln][0]
174 | cumtime = line_info[ln][1]
175 | percall = line_info[ln][2]
176 | output += ncalls + " " * (max_digits - len(ncalls))
177 | output += cumtime + " " * (max_digits - len(cumtime))
178 | output += percall + " " * (max_digits - len(percall))
179 | print(output)
180 |
181 |
182 | class Value:
183 | def __init__(self, value):
184 | self.value = value
185 |
186 | def __str__(self) -> str:
187 | return f"({self.__class__.__name__}: {self.value})"
188 |
189 | def equals(self, other):
190 | return BoolValue(self.value == other.value)
191 |
192 | def not_equals(self, other):
193 | return BoolValue(self.value != other.value)
194 |
195 | def check_type(self, line, col, some_type_or_types: str | List[str], message):
196 | if type(some_type_or_types) == str:
197 | if self.__class__.__name__ != some_type_or_types:
198 | raise LanguageError(line, col, f"[{self}] {message}")
199 | else:
200 | for some_type in some_type_or_types:
201 | if self.__class__.__name__ == some_type:
202 | return
203 | raise LanguageError(line, col, f"[{self}] {message}")
204 |
205 | def call_as_func(self, line, col, arguments) -> Value:
206 | self.check_type(line, col, "FunctionValue", "only functions are callable")
207 | try:
208 | return self.value(line, col, arguments)
209 | except RecursionError:
210 | raise LanguageError(line, col, "maximum recursion depth exceeded")
211 |
212 |
213 | class BoolValue(Value):
214 | pass
215 |
216 |
217 | class NilValue(Value):
218 | pass
219 |
220 |
221 | class NumberValue(Value):
222 | pass
223 |
224 |
225 | class StringValue(Value):
226 | pass
227 |
228 |
229 | class FunctionValue(Value):
230 | pass
231 |
232 |
233 | class DictValue(Value):
234 | value: Dict[str, Value]
235 | pass
236 |
237 |
238 | class ListValue(Value):
239 | value: List[Value]
240 | pass
241 |
242 |
243 | class ReturnEscape(Exception):
244 | def __init__(self, value: Value):
245 | self.value = value
246 |
247 |
248 | class BreakEscape(Exception):
249 | pass
250 |
251 |
252 | class ContinueEscape(Exception):
253 | pass
254 |
255 |
256 | def format_value(value: Value):
257 | # TODO: complete this function for all value types
258 | if type(value) == NilValue:
259 | return "nil"
260 | elif type(value) == ListValue:
261 | return [format_value(v) for v in value.value]
262 | return value.value
263 |
264 |
265 | def log(line: int, col: int, values: List[Value]):
266 | for v in values:
267 | print(format_value(v))
268 |
269 |
270 | def dictionary(line: int, col: int, values: List[Value]):
271 | if len(values) % 2 != 0:
272 | raise LanguageError(
273 | line,
274 | col,
275 | f"dict expects an even number of args e.g. `k, v, k, v`, got: {list(map(lambda x: str(x), values))}",
276 | )
277 |
278 | ret = DictValue({})
279 | for i in range(0, len(values), 2):
280 | key = values[i]
281 | try:
282 | key.check_type(
283 | line, col, "StringValue", "only strings or numbers can be keys"
284 | )
285 | except:
286 | key.check_type(
287 | line, col, "NumberValue", "only strings or numbers can be keys"
288 | )
289 | value = values[i + 1]
290 | ret.value[key.value] = value
291 | return ret
292 |
293 |
294 | def listof(line: int, col: int, values: List[Value]):
295 | return ListValue(values)
296 |
297 |
298 | def mut(line: int, col: int, values: List[Value]) -> NilValue:
299 | if len(values) != 3:
300 | raise LanguageError(
301 | line,
302 | col,
303 | f"mut() expects three args (object, index, value), got {values}",
304 | )
305 |
306 | list_value: ListValue | None = None
307 | dict_value: DictValue | None = None
308 | try:
309 | values[0].check_type(
310 | line, col, "ListValue", "only dicts or lists can be called with mut()"
311 | )
312 | assert isinstance(values[0], ListValue)
313 | list_value = values[0]
314 | except:
315 | values[0].check_type(
316 | line, col, "DictValue", "only dicts or lists can be called with mut()"
317 | )
318 | assert isinstance(values[0], DictValue)
319 | dict_value = values[0]
320 |
321 | if list_value:
322 | values[1].check_type(
323 | line,
324 | col,
325 | "NumberValue",
326 | "lists can only be indexed by numbers",
327 | )
328 | index: int
329 | if values[1].value < 0 or not values[1].value.is_integer():
330 | raise LanguageError(
331 | line,
332 | col,
333 | f"list index must be a positive whole number, got: {values[1].value}",
334 | )
335 | if values[1].value >= len(list_value.value):
336 | raise LanguageError(
337 | line,
338 | col,
339 | f"list index out of bounds, len: {len(list_value.value)}, got: {values[1].value}",
340 | )
341 | index = int(values[1].value)
342 | list_value.value[index] = values[2]
343 | return NilValue(None)
344 |
345 | if dict_value:
346 | key: str
347 | try:
348 | values[1].check_type(
349 | line,
350 | col,
351 | "StringValue",
352 | "lists can only be indexed by strings or numbers",
353 | )
354 | key = values[1].value
355 | except:
356 | values[1].check_type(
357 | line,
358 | col,
359 | "NumberValue",
360 | "lists can only be indexed by strings or numbers",
361 | )
362 | key = str(values[1].value)
363 | dict_value.value[key] = values[2]
364 | return NilValue(None)
365 |
366 |
367 | def at(line: int, col: int, values: List[Value]) -> Value:
368 | if len(values) != 2:
369 | raise LanguageError(
370 | line,
371 | col,
372 | f"at() expects two args (index, value), got {values}",
373 | )
374 |
375 | list_value: ListValue | None = None
376 | dict_value: DictValue | None = None
377 | try:
378 | values[0].check_type(
379 | line, col, "ListValue", "only dicts or lists can be called with mut()"
380 | )
381 | assert isinstance(values[0], ListValue)
382 | list_value = values[0]
383 | except:
384 | values[0].check_type(
385 | line, col, "DictValue", "only dicts or lists can be called with mut()"
386 | )
387 | assert isinstance(values[0], DictValue)
388 | dict_value = values[0]
389 |
390 | if list_value:
391 | values[1].check_type(
392 | line,
393 | col,
394 | "NumberValue",
395 | "lists can only be indexed by numbers",
396 | )
397 | index: int
398 | if values[1].value < 0 or not values[1].value.is_integer():
399 | raise LanguageError(
400 | line,
401 | col,
402 | f"list index must be a positive whole number, got: {values[1].value}",
403 | )
404 | if values[1].value >= len(list_value.value):
405 | raise LanguageError(
406 | line,
407 | col,
408 | f"list index out of bounds, len: {len(list_value.value)} got: {values[1].value}",
409 | )
410 |
411 | index = int(values[1].value)
412 | return list_value.value[index]
413 |
414 | if dict_value:
415 | key: str
416 | try:
417 | values[1].check_type(
418 | line,
419 | col,
420 | "StringValue",
421 | "lists can only be indexed by strings or numbers",
422 | )
423 | key = values[1].value
424 | except:
425 | values[1].check_type(
426 | line,
427 | col,
428 | "NumberValue",
429 | "lists can only be indexed by strings or numbers",
430 | )
431 | key = str(values[1].value)
432 | if not key in dict_value.value:
433 | return NilValue(None)
434 | return dict_value.value[key]
435 |
436 | raise Exception("unreachable")
437 |
438 |
439 | def keysof(line: int, col: int, values: List[Value]) -> ListValue:
440 | if len(values) != 1:
441 | raise LanguageError(
442 | line,
443 | col,
444 | f"keys() expects one arg (dict), got {values}",
445 | )
446 | values[0].check_type(
447 | line,
448 | col,
449 | "DictValue",
450 | "only dicts can be called with keys()",
451 | )
452 | return ListValue([StringValue(k) for k in values[0].value.keys()])
453 |
454 |
455 | def vals(line: int, col: int, values: List[Value]) -> ListValue:
456 | if len(values) != 1:
457 | raise LanguageError(
458 | line,
459 | col,
460 | f"vals() expects one arg (dict), got {values}",
461 | )
462 | values[0].check_type(
463 | line,
464 | col,
465 | "DictValue",
466 | "only dicts can be called with vals()",
467 | )
468 | return ListValue(list(values[0].value.values()))
469 |
470 |
471 | def read(line: int, col: int, values: List[Value]) -> Value:
472 | chunk_size = 1024
473 | if len(values) != 2:
474 | raise LanguageError(
475 | line,
476 | col,
477 | f"read() expects two args [string, function], got {values}",
478 | )
479 | values[0].check_type(
480 | line,
481 | col,
482 | "StringValue",
483 | f"read() expects a string file path as the first arg, got {values[0]}",
484 | )
485 | values[1].check_type(
486 | line,
487 | col,
488 | "FunctionValue",
489 | f"read() expects a read function as the second arg, got {values[1]}",
490 | )
491 | file_path = values[0].value
492 | read_function = values[1]
493 | try:
494 | with open(file_path, "r") as f:
495 | while True:
496 | b = f.read(chunk_size)
497 | read_function.call_as_func(line, col, [StringValue(b)])
498 | if b == "":
499 | break
500 | except Exception as e:
501 | raise LanguageError(line, col, f'error reading "{file_path}": (py: {e})')
502 | return StringValue("")
503 |
504 |
505 | def write(line: int, col: int, values: List[Value]) -> Value:
506 | if len(values) != 2:
507 | raise LanguageError(
508 | line,
509 | col,
510 | f"write() expects two args [string, string | number], got {values}",
511 | )
512 | values[0].check_type(
513 | line,
514 | col,
515 | "StringValue",
516 | f"write() expects a (string) file path as the first arg, got {values[0]}",
517 | )
518 | values[1].check_type(
519 | line,
520 | col,
521 | ["StringValue", "NumberValue"],
522 | f"write() expects a (string | number) as the second arg, got {values[1]}",
523 | )
524 | file_path = values[0].value
525 | try:
526 | with open(file_path, "a") as f:
527 | f.write(values[1].value)
528 | except Exception as e:
529 | raise LanguageError(line, col, f'error writing "{file_path}": (py: {e})')
530 | return NilValue(None)
531 |
532 |
533 | def join(line: int, col: int, values: List[Value]) -> Value:
534 | if len(values) != 2:
535 | raise LanguageError(
536 | line,
537 | col,
538 | f"join() expects two args [string, string] or [list, list], got {values}",
539 | )
540 |
541 | # (string, string)
542 | if values[0].__class__.__name__ == "StringValue":
543 | if values[0].__class__.__name__ != "StringValue":
544 | raise LanguageError(
545 | line,
546 | col,
547 | f"join() expects two args [string, string] or [list, list], got {values}",
548 | )
549 | return StringValue(values[0].value + values[1].value)
550 | elif values[0].__class__.__name__ == "ListValue":
551 | if values[0].__class__.__name__ != "ListValue":
552 | raise LanguageError(
553 | line,
554 | col,
555 | f"join() expects two args [string, string] or [list, list], got {values}",
556 | )
557 | ret = []
558 | for v in values[0].value:
559 | ret.append(v)
560 | for v in values[1].value:
561 | ret.append(v)
562 | return ListValue(ret)
563 | raise LanguageError(
564 | line,
565 | col,
566 | f"join() expects two args [string, string] or [list, list], got {values}",
567 | )
568 |
569 |
570 | def length(line: int, col: int, values: List[Value]) -> Value:
571 | if len(values) != 1:
572 | raise LanguageError(
573 | line,
574 | col,
575 | f"len() expects a single arg (string | list), got {values}",
576 | )
577 | values[0].check_type(
578 | line,
579 | col,
580 | ["StringValue", "ListValue"],
581 | f"len() expects a (string | list)",
582 | )
583 | return NumberValue(len(values[0].value))
584 |
585 |
586 | def key_from_identifier_node(node: Tree | Token) -> str:
587 | # we're not looking for the idenitifier's "reference"
588 | # when we context.set, we will update the lookup table
589 | # so we can just dig the literal string value here
590 | return node.children[0].value # type: ignore
591 |
592 |
593 | def eval_identifier(node: Any, context: Context):
594 | return context.get(node.meta.line, node.meta.column, node.children[0].value)
595 |
596 |
597 | def eval_primary(node: Tree | Token, context: Context) -> Value:
598 | if len(node.children) == 1:
599 | return eval_identifier(node.children[0], context)
600 | # 0th is '(' and 2nd is ')'
601 | if node.children[1].data == "expression":
602 | return eval_expression(node.children[1], context)
603 | raise Exception("unreachable")
604 |
605 |
606 | def eval_arguments(node: Tree | Token, context: Context) -> List[Value]:
607 | return [
608 | eval_expression(child, context)
609 | for child in node.children
610 | # filter out '(' and ')'
611 | if child.kind == "tree"
612 | ]
613 |
614 |
615 | def eval_call(node: Tree | Token, context: Context) -> Value:
616 | # primary aka not a function call
617 | if len(node.children) == 1:
618 | for child in node.children:
619 | if child.data == "true":
620 | return BoolValue(True)
621 | elif child.data == "false":
622 | return BoolValue(False)
623 | elif child.data == "primary":
624 | return eval_primary(child, context)
625 | elif child.data == "number":
626 | first_child_num: Any = child.children[0]
627 | return NumberValue(float(first_child_num.value))
628 | elif child.data == "nil":
629 | return NilValue(None)
630 | elif child.data == "string":
631 | # trim quote marks `"1"` -> `1`
632 | first_child_str: Any = child.children[0]
633 | return StringValue(first_child_str.value[1:-1])
634 | raise Exception("unreachable")
635 |
636 | start = time.perf_counter()
637 |
638 | # functions calls can be chained like `a()()(2)`
639 | # so we want the initial function and then an
640 | # arbitrary number of calls (with or without arguments)
641 | current_func = eval_primary(node.children[0], context)
642 |
643 | i = 0
644 | arguments: List[None | Tree | Token] = []
645 | while i < len(node.children) - 1:
646 | i += 1
647 | if node.children[i] == ")":
648 | if node.children[i - 1] == "(":
649 | arguments.append(None)
650 | elif (
651 | node.children[i - 1].kind == "tree"
652 | and node.children[i - 1].data == "arguments"
653 | ):
654 | arguments.append(node.children[i - 1])
655 | else:
656 | raise Exception("unreachable")
657 |
658 | for args in arguments:
659 | start = time.perf_counter()
660 | current_func = current_func.call_as_func(
661 | node.children[0].meta.line,
662 | node.children[0].meta.column,
663 | eval_arguments(args, context) if args else [],
664 | )
665 | context.track_call(node.children[0].meta.line, time.perf_counter() - start)
666 |
667 | return current_func
668 |
669 |
670 | def eval_unary(node: Tree | Token, context: Context) -> Value:
671 | if len(node.children) == 1:
672 | return eval_call(node.children[0], context)
673 | op = node.children[0]
674 | left = eval_unary(node.children[1], context)
675 |
676 | if op == "-":
677 | left.check_type(
678 | node.children[1].meta.line,
679 | node.children[1].meta.column,
680 | "NumberValue",
681 | "only numbers can be negated",
682 | )
683 | return NumberValue(-left.value)
684 | if op == "!":
685 | left.check_type(
686 | node.children[1].meta.line,
687 | node.children[1].meta.column,
688 | "BoolValue",
689 | "only booleans can be flipped",
690 | )
691 | # TODO: maybe a method on Value or BoolValue instead?
692 | return BoolValue(not left.value)
693 | raise Exception("unreachable")
694 |
695 |
696 | def eval_factor(node: Tree | Token, context: Context) -> Value:
697 | if len(node.children) == 1:
698 | return eval_unary(node.children[0], context)
699 | left = eval_unary(node.children[0], context)
700 | op = node.children[1]
701 | right = eval_unary(node.children[2], context)
702 | left.check_type(
703 | node.children[0].meta.line,
704 | node.children[0].meta.column,
705 | "NumberValue",
706 | "only numbers can be factored",
707 | )
708 | right.check_type(
709 | node.children[2].meta.line,
710 | node.children[2].meta.column,
711 | "NumberValue",
712 | "only numbers can be factored",
713 | )
714 | if op == "*":
715 | return NumberValue(left.value * right.value)
716 | elif op == "/":
717 | try:
718 | return NumberValue(left.value / right.value)
719 | except ZeroDivisionError:
720 | raise LanguageError(
721 | node.children[0].meta.line,
722 | node.children[0].meta.column,
723 | "cannot divide by zero",
724 | )
725 | raise Exception("unreachable")
726 |
727 |
728 | def eval_term(node: Tree | Token, context: Context) -> Value:
729 | if len(node.children) == 1:
730 | return eval_factor(node.children[0], context)
731 | left = eval_factor(node.children[0], context)
732 | op = node.children[1]
733 | right = eval_factor(node.children[2], context)
734 | left.check_type(
735 | node.children[0].meta.line,
736 | node.children[0].meta.column,
737 | "NumberValue",
738 | "only numbers can be added or subtracted",
739 | )
740 | right.check_type(
741 | node.children[2].meta.line,
742 | node.children[2].meta.column,
743 | "NumberValue",
744 | "only numbers can be added or subtracted",
745 | )
746 | if op == "+":
747 | return NumberValue(left.value + right.value)
748 | elif op == "-":
749 | return NumberValue(left.value - right.value)
750 | raise Exception("unreachable")
751 |
752 |
753 | def eval_comparison(node: Tree | Token, context: Context) -> Value:
754 | if len(node.children) == 1:
755 | return eval_term(node.children[0], context)
756 | left = eval_term(node.children[0], context)
757 | op = node.children[1]
758 | right = eval_term(node.children[2], context)
759 | left.check_type(
760 | node.children[0].meta.line,
761 | node.children[0].meta.column,
762 | "NumberValue",
763 | "only numbers can be compared",
764 | )
765 | right.check_type(
766 | node.children[2].meta.line,
767 | node.children[2].meta.column,
768 | "NumberValue",
769 | "only numbers can be compared",
770 | )
771 | if op == "<":
772 | return BoolValue(left.value < right.value)
773 | elif op == "<=":
774 | return BoolValue(left.value <= right.value)
775 | elif op == ">":
776 | return BoolValue(left.value > right.value)
777 | elif op == ">=":
778 | return BoolValue(left.value >= right.value)
779 | raise Exception("unreachable")
780 |
781 |
782 | def eval_equality(node: Tree | Token, context: Context) -> Value:
783 | if len(node.children) == 1:
784 | return eval_comparison(node.children[0], context)
785 |
786 | left = eval_comparison(node.children[0], context)
787 | op = node.children[1]
788 | right = eval_comparison(node.children[2], context)
789 | if op == "==":
790 | return left.equals(right)
791 | elif op == "!=":
792 | return left.not_equals(right)
793 | raise Exception("unreachable")
794 |
795 |
796 | def eval_logic_and(node: Tree | Token, context: Context) -> Value | BoolValue:
797 | if len(node.children) == 1:
798 | return eval_equality(node.children[0], context)
799 | left = eval_equality(node.children[0], context)
800 | op = node.children[1]
801 | right = eval_equality(node.children[2], context)
802 | left.check_type(
803 | node.children[0].meta.line,
804 | node.children[0].meta.column,
805 | "BoolValue",
806 | "only booleans can be used with 'and'",
807 | )
808 | right.check_type(
809 | node.children[2].meta.line,
810 | node.children[2].meta.column,
811 | "BoolValue",
812 | "only booleans can be used with 'and'",
813 | )
814 |
815 | if op == "and":
816 | return BoolValue(left.value and right.value)
817 | raise Exception("unreachable")
818 |
819 |
820 | def eval_logic_or(node: Tree | Token, context: Context) -> Value | BoolValue:
821 | if len(node.children) == 1:
822 | return eval_logic_and(node.children[0], context)
823 | left = eval_logic_and(node.children[0], context)
824 | op = node.children[1]
825 | right = eval_logic_and(node.children[2], context)
826 | left.check_type(
827 | node.children[0].meta.line,
828 | node.children[0].meta.column,
829 | "BoolValue",
830 | "only booleans can be used with 'or'",
831 | )
832 | right.check_type(
833 | node.children[2].meta.line,
834 | node.children[2].meta.column,
835 | "BoolValue",
836 | "only booleans can be used with 'or'",
837 | )
838 |
839 | if op == "or":
840 | return BoolValue(left.value or right.value)
841 | raise Exception("unreachable")
842 |
843 |
844 | def eval_assignment(node: Tree | Token, context: Context) -> NilValue | Value:
845 | if node.children[0].data == "identifier":
846 | key = key_from_identifier_node(node.children[0])
847 | value = eval_assignment(node.children[2], context)
848 | context.set(key, value)
849 | return NilValue(None)
850 | elif node.children[0].data == "logic_or":
851 | return eval_logic_or(node.children[0], context)
852 | raise Exception("unreachable")
853 |
854 |
855 | def eval_expression(node: Tree | Token, context: Context) -> Value:
856 | for child in node.children:
857 | if child.data == "assignment":
858 | return eval_assignment(child, context)
859 | raise Exception("unreachable")
860 |
861 |
862 | def eval_expression_stmt(node: Tree | Token, context: Context) -> Value:
863 | # support empty expression stmts
864 | for child in node.children:
865 | if child.kind == "tree":
866 | return eval_expression(child, context)
867 | return NilValue(None)
868 |
869 |
870 | def eval_return_stmt(node: Tree | Token, context: Context):
871 | for child in node.children:
872 | # filter out syntax like `return` and `;`
873 | if child.kind == "tree":
874 | raise ReturnEscape(eval_expression(child, context))
875 | # handle `return;``
876 | raise ReturnEscape(NilValue(None))
877 |
878 |
879 | def eval_if_stmt(node: Tree | Token, context: Context):
880 | # the tree shape is as follows (any number of childs)
881 | # ['if', '(', expr, ')', child_1, child_2, 'fi']
882 | if_check = eval_expression(node.children[2], context)
883 | if_check.check_type(
884 | node.meta.line, node.meta.column, "BoolValue", "if expressions expect a boolean"
885 | )
886 | if if_check.value != True:
887 | return NilValue(None)
888 | start, end = node.children.index(")") + 1, node.children.index("fi") # type: ignore
889 | for decl in node.children[start:end]:
890 | eval_declaration(decl, context)
891 | return NilValue(None)
892 |
893 |
894 | def eval_for_stmt(node: Tree | Token, context: Context):
895 | for_context = context.get_child_context()
896 | parts: List[Tree] = []
897 | for child in node.children:
898 | if child.kind == "tree":
899 | parts.append(child) # type: ignore
900 | initial_expr_stmt, limit_expr_stmt, increment_expr = parts[:3]
901 | eval_expression_stmt(initial_expr_stmt, for_context)
902 |
903 | while True:
904 | limit_check = eval_expression_stmt(limit_expr_stmt, for_context)
905 | limit_check.check_type(
906 | limit_expr_stmt.meta.line,
907 | limit_expr_stmt.meta.column,
908 | "BoolValue",
909 | "expected boolean",
910 | )
911 | if not limit_check.value:
912 | break
913 | for decl_expr in parts[3:]:
914 | try:
915 | eval_declaration(decl_expr, for_context)
916 | except BreakEscape:
917 | return NilValue(None)
918 | except ContinueEscape:
919 | break
920 | eval_expression(increment_expr, for_context)
921 | return NilValue(None)
922 |
923 |
924 | def eval_statement(node: Tree | Token, context: Context) -> Value:
925 | for child in node.children:
926 | if child.data == "expression_stmt":
927 | return eval_expression_stmt(child, context)
928 | elif child.data == "return_stmt":
929 | return eval_return_stmt(child, context)
930 | elif child.data == "if_stmt":
931 | return eval_if_stmt(child, context)
932 | elif child.data == "for_stmt":
933 | return eval_for_stmt(child, context)
934 | elif child.data == "break_stmt":
935 | raise BreakEscape()
936 | elif child.data == "continue_stmt":
937 | raise ContinueEscape()
938 | raise Exception("unreachable")
939 |
940 |
941 | def eval_parameters(node: Tree | Token, context: Context) -> List[str]:
942 | parameters = []
943 | for child in node.children:
944 | if child.kind == "tree" and child.data == "identifier":
945 | parameters.append(key_from_identifier_node(child))
946 | return parameters
947 |
948 |
949 | def eval_function(node: Tree | Token, context: Context) -> NilValue:
950 | function_context = context.get_child_context()
951 | key = key_from_identifier_node(node.children[0])
952 |
953 | parameters = []
954 | if node.children.index(")") - node.children.index("(") == 2: # type: ignore
955 | parameters = eval_parameters(
956 | node.children[node.children.index("(") + 1],
957 | context, # type: ignore
958 | )
959 | body = node.children[node.children.index(")") + 1 :] # type: ignore
960 |
961 | def function(line, col, arguments):
962 | if len(arguments) != len(parameters):
963 | raise LanguageError(
964 | line,
965 | col,
966 | "not enough (or too many) function arguments, "
967 | + f"want {len(parameters)}, got {len(arguments)}",
968 | )
969 | per_call_context = function_context.get_child_context()
970 | for i, arg in enumerate(arguments):
971 | per_call_context.set(parameters[i], arg)
972 | for child in body:
973 | try:
974 | eval_declaration(child, per_call_context)
975 | except ReturnEscape as e:
976 | return e.value
977 | except BreakEscape:
978 | raise LanguageError(
979 | child.meta.line,
980 | child.meta.column,
981 | "can't use 'break' outside of for loop body",
982 | )
983 | except ContinueEscape:
984 | raise LanguageError(
985 | child.meta.line,
986 | child.meta.column,
987 | "can't use 'continue' outside of for loop body",
988 | )
989 | return NilValue(None)
990 |
991 | context.set(key, FunctionValue(function))
992 | return NilValue(None)
993 |
994 |
995 | def eval_fun_decl(node: Tree | Token, context: Context):
996 | # 0th is 'fun', 2nd is 'nuf'
997 | return eval_function(node.children[1], context)
998 |
999 |
1000 | def eval_declaration(node: Tree | Token, context: Context):
1001 | for child in node.children:
1002 | if child.data == "statement":
1003 | return eval_statement(child, context)
1004 | elif child.data == "fun_decl":
1005 | return eval_fun_decl(child, context)
1006 | raise Exception("unreachable")
1007 |
1008 |
1009 | def eval_program(node: Tree | Token, context: Context):
1010 | last: NilValue | Value = NilValue(None)
1011 | for child in node.children:
1012 | try:
1013 | last = eval_declaration(child, context)
1014 | except BreakEscape:
1015 | raise LanguageError(
1016 | child.meta.line,
1017 | child.meta.column,
1018 | "can't use 'break' outside of for loop body",
1019 | )
1020 | except ContinueEscape:
1021 | raise LanguageError(
1022 | child.meta.line,
1023 | child.meta.column,
1024 | "can't use 'continue' outside of for loop body",
1025 | )
1026 | return last
1027 |
1028 |
1029 | def inject_builtins(context: Context):
1030 | funcs = {
1031 | "log": log,
1032 | "dict": dictionary,
1033 | "list": listof,
1034 | "mut": mut,
1035 | "at": at,
1036 | "keys": keysof,
1037 | "vals": vals,
1038 | "read": read,
1039 | "write": write,
1040 | "join": join,
1041 | "len": length,
1042 | }
1043 | for name, func in funcs.items():
1044 | context.set(name, FunctionValue(func))
1045 |
1046 |
1047 | def inject_std_lib(context: Context):
1048 | source = """
1049 | fun read_all(file_path)
1050 | ret = "";
1051 | fun each_chunk(chunk)
1052 | ret = join(ret, chunk);
1053 | nuf
1054 | read(file_path, each_chunk);
1055 | return ret;
1056 | nuf
1057 | """
1058 | root = build_nodots_tree([parser.parse(source)])[0]
1059 | eval_program(root, context=context)
1060 |
1061 |
1062 | def inject_all(context: Context):
1063 | inject_builtins(context)
1064 | inject_std_lib(context)
1065 |
1066 |
1067 | def get_root(source: str):
1068 | try:
1069 | parsed = parser.parse(source)
1070 | except Exception as e:
1071 | # Usually everything after the first line isn't helpful for a user
1072 | # TODO: surface the full parser error during development
1073 | raise Exception(f"{e}".split("\n")[0])
1074 | return build_nodots_tree([parsed])[0]
1075 |
1076 |
1077 | def get_context(opts: Dict[str, bool]) -> Context:
1078 | root_context = Context(None, opts=opts)
1079 | inject_all(root_context)
1080 | return root_context
1081 |
1082 |
1083 | def interpret(source: str, opts={}):
1084 | opts = {"debug": False, "profile": False} | opts
1085 | try:
1086 | root_context = get_context(opts)
1087 | root = get_root(source)
1088 | result = eval_program(root, context=root_context)
1089 | if opts["profile"]:
1090 | root_context.print_line_profile(source)
1091 | return result
1092 | except LanguageError as e:
1093 | return e
1094 |
--------------------------------------------------------------------------------