summaryrefslogtreecommitdiff
path: root/projects/hackc/classes.py
diff options
context:
space:
mode:
Diffstat (limited to 'projects/hackc/classes.py')
-rw-r--r--projects/hackc/classes.py245
1 files changed, 245 insertions, 0 deletions
diff --git a/projects/hackc/classes.py b/projects/hackc/classes.py
new file mode 100644
index 0000000..1875ce7
--- /dev/null
+++ b/projects/hackc/classes.py
@@ -0,0 +1,245 @@
+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' <name> '{'
+ <variable>*
+ <subroutine>*
+ '}'
+ """
+ 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" (<scope> = static | field) or "subroutine" (<scope> = var)
+
+ Format:
+ <scope> <type> <name> (, <name>)* ;
+
+ <scope> = static | field | var
+ <type> = int | char | boolean | <class>
+ """
+ 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:
+ '(' (<type> <name> (, <type> <name>)*)? ')'
+
+ <type> = int | char | boolean | <class>
+ """
+ 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:
+ <category> <return type> <name> <paramlist> <body>
+
+ <category> = constructor | method | function
+ <return type> = int | char | boolean | void | <class>
+ """
+ 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}")