Skip to content

Latest commit

 

History

History
456 lines (383 loc) · 31.6 KB

File metadata and controls

456 lines (383 loc) · 31.6 KB

Handoff Notes (living, update or delete later)

Last updated 2026-07-21. On branch fix-bug-13-match-arm-value (branched from main, which is at the merged std.http and growable-buffer work). Trim sections as they stop being true.

Bug 13 FIXED and committed on this branch, ready to merge to main

Branch fix-bug-13-match-arm-value (commits 978f5f3 --explain, da5b2fc the fix, 84437d6 comment cleanup). 375 compiler + 683 VM green. The only remaining step is merging this branch to main (nothing else outstanding on the branch).

  • What it fixes. return match e: case X: let a = ...; trailing_expr now yields trailing_expr (previously the trailing expression was dropped and the match result left unassigned, so it returned a stale value). Verified across Int, String, and reference-payload arms, plus a leak check.
  • How. In intermediate_pass1.rs, lower_match gained value_position: bool. In value position a block arm flattens its trailing ExprStmt into the match target. The subtlety the --explain tool revealed: create_scopes_map_ast appends the block's scope-end drops (marked statement_id == usize::MAX) at its end, freeing the block's locals BEFORE the trailing expression read them. Fix splits that trailing drop batch off and emits it AFTER the tail flatten. The bare match: statement path (in the ExprStmt lowering) calls lower_match(..., value_position=false) so statement matches still run arms for effect (what the http tests need). Also: parse_statement now routes literal/(/[/-/not-led statements to expression parsing, so a block-arm value can start with a literal.
  • Follow-up (future, not blocking), user's idea: replace the statement_id == usize::MAX trailing-drop partition heuristic with an explicit per-drop flag marking whether a drop must defer past a value expression (reassignment/return/arm-value). Cleaner than the marker.
  • Still a separate open item: if/else as an expression (let x = if c: 1 else: 2) is a language decision, not part of Bug 13 (SYNTAX_POLISH). Python-flavored languages often keep if a statement.

The --explain tool (committed this session, 978f5f3)

solid-snake-compiler -- --explain file.ss prints each source line with the IR it lowered to, grouped by span. It made Bug 13 tractable (showed the drop firing before the read). Built in runner.rs (explain), wired in main.rs. Compiled now carries ir_functions.

Prior session landmarks (all on main)

std.http v1 landed and adversarially reviewed (plans/HTTP.md). Static imports v1 (plans/MODULES.md, SourceId on spans for correct diagnostic attribution). Green threads, error handling, generics. The pluggable builtin registry (plans/BUILTIN_REGISTRY.md). The growable string buffer / StrBuf (plans/GROWABLE_BUFFER.md, stage 1a done and reviewed; stage 1b programmable strings PLANNED). Web demo rebuilt with a clickable examples sidebar and a Lisp interpreter example (examples/lisp.ss), GitHub Pages workflow (.github/workflows/pages.yml, needs Pages turned on in repo settings; demo URL guessed as axmouth.github.io/solid-snake). Bugs 8-11 fixed. Bug 12 (intrinsic call args are not type-checked) is OPEN (plans/KNOWN_BUGS.md). HTTP hello-world is ~285us p50, unchanged by the buffer (buffer helps large-string building, not hello-world; keep-alive is the real lever). Design corpus and status live in plans/INDEX.md.

Next arcs after Bug 13

Per the chosen order: Python-faithful per-file import visibility (plans/MODULES.md v2 section, the resolver-chokepoint audit gates it), then the intrinsically-safe-concurrency design consolidation (plans/CONCURRENCY.md + DESIGN_NOTES Owned/Ref). Also queued: Bug 12, the is_reference consolidation, stage 1b programmable strings, and the doctrine cleanup (mostly absorbed by 1b). The per-task process is the arc skill (.claude/skills/arc). The review-angle palette: correctness, simplification, altitude, efficiency, adversarial, consistency, test-coverage, API/future-compat.

Branch and state

  • Branch: parser-refactor, not merged to main (user chose not to merge yet). Every step is committed, tree clean.
  • Tests green: 342 compiler, 676 VM. Build clean. Adding any VM opcode means re-running cargo run --bin docgen or the two doc-sync tests fail.
  • When merging to main, early verbose commit bodies can be squashed at merge time.

Current thread: toward the web server (plans/WEB_SERVER_TARGET.md)

Done on this thread, in order:

  • String library in the prelude (char_at, str_to_int, starts_with, index_of, split, read_int) over native byte_at/substring, with >=/<=/!=/and/or wired in codegen.
  • Register allocator rebuilt (plans/KNOWN_BUGS.md): both control-flow RC bugs fixed, and spilling made branch-safe via a static register/memory partition with loop-weighted victim selection. Functions with more live variables than registers compile and run correctly across branches, loops, calls, and reference locals.
  • Error handling arc complete (plans/ERROR_HANDLING.md, all six phases): tagged-union enum declarations (variant-as-struct, the descriptor id is the tag), variant construction with generic instantiation, match as statement and expression with exhaustiveness checking, prelude Option/Result/Error with methods, the ? operator, and the fallible-builtin convention (persistent status plus __status(), first user parse_int). Generic types in function signatures monomorphize. plans/MATCH_MATRIX.md tracks match coverage, and examples/error_handling.ss shows the arc end to end.
  • I/O and concurrency designed before building (plans/IO_DESIGN.md): the readiness model (Ready/Pending plus one poll wait primitive), thin non-blocking socket builtins with blocking files, a swappable capability backend, a colorless blocking-looking surface (async/await rejected for its coloring cost), and green threads later via one language-neutral VM coroutine primitive with the scheduler as userland prelude code. Also settled: the host side is a poller not a runtime (mio decided, for Windows), fuel-based preemption with compiler-inserted costed checks, and the minimal VM footprint (two context builtins, everything else userland).
  • Function keyword migrated to def (was fn, clean slate, no alias). Cosmetic backlog lives in plans/SYNTAX_POLISH.md.
  • Metadata decorators built (plans/DECORATORS.md): @intrinsic.builtin("symbol") with a required ... stub body binds a prelude declaration to a native builtin through Builtin::from_symbol. The declared signature is validated against the intrinsic so drift is a compile error. Every fixed-signature builtin is now declared in the prelude, and Builtin::from_name keeps only the compiler-dispatched intrinsics (print, len, push, pop, set). Runtime (Python-style) decorators are a separate future feature needing first-class functions.

Next per the plans: file I/O builtins (plans/FILE_IO.md) binding through @intrinsic.builtin, then the non-blocking TCP substrate and the blocking Connection/Listener prelude (plans/TCP_NETWORKING.md), then a single-connection examples/web_server.ss.

Future design work is queued in plans/ASSESSMENTS.md (AOT-compilation compatibility ledger, static imports and a parallel front end, inlining/unrolling/TCO, refined types, async runtime survey follow-ups). Also noted there in spirit: several prelude free functions may become methods once built-in types can be method receivers.

Resume point (current)

The language now runs real programs (see examples/): variables and reassignment, Int/Float/Bool arithmetic and unary -/not, comparisons, if/elif/else, while, functions with recursion, nominal structs (construction, nested field read and assign), methods in the type body with static dispatch, protocol declarations with nominal conformance, extend blocks (inherent methods and retroactive conformance), variadic print (Int/Float/Bool/String), read_int() / read_line() from input, and reference counting for structs and strings (construction, recursive free, field- and variable-overwrite release). Detailed milestone sections are below.

Methods, protocols, and extend (plans/INTERFACES.md): a method is hoisted to a top-level function Type.method with self typed as the owning type, reusing the whole function pipeline. receiver.method(args) resolves statically. Conformance is checked by matching name, explicit parameter types, and return type. The ownership gate is trivial in a single program (the extend target must be a known type) and is fully enforced once modules land.

I/O runs through a builtin/host-function layer, not opcodes. Builtins ride CallFn with the top id bit set (FUNC_BUILTIN_FLAG, BuiltinFn registry in solid-snake-vm/src/opcodes.rs), dispatched in callfn to native handlers in executor/interpreted/builtins.rs that read args from R1.. and write R0. print lowers to Write* builtins and the typed print opcodes are retired. Input mirrors OutputSink via InputSource (Stdin/Buffer/Forward/Empty) on the executor. The design is in plans/INSTRUCTION_SET_DOCTRINE.md and plans/BUILTIN_FUNCS.md. The typed Write* and the parse in ReadInt are documented native intermediates, meant to become language std functions over the raw Write/ReadLine once formatting is expressible.

Interfaces build order (INTERFACES.md): methods, protocols, extend, the Delete finalizer, and generics are all done. Delete runs a conforming type's delete as its descriptor.finalizer at refcount zero (the executor owns the decoded program so it can invoke the finalizer re-entrantly from DecRef). Generics use monomorphization: type args inferred from arguments, bounds checked, instances substituted and lowered via a worklist. See examples/ for working programs (shapes, generics, methods, resources, input).

Iteration is done. There is a prelude (solid-snake-compiler/src/prelude.ss, embedded and prepended to every program) holding an Iterator protocol, a Range type, and range(n). for x in e: is pure parser/desugar sugar to let it = e.iter(); while it.has_next(): let x = it.next(); body, so iteration behavior is library code and any type providing iter/has_next/next is iterable (see examples/iteration.ss, including a user Countdown). The for desugar lowers in its own scope so the iterator is dropped exactly once.

Error handling was hardened: the front end skips semantic analysis when parsing failed (no more misleading follow-on errors), parse recovery skips to the next statement boundary, and the reporter no longer panics on end-of-input spans.

Arrays are done: literal [a, b, c], indexing arr[i] (bounds-checked, out-of-range traps), len, and reference counting including reference elements (the VM Descriptor gained array_elem_ref so release_cascade traces inline elements). Arrays are fixed-size (inline elements after the header).

for over anything is unified through the iterator protocol (plans/GENERIC_TYPES.md). for x in seq: desugars to let it = <iter>; while it.has_next(): let x = it.next(); body, one path for every iterable. A user iterable supplies iter(). Arrays and lists get prelude generic iterators ArrayIter[T] / ListIter[T] (built on len and indexing), constructed at the loop since the built-in collections are not method receivers. So iterating an array or list of any element type, including user structs, types the loop variable correctly and dispatches its methods (verified: an array of a user Point type calling p.sum() in the loop). The whole desugar lowers in one create_scopes_map_ast pass inside a dedicated scope so temporaries drop once (calling it twice on one scope double-drops, since scope-end drops fire per call). emit_assign_var carries the source type across variable-to-variable assignment (let b = a), which the iterable probe and indexing rely on.

Generics cover functions AND types now. Generic types (type Box[T]:) are monomorphized: type args inferred from constructor arguments (with nested unification, so ArrayIter(arr, 0) infers T from Array[T] vs Array[Int]), instances registered per concrete argument with a descriptor and hoisted methods, and reference counting works through substituted field types. Descriptor ids for arrays, the list, and generic type instances come from a monotonic counter (alloc_descriptor_id) so late registration during lowering never shifts ids. A parameterized type reference is IntermediateType::Generic { name, args }, collapsed to a mangled Custom at instantiation, so it never reaches codegen.

Overloading covers free functions and methods, by parameter types, with concrete overloads specializing generics (plans/FUNCTIONS.md).

Runtime strings and formatting are done (Phase A of plans/ROADMAP_GENERICS_STDLIB.md). A --no-opt flag (AnalysisOptions { fold_constants }, threaded through compile_with_opts/analyze_ast_with_opts) disables constant folding so runtime string paths run instead of being folded away. Source-based tests now run under both fold modes and assert identical output, catching mode divergences. Runtime string concat, string parameters, and string returns work. Number-to-string is IntToStr/FloatToStr/ BoolToStr builtins, exposed as int_to_str/float_to_str/bool_to_str intrinsics. The user-facing surface is a single str(x) in the prelude (scalar overloads over the builtins, plus a Display protocol and a generic bridge str[T: Display] routing user types to x.str()). Two real bugs the no-fold flag surfaced were fixed: string ==/!= compared heap identity not content (now a StrEq builtin), and + normalization pushed a constant operand left (valid for numbers, reversed string concat) so it now leaves string-literal operands in source order.

Growable lists are done: list(array) builds a List (a small {len, cap, backing-array} object reusing StructNew and one universal list descriptor whose backing field is traced), push(xs, x) appends with amortized doubling, xs[i] and len work, and freeing releases the backing and its elements. Arrays back the list storage. The backing's length field tracks valid elements so RC tracing is correct. pop(xs) (transfers ownership), set(xs, i, x) (releases old, retains new), and empty list() (element type from a let xs: List[T] = list() annotation) are also done.

String-keyed Map[K, V] is done: a native, hashed, open-addressing map (IntermediateType::Map, a Map keyword). map() builds an empty map (types from the annotation), set(m, k, v) inserts or updates, m[k] reads, has(m, k), len(m). Keys and values are reference counted via a universal map descriptor with an is_map flag. release_cascade traces each entry's key and (when values are references, a flag stored in the map object) its value, then frees the entries table. set/len dispatch to the map builtins when the receiver is a Map (builtin_for_call). Non-string keys and map iteration are not built yet.

let-type annotations (let x: T = expr) parse an optional declared type, checked against the initializer and used as a construction hint (this is what unblocks the empty list()/map()).

Open work, in priority order (see plans/GENERIC_TYPES.md and ROADMAP_GENERICS_STDLIB.md):

  1. Phase B, std lib assembly: grow the prelude into a small embedded std tree, move Write*/ReadInt into the language over the thin builtin layer, list[T]() empty constructor (now unblocked by generic types), list pop/set, List[T]/Map[K,V].
  2. Phase C: generic protocol conformance checking (parameterized protocol references in type/ extend headers). Type-checking polish, folds into ProtocolRegistry.
  3. Phase D: overloaded constructors, reusing the overload table (register the implicit all-fields constructor as an overload, allow user constructor overloads).
  4. The type-checker carve-out's remaining core (a literal pre-lowering typing pass) is deferred and scoped in plans/GENERIC_TYPES.md. The inference engine and module extractions are done.
  5. Dynamic dispatch (vtables). List polish already noted above.

Function overloading is done (plans/FUNCTIONS.md): free functions overload by parameter types, resolved by exact match (no conversions), with a concrete overload specializing a generic of the same name (concrete-then-generic). The prelude now has range(n) and range(start, stop) (a start > stop range is empty). Methods stay single dispatch for now, and specialization is the simple concrete-then-generic rule rather than a full specificity lattice.

Deferred with reasons (in the sections below): early-return drops for non-parameter locals (needs CFG-based drop insertion, risk of dropping the returned value). Reference parameters are already released on every return path (emit_return_param_drops), so functions and methods taking reference arguments do not leak. Also deferred: runtime string concat + string params (type-mismatch gap), the mega-pass split, F4 variable-width frames, escape-analysis RC elision, cycle collector, a --no-opt debug flag to disable folding, a del keyword for early unbind.

Function codegen and earlier lowering (all DONE)

Dual IR collapsed to generic Ir<Ty> (169ae1d), IrFunction units (5a30129), function codegen end to end (93c4e9a): collect_function_decls, per-function IR buffers, lower_program laying out main + functions, CallFn(func_id) + the FunctionMeta registry, process_instructions_with_labels for entry offsets. Args in R1.. (max 3, RESERVED_REGS - 1), result in R0, per-frame register files. Caveats: MoveU64 for moves is scalar-correct only, spilling-across-calls untested, legacy CallFunction still present. The mega-pass split (step 3) was skipped, not needed.

Demo status: working on the CLI

cargo run -p solid-snake-compiler -- test_data/fib_demo.ss prints fib(1)..fib(15). The demo uses a recursive fib, a while loop, and print. Pieces that landed for it:

  • CLI (a8a1408, clap): solid-snake-compiler [FILE], -c <code> inline, -i interactive session after running, -v dumps IR and decoded bytecode. The injected program runs on startup. The -i session runs each line standalone (no persistent scope yet, a real REPL is future work).
  • print builtin (04e5719, ccef7c2, 4fcc08e): variadic, space-separated, trailing newline. Expression statements in the parser (ASTNode::ExprStmt, parse_expr_or_assignment), a compiler intrinsic (Builtin::Print -> IrStmt::CallBuiltin), and per-argument codegen dispatch by type. VM opcodes: PrintI64, PrintF64, PrintBool, PrintStr, PrintByte (separators/newline). Int, Float, Bool, String all print. Design in plans/BUILTINS.md.
  • Output sink (4fcc08e): OutputSink on the executor, an enum Stdout / Buffer / Forward(Box<dyn FnMut(&[u8])>). The print opcodes write through executor.write_output*. Chosen over a channel because the WASM target is single-threaded. The Forward callback is the streaming hook for JS. The CLI uses Stdout (live), tests use Buffer and assert the text.

String heap layout (unified, 36d5d83)

Strings are [byte length: u64][bytes] on the heap, whatever their origin. The compiler no longer adds its own length prefix to string constants. StoreConstantArray's byte-length header is the single prefix, matching what emit_string_concat already produces. PrintStr reads length at offset 0 and bytes at offset 8. This also fixed concatenation of string literals, which previously misread the old double prefix (test prints_concatenated_string). The stored length is byte length. Char-count semantics are a separate higher-level concern for later. When heap objects get real descriptors, this header becomes part of that scheme.

Web demo (WASM): working (1ae794d)

solid-snake-wasm is a wasm-bindgen cdylib exposing run(src) -> String, calling the shared solid_snake_compiler::runner (factored out in 87c44b2 so the CLI and wasm share one compile path). The CRT-themed page is solid-snake-wasm/web/index.html. build.sh compiles for wasm32-unknown-unknown and runs wasm-bindgen --target web into web/pkg/ (gitignored). Verified end to end in Node (fib 1..15 prints correctly), so the browser path works too.

Setup: rustup target add wasm32-unknown-unknown and cargo install wasm-bindgen-cli --version 0.2.126 (must match the crate version). See solid-snake-wasm/README.md.

F4 frame slim done (99940ea): INITIAL_FRAMES_CAPACITY and FRAME_ALLOCATION_CHUNK dropped from 2^16 to 1024, so the frame stack starts at ~2 MiB instead of ~128 MiB and grows on demand. Deep recursion still works (tested depth 2000), and VM tests got ~6x faster as a side effect. The deeper part of F4 (variable-width frames sized by nlocals instead of fixed 128x2 registers) is still open if frame memory ever matters more.

Streaming + robustness (e8f956f): the page now runs the wasm in a Web Worker. Output streams live via runner::run_streaming -> OutputSink::Forward -> a JS callback (run(src, on_output) in the wasm crate, using js-sys). The main thread stays responsive, a Stop button terminates and respawns the worker (for the exponential hang at high fib n), and a VM trap is caught and the worker replaced. Compile/runtime errors render in a distinct color.

Codegen fix (c8c501e): the spill-table allocation is skipped when a frame cannot spill (var_count <= usable registers). Previously every call allocated a spill table that was never freed, so deep recursion (e.g. high fib) leaked heap until wasm memory ran out and trapped.

Convention: no unwrap/expect/panic in non-test code, because a panic traps and poisons the wasm instance. The run path (runner, solid-snake-wasm, the CLI VM run) was cleaned up. Pre-existing unwraps remain in bytecode_gen register allocation and VM opcode handlers, to clean up opportunistically.

Consolidation pass (done)

  • Duplicate diagnostics fixed (173c57d): undefined reads are reported once, only during lowering, not also during scope-mapping. Test undefined_variable_reported_once.
  • Unary codegen implemented (173c57d): -x (via 0 - x) and not x (LogicalNot) lower instead of hitting a todo!(). Test unary_negation_and_not.
  • String concat codegen returns Result instead of assert!/unwrap (d730b8a).
  • CallFunction is kept on purpose, not retired: it is the direct-address call primitive for hand-written asm and the VM benches/tests, alongside the compiler's registry-based CallFn.

Remaining opportunistic unwrap cleanup: the spill-path helpers (emit_spill_store/load, get_var_register restore) which only run when a frame spills (>117 vars), and BCType::from_ir_type's todo!() arms for aggregate types, which become real when those types land.

Roadmap (next)

The next breadth arc is types, objects/structs, and an interface mechanism, which also gives the memory model real work. for/range is deliberately deferred until range is a real type and iteration goes through an interface, so it generalizes instead of being a one-off. Design and the settled nominal-vs-structural model are in plans/TYPES_AND_OBJECTS.md.

Structs (nominal) work end to end (d12c2a9): type Point: with indented fields parses to an ordered StructDef, collect_struct_decls registers a layout (descriptor id, per-field offsets), construction is Point(1, 2) (a Call whose callee names a type), field access p.x and field assignment p.x = ... lower to IrStmt::StructNew/FieldGet/FieldSet, and codegen allocates a heap object ([descriptor id][fields]) and does offset load/store. Nominal type values resolve to ProcessedType::Custom (a u64 heap handle). Not yet done: RC (objects leak for now, as strings once did), structural types (the paren (x: Int, y: Int) syntax and parse_type switch from braces), interfaces, and nested/heap field types beyond scalars.

Reference counting baseline works for structs (14bcd31, 5d0b7dc). The object header is [descriptor id: u64][refcount: u64][fields] (OBJECT_HEADER_SIZE 16, REFCOUNT_OFFSET 8, matched by the VM). IncRef/DecRef opcodes adjust the count. DecRef frees the section at zero. Construction seeds the count at 1. Codegen emits retain at share points (alias assign, struct arg pass, field read, field store) and release at Drop, via type-directed emit_retain/emit_release glue (drop-glue / value-witness style, so call sites never repeat the reference-vs-scalar decision). Verified reclaiming: a 50-iteration construct+drop loop keeps the heap at a handful of sections (rc_frees_dropped_structs).

RC chose dedicated opcodes over compiler-emitted load/add/store: fewer interpreter dispatches, matches Swift/ObjC ARC precedent, keeps the header layout in one place, and keeps inc/dec elidable for the later escape-analysis optimization.

Descriptor table works (005c4e8). A Descriptor { size, ref_field_offsets, finalizer } table is registered with the VM (set_descriptors, built from StructLayout, indexed by the descriptor id in the object header). DecRef-to-zero now does worklist-based recursive release: it reads the descriptor, releases the object's reference fields, then frees, cascading. Struct args stored into fields are retained so they survive. Verified: nested Outer(Inner(..)) in a loop frees both, heap stays bounded (rc_recursively_frees_nested_structs). One fix along the way: DCE used to elide a dead temp's Drop, which is now a DecRef, so it must keep reference-typed drops.

Field overwrite (666f2a2) and variable reassignment (8916f69) now release the old value: reassigning a reference field or variable frees what it held. Reassignment lowers a reference target as evaluate-into-temp, release old, move temp in (so the right side can still read the old value). Scalars keep the direct path.

RC gaps (all safe leaks, not unsafety): only structs are RC'd (strings/arrays use a headerless heap layout and still leak). Early return skips drops of still-live reference locals. Reference cycles leak (needs the cycle collector). Finalizers (Delete interface -> descriptor.finalizer) are wired in the descriptor but finalizer is None until interfaces exist.

Early-return drops are deferred on purpose: doing them safely needs flow-sensitive drop placement that drops live reference locals on the exit path without dropping the value being returned (that would be a use-after-free, worse than the current leak), which is the CFG-based drop-insertion pass in LOWERING.md. An ad-hoc version in the current mega-pass risks double-free and use-after-free. The leak is niche (a function with reference locals and an early return. Scalar early returns like fib's do not leak).

Strings under RC are done (plans/memory/DESCRIPTOR_TABLE.md). Strings now carry the object header [descriptor][refcount][byte length][bytes] (length at 16, bytes at 24), created with refcount 1 by StoreConstantArray and read by PrintStr. emit_string_concat allocates the same headered layout (unreachable for now since constant concat folds, correct once non-constant strings exist). String joined the is_rc set, so strings are retained, released, and freed, and a struct releases its string fields recursively. The work surfaced a DCE bug (a dead reference temp's Drop was kept while its assign was elided, giving a DecRef on an uninitialized register once strings were reference counted). DCE now computes liveness up front so a drop and its definition share a fate. Arrays remain the only leaky type, and they do not exist yet.

Escape-analysis elision (drop instead of RC for non-escaping single-owner objects) is the planned optimization on top of all this.

Codegen gotcha worth remembering: StoreIndirectWithOffsetU64 operand order is (section_ptr, value, offset) and LoadIndirectWithOffsetU64 is (dest, section, offset). The section operand is a PtrRegisterType (the in-progress pointer-register separation), but the handlers still read it from the raw bank via get_value, so the bank is type-level only for now.

Other open follow-ups: unify string char-length vs byte-length semantics. Real REPL state persistence. The function-call ABI (widen the in-register arg region past 3, with a descriptive error past the cap, rather than heap arg-spilling). Deeper F4 (variable-width frames). See plans/NATIVE_BACKEND.md for the native seam.

What is done in this work stream

  • VM: dispatch loop uses the autogenerated DecodedInstruction::exec match (#[inline]), boxed dyn Fn path kept behind execute_processeded_bytecode_boxed for A/B benching. Function registry (FunctionMeta, set_functions, function_meta, function_entry) and CallFn(func_id) opcode added. Criterion bench at solid-snake-vm/benches/vm_exec.rs.
  • Deps: criterion 0.8, logos 0.16 (greedy-dot fix), strum 0.28, plus in-range updates.
  • Doc hygiene: instruction docs regenerated, main.rs no longer rewrites them on run, redundant top-level INSTRUCTIONS.md deleted (canonical is solid-snake-vm/INSTRUCTIONS.md).
  • Parser refactor (plans/PARSER.md): iterative precedence via shunting yard, non-self combinator toolkit (parser/combinator.rs), MAX_NESTING_DEPTH guard for blocks, dead Parse trait removed, fully iterative marker-based grouping (no recursion).
  • Function front-end (parsing only): fn, return, -> tokens, postfix call parsing, fn definition parsing, return parsing. AST nodes, pretty-printing, and tests in place.
  • Plans authored or mined: architecture audit (docs/ARCHITECTURE_AUDIT.md), benchmark report (docs/BENCHMARKS.md), memory and runtime design (plans/memory/ plus DESIGN_NOTES.md), PARSER.md, FUNCTIONS.md, CODEGEN.md, REWRITE_ENGINE.md, LOWERING.md.

Intentional stubs and deferrals (do not mistake for bugs)

  • bytecode_gen.rs: TypedIRStmt::Call, TypedIRStmt::Return, and TypedIRExpr::Unary arms are todo!() (codegen slices for step 4). The lenient-to-strict converter in intermediate_pass1.rs does handle Call/Return now.
  • intermediate_pass1.rs: create_scopes_map_ast has ASTNode::FunctionDef as a no-op and ASTNode::Return as scope-map only (no IR). flatten_expr_to_var returns an error for ExprKind::Call. These are the codegen slices not yet done.
  • Legacy address-based CallFunction still exists alongside CallFn. Retire once codegen uses the registry.
  • typing.rs still uses the passret!/passcont! record-and-recover style (PARSER.md step 3 deferred, it is entangled with test-pinned recovery).
  • Audit F4 (fat fixed-width call frames, 128 MiB reservation) not done, deferred, not urgent.
  • Adding any opcode means regenerating docs (cargo run --bin docgen), or the two doc-sync tests fail.

Settled decisions (so future self does not relitigate)

  • Lowering: one flat canonical IR, CFG or SSA only as derived extra layers. Generic Ir<Ty> for by-construction typing. Errors reported at the lenient Ir<IntermediateType> level. Function-granular phasing, not streaming (variables need forward references).
  • Calls are FuncId-based via the registry and CallFn, not raw addresses.
  • Descriptors and the bytecode file format wait until they have real consumers (heap objects, modules). The function registry was the wanted piece and is built.
  • Memory management should be invisible to the end programmer, explicit controls strictly opt-in. RC plus cycle collection is the chosen direction (see plans/memory/OVERVIEW.md).

Conventions

  • Prose and comments: no semicolons, no em dashes, plain keyboard quotes, low noise. Comments explain why, never reference the chat or reviews, and only exist when they earn it.
  • Commit messages: terse one-liners matching the repo's existing style.
  • Diagnostics, syntax errors especially, must be specific and well-spanned. Prefer ParseError::unexpected(tok, "what was expected").