├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples └── for.lox ├── pylox ├── __init__.py ├── __main__.py ├── scanner.py ├── token.py └── tokentype.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[po] 2 | *.pyc 3 | /build/ 4 | /dist/ 5 | /pylox.egg-info/ 6 | *~ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sasha Matijasic 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include pylox *.py 4 | recursive-exclude * *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pylox 2 | ===== 3 | 4 | pylox is Python implementation of Lox programming language which is a 5 | demo language from [Crafting Interpreters](http://www.craftinginterpreters.com/) 6 | book by [Bob Nystrom](https://github.com/munificent). 7 | 8 | I'm doing this because: 9 | 10 | 1. I want to learn about language design and implementation 11 | 2. I don't want to just read the book or copy and paste the code from it 12 | and I want to do something else than C or Java 13 | 3. Python is my main language these days and I want to use something I'm 14 | most comfortable with 15 | 4. Maybe after Python version I decide to reimplement it (or make my 16 | own toy language) in Go. Or force myself to learn Rust which seems 17 | like a good idea. 18 | 5. Fun. 19 | 20 | This is **work in progress**. 21 | 22 | Requirements 23 | ------------ 24 | 25 | Python 3.6 for no particular reasons except f-strings are used in few 26 | places. Other than that, it could easily be ported to even Python 2.7 27 | (but I don't plan to). 28 | 29 | Install it 30 | ---------- 31 | 32 | pip install pylox 33 | 34 | Make sure you run it with Python3, I suggest you install it inside 35 | virtualenv. 36 | 37 | Run it 38 | ------ 39 | 40 | from source: 41 | 42 | python3 -m pylox [script] 43 | 44 | or if installed via pip: 45 | 46 | pylox [script] 47 | 48 | License 49 | ------- 50 | 51 | MIT. 52 | -------------------------------------------------------------------------------- /examples/for.lox: -------------------------------------------------------------------------------- 1 | // print numbers from 0 to 9 2 | 3 | for (var i = 0; i < 10; i = i + 1) { 4 | print(i); 5 | } 6 | -------------------------------------------------------------------------------- /pylox/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | import sys 3 | 4 | from .scanner import Scanner 5 | 6 | 7 | class Lox(object): 8 | def __init__(self): 9 | self.had_error = False 10 | 11 | def error_code(self): 12 | return 1 if self.had_error else 0 13 | 14 | def run_file(self, filename): 15 | with open(filename, 'r') as f: 16 | self.run(f.read()) 17 | if self.had_error: 18 | sys.exit(65) 19 | 20 | def run_prompt(self): 21 | while True: 22 | s = input("> ") 23 | self.run(s) 24 | self.had_error = False 25 | 26 | def run(self, source): 27 | scanner = Scanner(source) 28 | tokens = scanner.scan_tokens() 29 | 30 | for token in tokens: 31 | print(token) 32 | 33 | def error(self, line, message): 34 | self.report(line, "", message) 35 | 36 | def report(self, line, where, message): 37 | text = f'[line {line}] Error {where}: {message}' 38 | print(text, file=sys.stderr) 39 | self.had_error = True 40 | -------------------------------------------------------------------------------- /pylox/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import argparse 4 | 5 | from . import Lox 6 | 7 | 8 | def get_args(): 9 | parser = argparse.ArgumentParser(description='PyLox') 10 | parser.add_argument( 11 | 'script', type=str, nargs='?', 12 | help='script file name') 13 | 14 | return parser.parse_args() 15 | 16 | def main(): 17 | args = get_args() 18 | 19 | lox = Lox() 20 | if args.script: 21 | lox.run_file(args.script) 22 | else: 23 | lox.run_prompt() 24 | 25 | return lox.error_code() 26 | 27 | 28 | if __name__ == '__main__': 29 | sys.exit(main()) 30 | -------------------------------------------------------------------------------- /pylox/scanner.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from .token import Token 4 | from .tokentype import TokenType 5 | 6 | 7 | class Scanner(object): 8 | def __init__(self, source): 9 | self.source = source 10 | self.tokens = [] 11 | 12 | self._start = 0 13 | self._current = 0 14 | self._line = 1 15 | self.keywords = { 16 | 'and': TokenType.AND, 17 | 'class': TokenType.CLASS, 18 | 'else': TokenType.ELSE, 19 | 'false': TokenType.FALSE, 20 | 'for': TokenType.FOR, 21 | 'fun': TokenType.FUN, 22 | 'if': TokenType.IF, 23 | 'nil': TokenType.NIL, 24 | 'or': TokenType.OR, 25 | 'print': TokenType.PRINT, 26 | 'return': TokenType.RETURN, 27 | 'super': TokenType.SUPER, 28 | 'this': TokenType.THIS, 29 | 'true': TokenType.TRUE, 30 | 'var': TokenType.VAR, 31 | 'while': TokenType.WHILE 32 | } 33 | 34 | def _is_at_end(self): 35 | return self._current >= len(self.source) 36 | 37 | def scan_tokens(self): 38 | while not self._is_at_end(): 39 | self._start = self._current 40 | self.scan_token() 41 | 42 | self.tokens.append(Token(TokenType.EOF, '', None, self._line)) 43 | return self.tokens 44 | 45 | def scan_token(self): 46 | c = self._advance() 47 | if c == '(': 48 | self.add_token(TokenType.LEFT_PAREN) 49 | elif c == ')': 50 | self.add_token(TokenType.RIGHT_PAREN) 51 | elif c == '{': 52 | self.add_token(TokenType.LEFT_BRACE) 53 | elif c == '}': 54 | self.add_token(TokenType.RIGHT_BRACE) 55 | elif c == ',': 56 | self.add_token(TokenType.COMMA) 57 | elif c == '.': 58 | self.add_token(TokenType.DOT) 59 | elif c == '-': 60 | self.add_token(TokenType.MINUS) 61 | elif c == '+': 62 | self.add_token(TokenType.PLUS) 63 | elif c == ';': 64 | self.add_token(TokenType.SEMICOLON) 65 | elif c == '*': 66 | self.add_token(TokenType.STAR) 67 | elif c == '!': 68 | self.add_token( 69 | TokenType.BANG_EQUAL if self._match('=') else TokenType.BANG) 70 | elif c == '=': 71 | self.add_token( 72 | TokenType.EQUAL_EQUAL if self._match('=') else TokenType.EQUAL) 73 | elif c == '<': 74 | self.add_token( 75 | TokenType.LESS_EQUAL if self._match('=') else TokenType.LESS) 76 | elif c == '>': 77 | self.add_token( 78 | TokenType.GREATER_EQUAL if self._match('=') else TokenType.GREATER) 79 | elif c == '/': 80 | if self._match('/'): 81 | while (self._peek() != '\n' and not self._is_at_end()): 82 | self._advance() 83 | else: 84 | self.add_token(TokenType.SLASH) 85 | elif c in [' ', '\r', '\t']: 86 | pass 87 | elif c == '\n': 88 | self._line += 1 89 | elif c == '"': 90 | self.string() 91 | elif self._is_digit(c): 92 | self.number() 93 | elif self._is_alpha(c): 94 | self.identifier() 95 | else: 96 | Lox().error(line, "Unexpected character."); 97 | 98 | def identifier(self): 99 | while self._is_alpha_numeric(self._peek()): 100 | self._advance() 101 | text = self.source[self._start:self._current] 102 | token_type = self.keywords.get(text) 103 | if token_type is None: 104 | token_type = TokenType.IDENTIFIER 105 | 106 | self.add_token(token_type) 107 | 108 | def number(self): 109 | while self._is_digit(self._peek()): 110 | self._advance() 111 | 112 | if self._peek() == '.' and self._is_digit(self._peek_next()): 113 | # consume the dot 114 | self._advance() 115 | while self._is_digit(self._peek()): 116 | self._advance() 117 | 118 | self.add_token( 119 | TokenType.NUMBER, float(self.source[self._start:self._current])) 120 | 121 | def string(self): 122 | while self._peek() != '"' and not self._is_at_end(): 123 | if self._peek == '\n': 124 | self._line += 1 125 | self._advance() 126 | 127 | # unterminated string 128 | if self._is_at_end(): 129 | Lox().error(line, "Unterminated string.") 130 | 131 | self._advance() 132 | value = self.source[self._start+1:self._current-1] 133 | self.add_token(TokenType.STRING, value) 134 | 135 | def _is_digit(self, c): 136 | return '0' <= c <= '9' 137 | 138 | def _is_alpha(self, c): 139 | return ('a' <= c <= 'z') or ('A' <= c <= 'Z') or c == '_' 140 | 141 | def _is_alpha_numeric(self, c): 142 | return self._is_alpha(c) or self._is_digit(c) 143 | 144 | def _match(self, expected): 145 | if self._is_at_end(): 146 | return False 147 | 148 | if self.source[self._current] != expected: 149 | return False 150 | 151 | self._current += 1 152 | return True 153 | 154 | def _peek(self): 155 | if self._current >= len(self.source): 156 | return '\0' 157 | return self.source[self._current] 158 | 159 | def _peek_next(self): 160 | if (self._current + 1) >= len(self.source): 161 | return '\0' 162 | return self.source[self._current+1] 163 | 164 | def _advance(self): 165 | self._current += 1 166 | return self.source[self._current-1] 167 | 168 | def add_token(self, token_type, literal=None): 169 | text = self.source[self._start:self._current] 170 | self.tokens.append(Token(token_type, text, literal, self._line)) 171 | -------------------------------------------------------------------------------- /pylox/token.py: -------------------------------------------------------------------------------- 1 | class Token(object): 2 | def __init__(self, token_type, lexeme, literal, line): 3 | self.token_type = token_type 4 | self.lexeme = lexeme 5 | self.literal = literal 6 | self.line = line 7 | 8 | def __str__(self): 9 | return f'{self.token_type} {self.lexeme} {self.literal}' 10 | 11 | -------------------------------------------------------------------------------- /pylox/tokentype.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class TokenType(Enum): 5 | # single character tokens 6 | LEFT_PAREN = auto() 7 | RIGHT_PAREN = auto() 8 | LEFT_BRACE = auto() 9 | RIGHT_BRACE = auto() 10 | COMMA = auto() 11 | DOT = auto() 12 | MINUS = auto() 13 | PLUS = auto() 14 | SEMICOLON = auto() 15 | SLASH = auto() 16 | STAR = auto() 17 | 18 | # one or two character tokens 19 | BANG = auto() 20 | BANG_EQUAL = auto() 21 | EQUAL = auto() 22 | EQUAL_EQUAL = auto() 23 | GREATER = auto() 24 | GREATER_EQUAL = auto() 25 | LESS = auto() 26 | LESS_EQUAL = auto() 27 | 28 | # literals 29 | IDENTIFIER = auto() 30 | STRING = auto() 31 | NUMBER = auto() 32 | 33 | # keywords 34 | AND = auto() 35 | CLASS = auto() 36 | ELSE = auto() 37 | FALSE = auto() 38 | FUN = auto() 39 | FOR = auto() 40 | IF = auto() 41 | NIL = auto() 42 | OR = auto() 43 | PRINT = auto() 44 | RETURN = auto() 45 | SUPER = auto() 46 | THIS = auto() 47 | TRUE = auto() 48 | VAR = auto() 49 | WHILE = auto() 50 | 51 | EOF = auto() 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import pylox 4 | 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | setup( 10 | name='pylox', 11 | version=pylox.__version__, 12 | author='Sasha Matijasic', 13 | author_email='sasha@selectnull.com', 14 | packages=find_packages(), 15 | url='https://github.com/selectnull/pylox', 16 | license='MIT', 17 | description='Python implementation of Lox programming language', 18 | long_description=open('README.md').read(), 19 | include_package_data=True, 20 | install_requires=[], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'pylox = pylox.__main__:main' 24 | ] 25 | }, 26 | classifiers=[ 27 | 'Development Status :: 2 - Pre-Alpha', 28 | 'Environment :: Console', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Environment :: Console', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3 :: Only' 36 | ], 37 | ) 38 | --------------------------------------------------------------------------------