Added JAPL 0.1.1

This commit is contained in:
nocturn9x 2020-07-29 16:14:17 +02:00
parent a5dac14f2b
commit 3623efc798
22 changed files with 1614 additions and 0 deletions

4
JAPL/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .lexer import Lexer
from .parser import Parser
from .interpreter import Interpreter

263
JAPL/interpreter.py Normal file
View File

@ -0,0 +1,263 @@
from .meta.expression import Expression, Variable, Call
from .meta.tokentype import TokenType
from .meta.exceptions import JAPLError, BreakException, ReturnException
from .meta.statement import Statement
from .meta.environment import Environment
from .types.callable import Callable
import operator
from .types.native import Clock, Type, JAPLFunction, Truthy, Stringify
from .wrapper import JAPL
class Interpreter(object):
"""
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.globals = self.environment
self.globals.define("clock", Clock())
self.globals.define("type", Type())
self.globals.define("truthy", Truthy())
self.globals.define("stringify", Stringify())
self.looping = False
self.in_function = False
def number_operand(self, operator, operand):
"""
An helper method to check if the operand
to a unary operator is a number
"""
if isinstance(operand, (int, float)):
return
raise JAPLError(operator, f"Unsupported unary operator '{operator.lexeme}' for object of type '{type(operand).__name__}'")
def compatible_operands(self, operator, left, right):
"""
Helper method to check types when doing binary
operations
"""
if operator.kind == TokenType.SLASH and right == 0:
raise JAPLError(operator, "Cannot divide by 0")
elif isinstance(left, (bool, type(None))) or isinstance(right, (bool, type(None))):
if operator.kind not in (TokenType.DEQ, TokenType.NE):
raise JAPLError(operator, f"Unsupported binary operator '{operator.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 operator.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 '{operator.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'")
def visit_literal(self, visitor: Expression.Visitor):
"""
Visits a Literal node in the Abstract Syntax Tree,
returning its value to the visitor
"""
return visitor.value
def visit_logical(self, expr):
"""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 interprerer's appropiate method to
evaluate itself
"""
return expr.accept(self)
def visit_grouping(self, grouping: Expression):
"""
Visits a Grouping node in the Abstract Syntax Tree,
recursively evaluating its subexpressions
"""
return self.eval(grouping.expr)
def visit_unary(self, expr: Expression):
"""
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: Expression):
"""
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_print(self, stmt: Statement):
"""
Visits the print statement node in the AST and
evaluates its expression before printing it to
stdout
"""
val = self.eval(stmt.expression)
print(val)
def visit_statement_expr(self, stmt: Statement):
"""Visits an expression statement and evaluates it"""
self.eval(stmt.expression)
def visit_if(self, statement):
"""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_while(self, statement):
"""Visits a while node and executes it"""
self.looping = True
while self.eval(statement.condition):
try:
self.exec(statement.body)
except BreakException:
break
self.looping = False
def visit_var_stmt(self, stmt: Statement):
"""Vitits a var statement"""
val = None
if stmt.init:
val = self.eval(stmt.init)
self.environment.define(stmt.name.lexeme, val)
def visit_var_expr(self, expr: Variable):
"""Visits a var expression"""
return self.environment.get(expr.name)
def visit_del(self, stmt: Statement):
"""Visits a del expression"""
return self.environment.delete(stmt.name)
def visit_assign(self, visitor):
"""Visits an assignment expression"""
right = self.eval(visitor.value)
self.environment.assign(visitor.name, right)
def visit_block(self, visitor):
"""Visits a new scope block"""
return self.execute_block(visitor.statements, Environment(self.environment))
def visit_break(self, visitor):
"""Visits a break statement"""
if self.looping:
raise BreakException()
raise JAPLError(visitor.token, "'break' outside loop")
def visit_call_expr(self, expr: Expression):
"""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, 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):
"""Visits a return statement"""
if self.in_function:
value = None
if statement.value:
value = self.eval(statement.value)
raise ReturnException(value)
else:
raise JAPLError(statement.keyword, "'return' outside function")
def visit_function(self, statement):
"""Visits a function"""
function = JAPLFunction(statement)
self.environment.define(statement.name.lexeme, function)
def exec(self, statement: Statement):
"""Executes a statement"""
statement.accept(self)
def interpret(self, statements):
"""
Executes a JAPL program
"""
for statement in statements:
self.exec(statement)

190
JAPL/lexer.py Normal file
View File

@ -0,0 +1,190 @@
from .objects import Token, TokenType, ParseError
from typing import List, Union
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,
"print": TokenType.PRINT, "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
"""
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 match(self, char: str) -> bool:
"""
Returns True if the next 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.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

0
JAPL/meta/__init__.py Normal file
View File

53
JAPL/meta/environment.py Normal file
View File

@ -0,0 +1,53 @@
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 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}'")

30
JAPL/meta/exceptions.py Normal file
View File

@ -0,0 +1,30 @@
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:
token, message = 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"""

