Skip to content

Commit f201919

Browse files
authored
v1 Milestone - Complete single file language (#8)
* Fixed silent bug for signed div/mod * Fixed binop widening bug in conditionals * Update CHANGELOG, add golden files for emu cases * Updated ir printing format * Added debug symbol table to generated binary for the disassembler * Added full 16 bit helper functions for mul/div/mod, factored out symbol table emitter * Added failure catch for ZP overflow * Bump version, update changelog * Added __heap_start implicit global holding first free ram addr after global data * ROM boundaries now enforced in codegen * Made compiler extern values more robust. Added __memory_top value * Update README, update CHANGELOG.md * Factored redundent checks for brevity * Local variables can now be initted with string literals * Added break/continue keywords, Made variable shadowing an explicit error, bumped version
1 parent 1b26049 commit f201919

72 files changed

Lines changed: 2377 additions & 1080 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
bin/
77
*.out
88
*.bin
9-
__pycache__/
9+
__pycache__/
10+
compile_commands.json

CHANGELOG.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,185 @@ and this project adheres to [Semantic Versioning](https://semver.org/) - while
77
the project is in `0.x`, breaking changes may land in MINOR releases; PATCH
88
releases are reserved for bug fixes only.
99

10+
## [v1.0.0] 2026-07-01
11+
12+
- **v1.0 milestone: Complete Single-File Language** — every must-have and
13+
should-have feature from `docs/roadmap.md`'s v1.0 checklist is now
14+
implemented: function calls, pointer store, address-of, implicit widening,
15+
string literal locals, pointer arithmetic (`*(ptr + i)`), break/continue,
16+
struct field access, multiply/divide/modulo, and bitwise/shift operators.
17+
The only items left unchecked are explicitly-optional "nice-to-have"s
18+
(short-circuit `&&`/`||` codegen outside boolean-context use, and the
19+
`&=`/`|=`/`^=`/`<<=`/`>>=` compound assignment forms) — the roadmap's own
20+
goal, "someone can sit down and write a non-trivial 65C02 program without
21+
hitting an unimplemented wall," is met;
22+
23+
- **`break` and `continue`** — both now supported inside `while` and `for`
24+
loops. The analyzer tracks a `loop_depth` counter (incremented for the
25+
duration of a loop body) and rejects either statement outside a loop
26+
(`ERR_BREAK_OUTSIDE_LOOP` / `ERR_CONTINUE_OUTSIDE_LOOP`). The IR generator
27+
maintains a `loop_ctx_t { continue_label, break_label }` stack; both
28+
statements desugar to a plain `TAC_JUMP` to the appropriate label, so
29+
codegen needed no changes. For `for` loops, `continue` jumps to a new
30+
label sitting between the body and the incrementer, so the incrementer
31+
still runs before the next condition check. See
32+
`docs/break-continue-implementation.md` for the full design writeup,
33+
including a bonus fix along the way: the `for`-loop incrementer was being
34+
lowered with `lower_expr` instead of `lower_stmt`, so `i = i + 1`-style
35+
incrementers (as opposed to `++i`) were silently generating no code;
36+
37+
- **Local string literal initializers**`u8 *p = "some string";` now works
38+
inside function bodies, not just at global scope. The string data is placed in
39+
the ROM data section via the existing `data_fixup_t` backpatch mechanism (same
40+
as global strings); the pointer value (ROM address of the string) is written
41+
into the local's ZP slot during function entry via `TAC_COPY` with an
42+
`OPERAND_CONST_STR` source. Callee-saves push and pop the pointer ZP slot
43+
across nested calls to preserve it across function boundaries. The pointer
44+
occupies 2 bytes of ZP for the lifetime of the function;
45+
46+
- **Variable shadowing is now a semantic error** — a variable declaration
47+
(or function parameter) that reuses a name still visible from an enclosing,
48+
still-live scope now raises `ERR_SHADOWED_DECLARATION` instead of silently
49+
compiling. Root cause: the IR/codegen identify a variable purely by its bare
50+
name (`OPERAND_VAR.name`, matched via `strcmp` in `zp_map_build`), with no
51+
per-scope qualifier, so a shadowed inner declaration (e.g. a `for (u8 i = 0;
52+
...)` nested inside a function that already has an outer `u8 i`) aliased the
53+
*same* zero-page storage as its outer namesake — the inner loop's own
54+
init/exit value silently clobbered the outer variable. This surfaced as
55+
`break` firing on the wrong iteration in `arithmetic_demo.c02`. Reusing a
56+
name across scopes that never overlap on the scope stack (e.g. two sibling
57+
`for` loops each declaring their own `i`) is unaffected and remains legal;
58+
59+
## [v0.2.17] 2026-06-27
60+
61+
- **ROM overflow detection** — the `EMIT()` macro now bounds-checks `code_pos`
62+
against `ROM_SIZE` before writing, setting an `overflow` flag on `emitter_t`
63+
instead of silently writing past the end of the 32 KB buffer. A new
64+
`PATCH_BYTE(pos, val)` macro applies the same guard to all branch-offset and
65+
address backpatches. `resolve_func_fixups` and `resolve_local_fixups`
66+
early-return when overflow is already set. `generate_rom` checks `e.overflow`
67+
independently after the code section, data section, and symbol-table phases,
68+
printing a targeted diagnostic and returning `NULL` on failure. Previously,
69+
programs that grew past 32 KB would corrupt the ROM buffer without any error.
70+
`emit_symbol_table` now also writes through `EMIT()` instead of raw pointer
71+
writes, inheriting the overflow guard.
72+
73+
- **`__heap_start` implicit compiler global** — programs may declare
74+
`decl u16 __heap_start;` to read the address of the first free RAM byte after
75+
all user globals are allocated. The driver injects the declaration
76+
automatically so no user `decl` is required in practice. The value is
77+
initialized during the bootstrap sequence. Intended as a base pointer for
78+
simple bump allocators.
79+
80+
- **`__memory_top` implicit compiler global**`decl u16 __memory_top;`
81+
evaluates to `$3FFF`, the top of the general-purpose RAM region (see
82+
`docs/memmap.md`). Injected alongside `__heap_start`. Both constants are
83+
defined as `RAM_TOP` in the codegen memory map.
84+
85+
- **Compiler extern two-pass allocation**`emit_compiler_extern_inits` is
86+
refactored into an explicit two-pass design: pass 1 allocates all RAM slots
87+
(settling `e->ram_pos`), pass 2 emits initializers. This ensures
88+
`__heap_start` captures the correct first-free-RAM address regardless of
89+
declaration order. Unknown non-function externs now print a diagnostic and
90+
cause codegen to fail instead of being silently skipped.
91+
- `ALLOC_COMPILER_SLOT` / `EMIT_COMPILER_VALUE` — pair of local macros that
92+
collapse the slot-allocation and value-emission boilerplate to one line each;
93+
both are `#undef`'d immediately after the function.
94+
95+
## [v0.2.16] 2026-06-27
96+
97+
- **16-bit multiply / divide / modulo (`__mul16`, `__div16`, `__sdiv16`)**
98+
`TAC_MUL`, `TAC_DIV`, and `TAC_MOD` on `u16`/`i16` operands now compile to
99+
subroutine calls rather than erroring. Three new helpers:
100+
- `__mul16` — 16-iteration shift-and-add. Correct for both `u16` and `i16`
101+
because the low 16 bits of a two's-complement product are sign-agnostic.
102+
Overflow silently wraps to the low 16 bits (same as C).
103+
- `__div16` — 16-iteration shift-subtract with CMP-based comparison. Uses a
104+
`BCS dosub` guard before the 16-bit subtract so divisors with bit 15 set
105+
(≥ `$8000`) are handled correctly; the naive SEC-before-compare approach
106+
clobbers the overflow carry and produces wrong quotients for those values.
107+
- `__sdiv16` — sign wrapper around `__div16`, mirroring `__sdiv8`: encodes
108+
signs in `HELPER_SIGN` (`$EC`, bit 7 = negate quotient, bit 6 = negate
109+
remainder), negates both operands, calls `__div16`, then restores signs.
110+
Follows C truncation-toward-zero convention. `needs_sdiv16 = 1` implies
111+
`needs_div16 = 1`.
112+
- New 16-bit helper ZP zone: `$E0–$E7` (`HELPER16_ARG1`/`ARG2`/`RES`/`REM`,
113+
2 bytes each), below the existing 8-bit zone at `$E8–$EC`.
114+
- ZP operand map upper bound tightened from `$EE` to `$DF` to reflect the
115+
new reserved zone; `zp_map_add` now enforces this with an address guard
116+
(previously only a count guard existed).
117+
- `EMIT_ARITH8(ROUTINE, RES_SLOT, NEEDS_FLAG)` /
118+
`EMIT_ARITH16(ROUTINE, RES_SLOT, NEEDS_FLAG)` — pair of local macros
119+
replacing the three verbose switch arms; bit width, arg-loading, and
120+
result-storing all collapse to one line per dispatch branch.
121+
- Emulator tests: `mul_u16` (300 × 13 = 3900), `mul_u16_wrap` (256 × 256 = 0,
122+
overflow), `div_u16` (50000 / `$C001` = 1, remainder = 847 — exercises the
123+
high-bit-divisor path).
124+
125+
- **Symbol table embedded in ROM** — compiled binaries now carry a `"C02S"`
126+
symbol table in the NOP fill area between the data section and `$FFF6`,
127+
letting `c02-objdump` show real function names instead of auto-generated
128+
labels. The table is always emitted by default; `--strip-debug` omits it.
129+
Binary size stays exactly 32 KB so EEPROM flashing is unaffected.
130+
- Footer layout: `$FFF6–$FFF7` = little-endian pointer to the table (or
131+
`$EAEA` NOP fill if absent); `$FFF8–$FFF9` = code/data boundary
132+
(unchanged); `$FFFA–$FFFF` = NMI/Reset/IRQ vectors.
133+
- Format: magic `C02S` (4 bytes) + u16 entry count (LE) + entries of
134+
u16 address (LE) + null-terminated name. All user-defined functions and
135+
emitted helpers (`__mul8`, `__div16`, etc.) are included.
136+
- Old binaries degrade gracefully: `$EAEA` at `$FFF6` passes the range
137+
check but fails the magic-byte check, so the disassembler falls back to
138+
`L0`/`L1`/… auto-labels without error.
139+
- `c02-objdump`: `parse_symbols` reads the table and merges it into the
140+
jump-target label map; `scan_end` for the data-section boundary scan
141+
stops at the symbol table start rather than `$FFF8` to avoid
142+
misidentifying table bytes as data.
143+
- `driver.h`: `params_t` gains `int strip_debug`; `main.c` adds
144+
`--strip-debug` to `long_options`.
145+
146+
- **ZP map overflow error propagation** — previously, `zp_map_add` printed a
147+
diagnostic to stderr and silently continued, potentially generating corrupt
148+
code. `zp_map_add`, `zp_map_add_operand`, and `zp_map_build` now all return
149+
`int` (0 = failure); `emit_function_from_cfg` checks `zp_map_build` and
150+
returns 0, propagating to `generate_rom` which returns `NULL` — the same
151+
path as all other codegen failures, ultimately exiting with
152+
`CODE_GEN_ERROR_RET_CODE` (7).
153+
154+
- **Bug fix: signed 8-bit division and modulo (`__sdiv8`)**`TAC_DIV` and
155+
`TAC_MOD` on `i8` operands previously routed through the unsigned `__div8`
156+
helper, so `i8 -6 / 2` computed `250 / 2 = 125` instead of `-3`. A new
157+
`__sdiv8` helper wraps `__div8`: it saves operand signs into a scratch byte at
158+
`$EC` (HELPER_SIGN), negates both operands to their absolute values, calls
159+
`__div8`, then restores the correct sign on the quotient (bit 7 of SIGN) and
160+
remainder (bit 6 of SIGN) per C's truncation-toward-zero convention. Codegen
161+
routes `TAC_DIV`/`TAC_MOD` through `__sdiv8` when `is_signed_type(dst.type)`;
162+
`__sdiv8` always calls `__div8`, so `needs_sdiv8 = 1` implies `needs_div8 = 1`.
163+
New opcode emitters: `bpl_rel`. `$EC` added to the helper ZP zone.
164+
- Emulator tests: `div_i8` (−6 / 2 = −3, PORTB = $FD), `mod_i8` (−7 % 2 = −1,
165+
PORTB = $FF).
166+
167+
- **Bug fix: binary op operand widening and sign normalisation** — binary ops
168+
derived both the result type and comparison signedness from the LEFT operand
169+
only (`ir.c`), ignoring the right operand entirely. Consequences: `u8 + u16`
170+
computed at 8 bits (the u16 high byte was silently dropped), while `u16 + u8`
171+
was accidentally correct; `i8 < u8` used a signed compare while `u8 < i8` used
172+
an unsigned compare — a trichotomy violation where both `a < b` and `b < a`
173+
could be simultaneously true. Fixed in the IR generator with four new helpers:
174+
- `ir_type_width` / `ir_is_signed` — predicates for 8-vs-16-bit and
175+
signedness without an `ir_gen_t *` context.
176+
- `binop_common_type(left, right)` — returns the wider type; for equal widths,
177+
unsigned wins (C's usual arithmetic conversions), eliminating operand-order
178+
dependence in mixed-sign comparisons.
179+
- `emit_widen_if_needed(gen, cfg, op, target)` — emits `TAC_CAST` when the
180+
operand type differs from the target; `OPERAND_CONST_INT` values are
181+
re-typed in place (no instruction emitted).
182+
- Binop lowering now normalises both operands to the common type before
183+
arithmetic and comparison ops. Shifts are guarded separately (result type =
184+
left, shift count is never widened). Pointer arithmetic skips widening.
185+
- Emulator tests: `binop_widen` (`u8(1) + u16(500) = 501`, high byte = $01),
186+
`cmp_mixed_sign` (`i8(−1) < u8(100)` with unsigned-wins → $FF reinterpreted as
187+
255, 255 < 100 = false, branch not taken, PORTB = $01).
188+
10189
## [v0.2.15] 2026-06-26
11190

12191
- **Function call codegen (`TAC_CALL`)** — full caller/callee ABI using a fixed

0 commit comments

Comments
 (0)