├── 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 | [![Tests](https://github.com/healeycodes/nodots-lang/actions/workflows/python-app.yml/badge.svg)](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 | --------------------------------------------------------------------------------