On this page

Normative engineering contract

Language and conformance contract

The stable authority model for Shoal syntax and semantics, executable registries, error/render contracts, and the normative TOML corpus.

Status
Canonical replacement for docs/TDD.md
For
Language, evaluator, editor, and conformance contributors
On this page
  1. Authority order
  2. Stable ownership map
  3. Lexical contract
    1. Common lexical invariants
    2. Command-mode contract
    3. Expression-mode contract
  4. Statement dispatch contract
  5. Grammar and precedence contract
  6. Desugaring contract
  7. Value and condition contract
  8. Calls, flags, and type annotations
  9. Scope, modules, cwd, and environment
  10. Process-position contract
  11. Stream contract
  12. Builtin and method contract
  13. Canonical render contract
  14. Stable language error codes
  15. Normative conformance corpus
    1. Harness lifecycle
    2. Current corpus state
    3. Exhaustive suite ledger
      1. Core syntax, control flow, and diagnostics
      2. Values, operators, methods, and rendering
      3. Filesystem, command binding, adapters, and namespaces
      4. External I/O, shell blocks, and outcomes
      5. Reef
      6. Streams and sinks
    4. Two-runner risk
  16. Behavior-change protocol
  17. Historical TDD reconciliation
  18. Invariants reviewers should defend

This is the stable narrative contract for Shoal’s language implementation. It replaces the old monolithic docs/TDD.md as a link target while deliberately refusing to freeze stale Rust details in prose. Syntax, semantics, registries, and tests each have a more precise executable authority.

The guiding rule is:

Narrative states intent and invariants; source defines accepted structure; the conformance corpus pins observable language behavior.

Authority order🔗

When two artifacts disagree, resolve the dispute using this order and then repair the lower layers:

  1. A deliberately reviewed conformance case is the normative observable behavior for the case it covers.
  2. Parser/evaluator/value source plus focused tests describes current behavior where the corpus is silent.
  3. Executable registries and schemas define enumerated names and shapes: builtin names, method metadata, AST variants, adapter schema, protocol types, and error enums.
  4. This chapter and focused architecture chapters define cross-cutting intent, invariants, and ownership.
  5. Historical root docs, comments, examples, and roadmap prose are migration inputs, not competing sources of truth.

This does not mean an accidental test can permanently override design. A behavior change begins by deciding the intended result, updating or adding the corpus case, then changing implementation. It does mean prose alone cannot declare an unimplemented feature “done.”

Stable ownership map🔗

