├── .circleci └── config.yml ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── .gitignore ├── LICENSE ├── README.md ├── build-executable.bat ├── examples ├── basic │ └── hello_world.jk ├── conditionals+loops │ ├── conditionals.jk │ └── fibonacci.jk ├── math │ └── quick_maths.jk └── modules │ ├── app.jk │ └── module.jk ├── jink.ico ├── jink.py ├── jink ├── __init__.py ├── interpreter.py ├── lexer.py ├── optimizer.py ├── parser.py ├── repl.py └── utils │ ├── __init__.py │ ├── classes.py │ ├── evals.py │ ├── func.py │ ├── future_iter.py │ └── names.py ├── requirements.txt ├── setup.py └── tests.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | python: circleci/python@1.4.0 5 | 6 | # https://circleci.com/docs/2.0/configuration-reference/#workflows 7 | workflows: 8 | tests: 9 | jobs: 10 | - run-tests 11 | 12 | jobs: 13 | run-tests: 14 | docker: 15 | - image: cimg/python:3.9.7 16 | steps: 17 | - checkout 18 | - python/install-packages: 19 | pkg-manager: pip 20 | pip-dependency-file: requirements.txt 21 | - run: 22 | name: Run tests 23 | command: python tests.py 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a bug report to help us improve Jink! Before submitting a bug report, 4 | please make sure it's a bug and not a user error. 5 | 6 | --- 7 | 8 | **Describe the problem you're facing:** 9 | Give us a clear and concise description of the bug, in as much detail as possible. 10 | 11 | **To reproduce:** 12 | Provide us steps to reproduce the issue: 13 | 14 | - Go to ... 15 | - Click on ... 16 | - Scroll down to ... 17 | - See an error 18 | 19 | **Expected behaviour:** 20 | Give us a clear and concise description of what you expected to happen. 21 | 22 | **Possible solutions you've considered:** 23 | Any solutions you've considered that may or may not solve the issue?: 24 | 25 | **What platform were you on when this occurred:** 26 | Give details about what you were using when this bug occurred. 27 | Python version, any particular device, browser (if you think a browser had something to do with it), OS, OS version. 28 | 29 | **Additional context:** 30 | Add any other context or screenshots about the bug report here. 31 | Tip: be specific; we'll be able to help you with the problem you're facing more and both benefit from it. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for the Jink project! 4 | 5 | --- 6 | 7 | **Is the feature you're suggesting related to an already-existing bug? Specify:** 8 | Give us a clear and concise description of what the problem is, or more preferably, link to an existing bug report. 9 | 10 | **Describe the feature:** 11 | Give us a clear and concise description of the feature you would like to be added. 12 | 13 | **Describe alternatives you've considered:** 14 | If you have any alternative solutions or features you've considered, describe them here. 15 | 16 | **Screenshots:** 17 | If applicable, add screenshots to help explain the feature you think would be a good addition to this project. 18 | 19 | **Additional context:** 20 | Add any other context or screenshots that you think would help explain the feature here. 21 | Tip: be specific; we'll be able to understand what you are suggesting more and both benefit from it. 22 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # pyenv 51 | .python-version 52 | 53 | # Environments 54 | .env 55 | .venv 56 | env/ 57 | venv/ 58 | ENV/ 59 | env.bak/ 60 | venv.bak/ 61 | 62 | # Other files 63 | .vscode/ 64 | .vs/ 65 | old/ 66 | .gitlab-ci.yml 67 | test.jk 68 | test.py 69 | jink.png 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jacob Noah, jink-lang contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jink (Python Interpreter) 2 | 3 | [![CircleCI](https://img.shields.io/circleci/build/github/jink-lang/jink-py?label=tests)](https://circleci.com/gh/jink-lang/jink-py/tree/master) 4 | [![Discord](https://img.shields.io/discord/365599795886161941?label=Discord)](https://discord.gg/cWzcQz2) 5 | ![License](https://img.shields.io/github/license/jink-lang/jink-py) 6 | ![GitHub contributors](https://img.shields.io/github/contributors-anon/jink-lang/jink-py) 7 | ![GitHub Repo stars](https://img.shields.io/github/stars/jink-lang/jink-py?style=social) 8 | --- 9 | 10 | > A simplistic Jink interpreter built using Python. 11 | 12 | ## Jink 13 | This is the original implementation of the [Jink](https://github.com/jink-lang/jink) programming language. More information can be found at the new repo. 14 | 15 | ## Goal Checklist 16 | 17 | (Not in or by any particular order or specification) 18 | 19 | - [✓] Interpreter & REPL 20 | - [ ] Classes & OOP 21 | - [ ] Arrays 22 | - [½] Modularity & packaging 23 | - [ ] Filesystem read & write 24 | - [ ] Networking 25 | 26 | ## Example 27 | 28 | ```js 29 | fun add(let a, let b) { 30 | return a + b 31 | } 32 | 33 | let c = add(1000, 337) 34 | print(c) // Print 1337 to the console! 35 | ``` 36 | 37 | For more examples, check the [examples](./examples) folder. 38 | 39 | ## Installation 40 | 41 | Assuming you have Python 3.6 or newer, you can get started right away after cloning the project! 42 | 43 | To launch the REPL: 44 | 45 | ```cmd 46 | python jink.py 47 | ``` 48 | 49 | To execute your own files: 50 | 51 | ```cmd 52 | python jink.py C:/path/to/file.jk 53 | ``` 54 | 55 | To execute the example files: 56 | 57 | ```cmd 58 | python jink.py ./examples/01-hello_world.jk 59 | ``` 60 | 61 | ### Building 62 | 63 | #### Prerequisites 64 | 65 | * Python 3.6+ 66 | 67 | #### Windows 68 | 69 | 1. Clone the project. 70 | 2. Run the included `build-executable.bat` file; this will install cx_Freeze and build the executable. 71 | 72 | ```cmd 73 | cd build/exe.win32-3.x 74 | jink.exe C:/path/to/your_file.jk 75 | ``` 76 | 77 | Optionally, you can move the contents of the /build/exe.win32-3.x folder to a folder you've added to your PATH. This will allow you to run Jink via your command line. 78 | 79 | ## Contributing 80 | 81 | I will set up a contribution guide when I can. In the meantime, feel free to provide feedback in any way you see fit. If you do decide to submit a PR, make your decisions as clear as possible and provide as many details as you can. 82 | 83 | ## Acknowledgements 84 | 85 | * Enormous thanks to [king1600](https://github.com/king1600) for helping me to better understand interpreter and compiler design and providing me the resources and support I needed to carry out this project. 86 | 87 | * This project also would not have been possible without the incredible resources on PL implementation at [Mihai Bazon's blog](http://lisperator.net). 88 | 89 | ## License 90 | 91 | This project is distributed under the MIT License - see the [license file](LICENSE) for details. 92 | 93 | Copyright © 2018-2024 Jacob Noah, jink-lang contributors 94 | -------------------------------------------------------------------------------- /build-executable.bat: -------------------------------------------------------------------------------- 1 | pip install cx_Freeze 2 | @REM pip install llvmlite 3 | pip install jsonpickle 4 | python setup.py build_exe 5 | @echo Look in build/ for the exe file. 6 | pause -------------------------------------------------------------------------------- /examples/basic/hello_world.jk: -------------------------------------------------------------------------------- 1 | print("Hello world!") -------------------------------------------------------------------------------- /examples/conditionals+loops/conditionals.jk: -------------------------------------------------------------------------------- 1 | let a = 2 2 | 3 | if (a == 2) { 4 | print("a is two!") 5 | } else { 6 | print("a is not two! D: oh noes") 7 | } 8 | -------------------------------------------------------------------------------- /examples/conditionals+loops/fibonacci.jk: -------------------------------------------------------------------------------- 1 | // Fibonacci sequencer 2 | fun Fibonacci(let number) { 3 | if (number <= 1) return number 4 | return Fibonacci(number - 2) + Fibonacci(number - 1) 5 | } 6 | 7 | print(Fibonacci(10)) 8 | -------------------------------------------------------------------------------- /examples/math/quick_maths.jk: -------------------------------------------------------------------------------- 1 | fun add(let a, let b) return a + b 2 | fun subtract(let a, let b) return a - b 3 | fun multiply(let a, let b) return a * b 4 | fun divide(let a, let b) return a / b 5 | 6 | print( 7 | add(5, 5), 8 | subtract(5, 5), 9 | multiply(5, 5), 10 | divide(5, 5) 11 | ) 12 | -------------------------------------------------------------------------------- /examples/modules/app.jk: -------------------------------------------------------------------------------- 1 | import module.abc; 2 | 3 | abc(); 4 | -------------------------------------------------------------------------------- /examples/modules/module.jk: -------------------------------------------------------------------------------- 1 | fun abc() { 2 | print("ABC"); 3 | } 4 | -------------------------------------------------------------------------------- /jink.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jink-lang/jink-py/db431abaa187d531f6c3be407848295c70ad3ff5/jink.ico -------------------------------------------------------------------------------- /jink.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | from pathlib import Path 5 | from jink import optimizer 6 | from jink.lexer import Lexer 7 | from jink.parser import Parser 8 | from jink.optimizer import Optimizer 9 | from jink.interpreter import Interpreter, Environment 10 | from jink.repl import REPL 11 | # from jink.compiler import Compiler 12 | 13 | help_str = '\n'.join([ 14 | "jink - strongly typed, JavaScript-like programming language.", 15 | "https://www.github.com/jink-lang/jink", 16 | "", 17 | "args:", 18 | " > -v -- verbose; will output AST." # and if compiling, both optimized and unoptimized LLVM IR.", 19 | # " > -c -- compile; will use compiler instead of interpreter." 20 | "", 21 | "usage:", 22 | " > [jink] help -- shows this prompt.", 23 | " > [jink] path/to/file[.jk] -- executes interpreter on file.", 24 | " > [jink] -v path/to/file[.jk] -- executes interpreter on file verbose mode.", 25 | # " > [jink] -c path/to/file[.jk] -- executes compiler on file.", 26 | # " > [jink] -c -v path/to/file[.jk] -- executes compiler on file in verbose mode.", 27 | " > [jink] -- launches interpreted interactive REPL.", 28 | " > [jink] -v -- launches interpreted interactive REPL in verbose mode." 29 | ]) 30 | 31 | if len(sys.argv) >= 1 and sys.argv[0].endswith('.py'): 32 | sys.argv.pop(0) 33 | 34 | verbose = False 35 | to_compile = False 36 | 37 | if '-v' in sys.argv: 38 | sys.argv.remove('-v') 39 | verbose = True 40 | if '-c' in sys.argv: 41 | sys.argv.remove('-c') 42 | to_compile = True 43 | 44 | # Launch REPL 45 | if len(sys.argv) == 0 or (len(sys.argv) == 1 and sys.argv[0] == '-v'): 46 | print("jink REPL - use '[jink] help' for help - type 'exit' to exit.") 47 | repl = REPL(sys.stdin, sys.stdout, verbose=verbose, file_dir=Path('.')) 48 | repl.main_loop() 49 | 50 | elif len(sys.argv) >= 1: 51 | if sys.argv[0] == 'help': 52 | print(help_str) 53 | 54 | else: 55 | path = Path(' '.join(sys.argv)) 56 | path = path.resolve() 57 | 58 | if path.is_dir(): 59 | raise Exception(f"File expected, was given dir: {path}") 60 | 61 | code = path.open().read() 62 | if to_compile: 63 | raise NotImplementedError("Compiler not yet implemented.") 64 | # Compiler()._eval(code, optimize=True, verbose=verbose) 65 | else: 66 | AST = Optimizer().optimize(Parser().parse(Lexer().parse(code), verbose=verbose), verbose=verbose) 67 | env = Environment() 68 | env.add_builtins() 69 | Interpreter().evaluate(AST, env, verbose=verbose, file_dir=path.parent) 70 | 71 | if __name__ == "__main__": 72 | pass 73 | -------------------------------------------------------------------------------- /jink/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jink-lang/jink-py/db431abaa187d531f6c3be407848295c70ad3ff5/jink/__init__.py -------------------------------------------------------------------------------- /jink/interpreter.py: -------------------------------------------------------------------------------- 1 | from jink.lexer import Lexer 2 | from jink.parser import Parser 3 | from jink.optimizer import Optimizer 4 | from jink.utils.classes import * 5 | from jink.utils.evals import * 6 | 7 | TYPES = { 8 | int: 'int', 9 | float: 'float', 10 | str: 'string', 11 | dict: 'obj' 12 | } 13 | 14 | # The interpreter environment 15 | class Environment: 16 | def __init__(self, parent=None, s_type=None, debug=False): 17 | self._id = parent._id + 1 if parent else 0 18 | self.index = {} 19 | self.parent = parent 20 | self.type = s_type 21 | self.debug = debug 22 | 23 | # To define builtin methods - only for use on the top level interpreter environment 24 | def add_builtins(self): 25 | self.def_func('print', lambda scope, args: print('\n'.join([str(x) for x in args]) or 'null')) 26 | self.def_func('string', lambda scope, args: [str(x or 'null') for x in args][0] if len(args) == 1 else [str(x or 'null') for x in args]) 27 | self.def_func('input', lambda scope, args: input(' '.join(args))) 28 | 29 | def extend(self, s_type): 30 | return Environment(self, s_type) 31 | 32 | def find_scope(self, name): 33 | if self.debug: 34 | print(f"Searching for {name} in scopes..") 35 | scope = self 36 | while scope: 37 | if self.debug: 38 | print(f"Searching {scope._id}.. found {list(self.index.keys())}") 39 | if name in scope.index: 40 | return scope 41 | scope = scope.parent 42 | 43 | def get_var(self, name): 44 | scope = self.find_scope(name) 45 | if not scope: 46 | raise Exception(f"{name} is not defined.") 47 | 48 | elif name in scope.index: 49 | return scope.index[name] 50 | 51 | raise Exception(f"{name} is not defined.") 52 | 53 | # Functions override to update values 54 | # from parent scopes in local scope during their lifecycle 55 | def set_var(self, name, value, var_type=None, fn_scoped=False): 56 | scope = self.find_scope(name) 57 | 58 | for py_type, _type in TYPES.items(): 59 | if isinstance(value, py_type): 60 | val_type = _type 61 | 62 | if fn_scoped: 63 | self.index[name] = { 'value': value, 'type': val_type, 'var_type': var_type } 64 | return value 65 | 66 | # Assignments 67 | if scope: 68 | v = scope.get_var(name) 69 | 70 | if var_type != None: 71 | raise Exception(f"{name} is already defined.") 72 | elif v['var_type'] == 'const': 73 | raise Exception(f"Constant {name} is not reassignable.") 74 | 75 | scope.index[name]['value'] = value 76 | scope.index[name]['type'] = val_type 77 | 78 | # Definitions 79 | else: 80 | if not var_type: 81 | raise Exception(f"Expected let or const, got 'null' for {name}.") 82 | self.index[name] = { 'value': value, 'type': val_type, 'var_type': var_type } 83 | 84 | return value 85 | 86 | def def_func(self, name, func): 87 | scope = self.find_scope(name) 88 | if scope: 89 | raise Exception(f"Function '{name}' is already defined!") 90 | 91 | if self.debug: 92 | print(f"Defining {name} in {self._id}") 93 | 94 | self.index[name] = func 95 | return func 96 | 97 | def __str__(self): 98 | return f"{self.parent or 'null'}->{self._id}:{list(self.index.keys())}" 99 | 100 | class Interpreter: 101 | def __init__(self): 102 | self.ast = [] 103 | 104 | def evaluate(self, ast, env, verbose=False, file_dir=None): 105 | self.env = env 106 | self.verbose = verbose 107 | self.dir = file_dir 108 | e = [] 109 | for expr in ast: 110 | evaled = self.evaluate_top(expr) 111 | 112 | # Unpack modules 113 | if isinstance(evaled, list): 114 | for mod_expr in evaled: 115 | e.append(mod_expr) 116 | 117 | else: 118 | e.append(evaled) 119 | return e 120 | 121 | def evaluate_top(self, expr): 122 | if isinstance(expr, IdentLiteral): 123 | if None in (expr.index['type'], expr.index['index']): 124 | return self.env.get_var(expr.name) 125 | 126 | elif expr.index['type'] == 'prop': 127 | var = self.env.get_var(expr.name) 128 | 129 | if var['type'] != 'obj': 130 | raise Exception(f"Variable '{expr.name}' of type {var['type']} does not support indexing") 131 | 132 | obj = var['value'] 133 | if isinstance(expr.index['index'], IdentLiteral): 134 | if expr.index['index'].name not in obj: 135 | raise Exception(f"Object '{expr.name}' does not contain the property '{expr.index['index'].name}'") 136 | 137 | else: 138 | return obj[expr.index['index'].name] 139 | 140 | # TODO: Object methods, classes. 141 | 142 | elif isinstance(expr.index['index'], CallExpression): 143 | return self.call_function(expr.index['index']) 144 | 145 | elif isinstance(expr, (StringLiteral, IntegerLiteral, FloatingPointLiteral)): 146 | return self.unwrap_value(expr) 147 | 148 | elif isinstance(expr, BooleanLiteral): 149 | return True if self.unwrap_value(expr) == 'true' else False 150 | 151 | elif isinstance(expr, Null): 152 | return { 'type': 'null', 'value': 'null' } 153 | 154 | # TODO Properly evaluate unary operators modifying variables 155 | # (e.g. pre and post increment ++i and i++) 156 | elif isinstance(expr, UnaryOperator): 157 | # print(expr.value) 158 | value = self.evaluate_top(expr.value) 159 | return UNOP_EVALS[expr.operator](self.unwrap_value(value)) or 0 160 | 161 | elif isinstance(expr, BinaryOperator): 162 | left, right = self.evaluate_top(expr.left), self.evaluate_top(expr.right) 163 | return BINOP_EVALS[expr.operator](self.unwrap_value(left), self.unwrap_value(right)) or 0 164 | 165 | elif isinstance(expr, Module): 166 | 167 | # Get nested Modules 168 | index = [] 169 | while expr: 170 | index.insert(0, expr.name) 171 | expr = expr.index 172 | 173 | # Relative import 174 | relative = False 175 | if index[0] == '.': 176 | index.pop() 177 | relative = True 178 | 179 | # Pretend like I know what I'm doing 180 | # TODO Standard Library 181 | 182 | try: 183 | if relative: 184 | module = (self.dir / f"{index[0]}.jk").open().read() 185 | else: 186 | # Is Directory 187 | if (self.dir / index[0]).is_dir(): 188 | pass 189 | # Is File 190 | elif (self.dir / f"{index[0]}.jk").is_file(): 191 | module = (self.dir / f"{index[0]}.jk").open().read() 192 | else: 193 | raise Exception(f"Module '{index[0]}' not found at '{self.dir}'.") 194 | except: 195 | raise Exception(f"Failed to import module {index[0]}.") 196 | 197 | lexed = Lexer().parse(module) 198 | parsed = Parser().parse(lexed, self.verbose) 199 | optimized = Optimizer().optimize(parsed, self.verbose) 200 | return self.evaluate(optimized, self.env, self.verbose, self.dir) 201 | 202 | elif isinstance(expr, Assignment): 203 | value = self.evaluate_top(expr.value) 204 | 205 | try: 206 | value = self.unwrap_value(value) 207 | 208 | except KeyError: 209 | pass 210 | return self.env.set_var(expr.ident.name, value if value != None else 'null', expr.type) 211 | 212 | elif isinstance(expr, Conditional): 213 | if hasattr(expr, 'expression') and expr.expression != None: 214 | result = self.evaluate_condition(self.evaluate_top(expr.expression)) 215 | 216 | if result not in ('true', 'false'): 217 | raise Exception("Conditional improperly used.") 218 | 219 | elif result == 'true': 220 | return self.evaluate(expr.body, self.env) 221 | 222 | elif result == 'false' and expr.else_body: 223 | return self.evaluate_top(expr.else_body[0]) 224 | 225 | else: 226 | return 227 | self.evaluate(expr.body, self.env) 228 | 229 | elif isinstance(expr, CallExpression): 230 | return self.call_function(expr) 231 | 232 | elif isinstance(expr, Function): 233 | return self.make_function(expr) 234 | 235 | elif isinstance(expr, Return): 236 | result = self.evaluate_top(expr.value) 237 | return { 'type': 'return', 'value': self.unwrap_value(result) } 238 | 239 | elif isinstance(expr, dict): 240 | return expr 241 | 242 | def evaluate_condition(self, cond): 243 | if cond in ('true', 'false'): 244 | return cond 245 | 246 | elif cond in (True, False): 247 | return 'true' if True else 'false' 248 | 249 | elif 'type' in cond: 250 | if cond['type'] == 'null': 251 | return 'false' 252 | 253 | elif cond['type'] != 'bool': 254 | return 'true' 255 | 256 | # Call a function in a new scope 257 | def call_function(self, expr): 258 | scope = self.env.extend(f"call_{expr.name.name}") 259 | func = self.evaluate_top(expr.name) 260 | return func(scope, [self.unwrap_value(self.evaluate_top(arg)) for arg in expr.args]) 261 | 262 | # Make a function 263 | def make_function(self, func): 264 | def function(scope, args): 265 | params = func.params 266 | 267 | # Exception upon overload 268 | if len(args) > len(params): 269 | raise Exception(f"Function '{func.name}' takes {len(params)} arguments but {len(args)} were given.") 270 | 271 | # Apply arguments to this call's scope 272 | # If argument doesn't exist use function default if it exists 273 | i = 0 274 | 275 | for p in params: 276 | default = None 277 | 278 | if p.default: 279 | default = self.unwrap_value(p.default) 280 | 281 | if len(args) > i: 282 | value = args[i] if args[i] not in (None, 'null') else default or 'null' 283 | 284 | else: 285 | value = default if default not in (None, 'null') else 'null' 286 | 287 | if value != None: 288 | try: 289 | scope.set_var(p.name, value, p.type, fn_scoped=True) 290 | except Exception as e: 291 | raise Exception(f"{e}\nException: Improper function parameter or call argument at function '{func.name}'.") 292 | i += 1 293 | 294 | # Ensure returning of the correct value 295 | _return = None 296 | for e in func.body: 297 | result = self.evaluate([e], scope)[0] 298 | 299 | if isinstance(result, list): 300 | result = result[0] if len(result) > 0 else [] 301 | 302 | if result and isinstance(result, dict) and result['type'] == 'return': 303 | if isinstance(result['value'], bool): 304 | _return = 'true' if result['value'] == True else 'false' 305 | 306 | elif isinstance(result['value'], (int, float)): 307 | _return = result['value'] if result['value'] != None else 0 308 | 309 | else: 310 | _return = result['value'] 311 | break 312 | 313 | # Step back out of this scope 314 | self.env = self.env.parent 315 | 316 | if _return is None or (isinstance(_return, list) and (_return[0] in (None, 'null') or _return[0]['value'] is None)): 317 | return 'null' 318 | 319 | else: 320 | return _return 321 | 322 | self.env.def_func(func.name, function) 323 | return function 324 | 325 | # Obtain literal values 326 | def unwrap_value(self, v): 327 | if hasattr(v, 'value'): 328 | return v.value 329 | 330 | elif hasattr(v, '__getitem__') and not isinstance(v, (str, list)): 331 | return v['value'] 332 | 333 | else: 334 | return v 335 | -------------------------------------------------------------------------------- /jink/lexer.py: -------------------------------------------------------------------------------- 1 | from jink.utils.names import * 2 | from jink.utils.classes import Token, TokenType 3 | from jink.utils.future_iter import FutureIter 4 | 5 | KEYWORDS = KEYWORDS + TYPES 6 | 7 | OPERATORS = ( 8 | '.', '::', '|', 9 | '=', '!', '?', ':', 10 | '+', '-', '*', '/', '%', '^', 11 | '&', '~', '#', 12 | '>', '<', '>=', '<=', '==', 13 | '!=', '&&', '||', 14 | '++', '--' 15 | ) 16 | 17 | # One lexer boi 18 | class Lexer: 19 | def __init__(self): 20 | self.pos = 0 21 | self.line = 1 22 | self.line_pos = 0 23 | 24 | def parse(self, code): 25 | self.code = FutureIter(code) 26 | self.code_end = len(str(self.code)) - 1 27 | return [token for token in self.parse_tokens()] 28 | 29 | def parse_literal(self, code): 30 | return str([token.smallStr() for token in self.parse(code)]) 31 | 32 | def parse_tokens(self): 33 | while self.code.current is not None: 34 | 35 | # All good, increment positions 36 | self.line_pos += 1 37 | self.pos += 1 38 | 39 | char = self.code._next() 40 | 41 | if char == '\\': 42 | if self.code.current == '\n': 43 | self.line += 1 44 | self.line_pos = 0 45 | self.code._next() 46 | char = self.code.current 47 | continue 48 | 49 | if char.isspace(): 50 | if char == '\n': 51 | self.line_pos = 0 52 | self.line += 1 53 | yield Token(TokenType.NEWLINE, 'newline', self.line, self.pos) 54 | 55 | # Comments 56 | elif char == '/': 57 | if self.code.current in ('/', '*'): 58 | self.process_comment() 59 | else: 60 | yield self.parse_operator(char) 61 | 62 | # Brackets 63 | elif char == '(': 64 | yield Token(TokenType.LPAREN, '(', self.line, self.pos) 65 | elif char == ')': 66 | yield Token(TokenType.RPAREN, ')', self.line, self.pos) 67 | elif char == '[': 68 | yield Token(TokenType.LBRACKET, '[', self.line, self.pos) 69 | elif char == ']': 70 | yield Token(TokenType.RBRACKET, ']', self.line, self.pos) 71 | elif char == '{': 72 | yield Token(TokenType.LBRACE, '{', self.line, self.pos) 73 | elif char == '}': 74 | yield Token(TokenType.RBRACE, '}', self.line, self.pos) 75 | 76 | elif char == ';': 77 | yield Token(TokenType.SEMICOLON, ';', self.line, self.pos) 78 | 79 | elif char == ',': 80 | yield Token(TokenType.COMMA, ',', self.line, self.pos) 81 | 82 | elif char in ("'", '"'): 83 | yield self.parse_string(char) 84 | 85 | elif char.isalpha() or char in ('_', '$'): 86 | yield self.parse_ident(char) 87 | 88 | elif char.isdigit() or (char == '.' and self.code.current.isdigit()): 89 | yield self.parse_number(char) 90 | 91 | elif char in OPERATORS: 92 | yield self.parse_operator(char) 93 | 94 | else: 95 | raise Exception('Invalid character on {0}:{1}\n {2}\n {3}'.format( 96 | self.line, self.line_pos, str(self.code).split('\n')[self.line - 1], f"{' ' * (self.line_pos - 1)}^" 97 | )) 98 | 99 | # Variables are fun, especially when you name them ridiculous things. 100 | def parse_ident(self, char): 101 | ident = char 102 | while self.code.current is not None and (self.code.current.isalnum() or self.code.current == '_'): 103 | ident += self.code.current 104 | self.code._next() 105 | self.line_pos += 1 106 | if ident in KEYWORDS: 107 | return Token(TokenType.KEYWORD, ident, self.line, self.pos) 108 | return Token(TokenType.IDENTIFIER, ident, self.line, self.pos) 109 | 110 | # 2 + 2 = 4 - 1 = 3 111 | def parse_operator(self, operator): 112 | line_start = self.line_pos 113 | start = self.pos 114 | while self.code.current is not None and self.code.current in OPERATORS: 115 | operator += self.code.current 116 | self.line_pos += 1 117 | self.code._next() 118 | if not operator in OPERATORS: 119 | raise Exception('Invalid operator on {0}:{1}\n {2}\n {3}'.format( 120 | self.line, line_start, str(self.code).split('\n')[self.line - 1], f"{' ' * (line_start - 1)}^" 121 | )) 122 | return Token(TokenType.OPERATOR, operator, self.line, start) 123 | 124 | # Yay, I can "Hello world" now! 125 | def parse_string(self, char): 126 | string = '' 127 | end = False 128 | start = self.line_pos 129 | while self.code.current is not None: 130 | 131 | # Ending the string? So soon? Aw. :( 132 | if self.code.current == char: 133 | end = True 134 | self.code._next() 135 | self.line_pos += 1 136 | break 137 | 138 | # Handle escaped characters 139 | if self.code.current == '\\': 140 | self.code._next() 141 | self.line_pos += 1 142 | 143 | # Get escaped character 144 | nxt = self.code._next() 145 | 146 | # Newline is a special case 147 | if nxt == 'n': 148 | string += "\n" 149 | self.line += 1 150 | self.line_pos = 0 151 | 152 | # Add escaped character and move on 153 | else: 154 | string += nxt 155 | self.line_pos += 1 156 | 157 | else: 158 | string += self.code._next() 159 | self.line_pos += 1 160 | 161 | if self.code.current == None and end == False: 162 | raise Exception('A string was not properly enclosed at {0}:{1}\n {2}\n {3}'.format( 163 | self.line, start, str(self.code).split('\n')[self.line - 1], f"{' ' * (start - 1)}^" 164 | )) 165 | 166 | return Token(TokenType.STRING, string, self.line, start) 167 | 168 | # Crunch those numbers. 169 | def parse_number(self, char): 170 | num = char 171 | line_start = self.line_pos 172 | while self.code.current is not None and not self.code.current.isspace() and (self.code.current.isdigit() or self.code.current == '.'): 173 | num += self.code.current 174 | self.code._next() 175 | self.line_pos += 1 176 | 177 | # The heck? 178 | if num.count('.') > 1: 179 | raise Exception('Invalid number at {0}:{1}\n {2}\n {3}'.format( 180 | self.line, line_start, str(self.code).split('\n')[self.line - 1], f"{' ' * (line_start - 1)}^" 181 | )) 182 | else: 183 | return Token(TokenType.NUMBER, num, self.line, self.pos) 184 | 185 | # Do I really need to comment on comments? 186 | def process_comment(self): 187 | cur = self.code.current 188 | # Single-line comment 189 | if self.code.current == '/': 190 | while not self.code.current in ('\r', '\n', None): 191 | self.line_pos += 1 192 | self.code._next() 193 | # Multi-line comment 194 | elif self.code._next() == '*': 195 | while self.code.current is not None and (f"{cur}{self.code.current}" != '*/'): 196 | self.line_pos += 1 197 | if self.code.current in ('\r', '\n'): 198 | self.line_pos = 0 199 | self.line += 1 200 | cur = self.code._next() 201 | 202 | if self.code.current is None or self.code.current is not '/': 203 | raise Exception('A multi-line comment was not closed.') 204 | self.code._next() 205 | -------------------------------------------------------------------------------- /jink/optimizer.py: -------------------------------------------------------------------------------- 1 | from jink.utils.classes import * 2 | from jink.utils.evals import * 3 | 4 | class Optimizer: 5 | def optimize(self, ast, verbose=False): 6 | self.verbose = verbose 7 | optimized = [] 8 | if ast is None: 9 | raise Exception("AST not found") 10 | for expr in ast: 11 | folded = self.const_fold(expr) 12 | if isinstance(folded, list): 13 | for f in folded: 14 | optimized.append(f) 15 | else: 16 | optimized.append(folded) 17 | return optimized 18 | 19 | def const_fold(self, expr): 20 | if isinstance(expr, UnaryOperator): 21 | left = self.const_fold(expr.value) 22 | 23 | if isinstance(left, IntegerLiteral): 24 | return IntegerLiteral(int(UNOP_EVALS[expr.operator](left.value))) 25 | 26 | elif isinstance(left, FloatingPointLiteral): 27 | return FloatingPointLiteral(UNOP_EVALS[expr.operator](left.value)) 28 | 29 | elif isinstance(expr, BinaryOperator): 30 | left, right = self.const_fold(expr.left), self.const_fold(expr.right) 31 | 32 | # Evaluate result of binop 33 | evaled = BINOP_EVALS[expr.operator](left.value, right.value) 34 | 35 | # Return corresponding value 36 | if isinstance(evaled, int): 37 | return IntegerLiteral(evaled) 38 | elif isinstance(evaled, float): 39 | return FloatingPointLiteral(evaled) 40 | 41 | # String concatenation 42 | elif isinstance(left, StringLiteral) and isinstance(right, StringLiteral): 43 | if expr.operator != '+': 44 | raise Exception(f"Only '+' operator can be used for string/string binop.") 45 | return StringLiteral(str(left.value) + str(right.value)) 46 | 47 | # String multiplication 48 | elif isinstance(left, StringLiteral) and isinstance(right, IntegerLiteral): 49 | if expr.operator != '*': 50 | raise Exception(f"Only '*' operator can be used for string/int binop.") 51 | return StringLiteral(str(left.value)*right.value) 52 | 53 | elif isinstance(expr, Assignment): 54 | expr.value = self.const_fold(expr.value) 55 | 56 | elif isinstance(expr, Function): 57 | expr.body = self.const_fold(expr.body) 58 | 59 | elif isinstance(expr, CallExpression): 60 | expr.args = [self.const_fold(e) for e in expr.args] 61 | 62 | return expr 63 | -------------------------------------------------------------------------------- /jink/parser.py: -------------------------------------------------------------------------------- 1 | from jink.utils.names import * 2 | from jink.utils.classes import * 3 | from jink.utils.future_iter import FutureIter 4 | from jink.utils.func import pickle 5 | import json 6 | 7 | PRNTLINE = "\n--------------------------------\n" 8 | 9 | class Parser: 10 | def __init__(self): 11 | self.tokens = None 12 | 13 | def consume(self, item, soft=False): 14 | """Removes expected token, given a type or a tuple of types.""" 15 | current = self.tokens.current 16 | 17 | if not item: 18 | return self.tokens._next() 19 | 20 | # Doesn't error out if the token isn't found 21 | # But removes it if found 22 | if soft: 23 | if isinstance(item, tuple): 24 | if current.type in item: 25 | return self.tokens._next() 26 | elif current.type == item: 27 | return self.tokens._next() 28 | else: 29 | self.tokens._next() 30 | if isinstance(item, tuple): 31 | if current.type not in item: 32 | raise Exception(f"Expected {' or '.join(item)}, got '{current.type}' on line {current.line}.") 33 | else: 34 | return current 35 | else: 36 | # Strings have text that could be used to spoof this check so I account for them here 37 | if current.value == item and current.type != TokenType.STRING: 38 | return current 39 | elif current.type == item: 40 | return current 41 | raise Exception(f"Expected '{item}', got '{current.type}' on line {current.line}.") 42 | 43 | def parse(self, tokens, verbose=False): 44 | self.tokens = FutureIter(tokens) 45 | ast = [] 46 | while self.tokens.current is not None: 47 | if self.tokens.current.type != TokenType.NEWLINE: 48 | ast.append(self.parse_top()) 49 | else: 50 | self.tokens._next() 51 | 52 | if verbose: 53 | print("AST:", PRNTLINE, json.dumps(pickle(ast), indent=2), PRNTLINE) 54 | 55 | return ast 56 | 57 | def parse_literal(self, tokens): 58 | return self.parse(tokens) 59 | 60 | def parse_to_console(self, tokens): 61 | program = self.parse(tokens) 62 | for expr in program: 63 | print(expr) 64 | 65 | def skip_newlines(self, count=-1): 66 | while self.tokens.current != None and self.tokens.current.type == TokenType.NEWLINE and count != 0: 67 | count -= 1 68 | self.tokens._next() 69 | return self.tokens.current 70 | 71 | def parse_top(self): 72 | init = self.tokens.current 73 | 74 | if init == None: 75 | return 76 | 77 | elif init.type != TokenType.KEYWORD: 78 | return self.parse_expr() 79 | 80 | elif init.value == 'import': 81 | # Skip 'import' 82 | self.tokens._next() 83 | return self.parse_module() 84 | 85 | elif init.value in ('let', 'const'): 86 | self.tokens._next() 87 | ident = self.consume(TokenType.IDENTIFIER) 88 | cur = self.tokens.current 89 | 90 | # Assignments 91 | if (cur.type == TokenType.OPERATOR and cur.value == '=') or cur.type in (TokenType.NEWLINE, TokenType.SEMICOLON): 92 | self.tokens._next() 93 | return self.parse_assignment(init.value, ident.value) 94 | 95 | elif init.value == 'fun': 96 | self.tokens._next() 97 | return self.parse_function() 98 | 99 | # Return statements 100 | elif init.value == 'return': 101 | return self.parse_return() 102 | 103 | # Conditionals 104 | elif init.value == 'if': 105 | return self.parse_conditional() 106 | 107 | # Null 108 | elif init.value == 'null': 109 | self.tokens._next() 110 | return Null() 111 | 112 | else: 113 | raise Exception(f"Expected keyword, got '{init.value}' on line {init.line}.") 114 | 115 | def parse_expr(self, precedence=0): 116 | left = self.parse_primary() 117 | current = self.tokens.current 118 | 119 | while current and current.type == TokenType.OPERATOR and self.get_precedence(current) >= precedence: 120 | operator = self.tokens._next() 121 | if operator.value in ('++', '--'): 122 | return UnaryOperator(operator.value + ':post', left) 123 | 124 | next_precedence = self.get_precedence(operator) 125 | if self.is_left_associative(operator): 126 | next_precedence += 1 127 | 128 | right = self.parse_expr(next_precedence) 129 | left = BinaryOperator(operator.value, left, right) 130 | 131 | current = self.tokens.current 132 | 133 | if current and current.type == TokenType.SEMICOLON: 134 | self.consume(TokenType.SEMICOLON) 135 | 136 | return left 137 | 138 | def parse_primary(self): 139 | self.skip_newlines() 140 | current = self.tokens.current 141 | if current == None: return 142 | 143 | if self.is_unary_operator(current): 144 | operator = self.tokens._next() 145 | if operator.value in ('-', '+', '!'): 146 | value = self.parse_primary() 147 | return UnaryOperator(operator.value, value) 148 | value = self.parse_expr(self.get_precedence(operator)) 149 | return UnaryOperator(operator.value, value) 150 | 151 | elif current.value == '(': 152 | self.consume(TokenType.LPAREN) 153 | value = self.parse_expr(0) 154 | self.consume(TokenType.RPAREN) 155 | return value 156 | 157 | elif current.value == '{': 158 | self.consume(TokenType.LBRACE) 159 | obj = self.parse_object() 160 | self.consume(TokenType.RBRACE) 161 | return obj 162 | 163 | elif current.type == TokenType.NUMBER: 164 | current = self.tokens._next() 165 | if current.value.count('.') > 0: 166 | return FloatingPointLiteral(float(current.value)) 167 | return IntegerLiteral(int(current.value)) 168 | 169 | elif current.type == TokenType.STRING: 170 | return StringLiteral(self.tokens._next().value) 171 | 172 | elif current.type == TokenType.IDENTIFIER: 173 | ident = self.tokens._next().value 174 | if self.tokens.current.value == '.': 175 | self.tokens._next() 176 | index = { 'type': 'prop', 'index': self.parse_top() } 177 | return IdentLiteral(ident, index) 178 | elif self.tokens.current.value == '(': 179 | return self.parse_call(ident) 180 | elif self.tokens.current.value == '=': 181 | self.tokens._next() 182 | return self.parse_assignment(None, ident) 183 | else: 184 | return IdentLiteral(ident) 185 | 186 | elif current.type == TokenType.KEYWORD: 187 | keyword = self.tokens._next().value 188 | if keyword in ('true', 'false'): 189 | return BooleanLiteral(keyword) 190 | elif self.tokens.current.value == '(': 191 | return self.parse_call(keyword) 192 | elif keyword == 'null': 193 | return Null() 194 | 195 | raise Exception(f"Expected primary expression, got '{current.value}' on line {current.line}.") 196 | 197 | def is_unary_operator(self, token): 198 | if hasattr(token, 'type'): 199 | if token.type == TokenType.STRING: 200 | return False 201 | return token.value in ('-', '+', '++', '--', '!') 202 | 203 | def is_left_associative(self, token): 204 | if hasattr(token, 'type'): 205 | if token.type == TokenType.STRING: 206 | return False 207 | return token.value not in ('++', '--', '+=', '-=', '=') 208 | 209 | def get_precedence(self, token): 210 | if token.value in ('+', '-'): 211 | return 1 212 | elif token.value in ('*', '/', '%'): 213 | return 2 214 | elif token.value in ('^'): 215 | return 3 216 | else: 217 | return 0 218 | 219 | # TODO Reverse nesting behaviour 220 | def parse_module(self, name=None, index=None): 221 | 222 | if self.tokens.current.type == None: 223 | return Module(name, index) 224 | 225 | if self.tokens.current.type in (TokenType.NEWLINE, TokenType.SEMICOLON): 226 | self.consume((TokenType.NEWLINE, TokenType.SEMICOLON)) 227 | return Module(name, index) 228 | 229 | # Expect package name 230 | if self.tokens.current.type not in (TokenType.IDENTIFIER, TokenType.OPERATOR): 231 | if self.tokens.current.type == TokenType.OPERATOR and self.tokens.current.value != '.': 232 | raise Exception(f"Expected '.' got '{self.tokens.current.value}' on line {self.tokens.current.line}.") 233 | raise Exception(f"Expected package index, got '{self.tokens.current.value}' on line {self.tokens.current.line}.") 234 | 235 | # Store current index and move on 236 | write_index = self.tokens._next() 237 | 238 | module = Module(write_index.value, index) 239 | 240 | if self.tokens.current.type in (TokenType.IDENTIFIER, TokenType.OPERATOR): 241 | return self.parse_module(self.tokens.current.value, Module(write_index.value, index)) 242 | 243 | if self.tokens.current.type in (TokenType.NEWLINE, TokenType.SEMICOLON): 244 | self.consume((TokenType.NEWLINE, TokenType.SEMICOLON)) 245 | 246 | return module 247 | 248 | def parse_assignment(self, var_type, name): 249 | if self.tokens.current.type in (TokenType.NEWLINE, TokenType.SEMICOLON, TokenType.COMMA): 250 | assignment = Assignment(var_type, IdentLiteral(name), None) 251 | elif self.tokens.current.type == TokenType.RPAREN: 252 | assignment = Assignment(var_type, IdentLiteral(name), None) 253 | return assignment 254 | else: 255 | assignment = Assignment(var_type, IdentLiteral(name), self.parse_expr()) 256 | 257 | if self.tokens.current != None and self.tokens.current.type != TokenType.COMMA: 258 | self.consume((TokenType.NEWLINE, TokenType.SEMICOLON)) 259 | 260 | return assignment 261 | 262 | def parse_call(self, func_name): 263 | args = self.parse_args_params('args') 264 | return CallExpression(IdentLiteral(func_name), args) 265 | 266 | def parse_function(self): 267 | ident = self.consume(TokenType.IDENTIFIER) 268 | params = self.parse_args_params('params') 269 | body = self.parse_block() 270 | return Function(ident.value, params, body) 271 | 272 | # Parse function parameters and call arguments 273 | def parse_args_params(self, location): 274 | self.consume(TokenType.LPAREN) 275 | l = [] 276 | 277 | # Function parameters 278 | if location == 'params': 279 | while True and self.tokens.current != None: 280 | if self.tokens.current.value == ')': 281 | self.consume(TokenType.RPAREN) 282 | break 283 | elif self.tokens.current.value == '{': 284 | break 285 | 286 | cur = self.tokens._next() 287 | if cur.type == TokenType.KEYWORD and cur.value in ('let', 'const'): 288 | ident = self.consume(TokenType.IDENTIFIER) 289 | _next = self.tokens.current 290 | 291 | # Close out function params 292 | if _next.type == TokenType.RPAREN: 293 | l.append(FunctionParameter(ident.value, cur.value, None)) 294 | 295 | # Expect comma or colon 296 | # fun test(let a<,> let b<:> 10) {} 297 | elif _next.type in (TokenType.COMMA, TokenType.OPERATOR) and _next.value in (',', ':'): 298 | if _next.value == ':': 299 | self.tokens._next() 300 | default = self.parse_expr() 301 | l.append(FunctionParameter(ident.value, cur.value, default)) 302 | self.tokens._next() 303 | elif _next.value == ',': 304 | l.append(FunctionParameter(ident.value, cur.value, None)) 305 | self.tokens._next() 306 | else: 307 | raise Exception(f"Expected comma or colon, got '{cur.value}' on line {cur.line}.") 308 | else: 309 | raise Exception(f"Expected let or const, got '{cur.value}' on line {cur.line}.") 310 | 311 | # Call arguments 312 | else: 313 | while True and self.tokens.current != None: 314 | if self.tokens.current.value == ')': 315 | self.consume(TokenType.RPAREN) 316 | break 317 | l.append(self.parse_top()) 318 | if self.tokens.current.value in (',', 'newline'): 319 | self.consume((TokenType.COMMA, TokenType.NEWLINE), soft=True) 320 | else: 321 | self.consume(TokenType.RPAREN) 322 | break 323 | return l 324 | 325 | # Return parsing 326 | def parse_return(self): 327 | self.tokens._next() 328 | if self.tokens.current.type == TokenType.SEMICOLON: 329 | self.tokens._next() 330 | return Return(None) 331 | if self.tokens.current.type == TokenType.NEWLINE: 332 | return Return(None) 333 | expr = self.parse_expr() 334 | return Return(expr) 335 | 336 | # Conditional parsing 337 | def parse_conditional(self): 338 | init = self.tokens._next() 339 | 340 | # Parse else first because it is unlike if/elseif 341 | if init.value == 'else': 342 | return Conditional(init.value, None, self.parse_block(), None) 343 | 344 | body = [] 345 | else_body = [] 346 | self.consume(TokenType.LPAREN) 347 | expr = self.parse_expr() 348 | self.consume(TokenType.RPAREN) 349 | body = self.parse_block() 350 | 351 | # If an else case is next 352 | self.skip_newlines() 353 | _next = self.tokens.current 354 | if _next and _next.type == TokenType.KEYWORD and _next.value in ('elseif', 'else'): 355 | else_body.append(self.parse_conditional()) 356 | 357 | return Conditional(init.value, expr, body, else_body) 358 | 359 | # Parse blocks for functions and conditionals 360 | def parse_block(self): 361 | body = [] 362 | if self.tokens.current.value == '{': 363 | self.consume(TokenType.LBRACE) 364 | self.skip_newlines() 365 | while self.tokens.current != None and self.tokens.current.value != '}': 366 | body.append(self.parse_top()) 367 | self.skip_newlines() 368 | if self.tokens._next() == None: 369 | raise Exception(f"Expected '}}', got '{self.tokens.current.value}' on line {self.tokens.current.line}.") 370 | 371 | # One or two lined 372 | # ex: fun say_hi() return print("Hi") 373 | else: 374 | init = self.tokens.current 375 | # Skip only one line 376 | # If there is more space before an expression, you're doing it wrong kiddo 377 | self.skip_newlines(1) 378 | if self.tokens.current.type == TokenType.NEWLINE: 379 | raise Exception(f"Empty function body on line {init.line}.") 380 | body.append(self.parse_top()) 381 | 382 | return body 383 | 384 | def parse_kv_pair(self): 385 | self.skip_newlines() 386 | k = self.consume((TokenType.IDENTIFIER, TokenType.STRING)).value 387 | self.consume(':') 388 | if self.tokens.current.type == TokenType.LBRACE: 389 | self.consume(TokenType.LBRACE) 390 | v = self.parse_object() 391 | self.consume(TokenType.RBRACE) 392 | else: 393 | v = self.consume((TokenType.IDENTIFIER, TokenType.STRING)).value 394 | return k, v 395 | 396 | def parse_object(self): 397 | obj = {} 398 | while self.tokens.current is not None and self.tokens.current.type is not TokenType.RBRACE: 399 | k, v = self.parse_kv_pair() 400 | obj[k] = v 401 | self.skip_newlines() 402 | if self.tokens.current.type == TokenType.RBRACE: 403 | break 404 | self.consume(TokenType.COMMA) 405 | 406 | if self.tokens.current.value != '}': 407 | raise Exception(f"Expected '}}', got '{self.tokens.current.value}' on line {self.tokens.current.line}.") 408 | return obj 409 | -------------------------------------------------------------------------------- /jink/repl.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from jink.lexer import Lexer 3 | from jink.parser import Parser 4 | from jink.optimizer import Optimizer 5 | from jink.interpreter import Interpreter, Environment 6 | from jink.utils.classes import TokenType 7 | 8 | class REPL: 9 | def __init__(self, stdin, stdout, environment=Environment(), lexer=Lexer(), parser=Parser(), interpreter=Interpreter(), verbose=False, file_dir=None): 10 | self.stdin = stdin 11 | self.stdout = stdout 12 | self.verbose = verbose 13 | self.dir = file_dir 14 | self.env = environment 15 | self.lexer = lexer 16 | self.parser = parser 17 | self.interpreter = interpreter 18 | 19 | def main_loop(self): 20 | while True: 21 | self.stdout.write("> ") 22 | self.stdout.flush() 23 | try: 24 | line = input() 25 | except KeyboardInterrupt: 26 | sys.exit(0) 27 | if not line: 28 | break 29 | self.run(line) 30 | 31 | def run(self, code): 32 | if code == 'exit': 33 | sys.exit(0) 34 | try: 35 | lexed = self.lexer.parse(code) 36 | if len(lexed) == 1 and lexed[0].type == TokenType.IDENTIFIER: 37 | var = self.env.get_var(lexed[0].value) 38 | ret = var['value'] if var != None and isinstance(var, (dict)) else var or 'null' 39 | print(ret) 40 | else: 41 | AST = Optimizer().optimize(self.parser.parse(lexed, verbose=self.verbose), verbose=self.verbose) 42 | e = self.interpreter.evaluate(AST, self.env, verbose=self.verbose, file_dir=self.dir) 43 | if len(e) == 1: 44 | print(e[0] if e[0] is not None else 'null') 45 | else: 46 | print(e[0] if e[0] is not None else 'null') 47 | except Exception as exception: 48 | print("Exception: {}".format(exception)) 49 | -------------------------------------------------------------------------------- /jink/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jink-lang/jink-py/db431abaa187d531f6c3be407848295c70ad3ff5/jink/utils/__init__.py -------------------------------------------------------------------------------- /jink/utils/classes.py: -------------------------------------------------------------------------------- 1 | from jink.utils.evals import * 2 | from enum import Enum 3 | 4 | class TokenType(Enum): 5 | EOF = 0 6 | NEWLINE = 1 7 | KEYWORD = 2 8 | IDENTIFIER = 3 9 | NUMBER = 4 10 | STRING = 5 11 | OPERATOR = 6 12 | LPAREN = 7 13 | RPAREN = 8 14 | LBRACKET = 9 15 | RBRACKET = 10 16 | LBRACE = 11 17 | RBRACE = 12 18 | SEMICOLON = 13 19 | COLON = 14 20 | COMMA = 15 21 | 22 | 23 | class Token: 24 | def __init__(self, _type, value, line, pos): 25 | self.type, self.value, self.line, self.pos = _type, value, line, pos 26 | def __str__(self): 27 | return f"{{ 'type': 'Token<{self.type}>', 'contents': {{ 'value': '{self.value}', 'line': {self.line}, 'pos': {self.pos} }} }}" 28 | def smallStr(self): 29 | return f"{{{self.type} {self.value}}}" 30 | __repr__ = __str__ 31 | 32 | 33 | class BinaryOperator: 34 | __slots__ = ('operator', 'left', 'right') 35 | def __init__(self, operator, left, right): 36 | self.operator, self.left, self.right = operator, left, right 37 | 38 | class UnaryOperator: 39 | __slots__ = ('operator', 'value') 40 | def __init__(self, operator, value): 41 | self.operator, self.value = operator, value 42 | 43 | class IntegerLiteral: 44 | __slots__ = ('value') 45 | def __init__(self, value): 46 | self.value = value 47 | 48 | class FloatingPointLiteral: 49 | __slots__ = ('value') 50 | def __init__(self, value): 51 | self.value = value 52 | 53 | class StringLiteral: 54 | __slots__ = ('value') 55 | def __init__(self, value): 56 | self.value = value 57 | 58 | class BooleanLiteral: 59 | __slots__ = ('value') 60 | def __init__(self, value): 61 | self.value = value 62 | 63 | class IdentLiteral: 64 | def __init__(self, name, index={ 'type': None, 'index': None }): 65 | self.name, self.index = name, index 66 | 67 | class Null: 68 | def __init__(self, value): 69 | self.value = "null" 70 | 71 | 72 | class Assignment: 73 | __slots__ = ('type', 'ident', 'value') 74 | def __init__(self, _type, ident, value): 75 | self.type, self.ident, self.value = _type, ident, value 76 | 77 | class CallExpression: 78 | __slots__ = ('name', 'args') 79 | def __init__(self, name, args): 80 | self.name, self.args = name, args 81 | 82 | class Function: 83 | __slots__ = ('name', 'params', 'body') 84 | def __init__(self, name, params, body): 85 | self.name, self.params, self.body = name, params, body 86 | 87 | class FunctionParameter: 88 | __slots__ = ('name', 'type', 'default') 89 | def __init__(self, name, _type, default=None): 90 | self.name, self.type, self.default = name, _type, default 91 | 92 | class Return: 93 | __slots__ = ('value') 94 | def __init__(self, value): 95 | self.value = value 96 | 97 | class Conditional: 98 | __slots__ = ('type', 'expression', 'body', 'else_body') 99 | def __init__(self, _type, expression, body, else_body): 100 | self.type, self.expression, self.body, self.else_body = _type, expression, body, else_body 101 | 102 | class Module: 103 | __slots__ = ('name', 'index') 104 | def __init__(self, name, index): 105 | self.name, self.index = name, index 106 | -------------------------------------------------------------------------------- /jink/utils/evals.py: -------------------------------------------------------------------------------- 1 | BINOP_EVALS = { 2 | '+': lambda x, y: 0 if x + y is None else x + y, 3 | '-': lambda x, y: 0 if x - y is None else x - y, 4 | '/': lambda x, y: 0 if x / y is None else x / y, 5 | '//': lambda x, y: 0 if x // y is None else x // y, 6 | '*': lambda x, y: 0 if x * y is None else x * y, 7 | '^': lambda x, y: 0 if x ** y is None else x ** y, 8 | '>': lambda x, y: 'true' if x > y else 'false', 9 | '<': lambda x, y: 'true' if x < y else 'false', 10 | '>=': lambda x, y: 'true' if x >= y else 'false', 11 | '<=': lambda x, y: 'true' if x <= y else 'false', 12 | '==': lambda x, y: 'true' if x == y else 'false', 13 | '!=': lambda x, y: 'true' if x != y else 'false' 14 | } 15 | 16 | UNOP_EVALS = { 17 | '!': lambda x: 'true' if x else 'false', 18 | '-': lambda x: 0 if x - x * 2 is None else x - x * 2, 19 | '++': lambda x: 0 if x + 1 is None else x + 1, 20 | '++:post': lambda x: 0 if x is None else x, 21 | '--': lambda x: 0 if x - 1 is None else x - 1 22 | } 23 | -------------------------------------------------------------------------------- /jink/utils/func.py: -------------------------------------------------------------------------------- 1 | import os 2 | import jsonpickle 3 | 4 | def pickle(obj): 5 | encoded = jsonpickle.encode(obj, unpicklable=False) 6 | return jsonpickle.decode(encoded) 7 | 8 | def get_path(): 9 | return os.getcwd().replace('\\', '/') 10 | -------------------------------------------------------------------------------- /jink/utils/future_iter.py: -------------------------------------------------------------------------------- 1 | class FutureIter: 2 | def __init__(self, i): 3 | self._input = i 4 | self._iter = iter(i) 5 | self._future() 6 | 7 | def __str__(self): 8 | return self._input 9 | 10 | def _future(self): 11 | try: 12 | self.current = next(self._iter) 13 | except StopIteration: 14 | self.current = None 15 | 16 | def _next(self): 17 | t = self.current 18 | self._future() 19 | return t 20 | -------------------------------------------------------------------------------- /jink/utils/names.py: -------------------------------------------------------------------------------- 1 | TYPES = ( 2 | 'int', 3 | 'float', 4 | 'string', 5 | 'bool', 6 | 'obj', 7 | 'array' 8 | ) 9 | 10 | KEYWORDS = ( 11 | 'if', 'else', 'elseif', 12 | 'import', 13 | 'return', 'delete', 'void', 14 | 'true', 'false', 'null', 15 | 'fun', 'let', 'const' 16 | ) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cx_Freeze 2 | jsonpickle 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from cx_Freeze import setup, Executable 3 | import os 4 | 5 | build_exe_options = { "include_msvcr": True } 6 | cwd = os.getcwd() 7 | base=None 8 | if sys.platform == "win32": 9 | base = "Console" 10 | 11 | setup( 12 | name="jink", 13 | version="0.0.2", 14 | description="A strongly typed, JavaScript-like programming language.", 15 | options={ "build_exe": build_exe_options }, 16 | executables=[Executable("jink.py", base=base, icon=f"{cwd}\\jink.ico")] 17 | ) 18 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from jink.lexer import Lexer 3 | from jink.parser import Parser 4 | from jink.optimizer import Optimizer 5 | from jink.interpreter import Interpreter, Environment 6 | from jink.utils.classes import * 7 | from jink.utils.func import pickle 8 | 9 | class LexerTest(unittest.TestCase): 10 | def setUp(self): 11 | self.lexer = Lexer() 12 | 13 | def test_string(self): 14 | """Ensures strings are interpreted properly.""" 15 | code = "'hi'" 16 | lexed = self.lexer.parse_literal(code) 17 | assert lexed == "['{TokenType.STRING hi}']", "Issue in string tokenization." 18 | 19 | def test_string_escape(self): 20 | """Ensures the escape character lexes properly.""" 21 | code = "let hey_there = '\\'Hello world\\''" 22 | lexed = self.lexer.parse_literal(code) 23 | assert lexed == "['{TokenType.KEYWORD let}', '{TokenType.IDENTIFIER hey_there}', '{TokenType.OPERATOR =}', \"{TokenType.STRING 'Hello world'}\"]", "Issue in escape character lexical analysis." 24 | 25 | def test_assignment_1(self): 26 | """Ensures variable declaration and assignment are lexed properly.""" 27 | code = "let hello = 'world'" 28 | lexed = self.lexer.parse_literal(code) 29 | assert lexed == "['{TokenType.KEYWORD let}', '{TokenType.IDENTIFIER hello}', '{TokenType.OPERATOR =}', '{TokenType.STRING world}']", "Issue in variable declaration tokenization." 30 | 31 | def test_assignment_2(self): 32 | """Ensures variable declaration and assignment are lexed properly.""" 33 | code = "let pi = 3.14" 34 | lexed = self.lexer.parse_literal(code) 35 | assert lexed == "['{TokenType.KEYWORD let}', '{TokenType.IDENTIFIER pi}', '{TokenType.OPERATOR =}', '{TokenType.NUMBER 3.14}']", "Issue in variable declaration tokenization." 36 | 37 | def test_call_1(self): 38 | """Ensures function calls are lexed properly.""" 39 | code = "print('Hello world!')" 40 | lexed = self.lexer.parse_literal(code) 41 | assert lexed == "['{TokenType.IDENTIFIER print}', '{TokenType.LPAREN (}', '{TokenType.STRING Hello world!}', '{TokenType.RPAREN )}']", "Issue in function call tokenization." 42 | 43 | 44 | class ParserTest(unittest.TestCase): 45 | def setUp(self): 46 | self.lexer = Lexer() 47 | self.parser = Parser() 48 | 49 | def test_call(self): 50 | """Ensures function calls are parsed properly.""" 51 | code = "print('hello')" 52 | tokens = self.lexer.parse(code) 53 | parsed = self.parser.parse_literal(tokens)[0] 54 | test = CallExpression( 55 | name=IdentLiteral(name='print'), 56 | args=[StringLiteral('hello')] 57 | ) 58 | assert pickle(parsed) == pickle(test), "Issue in function call parsing." 59 | 60 | def test_math(self): 61 | """Ensures arithmetic is parsed properly.""" 62 | code = "5 + 5 / 2" 63 | tokens = self.lexer.parse(code) 64 | parsed = self.parser.parse_literal(tokens)[0] 65 | test = BinaryOperator(operator='+', 66 | left=IntegerLiteral(5), 67 | right=BinaryOperator(operator='/', 68 | left=IntegerLiteral(5), 69 | right=IntegerLiteral(2) 70 | ) 71 | ) 72 | assert pickle(parsed) == pickle(test), "Issue in arithmetic parsing." 73 | 74 | def test_assignment_1(self): 75 | """Ensures variable declaration and assignment are parsed properly.""" 76 | code = "let test = 5 * 5 / 5" 77 | tokens = self.lexer.parse(code) 78 | parsed = self.parser.parse_literal(tokens)[0] 79 | test = Assignment(_type='let', 80 | ident=IdentLiteral(name='test'), 81 | value=BinaryOperator(operator='/', 82 | left=BinaryOperator(operator='*', 83 | left=IntegerLiteral(5), 84 | right=IntegerLiteral(5) 85 | ), 86 | right=IntegerLiteral(5) 87 | ) 88 | ) 89 | assert pickle(parsed) == pickle(test), "Issue in assignment parsing." 90 | 91 | def test_conditional_1(self): 92 | """Ensures conditionals are parsed properly.""" 93 | code = "if (1 == 1) return 1" 94 | tokens = self.lexer.parse(code) 95 | parsed = self.parser.parse_literal(tokens)[0] 96 | test = Conditional('if', 97 | expression=BinaryOperator(operator='==', 98 | left=IntegerLiteral(1), 99 | right=IntegerLiteral(1) 100 | ), 101 | body=[Return(IntegerLiteral(1))], 102 | else_body=[] 103 | ) 104 | assert pickle(parsed) == pickle(test), "Issue in inline conditional parsing." 105 | 106 | class InterpreterTest(unittest.TestCase): 107 | def setUp(self): 108 | self.lexer = Lexer() 109 | self.parser = Parser() 110 | self.optimizer = Optimizer() 111 | self.interpreter = Interpreter() 112 | self.env = Environment() 113 | 114 | def test_math(self): 115 | """Ensures arithmetic is evaluated properly.""" 116 | code = "4 + 2 / 2" 117 | tokens = self.lexer.parse(code) 118 | parsed = self.optimizer.optimize(self.parser.parse(tokens)) 119 | evaluated = self.interpreter.evaluate(parsed, self.env)[0] 120 | assert evaluated == 5, "Issue in arithmetic evaluation." 121 | 122 | if __name__ == "__main__": 123 | unittest.main() # run all tests 124 | --------------------------------------------------------------------------------