106
JAPL/meta/expression.py Normal file
View File

@ -0,0 +1,106 @@
from dataclasses import dataclass
from abc import ABC, abstractmethod
from .tokenobject import Token
from typing import List
class Expression(ABC):
"""
An object representing a JAPL expression.
This is an abstract base class and is not
meant to be instantiated directly, inherit
from it instead!
"""
class Visitor(ABC):
"""
Visitor abstract base class to implement
the Visitor pattern
"""
@abstractmethod
def accept(self, visitor):
raise NotImplementedError
def visit_literal(self, visitor):
raise NotImplementedError
def visit_binary(self, visitor):
raise NotImplementedError
def visit_grouping(self, visitor):
raise NotImplementedError
def visit_unary(self, visitor):
raise NotImplementedError
@dataclass
class Binary(Expression, Expression.Visitor):
left: Expression
operator: Token
right: Expression
def accept(self, visitor):
return visitor.visit_binary(self)
@dataclass
class Unary(Expression, Expression.Visitor):
operator: Token
right: Expression
def accept(self, visitor):
return visitor.visit_unary(self)
@dataclass
class Literal(Expression, Expression.Visitor):
value: object
def accept(self, visitor):
return visitor.visit_literal(self)
@dataclass
class Grouping(Expression, Expression.Visitor):
expr: Expression
def accept(self, visitor):
return visitor.visit_grouping(self)
@dataclass
class Variable(Expression, Expression.Visitor):
name: str
def accept(self, visitor):
return visitor.visit_var_expr(self)
@dataclass
class Assignment(Expression, Expression.Visitor):
name: Token
value: Expression
def accept(self, visitor):
return visitor.visit_assign(self)
@dataclass
class Logical(Expression, Expression.Visitor):
left: Expression
operator: Token
right: Expression
def accept(self, visitor):
return visitor.visit_logical(self)
@dataclass
class Call(Expression, Expression.Visitor):
callee: Expression
paren: Token
arguments: List[Expression]
def accept(self, visitor):
return visitor.visit_call_expr(self)

136
JAPL/meta/statement.py Normal file
View File

@ -0,0 +1,136 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from .expression import Expression
from .tokenobject import Token
from typing import List
class Statement(ABC):
"""
An Abstract Base Class representing
JAPL's statements
"""
class Visitor(ABC):
"""Wrapper to implement the Visitor Pattern"""
@abstractmethod
def accept(self, visitor):
raise NotImplementedError
def visit_print(self, visitor):
raise NotImplementedError
def visit_expr(self, visitor):
raise NotImplementedError
@dataclass
class Print(Statement, Statement.Visitor):
"""
The print statement
"""
expression: Expression
def accept(self, visitor):
visitor.visit_print(self)
@dataclass
class StatementExpr(Statement, Statement.Visitor):
"""
An expression statement
"""
expression: Expression
def accept(self, visitor):
visitor.visit_statement_expr(self)
@dataclass
class Var(Statement, Statement.Visitor):
"""
A var statement
"""
name: Token
init: Expression = None
def accept(self, visitor):
visitor.visit_var_stmt(self)
@dataclass
class Del(Statement, Statement.Visitor):
"""
A del statement
"""
name: Token
def accept(self, visitor):
visitor.visit_del(self)
@dataclass
class Block(Statement, Statement.Visitor):
"""A block statement"""
statements: List[Statement]
def accept(self, visitor):
visitor.visit_block(self)
@dataclass
class If(Statement, Statement.Visitor):
"""An if statement"""
condition: Expression
then_branch: Statement
else_branch: Statement
def accept(self, visitor):
visitor.visit_if(self)
@dataclass
class While(Statement, Statement.Visitor):
"""A while statement"""
condition: Expression
body: Statement
def accept(self, visitor):
visitor.visit_while(self)
@dataclass
class Break(Statement, Statement.Visitor):
"""A break statement"""
token: Token
def accept(self, visitor):
visitor.visit_break(self)
@dataclass
class Function(Statement, Statement.Visitor):
"""A function statement"""
name: Token
params: List[Token]
body: List[Statement]
def accept(self, visitor):
visitor.visit_function(self)
@dataclass
class Return(Statement, Statement.Visitor, BaseException):
"""A return statement"""
keyword: Token
value: Expression
def accept(self, visitor):
visitor.visit_return(self)

