diff --git a/docs/agents/compiler-review-openspec-handoff.md b/docs/agents/compiler-review-openspec-handoff.md new file mode 100644 index 00000000..55386809 --- /dev/null +++ b/docs/agents/compiler-review-openspec-handoff.md @@ -0,0 +1,115 @@ +# Compiler Deep-Review — OpenSpec Orchestration Handoff + +Handed off: 2026-08-21 · Branch: **julia/compiler-review-proposals** (base **main** @ 739150a) + +## 1. What this is + +A deep review of **packages/compiler** (twelve parallel subagent review reports, consolidated) +produced twelve remediation changes. All twelve are staged here as planning-complete OpenSpec +changes (proposal + design + tasks, plus spec deltas for the two behavior-changing ones). All +twelve pass **openspec validate**. No compiler source code has been implemented. You are +orchestrating the implementation across multiple worker sessions. + +Out of scope: **packages/llvm** (owned by a different agent — do not touch it) and the +uncommitted .claude/ and packages/llvm/** working-tree edits that belong to that other agent. Do +not git-add, commit, or modify those. + +## 2. The twelve changes + +| # | Change (dir under openspec/changes/) | Kind | Spec delta home | Primary files touched | +| - | --- | --- | --- | --- | +| 1 | remove-compiler-dead-code | refactor (skip_specs) | — | Analysis, Type, Mir, Hir, SuspensionMir, DeclarationIndex, ModuleTooling, SyntaxCorrespondence/ProjectAnalysis, CallableFieldRealization, OpaqueRealization | +| 2 | extract-compiler-shared-helpers | refactor (skip_specs) | — | ModuleClosure, DeclarationIndex, OpaqueRealization, ModuleSummary, ImportPlan, NameResolution, Layout, Backend, WasmBackend, Lexer, LiteralForm, StaticText, FloatingPoint, Transcendental, Type, Presentation, PhaseReport, Pipeline, Driver | +| 3 | reconcile-compiler-duplicated-semantics | behavior | bootstrap-complete-interface-contracts | Type, TypeCompatibility, InterfaceWitnessCompatibility, Ownership, SuspensionOwnership, Lower, Token | +| 4 | harden-compiler-native-boundary | behavior | bootstrap-native-toolchain, bootstrap-backend | NativeToolchain, Driver, Target, Backend, WasmBackend, BootstrapEvaluation | +| 5 | split-compiler-frontend | refactor (skip_specs) | — | Parser, Lexer | +| 6 | split-compiler-type-system | refactor (skip_specs) | — | Type, Instances, Pipeline | +| 7 | split-compiler-elaboration | refactor (skip_specs) | — | Elaboration | +| 8 | split-compiler-mid-end | refactor (skip_specs) | — | Lower, Ownership, SuspensionOwnership | +| 9 | split-compiler-layout | refactor (skip_specs) | — | Layout, OpaqueRealization, CallableFieldRealization, Scalar, Match | +| 10 | split-compiler-ir-evaluation | refactor (skip_specs) | — | Mir, ProvisionalMir, SuspensionMir, BootstrapEvaluation | +| 11 | split-compiler-declaration-index | refactor (skip_specs) | — | DeclarationIndex | +| 12 | split-compiler-backends | refactor (skip_specs) | — | Backend, WasmBackend | + +skip_specs: true means the change is a pure refactor with no observable behavior change; its +.openspec.yaml already carries the marker, so openspec validate accepts it with zero spec deltas. + +## 3. Dependency graph (waves) + +Edges are hard serialization because two changes edit the same file. Everything not listed is +parallelizable. + +Wave A — foundation (run first; 1 and 2 in parallel): + +- remove-compiler-dead-code — deletes symbols that later changes rename or relocate. +- extract-compiler-shared-helpers — creates the internal helpers every split imports. + +Wave B — semantic + boundary (3 and 4 in parallel, both after Wave A): + +- reconcile-compiler-duplicated-semantics — after 1 + 2 (edits Type.ts, Ownership, Lower). +- harden-compiler-native-boundary — after 1 + 2 (edits Backend, WasmBackend, BootstrapEvaluation). + +Wave C — the eight splits (parallelize, honoring these edges): + +| Change | Blocks on | +| --- | --- | +| split-compiler-elaboration | — (only touches Elaboration.ts; can start in Wave B) | +| split-compiler-frontend | 2 (Lexer byte-classification move) | +| split-compiler-type-system | 2 + 3 (Type.ts) and 4 (Pipeline/Driver) | +| split-compiler-mid-end | 3 (Ownership/SuspensionOwnership/Lower) | +| split-compiler-layout | 1 + 2 (OpaqueRealization/CallableFieldRealization/Layout) | +| split-compiler-ir-evaluation | 1 + 4 (Mir, BootstrapEvaluation) | +| split-compiler-declaration-index | 1 + 2 (DeclarationIndex) | +| split-compiler-backends | 2 + 4 (Backend/WasmBackend) | + +Recommended schedule: A → B (+ elaboration in parallel any time) → C in two mini-batches of four, +picking membership so no two mini-batch members touch the same file. + +## 4. Ground rules (non-negotiable, from AGENTS.md) + +- Green-field: no compatibility contract. Implement the clean target design, delete superseded + code, do not keep shims/aliases/fallbacks. A change is incomplete while an obsolete path remains. +- Verification order for every change: pnpm typecheck → pnpm exec biome check . → pnpm test. + Run pnpm check as the final gate before handing a change back. +- Do not touch packages/llvm or the other agent's uncommitted working-tree edits. +- Effect conventions: one module per actor, data-first + dual, Effect.fn / Effect.fnUntraced, wrap + external APIs in Effect (no bare throw or Promise across a boundary), @effect/vitest tests. +- No non-null assertions, no as-casts except truths TypeScript cannot express, no lint suppressions. +- Tests stay cheap: prove semantics with Analysis.evaluate; add corpus programs to + test/support/corpus.ts rather than per-feature native-parity tests; assert diagnostic codes and + spans, never message text; no per-feature fresh-process determinism tests. +- If a proposal needs revision during implementation, use the openspec-update-change skill, never + hand-edit the artifacts to force validation. + +## 5. Orchestration protocol + +1. For each change you dispatch, create a branch agent/ from + julia/compiler-review-proposals (or from the current integration tip). +2. Give the worker exactly one change, the worker prompt below, and the gating rules in section 4. +3. Worker applies via the openspec-apply-change skill (tasks.md drives the work; applyRequires is + already satisfied since tasks are present). +4. Worker returns: branch name, pnpm check result, a list of every diagnostic-code/span/golden + change with justification, and any unfinished task. +5. You review the diff scope (the change must not wander outside its listed files), merge the + branch back into the integration branch, re-run pnpm check once, then dispatch the next wave. +6. On a conflict you cannot resolve silently (two changes both need an overlapping hunk), pull the + later change into the same session rather than editing two proposals in parallel. +7. After a change is fully implemented and verified, archive it with openspec archive (or + the openspec-archive-change skill) to promote its spec deltas into openspec/specs/. + +## 6. Worker prompt template + +Load the openspec-apply-change skill, then implement the OpenSpec change openspec/changes/ +exactly as specified — proposal/design/tasks are authoritative. Follow AGENTS.md green-field and +Effect conventions. Touch only the files the proposal lists (plus the callers/tests/docs that must +move with them). Verify in order: pnpm typecheck, pnpm exec biome check ., pnpm test, then pnpm +check. Report the final check output, every intentional diagnostic-code/span/golden change, and any +task you could not complete. Do not touch packages/llvm, do not edit the change's own planning +artifacts, and do not modify unrelated working-tree state. + +## 7. Done criteria + +- All twelve changes applied, merged to one integration branch, and archived (spec deltas synced). +- pnpm check green on the integration branch at the end. +- Each refactor is byte-determinism clean (golden byte comparisons unchanged except where a + behavior change — items 3 and 4 — intentionally alters them and the delta is documented). \ No newline at end of file diff --git a/openspec/changes/extract-compiler-shared-helpers/.openspec.yaml b/openspec/changes/extract-compiler-shared-helpers/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/extract-compiler-shared-helpers/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/extract-compiler-shared-helpers/design.md b/openspec/changes/extract-compiler-shared-helpers/design.md new file mode 100644 index 00000000..57ded9e8 --- /dev/null +++ b/openspec/changes/extract-compiler-shared-helpers/design.md @@ -0,0 +1,23 @@ +## Context + +See proposal.md. Each helper is extracted by taking the most general existing implementation, moving it to a single owner, and routing every other call site through it. + +## Decisions + +- **`internal/Graph.stronglyConnected(nodes, neighbors)`**: parameterized by key/neighbor projection so the module-name cycle (`ModuleClosure`), inline struct-dependency cycle (`DeclarationIndex`), and opaque-realization cycle all call the same fixed point. Preserve each caller's existing deterministic ordering (sorted canonical roots); keep the per-caller sort in the caller. +- **`ImportPath.spelling(path)` / `canonicalTarget(path)`**: consolidate the "filter segments, require available syntax, join" step; the `/` source-spelling vs `.` canonical form is decided once. +- **`alignUp`**: export from one low-level module (e.g. `internal/Align.ts`) and import into `Backend.ts:1101`, `WasmBackend.ts:228`, `CoroutineFrame.ts:6`. +- **`internal/ByteClass.ts`**: move the classifiers from `Lexer`/`LiteralForm` and `hexValue`/`digitValue` from `StaticText`/`IntegerLiteral`. +- **`internal/Escape.ts`**: single `escapeExtent` (what `scalarCount` computes) derived from the same escape list `StaticText.decode` consumes. +- **`FloatingPoint.canonicalNaN(width)`**: returns `0x7fc00000n`/`0x7ff8000000000000n`; `fromNumber`, `squareRoot`, and the transcendental `Plan` records reference it. +- **`PhaseReport.measure`**: one `measure` accepting optional `{ counter, heapBytes }`; `Pipeline.measured`/`measuredModuleWork` and `Driver.phase`/`phaseWithHeap` and the three inline reports all call it. Heap source injected once, not read from `node:process` inline. +- **`suspensionPointKey`**: export from `Backend.ts` and delete the copy at `WasmBackend.ts:1596`. + +## Risks / Trade-offs + +- [Determinism] → each extraction preserves the exact existing key/ordering; golden byte comparisons guard regressions. +- [Ordering subtlety] → Tarjan component sort order stays per-caller. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test` (golden/determinism suites are the regression net). diff --git a/openspec/changes/extract-compiler-shared-helpers/proposal.md b/openspec/changes/extract-compiler-shared-helpers/proposal.md new file mode 100644 index 00000000..2d85b312 --- /dev/null +++ b/openspec/changes/extract-compiler-shared-helpers/proposal.md @@ -0,0 +1,29 @@ +## Why + +The same low-level algorithm is implemented two to four times across unrelated files, each copy a drift hazard. This change extracts one owner per helper so a fix or determinism change is made exactly once. + +## What Changes + +- **One Tarjan SCC helper** (`internal/Graph.ts`) replaces `ModuleClosure.cycleFacts`, `DeclarationIndex.stronglyConnected`, and the `OpaqueRealization` copy. +- **One import-path extractor** (`ImportPath.spelling`/`canonicalTarget`) replaces the four hand-rolled `/` vs `.` joins in `ModuleSummary`, `ImportPlan`, `ModuleClosure`, `NameResolution`. +- **One `alignUp`** shared by `Layout`, `Backend`, `WasmBackend`, and `CoroutineFrame`. +- **One ASCII byte-classification module** (`internal/ByteClass.ts`): `isAsciiLetter`/`isDecimalDigit`/`isIdentifierStart`/`isIdentifierContinue`/`hexValue`, used by `Lexer`, `LiteralForm`, `StaticText`, `IntegerLiteral`. +- **One escape-vocabulary module** (`internal/Escape.ts`) owning "which escapes extend a literal" and "what each escape means", closing the `scalarCount`↔`decode` sync risk. +- **One canonical-NaN constant** (`FloatingPoint.canonicalNaN(width)`) replacing the four hardcoded bit patterns in `FloatingPoint` and `Transcendental`. +- **One requirement-member renderer** (`Type.encodeRequirement`) consumed by `Presentation`, deleting five inline copies. +- **One phase-measurement path** (`PhaseReport.measure`) replacing the four re-implementations in `Pipeline` and `Driver`. +- **One `suspensionPointKey`** exported from `Backend` and imported by `WasmBackend`. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure dedup; no observable behavior change. Touches `ModuleClosure`, `DeclarationIndex`, `OpaqueRealization`, `ModuleSummary`, `ImportPlan`, `NameResolution`, `Layout`, `Backend`, `WasmBackend`, `CoroutineFrame`, `Lexer`, `LiteralForm`, `StaticText`, `IntegerLiteral`, `FloatingPoint`, `Transcendental`, `Type`, `Presentation`, `PhaseReport`, `Pipeline`, `Driver`. `skip_specs: true`. diff --git a/openspec/changes/extract-compiler-shared-helpers/tasks.md b/openspec/changes/extract-compiler-shared-helpers/tasks.md new file mode 100644 index 00000000..051f1fe6 --- /dev/null +++ b/openspec/changes/extract-compiler-shared-helpers/tasks.md @@ -0,0 +1,23 @@ +## 1. Graph and import helpers + +- [ ] 1.1 Add `internal/Graph.stronglyConnected` and route `ModuleClosure`, `DeclarationIndex`, `OpaqueRealization` through it +- [ ] 1.2 Add `ImportPath.spelling`/`canonicalTarget` and replace the four inline extractors + +## 2. Numeric and byte helpers + +- [ ] 2.1 Extract one `alignUp` and import it in `Layout`, `Backend`, `WasmBackend`, `CoroutineFrame` +- [ ] 2.2 Add `internal/ByteClass.ts` and route `Lexer`, `LiteralForm`, `StaticText`, `IntegerLiteral` through it +- [ ] 2.3 Add `internal/Escape.ts` and unify `scalarCount`/escape decoding +- [ ] 2.4 Add `FloatingPoint.canonicalNaN` and replace the four literals + +## 3. Rendering, measurement, ABI + +- [ ] 3.1 Route `Presentation` through `Type.encodeRequirement`; delete the five inline copies +- [ ] 3.2 Unify phase measurement on `PhaseReport.measure`; delete `Pipeline`/`Driver` wrappers and inline reports +- [ ] 3.3 Export `suspensionPointKey` from `Backend` and delete the `WasmBackend` copy + +## 4. Verification + +- [ ] 4.1 Run `pnpm typecheck` and verify clean +- [ ] 4.2 Run `pnpm exec biome check .` and verify clean +- [ ] 4.3 Run `pnpm test` and verify golden/determinism suites pass diff --git a/openspec/changes/harden-compiler-native-boundary/.openspec.yaml b/openspec/changes/harden-compiler-native-boundary/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/harden-compiler-native-boundary/design.md b/openspec/changes/harden-compiler-native-boundary/design.md new file mode 100644 index 00000000..52f4a7fd --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/design.md @@ -0,0 +1,49 @@ +## Context + +See proposal.md. The boundary sits between `NativeToolchain.ts` (Node, deep import), `Driver.ts` / `Pipeline.ts` (Effect orchestration), `Target.ts` (pure data), `Backend.ts` / `WasmBackend.ts` (Effect backends), and `BootstrapEvaluation.ts` (synchronous leaf). + +## Goals / Non-Goals + +**Goals:** one owner per external dependency; expected failures in the typed error channel; temp-dir/scope cleanup via `acquireRelease`; a genuinely browser-safe root barrel. + +**Non-Goals:** changing build-plan semantics; renaming public subpaths; god-module splits (separate changes). + +## Decisions + +### NativeToolchain service boundary + +Split `NativeToolchain` into a pure planning surface (existing `ToolchainPlan`) and an effectful executor. Add a `ToolchainError extends Data.TaggedError` with `operation`, `message`, and a `reason` union (`SpawnFailed`, `StorageFailed`, `LinkFailed`) where the wrapped branch carries `cause`. `withBuildScope` becomes `Effect.acquireRelease(mkdtemp, rmSync)`. `runPlanned`/`writeArtifact`/`commit`/`commitCachedArtifact`/`ClangLinker.link` all `yield*` typed failures. `Driver.compile` `yield*`s them instead of calling synchronously. + +### One atomic-commit seam + +Add `atomicCommit(destination, bytes, { mode? })`: mkdir, write temp sibling (`${path}.silk-tmp-${pid}`), `renameSync`, and `rmSync` the temp in a finally-like bracket on any failure. Route `makeDiskArtifactCache.set` (104), `commitCachedArtifact` (192), `commit` (322), and `ClangLinker.link` (458) through it. This fixes the leak where `makeDiskArtifactCache.set`'s catch did not remove its temp. + +### Host detection leaves Target + +Move `platform()`/`arch()` into `NativeToolchain` (or a small `HostTarget.ts` next to it) and have `Target.fromHost` accept the values instead of reading `node:os`. `Target.ts` becomes node-free. Collapse `Target.triple` into `Target.id` (they are always equal). + +### Driver artifact hand-off + +Have `finalizeWasm` / `ClangLinker.link` return `{ path, bytes }` so `Driver` seeds the artifact cache without `readFileSync`. Provide `memoryUsage` through an injected `HeapObservation` service (default Node impl at the app edge) instead of importing `node:process` in `Driver`. + +### Backend error channels + +`WasmBackend.ts:6133` returns `yield* new BackendError({ reason: { _tag: 'InvalidMir', ... } })` instead of throwing. `Backend.emitProgram` becomes `Effect.fnUntraced` pinned to `LlvmError | BackendError`; `LlvmBackend.emit` wraps via `patchErrors`/`catchTag` on `LlvmError` rather than `cause._tag === 'BackendError'` probing. `locate` takes the concrete instruction type (the return of `Value.instruction`) and drops the `unknown` cast. + +### BootstrapEvaluation host catches + +`HostWrite` (`BootstrapEvaluation.ts:2912`) and the `OsCall` arm (`:3342`) keep the typed-result path for expected failures and reserve the catch for unexpected throws, mapping them with `cause` preserved rather than a bare `'Other'`. + +### Remove NativeLinker + +Delete the `NativeLinker` interface; `Driver` calls the `ClangLinker` implementation directly (single implementation today). + +## Risks / Trade-offs + +- [Error channel change is observable] → new spec scenarios pin the typed-failure behavior; test replacements verify `cause`/stage. +- [Browser-safe barrel] → a bundle smoke check asserts no `node:` module in the root import graph. +- [Atomic commit behavior] → the determinism/durable-cache tests plus a new "failed rename leaves no temp" case. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test`; new `@effect/vitest` cases for spawn/storage/link typed failures, temp cleanup, and the wasm invalid-request `BackendError`. diff --git a/openspec/changes/harden-compiler-native-boundary/proposal.md b/openspec/changes/harden-compiler-native-boundary/proposal.md new file mode 100644 index 00000000..d6e102fa --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/proposal.md @@ -0,0 +1,28 @@ +## Why + +The compiler's native-toolchain boundary violates the effect-patterns boundary rule. `NativeToolchain.ts` is a fully synchronous imperative core (raw `node:child_process`/`node:crypto`/`node:fs`/`node:os`/`node:path` with scattered try/catch and a manual try/finally in `withBuildScope`) that returns data-union failures, then gets driven synchronously inside `Driver.compile`'s `Effect.gen`. A spawn/fs throw therefore escapes the generator as a Defect rather than a typed error, and the same fs failure lands in two different channels depending on which line threw. `node:*` ownership is split three ways, `Target.ts` imports `node:os` and is re-exported from the barrel the design claims is browser-safe, `Driver.ts` re-reads artifacts from disk with raw `readFileSync`, and `WasmBackend` throws a `RangeError` inside an `Effect.gen` instead of yielding a typed `BackendError`. + +## What Changes + +- **`NativeToolchain` becomes a thin Effect boundary**: `Effect.try` around spawn/fs, `Effect.acquireRelease` for build scopes and temp dirs, a `ToolchainError` family in the error channel; `ToolchainPlan` stays pure. +- **One `atomicCommit` seam** (guaranteed cleanup) replaces four divergent copy-paste copies and fixes the stale-temp leak in `makeDiskArtifactCache.set`. +- **`node:os` host detection moves out of `Target`** into a Node-only boundary, so importing the package root no longer pulls node built-ins; the redundant `Target.triple` field is collapsed. +- **`Driver` stops re-reading artifacts** with `readFileSync` (finalizers return bytes); `node:fs`/memory telemetry become single-owner. +- **`WasmBackend`** routes request-validation failures to `BackendError` instead of throwing; **`Backend.emitProgram`** pins its error channel with `catchTag` rather than `_tag`-string probing and becomes `Effect.fnUntraced` with a concrete debug-location type. +- **`BootstrapEvaluation`** host-provider catches preserve the cause instead of flattening to `'Other'`. +- **Remove the never-injected `NativeLinker` interface** (single implementation, never provided). + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + +- `bootstrap-native-toolchain`: typed failure channel, atomic artifact/cache commit with cleanup, browser-safe barrel, in-memory artifact hand-off. +- `bootstrap-backend`: expected request-validation failures yield typed `BackendError`, never defects. + +## Impact + +Observable: native-toolchain failures now surface as typed errors (callers can recover on stage and cause), and invalid backend requests no longer bypass `BackendError`. Everything else is internal refactoring. Node-only code stays a deep import; `@silk-effect/compiler` root becomes genuinely browser-safe. diff --git a/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-backend/spec.md b/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-backend/spec.md new file mode 100644 index 00000000..91abe23d --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-backend/spec.md @@ -0,0 +1,10 @@ +## ADDED Requirements + +### Requirement: Expected request-validation failures yield BackendError + +A backend SHALL model every expected caller-caused failure (invalid MIR, invalid module, invalid target, invalid request parameters) in its typed `BackendError` channel. It SHALL NOT throw inside an Effect generator for an expected failure. + +#### Scenario: An invalid private stack page bound is a typed failure + +- **WHEN** a wasm emit request specifies an invalid `privateExecutionStackPages` bound +- **THEN** the backend yields a `BackendError`, never a thrown `RangeError` defect, and error-channel mapping observes it diff --git a/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-native-toolchain/spec.md b/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-native-toolchain/spec.md new file mode 100644 index 00000000..85fca2c3 --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/specs/bootstrap-native-toolchain/spec.md @@ -0,0 +1,42 @@ +## ADDED Requirements + +### Requirement: Native toolchain failures yield typed errors + +Every expected native-toolchain failure (spawn, write, rename, temp-dir creation) SHALL surface in the typed error channel with operation and stage provenance and a preserved cause. An fs or subprocess failure SHALL NOT escape an Effect generator as a defect. + +#### Scenario: A spawn failure is a typed failure + +- **WHEN** the pinned Clang exits with a non-zero status or cannot be spawned +- **THEN** the operation yields a typed toolchain failure carrying the command and stage, not a thrown error + +#### Scenario: A storage failure carries the failing stage + +- **WHEN** an intermediate write fails +- **THEN** the yielded failure names the stage that failed and preserves the underlying cause + +### Requirement: Artifact and cache commits are atomic and always clean up + +Every durable commit SHALL stage to a temporary sibling and rename atomically, and SHALL remove the temporary on any failure so no stale temporary file survives. + +#### Scenario: A failed rename leaves no temp sibling + +- **WHEN** a staged write is followed by a failing rename +- **THEN** the temporary file is removed and the destination is left untouched + +### Requirement: The compiler root barrel stays browser-safe + +Importing the compiler package root SHALL NOT pull Node built-ins. Host target detection (platform/arch) SHALL be resolved through a Node-only boundary reached by deep import, not from modules re-exported by the root barrel. + +#### Scenario: Importing the root barrel does not load node:os + +- **WHEN** a consumer imports the compiler package root in a browser bundle +- **THEN** no Node built-in module is imported transitively + +### Requirement: The driver does not read artifacts back from the filesystem + +Artifact bytes produced by a finalizer SHALL be returned in-memory to the caller; the driver SHALL NOT re-read them with a synchronous filesystem call to seed the cache. + +#### Scenario: Finalized bytes are returned in memory + +- **WHEN** a finalizer produces an artifact +- **THEN** its bytes are available to the cache without a second read from disk diff --git a/openspec/changes/harden-compiler-native-boundary/tasks.md b/openspec/changes/harden-compiler-native-boundary/tasks.md new file mode 100644 index 00000000..91dfd7dd --- /dev/null +++ b/openspec/changes/harden-compiler-native-boundary/tasks.md @@ -0,0 +1,35 @@ +## 1. NativeToolchain boundary + +- [ ] 1.1 Add `ToolchainError` (`Data.TaggedError`) with `operation` + `reason` union (`SpawnFailed`/`StorageFailed`/`LinkFailed`, wrapped branch carries `cause`) +- [ ] 1.2 Convert `withBuildScope` to `Effect.acquireRelease` for the build scope/temp dir +- [ ] 1.3 Wrap `runPlanned`/`writeArtifact`/`commit`/`commitCachedArtifact`/`ClangLinker.link` in `Effect.try` yielding typed failures +- [ ] 1.4 Update `Driver.compile` to `yield*` the toolchain instead of calling synchronously +- [ ] 1.5 Add `atomicCommit` and route the four commit sites through it (fixes the temp leak) +- [ ] 1.6 Remove the never-injected `NativeLinker` interface + +## 2. Browser-safe barrel and host detection + +- [ ] 2.1 Move `platform()`/`arch()` out of `Target.ts` into a Node-only module; have `Target.fromHost` accept the values +- [ ] 2.2 Collapse `Target.triple` into `Target.id` +- [ ] 2.3 Add a bundle smoke check that the compiler root imports no node built-in + +## 3. Driver artifact hand-off and telemetry + +- [ ] 3.1 Have `finalizeWasm`/`ClangLinker.link` return `{ path, bytes }` and remove `readFileSync` from `Driver.ts` +- [ ] 3.2 Provide `memoryUsage` via an injected `HeapObservation` service rather than `node:process` in `Driver` + +## 4. Backend error channels + +- [ ] 4.1 `WasmBackend.ts:6133` yield `BackendError` instead of throwing `RangeError` +- [ ] 4.2 Pin `Backend.emitProgram` to `LlvmError | BackendError` and wrap via `catchTag` (drop the `_tag` probe) +- [ ] 4.3 Convert `emitProgram`/`locate` to `Effect.fnUntraced`; give `locate` a concrete instruction type and drop the cast + +## 5. BootstrapEvaluation host catches + +- [ ] 5.1 Preserve `cause` in the `HostWrite` and `OsCall` catches in `BootstrapEvaluation.ts` + +## 6. Verification + +- [ ] 6.1 Run `pnpm typecheck` and verify clean +- [ ] 6.2 Run `pnpm exec biome check .` and verify clean +- [ ] 6.3 Run `pnpm test` and verify the suite passes with new typed-failure and cleanup tests diff --git a/openspec/changes/reconcile-compiler-duplicated-semantics/.openspec.yaml b/openspec/changes/reconcile-compiler-duplicated-semantics/.openspec.yaml new file mode 100644 index 00000000..d160e09c --- /dev/null +++ b/openspec/changes/reconcile-compiler-duplicated-semantics/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-21 diff --git a/openspec/changes/reconcile-compiler-duplicated-semantics/design.md b/openspec/changes/reconcile-compiler-duplicated-semantics/design.md new file mode 100644 index 00000000..4e9a579a --- /dev/null +++ b/openspec/changes/reconcile-compiler-duplicated-semantics/design.md @@ -0,0 +1,43 @@ +## Context + +See proposal.md. All four reconciliation targets live in the pure type/ownership layer (no Effect, no node:*), so the work is deterministic and is covered by the existing golden and fresh-process determinism suites. + +## Goals / Non-Goals + +**Goals:** make every duplicated semantic rule resolve to exactly one owner; keep every reconciliation behavior-preserving except the access-satisfaction fix; pin the access rule in spec and in tests. + +**Non-Goals:** structural module splits (separate changes); boundary/Effect hygiene; dead-code removal. + +## Decisions + +### The access partial order (Shared < Exclusive < Take) + +Four encodings exist today. Decision: adopt the rank ordering already used by `InterfaceWitnessCompatibility.ts:181` (`accessRank(requirement.access) <= accessRank(allowed.access)`) as canonical, and correct `Type.ts:1682` (`haveSameRepresentationShape`, exact-only) to use the shared helper. Rationale: exact-only rejects a strictly larger set than every other path, and the three non-outlier sites agree on "stronger satisfies weaker". Alternatives: making everything exact-only was rejected — it would contradict the documented Shared/Exclusive examples in `bootstrap-service-declarations` and reject witness selections the compatibility path already accepts. + +Add to `Type.ts`: `accessRank(access): 0 | 1 | 2`, `compareAccess`, and `requirementSatisfies(supplied, required): boolean`. Route `TypeCompatibility.ts:66,81,94`, `InterfaceWitnessCompatibility.ts:84,181`, and the `Type.ts` sites (`1682,3046,3250,3264`) through them. + +### LIFO release-order helper + +Add an exported `inReleaseOrder(owned)` helper in `Ownership.ts` near `cleanupPlan` (it reuses cleanup-plan shapes): returns owned entries in reverse capture order, filtering `NoCleanup`. `SuspensionOwnership.ts:362` and `Lower.ts:5819,5986` import it and delete their local `.reverse()` + dedup variants. Keep the most conservative deduplication (dedupe by ordinal) so a release is never double-issued. + +### Set helpers + +Add `internal/SetOf.ts` (`equal`, `union`, `intersection`) and route `Ownership.ts:2680,2682` and `SuspensionOwnership.ts:72,78` through it. + +### `contains*` via one boolean fold + +Add `Type.some(self, predicate): boolean` (a boolean variant of the existing `fold`) and re-express the five `contains*` predicates as one-liners, choosing the most conservative recursion (recurse into `requirements`) so no predicate silently widens acceptance. + +### `Token.describe('Invalid')` + +Change the description from `'valid token'` to `'invalid byte'`. + +## Risks / Trade-offs + +- [Access change widens acceptance] → new scenario tests pin the exact before/after; `bootstrap-service-declarations` examples are the source of truth for the ordering. +- [`.reverse()` dedup differences] → keep the most conservative dedup (by ordinal) so releases are never double-issued. +- [Conservative `contains*`] → choosing the widest recursion could reject programs; verify against the full suite before merging. + +## Validation + +`pnpm typecheck`, then `pnpm exec biome check .`, then `pnpm test`. Add `@effect/vitest` cases for each Shared/Exclusive/Take pair and confirm the new scenarios in the spec each map to a passing test. diff --git a/openspec/changes/reconcile-compiler-duplicated-semantics/proposal.md b/openspec/changes/reconcile-compiler-duplicated-semantics/proposal.md new file mode 100644 index 00000000..12d9d551 --- /dev/null +++ b/openspec/changes/reconcile-compiler-duplicated-semantics/proposal.md @@ -0,0 +1,25 @@ +## Why + +A deep review of `packages/compiler` found that the same semantic rule is implemented two to eight times in the type/compatibility/ownership layer, and the copies have already drifted apart. The most serious case is requirement/access satisfaction, encoded four different ways with contradictory semantics (`Type.ts:1682` uses exact-only matching while every other site lets a stronger access mode satisfy a weaker requirement). Two compiler paths currently disagree about which programs type-check, the exact class of silent divergence a determinism-sensitive compiler cannot tolerate. + +## What Changes + +- **One canonical requirement/access satisfaction rule** reconciled across `Type`, `TypeCompatibility`, and `InterfaceWitnessCompatibility`. The four divergent encodings collapse into one `Type.compareAccess` / `Type.requirementSatisfies` helper pair. +- **One canonical LIFO release-order computation** shared by `Ownership`, `SuspensionOwnership`, and `Lower` (today four independent copies differ in deduplication and in how they apply the reverse order). +- **One shared set-equality/union primitive** replacing `sameLive`/`intersection` (Ownership) and `equalSet`/`union` (SuspensionOwnership). +- **One boolean fold over `Type.fold`** replacing the five near-identical `contains*` predicates, three of which already disagree on whether they recurse into `requirements`. +- **Fix `Token.describe('Invalid')`** so it no longer reports "valid token". + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + +- `bootstrap-complete-interface-contracts`: pin the single requirement/access satisfaction order and require it consistently across compatibility, representation-shape equality, and witness selection. + +## Impact + +Touches `Type.ts`, `TypeCompatibility.ts`, `InterfaceWitnessCompatibility.ts`, `Ownership.ts`, `SuspensionOwnership.ts`, `Lower.ts`, and `Token.ts`. No public subpath or import-renaming. The access-satisfaction fix can accept programs the exact-only path previously rejected (see the new scenario in `bootstrap-complete-interface-contracts`); every other change is behavior-preserving and is pinned by the existing determinism and golden suites. diff --git a/openspec/changes/reconcile-compiler-duplicated-semantics/specs/bootstrap-complete-interface-contracts/spec.md b/openspec/changes/reconcile-compiler-duplicated-semantics/specs/bootstrap-complete-interface-contracts/spec.md new file mode 100644 index 00000000..729b4f27 --- /dev/null +++ b/openspec/changes/reconcile-compiler-duplicated-semantics/specs/bootstrap-complete-interface-contracts/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Requirement access satisfaction follows one partial order + +Requirement/access satisfaction SHALL be decided by a single partial order applied identically by type compatibility, representation-shape equality, and interface-witness selection. Given access levels Shared, Exclusive, and Take, a supplied access SHALL satisfy a required access when the supplied access is at least as strong as the required access: Take satisfies every access, Exclusive satisfies Exclusive and Shared, and Shared satisfies only Shared. + +#### Scenario: A stronger access satisfies a weaker requirement + +- **WHEN** a requirement demands Shared access +- **THEN** a provider supplying Exclusive or Take access satisfies it + +#### Scenario: A weaker access does not satisfy a stronger requirement + +- **WHEN** a requirement demands Exclusive access +- **THEN** a provider supplying Shared access does not satisfy it + +#### Scenario: Shape equality and compatibility agree + +- **WHEN** two requirement rows are compared for representation-shape equality and for compatibility +- **THEN** both paths report the same satisfaction result for every Shared/Exclusive/Take pair + +#### Scenario: Witness selection obeys the same order + +- **WHEN** an interface witness is selected for a provider whose access differs from the requirement +- **THEN** satisfaction is judged by the same partial order used by compatibility, never by an exact-equality-only rule diff --git a/openspec/changes/reconcile-compiler-duplicated-semantics/tasks.md b/openspec/changes/reconcile-compiler-duplicated-semantics/tasks.md new file mode 100644 index 00000000..aff35db1 --- /dev/null +++ b/openspec/changes/reconcile-compiler-duplicated-semantics/tasks.md @@ -0,0 +1,27 @@ +## 1. Access satisfaction + +- [ ] 1.1 Add `Type.accessRank`/`compareAccess`/`requirementSatisfies` and verify the new unit cases pass +- [ ] 1.2 Route `TypeCompatibility.ts:66,81,94` through the helper and verify compatibility tests pass +- [ ] 1.3 Route `InterfaceWitnessCompatibility.ts:84,181` through the helper and verify witness tests pass +- [ ] 1.4 Correct `Type.ts:1682` and `:3046` to the shared rule and verify representation-shape tests pass +- [ ] 1.5 Add Shared/Exclusive/Take acceptance pairs to the interface-contracts suite and verify all three paths agree + +## 2. LIFO release order + +- [ ] 2.1 Add `inReleaseOrder` in `Ownership.ts` and verify the drop-order tests pass +- [ ] 2.2 Replace the local reverses in `SuspensionOwnership.ts:362` and `Lower.ts:5819,5986` and verify ownership/suspension tests pass + +## 3. Set and fold helpers + +- [ ] 3.1 Add `internal/SetOf.ts` and route the Ownership/SuspensionOwnership set helpers; verify tests pass +- [ ] 3.2 Add `Type.some` and collapse the five `contains*` predicates; verify tests pass + +## 4. Token wording + +- [ ] 4.1 Fix `Token.ts:159` to `'invalid byte'` and verify the lexer diagnostic test still passes + +## 5. Verification + +- [ ] 5.1 Run `pnpm typecheck` and verify clean +- [ ] 5.2 Run `pnpm exec biome check .` and verify clean +- [ ] 5.3 Run `pnpm test` and verify the compiler suite passes, reporting any diagnostic-text changes diff --git a/openspec/changes/remove-compiler-dead-code/.openspec.yaml b/openspec/changes/remove-compiler-dead-code/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/remove-compiler-dead-code/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/remove-compiler-dead-code/design.md b/openspec/changes/remove-compiler-dead-code/design.md new file mode 100644 index 00000000..94b899e9 --- /dev/null +++ b/openspec/changes/remove-compiler-dead-code/design.md @@ -0,0 +1,20 @@ +## Context + +See proposal.md. All removals are verified zero-caller by grep across `src/` and `test/`. Removing `SyntaxCorrespondence.between` from `ProjectAnalysis.ts:122` deletes a full-tree FNV fingerprint that runs on every changed module each revision while nothing reads the result. + +## Decisions + +- **`Analysis` projections**: delete the nine zero-caller functions; move the remaining test-only `*Of` projections into `test/support/projections.ts` so `Analysis.ts` keeps only operations with production callers. `declarationLookup`/`parameterLookup` are forwards into `DeclarationIndex`/Elaboration and are deleted. +- **`SyntaxCorrespondence`**: keep the module; stop calling `between` from `ProjectAnalysis` and delete the `correspondence` field on `Changed`. Reintroduce only behind a real LSP consumer later. +- **`Mir.samples`**: move builders to `test/support/mirSamples.ts`; `Mir.ts` drops its sole `effect/Option` import. +- **`Type.intrinsicConformances`**: delete the empty registry and `intrinsicallyConforms`; its two call sites return before reaching that branch, so behavior is unchanged. +- **`Hir.hasUnavailable`**: delete; express the single test as `Hir.firstUnavailable(fn) !== undefined`. + +## Risks / Trade-offs + +- [Test-only projections move] → tests import from `test/support`; no production import path changes. +- [Public exports removed] → green-field means no compat contract; `pnpm typecheck` catches any missed call site. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test`. diff --git a/openspec/changes/remove-compiler-dead-code/proposal.md b/openspec/changes/remove-compiler-dead-code/proposal.md new file mode 100644 index 00000000..70394236 --- /dev/null +++ b/openspec/changes/remove-compiler-dead-code/proposal.md @@ -0,0 +1,27 @@ +## Why + +The review found exported and internal symbols with no callers, test fixtures bundled into the production module, and a per-revision full-tree fingerprint that no production code reads. Under the repo's green-field policy these are deleted rather than preserved for compatibility optics. + +## What Changes + +- **Delete nine zero-caller `Analysis` facade exports** (`structLiteralsOf`, `appliedLayoutsOf`, `unionLayoutsOf`, `unionCallingShapesOf`, `hirUnionConversionsOf`, `mirUnionConversionsOf`, `declarationLookup`, `parameterLookup`, `unionTraceEventsOf`) and move the test-only `*Of` projections to `test/support/`. +- **Delete dead serializers/guards**: `IntrinsicAvailability.encode`, `Type.isOutOfMemoryError`, the permanently-empty `Type.intrinsicConformances`/`intrinsicallyConforms` pair, `ConformanceGoal.dependencies`, `OsRuntime.isSymbol`. +- **Delete dead realization narrowers**: `CallableFieldRealization.callableRealizationOf`/`effectRealizationOf`, `OpaqueRealization.publicOrigin`. +- **Delete dead IR walkers**: `SuspensionMir.hasSuspension`, `Hir.hasUnavailable` (fold its one test into `Hir.firstUnavailable`), `Mir.suspensionControlEdges`. +- **Delete `ModuleTooling.make`** and the `DeclarationIndex.presentParameterNameEntries` no-op alias; drop the `FrontendTooling` re-exports of `ModuleTooling` symbols. +- **Remove the eager `SyntaxCorrespondence.between` call** in `ProjectAnalysis` (no production consumer) and its `Changed.correspondence` field. +- **Relocate `Mir.samples()`** + fixtures to `test/support/mirSamples.ts`, dropping the `effect/Option` import from `Mir.ts`. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Removes unused public surface from `Analysis`, `IntrinsicAvailability`, `Type`, `ConformanceGoal`, `OsRuntime`, `CallableFieldRealization`, `OpaqueRealization`, `Mir`, `Hir`, `SuspensionMir`, `ModuleTooling`, `DeclarationIndex`, and `FrontendTooling`. No language behavior changes; `skip_specs: true`. diff --git a/openspec/changes/remove-compiler-dead-code/tasks.md b/openspec/changes/remove-compiler-dead-code/tasks.md new file mode 100644 index 00000000..b56012bd --- /dev/null +++ b/openspec/changes/remove-compiler-dead-code/tasks.md @@ -0,0 +1,26 @@ +## 1. Remove zero-caller facade exports + +- [ ] 1.1 Delete the nine zero-caller `Analysis` exports and verify typecheck finds no callers +- [ ] 1.2 Move the test-only `*Of` projections to `test/support/` and update their imports + +## 2. Delete dead serializers, guards, and narrowers + +- [ ] 2.1 Delete `IntrinsicAvailability.encode`, `Type.isOutOfMemoryError`, `intrinsicConformances`/`intrinsicallyConforms`, `ConformanceGoal.dependencies`, `OsRuntime.isSymbol` +- [ ] 2.2 Delete `CallableFieldRealization.callableRealizationOf`/`effectRealizationOf`, `OpaqueRealization.publicOrigin` +- [ ] 2.3 Delete `SuspensionMir.hasSuspension`, `Hir.hasUnavailable`, `Mir.suspensionControlEdges` + +## 3. Tooling and index cleanup + +- [ ] 3.1 Delete `ModuleTooling.make` and the `DeclarationIndex.presentParameterNameEntries` alias +- [ ] 3.2 Drop the `FrontendTooling` re-exports of `ModuleTooling` symbols and import `ModuleTooling` directly in `Analysis` + +## 4. Dead hot-path work and prod fixtures + +- [ ] 4.1 Remove the eager `SyntaxCorrespondence.between` call and `Changed.correspondence` +- [ ] 4.2 Relocate `Mir.samples` to `test/support/mirSamples.ts` and drop the `effect/Option` import from `Mir` + +## 5. Verification + +- [ ] 5.1 Run `pnpm typecheck` and verify clean +- [ ] 5.2 Run `pnpm exec biome check .` and verify clean +- [ ] 5.3 Run `pnpm test` and verify the suite passes diff --git a/openspec/changes/split-compiler-backends/.openspec.yaml b/openspec/changes/split-compiler-backends/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-backends/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-backends/design.md b/openspec/changes/split-compiler-backends/design.md new file mode 100644 index 00000000..bb6f65be --- /dev/null +++ b/openspec/changes/split-compiler-backends/design.md @@ -0,0 +1,26 @@ +## Context + +See proposal.md. The @silk-effect/llvm boundary is already thin and Effect-native (every op is yield*-ed LlvmError reconciled once via mapError); this change only factors it, it does not re-wrap the external API. + +## Decisions + +- **Shared contract module** (e.g. BackendShared.ts, or Artifact.ts + Symbol.ts): move CodegenRequest, ControlProvenance, Artifact, BackendError, Backend, emit, terminationOf, formatModuleViolations, sanitize/injectivePart/symbolFor/suspensionPointKey, and lineTable/positionOf. WasmBackend imports symbolFor/suspensionPointKey/terminationOf/formatModuleViolations/ControlProvenance from here instead of from Backend.ts. Rename Backend.ts to LlvmBackend.ts; BackendRegistry and Pipeline import changes are mechanical. + +- **Linearization**: move expandMatches/linearize/llvmControl (484–920) to a target-neutral module beside Mir (it is candidate-neutral control-flow lowering). + +- **LoweringContext record**: capture { builder, types, program, layout, lanesFor, packedLanes, declared, mutableStorage } explicitly; each Native* actor is a pure function (ctx, ...) => ... with no closure capture. The per-function body, coerceLane, callValues, emitCallableBinary/emitIntegerConversion, Binary/CheckedScalar, aggregate/cleanup, the operation dispatch switch, terminator switch, and coroutine thunks each become one actor. + +- **NativeArith seam**: one comparisonPredicates table and one checked-arithmetic/overflow/range/div-by-zero lowering, consumed by both emitCallableBinary and the Binary case (and by the conversion/CheckedScalar paths) with different read/write wrappers. + +- **Lane-pointer helper**: one lanePointer(lanes, base, offset, name) reconciling the six pointerAt/bytePointer/framePointer variants. + +- **WasmBackend split**: WasmMemory.ts (heap constants + MemoryContext + heapAllocateBody/heapReleaseBody + growToCover), WasmCleanup.ts (the four walks collapsed to one emitCleanupWalk(addressProducer, hook|reclaim)), WasmSuspension.ts (suspension inputs/context/runtime + originate/relay + thunk assembly), WasmLanes.ts (laneKindsOf/laneValueType/load/store mnemonics/packWasmLanes/alignUp). A WasmEmitContext record makes the emitOperation cases referencable as sibling functions. zeroConst is exported and reused by the resume-thunk local zeroing. + +## Risks / Trade-offs + +- [Emitted-code drift] -> golden wasm/native byte comparisons and the differential corpus are the gate; factor one actor at a time and re-run. +- [Cross-backend ABI] -> suspensionPointKey moves to the shared module with an identical format; add a test asserting Backend and WasmBackend produce the same key. + +## Validation + +pnpm typecheck, pnpm exec biome check ., pnpm test (native + wasm backends, suspension and corpus suites). \ No newline at end of file diff --git a/openspec/changes/split-compiler-backends/proposal.md b/openspec/changes/split-compiler-backends/proposal.md new file mode 100644 index 00000000..b686b9ed --- /dev/null +++ b/openspec/changes/split-compiler-backends/proposal.md @@ -0,0 +1,26 @@ +## Why + +**Backend.ts** is a 9,207-line file containing an ~8,200-line emitProgram Effect.gen (lines 933–9167) whose ~50 nested closures all capture one builder context, so no lowering is unit-testable in isolation and scalar arithmetic/binary lowering is duplicated byte-for-byte two to three times. The same file also hosts the shared cross-backend contract that **WasmBackend** imports by name; six near-identical lane-pointer helpers coexist; and emitProgram is authored as a generator-returning arrow with its error channel pinned only by tag-string probing. **WasmBackend.ts** is a 6,915-line file whose emitOperation is a 3,628-line god-function with four near-duplicate CleanupPlan walks and triplicated memory-grow/zero logic. + +## What Changes + +- **Extract the shared cross-backend contract** — Backend, BackendError, Artifact, CodegenRequest, ControlProvenance, terminationOf, formatModuleViolations, symbolFor, suspensionPointKey, and lineTable helpers — into a shared module so WasmBackend stops importing the LLVM file by name. Rename Backend.ts to LlvmBackend.ts (with a re-export for existing callers) and move the target-neutral MIR linearization beside Mir. +- **Split Backend emitProgram** into NativeType / NativeDeclare / NativeDebug / NativeFunction / NativeCall / NativeArith / NativeAggregate / NativeOperation / NativeControl / NativeSuspension, driven by an explicit LoweringContext record. +- **Deduplicate scalar arithmetic** (the byte-identical comparisonPredicates table and checked overflow/div/range blocks) into a single NativeArith seam; merge the six lane-pointer helpers into one. +- **Move the transcendental sin/cos/tan kernel** beside Transcendental.plan. +- **Split WasmBackend.ts** into WasmMemory / WasmCleanup / WasmSuspension / WasmLanes over a WasmEmitContext; each emitOperation case becomes a sibling function. +- **Deduplicate the four CleanupPlan walks** into one emitCleanupWalk, and the memory-grow idiom and zero-constant dispatch into single helpers. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor of both backends. Emitted IR/wasm must stay byte-identical (golden backends + native/wasm differential corpus are the net). skip_specs: true. \ No newline at end of file diff --git a/openspec/changes/split-compiler-backends/tasks.md b/openspec/changes/split-compiler-backends/tasks.md new file mode 100644 index 00000000..6356927e --- /dev/null +++ b/openspec/changes/split-compiler-backends/tasks.md @@ -0,0 +1,32 @@ +## 1. Extract the shared contract + +- [ ] 1.1 Create BackendShared.ts (contract, errors, symbols, lineTable) and move those exports +- [ ] 1.2 Rename Backend.ts to LlvmBackend.ts with a thin re-export for existing importers +- [ ] 1.3 Move linearize/expandMatches/llvmControl beside Mir +- [ ] 1.4 Update WasmBackend to import shared symbols (incl. suspensionPointKey) and verify backends still emit identically + +## 2. Split Backend emitProgram + +- [ ] 2.1 Introduce the LoweringContext record +- [ ] 2.2 Extract NativeType.ts, NativeDeclare.ts, NativeDebug.ts +- [ ] 2.3 Extract NativeFunction.ts, NativeCall.ts +- [ ] 2.4 Extract NativeArith.ts (dedup comparisonPredicates + checked ops) +- [ ] 2.5 Extract NativeAggregate.ts, NativeOperation.ts, NativeControl.ts, NativeSuspension.ts +- [ ] 2.6 Move the transcendental kernel beside Transcendental.plan +- [ ] 2.7 Collapse the six lane-pointer helpers into one lanePointer +- [ ] 2.8 Verify native backend goldens pass + +## 3. Split WasmBackend.ts + +- [ ] 3.1 Introduce WasmEmitContext +- [ ] 3.2 Extract WasmMemory.ts (with growToCover) +- [ ] 3.3 Extract WasmCleanup.ts (one emitCleanupWalk replacing four walks) +- [ ] 3.4 Extract WasmSuspension.ts and WasmLanes.ts +- [ ] 3.5 Convert emitOperation cases to sibling functions over WasmEmitContext +- [ ] 3.6 Dedup zeroConst and verify wasm suspension/corpus suites pass + +## 4. Verification + +- [ ] 4.1 Run pnpm typecheck and verify clean +- [ ] 4.2 Run pnpm exec biome check . and verify clean +- [ ] 4.3 Run pnpm test \ No newline at end of file diff --git a/openspec/changes/split-compiler-declaration-index/.openspec.yaml b/openspec/changes/split-compiler-declaration-index/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-declaration-index/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-declaration-index/design.md b/openspec/changes/split-compiler-declaration-index/design.md new file mode 100644 index 00000000..89f07706 --- /dev/null +++ b/openspec/changes/split-compiler-declaration-index/design.md @@ -0,0 +1,19 @@ +## Context + +See proposal.md. collect and complete are the only phase entry points (NameResolution.analyze and ModuleSurface.fromIndex are untouched), so the split is mechanical. + +## Decisions + +- **Five actors** along the existing seams: DeclarationFacts.ts (28–1324 + lookups), DeclarationCollection.ts (analyzeDeclaredType 1506–2340, collectModule 3116–3895), DeclarationResolution.ts (3916–5757), DeclarationCompletion.ts (complete 5818–7100 + its passes), ConformanceProof.ts (prove/conforms/witness/copyProof 7102–8088). +- **analyzeAppliedRows(source, list, typeParameters)**: returns failures/requirements/requirementParameters/rowDiagnostics; both the Effect special case and the generic branch consume it. +- **collectRowExpression(source, syntax, typeParameters, leaf)**: the structural RowWithout/UnionType/unavailable walk shared once, with the leaf discriminated (failure member vs row/requirement member). +- **Imports**: the five actors import SourceSpan/SyntaxTree/Type/Diagnostic directly; keep any NameResolution <-> DeclarationIndex cycles type-only. + +## Risks / Trade-offs + +- [Diagnostic parity] -> the shared row parser must reproduce current kind-mismatch/unknownType diagnostics exactly; diagnostic-code tests are the gate. +- [Large motion] -> one actor per commit. + +## Validation + +pnpm typecheck, pnpm exec biome check ., pnpm test (declaration-index + diagnostics suites). \ No newline at end of file diff --git a/openspec/changes/split-compiler-declaration-index/proposal.md b/openspec/changes/split-compiler-declaration-index/proposal.md new file mode 100644 index 00000000..0c5fb193 --- /dev/null +++ b/openspec/changes/split-compiler-declaration-index/proposal.md @@ -0,0 +1,23 @@ +## Why + +**DeclarationIndex.ts** (8,088 lines) mixes the fact model, syntax->fact collection, type resolution, completion/validation (complete is ~1,280 lines of several sub-passes), and conformance proof into one file. Its Effect branch re-parses the generic AppliedType rows nearly verbatim, and its failure vs requirement row-expression collectors are near-duplicates. + +## What Changes + +- **Split DeclarationIndex.ts** into **DeclarationFacts** (fact model + lookups), **DeclarationCollection**, **DeclarationResolution**, **DeclarationCompletion**, and **ConformanceProof**; collect/complete stay the phase entry points. +- **Extract analyzeAppliedRows** so the Effect and generic AppliedType branches share one row parser. +- **Extract collectRowExpression** with a leaf discriminator, replacing the two near-duplicate RowExpressionFact collectors. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor of the frontend index. Diagnostics, codes, and spans stay byte-identical (the shared row parser returns the same diagnostics). Tarjan/import-path dedup is in the shared-helpers change. skip_specs: true. \ No newline at end of file diff --git a/openspec/changes/split-compiler-declaration-index/tasks.md b/openspec/changes/split-compiler-declaration-index/tasks.md new file mode 100644 index 00000000..9074d127 --- /dev/null +++ b/openspec/changes/split-compiler-declaration-index/tasks.md @@ -0,0 +1,18 @@ +## 1. Split the five actors + +- [ ] 1.1 Extract DeclarationFacts.ts +- [ ] 1.2 Extract DeclarationCollection.ts +- [ ] 1.3 Extract DeclarationResolution.ts +- [ ] 1.4 Extract DeclarationCompletion.ts +- [ ] 1.5 Extract ConformanceProof.ts and verify index tests pass + +## 2. Dedup row analysis + +- [ ] 2.1 Extract analyzeAppliedRows and route the Effect + generic branches through it +- [ ] 2.2 Extract collectRowExpression with a leaf discriminator and replace the two collectors + +## 3. Verification + +- [ ] 3.1 Run pnpm typecheck and verify clean +- [ ] 3.2 Run pnpm exec biome check . and verify clean +- [ ] 3.3 Run pnpm test (diagnostic codes/spans unchanged) \ No newline at end of file diff --git a/openspec/changes/split-compiler-elaboration/.openspec.yaml b/openspec/changes/split-compiler-elaboration/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-elaboration/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-elaboration/design.md b/openspec/changes/split-compiler-elaboration/design.md new file mode 100644 index 00000000..5f191087 --- /dev/null +++ b/openspec/changes/split-compiler-elaboration/design.md @@ -0,0 +1,22 @@ +## Context + +See proposal.md. `elaborateModule` is a synchronous pure entry (`SyntaxTree` → facts → HIR). `ElaborationResult`, the fact unions, and `elaborateModule` are the stable surface; the four execution phases read them. + +## Decisions + +- **`ExpressionAnalysis`**: `analyzeExpression` + the `analyze*Literal/Identifier/Move/Borrow/Match/Struct/Array/Projection/Operator/Pipeline/Effect/Run` sub-actors (1408–8094). Pull the three inline mega-branches into named `analyzeEffectBlock`/`analyzeRun`/`analyzeCall`. +- **`CallResolution`**: the call-resolution tail (8566–9030) plus `analyzeCallTypeArguments`/`analyzeArguments`/`analyzeCallContract`/`solveCallableConstraints`/`seededSpecialization`/`resolvedFunctionReference`/`serviceOperation`/`boundOperationReference`/`finish*Call`/`analyzeBuiltinCall`/`finishCallableSection`/`finishCallableApplication`/`finishIntrinsicContractCall`. +- **`StatementAnalysis`**: `analyzeStatements`, `analyzeFunctionBody`, `returnFlowOf`, `executableStatements`, `effectCaptureFacts`. +- **`HirLowering`**: `hirExpression` (10603–11615), `hirExpectedExpression`, `hirReference`, `hirPatternSelection`, `hirWritePlace`, and the merged `lowerStatements`. +- **Statement lowering merge**: one `lowerStatements(facts, { resultType, functionId, eraseIntrinsicSections, borrowBindingInitializers })`; delete `hirEffectStatements` and route the closure through it, keeping the closure's stricter behavior. +- **`strongestEffectAccess`**: the one Take>Exclusive>Shared reducer used by all five sites. +- **`argumentBorrowId(argument, ordinal)` + `loanEndsOf`**: consumed by every call-lowering branch. + +## Risks / Trade-offs + +- [Drift reconciliation] → the merged statement lowering changes behavior for a couple of `UnavailableStatement`/Write edges; capture before/after with the golden determinism suite. +- [Import cycles] → the four actors import the `Elaboration` fact vocabulary; keep cycles type-only. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test` (elaboration/determinism golden suites). diff --git a/openspec/changes/split-compiler-elaboration/proposal.md b/openspec/changes/split-compiler-elaboration/proposal.md new file mode 100644 index 00000000..52b10d6f --- /dev/null +++ b/openspec/changes/split-compiler-elaboration/proposal.md @@ -0,0 +1,24 @@ +## Why + +`Elaboration.ts` is the package's largest file: 12,635 lines, 271 top-level declarations, no section banners, spanning five phases (fact vocabulary, syntax→fact analysis, call resolution, statement-flow, HIR lowering). Its two expression/statement walks have already drifted, borrow-id construction is copy-pasted six-plus times, and effect-access reduction is computed five different ways. + +## What Changes + +- **Keep `Elaboration.ts`** as the fact vocabulary + `Result` + the `elaborateModule` façade; **extract four execution actors**: `ExpressionAnalysis`, `CallResolution`, `StatementAnalysis`, and `HirLowering`. +- **Merge the duplicated statement lowering** — `hirEffectStatements` (10429–10585) and the `hirStatements` closure (12271–12468) — into one parameterized `lowerStatements`. +- **Add single helpers** for borrow-id construction (`argumentBorrowId`/`loanEndsOf`) and effect-access reduction (`strongestEffectAccess`), replacing the 6× and 5× inline copies. +- **Remove the two redundant `as SyntaxTree.Node` casts** by binding `arms.at(0)` once. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor of the front-middle-end semantics pass; no observable behavior change except where the drifted statement lowering is reconciled (the merged helper picks the closure's more complete behavior — the Borrow-id initializer and Mutable-write guard — verified against the full suite). `skip_specs: true`. diff --git a/openspec/changes/split-compiler-elaboration/tasks.md b/openspec/changes/split-compiler-elaboration/tasks.md new file mode 100644 index 00000000..41b1cb6c --- /dev/null +++ b/openspec/changes/split-compiler-elaboration/tasks.md @@ -0,0 +1,23 @@ +## 1. Extract the four actors + +- [ ] 1.1 Extract `ExpressionAnalysis.ts` (and pull the three inline branches into named functions) +- [ ] 1.2 Extract `CallResolution.ts` (the 8566–9030 tail + its sub-actors) +- [ ] 1.3 Extract `StatementAnalysis.ts` +- [ ] 1.4 Extract `HirLowering.ts` and verify elaboration tests pass at each step + +## 2. Merge the duplicated walks + +- [ ] 2.1 Add the parameterized `lowerStatements` helper and delete `hirEffectStatements` +- [ ] 2.2 Route the `hirStatements` closure through it and verify determineism tests + +## 3. Dedup borrow-id and access reduction + +- [ ] 3.1 Add `argumentBorrowId`/`loanEndsOf` and replace the six-plus inline copies +- [ ] 3.2 Route all five access-reduction sites through `strongestEffectAccess` +- [ ] 3.3 Replace the two `as SyntaxTree.Node` casts with a bound `arms.at(0)` + +## 4. Verification + +- [ ] 4.1 Run `pnpm typecheck` and verify clean +- [ ] 4.2 Run `pnpm exec biome check .` and verify clean +- [ ] 4.3 Run `pnpm test` diff --git a/openspec/changes/split-compiler-frontend/.openspec.yaml b/openspec/changes/split-compiler-frontend/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-frontend/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-frontend/design.md b/openspec/changes/split-compiler-frontend/design.md new file mode 100644 index 00000000..cf96e0e6 --- /dev/null +++ b/openspec/changes/split-compiler-frontend/design.md @@ -0,0 +1,20 @@ +## Context + +See proposal.md. The parser already threads an immutable `State` and a `syntaxNode`/recovery core, which is the seam the split preserves. + +## Decisions + +- **`internal/ParseState.ts`**: move `State`, `expect`, `syntaxNode`, trivia-skipping, and `peek(n)` here. Each grammar sub-module takes `State` and returns nodes. +- **`parseServiceLikeDeclaration(kind: 'service' | 'interface')`**: the two copies differ only in the keyword token and a loop-guard terminator; parameterize those. +- **`parseCallableContract()`**: produces the return/failure/requirement/where elements consumed by both operation and function declarations. +- **`peek(n)` lookahead**: one trivia-skipping scan primitive; the two statement lookahead predicates become token-kind predicates, removing the re-parse cost and double-encoded grammar decision. +- **`keywordSpellings`**: move `fn`/`let`/`move`/`pub`/`return`/`import` into the table and delete the byte-compare block (`Lexer.ts:136-176`). + +## Risks / Trade-offs + +- [Span/diagnostic drift] → parser golden/span tests are the net; keep `State` threading identical so offsets do not change. +- [Cycles] → grammar sub-modules may import each other; keep at type-only level (`import type`) or hoist shared node constructors into `ParseState`. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test` (parser golden + span suites). diff --git a/openspec/changes/split-compiler-frontend/proposal.md b/openspec/changes/split-compiler-frontend/proposal.md new file mode 100644 index 00000000..0f026e50 --- /dev/null +++ b/openspec/changes/split-compiler-frontend/proposal.md @@ -0,0 +1,25 @@ +## Why + +`Parser.ts` is a 3,274-line god-module holding expression, type, statement, declaration, and import grammars in one file threaded through a single `State`/recovery core. Two grammar seams (service/interface declarations and callable-contract tails) are already copy-pasted, and lookahead predicates re-parse tokens their target production parses again. + +## What Changes + +- **Split `Parser.ts` into per-grammar actors** (`Parser/Expression.ts`, `Parser/Type.ts`, `Parser/Statement.ts`, `Parser/Declaration.ts`, `Parser/Import.ts`) over a shared `internal/ParseState.ts`. +- **Merge `parseServiceDeclaration`/`parseInterfaceDeclaration`** into one `parseServiceLikeDeclaration(keyword)`. +- **Merge the callable-contract tail** shared by `parseServiceOperation` and `parseFunctionDeclaration`. +- **Route all lookahead through one trivia-skipping `peek(n)`** and convert `startsPatternBindingStatement`/`startsAssignmentStatement` into token-lookahead predicates instead of throwaway parses. +- **Unify keyword classification** in `Lexer.ts` (single `keywordSpellings` table; delete the manual per-byte block). + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +`Parser.ts` shrinks to dispatch; the five sub-modules are internal imports only, not public subpaths. `Lexer.ts` keyword-table change is behavior-preserving. Spans and diagnostics must stay byte-identical. `skip_specs: true`. diff --git a/openspec/changes/split-compiler-frontend/tasks.md b/openspec/changes/split-compiler-frontend/tasks.md new file mode 100644 index 00000000..f7bf8ac6 --- /dev/null +++ b/openspec/changes/split-compiler-frontend/tasks.md @@ -0,0 +1,24 @@ +## 1. Parser split + +- [ ] 1.1 Extract `internal/ParseState.ts` (`State`, `expect`, `syntaxNode`, trivia skip) +- [ ] 1.2 Move expression grammar into `Parser/Expression.ts` +- [ ] 1.3 Move type grammar into `Parser/Type.ts` +- [ ] 1.4 Move statement grammar into `Parser/Statement.ts` +- [ ] 1.5 Move declaration grammar into `Parser/Declaration.ts` +- [ ] 1.6 Move import grammar into `Parser/Import.ts` and verify parser tests pass + +## 2. Grammar seam merges + +- [ ] 2.1 Merge service/interface declaration parsing into `parseServiceLikeDeclaration` +- [ ] 2.2 Extract the shared callable-contract tail for operations and functions + +## 3. Lookahead and keyword unification + +- [ ] 3.1 Add one trivia-skipping `peek(n)` and convert the two statement lookahead predicates +- [ ] 3.2 Move `fn`/`let`/`move`/`pub`/`return`/`import` into `keywordSpellings` and delete the manual block + +## 4. Verification + +- [ ] 4.1 Run `pnpm typecheck` and verify clean +- [ ] 4.2 Run `pnpm exec biome check .` and verify clean +- [ ] 4.3 Run `pnpm test` and verify parser golden/span suites pass diff --git a/openspec/changes/split-compiler-ir-evaluation/.openspec.yaml b/openspec/changes/split-compiler-ir-evaluation/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-ir-evaluation/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-ir-evaluation/design.md b/openspec/changes/split-compiler-ir-evaluation/design.md new file mode 100644 index 00000000..78196d0f --- /dev/null +++ b/openspec/changes/split-compiler-ir-evaluation/design.md @@ -0,0 +1,19 @@ +## Context + +See proposal.md. Mir.verify/Mir.encode and the evaluator Outcome union are the stable surface. The ProvisionalMir -> Mir staging is deliberate (per-monomorphic control that backends must never consume); only its shared vocabulary needs a single owner. + +## Decisions + +- **MirVerification.ts**: verify (3630–6208) plus its private per-operation validators; RangeError guards stay as internal invariant defects. **MirEncoding.ts**: encode + the *Text/*Lines helpers (6213–6514). **Suspension.ts**: the 1043–1262 data block. +- **Shared suspension vocabulary**: move Classification/Runner/Completion/Provider to one owner (a Suspension.ts data module, or import Mir.SuspensionClassification into ProvisionalMir); SuspensionMir field mappers consume it instead of re-typing. Fix SuspensionMir.operationArguments to plain operation.arguments. +- **BootstrapArithmetic.ts**: integralBinary(op, scalar, pointerBits, left, right), compare, and checkedOp, used by both the callable path (invokeCallableTarget) and the MIR Binary/checked arms. +- **BootstrapPlace.ts**: walkPlace(root, selectors, indexes) returning selected + traceSelectors, plus replacePlaceByIndexes; resolvePlace, ReadPlace, WritePlace, referenced, and selectStoredPlace all delegate. + +## Risks / Trade-offs + +- [Evaluator fidelity] -> the corpus differential suite (evaluator vs wasm/native) is the gate; extract one block at a time and run it after each. +- [Large code motion] -> one actor per commit. + +## Validation + +pnpm typecheck, pnpm exec biome check ., pnpm test (bootstrap-evaluation + bootstrap-mir differential suites). \ No newline at end of file diff --git a/openspec/changes/split-compiler-ir-evaluation/proposal.md b/openspec/changes/split-compiler-ir-evaluation/proposal.md new file mode 100644 index 00000000..fcdda1f6 --- /dev/null +++ b/openspec/changes/split-compiler-ir-evaluation/proposal.md @@ -0,0 +1,24 @@ +## Why + +**Mir.ts** (6,679 lines) mixes data with a ~2,580-line verify, a deterministic encoder, and test fixtures; **ProvisionalMir** and **Mir** re-declare the same suspension vocabulary. **BootstrapEvaluation.ts** (5,939 lines) mixes six concerns and repeats its scalar-arithmetic logic twice and its place traversal four times inline. + +## What Changes + +- **Split Mir.ts** into **Suspension** (suspension data types), **MirVerification** (verify + per-operation validators), and **MirEncoding** (encode + text helpers); Mir keeps data + accessors. (samples relocation is in the dead-code change.) +- **One shared suspension vocabulary**: Classification, Runner, Completion, and Provider defined once; SuspensionMir projects rather than re-types. Fix the dead operationArguments ternary. +- **Split BootstrapEvaluation.ts** into BootstrapValue/BootstrapTrace, BootstrapArithmetic, BootstrapPlace, BootstrapOsIntrinsics, BootstrapStorage, and BootstrapEffect, keeping executeMachine/evaluate as the coordinator. +- **Deduplicate arithmetic** (callable path vs MIR Binary/checked) and **place traversal** (four walkers with verbatim strings) into one BootstrapArithmetic + walkPlace. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor. Mir.verify, Mir.encode, and evaluator Outcome/BlockedReason semantics stay byte-identical; the evaluator must remain a faithful oracle for the differential native/wasm checks. skip_specs: true. \ No newline at end of file diff --git a/openspec/changes/split-compiler-ir-evaluation/tasks.md b/openspec/changes/split-compiler-ir-evaluation/tasks.md new file mode 100644 index 00000000..4aa88702 --- /dev/null +++ b/openspec/changes/split-compiler-ir-evaluation/tasks.md @@ -0,0 +1,24 @@ +## 1. Split Mir.ts + +- [ ] 1.1 Extract Suspension.ts (suspension data types) +- [ ] 1.2 Extract MirVerification.ts (verify + validators) +- [ ] 1.3 Extract MirEncoding.ts and verify encoding goldens + +## 2. Unify suspension vocabulary + +- [ ] 2.1 Give Classification/Runner/Completion/Provider one owner consumed by ProvisionalMir and Mir +- [ ] 2.2 Fix the operationArguments dead ternary + +## 3. Split BootstrapEvaluation.ts + +- [ ] 3.1 Extract BootstrapValue.ts/BootstrapTrace.ts +- [ ] 3.2 Extract BootstrapArithmetic.ts and dedup the callable/MIR arithmetic +- [ ] 3.3 Extract BootstrapPlace.ts and dedup the four place walkers +- [ ] 3.4 Extract BootstrapOsIntrinsics.ts, BootstrapStorage.ts, BootstrapEffect.ts +- [ ] 3.5 Verify the evaluator differential suite passes + +## 4. Verification + +- [ ] 4.1 Run pnpm typecheck and verify clean +- [ ] 4.2 Run pnpm exec biome check . and verify clean +- [ ] 4.3 Run pnpm test \ No newline at end of file diff --git a/openspec/changes/split-compiler-layout/.openspec.yaml b/openspec/changes/split-compiler-layout/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-layout/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-layout/design.md b/openspec/changes/split-compiler-layout/design.md new file mode 100644 index 00000000..ebc16158 --- /dev/null +++ b/openspec/changes/split-compiler-layout/design.md @@ -0,0 +1,20 @@ +## Context + +See proposal.md. layout placement, verify, and encode are the stable entry points. The RepresentationField -> CallableFieldRealization -> Layout hand-off (plan/resolve -> enrich -> place) is already layered and is preserved. + +## Decisions + +- **pack(fields, target)**: the single cursor=0; alignment=1; offset=alignUp(cursor,a); cursor=offset+size; alignment=max; size=alignUp(cursor,alignment); tailPadding=size-cursor computation, parameterized by copy vs borrow-pointer field sizing. layoutEffectSlots, layoutRepresentedCallable, layoutNominal, layoutDirectRepresented, effect/callable environments, and the verifier recomputations all call it. +- **PlacedField**: one shared offset/size/alignment/padding; the Entry executable fields, EffectEnvironmentField, StoredEffectEnvironmentField, CallableEnvironmentField, CaptureSlot, and EffectEnvironmentSlot variants add only their identity fields. +- **Access vocabulary**: one exported Access (or reuse Type.CallableMode) referenced by CallableFieldRealization, OpaqueRealization, Layout, and Match. +- **unifyPayloadTypes(variants, target)**: collect candidate scalar lanes, sort by Scalar.bits descending, fall back to "i32" — consumed by EffectCompositeShape, SumShape, and OutcomeShape. +- **FieldRealization**: rename, and rewrite the header to name the two realizable kinds (CallableRealization / EffectRealization) instead of a future-tense note. + +## Risks / Trade-offs + +- [Layout drift] -> LayoutVerify plus the committed-golden byte comparisons are the net. +- [Rename] -> update all imports; pnpm typecheck catches misses. + +## Validation + +pnpm typecheck, pnpm exec biome check ., pnpm test (target-layout + callable-field-realization suites and golden encodings). \ No newline at end of file diff --git a/openspec/changes/split-compiler-layout/proposal.md b/openspec/changes/split-compiler-layout/proposal.md new file mode 100644 index 00000000..3cf1d184 --- /dev/null +++ b/openspec/changes/split-compiler-layout/proposal.md @@ -0,0 +1,25 @@ +## Why + +**Layout.ts** (4,548 lines) mixes five concerns — physical placement, a reachability walk, environment placement, calling-shape/ABI lanes, verification, and encoding — and repeats the struct-packing loop (cursor/alignUp/size/tailPadding) about eight times. The placed-field record is re-declared five times, the lane-width unification block is copy-pasted three times, the access vocabulary (Copy/Shared/Exclusive/Take) is re-declared per module, and **CallableFieldRealization.ts** is misnamed with a stale "not yet enabled" header even though its Effect half is fully implemented. + +## What Changes + +- **Split Layout.ts** into **Layout** (physical placement), **CallingShape** (shape nodes/lanes/selectors), **LayoutVerify** (verification), and **LayoutEncode** (text encoding). +- **Add one pack(fields, target) seam** (returns fields/size/alignment/tailPadding) and route the ~8 inline loops plus the verification recomputations through it. +- **One PlacedField** (offset/size/alignment/padding) and one **Access** base shared across the five placed-field types and three access vocabularies; Match.Access narrows only if it needs Move. +- **One unifyPayloadTypes** replaces the three copy-pasted lane-width blocks; one materializeTaggedCarrier for tagged carriers. +- **Rename CallableFieldRealization to FieldRealization** (or split EffectFieldRealization) and replace the stale header with the two realizable kinds. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor; every emitted offset, size, alignment, and encoding stays byte-identical. skip_specs: true. \ No newline at end of file diff --git a/openspec/changes/split-compiler-layout/tasks.md b/openspec/changes/split-compiler-layout/tasks.md new file mode 100644 index 00000000..f45e8465 --- /dev/null +++ b/openspec/changes/split-compiler-layout/tasks.md @@ -0,0 +1,26 @@ +## 1. One packing seam + +- [ ] 1.1 Add pack(fields, target) and route the ~8 inline loops through it +- [ ] 1.2 Route the verifier recomputations through the same seam and verify layout tests pass + +## 2. Shared vocabulary + +- [ ] 2.1 Introduce one PlacedField base and one Access vocabulary +- [ ] 2.2 Add unifyPayloadTypes/materializeTaggedCarrier and replace the three lane-width blocks + +## 3. Split Layout.ts + +- [ ] 3.1 Extract CallingShape.ts +- [ ] 3.2 Extract LayoutVerify.ts +- [ ] 3.3 Extract LayoutEncode.ts and verify goldens + +## 4. Rename realization module + +- [ ] 4.1 Rename CallableFieldRealization to FieldRealization (or split EffectFieldRealization) +- [ ] 4.2 Replace the stale header and update all imports + +## 5. Verification + +- [ ] 5.1 Run pnpm typecheck and verify clean +- [ ] 5.2 Run pnpm exec biome check . and verify clean +- [ ] 5.3 Run pnpm test \ No newline at end of file diff --git a/openspec/changes/split-compiler-mid-end/.openspec.yaml b/openspec/changes/split-compiler-mid-end/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-mid-end/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-mid-end/design.md b/openspec/changes/split-compiler-mid-end/design.md new file mode 100644 index 00000000..ed94e652 --- /dev/null +++ b/openspec/changes/split-compiler-mid-end/design.md @@ -0,0 +1,21 @@ +## Context + +See proposal.md. `Lower.lowerProgram` and `FunctionLowering` (the mutable transform state) are the stable seams; the split factors the helper families around them. + +## Decisions + +- **`Lower.ts` seams** (line ranges from the review): `Forwarding.ts` (325–818), `ValueType.ts` (820–1368), `EffectLowering.ts` (1369–2171), `WitnessLowering.ts` (2918–3371), `LowerExpression.ts` (3371–5691, with `LowerBuiltin.ts` 5017–5687), `CleanupEmission.ts` (5693–6338), `LowerStatements.ts` (6339–7520), `EntryAssembly.ts` (7520–8206). `FunctionLowering` moves to its own state module. +- **`lowerProvidedEffect(fn, recipe, success, span, requirements)`**: one helper owning the BeginLoan + borrow + loanLocals.delete + dropOwnedProvider choreography; the four near-identical 90-line blocks delegate to it. +- **End-loan loops**: route `finishBuiltin`, `Call`, `BoundOperationCall`, the Run recipe, and the Os path through `endLoans`/`endRunLoans`; make `finishBuiltin` dedupe via a `Set` of ended keys and drop the missing-loan `continue` vs `return undefined` disagreement. +- **`CleanupPlan.ts`**: `CleanupPlan` type + `cleanupPlan`/`specializeCleanup`/`cleanupFields`/`cleanupTypeAtPath`/`realizedCallableCleanup`/`cleanupHas*`. +- **`OwnershipEncoding.ts`**: `encode` + the five private formatters (spanText/identityLabel/verdictText/siteText/cleanupText). +- **`inReleaseOrder`**: shared helper; `SuspensionOwnership`/Lower`` import it; `SuspensionOwnership` keeps delegating to `Ownership.cleanupPlan`. + +## Risks / Trade-offs + +- [Correctness-critical borrow choreography] → after dedup, run the ownership/lowering and wasm/native suspension determinism tests and the fail-ordinal sweep. +- [Large code motion] → one actor per commit, typecheck+test after each. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test` (ownership/lowering/suspension suites incl. boundary points). diff --git a/openspec/changes/split-compiler-mid-end/proposal.md b/openspec/changes/split-compiler-mid-end/proposal.md new file mode 100644 index 00000000..28d93bef --- /dev/null +++ b/openspec/changes/split-compiler-mid-end/proposal.md @@ -0,0 +1,24 @@ +## Why + +`Lower.ts` is an 8,581-line god-module mixing ~10 lowering concerns with a 2,300-line per-expression switch; its provider-loan and end-loan choreography is duplicated four-to-five times and has already diverged on when loans end. `Ownership.ts` (3,453 lines) embeds a test-facing codec and a self-contained CleanupPlan sub-concept. `SuspensionOwnership.ts` duplicates `Ownership`'s release-order and set logic. + +## What Changes + +- **Split `Lower.ts`** into `Forwarding`, `ValueType`, `EffectLowering`, `WitnessLowering`, `LowerExpression` (+ `LowerBuiltin` for the intrinsic chain), `CleanupEmission`, `LowerStatements`, and `EntryAssembly`, driven by a shared `FunctionLowering` state module. +- **Deduplicate provider-loan lowering** into one `lowerProvidedEffect` and the five inline end-loan loops into `endLoans`/`endRunLoans`. +- **Extract `Ownership`'s CleanupPlan** sub-concept into `CleanupPlan.ts` and its deterministic codec into `OwnershipEncoding.ts`. +- **Share `inReleaseOrder`** across `Ownership`/`SuspensionOwnership`/`Lower` and route `SuspensionOwnership` through `Ownership.cleanupPlan` (already partially done). + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +Pure refactor of the HIR→MIR middle end. Borrow identities, loan-end ordering, and release ordering must stay byte-identical; golden/determinism suites are the net. `skip_specs: true`. diff --git a/openspec/changes/split-compiler-mid-end/tasks.md b/openspec/changes/split-compiler-mid-end/tasks.md new file mode 100644 index 00000000..ba19013e --- /dev/null +++ b/openspec/changes/split-compiler-mid-end/tasks.md @@ -0,0 +1,25 @@ +## 1. Split Lower.ts + +- [ ] 1.1 Move `FunctionLowering` into its own state module +- [ ] 1.2 Extract `Forwarding.ts` and `ValueType.ts` +- [ ] 1.3 Extract `EffectLowering.ts` and `WitnessLowering.ts` +- [ ] 1.4 Extract `LowerExpression.ts` + `LowerBuiltin.ts` +- [ ] 1.5 Extract `CleanupEmission.ts`, `LowerStatements.ts`, `EntryAssembly.ts` +- [ ] 1.6 Verify lowering tests pass + +## 2. Dedup loan choreography + +- [ ] 2.1 Add `lowerProvidedEffect` and replace the four provider-loan copies +- [ ] 2.2 Route the five inline end-loan loops through `endLoans`/`endRunLoans` +- [ ] 2.3 Verify borrow-identity/loan-end determinism tests pass + +## 3. Ownership factoring + +- [ ] 3.1 Extract `CleanupPlan.ts` and `OwnershipEncoding.ts` +- [ ] 3.2 Share `inReleaseOrder` and route `SuspensionOwnership` through `Ownership.cleanupPlan` + +## 4. Verification + +- [ ] 4.1 Run `pnpm typecheck` and verify clean +- [ ] 4.2 Run `pnpm exec biome check .` and verify clean +- [ ] 4.3 Run `pnpm test` diff --git a/openspec/changes/split-compiler-type-system/.openspec.yaml b/openspec/changes/split-compiler-type-system/.openspec.yaml new file mode 100644 index 00000000..f9194030 --- /dev/null +++ b/openspec/changes/split-compiler-type-system/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-08-21 diff --git a/openspec/changes/split-compiler-type-system/design.md b/openspec/changes/split-compiler-type-system/design.md new file mode 100644 index 00000000..a3d20f85 --- /dev/null +++ b/openspec/changes/split-compiler-type-system/design.md @@ -0,0 +1,18 @@ +## Context + +See proposal.md. Each split follows a seam already present in the code; the phase entry points (`instances`, `discover`, `frontend`, `realize`, `prepare`) stay put and callers are unchanged. + +## Decisions + +- **`Type.ts` → `TypeInference.ts`**: move `GenericArgumentConflict` through `substitution` (bind, infer, row/failure/requirement argument inference, `prefixSubstitution`) as one self-contained actor that only reads `Type` predicates. `Type.ts` keeps the vocabulary, keys/encoding, row policies, and re-exports `infer`/`inferOpenGenericArguments`. +- **`Instances.ts` split**: keep `discover`/`resolveEntry`/`specialize`/`keyOf`/`concreteConstraintEvidence` in `Instances`; move `copyDropViolations`/`requirementBindingViolations`/`unlowerableWitnessViolations`/`storedExecutableViolations`/`violationDiagnostics` to `InstanceDiagnostics.ts`; move `callableOriginOf`/`effectOriginOf`/`resultEffectIdentity`/`concreteCallables`/`concreteEffects`/`suspensionGraph`/`suspendableNodes`/`reachableIntrinsics`/`interfaceWitnessTargets`/`callTargets` to `ExecutableOrigin.ts`. +- **`Pipeline.ts` split**: `realize` (Analysis-facing) and `prepare` (Driver-facing) share one `discoverAndLower(self, options)` returning `{ instances, diagnostics, target, layout, mir, report }`; each public entry maps that result through its own gates. `frontend` and `frontendProject` move to their own modules with the reuse machinery (`ProjectReuseBasis`, `checkpointModuleBatch`). + +## Risks / Trade-offs + +- [Huge code motion] → do each actor extraction as its own commit; run `pnpm typecheck`/`pnpm test` after every step. +- [Circular imports] → `Instances` ↔ `InstanceDiagnostics`/ExecutableOrigin may share types; break cycles at type-only level. + +## Validation + +`pnpm typecheck`, `pnpm exec biome check .`, `pnpm test`. diff --git a/openspec/changes/split-compiler-type-system/proposal.md b/openspec/changes/split-compiler-type-system/proposal.md new file mode 100644 index 00000000..1a5a81a9 --- /dev/null +++ b/openspec/changes/split-compiler-type-system/proposal.md @@ -0,0 +1,23 @@ +## Why + +Three front/middle-end modules mix several concerns and, in `Pipeline`'s case, duplicate an entire lowering pipeline. `Type.ts` (3,367 lines) carries the full constraint-based generic-inference engine inside the data module; `Instances.ts` (3,297 lines) mixes instance discovery, diagnostic-violation checks, executable-origin recovery, and suspension analysis in one closure; `Pipeline.ts` (887 lines) hosts three interleaved lifecycles plus two near-identical `realize`/`prepare` lowering pipelines that are already drifting. + +## What Changes + +- **Extract `TypeInference.ts`** from `Type.ts` (lines 2806–3367); `Type` re-exports `infer`/`inferOpenGenericArguments`. +- **Split `Instances.ts`** into `Instances` (discovery worklist + `specialize`/`keyOf`), `InstanceDiagnostics` (the five `*Violations` + `storedRepresentation`/`representedNominals`), and `ExecutableOrigin` (`*OriginOf`, `concreteCallables`/`concreteEffects`, `suspensionGraph`, reachability/witness targets). +- **Split `Pipeline.ts`** into `Frontend`, an incremental-reuse module, and `Realization`; extract one shared `discoverAndLower` so `realize`/`prepare` become thin gate/error mappers instead of duplicate pipelines. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Impact + +No observable behavior change. Touches `Type.ts`, `Instances.ts`, `Pipeline.ts`; the split actors are internal imports. `realize`/`prepare` must keep their existing gate/error semantics byte-for-byte (only the shared skeleton is de-duplicated). `skip_specs: true`. diff --git a/openspec/changes/split-compiler-type-system/tasks.md b/openspec/changes/split-compiler-type-system/tasks.md new file mode 100644 index 00000000..558265cc --- /dev/null +++ b/openspec/changes/split-compiler-type-system/tasks.md @@ -0,0 +1,22 @@ +## 1. Type inference extraction + +- [ ] 1.1 Extract `TypeInference.ts` from `Type.ts:2806-3367` and re-export `infer`/`inferOpenGenericArguments` +- [ ] 1.2 Verify generic-inference and type tests pass + +## 2. Instances split + +- [ ] 2.1 Extract `InstanceDiagnostics.ts` (the five violation checks) +- [ ] 2.2 Extract `ExecutableOrigin.ts` (origin, callables, effects, suspension, reachability) +- [ ] 2.3 Verify instance/discovery and conformance tests pass + +## 3. Pipeline split and realize/prepare dedup + +- [ ] 3.1 Extract `discoverAndLower` and make `realize`/`prepare` thin mappers over it +- [ ] 3.2 Split `Pipeline.ts` into `Frontend`, incremental-reuse, and `Realization` modules +- [ ] 3.3 Verify the full driver/analysis pipeline tests pass, including the gate semantics + +## 4. Verification + +- [ ] 4.1 Run `pnpm typecheck` and verify clean +- [ ] 4.2 Run `pnpm exec biome check .` and verify clean +- [ ] 4.3 Run `pnpm test` diff --git a/packages/compiler/src/Analysis.ts b/packages/compiler/src/Analysis.ts index 4fa91bd0..3ffc8db3 100644 --- a/packages/compiler/src/Analysis.ts +++ b/packages/compiler/src/Analysis.ts @@ -11,7 +11,7 @@ import * as Completion from './Completion.js' import * as DeclarationIndex from './DeclarationIndex.js' import * as Diagnostic from './Diagnostic.js' import * as DocBlock from './DocBlock.js' -import * as Elaboration from './Elaboration.js' +import type * as Elaboration from './Elaboration.js' import * as FrontendTooling from './FrontendTooling.js' import * as Hir from './Hir.js' import type * as Instances from './Instances.js' @@ -22,7 +22,7 @@ import * as Mir from './Mir.js' import type * as ModuleClosure from './ModuleClosure.js' import type * as ModuleSemantics from './ModuleSemantics.js' import type * as ModuleSurface from './ModuleSurface.js' -import type * as ModuleTooling from './ModuleTooling.js' +import * as ModuleTooling from './ModuleTooling.js' import * as NameResolution from './NameResolution.js' import * as OpaqueRealization from './OpaqueRealization.js' import type * as Ownership from './Ownership.js' @@ -97,8 +97,8 @@ export interface Snapshot extends SingleRootFrontendSnapshot { /** One available anonymous expression type cached for position fallback. */ export interface AnonymousExpression { - readonly span: FrontendTooling.AnonymousExpression['span'] - readonly type: FrontendTooling.AnonymousExpression['type'] + readonly span: ModuleTooling.AnonymousExpression['span'] + readonly type: ModuleTooling.AnonymousExpression['type'] } /** The occurrence-first semantic subject selected for a hover request. */ @@ -528,7 +528,7 @@ const presentationOfIdentity = ( for (const result of self.results.values()) for (const fn of result.functions) for (const statement of fn.statements) - for (const expression of FrontendTooling.statementExpressions(statement)) + for (const expression of ModuleTooling.statementExpressions(statement)) if (expression._tag === 'Match') for (const arm of expression.arms) for (const binding of arm.bindings) @@ -732,8 +732,7 @@ export const expressionsOf = ( Object.freeze( self.results .get(module) - ?.functions.flatMap((fn) => fn.statements.flatMap(FrontendTooling.statementExpressions)) ?? - [], + ?.functions.flatMap((fn) => fn.statements.flatMap(ModuleTooling.statementExpressions)) ?? [], ) /** Returns every retained semantic match with source patterns and canonical coverage facts. */ @@ -748,18 +747,6 @@ export const matchesOf = ( ), ) -/** Returns every retained struct literal fact without reconstructing field mappings. */ -export const structLiteralsOf = ( - self: FrontendSnapshot, - module: string, -): ReadonlyArray => - Object.freeze( - expressionsOf(self, module).filter( - (expression): expression is Elaboration.StructLiteralExpressionFact => - expression._tag === 'StructLiteral', - ), - ) - /** Returns every canonical or explicitly unavailable field-projection step. */ export const fieldProjectionsOf = ( self: FrontendSnapshot, @@ -926,16 +913,6 @@ export const instancesOfCall = ( }), ) -/** Returns concrete reached applications while excluding open generic declarations. */ -export const appliedLayoutsOf = (self: Snapshot): ReadonlyArray => - self.layout._tag === 'Available' - ? Object.freeze( - self.layout.value.entries.filter( - (entry) => Type.isNominal(entry.type) && entry.type.arguments.length > 0, - ), - ) - : Object.freeze([]) - /** Returns concrete instances proven able to reach the suspension intrinsic. */ export const suspendableInstancesOf = (self: Snapshot): ReadonlyArray => self.instances.suspendable @@ -995,14 +972,6 @@ export const repeatedLayoutsOf = (self: Snapshot): ReadonlyArray = ) : Object.freeze([]) -/** Returns every reachable compiler-owned structural-union layout. */ -export const unionLayoutsOf = (self: Snapshot): ReadonlyArray => - self.layout._tag === 'Available' - ? Object.freeze( - self.layout.value.entries.filter((entry) => entry.representation._tag === 'Union'), - ) - : Object.freeze([]) - /** Returns every reachable array calling shape and its canonical physical paths. */ export const arrayCallingShapesOf = (self: Snapshot): ReadonlyArray => self.layout._tag === 'Available' @@ -1011,26 +980,6 @@ export const arrayCallingShapesOf = (self: Snapshot): ReadonlyArray => - self.layout._tag === 'Available' - ? Object.freeze(self.layout.value.callingShapes.filter((shape) => Type.isUnion(shape.type))) - : Object.freeze([]) - -/** Returns every explicit HIR union conversion in source semantic order. */ -export const hirUnionConversionsOf = ( - self: FrontendSnapshot, - module: string, -): ReadonlyArray> => - Object.freeze( - (self.results.get(module)?.hir.functions ?? []).flatMap((fn) => - fn.statements - .flatMap(Hir.statementExpressions) - .flatMap(Hir.expressionTree) - .flatMap((expression) => (expression._tag === 'UnionConvert' ? [expression] : [])), - ), - ) - /** Returns every typed structured HIR match in deterministic expression preorder. */ export const hirMatchesOf = ( self: FrontendSnapshot, @@ -1052,20 +1001,6 @@ export const ownershipMatchesOf = ( ): ReadonlyArray => Object.freeze(self.ownership.get(module)?.functions.flatMap((fn) => fn.matches) ?? []) -/** Returns every verified MIR union conversion in canonical topological order. */ -export const mirUnionConversionsOf = ( - self: Snapshot, -): ReadonlyArray> => - self.mir._tag === 'Unavailable' - ? Object.freeze([]) - : Object.freeze( - self.mir.value.functions.flatMap((fn) => - Mir.operations(fn).flatMap((operation) => - operation._tag === 'ConvertUnion' ? [operation] : [], - ), - ), - ) - /** Returns every structured MIR match, including nested expression matches, in DAG preorder. */ export const mirMatchesOf = ( self: Snapshot, @@ -1161,18 +1096,6 @@ export const fieldByName = ( spelling: string, ): DeclarationIndex.FieldLookup => DeclarationIndex.lookupField(declaration.fields, spelling) -/** Looks up one declaration name within one module's elaborated analysis. */ -export const declarationLookup = ( - result: Elaboration.Result, - spelling: string, -): DeclarationIndex.DeclarationLookup => Elaboration.declarationByName(result, spelling) - -/** Looks up one parameter name within one declaration's collected parameters. */ -export const parameterLookup = ( - declaration: DeclarationIndex.DeclarationFact, - spelling: string, -): DeclarationIndex.ParameterLookup => Elaboration.parameterByName(declaration, spelling) - /** The compilation's complete diagnostic sequence in deterministic driver order. */ export const diagnostics = (self: FrontendSnapshot): ReadonlyArray => self.diagnostics @@ -1286,17 +1209,6 @@ export const arrayTraceEventsOf = ( ), ) -/** Returns logical injection and widening events without exposing physical backend tags. */ -export const unionTraceEventsOf = ( - outcome: BootstrapEvaluation.Outcome, -): ReadonlyArray => - Object.freeze( - outcome.trace.filter( - (event): event is BootstrapEvaluation.UnionConversionTraceEvent => - event._tag === 'UnionConversion', - ), - ) - /** Returns logical allocation and typed-storage events without exposing host addresses. */ export const allocationTraceEventsOf = ( outcome: BootstrapEvaluation.Outcome, diff --git a/packages/compiler/src/Backend.ts b/packages/compiler/src/Backend.ts index 419ac4a9..6217efac 100644 --- a/packages/compiler/src/Backend.ts +++ b/packages/compiler/src/Backend.ts @@ -303,7 +303,7 @@ export const symbolFor = (fn: Mir.MirFunction, entry: Instances.InstanceKey): st .map(injectivePart) .join('_')}` -const suspensionPointKey = (point: Mir.SuspensionPointId): string => +export const suspensionPointKey = (point: Mir.SuspensionPointId): string => `${Instances.keyText(point.owner)}\u0000${point.sourceId}\u0000${point.spanStart}\u0000${point.spanEnd}\u0000${point.ordinal}` interface LineTable { diff --git a/packages/compiler/src/BootstrapEvaluation.ts b/packages/compiler/src/BootstrapEvaluation.ts index 317172f0..6d95edb8 100644 --- a/packages/compiler/src/BootstrapEvaluation.ts +++ b/packages/compiler/src/BootstrapEvaluation.ts @@ -2912,10 +2912,11 @@ function* executeFunction( if (state.standardStreams === undefined) return undefined try { return state.standardStreams.writeAll(destination, bytes) - } catch { + } catch (cause) { return Object.freeze({ _tag: 'WriteFailure' as const, message: 'standard stream provider threw', + cause, }) } })() @@ -3339,8 +3340,12 @@ function* executeFunction( status() commit(integerValue('i32', 1)) } - } catch { - const failure: OsFileSystemHost.Failure = { _tag: 'Failure', reason: 'Other' } + } catch (cause) { + const failure: OsFileSystemHost.Failure = { + _tag: 'Failure', + reason: 'Other', + ...(cause instanceof Error ? { cause } : {}), + } as OsFileSystemHost.Failure status(failure) commit( operation.type._tag === 'Union' diff --git a/packages/compiler/src/CallableFieldRealization.ts b/packages/compiler/src/CallableFieldRealization.ts index 61682484..205df956 100644 --- a/packages/compiler/src/CallableFieldRealization.ts +++ b/packages/compiler/src/CallableFieldRealization.ts @@ -661,26 +661,6 @@ export const isCallableRealization = (self: Realization): self is CallableRealiz export const isEffectRealization = (self: Realization): self is EffectRealization => self._tag === 'EffectFieldRealization' -/** Returns only callable realizations, preserving the Effect storage fence in callable consumers. */ -export const callableRealizationOf = ( - self: Index, - instance: Type.Nominal, - id: RepresentationField.Id, -): CallableRealization | undefined => { - const realization = realizationOf(self, instance, id) - return realization !== undefined && isCallableRealization(realization) ? realization : undefined -} - -/** Returns only Effect realizations to consumers that are explicitly prepared for that variant. */ -export const effectRealizationOf = ( - self: Index, - instance: Type.Nominal, - id: RepresentationField.Id, -): EffectRealization | undefined => { - const realization = realizationOf(self, instance, id) - return realization !== undefined && isEffectRealization(realization) ? realization : undefined -} - /** True when every realized field of one complete instance has a runtime realization. */ export const supportsInstance = (self: Index, instance: Type.Nominal): boolean => { const entries = self.entries.filter((entry) => diff --git a/packages/compiler/src/CoroutineFrame.ts b/packages/compiler/src/CoroutineFrame.ts index 578930b0..039ce22b 100644 --- a/packages/compiler/src/CoroutineFrame.ts +++ b/packages/compiler/src/CoroutineFrame.ts @@ -1,11 +1,9 @@ import * as Instances from './Instances.js' +import { alignUp } from './internal/Align.js' import * as Layout from './Layout.js' import * as Mir from './Mir.js' import * as SilkType from './Type.js' -const alignUp = (offset: number, alignment: number): number => - Math.ceil(offset / alignment) * alignment - const pointKey = (point: Mir.SuspensionPointId): string => [ point.owner.declaration.module, diff --git a/packages/compiler/src/DeclarationIndex.ts b/packages/compiler/src/DeclarationIndex.ts index f7c7cfd4..978efe0c 100644 --- a/packages/compiler/src/DeclarationIndex.ts +++ b/packages/compiler/src/DeclarationIndex.ts @@ -7391,8 +7391,6 @@ const proveGoal = ( if (Type.isNominal(goal.provider)) { if (Type.equals(goal.provider, goal.capability)) return provedGoal(goal, Object.freeze({ _tag: 'IdentitySelection' as const }), [], []) - if (Type.intrinsicallyConforms(goal.provider, goal.capability)) - return provedGoal(goal, Object.freeze({ _tag: 'IntrinsicSelection' as const }), [], []) } const matching = conformanceCandidates(self, goal) const selected = matching.at(0) @@ -7716,13 +7714,6 @@ export const witness = ( if (Type.equals(provider, capability)) { return Object.freeze({ _tag: 'IdentityConformanceWitness', capability, provider }) } - if (Type.intrinsicallyConforms(provider, capability)) { - return Object.freeze({ - _tag: 'IntrinsicConformanceWitness', - capability, - provider, - }) - } // Proof selection is the single authority for matching both the provider and capability heads. // Repeating only provider inference here would lose capability binders and could select a header // whose requirements failed. @@ -7877,14 +7868,11 @@ export const witnessOperation = ( ): CanonicalId | undefined => self.operations.find((operation) => operation.name === name)?.implementation -const presentParameterNameEntries = (parameters: ReadonlyArray) => - presentParameterEntries(parameters) - export const lookupParameter = ( parameters: ReadonlyArray, name: string, ): ParameterLookup => { - const matches = presentParameterNameEntries(parameters) + const matches = presentParameterEntries(parameters) .filter((entry) => entry.spelling === name) .map((entry) => entry.parameter) const first = matches.at(0) diff --git a/packages/compiler/src/FloatingPoint.ts b/packages/compiler/src/FloatingPoint.ts index dfab3a68..524b9013 100644 --- a/packages/compiler/src/FloatingPoint.ts +++ b/packages/compiler/src/FloatingPoint.ts @@ -117,10 +117,13 @@ export const toNumber = (value: Bits): number => { return view.getFloat64(0, true) } +/** The single canonical quiet NaN bit pattern for the given width. */ +export const canonicalNaN = (width: 32 | 64): bigint => + width === 32 ? 0x7fc00000n : 0x7ff8000000000000n + /** Encodes a host arithmetic result, canonicalizing NaNs as required by Silk evaluation. */ export const fromNumber = (value: number, width: 32 | 64): Bits => { - if (Number.isNaN(value)) - return Object.freeze({ width, bits: width === 32 ? 0x7fc00000n : 0x7ff8000000000000n }) + if (Number.isNaN(value)) return Object.freeze({ width, bits: canonicalNaN(width) }) if (width === 32) { view.setFloat32(0, Math.fround(value), true) return Object.freeze({ width, bits: BigInt(view.getUint32(0, true)) }) @@ -183,18 +186,18 @@ const integerSquareRoot = (value: bigint): bigint => { */ export const squareRoot = (value: Bits): Bits => { const spec = specification(value.width) - const canonicalNaN = value.width === 32 ? 0x7fc00000n : 0x7ff8000000000000n + const canonicalNaNValue = canonicalNaN(value.width) const exponentField = (value.bits >> BigInt(spec.precision - 1)) & ((1n << BigInt(spec.exponentBits)) - 1n) const fractionField = value.bits & ((1n << BigInt(spec.precision - 1)) - 1n) const negative = isSignNegative(value) if (exponentField === (1n << BigInt(spec.exponentBits)) - 1n) return fractionField !== 0n || negative - ? Object.freeze({ width: value.width, bits: canonicalNaN }) + ? Object.freeze({ width: value.width, bits: canonicalNaNValue }) : value // IEEE-754 keeps the sign of a zero root, so -0 squares back to -0. if (exponentField === 0n && fractionField === 0n) return value - if (negative) return Object.freeze({ width: value.width, bits: canonicalNaN }) + if (negative) return Object.freeze({ width: value.width, bits: canonicalNaNValue }) const significand = exponentField === 0n ? fractionField : fractionField | (1n << BigInt(spec.precision - 1)) diff --git a/packages/compiler/src/FrontendTooling.ts b/packages/compiler/src/FrontendTooling.ts index 85805528..2a48fa47 100644 --- a/packages/compiler/src/FrontendTooling.ts +++ b/packages/compiler/src/FrontendTooling.ts @@ -6,15 +6,14 @@ import type * as NameResolution from './NameResolution.js' import * as PhaseReport from './PhaseReport.js' import * as SemanticOccurrence from './SemanticOccurrence.js' -export type AnonymousExpression = ModuleTooling.AnonymousExpression - -export const statementExpressions = ModuleTooling.statementExpressions - /** Tooling indexes, reusable module artifacts, and observations for one completed frontend. */ export interface FrontendTooling { readonly toolingModules: ReadonlyMap readonly semanticOccurrences: SemanticOccurrence.Index - readonly anonymousExpressions: ReadonlyMap> + readonly anonymousExpressions: ReadonlyMap< + string, + ReadonlyArray + > readonly report: ReadonlyArray } diff --git a/packages/compiler/src/Hir.ts b/packages/compiler/src/Hir.ts index ae81948c..aa9ad3d5 100644 --- a/packages/compiler/src/Hir.ts +++ b/packages/compiler/src/Hir.ts @@ -1005,66 +1005,6 @@ export const expressionTree = (expression: Expression): ReadonlyArray { - const walk = (expression: Expression): boolean => { - switch (expression._tag) { - case 'Unavailable': - return true - case 'Move': - case 'Project': - return walk(expression.subject) - case 'RuntimeStringView': - return walk(expression.source) - case 'StringEquality': - case 'ShortCircuit': - return walk(expression.left) || walk(expression.right) - case 'Replace': - return walk(expression.value) - case 'UnionConvert': - return walk(expression.source) - case 'IndexPlace': - return walk(expression.subject) || walk(expression.index) - case 'SliceLength': - return walk(expression.slice) - case 'SliceIndexPlace': - return walk(expression.slice) || walk(expression.index) - case 'Construct': - return expression.fields.some((field) => walk(field.value)) - case 'ArrayConstruct': - return expression.elements.some(walk) - case 'Call': - case 'EffectConstruct': - case 'ServiceEffectConstruct': - case 'BuiltinCall': - case 'BoundOperationCall': - return expression.arguments.some(walk) - case 'CallableSection': - return expression.captures.some((capture) => walk(capture.value)) - case 'CallableApply': - return walk(expression.callee) || expression.arguments.some(walk) - case 'Run': - return walk(expression.subject) - case 'EffectResult': - return walk(expression.protected) - case 'EffectBindRequirement': - return walk(expression.protected) - case 'EffectCatch': - return walk(expression.protected) || walk(expression.handler) - case 'Match': - return ( - walk(expression.scrutinee) || - expression.arms.some( - (arm) => (arm.guard !== undefined && walk(arm.guard)) || walk(arm.result), - ) - ) - default: - return false - } - } - return self.statements.flatMap(statementExpressions).some(walk) -} - /** The first unavailable expression's cause and span, if the body has one. */ export const firstUnavailable = ( self: HirFunction, diff --git a/packages/compiler/src/ImportPath.ts b/packages/compiler/src/ImportPath.ts index 550e68ac..a95c154e 100644 --- a/packages/compiler/src/ImportPath.ts +++ b/packages/compiler/src/ImportPath.ts @@ -1,3 +1,4 @@ +import type * as SourceFile from './SourceFile.js' import type * as SyntaxTree from './SyntaxTree.js' import type * as Token from './Token.js' @@ -47,3 +48,28 @@ export const segments = (self: SyntaxTree.Node): ReadonlyArray => (element): element is Token.Token => element._tag === 'Token' && isSegmentKind(element.kind), ), ) + +const decoder = new TextDecoder() + +const tokenText = (source: SourceFile.SourceFile, token: Token.Token): string => + decoder.decode(Uint8Array.from(source.bytes.slice(token.span.start, token.span.end))) + +/** Renders the import path in source-spelling form (segments joined by "/"). */ +export const spelling = ( + source: SourceFile.SourceFile, + path: SyntaxTree.Node, +): string | undefined => { + const pathSegments = segments(path) + if (pathSegments.length === 0) return undefined + return pathSegments.map((segment) => tokenText(source, segment)).join('/') +} + +/** Renders the import path in canonical form (segments joined by "."). */ +export const canonicalTarget = ( + source: SourceFile.SourceFile, + path: SyntaxTree.Node, +): string | undefined => { + const pathSegments = segments(path) + if (pathSegments.length === 0) return undefined + return pathSegments.map((segment) => tokenText(source, segment)).join('.') +} diff --git a/packages/compiler/src/ImportPlan.ts b/packages/compiler/src/ImportPlan.ts index 9a745d5c..8d216ed1 100644 --- a/packages/compiler/src/ImportPlan.ts +++ b/packages/compiler/src/ImportPlan.ts @@ -40,9 +40,7 @@ const pathOf = ( ): string | undefined => { const path = SyntaxTree.directNode(declaration, 'ImportPath') if (path === undefined || !SyntaxTree.isAvailableSyntax(path)) return undefined - const names = ImportPath.segments(path) - if (names.length === 0) return undefined - return names.map((name) => text(source, name)).join('/') + return ImportPath.spelling(source, path) } const importedSpellings = ( diff --git a/packages/compiler/src/InterfaceWitnessCompatibility.ts b/packages/compiler/src/InterfaceWitnessCompatibility.ts index de9032e7..23d8b9ac 100644 --- a/packages/compiler/src/InterfaceWitnessCompatibility.ts +++ b/packages/compiler/src/InterfaceWitnessCompatibility.ts @@ -81,9 +81,6 @@ const operandShape = (type: Type.Type): OperandShape => { return Object.freeze({ _tag: 'Value', access: 'Take', type }) } -const accessRank = (access: Access): number => - access === 'Shared' ? 0 : access === 'Exclusive' ? 1 : 2 - const incompatible = (problem: Problem): Compatibility => Object.freeze({ _tag: 'Incompatible', problem }) @@ -95,7 +92,7 @@ const operandProblem = ( const promised = operandShape(contract.type) const required = operandShape(witness.type) if (promised._tag === required._tag) { - if (accessRank(required.access) > accessRank(promised.access)) + if (Type.accessRank(required.access) > Type.accessRank(promised.access)) return Object.freeze({ _tag: 'StrongerOperandAccess', ordinal, @@ -114,7 +111,7 @@ const operandProblem = ( }) return undefined } - if (accessRank(required.access) > accessRank(promised.access)) + if (Type.accessRank(required.access) > Type.accessRank(promised.access)) return Object.freeze({ _tag: 'StrongerOperandAccess', ordinal, @@ -178,8 +175,7 @@ export const check = (contract: Contract, witness: Witness): Compatibility => { Type.equals(requirement.capability, allowed.capability) && requirement.role === allowed.role, ) - if (matching.some((allowed) => accessRank(requirement.access) <= accessRank(allowed.access))) - continue + if (matching.some((allowed) => Type.compareAccess(requirement.access, allowed.access))) continue if (matching.length > 0) return incompatible( Object.freeze({ diff --git a/packages/compiler/src/IntrinsicAvailability.ts b/packages/compiler/src/IntrinsicAvailability.ts index 0acecd30..370b3f30 100644 --- a/packages/compiler/src/IntrinsicAvailability.ts +++ b/packages/compiler/src/IntrinsicAvailability.ts @@ -69,15 +69,3 @@ export const select = ( }), }) } - -/** Deterministic textual encoding for inventory tests, manifests, and diagnostics fixtures. */ -export const encode = (self: Inventory): string => - [ - `target ${self.target}`, - ...self.operations.map((operation) => `intrinsic ${operationKey(operation)}`), - ...self.calls.map( - (call) => - `call ${operationKey(call.operation)} ${call.span.sourceId}:${call.span.start}:${call.span.end}`, - ), - '', - ].join('\n') diff --git a/packages/compiler/src/Layout.ts b/packages/compiler/src/Layout.ts index dc3c1190..6065dd2d 100644 --- a/packages/compiler/src/Layout.ts +++ b/packages/compiler/src/Layout.ts @@ -3,6 +3,7 @@ import * as DeclarationIndex from './DeclarationIndex.js' import * as Diagnostic from './Diagnostic.js' import * as Hir from './Hir.js' import * as Instances from './Instances.js' +import { alignUp } from './internal/Align.js' import * as OpaqueRealization from './OpaqueRealization.js' import * as Ownership from './Ownership.js' import * as RepresentationField from './RepresentationField.js' @@ -495,9 +496,6 @@ const scalarEntry = (target: Target.Target, type: Type.Builtin): Entry => { }) } -const alignUp = (offset: number, alignment: number): number => - Math.ceil(offset / alignment) * alignment - const repeatedEntry = (type: Type.FixedArray, element: Entry): Entry | undefined => { const stride = alignUp(element.size, element.alignment) const size = stride * type.length diff --git a/packages/compiler/src/Lexer.ts b/packages/compiler/src/Lexer.ts index 7360563b..3929450b 100644 --- a/packages/compiler/src/Lexer.ts +++ b/packages/compiler/src/Lexer.ts @@ -1,5 +1,6 @@ import * as Option from 'effect/Option' import * as Diagnostic from './Diagnostic.js' +import * as ByteClass from './internal/ByteClass.js' import * as DigitSeparator from './internal/DigitSeparator.js' import * as IntegerLiteral from './internal/IntegerLiteral.js' import * as LiteralForm from './LiteralForm.js' @@ -17,18 +18,6 @@ export interface LexicalResult { const isWhitespace = (byte: number | undefined): boolean => byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d -const isAsciiLetter = (byte: number | undefined): boolean => - byte !== undefined && ((byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a)) - -const isDecimalDigit = (byte: number | undefined): boolean => - byte !== undefined && byte >= 0x30 && byte <= 0x39 - -const isIdentifierStart = (byte: number | undefined): boolean => - byte === 0x5f || isAsciiLetter(byte) - -const isIdentifierContinue = (byte: number | undefined): boolean => - isIdentifierStart(byte) || isDecimalDigit(byte) - const isLineCommentStart = (bytes: ReadonlyArray, index: number): boolean => bytes[index] === 0x2f && bytes[index + 1] === 0x2f @@ -85,8 +74,8 @@ const isSupportedTokenStart = (bytes: ReadonlyArray, index: number): boo const byte = bytes[index] return ( isWhitespace(byte) || - isIdentifierStart(byte) || - isDecimalDigit(byte) || + ByteClass.isIdentifierStart(byte) || + ByteClass.isDecimalDigit(byte) || isLiteralStart(bytes, index) || isLineCommentStart(bytes, index) || compoundPunctuationKind(bytes, index) !== undefined || @@ -96,6 +85,12 @@ const isSupportedTokenStart = (bytes: ReadonlyArray, index: number): boo const keywordSpellings: ReadonlyArray = Object.freeze([ ['as', 'AsKeyword'], + ['fn', 'FnKeyword'], + ['let', 'LetKeyword'], + ['move', 'MoveKeyword'], + ['pub', 'PubKeyword'], + ['return', 'ReturnKeyword'], + ['import', 'ImportKeyword'], ['run', 'RunKeyword'], ['fail', 'FailKeyword'], ['drop', 'DropKeyword'], @@ -134,59 +129,9 @@ const matchesSpelling = ( } const keywordKind = (bytes: ReadonlyArray, start: number, end: number): Token.TokenKind => { - if (end - start === 2 && bytes[start] === 0x66 && bytes[start + 1] === 0x6e) { - return 'FnKeyword' - } for (const [spelling, kind] of keywordSpellings) { if (matchesSpelling(bytes, start, end, spelling)) return kind } - if ( - end - start === 3 && - bytes[start] === 0x6c && - bytes[start + 1] === 0x65 && - bytes[start + 2] === 0x74 - ) { - return 'LetKeyword' - } - if ( - end - start === 4 && - bytes[start] === 0x6d && - bytes[start + 1] === 0x6f && - bytes[start + 2] === 0x76 && - bytes[start + 3] === 0x65 - ) { - return 'MoveKeyword' - } - if ( - end - start === 3 && - bytes[start] === 0x70 && - bytes[start + 1] === 0x75 && - bytes[start + 2] === 0x62 - ) { - return 'PubKeyword' - } - if ( - end - start === 6 && - bytes[start] === 0x72 && - bytes[start + 1] === 0x65 && - bytes[start + 2] === 0x74 && - bytes[start + 3] === 0x75 && - bytes[start + 4] === 0x72 && - bytes[start + 5] === 0x6e - ) { - return 'ReturnKeyword' - } - if ( - end - start === 6 && - bytes[start] === 0x69 && - bytes[start + 1] === 0x6d && - bytes[start + 2] === 0x70 && - bytes[start + 3] === 0x6f && - bytes[start + 4] === 0x72 && - bytes[start + 5] === 0x74 - ) { - return 'ImportKeyword' - } return 'Identifier' } @@ -376,14 +321,14 @@ export const lex = (source: SourceFile.SourceFile): LexicalResult => { continue } - if (isIdentifierStart(byte)) { + if (ByteClass.isIdentifierStart(byte)) { index += 1 - while (index < bytes.length && isIdentifierContinue(bytes[index])) index += 1 + while (index < bytes.length && ByteClass.isIdentifierContinue(bytes[index])) index += 1 pushToken(keywordKind(bytes, start, index), start, index) continue } - if (isDecimalDigit(byte)) { + if (ByteClass.isDecimalDigit(byte)) { const base = IntegerLiteral.recognize(bytes, index) if (base.radix !== 10) { index += base.width diff --git a/packages/compiler/src/LiteralForm.ts b/packages/compiler/src/LiteralForm.ts index 65422bef..bcd1162f 100644 --- a/packages/compiler/src/LiteralForm.ts +++ b/packages/compiler/src/LiteralForm.ts @@ -1,3 +1,4 @@ +import * as ByteClass from './internal/ByteClass.js' import type * as Token from './Token.js' /** The semantic category selected by a static-literal modifier and delimiter. */ @@ -71,18 +72,6 @@ const matches = (bytes: ByteSequence, index: number, form: LiteralForm): boolean export const recognize = (bytes: ByteSequence, index = 0): LiteralForm | undefined => forms.find((form) => matches(bytes, index, form)) -const isAsciiLetter = (byte: number | undefined): boolean => - byte !== undefined && ((byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a)) - -const isDecimalDigit = (byte: number | undefined): boolean => - byte !== undefined && byte >= 0x30 && byte <= 0x39 - -const isIdentifierStart = (byte: number | undefined): boolean => - byte === 0x5f || isAsciiLetter(byte) - -const isIdentifierContinue = (byte: number | undefined): boolean => - isIdentifierStart(byte) || isDecimalDigit(byte) - /** One identifier-like spelling reserved as an unrecognized adjacent literal modifier. */ export interface UnknownIntroduction { readonly modifier: string @@ -100,9 +89,9 @@ export const recognizeUnknown = ( bytes: ByteSequence, index = 0, ): UnknownIntroduction | undefined => { - if (!isIdentifierStart(bytes[index])) return undefined + if (!ByteClass.isIdentifierStart(bytes[index])) return undefined let cursor = index + 1 - while (cursor < bytes.length && isIdentifierContinue(bytes[cursor])) cursor += 1 + while (cursor < bytes.length && ByteClass.isIdentifierContinue(bytes[cursor])) cursor += 1 if (bytes[cursor] !== quote) return undefined const modifier = String.fromCharCode(...bytes.slice(index, cursor)) if (forms.some((form) => form.modifier === modifier)) return undefined @@ -155,38 +144,7 @@ export const scanBoundary = ( return Object.freeze({ end: bytes.length, terminated: false }) } -const isContinuation = (byte: number | undefined): boolean => - byte !== undefined && byte >= 0x80 && byte <= 0xbf - -/** - * Counts the Unicode scalars one escaped body denotes, without decoding any of them. - * - * The count is a scalar count and never a byte count: `é` is two UTF-8 bytes and one scalar, and - * one escape sequence denotes one scalar however it is spelled. Only the extent of an escape - * matters here, never its meaning, so the decoder in `StaticText` stays the single authority on - * what an escape produces and this rule stays valid for any escape it later accepts. - */ -export const scalarCount = (bytes: ByteSequence, contentStart: number, end: number): number => { - let index = contentStart - let scalars = 0 - while (index < end) { - scalars += 1 - if (bytes[index] !== 0x5c) { - index += 1 - while (index < end && isContinuation(bytes[index])) index += 1 - continue - } - const escaped = bytes[index + 1] - if (escaped === 0x75 && bytes[index + 2] === 0x7b) { - index += 3 - while (index < end && bytes[index] !== 0x7d) index += 1 - index += 1 - continue - } - index += escaped === 0x78 ? 4 : 2 - } - return scalars -} +export { scalarCount } from './internal/Escape.js' /** Returns the token kind selected by a valid form. */ export const tokenKind = (self: LiteralForm): Token.TokenKind => self.tokenKind diff --git a/packages/compiler/src/Mir.ts b/packages/compiler/src/Mir.ts index 39acdaa4..172759ee 100644 --- a/packages/compiler/src/Mir.ts +++ b/packages/compiler/src/Mir.ts @@ -1,4 +1,3 @@ -import * as Option from 'effect/Option' import * as CallableFieldRealization from './CallableFieldRealization.js' import type * as DeclarationIndex from './DeclarationIndex.js' import * as Hir from './Hir.js' @@ -1394,46 +1393,6 @@ export interface ControlEdge { readonly kind: 'Forward' | 'Taken' | 'Otherwise' | 'Following' | 'Condition' | 'Body' } -/** Target-neutral completion, relay, and resume edges owned by finalized suspension control. */ -export const suspensionControlEdges = (self: MirFunction): ReadonlyArray => - Object.freeze( - (self.suspension?.regions ?? []).flatMap((region) => - region._tag === 'SuspendEffectRegion' - ? [ - Object.freeze({ - _tag: 'SuspensionControlEdge' as const, - from: region.point, - to: Object.freeze({ _tag: 'RelayExit' as const }), - kind: 'RelayTransfer' as const, - }), - ] - : [ - Object.freeze({ - _tag: 'SuspensionControlEdge' as const, - from: region.point, - to: Object.freeze({ _tag: 'RelayExit' as const }), - kind: 'RelayTransfer' as const, - }), - ...(region.relay.state === undefined - ? [] - : [ - Object.freeze({ - _tag: 'SuspensionControlEdge' as const, - from: region.point, - to: region.relay.state.success.resume, - kind: 'ResumeSuccess' as const, - }), - Object.freeze({ - _tag: 'SuspensionControlEdge' as const, - from: region.point, - to: region.relay.state.failure.resume, - kind: 'ResumeFailure' as const, - }), - ]), - ], - ), - ) - /** Every local retained or referenced by finalized suspension control. */ export const suspensionLocals = (self: MirFunction): ReadonlyArray => Object.freeze( @@ -6547,11 +6506,11 @@ const sampleSpan = ( source: SourceFile.SourceFile, start: number, end: number, -): SourceSpan.SourceSpan => - Option.getOrThrowWith( - SourceSpan.make(source, start, end), - () => new RangeError('MIR sample produced an invalid span'), - ) +): SourceSpan.SourceSpan => { + const span = SourceSpan.make(source, start, end) + if (span._tag === 'None') throw new RangeError('MIR sample produced an invalid span') + return span.value +} const local = (ordinal: number): LocalId => Object.freeze({ _tag: 'Local', ordinal }) const region = (ordinal: number): RegionId => Object.freeze({ _tag: 'Region', ordinal }) const i32: Type = Object.freeze({ _tag: 'i32' }) diff --git a/packages/compiler/src/ModuleClosure.ts b/packages/compiler/src/ModuleClosure.ts index 35aa6ed8..3d33158d 100644 --- a/packages/compiler/src/ModuleClosure.ts +++ b/packages/compiler/src/ModuleClosure.ts @@ -128,14 +128,6 @@ const canonicalRoots = ( ) } -const spelling = (source: SourceFile.SourceFile, token: Token.Token): string => { - const bytes = Option.getOrThrowWith( - SourceFile.slice(source, token.span), - () => new RangeError(`Import token span does not belong to source ${source.id}`), - ) - return Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') -} - const unavailableSyntax = (parent: SyntaxTree.Node): SyntaxTree.Element => SyntaxTree.unavailableElement(parent.children, parent) @@ -169,12 +161,16 @@ const parseModule = ( const imports = syntax.root.children.flatMap((element): ParsedModule['imports'] => { if (!SyntaxTree.isNode(element) || element.kind !== 'ImportDeclaration') return [] const path = SyntaxTree.directNode(element, 'ImportPath') - const tokens = path === undefined ? [] : ImportPath.segments(path) - if (path === undefined || tokens.length === 0 || !SyntaxTree.isAvailableSyntax(path)) { + if (path === undefined || !SyntaxTree.isAvailableSyntax(path)) { return [Object.freeze({ syntax: element, path: path ?? element })] } - const sourceSpelling = tokens.map((token) => spelling(syntax.source, token)).join('.') - const canonicalTarget = tokens.map((token) => spelling(syntax.source, token)).join('/') + const canonical = ImportPath.spelling(syntax.source, path) + if (canonical === undefined) { + return [Object.freeze({ syntax: element, path })] + } + const sourceSpelling = canonical.split('/').join('.') + const canonicalTarget = canonical + const tokens = ImportPath.segments(path) const token = tokens.at(0) if (token === undefined) throw new RangeError('Available import path lost its first segment') return [Object.freeze({ syntax: element, path, sourceSpelling, canonicalTarget, token })] diff --git a/packages/compiler/src/ModuleSummary.ts b/packages/compiler/src/ModuleSummary.ts index bf07d405..11d4bbbc 100644 --- a/packages/compiler/src/ModuleSummary.ts +++ b/packages/compiler/src/ModuleSummary.ts @@ -58,9 +58,7 @@ const importModule = ( ): string | undefined => { const path = SyntaxTree.directNode(declaration, 'ImportPath') if (path === undefined || !SyntaxTree.isAvailableSyntax(path)) return undefined - const segments = ImportPath.segments(path) - if (segments.length === 0) return undefined - return segments.map((segment) => spelling(source, segment)).join('/') + return ImportPath.spelling(source, path) } interface PendingExport { diff --git a/packages/compiler/src/ModuleTooling.ts b/packages/compiler/src/ModuleTooling.ts index ca58f9ee..d0259c21 100644 --- a/packages/compiler/src/ModuleTooling.ts +++ b/packages/compiler/src/ModuleTooling.ts @@ -62,20 +62,6 @@ export const semanticOccurrenceIndex = ( ): SemanticOccurrence.ModuleIndex => SemanticOccurrence.makeModule(semantics.module, semantics.elaboration, index, resolution) -/** Builds one module's editor indexes from one closed semantic artifact. */ -export const make = ( - semantics: ModuleSemantics.ModuleSemantics, - index: DeclarationIndex.Index, - resolution: NameResolution.Resolution, -): ModuleTooling => - Object.freeze({ - _tag: 'ModuleTooling', - module: semantics.module, - semantics, - semanticOccurrences: semanticOccurrenceIndex(semantics, index, resolution), - anonymousExpressions: anonymousExpressionIndex(semantics), - }) - /** Closes already-built module indexes into one reusable tooling artifact. */ export const fromIndexes = ( semantics: ModuleSemantics.ModuleSemantics, diff --git a/packages/compiler/src/NativeToolchain.ts b/packages/compiler/src/NativeToolchain.ts index 3384a2ec..e3150e29 100644 --- a/packages/compiler/src/NativeToolchain.ts +++ b/packages/compiler/src/NativeToolchain.ts @@ -434,19 +434,8 @@ export const compileShim = ( }) } -/** The nominal native-linker service contract. */ -export interface NativeLinker { - readonly link: ( - toolchain: Toolchain, - target: Target.Target, - objects: ReadonlyArray, - libraries: ReadonlyArray, - destination: string, - ) => Executable | ToolchainFailure -} - /** The bootstrap linker driving the pinned Clang link driver with structured arguments. */ -export const ClangLinker: NativeLinker = Object.freeze({ +export const ClangLinker = Object.freeze({ link: ( toolchain: Toolchain, target: Target.Target, diff --git a/packages/compiler/src/OpaqueRealization.ts b/packages/compiler/src/OpaqueRealization.ts index 5b3f9897..b31e8a4c 100644 --- a/packages/compiler/src/OpaqueRealization.ts +++ b/packages/compiler/src/OpaqueRealization.ts @@ -647,11 +647,3 @@ export const definitionOf = ( /** Stable source family identity used by incremental dependency maps and test fixtures. */ export const key = (self: Definition): string => familyKey(self.family) - -/** One privacy-preserving dependency summary that intentionally omits target and capture details. */ -export const publicOrigin = ( - self: Definition, -): { - readonly family: string - readonly bound: string -} => Object.freeze({ family: key(self), bound: Type.key(self.instance.contract) }) diff --git a/packages/compiler/src/OsRuntime.ts b/packages/compiler/src/OsRuntime.ts index bb8e74c6..b89718a5 100644 --- a/packages/compiler/src/OsRuntime.ts +++ b/packages/compiler/src/OsRuntime.ts @@ -25,9 +25,6 @@ export const symbols = Object.freeze([ export type Symbol = (typeof symbols)[number] -export const isSymbol = (value: string): value is Symbol => - symbols.some((candidate) => candidate === value) - const common = `#if defined(__APPLE__) #define _DARWIN_C_SOURCE #elif defined(__linux__) diff --git a/packages/compiler/src/Ownership.ts b/packages/compiler/src/Ownership.ts index 65d06856..52cd0387 100644 --- a/packages/compiler/src/Ownership.ts +++ b/packages/compiler/src/Ownership.ts @@ -3,6 +3,7 @@ import * as DeclarationIndex from './DeclarationIndex.js' import * as Diagnostic from './Diagnostic.js' import * as Elaboration from './Elaboration.js' import * as Hir from './Hir.js' +import { equal as setEqual } from './internal/SetOf.js' import type * as Match from './Match.js' import type * as SourceSpan from './SourceSpan.js' import * as Type from './Type.js' @@ -2677,8 +2678,7 @@ const checkFunction = ( if (existing === undefined) states.set(loop.ordinal, [new Set(live)]) else existing.push(new Set(live)) } - const sameLive = (left: ReadonlySet, right: ReadonlySet): boolean => - left.size === right.size && [...left].every((site) => right.has(site)) + const sameLive = setEqual const intersection = (states: ReadonlyArray>): Set => { const [first, ...rest] = states return new Set([...(first ?? [])].filter((site) => rest.every((state) => state.has(site)))) diff --git a/packages/compiler/src/ProjectAnalysis.ts b/packages/compiler/src/ProjectAnalysis.ts index ee199718..6c146c14 100644 --- a/packages/compiler/src/ProjectAnalysis.ts +++ b/packages/compiler/src/ProjectAnalysis.ts @@ -1,5 +1,4 @@ import * as Effect from 'effect/Effect' -import * as Option from 'effect/Option' import type * as Analysis from './Analysis.js' import * as FrontendTooling from './FrontendTooling.js' import * as ModuleClosure from './ModuleClosure.js' @@ -12,7 +11,6 @@ import * as Pipeline from './Pipeline.js' import * as SemanticInvalidation from './SemanticInvalidation.js' import type * as SourceFile from './SourceFile.js' import type * as SourceResolver from './SourceResolver.js' -import * as SyntaxCorrespondence from './SyntaxCorrespondence.js' import type * as SyntaxFile from './SyntaxFile.js' /** One current module's syntax relationship to the optional previous project revision. */ @@ -33,7 +31,6 @@ export type SyntaxRevision = readonly module: string readonly previous: SyntaxFile.SyntaxFile readonly current: SyntaxFile.SyntaxFile - readonly correspondence: SyntaxCorrespondence.SyntaxCorrespondence } /** One frontend-query-compatible root view that cannot be passed to runtime realization. */ @@ -118,9 +115,6 @@ const analyze = Effect.fnUntraced(function* ( module: module.name, previous: previousSyntax, current: module.syntax, - correspondence: Option.getOrThrow( - SyntaxCorrespondence.between(previousSyntax, module.syntax), - ), }), ) } diff --git a/packages/compiler/src/SuspensionMir.ts b/packages/compiler/src/SuspensionMir.ts index 80a20ea8..b48cd714 100644 --- a/packages/compiler/src/SuspensionMir.ts +++ b/packages/compiler/src/SuspensionMir.ts @@ -343,10 +343,6 @@ export const finalize = ( }) } -/** True when final MIR contains any target-neutral suspension control. */ -export const hasSuspension = (program: Mir.Module): boolean => - program.functions.some((fn) => (fn.suspension?.regions.length ?? 0) > 0) - /** Stable inspection summary for focused finalization tests. */ export const summary = (program: Mir.Module): string => program.functions diff --git a/packages/compiler/src/Token.ts b/packages/compiler/src/Token.ts index 909f1e63..db522cba 100644 --- a/packages/compiler/src/Token.ts +++ b/packages/compiler/src/Token.ts @@ -156,7 +156,7 @@ const descriptions: Readonly> = Object.freeze({ Dot: '`.`', DotDot: '`..`', Arrow: '`->`', - Invalid: 'valid token', + Invalid: 'invalid byte', EndOfFile: 'end of file', }) diff --git a/packages/compiler/src/ToolchainIntegrity.generated.ts b/packages/compiler/src/ToolchainIntegrity.generated.ts index e32afae0..f5735d97 100644 --- a/packages/compiler/src/ToolchainIntegrity.generated.ts +++ b/packages/compiler/src/ToolchainIntegrity.generated.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-toolchain-integrity.mjs. Do not edit. -export const compilerDigest = '478783345319c1de5beb3efeacc845c67876026de0a88d8896af446c017873ba' +export const compilerDigest = '7de99140db1ac3ac56d4cefe8002be39759afe50758b92ab9ad862be7f9e3950' diff --git a/packages/compiler/src/Transcendental.ts b/packages/compiler/src/Transcendental.ts index 4ba962d1..4bdc09ab 100644 --- a/packages/compiler/src/Transcendental.ts +++ b/packages/compiler/src/Transcendental.ts @@ -16,7 +16,7 @@ export interface Plan { const f32: Plan = Object.freeze({ width: 32, - canonicalNaN: 0x7fc00000n, + canonicalNaN: FloatingPoint.canonicalNaN(32), one: 0x3f800000n, half: 0x3f000000n, four: 0x40800000n, @@ -53,7 +53,7 @@ const f32: Plan = Object.freeze({ const f64: Plan = Object.freeze({ width: 64, - canonicalNaN: 0x7ff8000000000000n, + canonicalNaN: FloatingPoint.canonicalNaN(64), one: 0x3ff0000000000000n, half: 0x3fe0000000000000n, four: 0x4010000000000000n, diff --git a/packages/compiler/src/Type.ts b/packages/compiler/src/Type.ts index dcbb5828..def13c05 100644 --- a/packages/compiler/src/Type.ts +++ b/packages/compiler/src/Type.ts @@ -498,15 +498,6 @@ export const intrinsicNominalOrdinal = (self: Nominal): number => (candidate) => candidate.module === self.module && candidate.name === self.name, ) -/** Compiler-shipped nominal capability witnesses; user declarations extend this in the index. */ -export const intrinsicConformances: ReadonlyMap> = new Map([]) - -/** Tests one compiler-shipped nominal capability witness without inspecting provider kinds. */ -export const intrinsicallyConforms = (provider: Type, capability: Nominal): boolean => - isNominal(provider) && (intrinsicConformances.get(key(provider))?.has(key(capability)) ?? false) - -/** Tests the one compiler-sealed allocation exhaustion payload. */ -export const isOutOfMemoryError = (self: Type): self is Nominal => equals(self, outOfMemoryError) export const isIntrinsicNominal = (self: Type): boolean => isNominal(self) && self.module === 'silk/core' && intrinsicNominals.get(self.name) !== undefined @@ -955,9 +946,22 @@ export const parameterArgument = (self: Parameter): GenericArgument => { } } -const accessRank = (access: CallableMode | Effect['access']): number => +/** Ranks access modes: Shared(0) < Exclusive(1) < Take(2). */ +export const accessRank = (access: CallableMode | Effect['access']): number => access === 'Shared' ? 0 : access === 'Exclusive' ? 1 : 2 +/** True when the supplied access is at least as strong as the required one. */ +export const compareAccess = ( + supplied: CallableMode | Effect['access'], + required: CallableMode | Effect['access'], +): boolean => accessRank(supplied) <= accessRank(required) + +/** True when one requirement is satisfied by a supplied requirement with compatible access. */ +export const requirementSatisfies = ( + supplied: { readonly access: 'Shared' | 'Exclusive' | 'Take' }, + required: { readonly access: 'Shared' | 'Exclusive' }, +): boolean => compareAccess(supplied.access, required.access) + /** * Intersects two uses of one representation contract. The result keeps the most restrictive * access while rejecting structurally unrelated callable or Effect contracts. @@ -1589,7 +1593,7 @@ const genericArgumentsHaveSameRepresentationShape = ( return ( compared !== undefined && requirement.role === compared.role && - requirement.access === compared.access && + requirementSatisfies(requirement, compared) && haveSameRepresentationShape(requirement.capability, compared.capability) ) }) && @@ -1652,11 +1656,9 @@ export const haveSameRepresentationShape = (left: Type, right: Type): boolean => ) if (isCallable(left) || isCallable(right)) { if (!isCallable(left) || !isCallable(right)) return false - const leftRank = left.mode === 'Shared' ? 0 : left.mode === 'Exclusive' ? 1 : 2 - const rightRank = right.mode === 'Shared' ? 0 : right.mode === 'Exclusive' ? 1 : 2 return ( (!left.unsafe || right.unsafe) && - leftRank <= rightRank && + compareAccess(left.mode, right.mode) && left.parameters.length === right.parameters.length && left.parameters.every((parameter_, ordinal) => { const compared = right.parameters.at(ordinal) @@ -1667,10 +1669,8 @@ export const haveSameRepresentationShape = (left: Type, right: Type): boolean => } if (isEffect(left) || isEffect(right)) { if (!isEffect(left) || !isEffect(right)) return false - const leftRank = left.access === 'Shared' ? 0 : left.access === 'Exclusive' ? 1 : 2 - const rightRank = right.access === 'Shared' ? 0 : right.access === 'Exclusive' ? 1 : 2 return ( - leftRank <= rightRank && + compareAccess(left.access, right.access) && haveSameRepresentationShape(left.success, right.success) && haveSameRepresentationShape(failureType(left), failureType(right)) && requirementMembers(left).length === requirementMembers(right).length && @@ -1679,7 +1679,7 @@ export const haveSameRepresentationShape = (left: Type, right: Type): boolean => return ( compared !== undefined && requirement.role === compared.role && - requirement.access === compared.access && + requirementSatisfies(requirement, compared) && haveSameRepresentationShape(requirement.capability, compared.capability) ) }) && @@ -2308,6 +2308,33 @@ const isClosedGenericArgument = (self: GenericArgument): boolean => { export const isRuntimeConcreteGenericArgument = (self: GenericArgument): boolean => isClosedGenericArgument(self) && runtimeAvailableGenericArgument(self) +/** True when any nested Type satisfies the predicate (including self). */ +export const someSubterm = (self: Type, predicate: (type: Type) => boolean): boolean => { + if (predicate(self)) return true + if (isReference(self)) return someSubterm(self.target, predicate) + if (isSlice(self)) return someSubterm(self.element, predicate) + if (isNominal(self)) + return self.arguments + .filter(isTypeArgument) + .some((argument) => someSubterm(argument, predicate)) + if (isFixedArray(self)) return someSubterm(self.element, predicate) + if (isCallable(self)) + return ( + self.parameters.some((parameter_) => someSubterm(parameter_, predicate)) || + someSubterm(self.result, predicate) + ) + if (isEffect(self)) + return ( + someSubterm(self.success, predicate) || + RowAlgebra.concreteMembers(failureRowPolicy(), self.failureRow).some((member) => + someSubterm(member, predicate), + ) + ) + if (isRepresented(self)) return someSubterm(self.contract, predicate) + if (isUnion(self)) return self.members.some((member) => someSubterm(member, predicate)) + return false +} + /** Tests whether a type contains a lexical borrow at any depth. */ export const containsBorrow = (self: Type): boolean => { if (isString(self)) return true diff --git a/packages/compiler/src/TypeCompatibility.ts b/packages/compiler/src/TypeCompatibility.ts index a2444559..2c7bf0e0 100644 --- a/packages/compiler/src/TypeCompatibility.ts +++ b/packages/compiler/src/TypeCompatibility.ts @@ -63,11 +63,9 @@ export const check = (source: Type.Type, target: Type.Type): Compatibility => { if (Type.equals(source, target)) return Object.freeze({ _tag: 'Exact', source, target }) if (Type.isNever(source)) return Object.freeze({ _tag: 'Bottom', source, target }) if (Type.isCallable(source) && Type.isCallable(target)) { - const sourceRank = source.mode === 'Shared' ? 0 : source.mode === 'Exclusive' ? 1 : 2 - const targetRank = target.mode === 'Shared' ? 0 : target.mode === 'Exclusive' ? 1 : 2 if ( (!source.unsafe || target.unsafe) && - sourceRank <= targetRank && + Type.compareAccess(source.mode, target.mode) && source.parameters.length === target.parameters.length && source.parameters.every((parameter, index) => Type.equals(parameter, target.parameters.at(index) ?? 'never'), @@ -78,8 +76,6 @@ export const check = (source: Type.Type, target: Type.Type): Compatibility => { } } if (Type.isEffect(source) && Type.isEffect(target)) { - const sourceRank = source.access === 'Shared' ? 0 : source.access === 'Exclusive' ? 1 : 2 - const targetRank = target.access === 'Shared' ? 0 : target.access === 'Exclusive' ? 1 : 2 const sameOutputs = Type.equals(source.success, target.success) && Type.equals(Type.failureType(source), Type.failureType(target)) @@ -91,7 +87,7 @@ export const check = (source: Type.Type, target: Type.Type): Compatibility => { expected !== undefined && Type.equals(requirement.capability, expected.capability) && requirement.role === expected.role && - (requirement.access === expected.access || expected.access === 'Exclusive') + Type.requirementSatisfies(requirement, expected) ) }) && Type.requirementRowParameters(source).length === @@ -99,7 +95,7 @@ export const check = (source: Type.Type, target: Type.Type): Compatibility => { Type.requirementRowParameters(source).every((parameter, index) => Type.equals(parameter, Type.requirementRowParameters(target).at(index) ?? 'never'), ) - if (sourceRank <= targetRank && sameOutputs && compatibleRequirements) + if (Type.compareAccess(source.access, target.access) && sameOutputs && compatibleRequirements) return Object.freeze({ _tag: 'EffectAccess', source, target }) } const members = sourceMembers(source) diff --git a/packages/compiler/src/WasmBackend.ts b/packages/compiler/src/WasmBackend.ts index 7ed19c93..6c89b2df 100644 --- a/packages/compiler/src/WasmBackend.ts +++ b/packages/compiler/src/WasmBackend.ts @@ -17,6 +17,7 @@ import { symbolFor } from './Backend.js' import type * as DeclarationIndex from './DeclarationIndex.js' import * as FloatingPoint from './FloatingPoint.js' import * as Instances from './Instances.js' +import { alignUp } from './internal/Align.js' import * as LayoutPlan from './Layout.js' import type * as Match from './Match.js' import * as Mir from './Mir.js' @@ -225,9 +226,6 @@ const laneStoreMnemonic = ( return 'i32.store' } -const alignUp = (value: number, alignment: number): number => - Math.ceil(value / alignment) * alignment - interface FrameRoot { readonly local: number readonly offset: number @@ -1593,9 +1591,6 @@ const layoutOf = ( } } -const suspensionPointKey = (point: Mir.SuspensionPointId): string => - `${Instances.keyText(point.owner)}\u0000${point.sourceId}\u0000${point.spanStart}\u0000${point.spanEnd}\u0000${point.ordinal}` - const suspensionOperationInputs = ( operation: Extract< Mir.Operation, @@ -5595,7 +5590,7 @@ const emitBody = ( originate: ( region: Extract, ) => { - const child = suspensionRuntime.origins.get(suspensionPointKey(region.point)) + const child = suspensionRuntime.origins.get(Backend.suspensionPointKey(region.point)) if (child === undefined) throw new RangeError('Wasm suspension origin lost child id') const inputs = suspensionOperationInputs(region.operation) const lanes = inputs.flatMap((local) => layout.lanes.at(local.ordinal) ?? []) @@ -5630,8 +5625,12 @@ const emitBody = ( Instr.ifElse(Instr.emptyBlockType, returnTransfer(), []), ] if (descriptor === undefined) return transfer - const targetLayout = suspensionRuntime.layouts.get(suspensionPointKey(descriptor.point)) - const resume = suspensionRuntime.resumes.get(suspensionPointKey(descriptor.point)) + const targetLayout = suspensionRuntime.layouts.get( + Backend.suspensionPointKey(descriptor.point), + ) + const resume = suspensionRuntime.resumes.get( + Backend.suspensionPointKey(descriptor.point), + ) const scratch = layout.suspensionScratch if (targetLayout === undefined || resume === undefined || scratch === undefined) throw new RangeError('Wasm stateful relay lost its layout or dispatch identity') @@ -5809,8 +5808,8 @@ const emitBody = ( const dispatch = (fn.suspension?.regions ?? []).flatMap((region) => { if (region._tag !== 'RunSuspendableEffectRegion' || region.relay.state === undefined) return [] - const id = suspensionRuntime.resumes.get(suspensionPointKey(region.point)) - const targetLayout = suspensionRuntime.layouts.get(suspensionPointKey(region.point)) + const id = suspensionRuntime.resumes.get(Backend.suspensionPointKey(region.point)) + const targetLayout = suspensionRuntime.layouts.get(Backend.suspensionPointKey(region.point)) const owner = regions.get(region.ownerRegion.ordinal) if (id === undefined || targetLayout === undefined || owner?._tag !== 'OperationRegion') throw new RangeError('Wasm resume dispatch lost its continuation region') @@ -6008,7 +6007,9 @@ const emitProgram = (program: Mir.Module, request: Backend.CodegenRequest) => ), ) .sort((left, right) => - suspensionPointKey(left.region.point).localeCompare(suspensionPointKey(right.region.point)), + Backend.suspensionPointKey(left.region.point).localeCompare( + Backend.suspensionPointKey(right.region.point), + ), ) const resumeRecords = program.functions .flatMap((fn) => @@ -6019,17 +6020,19 @@ const emitProgram = (program: Mir.Module, request: Backend.CodegenRequest) => ), ) .sort((left, right) => - suspensionPointKey(left.region.point).localeCompare(suspensionPointKey(right.region.point)), + Backend.suspensionPointKey(left.region.point).localeCompare( + Backend.suspensionPointKey(right.region.point), + ), ) const originIds = new Map( originRecords.map((record, ordinal) => [ - suspensionPointKey(record.region.point), + Backend.suspensionPointKey(record.region.point), ordinal + 1, ]), ) const resumeIds = new Map( resumeRecords.map((record, ordinal) => [ - suspensionPointKey(record.region.point), + Backend.suspensionPointKey(record.region.point), ordinal + 1, ]), ) @@ -6041,7 +6044,7 @@ const emitProgram = (program: Mir.Module, request: Backend.CodegenRequest) => ) const coroutineFrameStates = new Map( (program.coroutineFrames?.entries ?? []).flatMap((entry) => - entry.states.map((state) => [suspensionPointKey(state.point), state] as const), + entry.states.map((state) => [Backend.suspensionPointKey(state.point), state] as const), ), ) const transferHeaderSize = program.layout.target.pointerSize * 3 @@ -6130,7 +6133,17 @@ const emitProgram = (program: Mir.Module, request: Backend.CodegenRequest) => privateExecutionStackPages < 1 || privateExecutionStackPages > 65536 ) { - throw new RangeError('privateExecutionStackPages must be an integer from 1 through 65536') + return yield* Effect.fail( + new Backend.BackendError({ + operation: 'Backend.emit', + backend: 'Wasm', + message: 'privateExecutionStackPages must be an integer from 1 through 65536', + reason: { + _tag: 'UnsupportedMir', + detail: 'privateExecutionStackPages must be an integer from 1 through 65536', + }, + }), + ) } const coroutineFrameMemory = suspensionEnabled ? yield* Memory.make( diff --git a/packages/compiler/src/internal/Align.ts b/packages/compiler/src/internal/Align.ts new file mode 100644 index 00000000..e8edc330 --- /dev/null +++ b/packages/compiler/src/internal/Align.ts @@ -0,0 +1,3 @@ +/** Rounds an offset up to the nearest multiple of the given alignment. */ +export const alignUp = (offset: number, alignment: number): number => + Math.ceil(offset / alignment) * alignment diff --git a/packages/compiler/src/internal/ByteClass.ts b/packages/compiler/src/internal/ByteClass.ts new file mode 100644 index 00000000..63980ca7 --- /dev/null +++ b/packages/compiler/src/internal/ByteClass.ts @@ -0,0 +1,21 @@ +/** One canonical classification of every ASCII byte the compiler's scanner and lexer consume. */ + +/** True for A-Z and a-z. */ +export const isAsciiLetter = (byte: number | undefined): boolean => + byte !== undefined && ((byte >= 0x41 && byte <= 0x5a) || (byte >= 0x61 && byte <= 0x7a)) + +/** True for 0-9. */ +export const isDecimalDigit = (byte: number | undefined): boolean => + byte !== undefined && byte >= 0x30 && byte <= 0x39 + +/** True when the byte can begin a source identifier or keyword. */ +export const isIdentifierStart = (byte: number | undefined): boolean => + byte === 0x5f || isAsciiLetter(byte) + +/** True when the byte can appear after the first byte of a source identifier or keyword. */ +export const isIdentifierContinue = (byte: number | undefined): boolean => + isIdentifierStart(byte) || isDecimalDigit(byte) + +/** The numeric value of one hex digit byte (0-9, A-F, a-f). Callers validate the byte first. */ +export const hexValue = (byte: number): number => + byte <= 0x39 ? byte - 0x30 : byte <= 0x46 ? byte - 0x41 + 10 : byte - 0x61 + 10 diff --git a/packages/compiler/src/internal/Escape.ts b/packages/compiler/src/internal/Escape.ts new file mode 100644 index 00000000..c2f001bd --- /dev/null +++ b/packages/compiler/src/internal/Escape.ts @@ -0,0 +1,35 @@ +/** One canonical escape vocabulary for the Silk compiler. */ + +type ByteSequence = ReadonlyArray | Uint8Array + +const isContinuation = (byte: number | undefined): boolean => + byte !== undefined && (byte & 0xc0) === 0x80 + +/** + * Counts the Unicode scalars one escaped body denotes, without decoding any of them. + * + * The count is a scalar count and never a byte count. One escape sequence denotes one scalar + * however it is spelled. Only the extent of an escape matters here, never its meaning, so the + * decoder in StaticText stays the single authority on what an escape produces. + */ +export const scalarCount = (bytes: ByteSequence, contentStart: number, end: number): number => { + let index = contentStart + let scalars = 0 + while (index < end) { + scalars += 1 + if (bytes[index] !== 0x5c) { + index += 1 + while (index < end && isContinuation(bytes[index])) index += 1 + continue + } + const escaped = bytes[index + 1] + if (escaped === 0x75 && bytes[index + 2] === 0x7b) { + index += 3 + while (index < end && bytes[index] !== 0x7d) index += 1 + index += 1 + continue + } + index += escaped === 0x78 ? 4 : 2 + } + return scalars +} diff --git a/packages/compiler/src/internal/Graph.ts b/packages/compiler/src/internal/Graph.ts new file mode 100644 index 00000000..93486ecd --- /dev/null +++ b/packages/compiler/src/internal/Graph.ts @@ -0,0 +1,59 @@ +/** + * One parameterized Tarjan strongly-connected-component (SCC) implementation. + * + * Every caller passes its own key set and neighbor projection so the fixed-point algorithm + * stays in one place while the notion of "neighbor" is caller-specific. Sorting and cycle + * filtering stay per-caller; this module only returns the raw components in deterministic + * discovery order. + */ + +/** Returns components in deterministic reverse-topological order (dependents first). */ +export const stronglyConnected = ( + keys: Iterable, + dependencies: (key: string) => Iterable, +): ReadonlyArray> => { + const orderedKeys = [...new Set(keys)].sort() + const known = new Set(orderedKeys) + const adjacency = new Map( + orderedKeys.map((key) => [ + key, + Object.freeze( + [...new Set(dependencies(key))].filter((dependency) => known.has(dependency)).sort(), + ), + ]), + ) + let nextIndex = 0 + const indices = new Map() + const lows = new Map() + const stack: Array = [] + const stacked = new Set() + const components: Array> = [] + const visit = (key: string): void => { + indices.set(key, nextIndex) + lows.set(key, nextIndex) + nextIndex += 1 + stack.push(key) + stacked.add(key) + for (const dependency of adjacency.get(key) ?? []) { + if (!indices.has(dependency)) { + visit(dependency) + lows.set(key, Math.min(lows.get(key) ?? 0, lows.get(dependency) ?? 0)) + } else if (stacked.has(dependency)) { + lows.set(key, Math.min(lows.get(key) ?? 0, indices.get(dependency) ?? 0)) + } + } + if (lows.get(key) !== indices.get(key)) return + const component: Array = [] + for (;;) { + const member = stack.pop() + if (member === undefined) break + stacked.delete(member) + component.push(member) + if (member === key) break + } + component.sort() + components.push(Object.freeze(component)) + } + for (const key of orderedKeys) if (!indices.has(key)) visit(key) + return Object.freeze(components) +} diff --git a/packages/compiler/src/internal/IntegerLiteral.ts b/packages/compiler/src/internal/IntegerLiteral.ts index add25db6..c333a637 100644 --- a/packages/compiler/src/internal/IntegerLiteral.ts +++ b/packages/compiler/src/internal/IntegerLiteral.ts @@ -1,3 +1,4 @@ +import * as ByteClass from './ByteClass.js' import * as DigitSeparator from './DigitSeparator.js' /** The numeric base an integer literal's digits are read in. */ @@ -18,9 +19,6 @@ export interface Base { const codeAt = (digits: Digits, index: number): number => typeof digits === 'string' ? digits.charCodeAt(index) : (digits[index] ?? Number.NaN) -const digitValue = (byte: number): number => - byte <= 0x39 ? byte - 0x30 : byte <= 0x46 ? byte - 0x41 + 10 : byte - 0x61 + 10 - const make = (radix: Radix, width: 0 | 2): Base => Object.freeze({ radix, width }) /** The implicit base of a literal written without a prefix. */ @@ -90,7 +88,7 @@ export const magnitude = (digits: Digits): bigint => { for (let index = base.width; index < digits.length; index += 1) { const code = codeAt(digits, index) if (DigitSeparator.isSeparator(code)) continue - value = value * radix + BigInt(digitValue(code)) + value = value * radix + BigInt(ByteClass.hexValue(code)) } return value } diff --git a/packages/compiler/src/internal/SetOf.ts b/packages/compiler/src/internal/SetOf.ts new file mode 100644 index 00000000..a31dbb79 --- /dev/null +++ b/packages/compiler/src/internal/SetOf.ts @@ -0,0 +1,13 @@ +/** One immutable set of primitives for the compiler's ownership layer. */ + +/** True when the two sets contain exactly the same elements. */ +export const equal = (left: ReadonlySet, right: ReadonlySet): boolean => + left.size === right.size && [...left].every((element) => right.has(element)) + +/** The union of two sets as a new frozen set. */ +export const union = (left: ReadonlySet, right: ReadonlySet): ReadonlySet => + Object.freeze(new Set([...left, ...right])) + +/** The intersection of two sets as a new frozen set. */ +export const intersection = (left: ReadonlySet, right: ReadonlySet): ReadonlySet => + Object.freeze(new Set([...left].filter((element) => right.has(element)))) diff --git a/packages/compiler/test/ProjectAnalysis.test.ts b/packages/compiler/test/ProjectAnalysis.test.ts index 36d401a7..7457ed4c 100644 --- a/packages/compiler/test/ProjectAnalysis.test.ts +++ b/packages/compiler/test/ProjectAnalysis.test.ts @@ -238,8 +238,7 @@ it.effect('reuses exact unchanged syntax and module semantics inside one coheren assert.strictEqual(current.syntaxRevisions.get('shared/Core')?._tag, 'Reused') const changed = current.syntaxRevisions.get('app/A') assert.strictEqual(changed?._tag, 'Changed') - if (changed?._tag === 'Changed') - assert.isAbove(changed.correspondence.counts.correspondingElements, 0) + if (changed?._tag === 'Changed') assert.notStrictEqual(changed.previous, changed.current) const previousView = ProjectAnalysis.view(previous, 'app/A') const currentView = ProjectAnalysis.view(current, 'app/A') diff --git a/packages/compiler/test/RuntimeSliceSemantics.test.ts b/packages/compiler/test/RuntimeSliceSemantics.test.ts index 5729cc9c..da990ec8 100644 --- a/packages/compiler/test/RuntimeSliceSemantics.test.ts +++ b/packages/compiler/test/RuntimeSliceSemantics.test.ts @@ -384,7 +384,7 @@ pub fn main() -> i32 { return read(&) }`) 'Unavailable', ) const main = result.hir.functions.at(1) - assert.strictEqual(main === undefined ? undefined : Hir.hasUnavailable(main), true) + assert.notStrictEqual(main === undefined ? undefined : Hir.firstUnavailable(main), undefined) }) it('verifies mismatched HIR loan endings without introducing graph cycles', () => { diff --git a/packages/compiler/test/SuspensionMir.test.ts b/packages/compiler/test/SuspensionMir.test.ts index 009de8b1..0d53a2a4 100644 --- a/packages/compiler/test/SuspensionMir.test.ts +++ b/packages/compiler/test/SuspensionMir.test.ts @@ -96,19 +96,6 @@ it.effect( relay.relay.state?.slots.map((slot) => slot.local.ordinal), relay.liveLocals.map((local) => local.ordinal), ) - assert.deepEqual( - Mir.suspensionControlEdges( - left.functions.find((fn) => fn === stateful(left)[0]) ?? stateful(left)[0], - ) - .filter( - (edge) => - edge.from.sourceId === relay.point.sourceId && - edge.from.spanStart === relay.point.spanStart && - edge.from.ordinal === relay.point.ordinal, - ) - .map((edge) => edge.kind), - ['RelayTransfer', 'ResumeSuccess', 'ResumeFailure'], - ) }), )