From 0f5913c0dad70b7d97fe18acbe691f9e126ee44a Mon Sep 17 00:00:00 2001 From: sql-hkr Date: Mon, 26 Oct 2026 23:21:42 +0900 Subject: [PATCH] feat(compiler): support increment and decrement operators (++ / --) (#36) - Add `\+\+|--` tokens to `Lexer.TOKEN_SPEC`. - Add `IncDec` AST node for prefix and postfix `++` and `--`. - Update `Parser.parse_unary`, `parse_postfix`, `parse_inline_post`, and `parse_stmt` to support prefix and postfix increment and decrement expressions and statements. - Generate AVR `INC` and `DEC` instructions in `CodeGenerator`, returning the new value in R16 for prefix and the old value in R16 for postfix. - Add comprehensive test coverage in `tests/test_compiler.py`. Closes #36 --- src/tiny8/compiler.py | 88 ++++++++++++++- tests/test_compiler.py | 251 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+), 2 deletions(-) diff --git a/src/tiny8/compiler.py b/src/tiny8/compiler.py index 438c4f3..72c305c 100644 --- a/src/tiny8/compiler.py +++ b/src/tiny8/compiler.py @@ -3,6 +3,7 @@ Features: - Variable declarations and assignments (int/char) - Compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=) +- Increment and decrement operators (++, --) - Arithmetic (+, -, *, /, %), bitwise (&, |, ^, ~), logical (!), and relational (<, <=, >, >=, ==, !=) - Control structures: if / else, while, return - Functions with register-based parameter passing (R16..R19) and return value in R16 @@ -39,7 +40,7 @@ class Lexer: r"\b(int|char|void|if|else|while|for|break|return|peek|poke|in|out|vram_set|vram_get|vram_clear|vram_puts)\b", ), ("ID", r"[a-zA-Z_]\w*"), - ("OP", r"<<|>>|==|!=|<=|>=|&&|\|\||[+\-*/%&|^]=|[+\-*/%&|^=!<>]"), + ("OP", r"\+\+|--|<<|>>|==|!=|<=|>=|&&|\|\||[+\-*/%&|^]=|[+\-*/%&|^=!<>]"), ("PUNCT", r"[;,\(\)\{\}\[\]]"), ("WS", r"\s+"), ] @@ -156,6 +157,15 @@ class ArrayCompoundAssign(ASTNode): expr: ASTNode +@dataclass +class IncDec(ASTNode): + """Increment or decrement expression: ++x, --x, x++, x--.""" + + expr: ASTNode + is_inc: bool + is_prefix: bool + + @dataclass class IfStmt(ASTNode): """If-else statement.""" @@ -436,10 +446,18 @@ def parse_inline_stmt(self) -> ASTNode: def parse_inline_post(self) -> ASTNode: tok = self.peek() + if tok and tok.value in ("++", "--"): + op = self.consume().value + expr = self.parse_unary() + return IncDec(expr, is_inc=(op == "++"), is_prefix=True) if tok and tok.type == "ID": next_tok = ( self.tokens[self.pos + 1] if self.pos + 1 < len(self.tokens) else None ) + if next_tok and next_tok.value in ("++", "--"): + name = self.consume().value + op = self.consume().value + return IncDec(Var(name), is_inc=(op == "++"), is_prefix=False) if next_tok and next_tok.value in COMPOUND_ASSIGN_OPS: name = self.consume().value op = self.consume().value[:-1] @@ -577,6 +595,16 @@ def parse_stmt(self) -> ASTNode: expr = self.parse_expr() self.consume(";") return ArrayCompoundAssign(name, idx_expr, op_tok.value[:-1], expr) + if op_tok and op_tok.value in ("++", "--"): + self.consume() + self.consume(";") + return ExprStmt( + IncDec( + ArrayAccess(name, idx_expr), + is_inc=(op_tok.value == "++"), + is_prefix=False, + ) + ) self.consume("=") expr = self.parse_expr() self.consume(";") @@ -689,10 +717,24 @@ def parse_multiplicative(self) -> ASTNode: def parse_unary(self) -> ASTNode: tok = self.peek() + if tok and tok.value in ("++", "--"): + op = self.consume().value + return IncDec(self.parse_unary(), is_inc=(op == "++"), is_prefix=True) if tok and tok.value in ("-", "!", "~"): op = self.consume().value return _fold_unary(op, self.parse_unary()) - return self.parse_primary() + return self.parse_postfix() + + def parse_postfix(self) -> ASTNode: + node = self.parse_primary() + while True: + tok = self.peek() + if tok and tok.value in ("++", "--"): + op = self.consume().value + node = IncDec(node, is_inc=(op == "++"), is_prefix=False) + else: + break + return node def parse_primary(self) -> ASTNode: tok = self.consume() @@ -1098,6 +1140,9 @@ def gen_stmt(self, stmt: ASTNode) -> None: elif isinstance(stmt, ExprStmt): self.gen_expr(stmt.expr) + elif isinstance(stmt, IncDec): + self.gen_expr(stmt) + def gen_block(self, block: Block) -> None: for s in block.statements: self.gen_stmt(s) @@ -1145,6 +1190,45 @@ def gen_expr(self, expr: ASTNode) -> None: self.emit("LDI R16, 1") self.emit_label(lbl_end) + elif isinstance(expr, IncDec): + instr = "INC" if expr.is_inc else "DEC" + if isinstance(expr.expr, Var): + addr = self.allocate_var(expr.expr.name) + self.emit(f"LDI R30, {addr & 0xFF}") + if addr >= 256: + self.emit(f"LDI R31, {addr >> 8}") + self.emit("LD R16, R30") + if expr.is_prefix: + self.emit(f"{instr} R16") + self.emit("ST R30, R16") + else: + self.emit("MOV R17, R16") + self.emit(f"{instr} R17") + self.emit("ST R30, R17") + if addr >= 256: + self.emit("LDI R31, 0") + + elif isinstance(expr.expr, ArrayAccess): + base_addr = self.allocate_var(expr.expr.name) + self.gen_expr(expr.expr.index) + self.emit(f"LDI R30, {base_addr & 0xFF}") + self.emit(f"LDI R31, {base_addr >> 8}") + self.emit("ADD R30, R16") + self.emit("LDI R17, 0") + self.emit("ADC R31, R17") + self.emit("LD R16, R30") + if expr.is_prefix: + self.emit(f"{instr} R16") + self.emit("ST R30, R16") + else: + self.emit("MOV R17, R16") + self.emit(f"{instr} R17") + self.emit("ST R30, R17") + self.emit("LDI R31, 0") + + else: + raise SyntaxError(f"Invalid lvalue for {'++' if expr.is_inc else '--'}") + elif ( isinstance(expr, BinaryOp) and expr.op in ("<<", ">>") diff --git a/tests/test_compiler.py b/tests/test_compiler.py index b2f21d0..fdef0ed 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,5 +1,7 @@ """Unit tests for Tiny8 C compiler.""" +import pytest + from tiny8 import CPU, assemble from tiny8.compiler import compile_c, optimize_assembly @@ -524,3 +526,252 @@ def test_compiler_compound_assign_array_index_evaluated_once(): assert cpu.read_ram(0x90) == 1 assert cpu.read_ram(0x80) == 15 assert cpu.read_ram(0x81) == 7 + + +def test_compiler_inc_dec_scalars(): + cpu = _run_compound_program( + """ + int main() { + int a = 10; + int b = a++; + poke(0x80, a); + poke(0x81, b); + + int c = ++a; + poke(0x82, a); + poke(0x83, c); + + int d = a--; + poke(0x84, a); + poke(0x85, d); + + int e = --a; + poke(0x86, a); + poke(0x87, e); + return 0; + } + """ + ) + # a was 10. b = a++ -> a becomes 11, b is 10. + assert cpu.read_ram(0x80) == 11 + assert cpu.read_ram(0x81) == 10 + + # a was 11. c = ++a -> a becomes 12, c is 12. + assert cpu.read_ram(0x82) == 12 + assert cpu.read_ram(0x83) == 12 + + # a was 12. d = a-- -> a becomes 11, d is 12. + assert cpu.read_ram(0x84) == 11 + assert cpu.read_ram(0x85) == 12 + + # a was 11. e = --a -> a becomes 10, e is 10. + assert cpu.read_ram(0x86) == 10 + assert cpu.read_ram(0x87) == 10 + + +def test_compiler_inc_dec_standalone_statements(): + cpu = _run_compound_program( + """ + int main() { + int x = 5; + x++; + poke(0x80, x); + ++x; + poke(0x81, x); + x--; + poke(0x82, x); + --x; + poke(0x83, x); + return 0; + } + """ + ) + assert [cpu.read_ram(0x80 + i) for i in range(4)] == [6, 7, 6, 5] + + +def test_compiler_inc_dec_for_loop(): + cpu = _run_compound_program( + """ + int main() { + int sum = 0; + int i = 0; + for (i = 0; i < 5; i++) { + sum += i; + } + poke(0x80, sum); + poke(0x81, i); + + int count = 0; + for (i = 5; i > 0; --i) { + count += 1; + } + poke(0x82, count); + poke(0x83, i); + return 0; + } + """ + ) + assert cpu.read_ram(0x80) == 10 + assert cpu.read_ram(0x81) == 5 + assert cpu.read_ram(0x82) == 5 + assert cpu.read_ram(0x83) == 0 + + +def test_compiler_inc_dec_while_loop_issue_example(): + # Example from Issue #36 + cpu = _run_compound_program( + """ + int main() { + int i = 0; + while (i++ < 5) { + poke(0x80, i); + } + poke(0x81, i); + return 0; + } + """ + ) + # The last value poked inside the loop should be 5 + assert cpu.read_ram(0x80) == 5 + # When loop terminates (5 < 5 is False), i has been incremented to 6 + assert cpu.read_ram(0x81) == 6 + + +def test_compiler_inc_dec_while_countdown(): + cpu = _run_compound_program( + """ + int main() { + int n = 4; + int count = 0; + while (n--) { + count++; + } + poke(0x80, count); + return 0; + } + """ + ) + # Runs for n = 4, 3, 2, 1 (4 times) + assert cpu.read_ram(0x80) == 4 + + +def test_compiler_inc_dec_array_elements(): + cpu = _run_compound_program( + """ + int main() { + int arr[3] = {10, 20, 30}; + int a = arr[0]++; + poke(0x80, a); + poke(0x81, arr[0]); + + int b = ++arr[1]; + poke(0x82, b); + poke(0x83, arr[1]); + + int c = arr[2]--; + poke(0x84, c); + poke(0x85, arr[2]); + + int d = --arr[2]; + poke(0x86, d); + poke(0x87, arr[2]); + return 0; + } + """ + ) + assert cpu.read_ram(0x80) == 10 + assert cpu.read_ram(0x81) == 11 + assert cpu.read_ram(0x82) == 21 + assert cpu.read_ram(0x83) == 21 + assert cpu.read_ram(0x84) == 30 + assert cpu.read_ram(0x85) == 29 + assert cpu.read_ram(0x86) == 28 + assert cpu.read_ram(0x87) == 28 + + +def test_compiler_inc_dec_array_statement(): + cpu = _run_compound_program( + """ + int main() { + int arr[2] = {100, 200}; + arr[0]++; + arr[1]--; + poke(0x80, arr[0]); + poke(0x81, arr[1]); + return 0; + } + """ + ) + assert cpu.read_ram(0x80) == 101 + assert cpu.read_ram(0x81) == 199 + + +def test_compiler_inc_dec_array_index_evaluated_once(): + cpu = _run_compound_program( + """ + int main() { + int arr[2] = {5, 7}; + arr[poke(0x90, peek(0x90) + 1) - 1]++; + poke(0x80, arr[0]); + poke(0x81, arr[1]); + return 0; + } + """ + ) + assert cpu.read_ram(0x90) == 1 + assert cpu.read_ram(0x80) == 6 + assert cpu.read_ram(0x81) == 7 + + +def test_compiler_inc_dec_in_expressions(): + cpu = _run_compound_program( + """ + int main() { + int x = 5; + int y = x++ + 10; + poke(0x80, x); + poke(0x81, y); + + int z = ++x * 2; + poke(0x82, x); + poke(0x83, z); + return 0; + } + """ + ) + # x was 5. y = 5 + 10 = 15. x becomes 6. + assert cpu.read_ram(0x80) == 6 + assert cpu.read_ram(0x81) == 15 + # x was 6. ++x -> x is 7. z = 7 * 2 = 14. + assert cpu.read_ram(0x82) == 7 + assert cpu.read_ram(0x83) == 14 + + +def test_compiler_inc_dec_overflow_underflow(): + cpu = _run_compound_program( + """ + int main() { + int a = 255; + a++; + poke(0x80, a); + + int b = 0; + b--; + poke(0x81, b); + return 0; + } + """ + ) + assert cpu.read_ram(0x80) == 0 + assert cpu.read_ram(0x81) == 255 + + +def test_compiler_inc_dec_invalid_lvalue_raises(): + with pytest.raises(SyntaxError): + compile_c("int main() { 5++; return 0; }") + + with pytest.raises(SyntaxError): + compile_c("int main() { ++5; return 0; }") + + with pytest.raises(SyntaxError): + compile_c("int main() { (1 + 2)++; return 0; }")