from .tokens import Token from .utils import * SCOPES = ["static", "field", "var"] VAR_TYPES = ["int", "char", "boolean"] RETURN_TYPES = ["int", "char", "boolean", "void"] SUBROUTINE_CATS = ["constructor", "method", "function"] class Class: def __init__(self, name: Token, variables: list, subroutines: list): self.name = name self.variables = variables self.subroutines = subroutines @classmethod def from_tokens(cls, tokens: list): """Construct a class from a list of tokens. In standard Jack, one file is exactly one class. Format: 'class' '{' * * '}' """ tokens_total = len(tokens) if tokens_total < 4: return None if tokens[0] != "class": raise JackSyntaxError( f"Expected `class`, got `{tokens[0]}` instead", tokens[0] ) name = tokens[1] if name.type != "identifier": raise JackSyntaxError(f"You cannot name a class `{name}`", name) if tokens[2] != LEFT_BRACE: raise JackSyntaxError( f"Expected `{LEFT_BRACE}`, got `{tokens[2]}` instead", tokens[2] ) tokens_consumed = 3 while tokens_consumed < tokens_total: variables, token_cnt = Variable.from_tokens( tokens[tokens_consumed:], context="class" ) if variables is None: break variables.print_verbose() tokens_consumed += token_cnt while tokens_consumed < tokens_total: subroutine, token_cnt = Subroutine.from_tokens(tokens[tokens_consumed:]) if subroutine is None: break subroutine.print_verbose() tokens_consumed += token_cnt return Class(name, variables, []) class Variable: def __init__(self, scope: Token, type: Token, names: list[Token]): self.scope = scope self.type = type self.names = names @classmethod def from_tokens(cls, tokens: list, context: str) -> tuple: """Construct variable declaration statement from a list of tokens. Return a tuple of an instance of Variable and number of tokens consumed. When `tokens` does not begin with a variable declaration, return (None, 0). context -- "class" ( = static | field) or "subroutine" ( = var) Format: (, )* ; = static | field | var = int | char | boolean | """ if len(tokens) < 4 or tokens[0] not in SCOPES: # not variable declaration return (None, 0) scope = tokens[0] if scope in ["static", "field"] and context != "class": raise JackSyntaxError( f"You cannot declare a {scope} variable in a subroutine", scope ) if scope == "var" and context != "subroutine": raise JackSyntaxError( f"You cannot declare a local variable outside of a subroutine", scope, ) type = tokens[1] if type not in VAR_TYPES and type.type != "identifier": raise JackSyntaxError(f"Expected datatype, got `{tokens[1]}` instead", type) tokens_consumed = 2 names = [] # names of variables expecting_identifier = True found_semicolon = False for token in tokens[2:]: tokens_consumed += 1 if token.type == "identifier": if expecting_identifier: names.append(token) expecting_identifier = False else: raise JackSyntaxError(f"Expected `,`, got `{token}` instead", token) elif token == ",": if not expecting_identifier: expecting_identifier = True else: raise JackSyntaxError( f"Expected variable name, got `,` instead", token ) elif token == ";": if expecting_identifier: raise JackSyntaxError( f"Expected variable name, got `;` instead", token ) found_semicolon = True break else: expected = "variable name" if expecting_identifier else "`,` or `;`" raise JackSyntaxError( f"Expected {expected}, got `{token}` instead", token ) if not found_semicolon: # TODO: print caret at end of token raise JackSyntaxError(f"Missing semicolon", token) return (Variable(scope, type, names), tokens_consumed) def print_verbose(self): print(f"Declare {len(self.names)} variable(s):") for name in self.names: print(self.scope, self.type, name) class ParamList: def __init__(self, params: list[tuple]): self.params = params @classmethod def from_tokens(cls, tokens: list) -> tuple: """Construct parameter list of subroutine from tokens. Format: '(' ( (, )*)? ')' = int | char | boolean | """ if len(tokens) < 2 or tokens[0] != LEFT_PAREN: return (None, 0) if tokens[1] == RIGHT_PAREN: # empty param list, i.e. '(' ')' return (ParamList([]), 2) tokens_consumed = 1 params = [] for type, name, delim in zip(tokens[1::3], tokens[2::3], tokens[3::3]): tokens_consumed += 3 if type not in VAR_TYPES and type.type != "identifier": raise JackSyntaxError(f"Expected datatype, got `{type}` instead", type) if not name: # TODO: print caret at end of type raise JackSyntaxError("Expected variable name", type) if name.type != "identifier": raise JackSyntaxError( f"Expected variable name, got `{name}` instead", name ) if not delim: raise JackSyntaxError(f"Expected `,` or `{RIGHT_PAREN}`", name) if delim == ",": params.append((type, name)) continue elif delim == RIGHT_PAREN: params.append((type, name)) break else: raise JackSyntaxError( f"Expected `,` or `{RIGHT_PAREN}`, got `{delim}` instead", delim ) return (ParamList(params), tokens_consumed) class Subroutine: def __init__( self, category: Token, type: Token, name: Token, params: ParamList, body ): self.category = category self.type = type self.name = name self.params = params self.body = body @classmethod def from_tokens(cls, tokens: list) -> tuple: """Construct subroutine from tokens. Format: = constructor | method | function = int | char | boolean | void | """ if len(tokens) < 7 or tokens[0] not in SUBROUTINE_CATS: # not a subroutine return (None, 0) category = tokens[0] return_type = tokens[1] if return_type not in RETURN_TYPES and return_type.type != "identifier": raise JackSyntaxError( f"Expected datatype, got `{return_type}` instead", return_type ) name = tokens[2] if name.type != "identifier": raise JackSyntaxError( f"Expected {category} name, got `{name}` instead", name ) tokens_consumed = 3 params, token_cnt = ParamList.from_tokens(tokens[tokens_consumed:]) if params is None: raise JackSyntaxError("Expected parameter list", tokens[tokens_consumed]) tokens_consumed += token_cnt return (Subroutine(category, return_type, name, params, None), tokens_consumed) def print_verbose(self): print(f"Define {self.category} {self.name}, returns {self.type}")