Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
54 changes: 48 additions & 6 deletions src/tiny8/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+"),
]
Expand Down Expand Up @@ -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 == "!=":
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
123 changes: 123 additions & 0 deletions tests/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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 = [
Expand Down
Loading