├── .gitignore ├── README.md ├── environment.py ├── examples ├── 99bottles.pe ├── assignments.pe ├── erroneus │ ├── max_recursion.pe │ ├── type_err_in_fn.pe │ ├── type_error.pe │ ├── unexpected_char.pe │ ├── unexpected_token.pe │ └── uninizialized_var.pe ├── fact.pe ├── fact_auto_comment.pe ├── fizzbuzz.pe ├── hello_world.pe ├── input.pe ├── lexical_scope.pe ├── list.pe ├── meta_comments.pe ├── test.pe ├── tictactoe.pe └── unary.pe ├── exceptions.py ├── interpreter.py ├── jlast.py ├── jltypes.py ├── parser.py ├── pls_explain.py ├── prelude.py └── resolver.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Emacs 132 | .\#* 133 | \#* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlsExplain (aka jamlang0001) 2 | A small dynamically typed programming language with first-class comments, where every value is explained by a comment. 3 | 4 | This language was developed within 48 hours for the [first langjam](https://github.com/langjam/jam0001). 5 | 6 | [Main Repo](https://github.com/hoppecl/jamlang0001) 7 | 8 | ### Running the Interpreter 9 | #### Dependencies 10 | The interpreter depends on python3 and [lark](https://github.com/lark-parser/lark). Lark can be installed with `pip install lark`. 11 | #### REPL 12 | Invoke the interpreter without command line arguments to start the interactive mode 13 | 14 | $ ./pls_explain.py 15 | >>> print("hello world") 16 | hello world /*the string "hello world"*/ 17 | >>> 18 | 19 | To exit press `Ctrl-D`. 20 | 21 | To execute a Program written in *PlsExplain*, pass the path to the program as the first command line argument. 22 | 23 | $ ./pls_explain.py examples/hello_world.pe 24 | Hello World /*the string "Hello World"*/ 25 | 26 | ### First-Class Comments 27 | Comments are first-class values in *PlsExplain*. This means that they are expressions, can be stored in variables, passed as function arguments and be returned by functions. 28 | 29 | >>> let comment = /* This is a comment */; 30 | >>> print(comment) 31 | /* This is a comment */ /* a comment */ 32 | >>> print(/*another comment*/) 33 | /*another comment*/ /* a comment */ 34 | 35 | At a first glance comments might seem to be equivalent to strings. The difference is that comments can be used to explain something. In *PlsExplain* all values have an associated comment that explains the value. To explain a value with a comment, simply write it next to the expression. When called with a single argument, `print` shows the associated comment. 36 | 37 | >>> let x = 40 + 2 /* the number fourtytwo */; 38 | >>> print(x) 39 | 42 /* the number fourtytwo */ 40 | 41 | Explaining comments don't have to be comment-literals: 42 | 43 | >>> let comment = /* this is a comment */ 44 | >>> let x = 42 comment 45 | >>> print(x) 46 | 42 /* this is a comment */ 47 | 48 | Trying to explain a value with something that is not a comment obviously results in a type error. 49 | 50 | >>> 42 "this is not a comment" 51 | Backtrace (most recent call last): 52 | 53 | line 1: 54 | 42 "this is not a comment" 55 | ^~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | type error: type JlString can not be used to explain values 57 | 58 | >>> let s = "not a comment either" 59 | 60 | If you see this error, it often means that you have forgotten to separate two expressions with an semicolon. 61 | 62 | #### Auto-generated Comments 63 | If a value is not explained by an explicit comment, the interpreter automagically generates a helpful comment. 64 | 65 | >>> let x = 4 66 | >>> print(x) 67 | 4.0 /* the number 4.0 */ 68 | >>> let y = x + 10 69 | >>> print(y) 70 | 14.0 /* the sum of the number 4.0 and the number 10.0 */ 71 | 72 | #### The *explain* operator 73 | The comment explaining a value can be retrieved with the `?`operator. 74 | 75 | >>> print(x?) 76 | /* the number 4.0 */ 77 | 78 | #### Manipulating Comments 79 | The auto-generated comment can sometimes be a little bit verbose: 80 | 81 | let fact = fn(n) { 82 | if (n == 0) { 83 | 1 84 | } else { 85 | fact(n - 1) * n; 86 | } 87 | }; 88 | print(fact(4)); 89 | 90 | This program prints: 91 | 92 | 24.0 /*the product of the product of the product of the product 93 | of the number 1.0 and the difference of the difference of 94 | the difference of the number 4.0 and the number 1.0 and the 95 | number 1.0 and the number 1.0 and the difference of the 96 | difference of the number 4.0 and the number 1.0 and the number 97 | 1.0 and the difference of the number 4.0 and the number 1.0 98 | and the number 4.0*/ 99 | 100 | 101 | Since comments are first-class values we can manipulate them on the fly. This allows us to generate even more helpful comments. 102 | 103 | let fact = fn(n) { 104 | if (n == 0) { 105 | 1 106 | } else { 107 | /* lets generate a helpful comment for the return value */; 108 | /* (Comments can be concatenated with +) */ 109 | let comment = /* the factorial of */ + n?; 110 | /* explain the return value with the generated comment */ 111 | (fact(n - 1) * n) comment; 112 | } 113 | }; 114 | let h = 4 /*the number of hours i've slept*/; 115 | print(fact(h)); 116 | 117 | The output of this program is more concise: 118 | 119 | 24.0 /* the factorial of the number of hours i've slept*/ 120 | 121 | #### Meta-Comments and Meta-Meta-Comments and ... 122 | Since comments are first-class values, comments are also explained by comments. Obviously comments explaining a comment are also explained by comments which in turn are explained by comments and so on. 123 | 124 | >>> let x = 42 /* comment */ /* meta-comment */ /* meta-meta-comment */ 125 | >>> print(x) 126 | 42 /* comment */ 127 | >>> print(x?) 128 | /* comment */ /* meta-comment */ 129 | >>> print(x??) 130 | /* meta-comment */ /* meta-meta-comment */ 131 | >>> print(x???) 132 | /* meta-meta-comment */ /*a comment*/ <-- autogenerated meta-meta-meta-comment 133 | >>> print(x????) 134 | /*a comment*/ /*a comment*/ 135 | ... 136 | 137 | 138 | ### Datatypes 139 | |Type| Description | 140 | |--|--| 141 | | `Comment` | the most important type | 142 | | `String`| unicode strings | 143 | | `Number`| floating point numbers | 144 | | `Bool`| `True`or `False`| 145 | | `Unit`| only the value `()`| 146 | | `List`| Lists of values | 147 | 148 | #### Lists 149 | Currently there is no special syntax for lists, instead the builtin functions `list`,`append`,`put` and `get`have to be used to create and manipulate Lists. 150 | 151 | >>> let l = list(1, 2, 3) 152 | >>> print(l) 153 | [1, 2, 3] /*a list of the number 1 and the number 2 and the number 3*/ 154 | >>> append(l, "world") 155 | >>> print(l) 156 | [1, 2, 3, world] /*a list of the number 1 and the number 2 and the number 3 and the string "world"*/ 157 | >>> print(get(l, 2)) 158 | 3 /*the number 3*/ 159 | >>> put(l, 0, "hallo") 160 | >>> print(l) 161 | [hallo, 2, 3, world] /*a list of the string "hallo" and the number 2 and the number 3 and the string "world"*/ 162 | 163 | ### Syntax 164 | The Syntax is Expression based. 165 | 166 | #### Literals 167 | | Type | Examples | 168 | |--|--| 169 | | `Comment`| `/* this is a comment */` 170 | | `String` | `"hello"` `"with\n escape \" chars"` | 171 | | `Number` | `1`, `-1.0`, `42.5` | 172 | | `Bool`| `True`, `False` | 173 | | `Unit`| `()`| 174 | 175 | #### Grouping 176 | `(a + b) * c` 177 | 178 | #### Blocks 179 | `{ print("hello"); print("world") }` 180 | 181 | Multiple expressions can be grouped with curly braces. Expressions are separated with semicolons. The semicolon after the last Expression is optional. Blocks evaluate to the value of their last expression. 182 | 183 | #### Functions 184 | ##### Definition 185 | let f = fn (arg) { 186 | print(arg); 187 | }; 188 | 189 | Functions can be defined with the keyword `fn`followed by a parenthesized list of parameters and the function body. The braces are optional, when the body is a single expression. All functions are anonymous and first-class. 190 | ##### Calling a function 191 | `print("hello")` 192 | 193 | Nothing special here. 194 | 195 | #### Variables 196 | ##### Declaration 197 | `let x = 42` 198 | 199 | Variables are declared with the keyword `let` and must be initialized. Variables are lexicaly scoped. Declarations evaluate to the assigned value. 200 | 201 | ##### Assignment 202 | `x = 100` 203 | 204 | Assignments evaluate to the assigned value. 205 | 206 | #### Unary Expressions 207 | `-a` `!b` 208 | 209 | Nothing unusual. 210 | 211 | #### Binary Expressions 212 | `a + b` 213 | 214 | Operators ordered by decreasing precedence: 215 | | Operators | Examples | 216 | |--|--| 217 | | `?` | `x?` | 218 | | `*`, `-`, `%` | `x * y`, `x % 5` | 219 | | `+`, `-` | `x + y` | 220 | | `==`, `<`, `>` | `x < y` | 221 | | `&` | `x & y` | 222 | | `|` | `x | y` | 223 | 224 | The only unusual operator is the *explain* operator (`?`). See **First Class Comments** for more details. 225 | The logic operators `&`and `|`are not short circuiting. 226 | 227 | 228 | #### Control Flow Expressions 229 | ##### If 230 | `if (condition) { "true" } else { "false" }` 231 | 232 | Parenthesis around the condition are mandatory. Braces around single expressions and the `else` branch are optional. If expressions evaluate to the value of the taken branch. 233 | ##### While 234 | `while (condition) { do_something() }` 235 | 236 | Parenthesis around the condition are mandatory. The Braces cant be omitted, if the body is a single expression. While loops evaluate to the value of the loop body during the last iteration. 237 | 238 | ### Builtin Functions 239 | 240 | | Function | Description | 241 | |--|--| 242 | | `print(args...)` | Print any number of values. When called with a single argument, the arguments comment is printed as well | 243 | |`input()` | Read single line and return it as a string. | 244 | | `str(value)` | Convert value to string.| 245 | | `cmnt(value)` | Convert value to comment.| 246 | | `num(value)` | Convert string to number. Returns `()`if the conversion fails.| 247 | | `list(args...)` | Create list containing the arguments. | 248 | | `append(list, value)`| Append value to list | 249 | | `put(list, index, value)`|Put value into list | 250 | | `get(list, index)`| Get value out of list | 251 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /environment.py: -------------------------------------------------------------------------------- 1 | 2 | class Environment: 3 | def __init__(self, parent=None): 4 | self.bindings = {} 5 | self.parent = parent 6 | 7 | def put(self, name, value, depth=None): 8 | if depth is None: 9 | depth = name.binding_depth 10 | 11 | if depth == 0: 12 | self.bindings[name.name] = value 13 | else: 14 | self.parent.put(name, value, depth - 1) 15 | 16 | def get(self, name, depth=None): 17 | if depth is None: 18 | depth = name.binding_depth 19 | 20 | if depth == 0: 21 | if name.name in self.bindings: 22 | return self.bindings[name.name] 23 | return None 24 | 25 | return self.parent.get(name, depth - 1) 26 | 27 | -------------------------------------------------------------------------------- /examples/99bottles.pe: -------------------------------------------------------------------------------- 1 | /* when the body of a function is a single expression, the braces can be ommited */; 2 | let bottles = fn(n) 3 | if (n == 0) 4 | "no more bottles" 5 | else if (n == 1) 6 | "one bottle" 7 | else 8 | str(n) + " bottles"; 9 | 10 | let print_verse = fn(i) { 11 | let n = 99 - i; 12 | print(bottles(n) + " of beer on the wall" {/* the first line of verse number */ + cmnt(i)}); 13 | print(bottles(n) + " of beer" {/* the second line of verse number */ + cmnt(i)}); 14 | if (n > 0) { 15 | print("take one down, pass it around" {/* the third line of verse number */ + cmnt(i)}); 16 | print(bottles(n - 1) + " of beer on the wall" {/* the fouth line of verse number */ + cmnt(i)}); 17 | }; 18 | print(); 19 | }; 20 | 21 | let i = 0; 22 | while (i < 99) { 23 | print_verse(i); 24 | i = i + 1; 25 | }; 26 | -------------------------------------------------------------------------------- /examples/assignments.pe: -------------------------------------------------------------------------------- 1 | let str = "test" /* the string test */; 2 | let i = 43 /* an integer */; 3 | let u = () /* the unit */; 4 | 5 | let i2 = i /* the same integer */; 6 | 7 | let j = i2 /* the fourth integer variable */; 8 | let i = i + 1 /* the next index */; 9 | -------------------------------------------------------------------------------- /examples/erroneus/max_recursion.pe: -------------------------------------------------------------------------------- 1 | let f = fn() { f() }; 2 | f(); -------------------------------------------------------------------------------- /examples/erroneus/type_err_in_fn.pe: -------------------------------------------------------------------------------- 1 | let fact = fn(n) { 2 | if (n == 0) { 3 | 1 4 | } else { 5 | /* lets add a helpfull comment to the return value */; 6 | (fact(n - 1) * n - " ") { /* the factorial of */ + n? }; 7 | } 8 | }; 9 | 10 | let h = 4 /*the number of hours i've slept*/; 11 | 12 | print(fact(h)); 13 | 14 | -------------------------------------------------------------------------------- /examples/erroneus/type_error.pe: -------------------------------------------------------------------------------- 1 | (4 + 2 | 2 - 2) + 3 | "cant add numbers and strings :(" 4 | + "another string"; -------------------------------------------------------------------------------- /examples/erroneus/unexpected_char.pe: -------------------------------------------------------------------------------- 1 | t + $§; -------------------------------------------------------------------------------- /examples/erroneus/unexpected_token.pe: -------------------------------------------------------------------------------- 1 | 4 + + 4; -------------------------------------------------------------------------------- /examples/erroneus/uninizialized_var.pe: -------------------------------------------------------------------------------- 1 | let x = x; 2 | -------------------------------------------------------------------------------- /examples/fact.pe: -------------------------------------------------------------------------------- 1 | let fact = fn(n) { 2 | if (n == 0) { 3 | 1 4 | } else { 5 | /* lets add a helpfull comment to the return value */; 6 | (fact(n - 1) * n) { /* the factorial of */ + n? }; 7 | } 8 | }; 9 | 10 | let h = 4 /*the number of hours i've slept*/; 11 | 12 | print(fact(h)); 13 | 14 | -------------------------------------------------------------------------------- /examples/fact_auto_comment.pe: -------------------------------------------------------------------------------- 1 | let fact = fn(n) { 2 | if (n == 0) { 3 | 1 4 | } else { 5 | (fact(n - 1) * n); 6 | } 7 | }; 8 | 9 | print(fact(2)); 10 | 11 | -------------------------------------------------------------------------------- /examples/fizzbuzz.pe: -------------------------------------------------------------------------------- 1 | let i = 1 /* counter */; 2 | while (i < 100) { 3 | if (i % 3 == 0 & i % 5 == 0) { 4 | print("fizzbuzz"); 5 | } else if (i % 3 == 0) { 6 | print("fizz"); 7 | } else if (i % 5 == 0) { 8 | print("buzz"); 9 | } else { 10 | print(i); 11 | }; 12 | i = i + 1 /*incremented counter*/; 13 | } -------------------------------------------------------------------------------- /examples/hello_world.pe: -------------------------------------------------------------------------------- 1 | print("Hello World") /* prints hello world */; -------------------------------------------------------------------------------- /examples/input.pe: -------------------------------------------------------------------------------- 1 | let greet = fn(name, age) { 2 | print("Hello " + name + "!"); 3 | print("Next year you will be " + str(age + 1) + " years old!"); 4 | }; 5 | 6 | print("please enter your name:"); 7 | let name = input() /*the users name*/; 8 | print("please enter your age"); 9 | let age = (); 10 | while (age == ()) { 11 | age = num(input()) /*the users age*/; 12 | if (age == ()) { 13 | print("that was not a valid number!") 14 | }; 15 | }; 16 | greet(name, age); 17 | -------------------------------------------------------------------------------- /examples/lexical_scope.pe: -------------------------------------------------------------------------------- 1 | let a = "global"; 2 | { 3 | let showA = fn() { 4 | print(a); 5 | }; 6 | 7 | print(a) /* prints "global" */; 8 | showA() /* prints "global" */; 9 | let a = "local"; 10 | showA() /* prints "global" */; 11 | print(a) /* prints "local" */; 12 | }; -------------------------------------------------------------------------------- /examples/list.pe: -------------------------------------------------------------------------------- 1 | let l = list(); 2 | print(l); 3 | append(l, 42.0); 4 | print(l); 5 | 6 | let l2 = list(1, 2, 3); 7 | print(l2); 8 | print(get(l2, 2)); 9 | put(l2, 0, 42); 10 | print(l2); -------------------------------------------------------------------------------- /examples/meta_comments.pe: -------------------------------------------------------------------------------- 1 | let a = "a"; 2 | let x = a /* comment */ /* meta-comment */ /* meta-meta-comment */; 3 | 4 | print(x); 5 | print("the comment of x is:", x?); 6 | print("the comment of the comment of x is:", x??); 7 | print("the comment of the comment of the comment of x is:", x???); 8 | -------------------------------------------------------------------------------- /examples/test.pe: -------------------------------------------------------------------------------- 1 | print(print == False); 2 | print(print == print); 3 | print(print == str); 4 | print(5 == ()); 5 | print(cmnt(5)); -------------------------------------------------------------------------------- /examples/tictactoe.pe: -------------------------------------------------------------------------------- 1 | /* 2 | * Tic-Tac-Toe 3 | * Translated from https://rosettacode.org/wiki/Tic-tac-toe#Python 4 | */; 5 | 6 | let board = list(1, 2, 3, 4, 5, 6, 7, 8, 9); 7 | 8 | let wins = list( 9 | list(0, 1, 2), 10 | list(3, 4, 5), 11 | list(6, 7, 8), 12 | list(0, 3, 6), 13 | list(1, 4, 7), 14 | list(2, 5, 8), 15 | list(0, 4, 8), 16 | list(2, 4, 6)); 17 | 18 | let print_board = fn() { 19 | print(get(board, 0), get(board, 1), get(board, 2)); 20 | print(get(board, 3), get(board, 4), get(board, 5)); 21 | print(get(board, 6), get(board, 7), get(board, 8)); 22 | }; 23 | 24 | let score = fn() { 25 | let i = 0; 26 | let not_done = True; 27 | while (i < len(wins) & not_done) { 28 | let w = get(wins, i); 29 | i = i + 1; 30 | let c = get(board, get(w, 0)); 31 | if (c == get(board, get(w, 1)) & c == get(board, get(w, 2))) { 32 | not_done = False; 33 | True 34 | } else { 35 | False 36 | }; 37 | } 38 | }; 39 | 40 | let finished = fn() { 41 | let i = 0; 42 | let marks = 0; 43 | while (i < len(board)) { 44 | let c = get(board, i); 45 | if (c == "X" | c == "O") { 46 | marks = marks + 1; 47 | }; 48 | i = i + 1; 49 | }; 50 | marks == 9 51 | }; 52 | 53 | let spaces = fn() { 54 | let spaces = list(); 55 | let i = 0; 56 | while (i < len(board)) { 57 | let c = get(board, i); 58 | if (c == "X" | c == "O") { 59 | } else { 60 | append(spaces, i) 61 | }; 62 | i = i + 1; 63 | }; 64 | spaces /* the list of the free spaces on the board */ 65 | }; 66 | 67 | let my_turn = fn() { 68 | let options = spaces(); 69 | let choice = get(options, randint(0, len(options) - 1)); 70 | print("I go at index " + str(choice + 1)); 71 | put(board, choice, "O"); 72 | }; 73 | 74 | let your_turn = fn() { 75 | let not_done = True; 76 | while (not_done) { 77 | print("Your turn. Input the index of where you wish to place your mark" /* prompt */); 78 | let choice = num(input()); 79 | if (choice == ()) { 80 | print("sorry I did not undestand that" /* error message */); 81 | } else if (choice < 1 | choice > 9) { 82 | print("that is not on the board" /* error message */); 83 | } else if (get(board, choice - 1) == "X" | get(board, choice - 1) == "O") { 84 | print("that spot is already taken" /* error message */); 85 | } else { 86 | put(board, choice - 1, "X"); 87 | not_done = False; 88 | }; 89 | }; 90 | }; 91 | 92 | let not_done = True; 93 | print_board(); 94 | while (not_done) { 95 | your_turn(); 96 | print_board(); 97 | if (score()) { 98 | print("A strange game. The only winning move is not to play." /* a cool movie reference */); 99 | not_done = False; 100 | } else if (finished()) { 101 | print("It's a draw!"); 102 | not_done = False; 103 | } else { 104 | my_turn(); 105 | print_board(); 106 | if (score()) { 107 | print("I win!" /* victory message */); 108 | not_done = False; 109 | }; 110 | }; 111 | }; -------------------------------------------------------------------------------- /examples/unary.pe: -------------------------------------------------------------------------------- 1 | let x = 1; 2 | print(-x); 3 | print(!False); -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | def get_context(location, text): 2 | lines = text.split('\n') 3 | if location.line == location.end_line: 4 | line = lines[location.line - 1] 5 | marker = ' ' * (location.column - 1) + '^' + \ 6 | '~' * (location.end_column - location.column - 1) 7 | return line + '\n' + marker 8 | else: 9 | first_line = lines[location.line - 1] 10 | last_line = lines[location.end_line - 1] 11 | marker = ' ' * (location.column - 1) + '^' + \ 12 | '~' * (len(first_line) - location.column) 13 | end_marker = '~' * (location.end_column - 1) 14 | msg = first_line + '\n' + marker + '\n' 15 | if location.end_line != location.line + 1: 16 | msg += '...\n' 17 | msg += last_line + '\n' + end_marker 18 | return msg 19 | 20 | 21 | def format_backtrace(backtrace, text): 22 | bt = "Backtrace (most recent call last):\n\n" 23 | for loc in backtrace: 24 | bt += f"{loc.filename}:{loc.line}:{loc.column}\n" 25 | bt += get_context(loc, text) + '\n' 26 | return bt 27 | 28 | 29 | class JlException(Exception): 30 | def __init__(self, backtrace=None, location=None): 31 | self.backtrace = backtrace or [] 32 | if location is not None: 33 | self.backtrace = self.backtrace + [location] 34 | 35 | def get_backtrace(self, text): 36 | return format_backtrace(self.backtrace, text) + '\n' + str(self) 37 | 38 | 39 | class UnboundVariable(JlException): 40 | def __init__(self, name): 41 | super().__init__([], name.location) 42 | self.name = name 43 | 44 | def __str__(self): 45 | return f"unbound variable {self.name.name}" 46 | 47 | 48 | class UninizializedVariable(JlException): 49 | def __init__(self, bt, name): 50 | super().__init__(bt, name.location) 51 | self.name = name 52 | 53 | def __str__(self): 54 | return f"error: variable {self.name.name} was accessed before it was fully initialized" 55 | 56 | 57 | class JlTypeError(JlException): 58 | def __init__(self, msg, bt=None, expr=None): 59 | super().__init__(bt, expr) 60 | self.msg = msg 61 | 62 | def __str__(self): 63 | return f"type error: {self.msg}" 64 | -------------------------------------------------------------------------------- /interpreter.py: -------------------------------------------------------------------------------- 1 | import jlast 2 | from jltypes import * 3 | from copy import copy 4 | from environment import Environment 5 | from prelude import prelude 6 | from exceptions import * 7 | 8 | class Interpreter(jlast.AstVisitor): 9 | def __init__(self): 10 | super().__init__() 11 | self.environment = Environment(prelude) 12 | self.backtrace = [] 13 | 14 | def eval_with_env(self, expr, env): 15 | saved_env = self.environment 16 | self.environment = env 17 | value = self.visit(expr) 18 | self.environment = saved_env 19 | return value 20 | 21 | def clear_backtrace(self): 22 | self.backtrace = [] 23 | 24 | def visit_program(self, b): 25 | for stmt in b.exprs: 26 | value = self.visit(stmt) 27 | return value 28 | 29 | def visit_block(self, b): 30 | saved_env = self.environment 31 | self.environment = Environment(saved_env) 32 | value = JlUnit(); 33 | for stmt in b.exprs: 34 | value = self.visit(stmt) 35 | self.environment = saved_env 36 | return value 37 | 38 | def visit_commented_expr(self, e): 39 | value = self.visit(e.expr) 40 | comment = self.visit(e.comment) 41 | if not isinstance(comment, JlComment): 42 | raise JlTypeError(f"type {type(comment).__name__} can not be used to explain values", 43 | self.backtrace, e.location) 44 | value = copy(value) 45 | value.set_comment(comment) 46 | return value 47 | 48 | def visit_assignment(self, a): 49 | value = self.visit(a.expr) 50 | self.environment.put(a.name, value) 51 | return value 52 | 53 | def visit_declaration(self, d): 54 | value = self.visit(d.expr) 55 | self.environment.put(d.name, value) 56 | return value 57 | 58 | def visit_literal(self, l): 59 | return l.value 60 | 61 | def visit_name(self, n): 62 | value = self.environment.get(n) 63 | if value is None: 64 | raise UninizializedVariable(self.backtrace, n) 65 | return value 66 | 67 | def visit_bin_expr(self, e): 68 | lhs = self.visit(e.lhs) 69 | rhs = self.visit(e.rhs) 70 | try: 71 | if e.op == '&': 72 | return lhs & rhs 73 | if e.op == '|': 74 | return lhs | rhs 75 | if e.op == '+': 76 | return lhs + rhs 77 | if e.op == '-': 78 | return lhs - rhs 79 | if e.op == '*': 80 | return lhs * rhs 81 | if e.op == '*': 82 | return lhs * rhs 83 | if e.op == '/': 84 | return lhs / rhs 85 | if e.op == '%': 86 | return lhs % rhs 87 | if e.op == '==': 88 | return lhs == rhs 89 | if e.op == '<': 90 | return lhs < rhs 91 | if e.op == '>': 92 | return lhs > rhs 93 | assert False 94 | except TypeError: 95 | raise JlTypeError(f"`{e.op}` not possible for types {type(lhs).__name__} and {type(rhs).__name__}", 96 | self.backtrace, e.location) 97 | 98 | def visit_unary_expr(self, e): 99 | expr = self.visit(e.expr) 100 | try: 101 | if e.op == '!': 102 | return expr.not_() 103 | if e.op == '-': 104 | return -expr 105 | assert False 106 | except TypeError: 107 | raise JlTypeError(f"`{e.op}` not possible for type {type(expr).__name__}", 108 | self.backtrace, e.location) 109 | 110 | 111 | def visit_and_expr(self, e): 112 | return self.visit(e.lhs) & self.visit(e.rhs) 113 | 114 | def visit_or_expr(self, e): 115 | lhs = self.visit(e.lhs) 116 | if not lhs.value: 117 | return self.visit(e.rhs) 118 | return lhs 119 | 120 | def visit_call(self, c): 121 | f = self.visit(c.f) 122 | args = list(map(self.visit, c.args)) 123 | if not isinstance(f, JlCallable): 124 | raise JlTypeError(c.location, f"{type(f).__name__} is not callable", 125 | self.backtrace, c.location) 126 | arity = f.get_arity() 127 | if arity is not None and len(args) != arity: 128 | raise JlTypeError(f"wrong number of arguments", 129 | self.backtrace, c.location) 130 | self.backtrace.append(c.location) 131 | try: 132 | r = f.call(self, args) 133 | except JlException as e: 134 | if len(e.backtrace) == 0: 135 | e.backtrace = self.backtrace 136 | raise e 137 | 138 | self.backtrace.pop() 139 | if r is None: 140 | return JlUnit() 141 | else: 142 | return r 143 | 144 | def visit_fn_expr(self, f): 145 | return JlClosure(self.environment, f.params, f.body) 146 | 147 | def visit_explain_expr(self, c): 148 | return self.visit(c.expr).get_comment() 149 | 150 | def visit_while_expr(self, e): 151 | value = JlUnit() 152 | while self.visit(e.cond).value: 153 | value = self.visit(e.body) 154 | return value 155 | 156 | def visit_if_expr(self, e): 157 | if self.visit(e.cond).value: 158 | return self.visit(e.then_body) 159 | elif e.else_body is not None: 160 | return self.visit(e.else_body) 161 | return JlUnit() 162 | -------------------------------------------------------------------------------- /jlast.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from lark import ast_utils, visitors, Token 3 | from typing import List 4 | from jltypes import * 5 | 6 | @dataclass 7 | class SourceLocation: 8 | filename: str 9 | line: int 10 | column: int 11 | end_line: int 12 | end_column: int 13 | 14 | @classmethod 15 | def from_tree(self, tree, filename, line_offset=0): 16 | return SourceLocation( 17 | filename, 18 | tree.line + line_offset, 19 | tree.column, 20 | tree.end_line + line_offset, 21 | tree.end_column) 22 | 23 | 24 | @dataclass 25 | class Expr: 26 | location: SourceLocation 27 | 28 | 29 | @dataclass 30 | class CommentedExpr(Expr): 31 | expr: Expr 32 | comment: Expr 33 | 34 | def accept(self, visitor): 35 | return visitor.visit_commented_expr(self) 36 | 37 | 38 | @dataclass 39 | class BinExpr(Expr): 40 | lhs: Expr 41 | op: Token 42 | rhs: Expr 43 | 44 | def accept(self, visitor): 45 | return visitor.visit_bin_expr(self) 46 | 47 | @dataclass 48 | class UnaryExpr(Expr): 49 | op: Token 50 | expr: Expr 51 | 52 | def accept(self, visitor): 53 | return visitor.visit_unary_expr(self) 54 | 55 | @dataclass 56 | class Literal(Expr): 57 | value: object # TODO: JlObject 58 | def accept(self, visitor): 59 | return visitor.visit_literal(self) 60 | 61 | 62 | @dataclass 63 | class Name(Expr): 64 | name: str 65 | binding_depth: int = None 66 | 67 | def accept(self, visitor): 68 | return visitor.visit_name(self) 69 | 70 | 71 | @dataclass 72 | class Assignment(Expr): 73 | name: Name 74 | expr: Expr 75 | 76 | def accept(self, visitor): 77 | return visitor.visit_assignment(self) 78 | 79 | 80 | @dataclass 81 | class Declaration(Expr): 82 | name: Name 83 | expr: Expr 84 | 85 | def accept(self, visitor): 86 | return visitor.visit_declaration(self) 87 | 88 | 89 | @dataclass 90 | class IfExpr(Expr): 91 | cond: Expr 92 | then_body: Expr 93 | else_body: Expr = None 94 | 95 | def accept(self, visitor): 96 | return visitor.visit_if_expr(self) 97 | 98 | 99 | @dataclass 100 | class WhileExpr(Expr): 101 | cond: Expr 102 | body: Expr 103 | 104 | 105 | def accept(self, visitor): 106 | return visitor.visit_while_expr(self) 107 | 108 | 109 | @dataclass 110 | class CallExpr(Expr): 111 | f: Expr 112 | args: List[Expr] 113 | 114 | def accept(self, visitor): 115 | return visitor.visit_call(self) 116 | 117 | 118 | @dataclass 119 | class FnExpr(Expr): 120 | params: List[Name] 121 | body: Expr 122 | 123 | def accept(self, visitor): 124 | return visitor.visit_fn_expr(self) 125 | 126 | 127 | @dataclass 128 | class ExplainExpr(Expr): 129 | expr: Expr 130 | 131 | def accept(self, visitor): 132 | return visitor.visit_explain_expr(self) 133 | 134 | 135 | @dataclass 136 | class Block(Expr): 137 | exprs: List[Expr] 138 | 139 | def accept(self, visitor): 140 | return visitor.visit_block(self) 141 | 142 | 143 | @dataclass 144 | class Program(Expr): 145 | exprs: List[Expr] 146 | 147 | def accept(self, visitor): 148 | return visitor.visit_program(self) 149 | 150 | 151 | class AstVisitor: 152 | def visit(self, ast): 153 | return ast.accept(self) 154 | 155 | 156 | class AstPrinter(AstVisitor): 157 | def __init__(self): 158 | self.indent = 0 159 | 160 | def print_indent(self): 161 | print(" |" * self.indent, end='') 162 | 163 | def visit_program(self, block): 164 | self.print_indent() 165 | print('Program') 166 | self.indent += 1 167 | for e in block.exprs: 168 | self.visit(e) 169 | self.indent -= 1 170 | 171 | def visit_block(self, block): 172 | self.print_indent() 173 | print('Block') 174 | self.indent += 1 175 | for e in block.exprs: 176 | self.visit(e) 177 | self.indent -= 1 178 | 179 | def visit_literal(self, lit): 180 | self.print_indent() 181 | print(repr(lit.value)) 182 | 183 | def visit_assignment(self, a): 184 | self.print_indent() 185 | print("Assignment") 186 | self.visit(a.name) 187 | self.indent += 1 188 | self.visit(a.expr) 189 | self.indent -= 1 190 | 191 | def visit_declaration(self, a): 192 | self.print_indent() 193 | print("Declaration") 194 | self.visit(a.name) 195 | self.indent += 1 196 | self.visit(a.expr) 197 | self.indent -= 1 198 | 199 | def visit_name(self, a): 200 | self.print_indent() 201 | print(f"<{a.name} {a.binding_depth}>") 202 | 203 | def visit_commented_expr(self, e): 204 | self.print_indent() 205 | print("Commented Expr") 206 | self.indent += 1 207 | self.visit(e.expr) 208 | self.visit(e.comment) 209 | self.indent -= 1 210 | 211 | def visit_while_expr(self, e): 212 | self.print_indent() 213 | print("While") 214 | self.indent += 1 215 | self.visit(e.cond) 216 | self.visit(e.body) 217 | self.indent -= 1 218 | 219 | def visit_bin_expr(self, e): 220 | self.print_indent() 221 | print("BinExpr", e.op) 222 | self.indent += 1 223 | self.visit(e.lhs) 224 | self.visit(e.rhs) 225 | self.indent -= 1 226 | 227 | def visit_unary_expr(self, e): 228 | self.print_indent() 229 | print("Unary", e.op) 230 | self.indent += 1 231 | self.visit(e.expr) 232 | self.indent -= 1 233 | 234 | def visit_if_expr(self, e): 235 | self.print_indent() 236 | print("If") 237 | self.indent += 1 238 | self.visit(e.cond) 239 | self.visit(e.then_body) 240 | if e.else_body is not None: 241 | self.visit(e.else_body) 242 | self.indent -= 1 243 | 244 | def visit_call(self, e): 245 | self.print_indent() 246 | print("Call") 247 | self.indent += 1 248 | self.visit(e.f) 249 | for a in e.args: 250 | self.visit(a) 251 | self.indent -= 1 252 | 253 | def visit_fn_expr(self, f): 254 | self.print_indent() 255 | print("Function", list(map(lambda n: n.name, f.params))) 256 | self.indent += 1 257 | self.visit(f.body) 258 | self.indent -= 1 259 | 260 | def visit_explain_expr(self, e): 261 | self.print_indent() 262 | print("Explain") 263 | self.indent += 1 264 | self.visit(e.expr) 265 | self.indent -= 1 266 | 267 | 268 | class LiteralTransformer(visitors.Transformer): 269 | def __init__(self, filename, line_offset=0): 270 | super().__init__() 271 | self.filename = filename 272 | self.line_offset = line_offset 273 | 274 | def _source_loc(self, tree): 275 | return SourceLocation.from_tree(tree, self.filename, self.line_offset) 276 | 277 | def COMMENT(self, c): 278 | return Literal(self._source_loc(c), JlComment(c[2:-2])) 279 | 280 | def SIGNED_NUMBER(self, x): 281 | return Literal(self._source_loc(x), JlNumber(float(x))) 282 | 283 | def ESCAPED_STRING(self, s): 284 | return Literal(self._source_loc(s), JlString(s[1:-1])) 285 | 286 | def TRUE(self, t): 287 | return Literal(self._source_loc(t), JlBool(True)) 288 | 289 | def FALSE(self, f): 290 | return Literal(self._source_loc(f), JlBool(False)) 291 | 292 | def unit(self, u): 293 | return Literal(self._source_loc(u[0]), JlUnit()) 294 | 295 | def CNAME(self, n): 296 | return str(n) 297 | 298 | 299 | # would have been nice to use lark.ast_utils for this, but I couldn't figure out how to 300 | # maintain line and column number when using it 301 | class ToAst(visitors.Interpreter): 302 | def __init__(self, filename, line_offset=0): 303 | super().__init__() 304 | self.filename = filename 305 | self.line_offset = line_offset 306 | 307 | def _source_loc(self, tree): 308 | return SourceLocation.from_tree(tree, self.filename, self.line_offset) 309 | 310 | def __default__(self, tree): 311 | assert False 312 | 313 | def program(self, tree): 314 | return Program(self._source_loc(tree), self.visit_children(tree)) 315 | 316 | def block(self, tree): 317 | return Block(self._source_loc(tree), self.visit_children(tree)) 318 | 319 | def commented_expr(self, tree): 320 | return CommentedExpr(self._source_loc(tree), *self.visit_children(tree)) 321 | 322 | def bin_expr(self, tree): 323 | return BinExpr(self._source_loc(tree), 324 | *self.visit_children(tree)) 325 | 326 | def unary_expr(self, tree): 327 | return UnaryExpr(self._source_loc(tree), 328 | *self.visit_children(tree)) 329 | 330 | def name(self, tree): 331 | return Name(self._source_loc(tree), *self.visit_children(tree)) 332 | 333 | def assignment(self, tree): 334 | return Assignment(self._source_loc(tree), *self.visit_children(tree)) 335 | 336 | def declaration(self, tree): 337 | return Declaration(self._source_loc(tree), *self.visit_children(tree)) 338 | 339 | def if_expr(self, tree): 340 | return IfExpr(self._source_loc(tree), *self.visit_children(tree)) 341 | 342 | def while_expr(self, tree): 343 | return WhileExpr(self._source_loc(tree), *self.visit_children(tree)) 344 | 345 | def call_expr(self, tree): 346 | children = self.visit_children(tree) 347 | return CallExpr(self._source_loc(tree), children[0], children[1:]) 348 | 349 | def fn_expr(self, tree): 350 | children = self.visit_children(tree) 351 | return FnExpr(self._source_loc(tree), children[:-1], children[-1]) 352 | 353 | def explain_expr(self, tree): 354 | return ExplainExpr(self._source_loc(tree), *self.visit_children(tree)) 355 | -------------------------------------------------------------------------------- /jltypes.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from environment import Environment 3 | 4 | @dataclass 5 | class Value: 6 | value: object = None 7 | _comment: object = None 8 | 9 | def default_comment(self): 10 | return JlComment("something") 11 | 12 | def get_comment(self): 13 | if self._comment is not None: 14 | return self._comment 15 | return self.default_comment() 16 | 17 | def set_comment(self, comment): 18 | self._comment = comment 19 | 20 | def __str__(self): 21 | return str(self.value) 22 | 23 | def __eq__(self, other): 24 | if type(self) != type(other): 25 | res = False 26 | else: 27 | res = self.value == other.value 28 | return JlBool(res, JlComment(f"{self.get_comment().value} is equal to {other.get_comment().value}")) 29 | 30 | def not_(self): 31 | raise TypeError() 32 | 33 | 34 | @dataclass(eq=False) 35 | class JlComment(Value): 36 | def default_comment(self): 37 | return JlComment(f"a comment") 38 | 39 | def __str__(self): 40 | return '/*' + self.value + '*/' 41 | 42 | def __add__(self, other): 43 | if not isinstance(other, JlComment): 44 | raise TypeError() 45 | return JlComment(self.value + other.value) 46 | 47 | @dataclass(eq=False) 48 | class JlNumber(Value): 49 | def default_comment(self): 50 | return JlComment(f"the number {self}") 51 | 52 | def build_comment(self, name, other): 53 | return JlComment(f"the {name} of {self.get_comment().value} and {other.get_comment().value}") 54 | 55 | def __str__(self): 56 | return f"{self.value:g}" 57 | 58 | def __add__(self, other): 59 | if not isinstance(other, JlNumber): 60 | raise TypeError() 61 | return JlNumber(self.value + other.value, 62 | self.build_comment("sum", other)) 63 | 64 | def __sub__(self, other): 65 | if not isinstance(other, JlNumber): 66 | raise TypeError() 67 | return JlNumber(self.value - other.value, 68 | self.build_comment("difference", other)) 69 | 70 | def __mul__(self, other): 71 | if not isinstance(other, JlNumber): 72 | raise TypeError() 73 | return JlNumber(self.value * other.value, 74 | self.build_comment("product", other)) 75 | 76 | def __truediv__(self, other): 77 | if not isinstance(other, JlNumber): 78 | raise TypeError() 79 | return JlNumber(self.value / other.value, 80 | self.build_comment("quotient", other)) 81 | 82 | def __mod__(self, other): 83 | if not isinstance(other, JlNumber): 84 | raise TypeError() 85 | return JlNumber(self.value % other.value, 86 | self.build_comment("modulus", other)) 87 | 88 | def __lt__(self, other): 89 | if not isinstance(other, JlNumber): 90 | raise TypeError() 91 | return JlBool(self.value < other.value, 92 | JlComment(f"{self.get_comment().value} is less than {other.get_comment().value}")) 93 | 94 | def __gt__(self, other): 95 | if not isinstance(other, JlNumber): 96 | raise TypeError() 97 | return JlBool(self.value > other.value, 98 | JlComment(f"{self.get_comment().value} is greater than {other.get_comment().value}")) 99 | 100 | def __neg__(self): 101 | return JlNumber(-self.value, 102 | JlComment(f"the negative of{self.get_comment().value}")) 103 | 104 | 105 | @dataclass(eq=False) 106 | class JlString(Value): 107 | def default_comment(self): 108 | return JlComment(f"the string \"{self.value}\"") 109 | 110 | def __add__(self, other): 111 | if not isinstance(other, JlString): 112 | raise TypeError() 113 | return JlString(self.value + other.value, 114 | JlComment(f"{self.get_comment().value} concatenated with {other.get_comment().value}")) 115 | 116 | 117 | 118 | @dataclass(eq=False) 119 | class JlUnit(Value): 120 | def default_comment(self): 121 | return JlComment("the unit") 122 | 123 | def __str__(self): 124 | return "()" 125 | 126 | 127 | @dataclass(eq=False) 128 | class JlBool(Value): 129 | def default_comment(self): 130 | return JlComment(f"the boolean \"{self.value}\"") 131 | 132 | def __str__(self): 133 | return str(self.value) 134 | 135 | def __and__(self, other): 136 | if not isinstance(other, JlBool): 137 | raise TypeError() 138 | return JlBool(self.value and other.value, 139 | JlComment(f"{self.get_comment().value} and {other.get_comment().value}")) 140 | 141 | def __or__(self, other): 142 | if not isinstance(other, JlBool): 143 | raise TypeError() 144 | return JlBool(self.value or other.value, 145 | JlComment(f"{self.get_comment().value} or {other.get_comment().value}")) 146 | 147 | def not_(self): 148 | return JlBool(not self.value, 149 | JlComment(f"{self.get_comment().value}, not")) 150 | 151 | class JlCallable(Value): 152 | pass 153 | 154 | 155 | class JlPrimitive(JlCallable): 156 | def __init__(self, callback, arity=None, comment=None): 157 | super().__init__(None, comment) 158 | self.callback = callback 159 | self.arity = arity 160 | 161 | def __eq__(self, other): 162 | return JlBool(self is other, 163 | JlComment(f"{self.get_comment().value} is equal to {other.get_comment().value}")) 164 | def __repr__(self): 165 | return f"JlPrimitive({self.get_comment()})" 166 | 167 | def call(self, interpreter, args): 168 | return self.callback(*args) 169 | 170 | def get_arity(self): 171 | return self.arity 172 | 173 | 174 | class JlClosure(JlCallable): 175 | def __init__(self, environment, params, body, comment=None): 176 | super().__init__(None, comment) 177 | self.environment = environment 178 | self.params = params 179 | self.body = body 180 | 181 | def __str__(self): 182 | return "JlClosure({self.get_comment()})" 183 | 184 | def call(self, interpreter, args): 185 | env = Environment(self.environment) 186 | for p, a in zip(self.params, args): 187 | env.put(p, a, 0) 188 | return interpreter.eval_with_env(self.body, env) 189 | 190 | def get_arity(self): 191 | return len(self.params) 192 | 193 | 194 | class JlList(Value): 195 | def default_comment(self): 196 | if len(self.value) == 0: 197 | return JlComment("an empty list") 198 | vals = " and ".join([v.get_comment().value for v in self.value]) 199 | return JlComment(f"a list of {vals}") 200 | 201 | def __str__(self): 202 | vals = ", ".join(map(str, self.value)) 203 | return "[" + vals + "]" 204 | 205 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import lark 4 | import sys 5 | from dataclasses import dataclass 6 | 7 | grammar = r""" 8 | program : (expr ";")* [expr] 9 | 10 | 11 | COMMENT : /\/\*(\*(?!\/)|[^*])*\*\// 12 | TRUE: "True" 13 | FALSE: "False" 14 | %import common.WS 15 | %import common.SIGNED_NUMBER 16 | %import common.ESCAPED_STRING 17 | %import common.CNAME 18 | %ignore WS 19 | 20 | ?expr : commented_expr 21 | ?commented_expr : or_expr commented_expr 22 | | or_expr 23 | !?or_expr : or_expr "|" and_expr -> bin_expr 24 | | and_expr 25 | !?and_expr : and_expr "&" cmp_expr -> bin_expr 26 | | cmp_expr 27 | !?cmp_expr : cmp_expr ("==" | "<" | ">") mul_expr -> bin_expr 28 | | add_expr 29 | !?add_expr : add_expr ("+" | "-") mul_expr -> bin_expr 30 | | mul_expr 31 | !?mul_expr : mul_expr ("*" | "/" | "%") unary_expr -> bin_expr 32 | | unary_expr 33 | !?unary_expr : ("!" | "-") unary_expr 34 | | prim_expr 35 | ?prim_expr : COMMENT 36 | | TRUE 37 | | FALSE 38 | | SIGNED_NUMBER 39 | | ESCAPED_STRING 40 | | group_expr 41 | | unit 42 | | name 43 | | assignment 44 | | declaration 45 | | if_expr 46 | | while_expr 47 | | call_expr 48 | | fn_expr 49 | | explain_expr 50 | | block 51 | 52 | ?group_expr : "(" expr ")" 53 | !unit : "(" ")" 54 | assignment : name "=" expr 55 | declaration : "let" name "=" expr 56 | explain_expr : prim_expr "?" 57 | name : CNAME 58 | 59 | if_expr : "if" group_expr expr ["else" expr] 60 | while_expr : "while" group_expr expr 61 | call_expr : prim_expr "(" [expr ("," expr)*] ")" 62 | fn_expr : "fn" "(" [name ("," name)*] ")" expr 63 | block : "{" (expr ";")* [expr]"}" 64 | 65 | """ 66 | 67 | parser = lark.Lark(grammar, start='program', parser='lalr', propagate_positions=True) 68 | -------------------------------------------------------------------------------- /pls_explain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import lark 4 | import argparse 5 | import readline 6 | 7 | from parser import parser 8 | from jlast import LiteralTransformer, ToAst, AstPrinter 9 | from interpreter import Interpreter 10 | from resolver import Resolver 11 | from exceptions import JlException, format_backtrace 12 | from jltypes import JlUnit, JlComment 13 | 14 | 15 | def eval_source(filename, interpreter, source, debug=True, full_source=None, line_offset=0): 16 | if full_source is None: 17 | full_source = source 18 | 19 | try: 20 | parse_tree = parser.parse(source) 21 | if debug: 22 | print("Parse Tree:") 23 | print(parse_tree.pretty()) 24 | print() 25 | 26 | tree_with_literals = LiteralTransformer(filename, line_offset).transform(parse_tree) 27 | ast = ToAst(filename, line_offset).visit(tree_with_literals) 28 | Resolver(interpreter.environment).visit(ast) 29 | if debug: 30 | print("Abstract Syntax Tree:") 31 | AstPrinter().visit(ast) 32 | print() 33 | 34 | value = interpreter.visit(ast) 35 | 36 | if debug: 37 | print("Global Environment after Evaluation:") 38 | for k, v in interpreter.environment.bindings.items(): 39 | print(f"{k} = {repr(v)}") 40 | print() 41 | 42 | return value 43 | 44 | except lark.exceptions.UnexpectedToken as e: 45 | print(f"{filename}:{e.line}:{e.column} syntax error: expected one of {e.expected}"), 46 | print(e.get_context(source)) 47 | except lark.exceptions.UnexpectedCharacters as e: 48 | print(f"{filename}:{e.line}:{e.column} syntax error: unexpected characters"), 49 | print(e.get_context(source)) 50 | except lark.exceptions.UnexpectedEOF as e: 51 | print(f"{filename}:{e.line}:{e.column} syntax error: unexpected end of file"), 52 | print(e.get_context(source)) 53 | except JlException as e: 54 | print(e.get_backtrace(full_source)) 55 | except RecursionError: 56 | print(format_backtrace(interpreter.backtrace, full_source)) 57 | print("maximum recursion depth exceded") 58 | 59 | return JlUnit(JlComment(":(")) 60 | 61 | 62 | def run_file(path, debug=False): 63 | with open(path) as f: 64 | source = f.read() 65 | 66 | i = Interpreter() 67 | value = eval_source(path, i, source, debug=debug) 68 | if debug: 69 | print("Program Return Value:") 70 | print(value) 71 | 72 | 73 | def repl(debug=True, quiet=False): 74 | inter = Interpreter() 75 | full_source = "" 76 | line_offset = 0 77 | while True: 78 | try: 79 | source = input('>>> ') 80 | except KeyboardInterrupt: 81 | print() 82 | continue 83 | except EOFError: 84 | print() 85 | break 86 | full_source += source + '\n' 87 | value = eval_source("", inter, source, debug, full_source, line_offset) 88 | if not quiet: 89 | print("->", value, value.get_comment()) 90 | 91 | inter.clear_backtrace() 92 | line_offset += 1 93 | 94 | 95 | if __name__ == "__main__": 96 | argp = argparse.ArgumentParser( 97 | description="A programming language with first-class Comments") 98 | argp.add_argument("file", type=str, nargs='?', 99 | help="PlsExplain program to run") 100 | argp.add_argument("-d", "--debug", default=False, action="store_true", 101 | help="print verbose debug info") 102 | argp.add_argument("-q", "--quiet", default=False, action="store_true", 103 | help="don't print result of expressions when in interactive mode") 104 | args = argp.parse_args() 105 | if args.file is not None: 106 | run_file(args.file, args.debug) 107 | else: 108 | repl(args.debug, args.quiet) 109 | -------------------------------------------------------------------------------- /prelude.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | from environment import Environment 3 | from jltypes import * 4 | from exceptions import JlTypeError 5 | 6 | 7 | def jl_print(*args): 8 | if len(args) == 1: 9 | print(str(args[0]), str(args[0].get_comment())) 10 | else: 11 | print(*map(str, args)) 12 | 13 | 14 | def jl_input(): 15 | return JlString(input(), 16 | JlComment("the user input")) 17 | 18 | 19 | def jl_str(arg): 20 | return JlString(str(arg), 21 | JlComment(f"{arg.get_comment().value} as a string")) 22 | 23 | 24 | def jl_cmnt(arg): 25 | return JlComment(str(arg), 26 | JlComment(f"{arg.get_comment().value} as a comment")) 27 | 28 | 29 | def jl_num(arg): 30 | try: 31 | if isinstance(arg, JlString): 32 | return JlNumber(float(arg.value), 33 | JlComment(f"{arg.get_comment().value} as a number")) 34 | except ValueError: 35 | pass 36 | 37 | return JlUnit() 38 | 39 | 40 | def jl_list(*args): 41 | return JlList(list(args)); 42 | 43 | 44 | def jl_append(list, value): 45 | if not isinstance(list, JlList): 46 | raise JlTypeError("first argument must be a list") 47 | return list.value.append(value) 48 | 49 | 50 | def jl_put(list, index, value): 51 | if not isinstance(list, JlList): 52 | raise JlTypeError("first argument must be a list") 53 | if not isinstance(index, JlNumber): 54 | raise JlTypeError("second argument must be a number") 55 | i = int(index.value) 56 | if i < len(list.value): 57 | list.value[i] = value 58 | return value 59 | 60 | 61 | def jl_get(list, index): 62 | if not isinstance(list, JlList): 63 | raise JlTypeError("first argument must be a list") 64 | if not isinstance(index, JlNumber): 65 | raise JlTypeError("second argument must be a number") 66 | i = int(index.value) 67 | if i < len(list.value): 68 | return list.value[i] 69 | return JlUnit() 70 | 71 | 72 | def jl_len(list): 73 | if not isinstance(list, JlList) and not isinstance(list, JlString): 74 | raise JlTypeError("first argument must be a list or string") 75 | return JlNumber(len(list.value), 76 | JlComment(f"the length of {list.get_comment().value}")) 77 | 78 | 79 | def jl_randint(min, max): 80 | if not isinstance(min, JlNumber): 81 | raise JlTypeError("first argument must be a number") 82 | if not isinstance(max, JlNumber): 83 | raise JlTypeError("second argument must be a number") 84 | return JlNumber(randint(min.value, max.value), 85 | JlComment(f"a random integer between {min} and {max}")) 86 | 87 | 88 | prelude = Environment() 89 | prelude.bindings = { 90 | "print": JlPrimitive(jl_print, None, JlComment("the builtin print function")), 91 | "input": JlPrimitive(jl_input, 0, JlComment("the builtin input function")), 92 | "str": JlPrimitive(jl_str, 1, JlComment("the builtin str function")), 93 | "cmnt": JlPrimitive(jl_cmnt, 1, JlComment("the builtin cmnt function")), 94 | "num": JlPrimitive(jl_num, 1, JlComment("the builtin cmnt function")), 95 | "list": JlPrimitive(jl_list, None, JlComment("the builtin cmnt function")), 96 | "append": JlPrimitive(jl_append, 2, JlComment("the builtin append function")), 97 | "put": JlPrimitive(jl_put, 3, JlComment("the builtin put function")), 98 | "get": JlPrimitive(jl_get, 2, JlComment("the builtin get function")), 99 | "len": JlPrimitive(jl_len, 1, JlComment("the builtin len function")), 100 | "randint": JlPrimitive(jl_randint, 2, JlComment("the builtin randint function")), 101 | } 102 | -------------------------------------------------------------------------------- /resolver.py: -------------------------------------------------------------------------------- 1 | from exceptions import UnboundVariable; 2 | from jlast import AstVisitor 3 | from prelude import prelude 4 | 5 | 6 | class Resolver(AstVisitor): 7 | def __init__(self, env=None): 8 | super().__init__() 9 | self.scopes = [] 10 | if env is None: 11 | self.scopes.append(set(prelude.bindings.keys())) 12 | self.scopes.append(set()) 13 | else: 14 | while env is not None: 15 | self.scopes.insert(0, set(env.bindings.keys())) 16 | env = env.parent 17 | 18 | def begin_scope(self): 19 | self.scopes.append(set()) 20 | 21 | def end_scope(self): 22 | self.scopes.pop(-1) 23 | 24 | def visit_program(self, b): 25 | for stmt in b.exprs: 26 | self.visit(stmt) 27 | 28 | def visit_block(self, b): 29 | self.begin_scope() 30 | for stmt in b.exprs: 31 | self.visit(stmt) 32 | self.end_scope() 33 | 34 | def visit_commented_expr(self, e): 35 | self.visit(e.expr) 36 | self.visit(e.comment) 37 | 38 | def visit_assignment(self, a): 39 | self.visit(a.name) 40 | self.visit(a.expr) 41 | 42 | def visit_declaration(self, a): 43 | self.scopes[-1].add(a.name.name) 44 | self.visit(a.name) 45 | self.visit(a.expr) 46 | 47 | def visit_literal(self, l): 48 | pass 49 | 50 | def visit_name(self, n): 51 | for i in range(len(self.scopes)): 52 | if n.name in self.scopes[-i - 1]: 53 | n.binding_depth = i 54 | return 55 | raise UnboundVariable(n) 56 | 57 | def visit_bin_expr(self, e): 58 | self.visit(e.lhs) 59 | self.visit(e.rhs) 60 | 61 | def visit_unary_expr(self, e): 62 | self.visit(e.expr) 63 | 64 | def visit_and_expr(self, e): 65 | self.visit(e.lhs) 66 | self.visit(e.rhs) 67 | 68 | def visit_or_expr(self, e): 69 | self.visit(e.lhs) 70 | self.visit(e.rhs) 71 | 72 | def visit_call(self, c): 73 | self.visit(c.f) 74 | for a in c.args: 75 | self.visit(a) 76 | 77 | def visit_fn_expr(self, f): 78 | self.begin_scope() 79 | for p in f.params: 80 | self.scopes[-1].add(p.name) 81 | self.visit(f.body) 82 | self.end_scope() 83 | 84 | def visit_explain_expr(self, c): 85 | self.visit(c.expr) 86 | 87 | def visit_while_expr(self, e): 88 | self.visit(e.cond) 89 | self.visit(e.body) 90 | 91 | def visit_if_expr(self, e): 92 | self.visit(e.cond) 93 | self.visit(e.then_body) 94 | if e.else_body is not None: 95 | self.visit(e.else_body) 96 | --------------------------------------------------------------------------------