Plan 02 — soil0: the minimal Soil implementation
References: docs/soil-syntax-spec.md (the parser’s contract), design §3 (language), bootstrap plan §1.
Goal
A deliberately small Rust implementation of Soil — parser, static checks,
tree-walking interpreter over soil-rt values — whose every pass is a
JSON-in/JSON-out CLI command. It is the execution engine for the entire
bootstrap and the permanent differential oracle for soilc. Resist every
temptation to make it good; make it correct and small.
Scope
- The AST JSON schema — a spec artifact, written first. The exact
JSON encoding of the surface AST and of check results. This is the CLI
oracle contract that
soilc’s Trellis type definitions must later reproduce, so it follows the tr-grammar §7 value-encoding conventions (internally tagged sums, records) as if the AST were already Soil data. Deliverable:docs/contracts/soil0-cli.md, reviewed before code. - Lexer + parser. Hand-written recursive descent implementing
docs/soil-syntax-spec.mdexactly: theandbinding/connective disambiguation, non-associative comparisons, parenthesized non-tail match,..record patterns, the fixed escape set,decreaseslines. Errors carry source spans. - Renamer. Scope resolution, the no-shadowing rule,
::qualification,_privatevisibility. - Type + effect inference. ML inference (HM with records and sums from
registered type shapes — no row-polymorphic records needed), effect rows
as sets with row variables, subsumption at calls, operator-notation
elaboration (
==→T::eq, arithmetic → per-type primitives at monomorphic types only). No refinements (parsed, retained in the AST, otherwise ignored). No termination checking: every self-recursive orlet recdefinition conservatively acquiresdiv(consequence handled in plan 04) — recorded bycheckas an unverified-termination fact rather than a hard error, sototal-signed examples still pass (the demotion philosophy; soil0-cli §8.5, approved 2026-08-22). - Exhaustiveness + redundancy checking for matches.
- Interpreter. Strict tree-walk over
soil-rtvalues; closures; capability primitives implemented natively (fs_read_bytes, clock, rand, env, proc) plus the prelude’s fake capability constructors (fake_fs, seededfake_rand,fake_clock); arithmetic follows floor division/modulus and panics (asSoilError) on overflow and zero divisors — obligations don’t exist yet, so these are always-on runtime checks, matching debug-mode semantics. - CLI.
soil0 lex|parse|rename|infer|check|run|test, each reading source (or AST JSON) and emitting schema-conformant JSON on stdout, errors as structured JSON on stderr, nonzero exit.runtakes--entry name --args <json-array>and prints the canonical JSON result;testexecutes a JSON test bundle (assembled by the daemon) and reports per-case results in the lock’s result vocabulary.
Non-goals
Refinement checking, termination checking, codegen, optimization of any
kind, .tr parsing (daemon’s job, plan 03), Python FFI.
Testing
- Golden tests: a corpus of
.soilfragments → expected AST/type/effect JSON, including every syntax-spec static rule as a rejection test (one test per rule: shadowing, redundant arm,a < b < c, missing.., parameterlessletviolation, bad escape…). examples/read_file.soilandexamples/csvstats/median.soilmust parse, check, and (with stub callees) run.- Interpreter: expect-style tests mirroring the
.trexamples’ test blocks, run throughsoil0 test. - Fuzz the parser (cargo-fuzz or a simple generator) for panic-freedom.
Exit criteria
docs/contracts/soil0-cli.mdexists and every command conforms to it.- Both checked-in
.soilexamples check and run with correct results. - The rejection-test corpus covers every static rule in
docs/soil-syntax-spec.md§5. - A downstream consumer (plan 03) can drive lex→check→test entirely
through the CLI without linking
soil0as a library.
Decision points — resolved 2026-08-22
- Library + CLI: the daemon links
soil0as a crate for in-process checking, but the CLI is the frozen compatibility contract — oracle and conformance tests always go through the CLI, never the library API. - Spans: UTF-8 byte offsets are canonical (
start/end), with derivedline/colincluded alongside for display. - Type environment: commands take
--types env.json— type descriptors in the schema’s own encoding. Early tests hand-write it; the daemon generates it from.trfiles later.soil0never parses.tr.
Implementation plan
A detailed implementation guide exists at
impls/02-soil0-impl.md. It records a second
round of decisions resolved 2026-08-22 — union-find inference with naive
env-scan generalization, Spanned wrapper records in the AST JSON, a
signature-only frozen contract for soil0 infer (with a non-contractual
--dump-ast), and a machine-written program.json manifest referencing
env.json — plus the module map, build order, the required contents of
docs/contracts/soil0-cli.md, a set of micro-pins (its §8, approved 2026-08-22),
and three spec gaps it surfaced, all resolved 2026-08-22 (its
§9): constructor qualification Type::Ctor with the
exactly-one-spelling rule (now soil-syntax-spec §5.9, design §3.15),
the fake_fs example corrected to the canonical map encoding, and the
list primitives adopted as native-backed prelude surface (plan 04).
Step 1’s deliverable, docs/contracts/soil0-cli.md, was reviewed and
frozen as contract v1 on 2026-08-22 (amended v1.1 on 2026-08-23:
typed holes, print). All implementation steps (2–11) completed
2026-08-23: every contract command is live, both normative examples
check and run with correct results (median through a real insertion
sort; read_file through fake and real capabilities), the rejection
corpus is audited by an executable checklist, and the canonical printer
is byte-identical on the examples with parse → print a fixpoint. The
exit criteria are met; plan 03 is unblocked.