Skip to content

Commit

Permalink
Make compatible with LALR(1)
Browse files Browse the repository at this point in the history
  • Loading branch information
aazuspan committed Aug 6, 2024
1 parent c55ccd5 commit 3302c86
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 52 deletions.
47 changes: 8 additions & 39 deletions src/spinasm_lsp/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,10 @@ def BIT_VECTOR(self, token) -> int:
# Remove the % prefix and optional underscores
return int(token[1:].replace("_", ""), base=2)

def NAME(self, name) -> str:
# if name not in self.local_vars:
# TODO: This doesn't work because labels can be used before they are defined.
# Probably the simplest fix is making a special grammar for SKP since that's
# the only instruction that uses labels. Alternatively, maybe we keep a dict
# with all the defined and used labels and variables (separately) and their
# line numbers, and go through and resolve them after parsing.
# raise ParsingError(f"`{name}` is undefined.")

# NOTE: identifiers are case-insensitive. asfv1 warns when you redefine, while
# the original FV-1 assembler errors out. Both store labels as uppercase.
return name.upper()
# return self.local_vars.get(name, str(name))
def IDENT(self, token) -> str:
# Identifiers are case-insensitive and are stored in uppercase for consistency
# with the FV-1 assembler.
return token.upper()

@v_args(inline=False)
def args(self, tokens):
Expand All @@ -120,8 +111,6 @@ def expr(self, tokens):
def program(self, tokens):
return list(tokens)

IDENT = OPERATOR = OPCODE = str


class FV1Program:
constants = {
Expand Down Expand Up @@ -205,40 +194,20 @@ def __init__(self, code: str):
package="spinasm_lsp",
grammar_path="spinasm.lark",
start="program",
# parser="lalr",
# strict=True,
# transformer=self.transformer
parser="lalr",
strict=False,
transformer=self.transformer,
)

# Make sure the code ends with a newline to properly parse the last line
if not code.endswith("\n"):
code += "\n"

try:
self.tree = self.parser.parse(code)
self.statements: list[dict] = self.transformer.transform(self.tree)
self.statements = self.parser.parse(code)
except VisitError as e:
# Unwrap errors thrown by FV1ProgramTransformer
if wrapped_err := e.__context__:
raise wrapped_err from None

raise e


if __name__ == "__main__":
code = r"""
Tmp EQU 4
"""

with open("./demos/test.spn") as src:
code = src.read()

program = FV1Program(code)

# print(program)
print("program.statements =")
for statement in program.statements:
print("\t", statement)

print(f"{program.local_vars = }")
print(f"{program.memory = }")
35 changes: 30 additions & 5 deletions src/spinasm_lsp/spinasm.lark
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,41 @@ mem: IDENT "MEM"i expr | "MEM"i IDENT expr

label: IDENT ":"

// OPCODE cannot be MEM or EQU to avoid ambiguity between assignments and instructions
OPCODE: /(?!(MEM|EQU)\b)/ IDENT

OPCODE.1: "ABSA"i
| "AND"i
| "CHO"i
| "CLR"i
| "EXP"i
| "JAM"i
| "LDAX"i
| "LOG"i
| "MAXX"i
| "MULX"i
| "NOT"i
| "OR"i
| "RDA"i
| "RDAX"i
| "RDFX"i
| "RMPA"i
| "SKP"i
| "SOF"i
| "WLDR"i
| "WLDS"i
| "WRA"i
| "WRAP"i
| "WRAX"i
| "WRHX"i
| "WRLX"i
| "XOR"i

// NAME can be suffixed with ^ or # to modify memory addressing
NAME: OPCODE [ADDR_MODIFIER]
NAME: IDENT [ADDR_MODIFIER]
ADDR_MODIFIER: ["^" | "#"]
NEGATIVE: "-"
COMMENT: ";" /[^\n]/*
OPERATOR: "+" | "-" | "*" | "/" | "|" | "&"
HEX_NUM: "0x"i HEXDIGIT+ | "$" HEXDIGIT+
// Binary number prefixed by % with optional underscores, e.g. %00000000_00000001_00000000
HEX_NUM.1: "0x"i HEXDIGIT+ | "$" HEXDIGIT+
BIT_VECTOR: "%" /[01](_?[01])*/

%import common.WS_INLINE
Expand Down
38 changes: 30 additions & 8 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from spinasm_lsp.parser import (
Assignment,
Expression,
FV1Program,
Instruction,
Label,
Expand All @@ -23,23 +24,44 @@
)
def test_number_representations(expr):
"""Test supported number representations."""
assert FV1Program(f"OP {expr}").statements[0].args[0] == 42


@pytest.mark.parametrize("stmt", [None, "SOF 0", "x EQU 4"], ids=["none", "op", "equ"])
assert FV1Program(f"MULX {expr}").statements[0].args[0] == 42


def test_combined_statements():
"""Test a program with multiple statements."""
code = r"""
; This is a comment
start: Tmp EQU 4
EQU Tmp2 5
MULX 0+1
SOF -1,TMP
end:
"""

assert FV1Program(code).statements == [
Label("START"),
Assignment("equ", "TMP", 4),
Assignment("equ", "TMP2", 5),
Instruction("MULX", args=[Expression([0, "+", 1])]),
Instruction("SOF", args=[Expression([-1]), Expression(["TMP"])]),
Label("END"),
]


@pytest.mark.parametrize("stmt", ["", "SOF 0", "x EQU 4"], ids=["none", "op", "equ"])
def test_parse_label(stmt: str | None):
"""Test that labels are parsed, with and without following statements."""
prog = FV1Program(f"myLabel: {stmt}")
prog = FV1Program(f"myLabel:{stmt}")
assert len(prog.statements) == 2 if stmt else 1
assert prog.statements[0] == Label("myLabel")
assert prog.statements[0] == Label("MYLABEL")


@pytest.mark.parametrize("n_args", [0, 1, 3], ids=lambda x: f"{x} args")
def test_parse_instruction(n_args):
"""Test that instructions with varying number of arguments are parsed correctly."""
args = [random.randint(0, 100) for _ in range(n_args)]
code = f"OP {','.join(map(str, args))}"
assert FV1Program(code).statements[0] == Instruction("OP", args=args)
code = f"MULX {','.join(map(str, args))}"
assert FV1Program(code).statements[0] == Instruction("MULX", args=args)


@pytest.mark.parametrize("type", ["equ", "mem"])
Expand Down

0 comments on commit 3302c86

Please sign in to comment.