13
JAPL/meta/tokenobject.py Normal file
View File

@ -0,0 +1,13 @@
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

56
JAPL/meta/tokentype.py Normal file
View File

@ -0,0 +1,56 @@
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()
PRINT = auto()
RETURN = auto()
SUPER = auto()
THIS = auto()
TRUE = auto()
VAR = auto()
WHILE = auto()
DEL = auto()
BREAK = auto()
EOF = auto()

5
JAPL/objects.py Normal file
View File

@ -0,0 +1,5 @@
from .meta.statement import Statement, Print
from .meta.expression import Expression, Unary, Binary, Grouping, Literal
from .meta.exceptions import ParseError, JAPLError
from .meta.tokentype import TokenType
from .meta.tokenobject import Token

422
JAPL/parser.py Normal file
View File

@ -0,0 +1,422 @@
from .objects import Expression, Token, TokenType, Binary, Unary, \
Literal, Grouping, ParseError, JAPLError
from .meta.statement import Print, StatementExpr, Var, Del, Block, \
If, While, Break, Function, Return
from .meta.expression import Variable, Assignment, Logical, Call
from typing import List
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, message: str, token: Token) -> ParseError:
"""Returns ParseError with the given message"""
return ParseError(message, token)
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.PRINT,
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: list):
"""
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())
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)
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)
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 print_statement(self):
"""Returns a Print AST node"""
value = self.expression()
self.consume(TokenType.SEMICOLON, "Missing semicolon after print statement")
return Print(value)
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.PRINT):
return self.print_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")
parameters.append(self.consume(TokenType.ID, "Expecting parameter name"))
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 declaration(self):
"""Parses a declaration"""
try:
if 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

0
JAPL/types/__init__.py Normal file
View File

29
JAPL/types/callable.py Normal file
View File

@ -0,0 +1,29 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
class CallableBase(ABC):
"""Abstract base class for callables"""
def __init__(self, arity):
"""Object constructor"""
self.arity: int = arity
@abstractmethod
def call(self, interpreter, arguments):
"""Calls the callable"""
raise NotImplementedError
class Callable(CallableBase):
"""A generic callable"""
def call(self):
...
def __init__(self, arity):
"""Object constructor"""
self.arity: int = arity

90
JAPL/types/native.py Normal file
View File

@ -0,0 +1,90 @@
from .callable import Callable
import time
from ..meta.environment import Environment
from ..meta.exceptions import ReturnException
class Clock(Callable):
"""JAPL's wrapper around time.time"""
def __init__(self, *args):
"""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, *args):
"""Object constructor"""
self.arity = 1
def call(self, _, obj):
return type(obj[0])
def __repr__(self):
return f"<built-in function type>"
class JAPLFunction(Callable):
"""A generic wrapper for user-defined functions"""
def __init__(self, declaration):
"""Object constructor"""
self.declaration = declaration
self.arity = len(self.declaration.params)
def call(self, interpreter, arguments):
scope = Environment(interpreter.globals)
for name, value in zip(self.declaration.params, arguments):
scope.define(name.lexeme, value)
interpreter.in_function = True
try:
interpreter.execute_block(self.declaration.body, scope)
except ReturnException as error:
interpreter.in_function = False
return error.args[0]
interpreter.in_function = False
def __repr__(self):
return f"<function {self.declaration.name.lexeme}>"
class Truthy(Callable):
"""JAPL's wrapper around bool"""
def __init__(self, *args):
"""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, *args):
"""Object constructor"""
self.arity = 1
def call(self, _, obj):
return str(obj[0])
def __repr__(self):
return f"<built-in function stringify>"

73
JAPL/wrapper.py Normal file
View File

@ -0,0 +1,73 @@
from JAPL.lexer import Lexer
from JAPL.meta.exceptions import ParseError, JAPLError
from sys import stderr
class JAPL(object):
"""Wrapper around JAPL's interpreter, lexer and parser"""
def run(self, file: str):
"""Runs a file"""
from JAPL.parser import Parser # Avoids circular imports
from JAPL.interpreter import Interpreter
self.interpreter = Interpreter()
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.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{err}")
def repl(self):
"""Starts an interactive REPL"""
from JAPL.parser import Parser # Avoids circular imports
from JAPL.interpreter import Interpreter
self.interpreter = Interpreter()
print("[JAPL 0.1.1 - Interactive REPL]")
while True:
try:
source = input(">>> ")
except (EOFError, KeyboardInterrupt):
print()
exit()
if not source:
continue
lexer = Lexer(source)
try:
tokens = lexer.lex()
except ParseError as err:
print(f"\nAn exception occurred, details below\n\nParseError: {err.args[0]}")
else:
try:
ast = Parser(tokens).parse()
except ParseError as err:
token, message = err.args
print(f"An exception occurred at line {token.line} at '{token.lexeme}': {message}")
else:
try:
result = self.interpreter.interpret(ast)
except JAPLError as error:
token, message = error.args
print(f"A runtime exception occurred at line {token.line} at '{token.lexeme}': {message}")
else:
if result is not None:
print(repr(result))

