From 1c7fcdd55cfbbf8d55a4848a9f325b84602a0cb8 Mon Sep 17 00:00:00 2001 From: am-official <46477058+am-official@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:09:07 +0000 Subject: [PATCH] feat(compiler): support bitwise shift operators (<< and >>) - Lexer: add `<<` and `>>` to the OP token pattern, ahead of `<=`/`>=` and the single-character operators. - Parser: new parse_shift level between relational and additive, so `x << 2 + 1` is `x << 3` and `y << 1 < 7` is `(y << 1) < 7`, as in C. - Constant folding: fold `<<` and `>>` on literals to 8-bit results. - Code generation: a literal shift count unrolls into LSL/LSR R16, and a count of 8 or more loads 0 instead of emitting the instructions. A variable count runs a small loop that shifts once and decrements R17 until it reaches 0. `>>` is a logical shift (LSR), so 0x81 >> 7 is 1. - Tests for literal and variable counts, zero and >= 8 counts, precedence and associativity, folding, and the unrolled output. - README: list the shift operators among the C features. Closes #38 --- README.md | 2 +- src/tiny8/compiler.py | 54 ++++++++++++++++-- tests/test_compiler.py | 123 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e0547a6..e153ab2 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Tiny8 is a lightweight educational toolkit that bridges bare-metal computer arch ### **Built-in C Compiler & Extensions** - **Compile and run C programs directly**: Write C code (`.c`) and execute on Tiny8 (`tiny8 program.c`) -- **Enhanced C features**: Character literals (`'A'`, `'#'`), string literals, fixed-size arrays (`int arr[4];`), `for` loops, `break;`, logical `&&` and `||`, function calls, `peek()`/`poke()`, and VRAM helpers (`vram_set`, `vram_get`, `vram_puts`, `vram_clear`) +- **Enhanced C features**: Character literals (`'A'`, `'#'`), string literals, fixed-size arrays (`int arr[4];`), `for` loops, `break;`, logical `&&` and `||`, bit shifts `<<` and `>>`, function calls, `peek()`/`poke()`, and VRAM helpers (`vram_set`, `vram_get`, `vram_puts`, `vram_clear`) - **Compile-to-assembly inspection**: Generate and inspect clean Tiny8 assembly with `-c` flag ### **Optimized AVR Instruction Set** diff --git a/src/tiny8/compiler.py b/src/tiny8/compiler.py index da4656b..e7eaa14 100644 --- a/src/tiny8/compiler.py +++ b/src/tiny8/compiler.py @@ -38,7 +38,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+"), ] @@ -249,6 +249,10 @@ def _fold_binary(op: str, left: ASTNode, right: ASTNode) -> ASTNode: return Literal((lv | rv) & 0xFF) if op == "^": return Literal((lv ^ rv) & 0xFF) + if op == "<<": + return Literal((lv << rv) & 0xFF) + if op == ">>": + return Literal((lv >> rv) & 0xFF) if op == "==": return Literal(1 if lv == rv else 0) if op == "!=": @@ -588,16 +592,27 @@ def parse_equality(self) -> ASTNode: return node def parse_relational(self) -> ASTNode: - node = self.parse_additive() + node = self.parse_shift() while True: if self.match("<"): - node = _fold_binary("<", node, self.parse_additive()) + node = _fold_binary("<", node, self.parse_shift()) elif self.match("<="): - node = _fold_binary("<=", node, self.parse_additive()) + node = _fold_binary("<=", node, self.parse_shift()) elif self.match(">"): - node = _fold_binary(">", node, self.parse_additive()) + node = _fold_binary(">", node, self.parse_shift()) elif self.match(">="): - node = _fold_binary(">=", node, self.parse_additive()) + node = _fold_binary(">=", node, self.parse_shift()) + else: + break + return node + + def parse_shift(self) -> ASTNode: + node = self.parse_additive() + while True: + if self.match("<<"): + node = _fold_binary("<<", node, self.parse_additive()) + elif self.match(">>"): + node = _fold_binary(">>", node, self.parse_additive()) else: break return node @@ -1058,6 +1073,21 @@ def gen_expr(self, expr: ASTNode) -> None: self.emit("LDI R16, 1") self.emit_label(lbl_end) + elif ( + isinstance(expr, BinaryOp) + and expr.op in ("<<", ">>") + and isinstance(expr.right, Literal) + ): + # Constant shift count: unroll into LSL/LSR. Shifting an 8-bit + # value by 8 or more clears it. + self.gen_expr(expr.left) + if expr.right.value >= 8: + self.emit("LDI R16, 0") + else: + instr = "LSL" if expr.op == "<<" else "LSR" + for _ in range(expr.right.value): + self.emit(f"{instr} R16") + elif isinstance(expr, BinaryOp): self.gen_expr(expr.left) self.emit("PUSH R16") @@ -1099,6 +1129,18 @@ def gen_expr(self, expr: ASTNode) -> None: self.emit("OR R16, R17") elif expr.op == "^": self.emit("EOR R16, R17") + elif expr.op in ("<<", ">>"): + # Variable shift count: shift once per iteration until R17 is 0. + instr = "LSL" if expr.op == "<<" else "LSR" + lbl_loop = self.new_label("shift_loop") + lbl_end = self.new_label("shift_end") + self.emit_label(lbl_loop) + self.emit("CPI R17, 0") + self.emit(f"BREQ {lbl_end}") + self.emit(f"{instr} R16") + self.emit("DEC R17") + self.emit(f"JMP {lbl_loop}") + self.emit_label(lbl_end) elif expr.op == "&&": lbl_false = self.new_label("and_false") lbl_end = self.new_label("and_end") diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 9ab8aaa..33ec846 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -219,6 +219,111 @@ def test_compiler_array_initializer(): assert cpu.read_ram(0x72) == 50 +def _run_c(c_code: str, max_steps: int = 2000) -> CPU: + cpu = CPU() + cpu.load_program(assemble(compile_c(c_code))) + cpu.run(max_steps=max_steps, show_progress=False) + return cpu + + +def test_compiler_shift_by_literal(): + cpu = _run_c( + """ + int main() { + int x = 3; + int y = 200; + int z = 0x81; + poke(0xC0, x << 3); + poke(0xC1, y >> 2); + poke(0xC2, z << 1); + poke(0xC3, z >> 7); + poke(0xC4, x << 0); + poke(0xC5, y >> 8); + poke(0xC6, y << 9); + return 0; + } + """ + ) + assert cpu.read_ram(0xC0) == 24 + assert cpu.read_ram(0xC1) == 50 + assert cpu.read_ram(0xC2) == 0x02 # bit 7 shifted out of the 8-bit value + assert cpu.read_ram(0xC3) == 1 # logical shift: no sign extension + assert cpu.read_ram(0xC4) == 3 + assert cpu.read_ram(0xC5) == 0 + assert cpu.read_ram(0xC6) == 0 + + +def test_compiler_shift_by_variable(): + cpu = _run_c( + """ + int main() { + int a = 5; + int b = 0xF0; + int n = 3; + int zero = 0; + poke(0xC0, a << n); + poke(0xC1, b >> n); + poke(0xC2, a << zero); + poke(0xC3, b >> zero); + n = 8; + poke(0xC4, b >> n); + return 0; + } + """ + ) + assert cpu.read_ram(0xC0) == 40 + assert cpu.read_ram(0xC1) == 30 + assert cpu.read_ram(0xC2) == 5 + assert cpu.read_ram(0xC3) == 0xF0 + assert cpu.read_ram(0xC4) == 0 + + +def test_compiler_shift_precedence(): + cpu = _run_c( + """ + int main() { + int x = 1; + int y = 3; + poke(0xC0, x << 2 + 1); + poke(0xC1, y << 1 < 7); + poke(0xC2, y << 1 > 7); + poke(0xC3, 64 >> 2 >> 1); + return 0; + } + """ + ) + assert cpu.read_ram(0xC0) == 8 # x << (2 + 1) + assert cpu.read_ram(0xC1) == 1 # (y << 1) < 7 + assert cpu.read_ram(0xC2) == 0 # (y << 1) > 7 + assert cpu.read_ram(0xC3) == 8 # left-associative: (64 >> 2) >> 1 + + +def test_compiler_shift_by_literal_is_unrolled(): + asm = compile_c( + """ + int main() { + int x = 3; + return x << 3; + } + """ + ) + assert asm.count("LSL R16") == 3 + assert "shift_loop" not in asm + + +def test_compiler_shift_by_eight_or_more_is_zero_without_unrolling(): + asm = compile_c( + """ + int main() { + int x = 3; + return x << 200; + } + """ + ) + assert "LSL" not in asm + assert "shift_loop" not in asm + + class TestCompilerOptimization: """Unit tests for compiler peephole optimization and constant folding.""" @@ -250,6 +355,24 @@ def test_constant_folding_bitwise_and_logic(self): assert "LDI R16, 15" in asm # 0xFF & 0x0F = 15 assert "LDI R16, 1" in asm # 5 > 2 = 1, !0 = 1 + def test_constant_folding_shifts(self): + """Verify shifts on literals are folded to 8-bit results.""" + src = """ + int main() { + int a = 1 << 7; + int b = 0xF0 >> 4; + int c = 0x81 << 1; + return a + b + c; + } + """ + asm = compile_c(src, optimize=True) + assert "LDI R16, 128" in asm + assert "LDI R16, 15" in asm + assert "LDI R16, 2" in asm + assert "LSL" not in asm + assert "LSR" not in asm + assert "shift_loop" not in asm + def test_peephole_remove_redundant_mov(self): """Verify that MOV Rx, Rx is eliminated.""" lines = [