mirror of https://github.com/japl-lang/japl.git
Cleaned up the repository and moved the python POC to a separate repo
This commit is contained in:
parent
ddcf299c2a
commit
c41b6da81c
|
@ -1,132 +1,8 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# Default nim's gitignore
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
nimcache/
|
||||
nimblecache/
|
||||
htmldocs/
|
||||
|
||||
# PyCharm
|
||||
|
||||
|
@ -136,9 +12,12 @@ dmypy.json
|
|||
|
||||
.DS_Store
|
||||
|
||||
# Nim main
|
||||
# Executables
|
||||
|
||||
main
|
||||
*.exe
|
||||
|
||||
# Dependency graphs
|
||||
|
||||
*.deps
|
||||
*.dot
|
||||
|
|
|
@ -11,7 +11,7 @@ import types/functiontype
|
|||
import types/stringtype
|
||||
|
||||
|
||||
const FRAMES_MAX* = 4
|
||||
const FRAMES_MAX* = 400 # TODO: Inspect why the VM crashes if this exceeds 400
|
||||
const JAPL_VERSION* = "0.2.0"
|
||||
const JAPL_RELEASE* = "alpha"
|
||||
const DEBUG_TRACE_VM* = false
|
||||
|
@ -25,6 +25,7 @@ type
|
|||
function*: ptr Function
|
||||
ip*: int
|
||||
slot*: int
|
||||
endSlot*: int
|
||||
stack*: seq[Value]
|
||||
|
||||
VM* = ref object
|
||||
|
@ -51,12 +52,12 @@ type
|
|||
file*: string
|
||||
|
||||
|
||||
proc getAbsIndex(self: CallFrame, idx: int): int =
|
||||
return idx + len(self.stack[self.slot..len(self.stack) - 1]) - 1
|
||||
|
||||
|
||||
proc getView*(self: CallFrame): seq[Value] =
|
||||
result = self.stack[self.slot..len(self.stack) - 1]
|
||||
result = self.stack[self.slot..self.endSlot - 1]
|
||||
|
||||
|
||||
proc getAbsIndex(self: CallFrame, idx: int): int =
|
||||
return idx + len(self.getView()) - 1 # Inspect this code (locals, functions)
|
||||
|
||||
|
||||
proc len*(self: CallFrame): int =
|
||||
|
@ -64,7 +65,7 @@ proc len*(self: CallFrame): int =
|
|||
|
||||
|
||||
proc `[]`*(self: CallFrame, idx: int): Value =
|
||||
result = self.getView()[idx]
|
||||
result = self.stack[self.getAbsIndex(idx)]
|
||||
|
||||
|
||||
proc `[]=`*(self: CallFrame, idx: int, val: Value) =
|
||||
|
@ -74,7 +75,7 @@ proc `[]=`*(self: CallFrame, idx: int, val: Value) =
|
|||
|
||||
|
||||
proc delete*(self: CallFrame, idx: int) =
|
||||
if idx < self.slot:
|
||||
if idx < self.slot or idx > self.endSlot:
|
||||
raise newException(IndexError, "CallFrame index out of range")
|
||||
self.stack.delete(idx)
|
||||
|
|
@ -6,7 +6,4 @@ fun fib(n) {
|
|||
}
|
||||
|
||||
|
||||
var start = clock();
|
||||
fib(30);
|
||||
var end = clock() - start;
|
||||
print(end);
|
||||
fib(20);
|
|
@ -1,4 +0,0 @@
|
|||
from .lexer import Lexer
|
||||
from .parser import Parser
|
||||
from .interpreter import Interpreter
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
import operator
|
||||
from typing import List
|
||||
from .types.callable import Callable
|
||||
from .types.japlclass import JAPLClass
|
||||
from .types.instance import JAPLInstance
|
||||
from .meta.environment import Environment
|
||||
from .meta.tokentype import TokenType
|
||||
from .meta.exceptions import JAPLError, BreakException, ReturnException
|
||||
from .types.native import Clock, Type, JAPLFunction, Truthy, Stringify, PrintFunction, IsInstance, IsSubclass, IsSuperclass
|
||||
from .meta.expression import Expression, Variable, Literal, Logical, Binary, Unary, Grouping, Assignment, Call, Get, Set
|
||||
from .meta.statement import Statement, StatementExpr, If, While, Del, Break, Return, Var, Block, Function, Class
|
||||
|
||||
|
||||
class Interpreter(Expression.Visitor, Statement.Visitor):
|
||||
"""
|
||||
An interpreter for the JAPL
|
||||
programming language
|
||||
"""
|
||||
|
||||
OPS = {TokenType.MINUS: operator.sub, TokenType.PLUS: operator.add, TokenType.SLASH: operator.truediv,
|
||||
TokenType.STAR: operator.mul, TokenType.DEQ: operator.eq, TokenType.GT: operator.gt,
|
||||
TokenType.GE: operator.ge, TokenType.LT: operator.lt, TokenType.LE: operator.le, TokenType.EQ: operator.eq,
|
||||
TokenType.NE: operator.ne, TokenType.MOD: operator.mod, TokenType.POW: operator.pow}
|
||||
|
||||
def __init__(self):
|
||||
"""Object constructor"""
|
||||
|
||||
self.environment = Environment()
|
||||
self.locals = {}
|
||||
self.globals = self.environment
|
||||
self.globals.define("clock", Clock())
|
||||
self.globals.define("type", Type())
|
||||
self.globals.define("truthy", Truthy())
|
||||
self.globals.define("stringify", Stringify())
|
||||
self.globals.define("print", PrintFunction())
|
||||
self.globals.define("isinstance", IsInstance())
|
||||
self.globals.define("issuperclass", IsSuperclass())
|
||||
self.globals.define("issubclass", IsSubclass())
|
||||
|
||||
def number_operand(self, op, operand):
|
||||
"""
|
||||
An helper method to check if the operand
|
||||
to a unary operator is a number
|
||||
"""
|
||||
|
||||
if isinstance(operand, (int, float)):
|
||||
return
|
||||
raise JAPLError(op,
|
||||
f"Unsupported unary operator '{op.lexeme}' for object of type '{type(operand).__name__}'")
|
||||
|
||||
def compatible_operands(self, op, left, right):
|
||||
"""
|
||||
Helper method to check types when doing binary
|
||||
operations
|
||||
"""
|
||||
|
||||
if op.kind == TokenType.SLASH and right == 0:
|
||||
raise JAPLError(op, "Cannot divide by 0")
|
||||
elif isinstance(left, (bool, type(None))) or isinstance(right, (bool, type(None))):
|
||||
if op.kind not in (TokenType.DEQ, TokenType.NE):
|
||||
raise JAPLError(op, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'")
|
||||
return
|
||||
elif isinstance(left, (int, float)) and isinstance(right, (int, float)):
|
||||
return
|
||||
elif op.kind in (TokenType.PLUS, TokenType.STAR, TokenType.DEQ, TokenType.NE):
|
||||
if isinstance(left, str) and isinstance(right, str):
|
||||
return
|
||||
elif isinstance(left, str) and isinstance(right, int):
|
||||
return
|
||||
elif isinstance(left, int) and isinstance(right, str):
|
||||
return
|
||||
raise JAPLError(operator, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'")
|
||||
|
||||
def visit_literal(self, expr: Literal):
|
||||
"""
|
||||
Visits a Literal node in the Abstract Syntax Tree,
|
||||
returning its value to the visitor
|
||||
"""
|
||||
|
||||
return expr.value
|
||||
|
||||
def visit_logical(self, expr: Logical):
|
||||
"""Visits a logical node"""
|
||||
|
||||
left = self.eval(expr.left)
|
||||
if expr.operator.kind == TokenType.OR:
|
||||
if bool(left):
|
||||
return left
|
||||
elif not bool(left):
|
||||
return self.eval(expr.right)
|
||||
return self.eval(expr.right)
|
||||
|
||||
def eval(self, expr: Expression):
|
||||
"""
|
||||
Evaluates an expression by calling its accept()
|
||||
method and passing self to it. This mechanism is known
|
||||
as the 'Visitor Pattern': the expression object will
|
||||
later call the interpreter's appropriate method to
|
||||
evaluate itself
|
||||
"""
|
||||
|
||||
return expr.accept(self)
|
||||
|
||||
def visit_grouping(self, grouping: Grouping):
|
||||
"""
|
||||
Visits a Grouping node in the Abstract Syntax Tree,
|
||||
recursively evaluating its subexpressions
|
||||
"""
|
||||
|
||||
return self.eval(grouping.expr)
|
||||
|
||||
def visit_unary(self, expr: Unary):
|
||||
"""
|
||||
Visits a Unary node in the Abstract Syntax Teee,
|
||||
returning the negation of the given object, if
|
||||
the operation is supported
|
||||
"""
|
||||
|
||||
right = self.eval(expr.right)
|
||||
self.number_operand(expr.operator, right)
|
||||
if expr.operator.kind == TokenType.NEG:
|
||||
return not right
|
||||
return -right
|
||||
|
||||
def visit_binary(self, expr: Binary):
|
||||
"""
|
||||
Visits a Binary node in the Abstract Syntax Tree,
|
||||
recursively evaulating both operands first and then
|
||||
performing the operation specified by the operator
|
||||
"""
|
||||
|
||||
left = self.eval(expr.left)
|
||||
right = self.eval(expr.right)
|
||||
self.compatible_operands(expr.operator, left, right)
|
||||
return self.OPS[expr.operator.kind](left, right)
|
||||
|
||||
def visit_statement_expr(self, stmt: StatementExpr):
|
||||
"""
|
||||
Visits an expression statement and evaluates it
|
||||
"""
|
||||
|
||||
self.eval(stmt.expression)
|
||||
|
||||
def visit_if(self, statement: If):
|
||||
"""
|
||||
Visits an If node and evaluates it
|
||||
"""
|
||||
|
||||
if self.eval(statement.condition):
|
||||
self.exec(statement.then_branch)
|
||||
elif statement.else_branch:
|
||||
self.exec(statement.else_branch)
|
||||
|
||||
def visit_class(self, stmt: Class):
|
||||
"""Visits a class declaration"""
|
||||
|
||||
superclass = None
|
||||
if stmt.superclass:
|
||||
superclass = self.eval(stmt.superclass)
|
||||
if not isinstance(superclass, JAPLClass):
|
||||
raise JAPLError(stmt.superclass.name, "Superclass must be a class")
|
||||
self.environment.define(stmt.name.lexeme, None)
|
||||
if superclass:
|
||||
environment = Environment(self.environment)
|
||||
environment.define("super", superclass)
|
||||
else:
|
||||
environment = self.environment
|
||||
methods = {}
|
||||
for method in stmt.methods:
|
||||
func = JAPLFunction(method, environment)
|
||||
methods[method.name.lexeme] = func
|
||||
klass = JAPLClass(stmt.name.lexeme, methods, superclass)
|
||||
if superclass:
|
||||
self.environment = environment.enclosing
|
||||
self.environment.assign(stmt.name, klass)
|
||||
|
||||
def visit_while(self, statement: While):
|
||||
"""
|
||||
Visits a while node and executes it
|
||||
"""
|
||||
|
||||
while self.eval(statement.condition):
|
||||
try:
|
||||
self.exec(statement.body)
|
||||
except BreakException:
|
||||
break
|
||||
|
||||
def visit_var_stmt(self, stmt: Var):
|
||||
"""
|
||||
Visits a var statement
|
||||
"""
|
||||
|
||||
val = None
|
||||
if stmt.init:
|
||||
val = self.eval(stmt.init)
|
||||
self.environment.define(stmt.name.lexeme, val)
|
||||
|
||||
def lookup(self, name, expr: Expression):
|
||||
"""
|
||||
Performs name lookups in the closest scope
|
||||
"""
|
||||
|
||||
distance = self.locals.get(expr)
|
||||
if distance is not None:
|
||||
return self.environment.get_at(distance, name.lexeme)
|
||||
else:
|
||||
return self.globals.get(name)
|
||||
|
||||
def visit_var_expr(self, expr: Variable):
|
||||
"""
|
||||
Visits a var expression
|
||||
"""
|
||||
|
||||
return self.lookup(expr.name, expr)
|
||||
|
||||
def visit_del(self, stmt: Del):
|
||||
"""
|
||||
Visits a del expression
|
||||
"""
|
||||
|
||||
return self.environment.delete(stmt.name)
|
||||
|
||||
def visit_assign(self, stmt: Assignment):
|
||||
"""
|
||||
Visits an assignment expression
|
||||
"""
|
||||
|
||||
right = self.eval(stmt.value)
|
||||
distance = self.locals.get(stmt)
|
||||
if distance is not None:
|
||||
self.environment.assign_at(distance, stmt.name, right)
|
||||
else:
|
||||
self.globals.assign(stmt.name, right)
|
||||
return right
|
||||
|
||||
def visit_block(self, stmt: Block):
|
||||
"""
|
||||
Visits a new scope block
|
||||
"""
|
||||
|
||||
return self.execute_block(stmt.statements, Environment(self.environment))
|
||||
|
||||
def visit_break(self, stmt: Break):
|
||||
"""
|
||||
Visits a break statement
|
||||
"""
|
||||
|
||||
raise BreakException()
|
||||
|
||||
def visit_call_expr(self, expr: Call):
|
||||
"""
|
||||
Visits a call expression
|
||||
"""
|
||||
|
||||
callee = self.eval(expr.callee)
|
||||
if not isinstance(callee, Callable):
|
||||
raise JAPLError(expr.paren, f"'{type(callee).__name__}' is not callable")
|
||||
arguments = []
|
||||
for argument in expr.arguments:
|
||||
arguments.append(self.eval(argument))
|
||||
function = callee
|
||||
if function.arity != len(arguments):
|
||||
raise JAPLError(expr.paren, f"Expecting {function.arity} arguments, got {len(arguments)}")
|
||||
return function.call(self, arguments)
|
||||
|
||||
def execute_block(self, statements: List[Statement], scope: Environment):
|
||||
"""
|
||||
Executes a block of statements
|
||||
"""
|
||||
|
||||
prev = self.environment
|
||||
try:
|
||||
self.environment = scope
|
||||
for statement in statements:
|
||||
self.exec(statement)
|
||||
finally:
|
||||
self.environment = prev
|
||||
|
||||
def visit_return(self, statement: Return):
|
||||
"""
|
||||
Visits a return statement
|
||||
"""
|
||||
|
||||
value = None
|
||||
if statement.value:
|
||||
value = self.eval(statement.value)
|
||||
raise ReturnException(value)
|
||||
|
||||
def visit_function(self, statement: Function):
|
||||
"""
|
||||
Visits a function
|
||||
"""
|
||||
|
||||
function = JAPLFunction(statement, self.environment)
|
||||
self.environment.define(statement.name.lexeme, function)
|
||||
|
||||
def visit_get(self, expr: Get):
|
||||
"""Visits property get expressions and evaluates them"""
|
||||
|
||||
obj = self.eval(expr.object)
|
||||
if isinstance(obj, JAPLInstance):
|
||||
return obj.get(expr.name)
|
||||
raise JAPLError(expr.name, "Only instances have properties")
|
||||
|
||||
def visit_set(self, expr: Set):
|
||||
"""Visits property set expressions and evaluates them"""
|
||||
|
||||
obj = self.eval(expr.object)
|
||||
if not isinstance(obj, JAPLInstance):
|
||||
raise JAPLError(expr, "Only instances have fields")
|
||||
value = self.eval(expr.value)
|
||||
obj.set(expr.name, value)
|
||||
|
||||
def visit_this(self, expr):
|
||||
"""Evaluates 'this' expressions"""
|
||||
|
||||
return self.lookup(expr.keyword, expr)
|
||||
|
||||
def visit_super(self, expr):
|
||||
"""Evaluates 'super' expressions"""
|
||||
|
||||
distance = self.locals.get(expr)
|
||||
superclass = self.environment.get_at(distance, "super")
|
||||
instance = self.environment.get_at(distance - 1, "this")
|
||||
meth = superclass.get_method(expr.method.lexeme)
|
||||
if not meth:
|
||||
raise JAPLError(expr.method, f"Undefined property '{expr.method.lexeme}'")
|
||||
return meth.bind(instance)
|
||||
|
||||
def exec(self, statement: Statement):
|
||||
"""
|
||||
Executes a statement
|
||||
"""
|
||||
|
||||
statement.accept(self)
|
||||
|
||||
def interpret(self, statements: List[Statement]):
|
||||
"""
|
||||
Executes a JAPL program
|
||||
"""
|
||||
|
||||
for statement in statements:
|
||||
self.exec(statement)
|
||||
|
||||
def resolve(self, expr: Expression, depth: int):
|
||||
"""
|
||||
Stores the result of the name resolution: this
|
||||
info will be used later to know exactly in which
|
||||
environment to look up a given variable
|
||||
"""
|
||||
|
||||
self.locals[expr] = depth # How many environments to skip!
|
|
@ -1,212 +0,0 @@
|
|||
from .meta.tokenobject import Token
|
||||
from .meta.tokentype import TokenType
|
||||
from .meta.exceptions import ParseError
|
||||
from typing import List
|
||||
|
||||
|
||||
class Lexer(object):
|
||||
"""
|
||||
A simple tokenizer for the JAPL programming
|
||||
language, scans a input source file and
|
||||
produces a list of tokens. Some errors
|
||||
are caught here as well.
|
||||
"""
|
||||
|
||||
TOKENS = {"(": TokenType.LP, ")": TokenType.RP,
|
||||
"{": TokenType.LB, "}": TokenType.RB,
|
||||
".": TokenType.DOT, ",": TokenType.COMMA,
|
||||
"-": TokenType.MINUS, "+": TokenType.PLUS,
|
||||
";": TokenType.SEMICOLON, "*": TokenType.STAR,
|
||||
">": TokenType.GT, "<": TokenType.LT,
|
||||
"=": TokenType.EQ, "!": TokenType.NEG,
|
||||
"/": TokenType.SLASH, "%": TokenType.MOD}
|
||||
|
||||
RESERVED = {"or": TokenType.OR, "and": TokenType.AND,
|
||||
"class": TokenType.CLASS, "fun": TokenType.FUN,
|
||||
"if": TokenType.IF, "else": TokenType.ELSE,
|
||||
"for": TokenType.FOR, "while": TokenType.WHILE,
|
||||
"var": TokenType.VAR, "nil": TokenType.NIL,
|
||||
"true": TokenType.TRUE, "false": TokenType.FALSE,
|
||||
"return": TokenType.RETURN,
|
||||
"this": TokenType.THIS, "super": TokenType.SUPER,
|
||||
"del": TokenType.DEL, "break": TokenType.BREAK}
|
||||
|
||||
def __init__(self, source: str):
|
||||
"""Object constructor"""
|
||||
|
||||
self.source = source
|
||||
self.tokens: List[Token] = []
|
||||
self.line: int = 1 # Points to the line being lexed
|
||||
self.start: int = 0 # The position of the first character of the current lexeme
|
||||
self.current: int = 0 # The position of the current character being lexed
|
||||
|
||||
def step(self) -> str:
|
||||
"""
|
||||
'Steps' one character in the source code and returns it
|
||||
"""
|
||||
|
||||
if self.done():
|
||||
return ""
|
||||
self.current += 1
|
||||
return self.source[self.current - 1]
|
||||
|
||||
def peek(self) -> str:
|
||||
"""
|
||||
Returns the current character without consuming it
|
||||
or an empty string if all text has been consumed
|
||||
"""
|
||||
|
||||
if self.done():
|
||||
return ""
|
||||
return self.source[self.current]
|
||||
|
||||
def peek_next(self) -> str:
|
||||
"""
|
||||
Returns the next character after self.current
|
||||
or an empty string if the input has been consumed
|
||||
"""
|
||||
|
||||
if self.current + 1 >= len(self.source):
|
||||
return ""
|
||||
return self.source[self.current + 1]
|
||||
|
||||
def string(self, delimiter: str):
|
||||
"""Parses a string literal"""
|
||||
|
||||
while self.peek() != delimiter and not self.done():
|
||||
if self.peek() == "\n":
|
||||
self.line += 1
|
||||
self.step()
|
||||
if self.done():
|
||||
raise ParseError(f"unterminated string literal at line {self.line}")
|
||||
self.step() # Consume the '"'
|
||||
value = self.source[self.start + 1:self.current - 1] # Get the actual string
|
||||
self.tokens.append(self.create_token(TokenType.STR, value))
|
||||
|
||||
def number(self):
|
||||
"""Parses a number literal"""
|
||||
|
||||
convert = int
|
||||
while self.peek().isdigit():
|
||||
self.step()
|
||||
if self.peek() == ".":
|
||||
self.step() # Consume the "."
|
||||
while self.peek().isdigit():
|
||||
self.step()
|
||||
convert = float
|
||||
self.tokens.append(self.create_token(TokenType.NUM,
|
||||
convert(self.source[self.start:self.current])))
|
||||
|
||||
def identifier(self):
|
||||
"""Parses identifiers and reserved keywords"""
|
||||
|
||||
while self.peek().isalnum() or self.is_identifier(self.peek()):
|
||||
self.step()
|
||||
kind = TokenType.ID
|
||||
value = self.source[self.start:self.current]
|
||||
if self.RESERVED.get(value, None):
|
||||
kind = self.RESERVED[value]
|
||||
self.tokens.append(self.create_token(kind))
|
||||
|
||||
def comment(self):
|
||||
"""Handles multi-line comments"""
|
||||
|
||||
closed = False
|
||||
while not self.done():
|
||||
end = self.peek() + self.peek_next()
|
||||
if end == "/*": # Nested comments
|
||||
self.step()
|
||||
self.step()
|
||||
self.comment()
|
||||
elif end == "*/":
|
||||
closed = True
|
||||
self.step() # Consume the two ends
|
||||
self.step()
|
||||
break
|
||||
self.step()
|
||||
if self.done() and not closed:
|
||||
raise ParseError(f"Unexpected EOF at line {self.line}")
|
||||
|
||||
def match(self, char: str) -> bool:
|
||||
"""
|
||||
Returns True if the current character in self.source matches
|
||||
the given character
|
||||
"""
|
||||
|
||||
if self.done():
|
||||
return False
|
||||
elif self.source[self.current] != char:
|
||||
return False
|
||||
self.current += 1
|
||||
return True
|
||||
|
||||
def done(self) -> bool:
|
||||
"""
|
||||
Helper method that's used by the lexer
|
||||
to know if all source has been consumed
|
||||
"""
|
||||
|
||||
return self.current >= len(self.source)
|
||||
|
||||
def create_token(self, kind: TokenType, literal: object = None) -> Token:
|
||||
"""
|
||||
Creates and returns a token object
|
||||
"""
|
||||
|
||||
return Token(kind, self.source[self.start:self.current], literal, self.line)
|
||||
|
||||
def is_identifier(self, char: str):
|
||||
"""Returns if a character can be an identifier"""
|
||||
|
||||
if char.isalpha() or char in ("_", ): # More coming soon
|
||||
return True
|
||||
|
||||
def scan_token(self):
|
||||
"""
|
||||
Scans for a single token and adds it to
|
||||
self.tokens
|
||||
"""
|
||||
|
||||
char = self.step()
|
||||
if char in (" ", "\t", "\r"): # Useless characters
|
||||
return
|
||||
elif char == "\n": # New line
|
||||
self.line += 1
|
||||
elif char in ("'", '"'): # A string literal
|
||||
self.string(char)
|
||||
elif char.isdigit():
|
||||
self.number()
|
||||
elif self.is_identifier(char): # Identifier or reserved keyword
|
||||
self.identifier()
|
||||
elif char in self.TOKENS:
|
||||
if char == "/" and self.match("/"):
|
||||
while self.peek() != "\n" and not self.done():
|
||||
self.step() # Who cares about comments?
|
||||
elif char == "/" and self.match("*"):
|
||||
self.comment()
|
||||
elif char == "=" and self.match("="):
|
||||
self.tokens.append(self.create_token(TokenType.DEQ))
|
||||
elif char == ">" and self.match("="):
|
||||
self.tokens.append(self.create_token(TokenType.GE))
|
||||
elif char == "<" and self.match("="):
|
||||
self.tokens.append(self.create_token(TokenType.LE))
|
||||
elif char == "!" and self.match("="):
|
||||
self.tokens.append(self.create_token(TokenType.NE))
|
||||
elif char == "*" and self.match("*"):
|
||||
self.tokens.append(self.create_token(TokenType.POW))
|
||||
else:
|
||||
self.tokens.append(self.create_token(self.TOKENS[char]))
|
||||
else:
|
||||
raise ParseError(f"unexpected character '{char}' at line {self.line}")
|
||||
|
||||
def lex(self) -> List[Token]:
|
||||
"""
|
||||
Performs lexical analysis on self.source
|
||||
and returns a list of tokens
|
||||
"""
|
||||
|
||||
while not self.done():
|
||||
self.start = self.current
|
||||
self.scan_token()
|
||||
self.tokens.append(Token(TokenType.EOF, "", None, self.line))
|
||||
return self.tokens
|
|
@ -1,7 +0,0 @@
|
|||
from enum import Enum, auto
|
||||
|
||||
|
||||
class ClassType(Enum):
|
||||
|
||||
NONE = auto()
|
||||
CLASS = auto()
|
|
@ -1,71 +0,0 @@
|
|||
from .exceptions import JAPLError
|
||||
from .tokenobject import Token
|
||||
from .expression import Variable
|
||||
|
||||
|
||||
class Environment(object):
|
||||
"""
|
||||
A wrapper around a hashmap representing
|
||||
a scope
|
||||
"""
|
||||
|
||||
def __init__(self, enclosing=None):
|
||||
"""Object constructor"""
|
||||
|
||||
self.map = {}
|
||||
self.enclosing = enclosing
|
||||
|
||||
def define(self, name: str, attr: object):
|
||||
"""Defines a new variable in the scope"""
|
||||
|
||||
self.map[name] = attr
|
||||
|
||||
def get(self, name: Token):
|
||||
"""Gets a variable"""
|
||||
|
||||
if name.lexeme in self.map:
|
||||
return self.map[name.lexeme]
|
||||
elif self.enclosing:
|
||||
return self.enclosing.get(name)
|
||||
raise JAPLError(name, f"Undefined name '{name.lexeme}'")
|
||||
|
||||
def get_at(self, distance, name):
|
||||
"""Gets a variable in a specific scope"""
|
||||
|
||||
return self.ancestor(distance).map.get(name)
|
||||
|
||||
def ancestor(self, distance):
|
||||
"""Finds the scope specified by distance"""
|
||||
|
||||
env = self
|
||||
for _ in range(distance):
|
||||
env = env.enclosing
|
||||
return env
|
||||
|
||||
def assign_at(self, distance, name, value):
|
||||
"""Same as get_at, but assigns instead of retrieving"""
|
||||
|
||||
self.ancestor(distance).map[name.lexeme] = value
|
||||
|
||||
def delete(self, var):
|
||||
"""Deletes a variable"""
|
||||
|
||||
if var.name.lexeme in self.map:
|
||||
del self.map[var.name.lexeme]
|
||||
elif self.enclosing:
|
||||
self.enclosing.delete(var)
|
||||
else:
|
||||
raise JAPLError(var.name, f"Undefined name '{var.name.lexeme}'")
|
||||
|
||||
def assign(self, name: Token, value: object):
|
||||
"""Assigns a variable"""
|
||||
|
||||
if name.lexeme in self.map:
|
||||
if isinstance(value, Variable):
|
||||
self.map[name.lexeme] = self.get(value.name)
|
||||
else:
|
||||
self.map[name.lexeme] = value
|
||||
elif self.enclosing:
|
||||
self.enclosing.assign(name, value)
|
||||
else:
|
||||
raise JAPLError(name, f"Undefined name '{name.lexeme}'")
|
|
@ -1,32 +0,0 @@
|
|||
from .tokentype import TokenType
|
||||
|
||||
|
||||
class JAPLError(BaseException):
|
||||
"""JAPL's exceptions base class"""
|
||||
|
||||
def __repr__(self):
|
||||
return self.args[1]
|
||||
|
||||
|
||||
class ParseError(JAPLError):
|
||||
"""An error occurred while parsing"""
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.args) > 1:
|
||||
message, token = self.args
|
||||
if token.kind == TokenType.EOF:
|
||||
return f"Unexpected error while parsing at line {token.line}, at end: {message}"
|
||||
else:
|
||||
return f"Unexpected error while parsing at line {token.line} at '{token.lexeme}': {message}"
|
||||
return self.args[0]
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class BreakException(JAPLError):
|
||||
"""Notifies a loop that it's time to break"""
|
||||
|
||||
|
||||
class ReturnException(JAPLError):
|
||||
"""Notifies a function that it's time to return"""
|
|
@ -1,167 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
from .tokenobject import Token
|
||||
from typing import List
|
||||
|
||||
|
||||
class Expression(object):
|
||||
"""
|
||||
An object representing a JAPL expression.
|
||||
This class is not meant to be instantiated directly,
|
||||
inherit from it instead!
|
||||
"""
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
class Visitor(ABC):
|
||||
"""
|
||||
Visitor abstract base class to implement
|
||||
the Visitor pattern
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def visit_literal(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_binary(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_grouping(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_unary(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def visit_get(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def visit_set(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class Binary(Expression):
|
||||
left: Expression
|
||||
operator: Token
|
||||
right: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_binary(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Unary(Expression):
|
||||
operator: Token
|
||||
right: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_unary(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Literal(Expression):
|
||||
value: object
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_literal(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Grouping(Expression):
|
||||
expr: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_grouping(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Variable(Expression):
|
||||
name: Token
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_var_expr(self)
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Assignment(Expression):
|
||||
name: Token
|
||||
value: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_assign(self)
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Logical(Expression):
|
||||
left: Expression
|
||||
operator: Token
|
||||
right: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_logical(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Call(Expression):
|
||||
callee: Expression
|
||||
paren: Token
|
||||
arguments: List[Expression] = ()
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_call_expr(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Get(Expression):
|
||||
object: Expression
|
||||
name: Token
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_get(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Set(Expression):
|
||||
object: Expression
|
||||
name: Token
|
||||
value: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_set(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class This(Expression):
|
||||
keyword: Token
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_this(self)
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Super(Expression):
|
||||
keyword: Token
|
||||
method: Token
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_super(self)
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from enum import Enum, auto
|
||||
|
||||
|
||||
class FunctionType(Enum):
|
||||
NONE = auto()
|
||||
FUNCTION = auto()
|
||||
METHOD = auto()
|
||||
INIT = auto()
|
|
@ -1,7 +0,0 @@
|
|||
from enum import Enum, auto
|
||||
|
||||
|
||||
class LoopType(Enum):
|
||||
|
||||
NONE = auto()
|
||||
WHILE = auto()
|
|
@ -1,173 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from .expression import Expression, Variable
|
||||
from .tokenobject import Token
|
||||
from typing import List, Any
|
||||
|
||||
|
||||
class Statement(object):
|
||||
"""
|
||||
A Base Class representing JAPL statements
|
||||
"""
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
class Visitor(ABC):
|
||||
"""Wrapper to implement the Visitor Pattern"""
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def visit_statement_expr(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_var_stmt(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_del(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_block(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_if(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_while(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_break(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_function(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def visit_return(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def visit_class(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class StatementExpr(Statement):
|
||||
"""
|
||||
An expression statement
|
||||
"""
|
||||
|
||||
expression: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_statement_expr(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Var(Statement):
|
||||
"""
|
||||
A var statement
|
||||
"""
|
||||
|
||||
name: Token
|
||||
init: Expression = None
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_var_stmt(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Del(Statement):
|
||||
"""
|
||||
A del statement
|
||||
"""
|
||||
|
||||
name: Any
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_del(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block(Statement):
|
||||
"""A block statement"""
|
||||
|
||||
statements: List[Statement]
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_block(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class If(Statement):
|
||||
"""An if statement"""
|
||||
|
||||
condition: Expression
|
||||
then_branch: Statement
|
||||
else_branch: Statement
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_if(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class While(Statement):
|
||||
"""A while statement"""
|
||||
|
||||
condition: Expression
|
||||
body: Statement
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_while(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Break(Statement):
|
||||
"""A break statement"""
|
||||
|
||||
token: Token
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_break(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Function(Statement):
|
||||
"""A function statement"""
|
||||
|
||||
name: Token
|
||||
params: List[Token]
|
||||
body: List[Statement]
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_function(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Return(Statement, BaseException):
|
||||
"""A return statement"""
|
||||
|
||||
keyword: Token
|
||||
value: Expression
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_return(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Class(Statement):
|
||||
"""A class statement"""
|
||||
|
||||
name: Token
|
||||
methods: list
|
||||
superclass: Variable
|
||||
|
||||
def accept(self, visitor):
|
||||
visitor.visit_class(self)
|
|
@ -1,13 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
from .tokentype import TokenType
|
||||
|
||||
@dataclass
|
||||
class Token(object):
|
||||
"""The representation of a JAPL token"""
|
||||
|
||||
kind: TokenType
|
||||
lexeme: str
|
||||
literal: object
|
||||
line: int
|
||||
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
from enum import Enum, auto
|
||||
|
||||
class TokenType(Enum):
|
||||
"""
|
||||
An enumeration for all JAPL types
|
||||
"""
|
||||
|
||||
LP = auto()
|
||||
RP = auto()
|
||||
LB = auto()
|
||||
RB = auto()
|
||||
COMMA = auto()
|
||||
DOT = auto()
|
||||
PLUS = auto()
|
||||
MINUS = auto()
|
||||
SLASH = auto()
|
||||
SEMICOLON = auto()
|
||||
STAR = auto()
|
||||
|
||||
|
||||
NEG = auto()
|
||||
NE = auto()
|
||||
EQ = auto()
|
||||
DEQ = auto()
|
||||
GT = auto()
|
||||
LT = auto()
|
||||
GE = auto()
|
||||
LE = auto()
|
||||
MOD = auto()
|
||||
POW = auto()
|
||||
|
||||
ID = auto()
|
||||
STR = auto()
|
||||
NUM = auto()
|
||||
|
||||
|
||||
AND = auto()
|
||||
CLASS = auto()
|
||||
ELSE = auto()
|
||||
FOR = auto()
|
||||
FUN = auto()
|
||||
FALSE = auto()
|
||||
IF = auto()
|
||||
NIL = auto()
|
||||
OR = auto()
|
||||
RETURN = auto()
|
||||
SUPER = auto()
|
||||
THIS = auto()
|
||||
TRUE = auto()
|
||||
VAR = auto()
|
||||
WHILE = auto()
|
||||
DEL = auto()
|
||||
BREAK = auto()
|
||||
|
||||
EOF = auto()
|
|
@ -1,444 +0,0 @@
|
|||
from .meta.exceptions import ParseError
|
||||
from .meta.tokentype import TokenType
|
||||
from .meta.tokenobject import Token
|
||||
from typing import List, Union
|
||||
from .meta.expression import Variable, Assignment, Logical, Call, Binary, Unary, Literal, Grouping, Expression, Get, Set, This, Super
|
||||
from .meta.statement import StatementExpr, Var, Del, Block, If, While, Break, Function, Return, Class
|
||||
|
||||
|
||||
class Parser(object):
|
||||
"""A simple recursive-descent top-down parser"""
|
||||
|
||||
def __init__(self, tokens: List[Token]):
|
||||
"""Object constructor"""
|
||||
|
||||
self.tokens = tokens
|
||||
self.current: int = 0
|
||||
|
||||
def check(self, token_type):
|
||||
"""
|
||||
Helper method for self.match
|
||||
"""
|
||||
|
||||
if self.done():
|
||||
return False
|
||||
elif self.peek().kind == token_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
def throw(self, token: Token, message: str) -> ParseError:
|
||||
"""Returns ParseError with the given message"""
|
||||
|
||||
return ParseError(token, message)
|
||||
|
||||
def synchronize(self):
|
||||
"""Synchronizes the parser's state to recover after
|
||||
an error occurred while parsing"""
|
||||
|
||||
self.step()
|
||||
while not self.done():
|
||||
if self.previous().kind == TokenType.SEMICOLON:
|
||||
break
|
||||
else:
|
||||
token_type = self.peek().kind
|
||||
if token_type in (
|
||||
TokenType.IF, TokenType.CLASS, TokenType.VAR, TokenType.FOR, TokenType.WHILE,
|
||||
TokenType.RETURN, TokenType.FUN
|
||||
):
|
||||
return
|
||||
self.step()
|
||||
|
||||
def peek(self):
|
||||
"""
|
||||
Returns a token without consuming it
|
||||
"""
|
||||
|
||||
return self.tokens[self.current]
|
||||
|
||||
def previous(self):
|
||||
"""
|
||||
Returns the most recently consumed token
|
||||
"""
|
||||
|
||||
return self.tokens[self.current - 1]
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
Returns True if we reached EOF
|
||||
"""
|
||||
|
||||
return self.peek().kind == TokenType.EOF
|
||||
|
||||
def match(self, *types: Union[TokenType, List[TokenType]]):
|
||||
"""
|
||||
Checks if the current token matches
|
||||
any of the given token type(s)
|
||||
"""
|
||||
|
||||
for token_type in types:
|
||||
if self.check(token_type):
|
||||
self.step()
|
||||
return True
|
||||
return False
|
||||
|
||||
def consume(self, token_type, message: str):
|
||||
"""
|
||||
Consumes a token, raises an error
|
||||
with the given message if the current token
|
||||
differs from the expected one
|
||||
"""
|
||||
|
||||
if self.check(token_type):
|
||||
return self.step()
|
||||
raise self.throw(self.peek(), message)
|
||||
|
||||
def primary(self):
|
||||
"""Parses unary expressions (literals)"""
|
||||
|
||||
if self.match(TokenType.FALSE):
|
||||
return Literal(False)
|
||||
elif self.match(TokenType.TRUE):
|
||||
return Literal(True)
|
||||
elif self.match(TokenType.NIL):
|
||||
return Literal(None)
|
||||
elif self.match(TokenType.NUM, TokenType.STR):
|
||||
return Literal(self.previous().literal)
|
||||
elif self.match(TokenType.LP):
|
||||
expr: Expression = self.expression()
|
||||
self.consume(TokenType.RP, "Unexpected error while parsing parenthesized expression")
|
||||
return Grouping(expr)
|
||||
elif self.match(TokenType.ID):
|
||||
return Variable(self.previous())
|
||||
elif self.match(TokenType.SUPER):
|
||||
keyword = self.previous()
|
||||
self.consume(TokenType.DOT, "Expecting '.' after 'super'")
|
||||
method = self.consume(TokenType.ID, "Expecting superclass method name")
|
||||
return Super(keyword, method)
|
||||
elif self.match(TokenType.THIS):
|
||||
return This(self.previous())
|
||||
raise self.throw(self.peek(), "Invalid syntax")
|
||||
|
||||
def finish_call(self, callee):
|
||||
"""Parses a function call"""
|
||||
|
||||
arguments = []
|
||||
if not self.check(TokenType.RP):
|
||||
while True:
|
||||
if len(arguments) >= 255:
|
||||
raise self.throw(self.peek(), "Cannot have more than 255 arguments")
|
||||
arguments.append(self.expression())
|
||||
if not self.match(TokenType.COMMA):
|
||||
break
|
||||
paren = self.consume(TokenType.RP, "Unexpected error while parsing call")
|
||||
return Call(callee, paren, arguments)
|
||||
|
||||
def call(self):
|
||||
"""Parses call expressions"""
|
||||
|
||||
expr = self.primary()
|
||||
while True:
|
||||
if self.match(TokenType.LP):
|
||||
expr = self.finish_call(expr)
|
||||
elif self.match(TokenType.DOT):
|
||||
name = self.consume(TokenType.ID, "Expecting property after '.'")
|
||||
expr = Get(expr, name)
|
||||
else:
|
||||
break
|
||||
return expr
|
||||
|
||||
def unary(self):
|
||||
"""Parses unary expressions"""
|
||||
|
||||
if self.match(TokenType.NEG, TokenType.MINUS):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.unary()
|
||||
return Unary(operator, right)
|
||||
return self.call()
|
||||
|
||||
def pow(self):
|
||||
"""Parses pow expressions"""
|
||||
|
||||
expr: Expression = self.unary()
|
||||
while self.match(TokenType.POW):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.unary()
|
||||
expr = Binary(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def multiplication(self):
|
||||
"""
|
||||
Parses multiplications and divisions
|
||||
"""
|
||||
|
||||
expr: Expression = self.pow()
|
||||
while self.match(TokenType.STAR, TokenType.SLASH, TokenType.MOD):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.pow()
|
||||
expr = Binary(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def addition(self):
|
||||
"""
|
||||
Parses additions and subtractions
|
||||
"""
|
||||
|
||||
expr: Expression = self.multiplication()
|
||||
while self.match(TokenType.PLUS, TokenType.MINUS):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.multiplication()
|
||||
expr = Binary(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def comparison(self):
|
||||
"""
|
||||
Parses comparison expressions
|
||||
"""
|
||||
|
||||
expr: Expression = self.addition()
|
||||
while self.match(TokenType.GT, TokenType.GE, TokenType.LT, TokenType.LE, TokenType.NE):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.addition()
|
||||
expr = Binary(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def equality(self):
|
||||
"""
|
||||
Parses equality expressions
|
||||
"""
|
||||
|
||||
expr: Expression = self.comparison()
|
||||
while self.match(TokenType.NEG, TokenType.DEQ):
|
||||
operator: Token = self.previous()
|
||||
right: Expression = self.comparison()
|
||||
expr = Binary(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def logical_and(self):
|
||||
"""Parses a logical and expression"""
|
||||
|
||||
expr = self.equality()
|
||||
while self.match(TokenType.AND):
|
||||
operator = self.previous()
|
||||
right = self.equality()
|
||||
expr = Logical(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def logical_or(self):
|
||||
"""Parses a logical or expression"""
|
||||
|
||||
expr = self.logical_and()
|
||||
while self.match(TokenType.OR):
|
||||
operator = self.previous()
|
||||
right = self.logical_and()
|
||||
expr = Logical(expr, operator, right)
|
||||
return expr
|
||||
|
||||
def assignment(self):
|
||||
"""
|
||||
Parses an assignment expression
|
||||
"""
|
||||
|
||||
expr = self.logical_or()
|
||||
if self.match(TokenType.EQ):
|
||||
eq = self.previous()
|
||||
value = self.assignment()
|
||||
if isinstance(expr, Variable):
|
||||
name = expr.name
|
||||
return Assignment(name, value)
|
||||
elif isinstance(expr, Get):
|
||||
return Set(expr.object, expr.name, value)
|
||||
raise self.throw(eq, "Invalid syntax")
|
||||
return expr
|
||||
|
||||
def expression(self):
|
||||
"""
|
||||
Parses an expression
|
||||
"""
|
||||
|
||||
return self.assignment()
|
||||
|
||||
def step(self):
|
||||
"""Steps 1 token forward"""
|
||||
|
||||
if not self.done():
|
||||
self.current += 1
|
||||
return self.previous()
|
||||
|
||||
def del_statement(self):
|
||||
"""Returns a del AST node"""
|
||||
|
||||
value = self.expression()
|
||||
self.consume(TokenType.SEMICOLON, "Missing semicolon after statement")
|
||||
return Del(value)
|
||||
|
||||
def expression_statement(self):
|
||||
"""Returns a StatemenrExpr AST node"""
|
||||
|
||||
value = self.expression()
|
||||
self.consume(TokenType.SEMICOLON, "Missing semicolon after statement")
|
||||
return StatementExpr(value)
|
||||
|
||||
def block(self):
|
||||
"""Returns a new environment to enable block scoping"""
|
||||
|
||||
statements = []
|
||||
while not self.check(TokenType.RB) and not self.done():
|
||||
statements.append(self.declaration())
|
||||
self.consume(TokenType.RB, "Unexpected end of block")
|
||||
return statements
|
||||
|
||||
def if_statement(self):
|
||||
"""Parses an IF statement"""
|
||||
|
||||
self.consume(TokenType.LP, "The if condition must be parenthesized")
|
||||
cond = self.expression()
|
||||
self.consume(TokenType.RP, "The if condition must be parenthesized")
|
||||
then_branch = self.statement()
|
||||
else_branch = None
|
||||
if self.match(TokenType.ELSE):
|
||||
else_branch = self.statement()
|
||||
return If(cond, then_branch, else_branch)
|
||||
|
||||
def while_statement(self):
|
||||
"""Parses a while statement"""
|
||||
|
||||
self.consume(TokenType.LP, "The while condition must be parenthesized")
|
||||
cond = self.expression()
|
||||
self.consume(TokenType.RP, "The while condition must be parenthesized")
|
||||
body = self.statement()
|
||||
return While(cond, body)
|
||||
|
||||
def for_statement(self):
|
||||
"""Parses a for statement"""
|
||||
|
||||
self.consume(TokenType.LP, "The for condition must be parenthesized")
|
||||
if self.match(TokenType.SEMICOLON):
|
||||
init = None
|
||||
elif self.match(TokenType.VAR):
|
||||
init = self.var_declaration()
|
||||
else:
|
||||
init = self.expression_statement()
|
||||
condition = None
|
||||
if not self.check(TokenType.SEMICOLON):
|
||||
condition = self.expression()
|
||||
self.consume(TokenType.SEMICOLON, "Missing semicolon after loop condition")
|
||||
incr = None
|
||||
if not self.check(TokenType.RP):
|
||||
incr = self.expression()
|
||||
self.consume(TokenType.RP, "The for condition must be parenthesized")
|
||||
body = self.statement()
|
||||
if incr:
|
||||
body = Block([body, StatementExpr(incr)])
|
||||
if not condition:
|
||||
condition = Literal(True)
|
||||
body = While(condition, body)
|
||||
if init:
|
||||
body = Block([init, body])
|
||||
return body
|
||||
|
||||
def break_statement(self):
|
||||
"""Parses a break statement"""
|
||||
|
||||
if self.check(TokenType.SEMICOLON):
|
||||
return self.step()
|
||||
raise ParseError(self.peek(), "Invalid syntax")
|
||||
|
||||
def return_statement(self):
|
||||
"""Parses a return statement"""
|
||||
|
||||
keyword = self.previous()
|
||||
value = None
|
||||
if not self.check(TokenType.SEMICOLON):
|
||||
value = self.expression()
|
||||
self.consume(TokenType.SEMICOLON, "Missing semicolon after statement")
|
||||
return Return(keyword, value)
|
||||
|
||||
def statement(self):
|
||||
"""Parses a statement"""
|
||||
|
||||
if self.match(TokenType.IF):
|
||||
return self.if_statement()
|
||||
elif self.match(TokenType.RETURN):
|
||||
return self.return_statement()
|
||||
elif self.match(TokenType.FOR):
|
||||
return self.for_statement()
|
||||
elif self.match(TokenType.WHILE):
|
||||
return self.while_statement()
|
||||
elif self.match(TokenType.BREAK):
|
||||
return Break(self.break_statement())
|
||||
elif self.match(TokenType.LB):
|
||||
return Block(self.block())
|
||||
elif self.match(TokenType.DEL):
|
||||
return self.del_statement()
|
||||
return self.expression_statement()
|
||||
|
||||
def var_declaration(self):
|
||||
"""Parses a var declaration"""
|
||||
|
||||
name = self.consume(TokenType.ID, "Expecting a variable name")
|
||||
init = None
|
||||
if self.match(TokenType.EQ):
|
||||
init = self.expression()
|
||||
self.consume(TokenType.SEMICOLON, "Missing semicolon after declaration")
|
||||
return Var(name, init)
|
||||
|
||||
def function(self, kind: str):
|
||||
"""Parses a function declaration"""
|
||||
|
||||
name = self.consume(TokenType.ID, f"Expecting {kind} name")
|
||||
self.consume(TokenType.LP, f"Expecting parenthesis after {kind} name")
|
||||
parameters = []
|
||||
if not self.check(TokenType.RP):
|
||||
while True:
|
||||
if len(parameters) >= 255:
|
||||
raise self.throw(self.peek(), "Cannot have more than 255 arguments")
|
||||
parameter = self.consume(TokenType.ID, "Expecting parameter name")
|
||||
if parameter in parameters:
|
||||
raise self.throw(self.peek(), "Multiple parameters with the same name in function declaration are not allowed")
|
||||
parameters.append(parameter)
|
||||
if not self.match(TokenType.COMMA):
|
||||
break
|
||||
self.consume(TokenType.RP, "Unexpected error while parsing function declaration")
|
||||
self.consume(TokenType.LB, f"Expecting '{{' before {kind} body")
|
||||
body = self.block()
|
||||
return Function(name, parameters, body)
|
||||
|
||||
def class_declaration(self):
|
||||
"""Parses a class declaration"""
|
||||
|
||||
name = self.consume(TokenType.ID, "Expecting class name")
|
||||
superclass = None
|
||||
if self.match(TokenType.LT):
|
||||
self.consume(TokenType.ID, "Expecting superclass name")
|
||||
superclass = Variable(self.previous())
|
||||
self.consume(TokenType.LB, "Expecting '{' before class body")
|
||||
methods = []
|
||||
while not self.check(TokenType.RB) and not self.done():
|
||||
methods.append(self.function("method"))
|
||||
self.consume(TokenType.RB, "Expecting '}' after class body")
|
||||
return Class(name, methods, superclass)
|
||||
|
||||
def declaration(self):
|
||||
"""Parses a declaration"""
|
||||
|
||||
try:
|
||||
if self.match(TokenType.CLASS):
|
||||
return self.class_declaration()
|
||||
elif self.match(TokenType.FUN):
|
||||
return self.function("function")
|
||||
elif self.match(TokenType.VAR):
|
||||
return self.var_declaration()
|
||||
return self.statement()
|
||||
except ParseError:
|
||||
self.synchronize()
|
||||
raise
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Starts to parse
|
||||
"""
|
||||
|
||||
statements = []
|
||||
while not self.done():
|
||||
statements.append(self.declaration())
|
||||
return statements
|
|
@ -1,281 +0,0 @@
|
|||
from .meta.exceptions import JAPLError
|
||||
from .meta.expression import Expression
|
||||
from .meta.statement import Statement
|
||||
from .meta.functiontype import FunctionType
|
||||
from .meta.classtype import ClassType
|
||||
from .meta.looptype import LoopType
|
||||
try:
|
||||
from functools import singledispatchmethod
|
||||
except ImportError:
|
||||
from singledispatchmethod import singledispatchmethod # Backport
|
||||
from typing import List, Union
|
||||
from collections import deque
|
||||
|
||||
|
||||
class Resolver(Expression.Visitor, Statement.Visitor):
|
||||
"""
|
||||
This class serves the purpose of correctly resolving
|
||||
name bindings (even with closures) efficiently
|
||||
"""
|
||||
|
||||
def __init__(self, interpreter):
|
||||
"""
|
||||
Object constructor
|
||||
"""
|
||||
|
||||
self.interpreter = interpreter
|
||||
self.scopes = deque()
|
||||
self.current_function = FunctionType.NONE
|
||||
self.current_loop = LoopType.NONE
|
||||
self.current_class = ClassType.NONE
|
||||
|
||||
@singledispatchmethod
|
||||
def resolve(self, stmt_or_expr: Union[Statement, Expression, List[Statement]]):
|
||||
"""Generic method to dispatch statements/expressions"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def begin_scope(self):
|
||||
"""
|
||||
Opens a new scope
|
||||
"""
|
||||
|
||||
self.scopes.append({})
|
||||
|
||||
def end_scope(self):
|
||||
"""
|
||||
Ends a scope
|
||||
"""
|
||||
|
||||
self.scopes.pop()
|
||||
|
||||
@resolve.register
|
||||
def resolve_statement(self, stmt: Statement):
|
||||
"""
|
||||
Resolves names for the given group
|
||||
of statements
|
||||
"""
|
||||
|
||||
stmt.accept(self)
|
||||
|
||||
@resolve.register
|
||||
def resolve_expression(self, expression: Expression):
|
||||
"""
|
||||
Resolves an expression
|
||||
"""
|
||||
|
||||
return expression.accept(self)
|
||||
|
||||
@resolve.register
|
||||
def resolve_statements(self, stmt: list):
|
||||
"""Resolves multiple statements"""
|
||||
|
||||
for statement in stmt:
|
||||
self.resolve(statement)
|
||||
|
||||
def declare(self, name):
|
||||
"""
|
||||
Declares a new variable
|
||||
"""
|
||||
|
||||
if not self.scopes:
|
||||
return
|
||||
scope = self.scopes[-1]
|
||||
if name.lexeme in scope:
|
||||
raise JAPLError(name, "Cannot re-declare the same variable in local scope, use assignment instead")
|
||||
scope[name.lexeme] = False
|
||||
|
||||
def define(self, name):
|
||||
"""
|
||||
Defines a new variable
|
||||
"""
|
||||
|
||||
if not self.scopes:
|
||||
return
|
||||
scope = self.scopes[-1]
|
||||
scope[name.lexeme] = True
|
||||
|
||||
def visit_block(self, block):
|
||||
"""Starts name resolution on a given block"""
|
||||
|
||||
self.begin_scope()
|
||||
self.resolve(block.statements)
|
||||
self.end_scope()
|
||||
|
||||
def visit_var_stmt(self, stmt):
|
||||
"""Visits a var statement node"""
|
||||
|
||||
self.declare(stmt.name)
|
||||
if stmt.init:
|
||||
self.resolve(stmt.init)
|
||||
self.define(stmt.name)
|
||||
|
||||
def visit_var_expr(self, expr):
|
||||
"""Visits a var expression node"""
|
||||
|
||||
if self.scopes and self.scopes[-1].get(expr.name.lexeme) is False:
|
||||
raise JAPLError(expr.name, f"Cannot read local variable in its own initializer")
|
||||
self.resolve_local(expr, expr.name)
|
||||
|
||||
def resolve_local(self, expr, name):
|
||||
"""Resolves local variables"""
|
||||
|
||||
i = 0
|
||||
for scope in reversed(self.scopes):
|
||||
if name.lexeme in scope:
|
||||
self.interpreter.resolve(expr, i)
|
||||
i += 1
|
||||
|
||||
def resolve_function(self, function, function_type: FunctionType):
|
||||
"""Resolves function objects"""
|
||||
|
||||
enclosing = self.current_function
|
||||
self.current_function = function_type
|
||||
self.begin_scope()
|
||||
for param in function.params:
|
||||
self.declare(param)
|
||||
self.define(param)
|
||||
self.resolve(function.body)
|
||||
self.end_scope()
|
||||
self.current_function = enclosing
|
||||
|
||||
def visit_assign(self, expr):
|
||||
"""Visits an assignment expression"""
|
||||
|
||||
self.resolve(expr.value)
|
||||
self.resolve_local(expr, expr.name)
|
||||
|
||||
def visit_function(self, stmt):
|
||||
"""Visits a function statement"""
|
||||
|
||||
self.declare(stmt.name)
|
||||
self.define(stmt.name)
|
||||
self.resolve_function(stmt, FunctionType.FUNCTION)
|
||||
|
||||
def visit_class(self, stmt):
|
||||
"""Visits a class statement"""
|
||||
|
||||
enclosing = self.current_class
|
||||
self.current_class = ClassType.CLASS
|
||||
self.declare(stmt.name)
|
||||
self.define(stmt.name)
|
||||
if stmt.superclass:
|
||||
if stmt.superclass.name.lexeme == stmt.name.lexeme:
|
||||
raise JAPLError(stmt.name, "A class cannot inherit from itself")
|
||||
self.resolve(stmt.superclass)
|
||||
self.begin_scope()
|
||||
self.scopes[-1]["super"] = True
|
||||
self.begin_scope()
|
||||
self.scopes[-1]["this"] = True
|
||||
for method in stmt.methods:
|
||||
ftype = FunctionType.METHOD
|
||||
if method.name.lexeme == "init":
|
||||
ftype = FunctionType.INIT
|
||||
self.resolve_function(method, ftype)
|
||||
self.end_scope()
|
||||
if stmt.superclass:
|
||||
self.end_scope()
|
||||
self.current_class = enclosing
|
||||
|
||||
def visit_statement_expr(self, stmt):
|
||||
"""Visits a statement expression node"""
|
||||
|
||||
self.resolve(stmt.expression)
|
||||
|
||||
def visit_if(self, stmt):
|
||||
"""Visits an if statement node"""
|
||||
|
||||
self.resolve(stmt.condition)
|
||||
self.resolve(stmt.then_branch)
|
||||
if stmt.else_branch:
|
||||
self.resolve(stmt.else_branch)
|
||||
|
||||
def visit_return(self, stmt):
|
||||
"""Visits a return statement node"""
|
||||
|
||||
if self.current_function == FunctionType.NONE:
|
||||
raise JAPLError(stmt.keyword, "'return' outside function")
|
||||
elif self.current_function == FunctionType.INIT:
|
||||
raise JAPLError(stmt.keyword, "Cannot explicitly return from constructor")
|
||||
elif stmt.value is not None:
|
||||
self.resolve(stmt.value)
|
||||
|
||||
def visit_while(self, stmt):
|
||||
"""Visits a while statement node"""
|
||||
|
||||
loop = self.current_loop
|
||||
self.current_loop = LoopType.WHILE
|
||||
self.resolve(stmt.condition)
|
||||
self.resolve(stmt.body)
|
||||
self.current_loop = loop
|
||||
|
||||
def visit_binary(self, expr):
|
||||
"""Visits a binary expression node"""
|
||||
|
||||
self.resolve(expr.left)
|
||||
self.resolve(expr.right)
|
||||
|
||||
def visit_call_expr(self, expr):
|
||||
"""Visits a call expression node"""
|
||||
|
||||
self.resolve(expr.callee)
|
||||
for argument in expr.arguments:
|
||||
self.resolve(argument)
|
||||
|
||||
def visit_grouping(self, expr):
|
||||
"""Visits a grouping expression"""
|
||||
|
||||
self.resolve(expr.expr)
|
||||
|
||||
def visit_literal(self, expr):
|
||||
"""Visits a literal node"""
|
||||
|
||||
return # Literal has no subexpressions and does not reference variables
|
||||
|
||||
def visit_logical(self, expr):
|
||||
"""Visits a logical node"""
|
||||
|
||||
self.visit_binary(expr) # No need to short circuit, so it's the same!
|
||||
|
||||
def visit_unary(self, expr):
|
||||
"""Visits a unary node"""
|
||||
|
||||
self.resolve(expr.right)
|
||||
|
||||
def visit_del(self, stmt):
|
||||
"""Visits a del statement"""
|
||||
|
||||
self.resolve(stmt.name)
|
||||
|
||||
def visit_break(self, stmt):
|
||||
"""Visits a break statement"""
|
||||
|
||||
if self.current_loop == LoopType.NONE:
|
||||
raise JAPLError("'break' outside loop")
|
||||
|
||||
def visit_get(self, expr):
|
||||
"""Visits a property get expression"""
|
||||
|
||||
self.resolve(expr.object)
|
||||
|
||||
def visit_set(self, expr):
|
||||
"""Visits a property set expression"""
|
||||
|
||||
self.resolve(expr.value)
|
||||
self.resolve(expr.object)
|
||||
|
||||
def visit_this(self, expr):
|
||||
"""Visits a 'this' expression"""
|
||||
|
||||
if self.current_class == ClassType.NONE:
|
||||
raise JAPLError(expr.keyword, "'this' outside class")
|
||||
self.resolve_local(expr, expr.keyword)
|
||||
|
||||
def visit_super(self, expr):
|
||||
"""Visits a 'super' expression"""
|
||||
|
||||
if self.current_class == ClassType.NONE:
|
||||
raise JAPLError(expr.keyword, "'super' outside class")
|
||||
self.resolve_local(expr, expr.keyword)
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
class Callable(object):
|
||||
"""A generic callable"""
|
||||
|
||||
def call(self, interpreter, arguments):
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, arity):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = arity
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from ..meta.exceptions import JAPLError
|
||||
from ..meta.tokenobject import Token
|
||||
|
||||
|
||||
class JAPLInstance:
|
||||
"""A class instance"""
|
||||
|
||||
def __init__(self, klass):
|
||||
self.klass = klass
|
||||
self.fields = {}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<instance of '{self.klass.name}'>"
|
||||
|
||||
def get(self, name: Token):
|
||||
if name.lexeme in self.fields:
|
||||
return self.fields[name.lexeme]
|
||||
meth = self.klass.get_method(name.lexeme)
|
||||
if meth:
|
||||
return meth.bind(self)
|
||||
raise JAPLError(name, f"Undefined property '{name.lexeme}'")
|
||||
|
||||
def set(self, name: Token, value: object):
|
||||
self.fields[name.lexeme] = value
|
|
@ -1,35 +0,0 @@
|
|||
from .callable import Callable
|
||||
from .instance import JAPLInstance
|
||||
|
||||
|
||||
class JAPLClass(Callable):
|
||||
"""A JAPL class"""
|
||||
|
||||
def __init__(self, name: str, methods: dict, superclass):
|
||||
self.name = name
|
||||
self.methods = methods
|
||||
self.superclass = superclass
|
||||
if self.get_method("init"):
|
||||
self.arity = self.get_method("init").arity
|
||||
else:
|
||||
self.arity = 0
|
||||
|
||||
def get_method(self, name: str):
|
||||
if name in self.methods:
|
||||
return self.methods[name]
|
||||
superclass = self.superclass
|
||||
while superclass:
|
||||
if name in superclass.methods:
|
||||
return superclass.methods[name]
|
||||
superclass = superclass.superclass
|
||||
|
||||
def __repr__(self):
|
||||
return f"<class '{self.name}'>"
|
||||
|
||||
def call(self, interpreter, arguments):
|
||||
instance = JAPLInstance(self)
|
||||
constructor = self.get_method("init")
|
||||
if constructor:
|
||||
constructor.bind(instance).call(interpreter, arguments)
|
||||
return instance
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
from .callable import Callable
|
||||
import time
|
||||
from ..meta.environment import Environment
|
||||
from ..meta.exceptions import ReturnException
|
||||
from .instance import JAPLInstance
|
||||
from .japlclass import JAPLClass
|
||||
|
||||
|
||||
class Clock(Callable):
|
||||
"""JAPL's wrapper around time.time"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 0
|
||||
|
||||
def call(self, *args):
|
||||
return time.time()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<built-in function clock>"
|
||||
|
||||
|
||||
class Type(Callable):
|
||||
"""JAPL's wrapper around type"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 1
|
||||
|
||||
def call(self, _, obj):
|
||||
return type(obj[0])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<built-in function type>"
|
||||
|
||||
|
||||
class Truthy(Callable):
|
||||
"""JAPL's wrapper around bool"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 1
|
||||
|
||||
def call(self, _, obj):
|
||||
return bool(obj[0])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<built-in function truthy>"
|
||||
|
||||
|
||||
class Stringify(Callable):
|
||||
"""JAPL's wrapper around str()"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 1
|
||||
|
||||
def call(self, _, obj):
|
||||
return str(obj[0])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<built-in function stringify>"
|
||||
|
||||
|
||||
class PrintFunction(Callable):
|
||||
"""The print function"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 1
|
||||
|
||||
def call(self, _, *args):
|
||||
print(*args[0])
|
||||
|
||||
def __repr__(self):
|
||||
return "<built-in function print>"
|
||||
|
||||
|
||||
class IsInstance(Callable):
|
||||
"""The isinstance function"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 2
|
||||
|
||||
def call(self, _, args):
|
||||
instance, klass = args
|
||||
if not isinstance(instance, JAPLInstance):
|
||||
return False
|
||||
elif not isinstance(klass, JAPLClass):
|
||||
return False
|
||||
return instance.klass == klass
|
||||
|
||||
def __repr__(self):
|
||||
return "<built-in function isinstance>"
|
||||
|
||||
|
||||
class IsSubclass(Callable):
|
||||
"""The isinstance function"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 2
|
||||
|
||||
def call(self, _, args):
|
||||
first, second = args
|
||||
if not isinstance(first, JAPLClass):
|
||||
return False
|
||||
elif not isinstance(second, JAPLClass):
|
||||
return False
|
||||
return first.superclass == second
|
||||
|
||||
def __repr__(self):
|
||||
return "<built-in function issubclass>"
|
||||
|
||||
|
||||
class IsSuperclass(Callable):
|
||||
"""The isinstance function"""
|
||||
|
||||
def __init__(self, *_):
|
||||
"""Object constructor"""
|
||||
|
||||
self.arity = 2
|
||||
|
||||
def call(self, _, args):
|
||||
first, second = args
|
||||
if not isinstance(first, JAPLClass):
|
||||
return False
|
||||
elif not isinstance(second, JAPLClass):
|
||||
return False
|
||||
return second.superclass == first
|
||||
|
||||
def __repr__(self):
|
||||
return "<built-in function issuperclass>"
|
||||
|
||||
|
||||
class JAPLFunction(Callable):
|
||||
"""A generic wrapper for user-defined functions"""
|
||||
|
||||
def __init__(self, declaration, closure):
|
||||
"""Object constructor"""
|
||||
|
||||
self.declaration = declaration
|
||||
self._repr = f"<function {self.declaration.name.lexeme}>"
|
||||
self.arity = len(self.declaration.params)
|
||||
self.closure = closure
|
||||
|
||||
def bind(self, obj: object):
|
||||
"""Binds a method to an object"""
|
||||
|
||||
env = Environment(self.closure)
|
||||
env.define("this", obj)
|
||||
func = type(self)(self.declaration, env)
|
||||
func._repr = f"<bound method {func.declaration.name.lexeme} of object {obj.klass.name}>"
|
||||
return func
|
||||
|
||||
def call(self, interpreter, arguments):
|
||||
scope = Environment(self.closure)
|
||||
for name, value in zip(self.declaration.params, arguments):
|
||||
scope.define(name.lexeme, value)
|
||||
try:
|
||||
interpreter.execute_block(self.declaration.body, scope)
|
||||
except ReturnException as error:
|
||||
return error.args[0]
|
||||
|
||||
def __repr__(self):
|
||||
return self._repr
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
from JAPL.lexer import Lexer
|
||||
from JAPL.meta.exceptions import ParseError, JAPLError
|
||||
from JAPL.resolver import Resolver
|
||||
from JAPL.parser import Parser
|
||||
from JAPL.interpreter import Interpreter
|
||||
|
||||
|
||||
class JAPL(object):
|
||||
"""Wrapper around JAPL's interpreter, lexer and parser"""
|
||||
|
||||
interpreter = Interpreter()
|
||||
resolver = Resolver(interpreter)
|
||||
|
||||
def run(self, file: str):
|
||||
"""Runs a file"""
|
||||
|
||||
if not file:
|
||||
self.repl()
|
||||
else:
|
||||
try:
|
||||
with open(file) as source_file:
|
||||
source_code = source_file.read()
|
||||
lexer = Lexer(source_code)
|
||||
tokens = lexer.lex()
|
||||
parser = Parser(tokens)
|
||||
ast = parser.parse()
|
||||
self.resolver.resolve(ast)
|
||||
self.interpreter.interpret(ast)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: '{file}', no such file or directory")
|
||||
except PermissionError:
|
||||
print(f"Error' '{file}', permission denied")
|
||||
except JAPLError as err:
|
||||
if len(err.args) == 2:
|
||||
token, message = err.args
|
||||
print(f"An exception occurred at line {token.line}, file '{file}' at '{token.lexeme}': {message}")
|
||||
else:
|
||||
print(f"An exception occurred, details below\n\n{type(err).__name__}: {err}")
|
||||
|
||||
def repl(self):
|
||||
"""Starts an interactive REPL"""
|
||||
|
||||
print("[JAPL 0.1.1 - Interactive REPL]")
|
||||
while True:
|
||||
try:
|
||||
source = input(">>> ")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
return
|
||||
if not source:
|
||||
continue
|
||||
lexer = Lexer(source)
|
||||
try:
|
||||
tokens = lexer.lex()
|
||||
ast = Parser(tokens).parse()
|
||||
self.resolver.resolve(ast)
|
||||
result = self.interpreter.interpret(ast)
|
||||
except ParseError as err:
|
||||
if len(err.args) == 2:
|
||||
token, message = err.args
|
||||
print(f"An exception occurred at line {token.line} at '{token.lexeme}': {message}")
|
||||
else:
|
||||
print(f"\nAn exception occurred, details below\n\nParseError: {err.args[0]}")
|
||||
except JAPLError as error:
|
||||
if len(error.args) == 2:
|
||||
token, message = error.args
|
||||
print(f"An exception occurred at line {token.line}, file 'stdin' at '{token.lexeme}': {message}")
|
||||
else:
|
||||
print(f"An exception occurred, details below\n\n{type(error).__name__}: {error}")
|
|
@ -1,9 +0,0 @@
|
|||
import sys
|
||||
from JAPL.wrapper import JAPL
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 1:
|
||||
JAPL().repl()
|
||||
else:
|
||||
JAPL().run(sys.argv[1])
|
|
@ -1 +0,0 @@
|
|||
singledispatchmethod
|
|
@ -1,16 +0,0 @@
|
|||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
name="JAPL - Just another programming language",
|
||||
version="0.1.1",
|
||||
author="nocturn9x",
|
||||
author_email="nocturn9x@intellivoid.net",
|
||||
description="The JAPL programming language",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
)
|
|
@ -38,10 +38,14 @@ type
|
|||
|
||||
|
||||
func handleInterrupt() {.noconv.} =
|
||||
## Raises an appropriate exception
|
||||
## to let us catch and handle
|
||||
## Ctrl+C gracefully
|
||||
raise newException(KeyboardInterrupt, "Ctrl+C")
|
||||
|
||||
|
||||
proc resetStack*(self: var VM) =
|
||||
## Resets the VM stack to a blank state
|
||||
self.stack = @[]
|
||||
self.frames = @[]
|
||||
self.frameCount = 0
|
||||
|
@ -50,6 +54,7 @@ proc resetStack*(self: var VM) =
|
|||
|
||||
|
||||
proc error*(self: var VM, error: ptr JAPLException) =
|
||||
## Reports runtime errors with a nice traceback
|
||||
var previous = "" # All this stuff seems overkill, but it makes the traceback look nicer
|
||||
var repCount = 0 # and if we are here we are far beyond a point where performance matters
|
||||
var mainReached = false
|
||||
|
@ -79,26 +84,36 @@ proc error*(self: var VM, error: ptr JAPLException) =
|
|||
|
||||
|
||||
proc pop*(self: var VM): Value =
|
||||
## Pops a value off the stack
|
||||
result = self.stack.pop()
|
||||
self.stackTop -= 1
|
||||
|
||||
|
||||
proc push*(self: var VM, value: Value) =
|
||||
## Pushes a value onto the stack
|
||||
self.stack.add(value)
|
||||
self.stackTop += 1
|
||||
|
||||
|
||||
proc peek*(self: var VM, distance: int): Value =
|
||||
## Peeks a value (at a given disnatance from the
|
||||
## current index) from the stack
|
||||
return self.stack[self.stackTop - distance - 1]
|
||||
|
||||
|
||||
template addObject*(self: ptr VM, obj: ptr Obj): untyped =
|
||||
## Stores an object in the VM's internal
|
||||
## list of objects in order to reclaim
|
||||
## its memory later
|
||||
let temp = obj
|
||||
self.objects.add(temp)
|
||||
temp
|
||||
|
||||
|
||||
proc slice(self: var VM): bool =
|
||||
## Handles single-operator slice expressions
|
||||
## (consider moving this to an appropriate
|
||||
## slice method)
|
||||
var idx = self.pop()
|
||||
var peeked = self.pop()
|
||||
case peeked.kind:
|
||||
|
@ -129,6 +144,8 @@ proc slice(self: var VM): bool =
|
|||
|
||||
|
||||
proc sliceRange(self: var VM): bool =
|
||||
## Handles slices when there's both a start
|
||||
## and an end index (even implicit ones)
|
||||
var sliceEnd = self.pop()
|
||||
var sliceStart = self.pop()
|
||||
var popped = self.pop()
|
||||
|
@ -167,6 +184,8 @@ proc sliceRange(self: var VM): bool =
|
|||
|
||||
|
||||
proc call(self: var VM, function: ptr Function, argCount: uint8): bool =
|
||||
## Sets up the call frame and performs error checking
|
||||
## when calling callables
|
||||
var argCount = int argCount
|
||||
if argCount != function.arity:
|
||||
self.error(newTypeError(&"function '{stringify(function.name)}' takes {function.arity} argument(s), got {argCount}"))
|
||||
|
@ -174,45 +193,63 @@ proc call(self: var VM, function: ptr Function, argCount: uint8): bool =
|
|||
if self.frameCount == FRAMES_MAX:
|
||||
self.error(newRecursionError("max recursion depth exceeded"))
|
||||
return false
|
||||
var frame = CallFrame(function: function, ip: 0, slot: argCount, stack: self.stack)
|
||||
var frame = CallFrame(function: function, ip: 0, slot: argCount, endSlot: self.stackTop, stack: self.stack) # TODO:
|
||||
# Check why this raises NilAccessError when high recursion limit is hit
|
||||
self.frames.add(frame)
|
||||
self.frameCount += 1
|
||||
return true
|
||||
|
||||
|
||||
proc callValue(self: var VM, callee: Value, argCount: uint8): bool =
|
||||
if callee.isObj():
|
||||
## Wrapper around call() to do type checking
|
||||
if callee.isObj(): # TODO: Consider adding a callable() method
|
||||
case callee.obj.kind:
|
||||
of ObjectType.Function:
|
||||
return self.call(cast[ptr Function](callee.obj), argCount)
|
||||
else:
|
||||
discard
|
||||
discard # Not callable
|
||||
self.error(newTypeError(&"object of type '{callee.typeName}' is not callable"))
|
||||
return false
|
||||
|
||||
|
||||
proc run(self: var VM, repl: bool): InterpretResult =
|
||||
## Chews trough bytecode instructions executing
|
||||
## them one at a time, this is the runtime's
|
||||
## main loop
|
||||
var frame = self.frames[self.frameCount - 1]
|
||||
template readByte: untyped =
|
||||
## Reads a single byte from the current
|
||||
## frame's chunk of bytecode
|
||||
inc(frame.ip)
|
||||
frame.function.chunk.code[frame.ip - 1]
|
||||
template readBytes: untyped =
|
||||
## Reads and decodes 3 bytes from the
|
||||
## current frame's chunk into an integer
|
||||
var arr = [readByte(), readByte(), readByte()]
|
||||
var index: int
|
||||
copyMem(index.addr, unsafeAddr(arr), sizeof(arr))
|
||||
index
|
||||
template readShort: untyped =
|
||||
## Reads a 16 bit number from the
|
||||
## current frame's chunk
|
||||
inc(frame.ip)
|
||||
inc(frame.ip)
|
||||
cast[uint16]((frame.function.chunk.code[frame.ip - 2] shl 8) or frame.function.chunk.code[frame.ip - 1])
|
||||
template readConstant: Value =
|
||||
## Reads a constant from the current
|
||||
## frame's constant table
|
||||
frame.function.chunk.consts.values[int(readByte())]
|
||||
template readLongConstant: Value =
|
||||
## Reads a long constant from the
|
||||
## current frame's constant table
|
||||
var arr = [readByte(), readByte(), readByte()]
|
||||
var idx: int
|
||||
copyMem(idx.addr, unsafeAddr(arr), sizeof(arr))
|
||||
frame.function.chunk.consts.values[idx]
|
||||
template binOp(op, check) =
|
||||
## Performs binary operations on types,
|
||||
## this will be soon ditched in favor
|
||||
## of a more idiomatic a.op(b)
|
||||
var rightVal {.inject.} = self.pop()
|
||||
var leftVal {.inject.} = self.pop()
|
||||
if leftVal.isInf():
|
||||
|
@ -277,6 +314,7 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
self.error(newTypeError(&"unsupported binary operator for objects of type '{leftVal.typeName()}' and '{rightVal.typeName()}'"))
|
||||
return RUNTIME_ERROR
|
||||
template binBitWise(op): untyped =
|
||||
## Handles binary bitwise operators
|
||||
var rightVal {.inject.} = self.pop()
|
||||
var leftVal {.inject.} = self.pop()
|
||||
if isInt(leftVal) and isInt(rightVal):
|
||||
|
@ -285,19 +323,20 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
self.error(newTypeError(&"unsupported binary operator for objects of type '{leftVal.typeName()}' and '{rightVal.typeName()}'"))
|
||||
return RUNTIME_ERROR
|
||||
template unBitWise(op): untyped =
|
||||
var leftVal {.inject.} = self.pop()
|
||||
if isInt(leftVal):
|
||||
self.push(Value(kind: INTEGER, intValue: `op`(leftVal.toInt())))
|
||||
else:
|
||||
self.error(newTypeError(&"unsupported unary operator for object of type '{leftVal.typeName()}'"))
|
||||
return RUNTIME_ERROR
|
||||
## Handles unary bitwise operators
|
||||
var leftVal {.inject.} = self.pop()
|
||||
if isInt(leftVal):
|
||||
self.push(Value(kind: INTEGER, intValue: `op`(leftVal.toInt())))
|
||||
else:
|
||||
self.error(newTypeError(&"unsupported unary operator for object of type '{leftVal.typeName()}'"))
|
||||
return RUNTIME_ERROR
|
||||
var instruction: uint8
|
||||
var opcode: OpCode
|
||||
while true:
|
||||
{.computedgoto.}
|
||||
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
|
||||
instruction = readByte()
|
||||
opcode = OpCode(instruction)
|
||||
when DEBUG_TRACE_VM:
|
||||
when DEBUG_TRACE_VM: # Insight inside the VM
|
||||
stdout.write("Current VM stack status: [")
|
||||
for v in self.stack:
|
||||
stdout.write(stringify(v))
|
||||
|
@ -322,7 +361,7 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
stdout.write(", ")
|
||||
stdout.write("]\n")
|
||||
discard disassembleInstruction(frame.function.chunk, frame.ip - 1)
|
||||
case opcode:
|
||||
case opcode: # Main OpCodes dispatcher
|
||||
of OpCode.Constant:
|
||||
var constant: Value = readConstant()
|
||||
self.push(constant)
|
||||
|
@ -471,7 +510,7 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
else:
|
||||
self.globals[constant] = self.peek(0)
|
||||
of OpCode.DeleteGlobal:
|
||||
# This OpCode, as well as OP_DELETE_LOCAL, is currently unused due to issues with the GC
|
||||
# This OpCode, as well as DeleteLocal, is currently unused due to potential issues with the GC
|
||||
if frame.function.chunk.consts.values.len > 255:
|
||||
var constant = readLongConstant().toStr()
|
||||
if constant notin self.globals:
|
||||
|
@ -505,10 +544,8 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
if frame.len > 255:
|
||||
var slot = readBytes()
|
||||
frame.delete(slot)
|
||||
# TODO unimplemented
|
||||
else:
|
||||
var slot = readByte()
|
||||
# TODO unimplemented
|
||||
frame.delete(int slot)
|
||||
of OpCode.Pop:
|
||||
self.lastPop = self.pop()
|
||||
|
@ -532,7 +569,8 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
of OpCode.Return:
|
||||
var retResult = self.pop()
|
||||
if repl:
|
||||
if not self.lastPop.isNil():
|
||||
if not self.lastPop.isNil() and self.frameCount == 1: # This is to avoid long outputs
|
||||
# with recursive calls
|
||||
echo stringify(self.lastPop)
|
||||
self.lastPop = Value(kind: ValueType.Nil) # TODO: asNil()?
|
||||
self.frameCount -= 1
|
||||
|
@ -546,6 +584,8 @@ proc run(self: var VM, repl: bool): InterpretResult =
|
|||
|
||||
|
||||
proc freeObject(obj: ptr Obj) =
|
||||
## Frees the associated memory
|
||||
## of an object
|
||||
case obj.kind:
|
||||
of ObjectType.Function: # Having function before string is important so that
|
||||
# the function's name is never freed before the object itself
|
||||
|
@ -565,6 +605,8 @@ proc freeObject(obj: ptr Obj) =
|
|||
|
||||
|
||||
proc freeObjects(self: var VM) =
|
||||
## Fress all the allocated objects
|
||||
## from the VM
|
||||
var objCount = len(self.objects)
|
||||
for obj in reversed(self.objects):
|
||||
freeObject(obj)
|
||||
|
@ -574,6 +616,7 @@ proc freeObjects(self: var VM) =
|
|||
|
||||
|
||||
proc freeVM*(self: var VM) =
|
||||
## Tears down the VM
|
||||
when DEBUG_TRACE_ALLOCATION:
|
||||
echo "\nFreeing all allocated memory before exiting"
|
||||
unsetControlCHook()
|
||||
|
@ -585,12 +628,14 @@ proc freeVM*(self: var VM) =
|
|||
|
||||
|
||||
proc initVM*(): VM =
|
||||
## Initializes the VM
|
||||
setControlCHook(handleInterrupt)
|
||||
result = VM(lastPop: Value(kind: ValueType.Nil), objects: @[], globals: initTable[string, Value](), source: "", file: "")
|
||||
# TODO asNil() ?
|
||||
|
||||
|
||||
proc interpret*(self: var VM, source: string, repl: bool = false, file: string): InterpretResult =
|
||||
## Interprets a source string containing JAPL code
|
||||
self.resetStack()
|
||||
var compiler = initCompiler(SCRIPT, file=file)
|
||||
var compiled = compiler.compile(source)
|
Loading…
Reference in New Issue