51
example.jpl Normal file
View File

@ -0,0 +1,51 @@
// Example file to test JAPL's syntax
// Mathematical expressions
2 + 2;
-1 * 6;
3 * (9 / 2); // Parentheses for grouping
// Variable definition and assignment
var name = "bob"; // Dynamically typed
name = "joe"; // Can only be assigned if it's defined
del name; // Delete a variable
var foo; // Unitialized variables are equal to nil
print foo;
// Scoping
var a = "global";
var b = "global1";
{ // open a new scope
var b = "local"; // Shadow the global variable
print a; // This falls back to the global scope
print b;
}
print a;
print b; // The outer scope isn't affected
// Control flow statements
for (var i = 0; i < 10; i = i + 1) { // For loops
print i;
}
var n = 0;
while (n <= 10) { // While loops
if (n <= 5) { // If statements
print n;
}
n = n + 1;
}
print clock(); // Function calls
fun count(n) { // Function definitions
if (n > 1) count(n - 1);
print n;
}
count(3);

19
factorial.jpl Normal file
View File

@ -0,0 +1,19 @@
fun factorial(n) {
// Computes the factorial of n iteratively
if (n < 0) return nil;
if (n == 1 or n == 2) return n;
var fact = 1;
for (var i = 1; i < n + 1; i = i + 1) {
fact = fact * i;
}
return fact;
}
var start = clock();
print "Computing factorials from 0 to 200";
for (var i = 0; i < 201; i = i + 1) factorial(i);
var result = clock() - start;
print "Computed factorials in " + stringify(result) + " seconds";

10
fib.jpl Normal file
View File

@ -0,0 +1,10 @@
// A recursive implementation of the fibonacci sequence
fun fib(n) {
if (n <= 1) return n;
return fib(n - 2) + fib(n - 1);
}
for (var i = 0; i < 20; i = i + 1) {
print fib(i);
}

9
japl.py Normal file
View File

@ -0,0 +1,9 @@
import sys
from JAPL.wrapper import JAPL
if __name__ == "__main__":
if len(sys.argv) == 1:
JAPL().repl()
else:
JAPL().run(sys.argv[1])

39
repl.py Normal file
View File

@ -0,0 +1,39 @@
from JAPL.lexer import Lexer
from JAPL.parser import Parser
from JAPL.interpreter import Interpreter
from JAPL.objects import ParseError, JAPLError
def repl():
interpreter = Interpreter()
print("[JAPL 0.1.1 - Interactive REPL]")
while True:
try:
source = input(">>> ")
except (EOFError, KeyboardInterrupt):
print()
exit()
if source:
lexer = Lexer(source)
try:
tokens = lexer.lex()
except ParseError as err:
print(f"\nAn error occurred, details below\n\nParseError: {err.args[0]}")
else:
if tokens:
try:
ast = Parser(tokens).parse()
except ParseError as err:
token, message = err.args
print(f"An error occurred at line {token.line} at '{token.lexeme}': {message}")
else:
if ast:
try:
result = interpreter.interpret(ast)
except JAPLError as error:
token, message = error.args
print(f"A runtime error occurred at line {token.line} at '{token.lexeme}': {message}")
else:
if result is not None:
print(repr(result))

16
setup.py Normal file
View File

@ -0,0 +1,16 @@
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',
)