Files
AILang/docs/specs/0003-env-construction-unify.md
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

8.9 KiB

Env-construction unify — Design Spec

Date: 2026-05-10 Status: Draft — awaiting user spec review Authors: Brummel (orchestrator) + Claude

Goal

Eliminate the structural drift between check_in_workspace (crates/ailang-check/src/lib.rs, near line 1059) and mono::build_workspace_env (crates/ailang-check/src/mono.rs, near line 367) — two paths that construct the same Env shape and have repeatedly diverged under feature pressure.

The empirical motivation is three consecutive bug fixes patching the same drift class at three different fields:

Commit Field forgotten by mono path Iter
5c5180f env.types, env.ctor_index 22c
13b36cc env.globals post-22 debug
a9c685d env.imports post-22 audit

Each bug shipped as ail check passes / ail build fails with an env-table-miss in monomorphise_workspace. The pattern says a fourth field is one feature away.

Architecture

A single source-of-truth function

pub fn build_check_env(ws: &Workspace) -> Env

in crates/ailang-check/src/lib.rs populates every workspace-flat Env field — that is, every field whose contents do not depend on a current_module overlay. Both existing entry points become thin consumers of this helper:

  • check_in_workspace calls build_check_env(ws), then applies the per-module overlay for the module it is checking (current_module, globals from module_globals[m.name], imports from m.imports).
  • mono::build_workspace_env is a thin wrapper: pub fn build_workspace_env(ws: &Workspace) -> Env { build_check_env(ws) }. No additional seeding lives here. The per-module overlays in collect_mono_targets and collect_residuals_ordered (added in 13b36cc and a9c685d) are already correctly placed at the per-fn call sites — they are not duplicated by this milestone, they stay where they are.

The split mirrors the semantic distinction:

  • Workspace-flat fields are derivable from Workspace alone and do not change per call. They go in build_check_env.
  • Per-module / per-fn overlay fields depend on the call's current_module (or rigid vars). They stay at the call site.

After this refactor, when synth learns to read a new Env field, exactly one place needs the seeding edit.

Components

Files modified:

  • crates/ailang-check/src/lib.rs — adds pub fn build_check_env(ws: &Workspace) -> Env. Refactors check_in_workspace to call it and delete the now-duplicated inline seeding (effect_ops install, types, ctor_index, module_globals, module_types, class_methods, class_superclasses, workspace_registry, module_imports). check_in_workspace retains its per-module overlay logic (current_module, globals, imports for the module it is checking).
  • crates/ailang-check/src/mono.rsbuild_workspace_env becomes a one-line wrapper around build_check_env. The drift-risk comment ("If synth starts reading a new Env field, update both paths") is deleted — there is no longer a pair of paths to keep in sync.

Files added:

  • crates/ailang-check/tests/env_construction_pin.rs — a new integration test that pins the drift-shape invariant. See Testing strategy.

Fields seeded by build_check_env (workspace-flat):

  • effect_ops — via builtins::install
  • types — workspace-flat from every Def::Type
  • ctor_index — workspace-flat from every Def::Type's ctors
  • module_globals — per-module index built from build_module_globals(ws)
  • module_types — via build_module_types(ws)
  • module_imports — per-module index built from each Workspace::modules.values().imports
  • class_methods — workspace-merged from each module's ModuleGlobals::class_methods
  • class_superclasses — walk every Def::Class for superclass
  • workspace_registry — clone of ws.registry

Fields not seeded by build_check_env (per-call overlay, applied by callers):

  • current_module
  • globals
  • imports
  • rigid_vars

Data flow

ws: &Workspace
   │
   ▼
build_check_env(ws) ─► Env (workspace-flat seeded)
   │
   ├─► check_in_workspace(m: &Module):
   │     env.current_module = m.name
   │     seed env.globals from env.module_globals[m.name]
   │     build env.imports from m.imports
   │     run check on each Def in m
   │
   └─► mono::build_workspace_env(ws):
         return build_check_env(ws) verbatim
         (callers apply per-fn overlay)
   │
   ▼
