# Syntax Notes Every decision the example programs implicitly make, so we can revisit them deliberately instead of by accident. Nothing here is final. ## Decisions embodied in the examples ### Lexical - **Comments:** `#` line comments. Chosen over `--` because `--` collides with unary minus (`x--1` would silently comment out the rest of the line; Haskell needs special lexing rules to avoid this). `@` stays reserved for attributes (e.g. `@interrupt`). No block comments yet. - **Statements:** newline-terminated, no semicolons. Blocks use `{ }`. - **Line continuation (Option C):** a newline does *not* end a statement when (a) inside `(...)`/`[...]`, (b) the line ends with a token that can't end an expression — binary operators, `,`, `=`, `=>` — or (c) the next line starts with a token that can't begin a statement — `.`, `?`, `|`, `else`. The prefix/infix dual-role tokens (`-`, `*`, `&`, `(`) are deliberately in *neither* set, avoiding the JS-style ASI hazard. (Gleam goes further — newlines mean nothing, parsing is greedy — but that only works because it has no paren grouping and almost no prefix operators. Our four dual-role tokens rule it out. Lesson kept: every new prefix/infix dual-role token complicates continuation.) - **Hex literals:** `0x0400`. (Considered `$0400` for 6809 charm; `$` is still free if we want it as an alias.) - **Identifiers:** `snake_case` values/functions, `PascalCase` types and variant constructors, lowercase region parameters, `SCREAMING_CASE` consts. ### Types - **Primitives:** `Uint8`, `Int8`, `Uint16`, `Int16`, `Bool`, `Unit`, `Never`. Nothing wider. `Unit` is Rust's `()` (blocks/functions with no value; spellable, no literal); `Never` is Rust's `!` (uninhabited, diverging expressions, coerces to any type). `String` deferred (see open questions). - **No implicit conversions:** `x as Uint16` required to widen. On a 6809, 8-bit vs 16-bit arithmetic is a real codegen difference, so it stays visible. - **Integer literal typing:** context determines the type where possible (annotation, function argument, other operand, element type). No context: `Uint8` fallback (`Int8` when negative). A literal that doesn't fit its resolved type is a compile error requiring an annotation — including the fallback case (`let x = 300` errors rather than silently promoting to `Uint16`, so shrinking a literal never changes a type). - **Fixed arrays:** `[Uint8; 512]`, size part of the type. No slices yet — a fat pointer is 4 bytes on this machine; `for`/`enumerate` may cover enough use cases to skip them entirely at first. - **Records:** `type Point = { x: Uint8, y: Uint8 }`. - **Sum types:** `type Input = Idle | Move(Direction) | Fire`, ML-style, with optional leading `|`. Variants may carry positional payloads or named fields (`Shooting { cooldown: Uint8 }`). - **Generics:** `Option`, `Result`. Type params PascalCase. - **Recursive types:** only behind an arena pointer, so every type has a compile-time footprint. ### Bindings and functions - `let` immutable, `var` mutable. `const` for compile-time constants. - `fn name(x: Type) -> Type { ... }`; last expression of a block is its value (ML-style), `return` for early exit. - `if`/`match` are expressions. - **UFCS from day one:** `x.foo(y)` is sugar for `foo(x, y)`. There are no methods as a distinct concept — `arr.enumerate()`, `frame.new(e)`, and `cmds.push(x)` are all free-function calls. Resolution order for `.name`: field first, function in scope second. ### Control flow - `for x in array` — by value, bounds proven once per loop. **This is the load-bearing construct** for the bounds-elision design principle. - `for var x in array` — by-reference mutable iteration (02_blit `clear`). - `for (ix, x) in array.enumerate()` — index-carrying; the index is known in-bounds by construction and feeds downstream bounds proofs. - `for ix in 0 .. n` — half-open ranges. - `while cond { }`, `loop { }` (types as `Never`). - `match` must be exhaustive; `_` catch-all; tuple scrutinees allowed; integer patterns allowed for 8-bit types. `if let` for one-armed matches. - `?` propagates `Result` errors, as in Rust. (Prefix `try`, Zig-style, was considered — reads better in statement position but needs parens mid-expression. UFCS chaining is `?`'s main advantage, and we have UFCS, so `?` stays.) ### Arithmetic - **Overflow is checked, always:** `+`, `-`, `*` trap on overflow in every build — no debug/release divergence. The 6809 sets carry/overflow flags for free, so a check is one conditional branch (~3 cycles), and the same range analysis that elides bounds checks elides overflow checks (loop counters with proven ranges compile clean). - **Wrapping is explicit:** `+%`, `-%`, `*%` (Zig-style) for intentional modular arithmetic. ### Failure model (the "panic" story) - A failed check **traps**: no unwinding — disable interrupts, flash the border and write a fault code + address to the text screen (visible even with a corrupted display), halt. User-overridable later via `@panic_handler`. Costs a few bytes and a `JMP`. *(As implemented: `__trap` paints the fault code across the top 16 screen cells and hangs — no interrupt disable or address dump yet. Trap codes: 1 = bounds, 2 = overflow, 3 = arena exhaustion.)* - The complete list of trap sources, each with a total (non-trapping) alternative: 1. `arr[ix]` — avoid via `for`/`enumerate`/ranges, or `get(arr, ix) -> Option` 2. arithmetic overflow — avoid via proven ranges or `+%` 3. arena exhaustion in `new` — avoid via `try_new -> Option<&r T>`; static arena size bounds are a possible future for eliminating this statically 4. explicit `panic("...")` - Indexing with `[]` is therefore the *explicit* "trapping is acceptable here" form, not the idiom. A future lint/annotation could enforce "this function cannot trap". ### Memory model surface syntax - **MMIO:** `var pia1_a: Uint8 at 0xFF20` — pins a global to an absolute address; no storage allocated. Language feature, not library. - **Arenas:** `arena frame { ... }` opens a lexical region; bulk free at the closing brace. - **Allocation:** `frame.new(expr)` returns `&frame T`. The arena name is part of the pointer type. - **Region polymorphism:** lowercase region params share the `<>` list with type params: `fn cons(a: Arena, ...) -> &r List`. Casing distinguishes regions from types — no Rust-style `'r` tick. - **Subtyping:** outer arenas outlive inner ones; `&outer T` is usable where `&inner T` is expected, never the reverse. Globals can hold no arena pointers. - **Deref:** `*ptr`, explicit (05_regions `sum`). Whether field access auto-derefs (`ptr.head` vs `(*ptr).head`) is open. *(As implemented: arenas, `new`, `&a T`, deref, `*p = v` stores, the subtyping/escape rules, and region polymorphism (`fn f(a: Arena, ...) -> &r T`, `type Node = ...`, recursive types behind arena pointers) all work; type parameters (`T`) still wait on generics. Region bindings are inferred at call sites (arena arguments bind exactly, pointer arguments covariantly); distinct region parameters are unrelated, and a parameter's region outlives any arena the callee opens locally. Allocation enforces the escape rule too: memory in `a` may not hold pointers into arenas `a` outlives. Field access does **not** auto-deref: write `(*p).x`. Arena memory is mutable through any pointer, even one held by a `let` binding. The backing store is a bump heap from the end of BSS up to a per-target limit ($7000 for now, below Color BASIC's stack on a 32K machine); each `arena` block snapshots the bump pointer on entry and restores it on exit, and a function containing arenas also restores in its epilogue so early `return`s can't leak — unless it takes `Arena` parameters, in which case it allocates on its caller's behalf and must not restore (an early `return` from a local `arena` block then leaks into the caller's region until that region's block exits). Known simplification: everything shares one bump pointer, so allocating into an *outer* arena while an inner block is open hands back memory the inner block's exit will reclaim; the checker doesn't reject this yet. `new` traps with code 3 on exhaustion; `try_new` waits on `Option`.)* ### Target & program entry - **First target: disk `.BIN`** (`LOADM`/`EXEC` under Disk Extended Color BASIC), for the emulator dev loop (XRoar/MAME) and because a clean return to the BASIC prompt is itself a smoke test. ROM cartridge target later (different startup shim + copy globals from ROM to RAM). - **Memory layout** (globals, stack, arena backing store) lives in per-target config, not language syntax. `at` covers the only case where source should care. - **Startup shim** before `main`: set stack pointer, set DP register (direct-page globals are a future optimization target), call `main`. *(As implemented: the shim only runs global initializers and `JSR`s `main`, keeping BASIC's stack and DP — sufficient for the disk target, where BASIC's environment is live and we return into it.)* - **`main` signature:** both `fn main()` (may return — disk target: `RTS` back to BASIC) and `fn main() -> Never` allowed; the target checks compatibility. ## Decisions made during implementation Things the vertical-slice implementation forced that the paper design never specified. Semantics first, then ABI/codegen facts (which are implementation choices, not language contract — but record them so changes are deliberate). ### Semantics - **Shift amounts are always `Uint8`**, whatever the shifted operand's type — a 16-bit count is meaningless for 8/16-bit values. Shifts don't trap (they're bit ops, not arithmetic). - **Ranges (`a .. b`) are only legal as `for` iterables.** They are not first-class values; anywhere else is a type error. - **A const may only reference consts declared above it** (evaluated in declaration order; forward references error). Other items are fully order-independent. - **`==`/`!=` work on `Bool`; ordering comparisons (`<` etc.) don't.** - **Evaluation order is left-to-right** for binary operands and call arguments (pushed left-to-right). Assignments evaluate the *value* first, then the place (matters for `a[f()] = g()`). - **Integer consts must be constant-evaluable** (literals, earlier consts, and arithmetic on them — see `const_eval` in `check.rs`); the same evaluator serves array lengths and `at` addresses. ### ABI and codegen (crates/dodo/src/codegen.rs) - **Output:** DECB disk `.BIN`, ORG `$3000`, exec address = ORG. BSS (initialized non-`at` globals) sits immediately after the code; initializers run in the startup shim, so the file contains only code. - **Frames:** `U` is the frame pointer (`PSHS U; TFR S,U; LEAS -n,S`). Locals at negative offsets from `U`, args at positive (first arg highest); caller pushes args and cleans up. Frame limit 120 bytes. - **Returns:** `B` for 1-byte values, `D` for 2-byte, nothing for `Unit`/`Never`. Records and sums larger than 2 bytes can't be returned yet (needs an out-pointer convention). - **Sum layout:** a 1-byte tag followed by the largest variant's payload fields, concatenated with no padding. Sums that fit in 2 bytes ride the register conventions (tag in `A`, payload byte in `B`, i.e. `D`); larger sums are memory-resident like records: built/copied via the aggregate paths (`__memcpy` for place-to-place copies), matched by testing the tag byte in a hidden frame slot (the scrutinee is copied in, preserving by-value semantics), with payload bindings aliasing the slot at their field offsets. - **Runtime routines:** `__trap` (see failure model), `__mul16` (checked unsigned 16×16; `X * D -> D`, traps on overflow), `__memcpy`, and — only when the module uses arenas — `__alloc` (size in `D`, address out in `X`; bumps the 2-byte BSS cell `__arena_next` and traps with code 3 past the arena top). Arena pointers are plain 16-bit addresses, so they ride the 2-byte value conventions (`D` returns). - **Bounds checks** are elided for literal indexes provably in range and for `Uint8` indexes into arrays longer than 255; `for x in array` compiles to a pointer walk with no checks at all. - **Known cycle tax:** naive stack-based expression codegen makes tight loops ~10–20× slower than hand-written 6809 (measured empirically via `07_sound.dodo`, whose timing constants are tuned to current output). See TODO.md §2 (peephole optimizer). ### Temporary implementation gaps (not language decisions) These produce clean "not supported yet" errors; the *language* includes them, the compiler doesn't yet: - division and remainder (`/`, `%`) — no hardware divide; needs a runtime routine - signed multiplication - `for var` (by-reference mutable iteration) - array parameters, returns, and whole-array copies (arrays are initializer-only aggregates for now) - record returns, and returns of sums larger than 2 bytes - region parameters on sum types (`type List = ...`) - `Bool`/array consts in expression position (integer consts only; sums ≤2 bytes work as consts, bigger ones don't) ## Open questions 1. **String literal spelling.** The shape is decided: the VDG's character set is not ASCII (its 64-char set has inverse/normal video in bit 6, semigraphics in bit 7; uppercase happens to align with ASCII, digits and punctuation don't, lowercase doesn't exist). So we want *two* compile-time literal encodings: ASCII by default (ROM routines, disk, printer) and a VDG-encoded variant (direct screen pokes), e.g. `v"HELLO"`. Exact spelling and the `String` type itself still open. 2. **Auto-deref on field access** through `&r T` (see above). 3. **Growable `List` in arenas** (04_frame_arena): day-one feature or later? It's the first thing that needs realloc-ish behavior (bump allocators can only grow the *newest* allocation in place). 4. **Tuples.** 05_regions allocates `(xs, total)`; do we want full tuple types or only in `match` scrutinees? (A distinct tuple sigil à la Gleam's `#(...)` would remove `(` from the dual-role token list — but `#` is taken by comments, so it'd need another spelling.) 5. **`@interrupt` handlers** and other entry points beyond `main` — deferred until the disk target works. ## What the vertical-slice subset should be *(Status: ✅ done — implemented end-to-end and verified on XRoar; `01_hello` and `07_sound` compile and run. Kept for the record.)* Small enough to implement quickly, large enough that 01 and 07 compile: - `fn`, `let`, `var`, `const`, `at` globals - `Uint8`/`Uint16`/`Bool`, fixed arrays, `as` - `if`, `while`, `loop`, `for _ in array`, `for _ in a .. b` - arithmetic (checked, trapping) + `+%`/`-%`/`*%`, bitwise ops, comparisons, indexing with runtime checks - the trap handler (border flash + fault code + halt) Defer: sum types, `match`, generics, arenas, records, `?`, `enumerate`, UFCS. Reserve all their keywords now.