Analyze β Codegen contract
Spinel runs analysis and code generation as two phases of a single binary, sharing one in-memory state object:
.rb ββ[sp_parse_file_to_text]βββΆ text AST ββ[nt_load_text]βββΆ NodeTable
ββ[analyze_program(c)]βββΆ Compiler (filled) ββ[codegen emits]βββΆ .c
codegen_program(nt) (src/codegen.c) builds a Compiler, calls
analyze_program(c) to populate it, then walks the AST emitting C. The
analyzer and code generator are the same process and share heap state;
nothing is serialized between them.
Historical note. The retired Ruby self-host ran
spinel_analyzeandspinel_codegenas separate binaries that communicated through a text.irfile (SPINEL-IR v1, withINT/STR/SA/IA/T/NM/SN/STrecords). That file no longer exists. The C rewrite folded both stages into one binary, so the.irround-trip, its percent-encoding, and the four-way bootstrap fixpoint it backed are all gone.src/compiler.hkeeps the lineage explicit: "its field set corresponds to the legacy .ir dump."
The shared object: Compiler
src/compiler.h defines Compiler -- the modern equivalent of the old
.ir. Everything analysis derives and codegen consumes is a field on it.
The pieces that cross the analyze β codegen boundary:
Per-node arrays (indexed by AST node id)
| Field | Meaning |
|---|---|
ntype[id] | Inferred TyKind for the node. Read via comp_ntype(c, id), the type cache codegen consults instead of re-inferring. |
nscope[id] | Owning scope index (comp_scope_of). |
node_cbody[id] | Enclosing class/module-body class id, or -1. |
comp_ntype is the contract codegen relies on most: infer_type
(src/analyze*.c) fills ntype[] during analysis, and codegen reads it
back as the static type of every expression. (TY_STRBUF, a codegen-only
mutable-string storage refinement, is normalized to TY_STRING on read;
codegen consults the raw scope-local type where the distinction matters.)
Scopes and locals
scopes[] holds one Scope per def / class method / lambda / block
body plus the top-level scope (scopes[0]). Each Scope carries its
parameter list, inferred return type (ret), yield/block metadata, and a
LocalVar[] of params + body locals with their inferred type. Codegen
emits C function signatures, local declarations, and call dispatch
straight from these.
Program tables
classes[] (ivars + types, attr readers/writers, prepend chains, value-type
flag, compiler_state fields, β¦), consts[], gvars[] (+ aliases),
symbols[] (intern table), toplevel_includes[], and the FFI registry
(ffi_funcs / ffi_consts / ffi_bufs / ffi_readers / ffi_libs /
ffi_cflags). These are queried through the comp_* accessors declared
in compiler.h (comp_class_index, comp_method_in_chain,
comp_reader_in_chain, comp_resolve_alias, β¦).
Feature gating (which runtime helper blocks to emit, the class-machinery
tables, the main() prologue) is not stored as @needs_* flags the way
the legacy IR did; codegen recomputes it from the analyzed program via
scan passes (scan_prologue_features, program_needs_class_machinery,
β¦) immediately before emission.
What analyze does
analyze_program(c) registers the program's declarations, then runs type
inference to a fixpoint and fills the per-node type cache. Conceptually:
- Collect -- walk the AST registering classes, modules, methods,
constants, ivars, globals, and FFI declarations into the tables above
(
walk_scopeand friends insrc/analyze_scope.c). - Infer to fixpoint -- iterate the inference passes
(
src/analyze_infer.c,src/analyze_pass.c): return types, ivar types from writers, parameter types narrowed from bodies, poly locals/params, hash/array variant selection, bigint loop promotion, etc. The loop repeats until the derived types stop changing. - Finalize -- post-fixpoint refinements (value-type detection,
nullable re-narrowing, live-method marking) and then fill
ntype[]for every reachable node so codegen reads cached types.
Because the two phases share memory, codegen can also call infer_type
directly for the few nodes whose type is context-dependent at emit time
(block-body expressions whose iterator-derived param types are not
pre-cached, super arms that need the current class). The cache covers
the overwhelming majority; these are the deliberate exceptions.
Adding analysis state
Add a field to the relevant struct in src/compiler.h (Compiler,
Scope, ClassInfo, β¦), populate it in the analysis pass that derives
it, and read it in codegen. There is no serializer to update, no encoding
to round-trip, and no IR fixpoint to keep stable -- the value is just a
struct field both phases see. Verify with make check (test) plus
make optcarrot / make bench when the change touches type inference
(see the gates).
See also
- docs/AST.md -- the text AST format and the
NodeTablethe analyzer and code generator both read. src/compiler.h-- theCompiler/Scope/ClassInfostructs and thecomp_*accessor API.src/analyze.h--analyze_program/infer_typeentry points.