collect_mono_targets / collect_residuals_ordered (per-fn):
   env.clone()
   env.current_module = module_name
   seed env.globals from env.module_globals[module_name]
   seed env.imports from env.module_imports[module_name]
   for v in fn rigids: env.rigid_vars.insert(v)
   run synth on body

One construction; two consumers; per-fn overlays at the per-fn call sites.

Error handling

No new error variants. The existing build_module_globals(ws).expect(...) call (used by build_workspace_env today) is a caller-contract panic that fires only if the mono pass is invoked on a workspace that has not passed typecheck — a programmer error, not a user error. That panic moves into build_check_env unchanged.

build_check_env has no fallible operations: every seeding step is a clone + insert into BTreeMap / IndexMap, all infallible.

User-facing diagnostics (UnknownIdent, UnknownModule, UnknownType, UnknownCtor) continue to fire at the same call sites in synth; the refactor changes the construction of Env, not its read paths.

Testing strategy

Existing regression suite (drift class): the three RED tests that pinned today's drift-class bugs run automatically and stay green:

  • crates/ail/tests/typeclass_22c.rs — covers env.types / env.ctor_index (5c5180f).
  • crates/ail/tests/mono_recursive_fn.rs — covers env.globals (13b36cc).
  • crates/ail/tests/mono_xmod_qualified_ref.rs — covers env.imports (a9c685d).

If any of these regress after the refactor, the unify is broken and must not ship.

New drift-shape pin test (crates/ailang-check/tests/env_construction_pin.rs): a single integration test that:

  1. Builds a representative class-bearing workspace fixture covering at least one Def::Class, one Def::Instance, one user ADT, and one cross-module import.
  2. Computes env_a = build_check_env(&ws) (the new helper).
  3. Computes env_b by running the inline construction the way check_in_workspace did pre-refactor — captured by reading the same code path and reproducing it inline in the test. (Practically: a small in-test helper that mirrors the pre-refactor seeding so the assertion has something to compare against.)
  4. Asserts that env_a and env_b agree on every workspace-flat field — same key-set, same per-key value (or, where types do not implement PartialEq, same key set).

The test serves as a tripwire: any future change that adds a new workspace-flat seed to one path but not the helper fails the test before it can ship a real bug.

Bench gates: bench/check.py, bench/compile_check.py, bench/cross_lang.py must each report 0 regressed. Performance of the refactor must be neutral — build_check_env is logically the same work as the pre-refactor inline.

Workspace test suite: cargo test --workspace must remain fully green (~344 tests).

Acceptance criteria

  1. pub fn build_check_env(ws: &Workspace) -> Env exists in crates/ailang-check/src/lib.rs.
  2. mono::build_workspace_env is a one-line wrapper: it calls build_check_env(ws) and returns the result. No inline seeding remains in this function.
  3. check_in_workspace calls build_check_env(ws) instead of inline workspace-flat seeding. Per-module overlay logic (current_module, globals, imports for the module being checked) remains.
  4. The drift-shape pin test (crates/ailang-check/tests/env_construction_pin.rs) is green.
  5. The three existing RED tests (typeclass_22c, mono_recursive_fn, mono_xmod_qualified_ref) remain green.
  6. cargo test --workspace is fully green.
  7. bench/check.py, bench/compile_check.py, bench/cross_lang.py each report 0 regressed.
  8. The // Drift risk: this fn and crate::check_in_workspaceboth populatecrate::Env. If synthstarts reading a newEnv field, update both paths. comment in mono.rs::build_workspace_env is deleted (it no longer applies).

Out of scope

  • Pipeline-topology changes (e.g. moving the mono pass inside check_workspace). The pass-decomposition invariant — mono is post-typecheck / pre-codegen — stays.
  • Refactoring the per-module overlay shape. The current_module / globals / imports / rigid_vars overlay is semantically correct as it is and stays at the call site.
  • Primitive-name-set consolidation (still queued from 22-tidy). Different drift class, different milestone.
  • New env fields. This milestone preserves the current shape; it only changes who constructs it.