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.
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_exprnow yieldstrailing_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_matchgainedvalue_position: bool. In value position a block arm flattens its trailingExprStmtinto the matchtarget. The subtlety the--explaintool revealed:create_scopes_map_astappends the block's scope-end drops (markedstatement_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 barematch:statement path (in theExprStmtlowering) callslower_match(..., value_position=false)so statement matches still run arms for effect (what the http tests need). Also:parse_statementnow 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::MAXtrailing-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/elseas an expression (let x = if c: 1 else: 2) is a language decision, not part of Bug 13 (SYNTAX_POLISH). Python-flavored languages often keepifa statement.
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.
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.
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:
parser-refactor, not merged tomain(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 docgenor the two doc-sync tests fail. - When merging to
main, early verbose commit bodies can be squashed at merge time.
Done on this thread, in order:
- String library in the prelude (
char_at,str_to_int,starts_with,index_of,split,read_int) over nativebyte_at/substring, with>=/<=/!=/and/orwired 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-unionenumdeclarations (variant-as-struct, the descriptor id is the tag), variant construction with generic instantiation,matchas statement and expression with exhaustiveness checking, preludeOption/Result/Errorwith methods, the?operator, and the fallible-builtin convention (persistent status plus__status(), first userparse_int). Generic types in function signatures monomorphize.plans/MATCH_MATRIX.mdtracks match coverage, andexamples/error_handling.ssshows the arc end to end. - I/O and concurrency designed before building (
plans/IO_DESIGN.md): the readiness model (Ready/Pending plus onepollwait 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(wasfn, clean slate, no alias). Cosmetic backlog lives inplans/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 throughBuiltin::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, andBuiltin::from_namekeeps 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.
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):
- Phase B, std lib assembly: grow the prelude into a small embedded std tree, move
Write*/ReadIntinto the language over the thin builtin layer,list[T]()empty constructor (now unblocked by generic types), listpop/set,List[T]/Map[K,V]. - Phase C: generic protocol conformance checking (parameterized protocol references in
type/extendheaders). Type-checking polish, folds intoProtocolRegistry. - Phase D: overloaded constructors, reusing the overload table (register the implicit all-fields constructor as an overload, allow user constructor overloads).
- 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. - 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.
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.
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,-iinteractive session after running,-vdumps IR and decoded bytecode. The injected program runs on startup. The-isession runs each line standalone (no persistent scope yet, a real REPL is future work). printbuiltin (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 inplans/BUILTINS.md.- Output sink (
4fcc08e):OutputSinkon the executor, an enumStdout/Buffer/Forward(Box<dyn FnMut(&[u8])>). The print opcodes write throughexecutor.write_output*. Chosen over a channel because the WASM target is single-threaded. TheForwardcallback is the streaming hook for JS. The CLI usesStdout(live), tests useBufferand assert the text.
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.
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.
- Duplicate diagnostics fixed (
173c57d): undefined reads are reported once, only during lowering, not also during scope-mapping. Testundefined_variable_reported_once. - Unary codegen implemented (
173c57d):-x(via0 - x) andnot x(LogicalNot) lower instead of hitting atodo!(). Testunary_negation_and_not. - String concat codegen returns
Resultinstead ofassert!/unwrap(d730b8a). CallFunctionis 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-basedCallFn.
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.
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.
- VM: dispatch loop uses the autogenerated
DecodedInstruction::execmatch (#[inline]), boxeddyn Fnpath kept behindexecute_processeded_bytecode_boxedfor A/B benching. Function registry (FunctionMeta,set_functions,function_meta,function_entry) andCallFn(func_id)opcode added. Criterion bench atsolid-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.rsno longer rewrites them on run, redundant top-levelINSTRUCTIONS.mddeleted (canonical issolid-snake-vm/INSTRUCTIONS.md). - Parser refactor (
plans/PARSER.md): iterative precedence via shunting yard, non-self combinator toolkit (parser/combinator.rs),MAX_NESTING_DEPTHguard for blocks, deadParsetrait removed, fully iterative marker-based grouping (no recursion). - Function front-end (parsing only):
fn,return,->tokens, postfix call parsing,fndefinition parsing,returnparsing. 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/plusDESIGN_NOTES.md),PARSER.md,FUNCTIONS.md,CODEGEN.md,REWRITE_ENGINE.md,LOWERING.md.
bytecode_gen.rs:TypedIRStmt::Call,TypedIRStmt::Return, andTypedIRExpr::Unaryarms aretodo!()(codegen slices for step 4). The lenient-to-strict converter inintermediate_pass1.rsdoes handle Call/Return now.intermediate_pass1.rs:create_scopes_map_asthasASTNode::FunctionDefas a no-op andASTNode::Returnas scope-map only (no IR).flatten_expr_to_varreturns an error forExprKind::Call. These are the codegen slices not yet done.- Legacy address-based
CallFunctionstill exists alongsideCallFn. Retire once codegen uses the registry. typing.rsstill uses thepassret!/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.
- Lowering: one flat canonical IR, CFG or SSA only as derived extra layers. Generic
Ir<Ty>for by-construction typing. Errors reported at the lenientIr<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).
- 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").