Contract surfaceExecutable authorityFocused narrative
AST node and span shapecrates/shoal-ast/src/ serde typesAST model
lexical tokens and parse statuscrates/shoal-syntax/src/lexer.rs, parser modulesParser and formatter
formattingshoal_syntax::format_program + idempotence testsParser and formatter
statement/expression evaluationcrates/shoal-eval/src/Evaluator state
calls/modules/namespacesevaluator call/module/namespace modulesCalls and modules
builtin command namesshoal_syntax::commands::builtin_names()Builtin registry
value variants and operatorsshoal-value/src/lib.rs, ops.rs, value_types.rsValue algebra
value method namesshoal-value/src/methods/suggest.rs metadata plus dispatch modulesMethod dispatch
canonical renderingshoal-value/src/render.rs + corpus expectationsValue algebra
streamsshoal-value/src/stream/, evaluator stream/channel modulesStreams and channels
external executionshoal-exec + evaluator command pathProcess execution
observable semanticsspec/cases/*.tomlthis chapter

Lexical contract🔗

Shoal source is UTF-8 and lexing is modal. The parser requests tokens in expression or command mode according to grammar position; the lexer does not inspect runtime bindings.

Common lexical invariants🔗

  • Newline and semicolon terminate statements unless the parser recognizes continuation.
  • A comment begins with # only where # starts a token; command word ver#2 remains one word.
  • Double-quoted strings support interpolation and escapes; single-quoted strings are raw; triple forms span lines.
  • Numeric maximal munch includes size and duration suffixes.
  • Tagged regex and datetime literals are lexically distinct values.
  • Illegal shell sigils and lone pipe syntax receive curated diagnostics rather than generic token errors.
  • Every successful token span must advance; lexer fuzz/property tests defend against non-progress.

The exact token enum and spelling rules live in shoal-syntax::lexer. A new token is incomplete until both modes, parser use, formatter round-trip, highlighter/completer behavior, and diagnostics have been audited.

Command-mode contract🔗

Command mode recognizes words, path-shaped words, glob-shaped words, flags, leading environment assignments, redirects, the end-of-flags marker, background suffix, and expression arguments. It is word-oriented: punctuation that would be an operator in expression mode can be literal command text.

Shape classification carries semantic consequences:

ShapeMeaning
~/…, ./…, ../…, /…path word
unquoted glob metacharactersglob value/expansion site
--name, --name=value, -abcflag token, interpreted through known signatures when available
leading NAME=valuescoped child environment prefix
(expr)evaluated expression as one command argument
>, >>, <capture/save or stdin redirection syntax
trailing &background/spawn sugar
trailing blockthunk/block argument

Unknown external commands retain raw string behavior where no adapter or closure signature exists. Known calls can bind named flags and coerce word values according to their signature.

Expression-mode contract🔗

Expression mode recognizes identifiers, literals, delimiters, operators, calls, field/method/index postfixes, collections, lambdas, and control expressions. Bare unquoted filesystem paths and globs are not general expression literals; explicit constructors or strings are required outside command positions.

Statement dispatch contract🔗

Statement dispatch is the language’s central ambiguity rule:

The parser accepts a ParseCtx classification because lexical scope changes whether an identifier head is a value expression or command. This is the one intentional parse-time environment seam. Script/module parsing without a populated context cannot reproduce every REPL binding-dependent decision; hosts must choose and test their context deliberately.

^head forces command interpretation. Dynamic execution uses the explicit runtime call surface. An identifier immediately followed by a method/field chain can use invoke-then-chain behavior where implemented. The highlighter and completer approximate this rule; the parser remains authoritative.

Grammar and precedence contract🔗

The recursive-descent/Pratt parser owns the accepted grammar. The stable conceptual precedence from tight to loose is:

postfix field/method/index/call
unary
multiplicative
additive
range
comparison/membership
logical and
logical or
null coalescing
postfix catch
assignment

Comparison chaining is rejected rather than interpreted Python-style. Logical operators short-circuit. Assignments require a mutable binding/lvalue. Control flow is represented explicitly in the AST rather than implemented through textual rewriting.

Do not copy a full EBNF into another narrative page. The parser modules, AST serde types, formatter, and conformance corpus evolve together; a second grammar inevitably drifts. The parser/formatter chapter supplies the detailed ambiguity map and change procedure.

Desugaring contract🔗

Surface sugar must lower to a stable AST meaning without text splicing:

SurfaceSemantic lowering
leading command environment assignmentsdynamically scoped environment around the call
trailing command &spawn/background block
redirectscaptured stdin/stdout save/append behavior
a trailing block after a callcallable block argument
implicit .field/.method argumentclosure over the implicit item
leading-dot continuation linepostfix continuation of prior expression
invoke-then-chainzero-argument invocation followed by value chain where dispatch permits
alias declarationstored partial command AST, never textual macro expansion
postfix catchexplicit try/catch evaluation form
optional field/method chainnull-preserving access semantics

The exact desugaring representation is owned by shoal-ast and shoal-syntax; semantic equivalence belongs in corpus cases. If formatting a parsed form emits a canonical spelling, its parse-format-parse AST must remain equivalent.

Value and condition contract🔗

The live Value enum, not an old prose list, is authoritative. Values include scalar quantities, paths/globs/regex, structured collections, errors/outcomes, streams/tasks/plans, closures/partials, secrets, lazy CAS-backed bytes, namespaces, and host snapshots.

Key invariants:

  • conditions accept booleans and command outcomes; arbitrary list/string/number truthiness is a type error;
  • data equality is structural for variants that define it, with special/opaque variants following explicit implementation rules;
  • integer and quantity arithmetic detects overflow and division by zero;
  • paths preserve OS-native bytes; strict string conversion can fail while display is lossy;
  • secrets render by name and do not become ordinary interpolated strings;
  • a stream is single-consumption and unbounded streams reject inherently terminal collection without an explicit bound;
  • an outcome preserves status versus signal, stdout/stderr, duration, pid, command, and success.

Detailed operator and render matrices live in Value algebra.

Calls, flags, and type annotations🔗

Evaluation is strict and left-to-right. Positional and named arguments bind in signature order; defaults are evaluated according to the closure/call implementation; variadic tails collect remaining arguments. Adapter signatures can translate long and short flags before argv assembly.

Current implementation does not provide complete runtime type soundness for user annotations:

  • expression-call paths do not uniformly enforce parameter annotations;
  • command word coercion converts recognized word forms, but already-typed non-string values can pass through;
  • declared return annotations are not enforced.

Annotations are therefore useful call metadata/coercion hints, not a proof of static or dynamic type safety. The canonical call chapter documents exact behavior. A future enforcement change is a language behavior change and needs corpus coverage for expression calls, command calls, defaults, variadics, and return values.

Scope, modules, cwd, and environment🔗

  • let creates immutable bindings; var creates mutable bindings.
  • Blocks and closures use lexical environments.
  • Modules load source files, evaluate them in a module environment, and expose exports through a module value.
  • Session cwd and environment are evaluator state; scoped with behavior must restore state on all exits.
  • Host-injected ports, configuration, Reef, policy, event bus, and journal are not ordinary lexical bindings and require deliberate inheritance in child evaluators.

The last point is a current correctness/security gap: several child-evaluator construction paths copy only part of host state. Narrative claims about structured concurrency or scoped authority do not override that source fact. See the evaluator and security chapters for the audited map.

Process-position contract🔗

An external command can be executed in captured mode or PTY mode. The high-level intent is:

  • an ordinary interactive statement can preserve terminal behavior through PTY tee;
  • value-producing, redirected, scripted, or non-TTY execution uses capture;
  • interact/TUI adapter classification can request terminal treatment;
  • cancellation targets the child’s process group and records signal death distinctly;
  • captured output is bounded in memory, optionally spilling to the journal CAS;
  • adapter ok_codes defines success; the default is exit code zero.

Source position alone is not the complete implementation key: host interactivity, redirection, adapter class, and explicit overrides participate. Consult the process and PTY chapters before changing this decision.

Stream contract🔗

Streams are pull-driven values with single-consumption state. Finite and unbounded sources are distinguished so terminal operations can reject unsafe collection. Operators compose lazily where implemented; bounded channels/tee paths must express backpressure or drop behavior explicitly.

Do not use the word “bounded” as a blanket claim. Some bridges use bounded synchronous channels, while the evaluator’s in-language event bus has unbounded live subscriber channels behind a bounded replay ring. The separate kernel EventBus uses bounded 256-event subscriber queues and coalesced gap summaries; the two buses must not be described as one identical backpressure implementation. The stream/channel chapter is the source-derived matrix.

Builtin and method contract🔗

Names are never pinned by prose lists. The canonical builtin command-head registry is shoal_syntax::commands::builtin_names(), consumed by parser/host tooling and evaluator dispatch. The discoverable value-method metadata lives in shoal-value/src/methods/suggest.rs; actual method behavior lives in the dispatch modules. They are intended to agree but currently have known drift, so neither may be treated as a generated perfect registry yet.

Adding a builtin or method requires:

  1. executable registry/metadata update;
  2. actual dispatch implementation;
  3. argument/error/effect behavior tests;
  4. a conformance case for stable user-visible behavior;
  5. completion/highlighter/LSP audit;
  6. external reference and focused internal ledger update.

Current registry/dispatch drift is documented rather than hidden. Do not “fix” prose to claim they match until tests prove it.

Canonical render contract🔗

The conformance harness compares the final value using render_inline, so its output is a protocol within the repository. Stable principles include:

  • null and booleans use lowercase tokens;
  • integers are decimal; floats follow the Rust display path used by the renderer;
  • inline strings are quoted/escaped; top-level block rendering may print raw string contents;
  • paths display lossily and quote when required by the renderer;
  • regex, size, duration, datetime, time, list, record, table, error, outcome, secret, stream, task, plan, and lazy-byte values have variant-specific stable forms;
  • record order follows the value representation rather than a prose promise of arbitrary map order.

Any render change can alter corpus expectations, shell output, prompt/picker display, journal value blobs, and test snapshots. Treat it as a compatibility change and audit every consumer.

Stable language error codes🔗

ErrorVal.code is program-visible through caught errors and is asserted by the corpus. Current source-emitted families include:

FamilyCodes
syntax/evaluationparse_error, type_error, arg_error, undefined_var, field_missing, index_range
execution/filesystemnot_found, cmd_failed, io_error, permission, utf8_error, no_matches, feed_error
numeric/controldiv_zero, overflow, recursion_limit, assert_failed
streams/eventsstream_consumed, stream_unbounded, channel_closed
network/generalnet_error, custom
Reefreef_unlocked, reef_drift, reef_conflict, reef_not_found, reef_provider
language runnersrunner_not_found

Some codes appear only in focused/live paths and not yet in corpus expectations. Conversely, historical lists named codes such as lang_block_unbalanced that current source may not emit as a distinct ErrorVal. The executable construction sites plus ReefCode enum are current authority.

Rules for codes:

  • code spelling is stable API; messages and hints may improve without forcing callers to parse text;
  • choose a specific existing code before falling back to custom;
  • caught error fields must preserve code, message, hint, stderr, status, and span where available;
  • new cross-subsystem codes need an executable enum/registry if possible, corpus coverage, and both internal/external documentation;
  • JSON-RPC integer error codes are a different layer and must not be conflated with ErrorVal.code.

Normative conformance corpus🔗

spec/cases/*.toml is the behavioral specification. At the 2026-07-16 audit it contains 77 suite files and 1,310 globally named cases. A case has this conceptual shape:

[[case]]
name = "globally-unique-behavior-name"
src = """
let x = 2 + 3
x * 2
"""

# exactly one expectation family:
value = "10"
# error = "type_error"
# error_contains = "optional teaching substring"
# parse_error = true
# parse_error_contains = "optional parse teaching substring"

fixture = ["a.txt", "sub/b.log"]
stdin = "reserved / harness-dependent"
skip = "specific host-dependent reason"

Harness lifecycle🔗

sequenceDiagram
accTitle: Harness lifecycle
accDescr: Shows the components and relationships described in Harness lifecycle.
  participant H as conformance harness
  participant T as fresh temp directory
  participant P as shoal-syntax
  participant E as fresh Evaluator
  participant R as render_inline

  H->>T: create fixture paths
  H->>P: parse source in script context
  alt parse expectation
    P-->>H: diagnostic and span
  else evaluation expectation
    P-->>E: Program
    E-->>H: final Value or ErrorVal
    H->>R: render final Value
    R-->>H: canonical text
  end
  H->>H: compare and collect all failures

Each case uses a fresh evaluator and cwd, with no shared journal. Multi-statement source compares the last value. Fixtures create empty files and parent directories. Expectations should avoid ambient PATH, locale, current time, network, user config, or OS-specific rendering unless the case is explicitly skipped with a reason.

Current corpus state🔗

The live audit result was 1,306 passed, 0 failed, and 4 skipped. The skips cover a native-thread recursion-stack condition, a Node block, a jq feed composition, and full-chain Reef which. Counts are evidence from that run, not a permanently hardcoded health claim; release notes must run the corpus again.

Exhaustive suite ledger🔗

Every suite is named below so a language area cannot disappear behind an aggregate count. Counts come from [[case]] records in the current tree and sum to 1,310. This table should eventually be generated and checked in CI; until then, adding, renaming, or splitting a suite requires updating it.

Core syntax, control flow, and diagnostics🔗

SuiteCasesBehavioral family
assert.toml6assertion success, messages, and stable failures
box-era-diagnostics.toml10pinned historical parser/evaluator defect diagnostics
catch-forms.toml12try/postfix catch, bindings, and caught error fields
closures-more.toml4additional closure capture/call behavior
closures.toml8closure definition, capture, invocation, and defaults
core-more.toml8incremental core-language edge behavior
core.toml63declarations, expressions, control flow, functions, collections, errors
desugar-more.toml5additional surface-sugar equivalences
desugar.toml13primary background/env/redirect/implicit/catch desugaring
edges.toml18boundary cases and stable error behavior
fn-param-binding-more.toml7function parameter/default/named binding extensions
iife.toml6immediately invoked function/lambda forms
lambda-and-record-strict.toml17lambda parsing and strict record access
match-guard-lambda.toml6guarded arms and lambda interaction
match-more.toml15extended pattern and arm behavior
match-type-patterns-2.toml16typed patterns and follow-up boundaries
match.toml17literals, bindings, alternation, list/record patterns
pipe-and-repl-only.toml7curated pipe rejection and surface restrictions
teaching-diagnostics-2.toml9follow-up curated parse/eval messages
teaching-diagnostics.toml7primary user-facing diagnostic guidance

Values, operators, methods, and rendering🔗

SuiteCasesBehavioral family
bytes-value.toml16bytes conversion, rendering, UTF-8, comparison/feed boundaries
coercion-cells-3.toml12nested/cell coercion strictness
coercion-more.toml6additional word/value coercions
coercion.toml62numeric, quantity, boolean, datetime, and invalid coercion matrix
collections.toml38list/record/table construction, access, transforms
datetime-fields.toml12datetime field projection and missing-field behavior
datetime-methods.toml9datetime method operations
datetime-more.toml9additional datetime arithmetic/parsing edges
datetime-relative.toml10relative time anchors and duration composition
error-codes-2.toml3follow-up stable error code cases
error-codes.toml11representative stable runtime error taxonomy
field-method-fallback.toml16field lookup versus zero-arg method fallback
list-methods-2.toml35extended list transformations and aggregates
list-methods-3.toml14later list-method coverage
list-record-error-boundaries.toml14strict heterogeneous/list-record failure boundaries
literals.toml48scalar, string, regex, size, duration, time, and collection literals
method-coercion-more.toml44method argument coercion and type-specific dispatch
method-errors.toml30arity, type, and missing-method diagnostics
misc-composition.toml10cross-value composition behavior
numbers-more.toml38overflow, bases, floats, quantities, and numeric edges
numbers.toml11primary numeric operations and failures
operators-3.toml4third-wave operator edge cases
operators-more.toml10extended precedence/type behavior
operators-safenav-more.toml3optional/safe navigation additions
operators.toml29arithmetic, comparison, logic, coalescing, membership
ranges.toml15inclusive/exclusive ranges and iteration/type errors
record-table-methods-2.toml21record/table transforms and aggregates
record-table-more.toml13additional record/table structure behavior
size-duration-more.toml15quantity arithmetic, overflow, and type boundaries
size-duration-render-more.toml8canonical size/duration presentation
strings-methods-2.toml45extended Unicode/string/regex transformations
strings.toml31primary string indexing, splitting, matching, replacement

Filesystem, command binding, adapters, and namespaces🔗

SuiteCasesBehavioral family
adapters-pack.toml18bundled adapter binding, parsing, and success codes
dir-stack.toml14pushd/popd/dirs state and errors
fs-builtins-more.toml7later filesystem builtin cases
fs-builtins.toml39path operations, globbing, mutation, metadata, failures
glob-more.toml3additional glob matching/empty behavior
list-path-glob-binding.toml4command parameter accumulation and glob/path binding
namespace-roundtrips-2.toml8serialization namespace round trips
namespaces-more.toml22additional structured namespace operations
namespaces.toml77JSON/YAML/TOML/CSV/math/config namespace breadth
os-namespace.toml11environment/OS namespace reads and errors
path-field-accessors.toml14pure and filesystem-backed path fields
path-fs-methods.toml44path read/write/metadata/conversion methods
word-binding-2.toml20command words, flags, typed positions, and invalid binds

External I/O, shell blocks, and outcomes🔗

SuiteCasesBehavioral family
io-feed-more.toml9additional stdin/feed composition and failures
io-sh-more.toml4additional language-block/verbatim shell behavior
io.toml14feed, shell blocks, runners, stdin/output boundaries
outcome-more.toml4additional outcome fields/composition
outcome.toml15status/signal/success/output and condition behavior

Reef🔗

SuiteCasesBehavioral family
reef-provider-errors.toml6provider error mapping and argument validation
reef.toml25scope/lock/runner/builtin integration and stable failures

Streams and sinks🔗

SuiteCasesBehavioral family
stream-sinks-more.toml8save/feed/terminal sink extensions
streams-3.toml2third-wave stream regressions
streams-backpressure.toml6boundedness, timeout, and pressure behavior
streams-more.toml7additional transformations and consumption rules
streams.toml33source/operator/sink baseline, single consumption, unbounded errors

Two-runner risk🔗

There are parallel harness entrypoints in shoal and shoal-eval. They should execute the same schema and semantics, but duplicated parsing/fixture/error-comparison logic can drift. The long-term contract should live in one shared test-support implementation invoked from both packages.

Behavior-change protocol🔗

For any observable language change:

  1. state whether the change is bug fix, specification correction, or compatibility break;
  2. add or revise the smallest corpus cases that distinguish old and new behavior;
  3. add focused unit/property/live tests for the implementation invariant;
  4. change source and registries;
  5. run formatter idempotence and conformance through both harnesses;
  6. audit completion, highlighting, LSP, adapters, wire rendering, and docs as applicable;
  7. record migration advice when existing scripts can break;
  8. update implementation status—do not leave completed roadmap prose as the only evidence.

Historical TDD reconciliation🔗

The old TDD mixed durable choices, stale lists, implemented behavior, and aspirations. Its valuable content is absorbed as follows:

Former sectionCanonical destinationReconciliation note
naming and product thesisexternal overview/visionhistorical rationale, not runtime contract
kernel optionality and hostingSystem map, shell/kernel chapterslocal interactive shell is embedded today; “interactive always attaches” was aspirational
lexical structure, grammar, dispatch, desugaringthis page + parser/AST chaptersexecutable parser replaces copied EBNF
value semantics/coercion/callsvalue/call chaptersannotation enforcement gaps are now explicit
PTY/process positionprocess and PTY chaptersexact host decision replaces broad position slogan
builtin surfacebuiltin registry chapterold handwritten name list was stale
adapter schemaadapter runtime chapterexecutable serde schema is authority
wire protocolkernel/protocol referencecurrent method/type registry replaces proposal table
effects and Leasheffects/security chaptersseparates derivation, authorization, and actual OS enforcement
journal/CASpersistence/storage referenceactual schema/versioning/path semantics replace early sketch
implementation plan/open itemsimplementation status and roadmapevidence-ranked rather than milestone fiction
testing/benchmarkstooling/quality chaptercurrent corpus counts and reviewed-not-asserted budgets
edge-case registerfocused language/execution chapters + corpusonly tested rulings remain normative

This page is deletion-ready with respect to docs/TDD.md: no source comment should continue to use an ignored or root Markdown document as its normative semantic link.

Invariants reviewers should defend🔗

  • Parser mode is driven by grammar position, not runtime token guessing.
  • Binding-aware statement dispatch enters through an explicit parse context.
  • Aliases and other sugar manipulate AST/value structures, never splice shell text.
  • Only boolean/outcome conditions are accepted.
  • Error codes and canonical render forms are program-visible compatibility surfaces.
  • Streams cannot be silently consumed twice or infinitely collected.
  • OS effects are represented at a host/port boundary even where current code has known leaks.
  • Enumerated names come from executable registries.
  • A feature with renderer/schema support but no host producer is labeled scaffolded.
  • The corpus decides covered observable behavior, and a behavior change updates the corpus first.
Type to search every guide navigate open esc close
Diagram