diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 80028fb..28ad47f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,6 +12,7 @@ Link the canonical public discussion thread here. - [x] I linked the canonical discussion thread. - [x] I used the correct SEP template for this proposal type. - [x] I updated front matter metadata and required sections. +- [x] I considered the SEP-0000 guiding questions in the relevant proposal sections. ## Notes for reviewers diff --git a/.github/styles/Spore/Terms.yml b/.github/styles/Spore/Terms.yml index 33a1bd1..f386c25 100644 --- a/.github/styles/Spore/Terms.yml +++ b/.github/styles/Spore/Terms.yml @@ -11,5 +11,5 @@ swap: Pullrequest: Pull request spore proposal: SEP Spore proposal: SEP - spore enhancement proposal: SEP - Spore enhancement proposal: SEP + spore evolution proposal: Spore Evolution Proposal + Spore evolution proposal: Spore Evolution Proposal diff --git a/.github/workflows/ci-pr-checks.yml b/.github/workflows/ci-pr-checks.yml index 04bc8af..baf8349 100644 --- a/.github/workflows/ci-pr-checks.yml +++ b/.github/workflows/ci-pr-checks.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v6 - name: Check PR title - uses: zrr1999/zendev/actions/validate-title@v0.0.5 + uses: zendev-lab/zendev/actions/validate-title@v0.0.7 with: text: ${{ github.event.pull_request.title }} @@ -44,36 +44,12 @@ jobs: echo "is_sep=false" >> "$GITHUB_OUTPUT" fi - - name: Validate PR body checklist (SEP PRs only) + - name: Validate PR body template shape and checklist if: steps.changes.outputs.is_sep == 'true' - env: - GITHUB_EVENT_PATH: ${{ github.event_path }} - run: | - python - <<'PY' - import json - import os - import sys - from pathlib import Path - - REQUIRED_CHECKBOXES = [ - "- [x] I opened or linked a public Discussion or Issue for the pitch before submitting this draft SEP.", - "- [x] I linked the canonical discussion thread.", - "- [x] I used the correct SEP template for this proposal type.", - "- [x] I updated front matter metadata and required sections.", - ] - - payload = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text(encoding="utf-8")) - body = (payload.get("pull_request") or {}).get("body") or "" - missing = [item for item in REQUIRED_CHECKBOXES if item not in body] - - if missing: - print("Pull request body is missing required checked items:\n", file=sys.stderr) - for item in missing: - print(f"- {item}", file=sys.stderr) - raise SystemExit(1) - - print("Pull request body checklist looks good.") - PY + uses: zendev-lab/zendev/actions/validate-body@v0.0.7 + with: + body: ${{ github.event.pull_request.body }} + require-checklist: "true" - name: PR body OK (non-SEP PR) if: steps.changes.outputs.is_sep == 'false' diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index a10f48e..861cfe4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,13 +28,7 @@ jobs: with: python-version: "3.14" - - name: Check SEP index - run: uv run scripts/check_sep_index.py - - - name: Check contract schemas - run: uv run scripts/check_contract_schemas.py - - - name: Validate SEP documents + - name: Run repository checks env: GITHUB_BASE_REF: ${{ github.base_ref }} - run: uv run scripts/validate_sep_documents.py + run: uv run scripts/check_repo.py diff --git a/.gitignore b/.gitignore index 6586859..4201f44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ .cursor/ .vscode/ .idea/ + +.pi-lens/ diff --git a/.spark/artifacts/cross-check-impl.md b/.spark/artifacts/cross-check-impl.md new file mode 100644 index 0000000..1580451 --- /dev/null +++ b/.spark/artifacts/cross-check-impl.md @@ -0,0 +1,289 @@ +# PR45 patch plan implementation cross-check + +This review maps `.spark/artifacts/sep-patch-plan.md` against the sibling +`spore` implementation. It is review-only: no source code, issues, or TODO files +are changed. + +## Review categories + +- **Aligned**: implementation already has a recognizable matching concept. +- **Partial**: implementation has adjacent machinery but vocabulary or semantics + differ from the PR45 patch plan. +- **Follow-up**: implementation work is likely needed after the design patch. +- **Conflict**: implementation behavior directly contradicts the selected PR45 + design and should be tracked before release. + +## Summary + +| Area | Status | Evidence | +| --- | --- | --- | +| SEP-0001 property surface | Follow-up | Parser and CLI still use `spec`, not `properties`. | +| SEP-0001 handler grammar | Partial | Parser AST already has fields, handles, uses, and impl blocks; tests still omit explicit `self` receiver. | +| SEP-0002 property body typing | Follow-up | Runtime spec testing exists, but Claim/Evidence property typing is not visible in scanned implementation. | +| SEP-0003 effect surface | Aligned | `uses`, `perform`, handlers, and effect contexts exist. | +| SEP-0003 handler state policy | Conflict | Implementation accepts handler fields and materializes payloads; PR45 selects immutable fields and state via primitive effects. | +| SEP-0005 HoleReport schema | Partial | Sibling has HoleInfo/HoleReport JSON, but some field vocabulary differs. | +| SEP-0005 workflow/ranker status | Partial | Sibling has candidate ranking formula; PR45 will mark it informational. | +| SEP-0006 evidence channel | Partial | Diagnostics and hole records exist; full Claim/Evidence property channel is design-forward. | +| SEP-0008 Platform primitive handlers | Follow-up | Platform handled effects exist, but primitive host/mock conformance is not modeled. | +| SEP-0009 primitive effect set | Follow-up | `Clock` and `Random` overlap; `Cell`, `Output`, and effect-level `Map` are missing from collected evidence. | +| SEP-0010 explain protocol | Partial | Low-level `sporec explain ` exists; user-facing `spore explain ` and ConceptDoc registry are not visible. | + +## Patch item cross-check + +### SEP-0001 property delegation and grammar + +Status: **Follow-up**. + +PR45 patch plan uses `properties { name(params): expr }` and delegates typing to +SEP-0002 plus Claim/Evidence lowering to SEP-0006. + +Implementation evidence: + +- Lexer still has `Token::Spec` in `../spore/crates/sporec-parser/src/lexer.rs:121-123`. +- Function AST stores `spec_clause` in `../spore/crates/sporec-parser/src/ast.rs:90-95`. +- `SpecClause` is documented as examples plus property-based invariants in + `../spore/crates/sporec-parser/src/ast.rs:159-166`. +- CLI test source uses `spec { ... property ... }` in + `../spore/crates/spore/src/main.rs:208-211`. + +Review result: design patch can proceed, but implementation follow-up should +plan a surface migration from `spec` to `properties` or a compatibility story. + +### SEP-0001 handler grammar + +Status: **Partial**. + +Implementation evidence: + +- `HandlerDef` includes `fields`, `handles_clause`, optional `uses_clause`, and + `impls` in `../spore/crates/sporec-parser/src/ast.rs:493-499`. +- `HandlerImpl` stores `effect` plus `methods` in + `../spore/crates/sporec-parser/src/ast.rs:483-489`. +- `handle ... with` has named `use` and inline `on` arms through + `HandleBinding::Use` and `HandleBinding::On` in + `../spore/crates/sporec-parser/src/ast.rs:310-326`. +- Parser test covers `handler MockConsole(output: List[Str]) handles [Console] + uses [Clock] { impl Console { ... } }` in + `../spore/crates/sporec-parser/tests/parser_tests.rs:486-500`. + +Review result: grammar shape is close to the PR45 patch plan. The explicit +read-only `self` receiver remains a follow-up because the observed test method +uses `fn println(msg: String) -> Unit`, not `fn println(self, msg: Str) -> ()`. + +### SEP-0002 property body typing + +Status: **Follow-up**. + +Implementation evidence: + +- `test_specs` runs spec clauses in `../spore/crates/sporec-driver/src/compiler/source.rs:88-95`. +- The CLI test command is described as executing examples and properties in + `../spore/crates/spore/src/cli.rs:103-105`. +- Runtime spec evaluation is driven from functions with spec clauses in + `../spore/crates/sporec-codegen/src/interpret/mod.rs:201-205`. + +Review result: the implementation has executable property-like tests, but the +scanned evidence does not show the selected PR45 rule: ordinary expression body, +strict `Bool`, effect-context inheritance, and lowering into Claim/Evidence. + +### SEP-0003 effect surface and handlers + +Status: **Aligned / Partial**. + +Aligned evidence: + +- `Token::Uses` exists in `../spore/crates/sporec-parser/src/lexer.rs:103-105`. +- Function AST stores `uses_clause` in `../spore/crates/sporec-parser/src/ast.rs:94-95`. +- `Expr::Perform` records effect, operation, and args in + `../spore/crates/sporec-parser/src/ast.rs:285-294`. +- Interpreter has named handlers and a handler stack in + `../spore/crates/sporec-codegen/src/interpret/mod.rs:33-38` and + `../spore/crates/sporec-codegen/src/interpret/eval.rs:431-432`. + +Partial evidence: + +- Implementation still has legacy effect alias terminology in + `../spore/crates/sporec-parser/tests/parser_tests.rs:470-480`, while PR45 uses + `surface` for named effect surfaces. + +Review result: core effect mechanics are present, but terminology and handler +state rules need follow-up. + +### SEP-0003 handler state policy + +Status: **Conflict**. + +Implementation evidence: + +- Handler payload initialization is modeled by `HandlerUse.payload` in + `../spore/crates/sporec-parser/src/ast.rs:316-321`. +- Interpreter materializes handler payload values in + `../spore/crates/sporec-codegen/src/interpret/builtins.rs:84-86`. +- Current SEP-0003 example mutates `self.output.push(msg)` in PR45 text, while + D3 selects immutable handler fields and primitive-effect state. + +Review result: the design patch should explicitly declare field immutability and +primitive-effect state. Implementation follow-up should audit whether any parser, +formatter, interpreter, or docs imply field mutation. + +### SEP-0005 HoleReport schema + +Status: **Partial**. + +Implementation evidence: + +- `HoleInfo` includes `name`, `location`, `expected_type`, + `type_inferred_from`, `function`, `enclosing_signature`, `bindings`, + `binding_dependencies`, `available_effects`, `errors_to_handle`, + `effect_context`, `cost_budget`, `residual_context`, `candidates`, + `dependent_holes`, `confidence`, and `error_clusters` in + `../spore/crates/sporec-typeck/src/hole.rs:124-170`. +- JSON mapping happens in `hole_info_json` at + `../spore/crates/sporec-driver/src/compiler/hole_json.rs:94-171`. + +Review result: the implementation already has a rich HoleReport. Follow-up is +needed for `property_context`, budget-vocabulary migration, and per-hole versus +candidate-level rejection-reason shape. + +### SEP-0005 workflow and ranker demotion + +Status: **Partial**. + +Implementation evidence: + +- `CandidateScore::overall` implements the current formula in + `../spore/crates/sporec-typeck/src/hole.rs:36-42`. +- Candidate JSON emits `overall`, fit fields, rejection reasons, explanation, + adjustments, and cost check in + `../spore/crates/sporec-driver/src/compiler/hole_json.rs:130-160`. + +Review result: implementation may keep this as reference behavior, but PR45 +should avoid making the formula a normative Agent protocol. + +### SEP-0006 Claim/Evidence channel + +Status: **Partial / Follow-up**. + +Implementation evidence: + +- Diagnostic and HoleReport JSON records are exported from + `../spore/crates/sporec-driver/src/lib.rs:27-31`. +- Compile output currently exposes warnings, including cost budget warnings, in + `../spore/crates/sporec-driver/src/compiler/mod.rs:21-23`. +- Signature hashing machinery exists in + `../spore/crates/sporec-typeck/src/sig_hash.rs:146-177`. + +Review result: shared diagnostics and hash machinery exist, but the PR45 +Claim/EvidenceRecord channel for source properties is not visible in the scanned +implementation and should be a follow-up. + +### SEP-0008 Platform primitive handler obligation + +Status: **Follow-up**. + +Implementation evidence: + +- Platform model includes `handled_effects` and startup contract fields in + `../spore/crates/sporec-typeck/src/platform.rs:12-31`. +- CLI Platform handles `Console`, `FileRead`, `FileWrite`, `NetConnect`, + `NetListen`, `Env`, `Spawn`, `Clock`, `Random`, and `Exit` in + `../spore/crates/sporec-typeck/src/platform.rs:46-56`. +- Web Platform handles `Console`, `NetConnect`, `Random`, and `Clock` in + `../spore/crates/sporec-typeck/src/platform.rs:79-84`. +- Project manifests also model platform handled effects in + `../spore/crates/sporec-driver/src/project/mod.rs:184-217`. + +Review result: Platform handled effects exist. No scanned evidence shows the +new PR45 requirement that each state primitive has both a host handler family and +an in-memory mock handler family. + +## Primitive effect handler mapping + +| Primitive effect | Implementation status | Evidence | +| --- | --- | --- | +| `Cell[T]` | Missing as effect | `Cell` appears only as Rust `std::cell::Cell` in `../spore/crates/sporec-codegen/src/native.rs:1`; no effect-level `Cell` evidence found. | +| `Output[T]` | Missing as effect | Output is present as Rust/CLI output types, but no `Output` effect evidence found in scanned files. | +| `Map[K, V]` | Partial as value type, missing as effect | `Value::Map` exists in `../spore/crates/sporec-codegen/src/value.rs:26-27`; no effect-level `Map` evidence found. | +| `Clock` | Present as Platform handled effect | CLI and web handled effects include `Clock` in `../spore/crates/sporec-typeck/src/platform.rs:53-55` and `../spore/crates/sporec-typeck/src/platform.rs:81-84`. | +| `Random` | Present as Platform handled effect | CLI and web handled effects include `Random` in `../spore/crates/sporec-typeck/src/platform.rs:53-55` and `../spore/crates/sporec-typeck/src/platform.rs:81-84`. | + +## SEP-0010 explain protocol + +Status: **Partial**. + +Implementation evidence: + +- Low-level `sporec explain` command exists in + `../spore/crates/sporec-driver/src/main.rs:68-74` and dispatches in + `../spore/crates/sporec-driver/src/main.rs:93-94`. +- `exec_explain` normalizes a diagnostic code and renders explanation data in + `../spore/crates/sporec-driver/src/main.rs:290-300`. +- Error codes expose `explain()` in `../spore/crates/sporec-typeck/src/error.rs:202-204`. +- A test covers `sporec explain E0001` in + `../spore/crates/sporec-driver/tests/sporec_cli_tests.rs:437-442`. +- Project CLI evidence in the scan did not show a user-facing `spore explain` + command. +- No `ConceptDoc`, `ConceptRegistry`, `concept_refs`, `repair`, or + `explanation_key` implementation evidence was found in the scan. + +Review result: the implementation has a diagnostic-code explanation seed, but +SEP-0010's concept query protocol and teaching metadata remain design-forward. + +## SPARK.md and syntax decision conflicts + +Status: **No direct file edits performed**. + +Cross-check scope was review-only. The collected evidence from sibling code +matches prior context: user-level mutation is absent from the language surface, +while runtime internals use `RefCell` for channels and tasks. Examples: + +- `TaskHandle` and `ChannelEndpoint` carry `Rc>` in + `../spore/crates/sporec-codegen/src/interpret/eval.rs:247-282`. +- `Value::Map` exists as runtime data in + `../spore/crates/sporec-codegen/src/value.rs:26-27`. + +No implementation edit is recommended in this PR. These are follow-up +classification points only. + +## Follow-up recommendations + +| Priority | Recommendation | Kind | +| --- | --- | --- | +| P1 | Decide implementation migration path from `spec` to `properties`, or document compatibility. | change implementation | +| P1 | Migrate cost-vector user surface and HoleReport JSON vocabulary to realization-shape budgets when PR45 text lands. | change implementation | +| P1 | Add property context to HoleInfo/HoleReport if PR45 keeps it normative. | change implementation | +| P1 | Audit handler implementation for any field mutation semantics and align with immutable handler field policy. | change implementation | +| P2 | Add primitive-effect Platform conformance model for `Cell`, `Output`, `Map`, `Clock`, and `Random`. | write issue / change implementation | +| P2 | Extend low-level `sporec explain` into user-facing `spore explain` plus ConceptDoc registry if SEP-0010 remains in PR45. | write issue / change implementation | +| P2 | Add implementation docs clarifying no default Platform if current compatibility paths are retained. | write issue | + +## Aligned samples + +At least three patch-plan themes already have implementation anchors: + +1. Effect declarations and `uses` are already parsed and checked at the concept + level (`Token::Uses`, `uses_clause`, `Expr::Perform`). +2. Rich HoleReport data exists through `HoleInfo` and `hole_info_json`. +3. Platform handled effects exist and include `Clock` and `Random`. +4. Low-level diagnostic explanations exist through `sporec explain`. + +## Follow-up samples + +At least three patch-plan themes need implementation follow-up: + +1. `properties` plus strict `Bool` body typing and Claim/Evidence lowering. +2. Named realization-shape `budget` replacing old `cost` vector vocabulary. +3. Primitive effect conformance for `Cell`, `Output`, and effect-level `Map`. +4. SEP-0010 ConceptDoc and concept-reference metadata. + +## Review conclusion + +The PR45 design patch can proceed without modifying sibling implementation. The +largest implementation gaps are vocabulary and protocol migrations rather than +missing parser scaffolding: handlers, effects, holes, platforms, diagnostics, +and low-level explanations all have partial anchors. The riskiest divergence is +that PR45 now selects immutable handler fields with state routed through +primitive effects, while the implementation still carries handler payloads and +older stateful examples. Track that as a P1 implementation follow-up after the +spore-evolution design patch lands. diff --git a/.spark/artifacts/d1-property-spec.md b/.spark/artifacts/d1-property-spec.md new file mode 100644 index 0000000..6982371 --- /dev/null +++ b/.spark/artifacts/d1-property-spec.md @@ -0,0 +1,223 @@ +# D1 property expression specification draft + +This artifact turns the D1 decision into patch-ready text for SEP-0001, +SEP-0002, and SEP-0006. It does not edit the SEP files directly. + +## Locked decision + +D1 is fixed as follows: + +- A source property body is an ordinary Spore expression. +- The property body result type is strictly `Bool`. +- The property body is checked in the enclosing callable's effect context. +- The property body can perform only effects included by the enclosing + `uses` surface or discharged by a local handler context. +- A non-`Bool` property body is a compile-time typing error. +- A property body that the checker cannot decide lowers to an evidence claim + with result `unknown`; this state does not by itself reject compilation. +- Source properties and refinement obligations share the SEP-0006 `Claim` and + `EvidenceRecord` projection. + +## Evidence from current text + +| Document | Current evidence | D1 impact | +| --- | --- | --- | +| SEP-0001 | `properties` follows `uses` and `budget` in fixed intent-signature order at `seps/SEP-0001-core-syntax.md:67-83`. | The fixed order stays unchanged. | +| SEP-0001 | The reference layout says `[properties { (): , ... }]` at `seps/SEP-0001-core-syntax.md:227-230`. | The grammar already allows an arbitrary expression after `:`. | +| SEP-0001 | `PropertyItem = Ident "(" [ PropertyParamList ] ")" ":" Expr` at `seps/SEP-0001-core-syntax.md:286-287`. | The syntax stays, but the semantic result type must be specified as `Bool`. | +| SEP-0002 | Refinement obligations lower into a Claim when not decidable immediately at `seps/SEP-0002-type-system.md:209-213`. | Property obligations should use the same claim/evidence channel. | +| SEP-0006 | Source properties lower into Claims at `seps/SEP-0006-compiler-architecture.md:64-77`. | This is the owner of source property lowering. | +| SEP-0006 | EvidenceRecord includes `passed`, `failed`, `unknown`, and `skipped` under result at `seps/SEP-0006-compiler-architecture.md:128-160`. | D1 uses `unknown` for undecided property checks. | +| SEP-0006 | Diagnostics include property and claim diagnostics at `seps/SEP-0006-compiler-architecture.md:219-227`. | Non-`Bool` properties produce type diagnostics; failed checks produce property diagnostics. | + +## SEP-0001 patch draft + +### Target + +Patch the EBNF area around `seps/SEP-0001-core-syntax.md:286-290`. + +### Keep + +Keep the source grammar shape: + +```ebnf +PropertiesBlock = "properties" "{" { PropertyItem } "}" ; +PropertyItem = Ident "(" [ PropertyParamList ] ")" ":" Expr ; +PropertyParamList = PropertyParam { "," PropertyParam } [ "," ] ; +PropertyParam = Ident ":" TypeExpr ; +``` + +### Add after the grammar block + +```markdown +`PropertyItem` syntax accepts an ordinary expression after `:`. SEP-0002 owns +its typing rule and SEP-0006 owns its lowering into claims and evidence. The +expression is not a separate proof sublanguage. +``` + +### Add to the delegation paragraph + +Current text at `seps/SEP-0001-core-syntax.md:82-83` delegates property +semantics to SEP-0006. Replace it with: + +```markdown +Clause semantics are delegated: `uses` -> SEP-0003, `budget` -> SEP-0004, +and `properties` -> SEP-0002 for expression typing plus SEP-0006 for +claim/evidence lowering. +``` + +### Remove or replace unresolved wording + +SEP-0001 currently asks which property expression subset is accepted at +`seps/SEP-0001-core-syntax.md:435`. Replace that question with: + +```markdown +How should the compiler present `unknown` property evidence when a checker cannot +decide an otherwise well-typed `Bool` property body? +``` + +Reason: the expression subset is no longer open. D1 allows ordinary Spore +expressions and relies on type, effect, and evidence checks. + +## SEP-0002 patch draft + +### Target + +Add a subsection after `### Outcome typing` and before `### Refinement +obligations`, near `seps/SEP-0002-type-system.md:191-213`. + +### New subsection + +````markdown +### Property body typing + +A source property body is an ordinary Spore expression checked under the +surrounding callable's type environment. + +If a property item is written as: + +```spore +properties { + name(params...): body +} +``` + +then `body` must check against `Bool`. A body with any other result type is a +type error. Property parameters introduce local bindings with the declared types +for the property body only; they do not change the callable's Base Signature. + +The property body inherits the enclosing callable's effect context. It may only +perform effects included in the enclosing `uses` surface or effects discharged +by a narrower local handler context. A property body that requires an unavailable +effect is an effect-checking error. + +The type checker does not need to decide whether every well-typed property is +true. It only establishes that the property is a `Bool` expression in the right +type and effect context. Decidable failures may become immediate diagnostics; +undecidable obligations lower into SEP-0006 claims. +```` + +### Link refinement obligations to the same channel + +Current text at `seps/SEP-0002-type-system.md:209-213` says undecidable +refinements lower into a Claim. Extend the paragraph with: + +```markdown +This is the same claim/evidence channel used for source properties. Source +properties and refinement obligations differ in where they come from, not in the +shape of the downstream claim record. +``` + +## SEP-0006 patch draft + +### Target + +Patch `### Properties become claims` around +`seps/SEP-0006-compiler-architecture.md:64-77` and `### EvidenceRecord +structure` around `seps/SEP-0006-compiler-architecture.md:128-160`. + +### Replace the lowering paragraph + +Current text says the parser records a source property and the compiler lowers +it into an internal Claim. Replace the paragraph with: + +```markdown +The parser records a source property. SEP-0002 first checks the property body as +an ordinary Spore expression whose result type must be `Bool` and whose required +effects must fit inside the enclosing effect context. After that check, the +compiler lowers the property into an internal `Claim` with normalized subject, +parameters, predicate expression, effect context, and source span. +``` + +### Add a shared-origin paragraph + +Add after the lowering paragraph: + +```markdown +A `Claim` may originate from a source `properties` item, a refinement obligation, +a budget check, an effect check, or a validator. Source properties and refinement +obligations share the same `Claim` and `EvidenceRecord` structure so tools do +not need a separate property protocol. +``` + +### Add result semantics + +Add after the `EvidenceRecord` tree near `seps/SEP-0006-compiler-architecture.md:128-160`: + +```markdown +For property claims, `result.passed` means the checker established the property +for the checked subject. `result.failed` means the checker produced a failing +case, counter-witness, or direct contradiction. `result.unknown` means the +property was well typed but the checker could not decide it with the available +analysis or evidence. `result.skipped` means the checker did not run. + +A well-typed property with `unknown` evidence remains visible to tools and +reviewers. It is not the same as a type error and does not by itself reject +compilation. Release gates and package policies may choose stricter treatment +for unknown evidence. +``` + +### Diagnostics split + +Add to the diagnostics impact section near `seps/SEP-0006-compiler-architecture.md:219-227`: + +```markdown +A property body that does not check as `Bool` is reported as a type diagnostic +because the source expression is ill typed. A property body that checks as +`Bool` but fails or remains unknown is reported as a property or claim diagnostic +because the source shape is valid and the evidence result is the issue. +``` + +## Open questions removed by D1 + +D1 closes these previous ambiguities: + +- Property bodies are not restricted to a verifier-only expression subset. +- Property bodies are not assumed to be pure unless the enclosing effect context + is pure. +- Non-`Bool` property bodies are not interpreted as truthy values. +- Unknown evidence is not a compile-time type failure. + +## Compatibility notes for the sibling implementation + +The sibling implementation currently groups examples and properties under +`spec`, for example `sporec-parser/src/ast.rs:159-166` and +`spore/src/main.rs:208-211`. This artifact does not ask the implementation to +rename that surface. It only records that PR45's design vocabulary is +`properties` plus `Claim`/`EvidenceRecord`. + +## Routing to the patch plan + +`@sep-patch-plan` should include these concrete edits: + +1. SEP-0001 delegation paragraph update. +2. SEP-0001 note that `PropertyItem` body is ordinary `Expr`, not a proof + sublanguage. +3. SEP-0001 unresolved question replacement. +4. SEP-0002 new `Property body typing` subsection. +5. SEP-0002 refinement-obligation paragraph extension. +6. SEP-0006 property-to-claim lowering replacement. +7. SEP-0006 shared-origin paragraph. +8. SEP-0006 property evidence result semantics. +9. SEP-0006 diagnostics split for non-`Bool` versus failed or unknown property + evidence. diff --git a/.spark/artifacts/d10-explain-boundary-spec.md b/.spark/artifacts/d10-explain-boundary-spec.md new file mode 100644 index 0000000..767ed89 --- /dev/null +++ b/.spark/artifacts/d10-explain-boundary-spec.md @@ -0,0 +1,164 @@ +# D10 explain boundary specification draft + +This artifact defines the boundary between SEP-0005 HoleReport records and +SEP-0010 compiler-as-documentation projections. It does not edit SEP files +directly. + +## Locked decision + +- SEP-0005 owns hole syntax, HoleReport records, and hole dependency graphs. +- SEP-0010 owns ConceptDoc records, concept references, diagnostic teaching + metadata, `spore explain`, and educational projections over compiler records. +- SEP-0010 may render or explain HoleReport data, but it must not define a + second hole protocol. +- SEP-0006 remains the owner of shared compiler records and diagnostic code + families. SEP-0010 layers teaching metadata over them. + +## Evidence from current text + +| Document | Current evidence | Boundary impact | +| --- | --- | --- | +| SEP-0005 | HoleReport is the collaboration boundary at `seps/SEP-0005-hole-system.md:52-54`. | SEP-0005 owns the data protocol. | +| SEP-0005 | Per-hole fields are listed at `seps/SEP-0005-hole-system.md:112-134`. | Field definitions stay in SEP-0005. | +| SEP-0005 | Single-hole queries return the same per-hole object directly at `seps/SEP-0005-hole-system.md:199-203`. | Query shape stays HoleReport-owned. | +| SEP-0005 | Educational renderings are owned by SEP-0010 at `seps/SEP-0005-hole-system.md:205-207`. | Existing text already points the teaching layer to SEP-0010. | +| SEP-0006 | Shared machine records include `Diagnostic`, `HoleReport`, `Claim`, `EvidenceRecord`, and `VerificationBundle` at `seps/SEP-0006-compiler-architecture.md:201-210`. | SEP-0010 must layer over these records, not replace them. | +| SEP-0006 | Diagnostic teaching metadata and `spore explain` behavior are owned by SEP-0010 at `seps/SEP-0006-compiler-architecture.md:228-230`. | SEP-0006 already delegates teaching metadata. | +| SEP-0010 | Summary says diagnostics and HoleReports link to concepts through stable concept references at `seps/SEP-0010-compiler-as-documentation.md:23-39`. | Concept refs are teaching metadata. | +| SEP-0010 | Motivation says SEP-0005 HoleReports are self-contained and SEP-0010 adds the teaching layer at `seps/SEP-0010-compiler-as-documentation.md:46-55`. | Clear dependency: SEP-0010 depends on SEP-0005. | +| SEP-0010 | Hole teaching projection states that HoleReports remain SEP-0005 records at `seps/SEP-0010-compiler-as-documentation.md:114-135`. | Keep this wording, but make the projection boundary sharper. | +| SEP-0010 | Reference text says the educational projection is not a second hole protocol at `seps/SEP-0010-compiler-as-documentation.md:188-194`. | This should become the central boundary sentence. | +| SEP-0010 | Agent impact says expected types come from HoleReport and concept docs explain the rules at `seps/SEP-0010-compiler-as-documentation.md:219-223`. | Agents consume HoleReport data plus ConceptDoc teaching metadata. | + +## Responsibility table + +| Responsibility | Owner SEP | Notes | +| --- | --- | --- | +| Hole expression syntax (`?name`, `?name: Type`) | SEP-0005 | SEP-0010 may explain it, but cannot change syntax. | +| Per-hole field names and meanings | SEP-0005 | Includes expected type, bindings, effect context, budget context, property context, candidates, dependent holes, confidence, and rejection reasons. | +| Batch and single-hole query shape | SEP-0005 | SEP-0010 may render the result, not redefine it. | +| Hole dependency graph | SEP-0005 | Concept docs may link to `holes` or `hole-dependency-graph`. | +| Candidate workflow notes | SEP-0005 informational appendix | D5 demotes workflow to guidance. SEP-0010 may teach the workflow as concept docs if needed. | +| Reference candidate ranker | SEP-0005 informational appendix | The ranker is not an explain protocol field. | +| Diagnostic code families | SEP-0006 | SEP-0010 attaches concept refs and repair metadata to diagnostics. | +| Shared record list | SEP-0006 | `Diagnostic`, `HoleReport`, `Claim`, `EvidenceRecord`, `VerificationBundle`. | +| `ConceptDoc` schema | SEP-0010 | Does not alter program identity or HoleReport schema. | +| `ConceptRegistry` schema | SEP-0010 | Later schema publication can live under `schemas/contracts`. | +| `concept_refs`, `repair`, `explanation_key` | SEP-0010 | Optional teaching metadata over diagnostics and projections. | +| `spore explain` query resolution | SEP-0010 | Query resolution maps codes, concepts, surface symbols, and aliases to teaching records. | +| Human-readable hole teaching projection | SEP-0010 | Projection over SEP-0005 data; not a second hole protocol. | +| LSP hover or terminal prose for holes | SEP-0010 projection | Renderer-specific, based on SEP-0005 data plus ConceptDoc metadata. | + +## SEP-0005 cross-reference patch draft + +### Keep and strengthen the existing boundary sentence + +Current text at `seps/SEP-0005-hole-system.md:205-207` already says educational +renderings belong to SEP-0010. Replace it with: + +```markdown +Human-facing educational renderings of HoleReport records are owned by SEP-0010. +Those renderings must stay projections over the same HoleReport data rather than +forming a separate hole protocol. SEP-0005 remains the owner of per-hole field +names, dependency graph shape, and batch/single-hole query shape. +``` + +### Add a reference near the normative field table + +After the normative HoleReport field table introduced by `@d5-agent`, add: + +```markdown +SEP-0010 may attach concept references and render these fields for teaching, but +it does not add required HoleReport fields. Optional teaching metadata is outside +the normative HoleReport schema unless a later SEP moves it here. +``` + +## SEP-0010 cross-reference patch draft + +### Strengthen the Hole teaching projection section + +Replace the first paragraph of `### Hole teaching projection` near +`seps/SEP-0010-compiler-as-documentation.md:188-194` with: + +```markdown +The educational projection of a HoleReport is a rendering over SEP-0005 fields. +It should include expected type, source location, visible bindings, effect +context, budget context, property context, candidates, dependent holes, and +rejection reasons when those fields are present. SEP-0010 may choose which fields +to highlight for a user-facing explanation, but it does not define different +field names or a second hole query payload. +``` + +### Add a normative-owner sentence + +Add after the projection paragraph: + +```markdown +When SEP-0005 and SEP-0010 appear to overlap, SEP-0005 owns the data contract +and SEP-0010 owns the teaching projection over that contract. +``` + +### Add related SEP guidance to ConceptDoc + +In the `ConceptDoc fields` table, add this explanatory paragraph: + +```markdown +For hole-related concept docs, `related_seps` should include SEP-0005 when the +concept explains HoleReport data and SEP-0010 when the concept explains the +teaching or query layer. +``` + +## SEP-0006 cross-reference patch draft + +### Keep shared-record ownership clear + +Current text at `seps/SEP-0006-compiler-architecture.md:201-210` says default +text, JSON, LSP, and watch outputs render shared records, while SEP-0010 owns +the concept registry and explain protocol. Add: + +```markdown +SEP-0010 teaching metadata is optional metadata over these shared records. It +does not change the identity, hash, or required fields of `Diagnostic`, +`HoleReport`, `Claim`, `EvidenceRecord`, or `VerificationBundle`. +``` + +## Migration candidate + +One current SEP-0010 sentence can be made safer during the patch step: + +- Current: `Every user-facing diagnostic should map to at least one concept id` + at `seps/SEP-0010-compiler-as-documentation.md:291-292`. +- Suggested: `Every user-facing diagnostic produced by the compiler should have + a path to at least one concept id, either directly or through its diagnostic + family.` + +Reason: the original wording may overconstrain early diagnostics and generated +third-party diagnostics. The revised wording preserves the teaching goal while +allowing family-level concepts. + +## Non-overlap rule + +Use this rule in the later patch plan: + +```text +If a paragraph names fields, schemas, dependency edges, or batch/single-hole +payload shape, it belongs in SEP-0005. If a paragraph names concept ids, repair +hints, explain queries, hover prose, terminal teaching lines, or ConceptDoc +records, it belongs in SEP-0010. +``` + +## Routing to the patch plan + +`@sep-patch-plan` should include these concrete edits: + +1. SEP-0005 strengthens the existing SEP-0010 rendering boundary sentence. +2. SEP-0005 adds a note after the normative field table that teaching metadata + is not part of required HoleReport fields. +3. SEP-0010 strengthens `### Hole teaching projection` to say it renders + SEP-0005 fields and does not define alternate field names. +4. SEP-0010 adds the explicit owner rule: SEP-0005 owns data, SEP-0010 owns + teaching projection. +5. SEP-0010 adds `related_seps` guidance for hole-related ConceptDocs. +6. SEP-0006 adds an optional-metadata sentence for SEP-0010 teaching fields. +7. SEP-0010 softens the `Every user-facing diagnostic` line to allow + diagnostic-family concepts. diff --git a/.spark/artifacts/d3-handler-spec.md b/.spark/artifacts/d3-handler-spec.md new file mode 100644 index 0000000..9de8762 --- /dev/null +++ b/.spark/artifacts/d3-handler-spec.md @@ -0,0 +1,260 @@ +# D3 handler state specification draft + +This artifact turns the D3 decision into patch-ready text for SEP-0001 and +SEP-0003. It depends on the primitive-effect draft and does not edit SEP files +directly. + +## Locked decision + +D3 is fixed as follows: + +- User-level Spore does not introduce `mut`, `mut self`, or mutable handler + fields. +- Handler fields may exist, but they are immutable runtime configuration. +- Handler methods write the receiver explicitly as the first parameter: + `self` or `self: Self`. The receiver is read-only. +- `self.field = expr` is not valid handler syntax. +- Stateful mock and instrumentation use cases route through state primitive + effects (`Cell`, `Output`, `Map`, `Clock`, `Random`). +- Handler instances are task-local. A handler installed inside one task is not + visible to sibling tasks unless installed again. +- Handler fields do not participate in signature hash or intent hash. They are + instance payload, not callable boundary. + +## Evidence from current text + +| Document | Current evidence | D3 impact | +| --- | --- | --- | +| SEP-0001 | Current grammar uses `handler Ident for SurfaceExpr` and `HandlerItem = fn QualifiedIdent ...` at `seps/SEP-0001-core-syntax.md:312-316`. | Needs to align with accepted `handles`, `uses`, fields, and `impl Effect` blocks. | +| SEP-0001 | Grammar notes say handler items must name effect operations with qualified identifiers at `seps/SEP-0001-core-syntax.md:333-338`. | Replace with `impl Effect` blocks and explicit receiver method shape. | +| SEP-0003 | Current handler example mutates `self.output.push(msg)` at `seps/SEP-0003-effect-system.md:101-115`. | Replace with primitive-effect based examples. | +| SEP-0003 | Handler checking requires implementing every operation of discharged atomic effects at `seps/SEP-0003-effect-system.md:143-146`. | Keep this rule; add receiver/state policy. | +| Sibling parser AST | `Expr::Handle` owns named `use` bindings and inline `on` arms at `../spore/crates/sporec-parser/src/ast.rs:296-326`. | PR45 should acknowledge `handle ... with { use ..., on ... }` expression shape. | +| Sibling parser AST | `HandlerDef` has `fields`, `handles_clause`, optional `uses_clause`, and `impls` at `../spore/crates/sporec-parser/src/ast.rs:483-499`. | PR45 grammar should converge on this structure, with immutable-state policy added. | +| Sibling parser tests | A handler example with fields, `handles`, `uses`, and `impl Console` appears at `../spore/crates/sporec-parser/tests/parser_tests.rs:486-500`. | This is the implementation shape to document, except method receivers need PR45 policy. | +| Primitive draft | Primitive state endpoints are `Cell`, `Output`, `Map`, `Clock`, `Random` in `.spark/artifacts/primitive-effects-spec.md`. | Mock examples should use these effects instead of handler field mutation. | + +## SEP-0001 grammar patch draft + +### Target + +Replace the handler grammar around `seps/SEP-0001-core-syntax.md:312-316`. + +### Replacement grammar + +```ebnf +HandlerDecl = { Attribute } [ Visibility ] "handler" Ident + [ "(" [ FieldDecl { "," FieldDecl } [ "," ] ] ")" ] + HandlesClause [ UsesClause ] + "{" { HandlerImplBlock } "}" ; +HandlesClause = "handles" SurfaceExpr ; +HandlerImplBlock = "impl" Ident [ TypeArgs ] + "{" { HandlerMethod } "}" ; +HandlerMethod = "fn" Ident [ TypeParams ] + "(" ReceiverParam [ "," ParamList ] ")" + "->" TypeExpr ( Block | ";" ) ; +``` + +### Grammar notes update + +Replace the handler grammar note with: + +```markdown +Handler declarations use `handles` to name the discharged effect surface and may +use `uses` to declare effects required by handler method bodies. Handler fields +are immutable instance payload. Handler methods live inside `impl Effect { ... }` +blocks and write `self` as the first parameter. The receiver is read-only; this +SEP does not introduce `mut self` or field assignment. +``` + +### Handle expression syntax note + +Add this note near expression forms when expression grammar is expanded: + +````markdown +A `handle` expression installs named handler instances and inline arms for a +lexical scope: + +```spore +handle { body } with { + use HandlerName { field: value }, + on Effect.operation(param) => arm_body +} +``` + +Named `use` entries instantiate a handler payload. Inline `on` arms handle a +single effect operation directly. SEP-0003 owns the checking rules. +```` + +## SEP-0003 handler patch draft + +### Replace the current handler example + +Current example at `seps/SEP-0003-effect-system.md:101-115` uses +`self.output.push(msg)`. Replace it with: + +```spore +effect Console { + fn println(msg: Str) -> (); +} + +effect Output[T] { + fn emit(value: T) -> (); +} + +handler MockConsole handles [Console] uses [Output[Str]] { + impl Console { + fn println(self, msg: Str) -> () { + perform Output.emit(msg) + } + } +} + +handle { + greet("spore") +} with { + use MockConsole {} +} +``` + +This example intentionally omits the `Output[Str]` handler. The enclosing test +or Platform scope must install it explicitly. + +### New subsection: Handler state policy + +Add after the guide-level handler example: + +```markdown +### Handler state policy + +Handler fields are immutable runtime configuration. Handler method bodies may +read `self` and fields on `self`, but they may not assign to `self.field` or +otherwise update handler instance payload. Spore does not add `mut`, `mut self`, +or mutable handler fields for stateful handlers. + +Stateful handler use cases route through state primitive effects such as +`Cell`, `Output`, `Map`, `Clock`, and `Random`. A handler that needs state lists +the relevant primitive effects in its own `uses` clause and performs those +effects in method bodies. +``` + +### New subsection: Handler instance visibility + +Add to the reference-level handler section: + +```markdown +Handler instances are lexical and task-local. Installing a handler with +`handle ... with` affects only the dynamic extent of that expression within the +current task. Spawned or sibling tasks do not inherit the handler instance unless +that handler is installed in their own dynamic extent. +``` + +### New subsection: Handler fields and hashes + +Add near structured representation or hash references: + +```markdown +Handler fields are instance payload and do not participate in `signature_hash`, +`intent_hash`, or `property_hash`. Hashes cover callable boundaries and intent +metadata; handler payload values are runtime configuration for a specific +installation. +``` + +## Mock examples + +### Console capture + +```spore +effect Console { + fn println(msg: Str) -> (); +} + +effect Output[T] { + fn emit(value: T) -> (); +} + +handler CaptureConsole handles [Console] uses [Output[Str]] { + impl Console { + fn println(self, msg: Str) -> () { + perform Output.emit(msg) + } + } +} +``` + +### Counter + +```spore +effect Cell[T] { + fn get() -> T; + fn set(value: T) -> (); +} + +handler CountingConsole handles [Console] uses [Cell[I64]] { + impl Console { + fn println(self, msg: Str) -> () { + let n = perform Cell.get(); + perform Cell.set(n + 1) + } + } +} +``` + +### Memo table + +```spore +effect Compute { + fn run(input: Input) -> Output; +} + +effect Map[K, V] { + fn get(key: K) -> Option[V]; + fn put(key: K, value: V) -> (); +} + +handler MemoCompute handles [Compute] uses [Map[Input, Output]] { + impl Compute { + fn run(self, input: Input) -> Output { + match perform Map.get(input) { + Some(value) => value, + None => { + let value = expensive(input); + perform Map.put(input, value); + value + } + } + } + } +} +``` + +These examples use placeholder primitive method names from the primitive-effect +draft. The later patch step may choose different exact spellings. + +## Sibling implementation alignment notes + +| Implementation point | Alignment | +| --- | --- | +| `HandlerDef.fields` at `../spore/crates/sporec-parser/src/ast.rs:493-499` | Keep fields, but specify that they are immutable instance payload. | +| `handles_clause` and `uses_clause` at `../spore/crates/sporec-parser/src/ast.rs:493-499` | Align PR45 grammar with existing parser structure. | +| `HandlerImpl { effect, methods }` at `../spore/crates/sporec-parser/src/ast.rs:483-489` | Use `impl Effect { fn operation(self, ...) ... }` in PR45 grammar. | +| `Expr::Handle { body, handlers }` at `../spore/crates/sporec-parser/src/ast.rs:296-306` | Acknowledge `handle ... with` expression in SEP-0001/0003. | +| `HandleBinding::Use` and `HandleBinding::On` at `../spore/crates/sporec-parser/src/ast.rs:310-326` | Keep named and inline handler installation forms. | +| Parser test at `../spore/crates/sporec-parser/tests/parser_tests.rs:486-500` | Good shape for fields/handles/uses/impl; method receiver remains a future implementation follow-up. | + +## Routing to the patch plan + +`@sep-patch-plan` should include these concrete edits: + +1. SEP-0001 replaces handler grammar with fields, `handles`, optional `uses`, + and `impl Effect` blocks. +2. SEP-0001 updates grammar notes to say handler fields are immutable and method + receivers are explicit read-only `self`. +3. SEP-0001 acknowledges `handle ... with { use ..., on ... }` expression shape. +4. SEP-0003 replaces the `self.output.push` example with a primitive-effect + `Output` example. +5. SEP-0003 adds handler state policy text. +6. SEP-0003 adds task-local handler instance visibility. +7. SEP-0003 adds the handler field hash boundary. +8. GLOSSARY adds or updates handler entries for immutable fields and task-local + instances if needed. diff --git a/.spark/artifacts/d5-agent-spec.md b/.spark/artifacts/d5-agent-spec.md new file mode 100644 index 0000000..5126863 --- /dev/null +++ b/.spark/artifacts/d5-agent-spec.md @@ -0,0 +1,201 @@ +# D5 HoleReport protocol boundary draft + +This artifact turns the D5 decision into patch-ready text for SEP-0005. It does +not edit the SEP files directly. + +## Locked decision + +D5 is fixed as follows: + +- SEP-0005 normatively defines the HoleReport schema and the hole dependency + graph. +- The Agent realization workflow is informational guidance, not a required + protocol state machine. +- Candidate ranking and scoring are informational reference behavior, not a + required scoring contract. +- Human-facing teaching projections of HoleReport data belong to SEP-0010. +- The SEP should be renamed from `Hole System & Agent Protocol` to + `Hole System & HoleReport Protocol`. +- The file should be renamed from `seps/SEP-0005-hole-system.md` to + `seps/SEP-0005-hole-report-protocol.md` during the later patch step. + +## Evidence from current text + +| Document | Current evidence | D5 impact | +| --- | --- | --- | +| SEP-0005 front matter | Title is `SEP-0005: Hole System & Agent Protocol` at `seps/SEP-0005-hole-system.md:2-3`. | Rename to put the normative protocol in the title. | +| SEP-0005 heading | Main heading repeats `Hole System & Agent Protocol` at `seps/SEP-0005-hole-system.md:18`. | Rename the heading with the front matter. | +| SEP-0005 motivation | HoleReport is named as the collaboration boundary at `seps/SEP-0005-hole-system.md:52-54`. | This should become the normative center. | +| SEP-0005 workflow | Agent workflow states `DISCOVER -> ANALYZE -> PROPOSE -> VERIFY -> ACCEPT or REJECT` at `seps/SEP-0005-hole-system.md:90-99`. | Move to an informational appendix. | +| SEP-0005 fields | Per-hole fields are listed at `seps/SEP-0005-hole-system.md:112-134`. | Keep and strengthen as the normative core. | +| SEP-0005 / SEP-0010 | Educational renderings are owned by SEP-0010 at `seps/SEP-0005-hole-system.md:205-207`. | Keep as the boundary sentence and cross-link it in the rename patch. | +| Sibling type checker | `CandidateScore::overall` uses `0.40`, `0.20`, `0.25`, `0.15` in `../spore/crates/sporec-typeck/src/hole.rs:20-47`. | Treat as reference ranker, not required protocol. | +| Sibling type checker | `HoleInfo` fields appear in `../spore/crates/sporec-typeck/src/hole.rs:124-170`. | Use this as an implementation comparison for the HoleReport field table. | +| Sibling JSON projection | `hole_info_json` maps type-checker data to JSON in `../spore/crates/sporec-driver/src/compiler/hole_json.rs:94-171`. | Use this as evidence that HoleReport schema should stay self-contained. | + +## Rename draft + +### File and index changes for the later patch step + +- Rename `seps/SEP-0005-hole-system.md` to + `seps/SEP-0005-hole-report-protocol.md`. +- Update `seps-index.json` path for SEP 5. +- Update `README.md` links that mention `seps/SEP-0005-hole-system.md`. +- Update any cross-references from other SEP files to the old file path. + +### Front matter and heading + +Replace the front matter title and H1 with: + +```markdown +title: "SEP-0005: Hole System & HoleReport Protocol" +``` + +```markdown +# SEP-0005: Hole System & HoleReport Protocol +``` + +### Executive summary replacement + +Replace the current executive summary with: + +```markdown +> **Executive Summary**: Defines holes as typed absence constrained by Base +> Signature and Intent Signature context, and normatively specifies the +> HoleReport records emitted for those holes. HoleReport exposes expected type, +> visible bindings, effect context, budget context, property context, +> candidates, dependencies, and confidence data. Agent workflows and candidate +> rankers are informative projections over this data rather than a required +> protocol state machine. +``` + +## Normative scope banner + +Add this after the summary paragraph: + +```markdown +This SEP normatively defines hole syntax, HoleReport records, and the hole +dependency graph. It does not normatively define an Agent workflow state +machine, candidate scoring formula, or human teaching projection. Those topics +are informative guidance here or belong to SEP-0010 when they teach users how to +read HoleReport data. +``` + +## HoleReport core draft + +### Keep the existing field list + +Keep the field table at `seps/SEP-0005-hole-system.md:112-134`, but retitle it +from `### HoleReport fields` to: + +```markdown +### Normative HoleReport fields +``` + +### Add JSON-shape language + +Add this text after the field table: + +```markdown +The per-hole object is the normative schema boundary for tools. JSON producers +may add versioned optional fields, but they must preserve the meaning of these +fields when present. A batch response wraps per-hole objects in a `holes` array +and may include a `dependency_graph` object. A single-hole query returns one +per-hole object directly. +``` + +### Field mapping against the sibling implementation + +| SEP-0005 field | Sibling evidence | Notes | +| --- | --- | --- | +| `name` | `HoleInfo.name` at `../spore/crates/sporec-typeck/src/hole.rs:126-127`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:95-97`. | Aligned. | +| `display_name` | JSON derives display name in `../spore/crates/sporec-driver/src/compiler/hole_json.rs:25-31` and maps it at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:95-97`. | Aligned in JSON, not stored in type-checker `HoleInfo`. | +| `location` | `SourceLocation` and `HoleInfo.location` at `../spore/crates/sporec-typeck/src/hole.rs:10-18` and `../spore/crates/sporec-typeck/src/hole.rs:128-131`. | Aligned. | +| `expected_type` | `HoleInfo.expected_type` at `../spore/crates/sporec-typeck/src/hole.rs:132-133`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:99-100`. | Aligned. | +| `type_inferred_from` | `HoleInfo.type_inferred_from` at `../spore/crates/sporec-typeck/src/hole.rs:134-135`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:100-101`. | Aligned. | +| `function` | `HoleInfo.function` at `../spore/crates/sporec-typeck/src/hole.rs:136-137`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:101-102`. | Aligned. | +| `enclosing_signature` | `HoleInfo.enclosing_signature` at `../spore/crates/sporec-typeck/src/hole.rs:138-139`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:102-103`. | Aligned. | +| `bindings` | `HoleInfo.bindings` at `../spore/crates/sporec-typeck/src/hole.rs:140-141`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:103-108`. | Aligned. | +| `binding_dependencies` | `HoleInfo.binding_dependencies` at `../spore/crates/sporec-typeck/src/hole.rs:142-143`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:109`. | Aligned. | +| `effect_context` | `EffectContext` and `HoleInfo.effect_context` at `../spore/crates/sporec-typeck/src/hole.rs:103-107` and `../spore/crates/sporec-typeck/src/hole.rs:146-149`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:111-117`. | Partially aligned; SEP uses declared/expanded/active/discharged vocabulary, implementation uses discharged/surviving vocabulary. | +| `budget_context` | `CostBudget`, `ResidualContext`, and `HoleInfo.cost_budget`/`residual_context` at `../spore/crates/sporec-typeck/src/hole.rs:86-101` and `../spore/crates/sporec-typeck/src/hole.rs:150-153`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:118-129`. | Mismatch vocabulary: implementation still uses cost-vector terms. | +| `property_context` | No direct field in the scanned `HoleInfo` struct. | Gap for later implementation review. | +| `errors_to_handle` | `HoleInfo.errors_to_handle` at `../spore/crates/sporec-typeck/src/hole.rs:144-145`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:110`. | Aligned. | +| `candidates` | `HoleInfo.candidates` at `../spore/crates/sporec-typeck/src/hole.rs:154-155`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:130-160`. | Aligned as data. Ranking policy stays informative. | +| `dependent_holes` | `HoleInfo.dependent_holes` at `../spore/crates/sporec-typeck/src/hole.rs:156-157`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:162`. | Aligned. | +| `confidence` | `Confidence` and `HoleInfo.confidence` at `../spore/crates/sporec-typeck/src/hole.rs:52-73` and `../spore/crates/sporec-typeck/src/hole.rs:158-159`; JSON mapping at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:163-171`. | Aligned as data. | +| `rejection_reasons` | Candidate-level rejection reasons are in `CandidateScore.rejection_reasons` at `../spore/crates/sporec-typeck/src/hole.rs:20-35`; JSON maps them at `../spore/crates/sporec-driver/src/compiler/hole_json.rs:138-145`. | Shape differs: PR45 field is per-hole; implementation stores reasons per candidate and error cluster. | + +## Workflow demotion draft + +Move the `### Realization workflow` section at +`seps/SEP-0005-hole-system.md:90-99` to an appendix named: + +```markdown +## Informational appendix: Realization workflow notes +``` + +Use this introduction: + +```markdown +This workflow is an informative reference loop for Agents and tools. A conforming +HoleReport producer is not required to expose these states, and a conforming +Agent is not required to use this exact state machine. +``` + +Keep the existing state text, but replace the acceptance sentence with: + +```markdown +A proposed fill is reviewable when it is checked against type, effect, budget, +and property context from the HoleReport and either passes those checks or +produces evidence states that the selected policy accepts. +``` + +## Candidate ranker demotion draft + +Add an appendix after the workflow appendix: + +```markdown +## Informational appendix: Reference candidate ranker + +Implementations may rank candidates using the fields exposed by HoleReport. One +reference ranker combines type match, budget fit, required-effect fit, and error +coverage. This ranker is informative. Tools may use a different ranker when they +preserve the normative HoleReport data. +``` + +If the current sibling formula is mentioned, present it only as an example: + +```text +overall = 0.40 * type_match + + 0.20 * budget_fit + + 0.25 * required_effects_fit + + 0.15 * error_coverage +``` + +The formula corresponds to `CandidateScore::overall` in +`../spore/crates/sporec-typeck/src/hole.rs:36-42`, but PR45 should name the +budget dimension `budget_fit` rather than inherit the implementation's +`cost_fit` spelling. + +## SEP-0010 boundary note + +Do not expand this task into explain-protocol design. Keep the existing boundary +sentence at `seps/SEP-0005-hole-system.md:205-207` and let +`@d10-explain-boundary` refine cross-references. SEP-0005 owns the data record; +SEP-0010 owns educational rendering and explain queries. + +## Routing to the patch plan + +`@sep-patch-plan` should include these concrete edits: + +1. Rename SEP-0005 file path and update `seps-index.json`. +2. Rename SEP-0005 title and H1. +3. Replace the executive summary. +4. Add the normative scope banner. +5. Retitle `HoleReport fields` as normative. +6. Add JSON-shape text after the field table. +7. Move `Realization workflow` to an informational appendix. +8. Add `Reference candidate ranker` as an informational appendix. +9. Keep SEP-0010 rendering boundary text and let `@d10-explain-boundary` refine + cross-references. diff --git a/.spark/artifacts/pr45-vocab-map.md b/.spark/artifacts/pr45-vocab-map.md new file mode 100644 index 0000000..124a6df --- /dev/null +++ b/.spark/artifacts/pr45-vocab-map.md @@ -0,0 +1,103 @@ +# PR45 vocabulary map + +This artifact maps the current PR45 SEP vocabulary to the sibling `spore` +implementation vocabulary. It is descriptive evidence for the later design patch +plan; it does not propose source changes. + +## Scan scope + +- PR45 design repository: `spore-evolution` at `080b97e` on + `refine-vision-sep-process`. +- Design inputs: `seps/SEP-0000` through `seps/SEP-0010`, `GLOSSARY.md`, + `VISION.md`, `README.md`, and `drafts/script-mode.md`. +- Sibling implementation repository: `../spore`, especially + `crates/sporec-parser`, `crates/sporec-typeck`, `crates/sporec-driver`, + `crates/sporec-codegen`, `crates/spore-lsp`, and `crates/spore`. + +## Reproducible scans + +Representative commands used for this pass: + +```bash +grep -RInE '\b(properties|property|Claim|EvidenceRecord|refinement|obligation)\b' \ + GLOSSARY.md VISION.md README.md drafts seps + +grep -RInE '\b(budget|realization shape|branches|nesting|recursion|parallelism|effects)\b' \ + GLOSSARY.md VISION.md README.md drafts seps + +grep -RInE '\b(uses|effect surface|atomic effect|handler|handles|handle|with|use|on)\b' \ + GLOSSARY.md VISION.md README.md drafts seps + +grep -RInE '\b(HoleReport|hole report|explain|compiler-as-documentation|candidate|scoring|workflow)\b' \ + GLOSSARY.md VISION.md README.md drafts seps + +grep -RInE 'Spec|spec|Cost|cost|Budget|budget|Property|property' \ + ../spore/crates + +grep -RInE 'Handler|handler|handles|handle|with|HoleInfo|HoleReport|candidate|score|confidence' \ + ../spore/crates +``` + +The table below records representative file and line evidence rather than every +match. + +## Vocabulary table + +| Area | PR45 vocabulary and evidence | Sibling implementation evidence | Status | +| --- | --- | --- | --- | +| Signature path | `Signature -> Property -> Hole -> Realization -> Evidence` is stated in `VISION.md:9` and `README.md:25`. | The sibling command surface still exposes `spore test` as spec execution in `spore/src/cli.rs:103-105`. | Partial mismatch: the vision path is property centered, while the implementation command names remain spec centered. | +| Intent clauses | `uses`, `budget`, and `properties` are the fixed intent-signature clause order in `seps/SEP-0001-core-syntax.md:67-83`. | Parser data structures still store `cost_clause`, `spec_clause`, and `uses_clause` in `sporec-parser/src/ast.rs:90-95`. | Mismatch: `budget` and `properties` are PR45 names; `cost` and `spec` are implementation names. | +| Property source surface | `PropertyItem = Ident "(" [ PropertyParamList ] ")" ":" Expr` is the PR45 grammar shape in `seps/SEP-0001-core-syntax.md:286-287`; `Property` is defined in `GLOSSARY.md:158-160`. | The parser owns a `SpecClause` with examples and property-based invariants in `sporec-parser/src/ast.rs:159-166`; CLI tests use `spec { ... property ... }` in `spore/src/main.rs:208-211`. | Mismatch: PR45 has a `properties { ... }` block, while implementation groups examples and properties under `spec`. | +| Property lowering | PR45 defines `Claim` in `GLOSSARY.md:64` and `EvidenceRecord` in `GLOSSARY.md:100`; SEP-0002 says undecidable refinements lower into a Claim in `seps/SEP-0002-type-system.md:209-213`. | Spec evaluation is runtime test execution through `test_specs` in `sporec-driver/src/compiler/source.rs:88-95` and interpreter enumeration in `sporec-codegen/src/interpret/mod.rs:201-205`. | Mismatch: PR45 is claim/evidence oriented; implementation is test-run oriented. | +| Budget surface | `Budget` is a named integer upper bound on realization shape in `GLOSSARY.md:58`; SEP-0004 replaces resource vectors with named shape budgets in `seps/SEP-0004-cost-analysis.md:20`. | Parser comments define `cost [compute, alloc, io, parallel]` in `sporec-parser/src/ast.rs:77-91`; JSON still exposes compute/alloc/io/parallel in `sporec-driver/src/compiler/hole_json.rs:85-90`. | Clear mismatch: PR45 uses named realization-shape budget; implementation still uses the older four-vector cost model. | +| Budget context in holes | PR45 `HoleReport` includes `budget_context` in `seps/SEP-0005-hole-system.md:126-129`, and SEP-0004 says HoleReport embeds budget constraints in `seps/SEP-0004-cost-analysis.md:213-214`. | Sibling JSON has `HoleCostBudgetJson`, `HoleCostVectorJson`, and residual cost fields in `sporec-driver/src/compiler/hole_json.rs:5-8` and `sporec-driver/src/compiler/hole_json.rs:118-129`. | Partial alignment: both expose hole budget context, but the payload vocabulary is cost-vector based in implementation. | +| Effect surface | PR45 defines `Effect surface` in `GLOSSARY.md:94` and states that every `uses` item resolves to an atomic effect or named surface in `seps/SEP-0003-effect-system.md:35-40`. | Parser has `Token::Uses` in `sporec-parser/src/lexer.rs:103-105` and a `uses_clause` field in `sporec-parser/src/ast.rs:94-95`. | Aligned at the keyword level; PR45 adds the surface/atomic taxonomy that the implementation only partly names. | +| Handler declaration | PR45 owns handlers in SEP-0003 and says handlers discharge or reinterpret effects in `seps/SEP-0003-effect-system.md:115-116`; handler method typing appears in `seps/SEP-0003-effect-system.md:143-146`. | Sibling parser has `Token::Handler` in `sporec-parser/src/lexer.rs:121-123`; interpreter stores named handlers in `sporec-codegen/src/interpret/mod.rs:33-34`; handler stack execution appears in `sporec-codegen/src/interpret/eval.rs:431-432`. | Partial alignment: both have handlers, but D3 still needs the PR45 state policy and grammar closure. | +| Handler state examples | PR45 currently shows a stateful-looking handler example around `seps/SEP-0003-effect-system.md:101-115`. | Sibling execution materializes handle bindings and pushes handler frames in `sporec-codegen/src/interpret/eval.rs:431-432`; previous parser evidence includes stateful handler tests around `sporec-parser/tests/parser_tests.rs:486-490`. | Design mismatch to resolve: PR45 D3 now selects immutable handler fields and state via primitive effects. | +| Platform-provided effects | PR45 says Platform packages supply replaceable handlers in `seps/SEP-0003-effect-system.md:43-46`; standard-library prelude text says Platform-specific effects remain in Platform packages in `seps/SEP-0009-standard-library.md:135-137`. | Sibling type checker has built-in platform effect sets including `Console`, `FileRead`, `FileWrite`, `Spawn`, `Clock`, `Random`, and `Exit` in `sporec-typeck/src/platform.rs:46-56` and `sporec-typeck/src/platform.rs:79-84`. | Partial alignment: implementation already recognizes Platform effects; PR45 needs the primitive-effect taxonomy and explicit no-default-platform stance. | +| Primitive effect candidates | PR45 currently has no `State primitive effect` term; standard-library open question asks which Platform packages ship with the standard library in `seps/SEP-0009-standard-library.md:240-241`. | Sibling already treats `Clock` and `Random` as built-in platform effects in `sporec-typeck/src/platform.rs:53-55`; no matching `Cell`, `Output`, or `Map` primitive effect was found in the collected implementation evidence. | Gap: PR45 must introduce the five-effect semantic set; implementation only partially overlaps through `Clock` and `Random`. | +| HoleReport schema | PR45 names `HoleReport` as collaboration boundary in `seps/SEP-0005-hole-system.md:53-54` and lists fields in `seps/SEP-0005-hole-system.md:112-134`. | Sibling exports `HoleInfoJson`, `HoleReportJson`, and related JSON structs through `sporec-driver/src/lib.rs:27-31` and maps type-checker data in `sporec-driver/src/compiler/hole_json.rs:94-121`. | Broadly aligned, but field names and budget vocabulary need cross-checking. | +| Candidate ranking | PR45 says Agents can rank candidates using typed context in `seps/SEP-0005-hole-system.md:179-180`; workflow text starts at `seps/SEP-0005-hole-system.md:90-99`. | Sibling maps `CandidateRanking` to JSON in `sporec-driver/src/compiler/hole_json.rs:41-46` and exposes candidate fit fields in `sporec-driver/src/compiler/hole_json.rs:138-146`. | Partial alignment: implementation has ranking data, while D5 decides that workflow and scoring are informational. | +| Explain protocol | PR45 SEP-0010 defines the explain protocol and compiler-as-documentation in `seps/SEP-0010-compiler-as-documentation.md:18-34`; `ConceptDoc` is defined in `GLOSSARY.md:68-70`. | Sibling has CLI `holes` and LSP hole hovers, for example `spore/src/cli.rs:133-138` and `spore-lsp/tests/lsp_tests.rs:287-303`; no `spore explain` implementation evidence was found in this scan. | Gap: SEP-0010 is design-forward; implementation has adjacent diagnostics and hover surfaces, not the explain protocol. | +| SEP-0005 versus SEP-0010 boundary | PR45 already states that educational renderings of HoleReport records are owned by SEP-0010 in `seps/SEP-0005-hole-system.md:205-207`; SEP-0010 says HoleReports remain SEP-0005 records in `seps/SEP-0010-compiler-as-documentation.md:114-115` and `seps/SEP-0010-compiler-as-documentation.md:189-191`. | Sibling hole JSON and LSP hovers both project from hole data, for example `sporec-driver/src/compiler/hole_json.rs:94-121` and `spore-lsp/tests/lsp_tests.rs:287-303`. | Mostly aligned conceptually; @d10-explain-boundary must make this cross-reference precise. | +| Signature hash / content identity | PR45 defines `Intent hash` in `GLOSSARY.md:130` and mentions package identity over signatures, intents, properties, realizations, evidence, and dependencies in `GLOSSARY.md:66`. | Sibling has `SigHashMap` and changed-item diffing in `sporec-typeck/src/sig_hash.rs:146-177`. | Partial alignment: both have hash identity, but handler runtime fields and primitive effects need PR45 policy before implementation follow-up. | +| Concurrency effects | PR45 SEP-0007 expresses concurrency through `uses [Spawn]` and budget `parallelism` in `seps/SEP-0007-concurrency-model.md:135-136` and `seps/SEP-0007-concurrency-model.md:195-196`. | Sibling platform includes `Spawn` in built-in effects in `sporec-typeck/src/platform.rs:53-56`; runtime uses `TaskHandle` and `ChannelEndpoint` with `RefCell` in `sporec-codegen/src/interpret/eval.rs:247-282`. | Aligned at the named-effect level; implementation has runtime mutable state below the user language surface. | +| Script/platform default | PR45 script-mode draft says standalone execution does not inherit project Platform contracts in `drafts/script-mode.md:273-279` and `drafts/script-mode.md:297-301`. | Sibling CLI resolves standalone targets in `spore/src/target.rs:170-210` and project roots in `spore/src/target.rs:42-73`. | Partial alignment: no default Platform is the selected PR45 stance, while implementation has compatibility paths to audit later. | + +## Main mismatches for downstream tasks + +1. **`properties` versus `spec`**: PR45 has intent-signature properties; + sibling implementation still groups examples and properties in `spec`. +2. **`budget` versus `cost`**: PR45 has named realization-shape budgets; + sibling implementation still serializes the older compute/alloc/io/parallel + cost-vector payload. +3. **Hole protocol scope**: PR45 will make HoleReport the normative protocol; + workflow and scoring become informational. Sibling implementation already has + candidate ranking data, so later review must separate data shape from ranker + policy. +4. **Handler state policy**: PR45 D3 selects immutable handler fields and state + through primitive effects; sibling implementation has named handlers and + handle frames but still needs a spec-level state policy audit. +5. **Primitive effect taxonomy**: PR45 will introduce a five-member primitive + set (`Cell`, `Output`, `Map`, `Clock`, `Random`) with membership rules. + Sibling implementation only shows direct overlap for `Clock` and `Random` in + the collected evidence. +6. **SEP-0010 forward design**: PR45 now includes explain protocol design; + sibling implementation has hole JSON and LSP hover surfaces but no observed + `spore explain` command in this scan. + +## Follow-on task routing + +- `@d1-property` should use the `properties` versus `spec` rows to draft the + PR45 property rule without inheriting implementation `spec` syntax. +- `@primitive-effects` should use the platform rows to keep primitive-effect + membership rules in SEP-0003, concrete interfaces in SEP-0009, and Platform + handler obligations in SEP-0008. +- `@d3-handler` should use the handler rows to distinguish accepted handler + syntax from the newly selected immutable-state policy. +- `@d5-agent` should use the HoleReport rows to keep schema normative while + moving workflow and scoring to informational text. +- `@d10-explain-boundary` should use the SEP-0005 versus SEP-0010 rows to keep + HoleReport data and explain projections separate. +- `@cross-check-impl` should later expand each partial-alignment row into + implementation follow-up categories. diff --git a/.spark/artifacts/primitive-effects-spec.md b/.spark/artifacts/primitive-effects-spec.md new file mode 100644 index 0000000..2b4477a --- /dev/null +++ b/.spark/artifacts/primitive-effects-spec.md @@ -0,0 +1,235 @@ +# State primitive effects specification draft + +This artifact turns the primitive-effect decision into patch-ready text for +SEP-0003, SEP-0008, and SEP-0009. It does not edit the SEP files directly. + +## Locked decision + +The state primitive effect set is fixed to five semantic members: + +- `Cell[T]` +- `Output[T]` +- `Map[K, V]` +- `Clock` +- `Random` + +The draft freezes semantics, not final method spelling. Names such as `get`, +`set`, `emit`, `now`, `next_u64`, and `remove` are placeholders for the later +patch step. + +The default Platform decision is P1: there is no default Platform. Primitive +interfaces live in the standard library surface, but handlers are supplied by an +explicitly selected Platform package or an explicit local handler. + +## Why the split belongs in three SEPs + +The split follows dependency direction: + +```text +SEP-0003 effect taxonomy + -> SEP-0008 Platform obligations + -> SEP-0009 standard-library surface +``` + +- SEP-0003 owns the membership rule because it defines atomic effects, + surfaces, handlers, and effect checking. +- SEP-0008 owns the Platform obligation because Platform packages provide + runtime handlers and startup contracts. +- SEP-0009 owns the concrete standard-library surface because it lists standard + modules, types, and effects available as stable APIs. + +Putting all text in SEP-0009 would leave SEP-0003 without the taxonomy rule. +Putting all text in SEP-0003 would make the effect-system SEP own concrete +standard-library signatures. The three-way split keeps each SEP at its natural +contract boundary. + +## Evidence from current text + +| Document | Current evidence | Primitive-effect impact | +| --- | --- | --- | +| SEP-0003 | Every `uses` item resolves to an atomic effect or named surface at `seps/SEP-0003-effect-system.md:35-40`. | The primitive membership rule belongs to the atomic-effect taxonomy. | +| SEP-0003 | Handler checking requires implementing every operation of the discharged atomic effects at `seps/SEP-0003-effect-system.md:143-146`. | Primitive effects are just atomic effects with extra membership rules. | +| SEP-0003 | HoleReport exposes effect context at `seps/SEP-0003-effect-system.md:161-177`. | Primitive effects will appear in the same effect context projection. | +| SEP-0008 | Platform packages provide effect handlers and validate startup contracts at `seps/SEP-0008-module-package-system.md:52-60`. | Host and mock handlers belong to Platform package conformance. | +| SEP-0008 | A selected Platform declares runtime handlers and host adapter at `seps/SEP-0008-module-package-system.md:111-115`. | Add the primitive handler obligation near this contract. | +| SEP-0009 | Standard-library prelude excludes Platform-specific effects at `seps/SEP-0009-standard-library.md:133-137`. | Primitive effect interfaces can be standard while handlers remain Platform-provided. | +| SEP-0009 | Effects are shown through `@foreign` functions with `uses` at `seps/SEP-0009-standard-library.md:154-161`. | Add the primitive effect surface near this section. | +| SEP-0009 | Open question asks which Platform packages ship with stdlib at `seps/SEP-0009-standard-library.md:240-241`. | Primitive effects do not create a default Platform. | +| Sibling implementation | `Clock` and `Random` already appear in CLI and web Platform handled effects at `../spore/crates/sporec-typeck/src/platform.rs:46-56` and `../spore/crates/sporec-typeck/src/platform.rs:79-84`. | Implementation partially overlaps the chosen primitive set. | + +## SEP-0003 patch draft + +### Target + +Add a subsection after `### Declaring effects` and before `### Using effects`. + +### New subsection + +```markdown +### State primitive effects + +A state primitive effect is a standard atomic effect whose purpose is to carry a +minimal state or event boundary without exposing user-level mutation. State +primitive effects are still ordinary atomic effects for `uses` resolution, +handler checking, and HoleReport effect context. + +An effect belongs to the state primitive set only when it satisfies all of these +membership rules: + +1. It cannot be defined as a composition of other effects. +2. It carries one minimal state or event responsibility. +3. It is broadly useful across application, test, and tooling contexts. +4. A Platform package can provide a host handler and an in-memory mock handler. +5. Its operation set is small and stable enough to be part of the standard + effect surface. + +The initial state primitive set is defined by SEP-0009. Adding a new state +primitive effect requires a separate SEP because it changes the standard effect +taxonomy and the Platform conformance surface. +``` + +### Reference-level addition + +Add this sentence to `### Surface resolution` near +`seps/SEP-0003-effect-system.md:132-139`: + +```markdown +State primitive effects resolve as atomic effects. Their primitive status affects +standard-library and Platform obligations, not the surface-expansion algorithm. +``` + +## SEP-0008 patch draft + +### Target + +Add text to `### Platform startup contract` around +`seps/SEP-0008-module-package-system.md:111-115`. + +### New paragraph + +```markdown +A conforming Platform package that claims support for the standard state +primitive effect set provides two handler families for each supported primitive: +a host handler backed by the target environment and an in-memory mock handler +usable by tests and local handler scopes. This requirement does not create a +default Platform. Programs still select a Platform explicitly. +``` + +### Diagnostics addition + +Add a diagnostic note near the `M050x` Platform diagnostics table: + +```markdown +Missing required state primitive handlers are reported as Platform binding +violations, not as missing standard-library functions. +``` + +## SEP-0009 patch draft + +### Target + +Add a new section after `### Effects` around +`seps/SEP-0009-standard-library.md:154-161`. + +### New section + +````markdown +### State primitive effects + +SEP-0003 defines membership rules for state primitive effects. The standard +library defines the initial primitive surface by semantic contract. Method names +below are placeholders until the final surface patch chooses exact spelling. + +```spore +effect Cell[T] { + fn get() -> T; + fn set(value: T) -> (); +} + +effect Output[T] { + fn emit(value: T) -> (); +} + +effect Map[K, V] { + fn get(key: K) -> Option[V]; + fn put(key: K, value: V) -> (); + fn remove(key: K) -> (); +} + +effect Clock { + fn now() -> Instant; +} + +effect Random { + fn next_u64() -> U64; + fn next_f64() -> F64; +} +``` + +These effects are the standard state and event endpoints used by handler-local +mocking and test instrumentation. Their interfaces are standard-library surface; +their handlers are supplied by the selected Platform package or by explicit +local handlers. +```` + +### Prelude note + +Add to `### Prelude` near `seps/SEP-0009-standard-library.md:133-137`: + +```markdown +State primitive effect names are standard names, but their host handlers remain +Platform-provided. Importing the prelude does not implicitly select a Platform. +``` + +## Excluded candidates + +The membership rules intentionally exclude common derived effects: + +| Candidate | Exclusion reason | +| --- | --- | +| `Counter` | It is derivable from `Cell[I64]` plus helper functions, so it fails the non-composition rule. | +| `Cache[K, V]` | It is a policy-shaped key/value service derivable from map-like state plus eviction policy, so it is not a minimal primitive. | +| `Logger` | It is a behavior contract over `Output[LogEvent]` or `Output[Str]`, so it is not a minimal event endpoint. | +| `Tracer` | It is a domain-specific event stream over `Output[TraceEvent]`, so it is not primitive. | +| `Source[T]` | It is useful, but not needed for the selected D3 mock use cases. It can remain a normal standard-library or Platform effect until a later SEP proves primitive status. | +| `FileRead` / `FileWrite` | These are Platform I/O effects, not minimal state primitives. | +| `Spawn` / `Channel` | These are concurrency effects owned by SEP-0007. | + +## Relationship to D3 handler state + +D3 selects immutable handler fields and no user-level mutation. Stateful handler +use cases route through these primitives: + +| Use case | Primitive effect route | +| --- | --- | +| Console capture | `Output[Str]` | +| Counter | `Cell[I64]` | +| Memo table | `Map[K, V]` | +| Time-dependent checks | `Clock` | +| Fuzzing or randomized tests | `Random` | + +Handler fields remain runtime configuration and do not become mutable state. + +## Implementation comparison notes + +The sibling implementation already includes `Clock` and `Random` in built-in +Platform effect sets for CLI and web Platforms at +`../spore/crates/sporec-typeck/src/platform.rs:46-56` and +`../spore/crates/sporec-typeck/src/platform.rs:79-84`. It does not expose the +chosen `Cell`, `Output`, and `Map` primitive effects in the collected evidence. +That gap should become a follow-up item in `@cross-check-impl`, not a source +change in this design-only pass. + +## Routing to the patch plan + +`@sep-patch-plan` should include these concrete edits: + +1. SEP-0003 adds the membership-rule subsection. +2. SEP-0003 states that primitive effects resolve as atomic effects. +3. SEP-0008 adds Platform host and mock handler obligations. +4. SEP-0008 adds Platform diagnostic wording for missing primitive handlers. +5. SEP-0009 adds the five-effect semantic interface table. +6. SEP-0009 adds a prelude note that primitive names do not select a Platform. +7. GLOSSARY adds `State primitive effect` and references SEP-0003/0009. +8. VISION adds a short sentence that state is explicit through effects rather + than user-level mutation. diff --git a/.spark/artifacts/sep-patch-plan.md b/.spark/artifacts/sep-patch-plan.md new file mode 100644 index 0000000..b0ef748 --- /dev/null +++ b/.spark/artifacts/sep-patch-plan.md @@ -0,0 +1,346 @@ +# PR45 SEP patch plan + +This artifact merges the five design drafts into a file-level patch plan. It is +a checklist for the later document-editing pass and does not edit SEP files. + +## Inputs + +- `.spark/artifacts/pr45-vocab-map.md` +- `.spark/artifacts/d1-property-spec.md` +- `.spark/artifacts/d3-handler-spec.md` +- `.spark/artifacts/d5-agent-spec.md` +- `.spark/artifacts/primitive-effects-spec.md` +- `.spark/artifacts/d10-explain-boundary-spec.md` + +## Global constraints + +- Scope is design-only in `spore-evolution`; do not modify the sibling `spore` + implementation repository. +- Do not introduce a new SEP number. +- Keep the no-default-Platform decision. +- Keep primitive-effect method spellings semantic until the final text pass. +- Keep workflow and candidate ranker text informational. +- Preserve SEP-0010 as the compiler-as-documentation layer over SEP-0005 and + SEP-0006 records. +- Run the full validation suite after the actual edits: + `uvx --from git+https://github.com/j178/prek prek run -a`. + +## Validation gates + +The final edit pass must keep these hooks green: + +| Hook | Expected impact | +| --- | --- | +| `terminology-consistency` | New terms must be added to `GLOSSARY.md` when they enter SEP text. | +| `contract-schemas` | No schema changes are planned; this hook should remain unchanged. | +| `surface-consistency` | `README.md`, `VISION.md`, `GLOSSARY.md`, and SEP links must remain aligned. | +| `sep-index` | Required because SEP-0005 is renamed and `seps-index.json` path/title change. | +| `sep-documents` | Required for front matter, title, requires, and template shape after renaming. | + +## File plan summary + +| File | Action | Source artifacts | +| --- | --- | --- | +| `seps/SEP-0001-core-syntax.md` | Patch property delegation and handler grammar. | D1, D3 | +| `seps/SEP-0002-type-system.md` | Add property body typing and connect refinements to claims. | D1 | +| `seps/SEP-0003-effect-system.md` | Add primitive membership rules, replace handler example, add handler state policy. | Primitive, D3 | +| `seps/SEP-0005-hole-system.md` | Rename file, title, and scope; strengthen HoleReport schema; demote workflow/ranker. | D5, D10 | +| `seps/SEP-0006-compiler-architecture.md` | Clarify property claims, evidence result semantics, and SEP-0010 metadata layering. | D1, D10 | +| `seps/SEP-0008-module-package-system.md` | Add Platform obligation for primitive host and mock handlers. | Primitive | +| `seps/SEP-0009-standard-library.md` | Add state primitive effect semantic surface. | Primitive | +| `seps/SEP-0010-compiler-as-documentation.md` | Sharpen HoleReport projection boundary and ConceptDoc related-SEP guidance. | D10 | +| `GLOSSARY.md` | Add/update terms. | All | +| `VISION.md` | Add small state/effect and explain-boundary clarifications. | Primitive, D10 | +| `README.md` | Update SEP-0005 link after rename. | D5 | +| `seps-index.json` | Update SEP-0005 path/title after rename. | D5 | +| `schemas/` | No planned changes. | None | + +## SEP-0001 patch plan + +Affected areas: + +- Intent Signature delegation at `seps/SEP-0001-core-syntax.md:67-83`. +- Reference signature layout at `seps/SEP-0001-core-syntax.md:220-230`. +- Property grammar at `seps/SEP-0001-core-syntax.md:286-290`. +- Handler grammar at `seps/SEP-0001-core-syntax.md:312-316`. +- Handler grammar notes at `seps/SEP-0001-core-syntax.md:333-338`. +- Unresolved property subset question at `seps/SEP-0001-core-syntax.md:435`. + +Edits: + +1. Replace the clause delegation sentence so `properties` delegates expression + typing to SEP-0002 and claim/evidence lowering to SEP-0006. +2. Keep `PropertyItem = ... ":" Expr`, then add a note that this is ordinary + Spore expression syntax and not a separate proof sublanguage. +3. Replace the handler grammar with fields, `handles`, optional `uses`, and + `impl Effect { fn operation(self, ...) ... }` blocks. +4. Add a grammar note that handler fields are immutable instance payload and the + handler receiver is explicit read-only `self`. +5. Add an expression-shape note for `handle ... with { use ..., on ... }`. +6. Replace the unresolved property-expression-subset question with an unknown + evidence presentation question. + +## SEP-0002 patch plan + +Affected areas: + +- Reference-level typing section after outcome typing and before refinement + obligations, near `seps/SEP-0002-type-system.md:191-213`. + +Edits: + +1. Add `### Property body typing`. +2. State that a property body is an ordinary Spore expression checked against + `Bool`. +3. State that property parameters are local to the property body. +4. State that property bodies inherit the enclosing effect context and may use + only available or locally discharged effects. +5. State that non-`Bool` property bodies are type errors. +6. Extend refinement obligations to say they share the same Claim and + EvidenceRecord channel as source properties. + +## SEP-0003 patch plan + +Affected areas: + +- `### Declaring effects` / `### Using effects`, around + `seps/SEP-0003-effect-system.md:54-76`. +- `### Handlers`, around `seps/SEP-0003-effect-system.md:101-116`. +- `### Surface resolution`, around `seps/SEP-0003-effect-system.md:132-139`. +- `### Handler checking`, around `seps/SEP-0003-effect-system.md:143-146`. +- Structured effect context section around `seps/SEP-0003-effect-system.md:161-177`. + +Edits: + +1. Add `### State primitive effects` with the five membership rules. +2. State that state primitive effects resolve as ordinary atomic effects. +3. Replace the stateful `self.output.push` handler example with an `Output[Str]` + primitive-effect example. +4. Add `### Handler state policy` describing immutable fields and no `mut`. +5. Add handler method receiver rules: first parameter is explicit read-only + `self`. +6. Add task-local handler instance visibility. +7. Add handler payload hash boundary: fields do not enter signature, intent, or + property hash. +8. Cross-reference SEP-0009 for the concrete primitive set and SEP-0008 for + Platform handlers. + +## SEP-0005 patch plan + +Rename: + +- Move `seps/SEP-0005-hole-system.md` to + `seps/SEP-0005-hole-report-protocol.md`. + +Affected areas after rename: + +- Front matter title and H1. +- Executive summary. +- Motivation and normative scope near `HoleReport is the collaboration boundary`. +- `### Realization workflow` at old lines `90-99`. +- `### HoleReport fields` at old lines `112-134`. +- Structured representation section around old lines `190-207`. + +Edits: + +1. Change title and H1 to `SEP-0005: Hole System & HoleReport Protocol`. +2. Replace the executive summary to emphasize normative HoleReport schema. +3. Add a normative scope banner: hole syntax, HoleReport records, and dependency + graph are normative; Agent workflow, ranker, and teaching projections are not. +4. Retitle `### HoleReport fields` to `### Normative HoleReport fields`. +5. Add JSON-shape language after the field table. +6. Add a note that SEP-0010 may attach concept refs and render fields but does + not add required HoleReport fields. +7. Move `### Realization workflow` to an informational appendix. +8. Add `## Informational appendix: Reference candidate ranker` with the formula + as an example only. +9. Strengthen the SEP-0010 rendering boundary sentence. + +## SEP-0006 patch plan + +Affected areas: + +- `### Properties become claims`, around + `seps/SEP-0006-compiler-architecture.md:64-77`. +- `### EvidenceRecord structure`, around + `seps/SEP-0006-compiler-architecture.md:128-160`. +- Shared records and SEP-0010 handoff around + `seps/SEP-0006-compiler-architecture.md:201-210`. +- Diagnostics around `seps/SEP-0006-compiler-architecture.md:219-230`. + +Edits: + +1. Update property-to-claim lowering to mention SEP-0002 Bool typing and effect + context checks. +2. Add shared-origin text: source properties, refinements, budget checks, effect + checks, and validators lower into compatible Claim/EvidenceRecord structures. +3. Add property evidence result semantics for `passed`, `failed`, `unknown`, and + `skipped`. +4. Split diagnostics: non-`Bool` property bodies are type diagnostics; failed or + unknown checks are property/claim diagnostics. +5. Add that SEP-0010 teaching metadata is optional metadata over shared records + and does not change identity, hash, or required fields. + +## SEP-0008 patch plan + +Affected areas: + +- `### Platform startup contract`, around + `seps/SEP-0008-module-package-system.md:111-115`. +- Platform diagnostics table around `M050x` entries. + +Edits: + +1. Add the primitive handler obligation: a conforming Platform package claiming + primitive support provides host and in-memory mock handler families for each + supported primitive. +2. State that this does not create a default Platform. +3. Add a diagnostic note that missing primitive handlers are Platform binding + violations, not missing stdlib functions. + +## SEP-0009 patch plan + +Affected areas: + +- `### Prelude`, around `seps/SEP-0009-standard-library.md:133-137`. +- `### Effects`, around `seps/SEP-0009-standard-library.md:154-161`. +- Unresolved Platform package question around + `seps/SEP-0009-standard-library.md:240-241`. + +Edits: + +1. Add a prelude note: state primitive names are standard names, but host + handlers remain Platform-provided. +2. Add `### State primitive effects` with semantic interfaces for `Cell`, + `Output`, `Map`, `Clock`, and `Random`. +3. Mark method names as final-patch spelling choices if the final edit keeps the + placeholder stance. +4. Add a short exclusion note for `Counter`, `Cache`, `Logger`, `FileRead`, + `FileWrite`, `Spawn`, and `Channel`. +5. Update the open Platform package question so it does not imply a default + Platform. + +## SEP-0010 patch plan + +Affected areas: + +- `### Hole teaching projection`, around + `seps/SEP-0010-compiler-as-documentation.md:188-194`. +- `### ConceptDoc fields`, around + `seps/SEP-0010-compiler-as-documentation.md:160-170`. +- Diagnostics impact around `seps/SEP-0010-compiler-as-documentation.md:291-296`. + +Edits: + +1. Strengthen the hole teaching projection paragraph to say it renders SEP-0005 + fields and does not define alternate field names. +2. Add the owner rule: SEP-0005 owns data contracts; SEP-0010 owns teaching + projection. +3. Add `related_seps` guidance for hole-related ConceptDocs. +4. Soften `Every user-facing diagnostic should map to at least one concept id` + to allow direct or diagnostic-family concept paths. + +## GLOSSARY.md patch plan + +Add or update terms: + +| Term | Action | +| --- | --- | +| `State primitive effect` | Add under S, reference SEP-0003 and SEP-0009. | +| `Handler instance` | Add or expand under H, mention lexical and task-local visibility. | +| `Handler field` | Add or expand under H, mention immutable instance payload. | +| `HoleReport Protocol` | Add or update near HoleReport after SEP-0005 rename. | +| `Concept projection` | Add under C if SEP-0010 text uses the term prominently. | +| `Property body` | Add if SEP-0002 uses the term as a heading. | + +Update the multilingual table for any new user-facing term added to the body. + +## VISION.md patch plan + +Affected areas: + +- Intent Signature explanation around `VISION.md:27-30`. +- Holes and reports around `VISION.md:47-53`. +- Evidence and structured data around `VISION.md:70-76`. +- Prior-art table if needed. + +Edits: + +1. Add one sentence that stateful testing and instrumentation use explicit + effects rather than user-level mutation. +2. Add one sentence that HoleReport data and compiler explain projections share + the same compiler facts but are different contracts. +3. Do not add implementation or migration detail to VISION. + +## README.md patch plan + +Affected area: + +- SEP dependency list around `README.md:49-53`. + +Edits: + +1. Update the SEP-0005 link target from `seps/SEP-0005-hole-system.md` to + `seps/SEP-0005-hole-report-protocol.md`. +2. Keep the current grouping with SEP-0006 and SEP-0010. + +## SEP index JSON patch plan + +Affected area: + +- SEP 5 entry currently points to `seps/SEP-0005-hole-system.md` and title + `SEP-0005: Hole System & Agent Protocol`. + +Edits: + +1. Update `path` to `seps/SEP-0005-hole-report-protocol.md`. +2. Update `title` to `SEP-0005: Hole System & HoleReport Protocol`. +3. Leave `sep`, `status`, `type`, `authors`, `created`, `requires`, + `discussion`, `pr`, and `superseded_by` unchanged. + +## schemas plan + +No schema changes are planned in this patch. SEP-0010 may later publish +ConceptRegistry schemas under `schemas/contracts`, but this PR should not add +that surface. + +## Cross-reference checklist + +After the actual file edit pass, run these checks manually before `prek`: + +```bash +grep -RIn "SEP-0005-hole-system" README.md GLOSSARY.md VISION.md seps seps-index.json +grep -RIn "Hole System & Agent Protocol" README.md GLOSSARY.md VISION.md seps seps-index.json +grep -RIn "self.output.push" seps +``` + +Expected result: no matches after the patch except historical discussion text if +intentionally retained. + +## Implementation follow-up seeds + +These are not PR45 edits, but `@cross-check-impl` should classify them: + +1. Sibling implementation still uses `spec` where PR45 uses `properties`. +2. Sibling implementation still uses `cost [compute, alloc, io, parallel]` where + PR45 uses named realization-shape budgets. +3. Sibling implementation lacks direct `property_context` evidence in scanned + `HoleInfo`. +4. Sibling implementation has `Clock` and `Random`, but not `Cell`, `Output`, or + primitive-effect `Map` in the collected Platform evidence. +5. Sibling implementation has hole JSON and LSP hovers, but no observed + `spore explain` command in the collected evidence. + +## Final edit order + +Use this order for the actual SEP editing pass: + +1. Rename SEP-0005 file and update `seps-index.json` plus `README.md` link. +2. Patch SEP-0005 and SEP-0010 boundary text together. +3. Patch SEP-0001, SEP-0002, and SEP-0006 for D1 properties. +4. Patch SEP-0003, SEP-0008, and SEP-0009 for primitive effects and handler + state. +5. Patch `GLOSSARY.md`. +6. Patch `VISION.md`. +7. Run the grep checklist. +8. Run `uvx --from git+https://github.com/j178/prek prek run -a`. diff --git a/GLOSSARY.md b/GLOSSARY.md index bdb8b1a..6436eb3 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -1,201 +1,238 @@ # Spore Glossary -Unified terminology index for Spore. Each term links to the SEP where it is authoritatively defined. +Unified terminology index for Spore. Each term links to the SEP where it is +authoritatively defined. + +This file also owns the multilingual terminology table used by Spore +documentation translations. When a new user-facing term is added to Spore docs, +update both its glossary definition and its multilingual correspondence here. + +## Multilingual terminology + +| Term | zh-CN | +| ----------------- | ------------ | +| Spore | 孢子 | +| Agent | 智能体 | +| Signature | 签名 | +| Base Signature | 基础签名 | +| Intent Signature | 意图签名 | +| Outcome | 结果 | +| Property | 性质 | +| Hole | 洞 | +| Typed absence | 有类型的缺失 | +| Realization | 实现 | +| Evidence | 证据 | +| Claim | 检查主张 | +| Budget | 预算 | +| ConceptDoc | 概念文档 | +| ConceptRegistry | 概念注册表 | +| Concept ref | 概念引用 | +| Realization shape | 实现形态 | +| Effect surface | 效应表面 | +| Effect | 效应 | +| Effect context | 效应上下文 | +| Effect handler | 效应处理器 | +| Handler field | 处理器字段 | +| Handler instance | 处理器实例 | +| Platform | 平台 | +| Content-addressed | 内容寻址 | +| Content identity | 内容身份 | +| Provenance hash | 来源哈希 | +| Explain protocol | 解释协议 | +| Diagnostic | 诊断 | +| Concept projection | 概念投影 | +| HoleReport | 洞报告 | +| HoleReport Protocol | 洞报告协议 | +| Repair hint | 修复提示 | +| Dependency | 依赖 | +| Checker | 检查器 | +| Property body | 性质主体 | +| State primitive effect | 状态原语效应 | ## A -**Atomic effect** (SEP-0003): One of the 10 built-in intent-oriented atomic effects in Spore's effect system: Console, FileRead, FileWrite, NetConnect, NetListen, Env, Spawn, Clock, Random, Exit. In compiler/tooling contexts, these effect names form the checked effect set. +**Atomic effect** (SEP-0003): A single effect protocol declared with `effect Name { ... }`, as opposed to a reusable surface declaration. -**`Add`** (SEP-0002): Compiler-known trait for the `+` operator on types that explicitly implement addition. - -**`@allows`** (SEP-0002, SEP-0003, SEP-0005): Hole-level annotation that restricts which candidate functions may be used to fill a specific Hole. It cannot suppress resource checks. - -**`@unbounded`** (SEP-0004): Annotation declaring that a function's cost is intentionally unanalyzed, opting out of cost verification entirely. - -**`await`** (SEP-0007): Expression that blocks until a `Task[T]` completes and extracts its result value of type `T`. +**`await`** (SEP-0007): Expression that blocks until a `Task[T, E]` completes, extracts its success value of type `T`, and reintroduces failure type `E` at the await site. ## B -**Bidirectional inference** (SEP-0002): Type inference strategy combining synthesis (bottom-up, infer type from expression) and checking (top-down, verify expression against expected type). - -## C - -**Declared effects** (SEP-0003): The effect names explicitly written on a function or platform/manifest surface. +**Base Signature** (SEP-0001): The callable boundary made of function name, type parameters, parameter types, and result type (including any outcome boundary). It defines possible implementation space. -**Effect ceiling** (SEP-0003, SEP-0008): The maximum set of effects available to a scope. Function-level `uses [...]` clauses are standardized today; broader module/project ceilings remain reserved follow-up design space. +**Bidirectional inference** (SEP-0002): Type inference strategy combining synthesis from expressions and checking against expected types. -**Effect narrowing** (SEP-0003): Restricting the available effect set when entering a nested scope, ensuring inner code cannot exceed outer permissions. +**Budget** (SEP-0004): A named integer upper bound on realization shape, written in a `budget { ... }` block. -**Effect set (EffectSet)** (SEP-0003): An unordered collection of effects associated with a function or scope, written as `uses [Effect1, Effect2]`. +**Budget context** (SEP-0005): The subset of enclosing budget constraints relevant to a hole or realization candidate. -**`Clone`** (SEP-0002): Compiler-known trait for explicitly duplicating a value. - -**`Channel[T]`** (SEP-0007): Bounded channel type for inter-task message passing, parameterized by the message type. +## C -**Content-addressed package** (SEP-0008): Package identified by BLAKE3 hashes of its normalized signatures and implementations, enabling reproducible builds and cache deduplication. +**Claim** (SEP-0006): Internal compiler representation derived from a source `properties` item. -**Cost budget** (SEP-0004): The declared cost bound on a function, verified at compile time against inferred cost of the function body. +**Content-addressed package** (SEP-0008): Package identified by hashes of normalized signatures, intents, properties, realizations, evidence, and dependency inputs. -**Cost dimension** (SEP-0004): One of four orthogonal resource axes in a CostVector: compute, alloc, io, parallel. +**ConceptDoc** (SEP-0010): Machine-readable record that explains one Spore concept for `spore explain`, diagnostics, LSP, and Agent tooling. -**Cost expression (CostExpr)** (SEP-0004): Arithmetic expression over compile-time Index symbols and `cost(f)` summaries. CostExpr does not reference ordinary runtime values. +**Concept ref** (SEP-0010): Stable concept id attached to a diagnostic, HoleReport projection, or explain response. -**CostVector** (SEP-0004): A 4-tuple `(compute(op), alloc(cell), io(call), parallel(lane))` representing multidimensional resource usage. +**ConceptRegistry** (SEP-0010): Ordered collection of `ConceptDoc` records plus schema identity. -**`Count[N]`** (SEP-0002, SEP-0009): Runtime non-negative count value that carries a compile-time Index parameter `N`. Cost analysis sees `N`, not the runtime integer. +**Concept projection** (SEP-0010): Human-facing rendering over compiler-owned structured records, such as a HoleReport teaching view backed by SEP-0005 data. ## D -**`Debug`** (SEP-0002): Compiler-known trait for programmer-facing string representation, used in diagnostics and logging. +**Declared effects** (SEP-0003): Effect names explicitly written on a function or platform surface. -**`Default`** (SEP-0002): Compiler-known trait providing a default value for a type. +**Default literals** (SEP-0002): Unsuffixed integer literals synthesize as `I64` and float literals as `F64` unless a signature or context fixes another width. -**`Deserialize`** (SEP-0002): Compiler-known trait for converting serialized data back into a typed value. +**Diagnostic code** (SEP-0006): Structured error or warning identifier in the format `X0NNN`, where X is a category letter. -**Derivable trait** (SEP-0002): A compiler-known trait whose implementation can be auto-generated from the type's structure, using the `deriving [...]` syntax. +## E -**`Display`** (SEP-0002): Compiler-known trait for user-facing string representation. +**Effect** (SEP-0003): Observable interaction with the outside world that must be declared in a `uses` effect surface. -**`Div`** (SEP-0002): Compiler-known trait for the `/` operator on types that explicitly implement division. +**Effect surface declaration** (SEP-0003): A named reusable surface written as `surface Name = [EffectA, EffectB]`. -**Diagnostic code** (SEP-0006): Structured error/warning identifier in the format `X0NNN`, where X is a category letter: E (type error), C (effect), K (cost), M (module), W (warning). +**Effect context** (SEP-0005): The declared effects, expanded effect set, active handlers, and discharged effects visible at a hole or realization candidate. -**Default entry** (SEP-0008): The manifest-selected entry used when project commands omit an explicit entry name. +**Effect handler** (SEP-0003, SEP-0008): Implementation of effect operations, often provided by a selected Platform package. -## E +**Effect set** (SEP-0003): Unordered collection of effects associated with a function or scope. -**Entry** (SEP-0008): A manifest-selected executable target within a project. An entry resolves to an entry module. +**Effect surface** (SEP-0001, SEP-0003): The `uses` surface expression attached to an intent signature. SEP-0003 defines the accepted effect names, surface declarations, handlers, and checking rules. -**Entry module** (SEP-0008): The source file and derived module selected by an entry, such as `src/main.sp` → `main`. +**Enum** (SEP-0002): Algebraic data type with named variants, each optionally carrying data. Defined with `enum Name { Variant(T) }`. -**`Eq`** (SEP-0002): Compiler-known trait for structural equality comparison. +**Evidence** (SEP-0006): Generated checked record of what held for a concrete realization. -**Effect** (SEP-0003): An observable interaction with the outside world (I/O, mutation, randomness), tracked via the effect system. +**EvidenceRecord** (SEP-0006): Machine payload containing subject, claim, checker, result, and provenance hash data. -**Effect alias** (SEP-0003): A named shorthand for a set of atomic effects, written as `effect FileIO = FileRead | FileWrite;`. +**Evidence hash** (SEP-0008): Hash of the evidence records selected by a package or publication policy. -**Effect handler** (SEP-0003, SEP-0008): Implementation of an effect's operations. SEP-0003 owns handler semantics; SEP-0008 explains Platform-provided handlers and host adapters. +**Explain protocol** (SEP-0010): CLI and JSON contract for resolving diagnostic codes, language concepts, and surface symbols into compiler-owned concept docs. -**Enum** (SEP-0002): Algebraic data type with named variants, each optionally carrying data. Defined with `type Name { Variant1(T), Variant2 }`. +**`@export`** (SEP-0001, SEP-0008): Attribute marking a public Spore function as an outbound ABI surface, for example `@export("C")`. ## F -**`foreign fn`** (SEP-0008): Function declaration whose implementation is provided by the platform rather than written in Spore. Used for I/O bindings. +**`@foreign`** (SEP-0001, SEP-0008): Attribute marking a function or type as externally provided. On functions it may also carry linkage metadata such as `@foreign("ssl", name = "SSL_new")`. ## G -**Generics** (SEP-0002): Parametric polymorphism using type variables, written as `fn f[T](x: T)` with optional `where` clause bounds. +**Generics** (SEP-0002): Parametric polymorphism using type variables in square brackets, with bounds written inline, as in `fn f[T: Eq + Hash](x: T)`. ## H -**`Hash`** (SEP-0002): Compiler-known trait for computing hash values, often required alongside `Eq` for use in hash-based collections. +**Hole** (SEP-0005): Typed absence in source code, written as `?name`, constrained by base signature, effect context, budget context, and property context. -**Hole** (SEP-0005): A typed placeholder in source code written as `?name`, representing incomplete code that carries type, effect, and cost context for agent-assisted completion. +**Hole context** (SEP-0005): The type environment, visible bindings, effect context, budget context, property context, and dependency information associated with a typed hole. -**Hole context** (SEP-0005): The full type environment, effect set, cost budget, and dependency information associated with a typed hole. +**Hole Dependency Graph** (SEP-0005): DAG ordering typed holes by data-flow, type, effect, budget, and property dependencies for fill scheduling. -**Hole Dependency Graph** (SEP-0005): DAG ordering typed holes by data-flow, type, effect, and cost dependencies for parallel fill scheduling. +**HoleReport Protocol** (SEP-0005): Normative per-hole record and dependency graph contract used by humans, Agents, and tools. -**Hole state machine** (SEP-0005): Lifecycle of a hole: Open → Filling → Filled → Accepted. +**Hole realization workflow** (SEP-0005): Informational Agent-facing realization loop: DISCOVER -> ANALYZE -> PROPOSE -> VERIFY -> ACCEPT or REJECT. -## I +**Handler field** (SEP-0003): Immutable runtime configuration stored on a handler instance. Handler fields are not user-level mutable state and do not participate in signature or intent hashes. + +**Handler instance** (SEP-0003): Lexical, task-local installation of a handler inside a `handle ... with` expression. -**Implementation hash (impl hash)** (SEP-0006): Content hash of a function's full body, used for incremental compilation — a change in impl hash triggers recompilation. +## I -**Import resolution** (SEP-0008): The process of mapping `import pkg.module` declarations to concrete module files and verifying symbol visibility. +**Intent Signature** (SEP-0001): Signature metadata after the base signature: `uses`, `budget`, and `properties`. -**Index** (SEP-0002, SEP-0004): Compile-time non-negative size kind used by cost analysis. Index parameters such as `N: Index` are the only variables that may appear directly in CostExpr. +**Intent hash** (SEP-0006, SEP-0008): Hash of canonical `uses`, `budget`, and `properties` metadata attached to a callable. -## L +**Import resolution** (SEP-0008): Mapping import declarations to concrete module files and verifying symbol visibility. -**Lane** (SEP-0007): The parallel cost dimension — each `spawn` creates a new lane, tracked in CostVector's `parallel` field. +**Index** (SEP-0002): Compile-time non-negative size kind used by indexed types such as `Array[T, N]` and `Vec[T, max: N]`. ## M -**`Mul`** (SEP-0002): Compiler-known trait for the `*` operator on types that explicitly implement multiplication. - -**Module** (SEP-0008): A single Spore source file whose module path is derived from its filesystem path. Function-level signatures inside the module declare effects. +**Module** (SEP-0008): A single Spore source file whose module path is derived from its filesystem path. ## N -**NDJSON (Newline-Delimited JSON)** (SEP-0006): Output format for watch mode, where each compiler event is a single JSON object on one line, enabling streaming IDE integration. +**NDJSON** (SEP-0006): Newline-delimited JSON output format for watch mode. -**Nominal typing** (SEP-0002): Types are distinguished by name, not structure. Two structs with identical fields but different names are different types. +**Never** (SEP-0002): The bottom type, used for functions that never return. -**`Never`** (SEP-0002): The bottom type — uninhabited, used as the return type of functions that never return (e.g., `exit()`). +**Nominal typing** (SEP-0002): Types are distinguished by name, not by structure alone. ## O -**`Ord`** (SEP-0002): Compiler-known trait for total ordering comparison, enabling `<`, `>`, `<=`, `>=` operators. - **`Option[T]`** (SEP-0009): Prelude type representing an optional value: `Some(T)` or `None`. ## P -**Platform** (SEP-0008): A package that provides effect handlers for a target environment (e.g., CLI, Web, Embedded). Each manifest-backed project binds exactly one Platform, and each selected entry module must satisfy its startup contract. +**Platform** (SEP-0008): Package that provides effect handlers for a target environment and validates startup contracts. -**Prelude** (SEP-0009): The set of types and functions available in every Spore module without explicit import. +**Prelude** (SEP-0009): Types, traits, and functions available in every Spore module without explicit import. -## R +**Property** (SEP-0001, SEP-0006): Source-level validity rule written in `properties { name(params): expr }`. -**Refinement type** (SEP-0002): Type augmented with a predicate constraint. Three tiers: L0 (decidable, compile-time), L1 (abstract interpretation), L2 (proof obligation). +**Property body** (SEP-0002): Ordinary Spore expression after `:` in a property item. It must check as `Bool` under the enclosing effect context. -**`Result[T, E]`** (SEP-0009): Prelude type representing success (`Ok(T)`) or failure (`Err(E)`). +**Property context** (SEP-0005): Properties from the enclosing intent signature projected into a hole report. -## S +**Property hash** (SEP-0006, SEP-0008): Hash of normalized properties attached to a callable or contract. + +## R + +**Realization** (SEP-0005, SEP-0006): Property-preserving completion of typed absence. It is a process and artifact identity, not a source keyword. + +**Realization hash** (SEP-0006, SEP-0008): Hash of a concrete completed body or generated artifact. -**`Serialize`** (SEP-0002): Compiler-known trait for converting a typed value into a serialized format. +**Refinement type** (SEP-0002): Type augmented with a predicate constraint. -**`select`** (SEP-0007): Expression that waits for the first ready channel arm or timeout arm, enabling concurrent race patterns. +**Outcome** (SEP-0002): First-class result type written as `A ! E`, representing success `A` or failure `E`. -**SEP (Spore Enhancement Proposal)** (SEP-0000): A design document proposing a change or addition to Spore, following a structured review process. +## S -**Signature hash (sig hash)** (SEP-0006): Content hash of a function's public interface (name, params, return type, required effects), used for dependency tracking — a change in sig hash invalidates all callers. +**Signature** (SEP-0001): The combination of Base Signature and optional Intent Signature. -**`spawn`** (SEP-0007): Expression that creates a new `Task[T]` for concurrent execution, requiring the `Spawn` effect. +**Surface** (SEP-0003): Finite, unordered effect requirement set used by `uses` and named by `surface` declarations. -**`Sub`** (SEP-0002): Compiler-known trait for the `-` operator on types that explicitly implement subtraction. +**Signature hash** (SEP-0006, SEP-0008): Hash of the normalized Base Signature that participates in dependency tracking. -**Struct** (SEP-0002): Product type with named fields, defined as `struct Name { field1: T1, field2: T2 }`. +**`spawn`** (SEP-0007): Expression that creates a scoped `Task[T, E]`, requiring the `Spawn` effect. -**spore** (SEP-0006, SEP-0008): The project/package workflow CLI. It owns `build`, `check`, `test`, `run`, `fmt`, `update`, and `upgrade`. +**spore** (SEP-0006, SEP-0008): Project and package workflow CLI. -**sporec** (SEP-0006, SEP-0008): The low-level explicit-input compiler CLI. It owns `compile`, `holes`, `query-hole`, and `explain`. +**sporec** (SEP-0006, SEP-0008): Low-level explicit-input compiler CLI. -**Structured concurrency** (SEP-0007): Concurrency model where all spawned tasks are scoped to their parent block, ensuring no task outlives its creator. +**Startup contract** (SEP-0008): Platform-defined requirement on startup function parameters, result type, and effect surface. -**Startup contract** (SEP-0008): The Platform-defined requirement on the startup function's parameters, return type, and effect boundary. +**Startup function** (SEP-0008): Callable inside the selected entry module that satisfies the Platform startup contract. -**Startup function** (SEP-0008): The callable inside the selected entry module that satisfies the Platform's startup contract. Today this is usually `main`. +**State primitive effect** (SEP-0003, SEP-0009): Standard atomic effect that carries a minimal state or event responsibility without exposing user-level mutation. The initial set is `Cell`, `Output`, `Map`, `Clock`, and `Random`. -**`Str`** (SEP-0002): The UTF-8 text primitive in Spore surface syntax (not `String`). Single Unicode scalars use length-1 `Str` values; there is no separate `Char` type (see implementation PR #113). +**Str** (SEP-0002): UTF-8 text primitive in Spore surface syntax. -**Default literals** (SEP-0002): In the reference compiler (`sporec-typeck`), unsuffixed integer literals synthesize as **`I64`** and float literals as **`F64`**, unless a signature or context fixes another width (see metavariables **ι** / **φ** in SEP-0002 for other fixed sizes). +**Struct** (SEP-0002): Product type with named fields, defined as `struct Name { field: T }`. ## T -**`Task[T]`** (SEP-0007): Typed future representing an asynchronous computation that will produce a value of type `T`. +**Task[T, E]** (SEP-0007): Typed future representing an asynchronous computation with success type `T` and await-time failure type `E`. -**Typed hole** (SEP-0005): See **Hole**. +**Typed hole** (SEP-0005): See Hole. ## U -**`uses` clause** (SEP-0003): Annotation on a function declaring required effects, written as `uses [Effect1, Effect2]`. +**uses clause** (SEP-0001, SEP-0003): Signature clause declaring an effect surface expression, written as `uses [Name1, Name2]` or `uses SurfaceName`. ## V -**Visibility** (SEP-0008): Access control on module exports: `pub` (public to all), `pub(pkg)` (package-internal), or private (default, module-only). +**Visibility** (SEP-0008): Access control on module exports: `pub`, `pub(pkg)`, or private. ## W -**Watch mode** (SEP-0006): Compiler mode that continuously monitors source files and emits NDJSON events on changes, designed for IDE integration. - -**`where` clause** (SEP-0002): Constraint block on generic functions specifying trait bounds, written as `where T: Eq + Hash`. +**Watch mode** (SEP-0006): Compiler mode that monitors source files and emits diagnostics and hole events on change. ## Process terms -**Discussion** (SEP-0000): The canonical public thread where a pitch is debated before a -formal SEP draft is proposed. +**SEP** (SEP-0000): **Spore Evolution Proposal** (Spore 演进提案). A design document that records and reviews language, tooling, ecosystem, and governance changes across Spore's evolution. + +**Discussion** (SEP-0000): Public thread where a pitch is debated before a formal SEP draft is proposed. -**Pitch** (SEP-0000): A public discussion-stage proposal that tests whether an idea is -worth turning into a repository-backed SEP. +**Pitch** (SEP-0000): Public discussion-stage proposal that tests whether an idea is worth turning into a repository-backed SEP. diff --git a/README.md b/README.md index ada7b15..8a5f74f 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,54 @@ Proposal portal for the Spore programming project. -This repository is the long-lived home for Spore Enhancement Proposals (SEPs): +This repository is the long-lived home for Spore Evolution Proposals (SEPs): process decisions, language design records, tooling protocols, package-system -design, and other cross-cutting changes that affect Spore as a whole. +design, and cross-cutting changes that affect Spore as a whole. ## Read this first -The SEPs in this repository are design records. They are not, by themselves, the -current implementation truth, a compatibility guarantee, or a public release -contract for Spore. +The SEPs in this repository are design records. They define design intent; they +are not, by themselves, compatibility guarantees or public release contracts for +Spore. -For current release behavior, installation guidance, supported syntax, and -implementation status, start with the implementation repository: -`spore/README.md` and `spore/docs/DESIGN.md`. +For release-facing installation guidance, implementation roadmap, and shipped +tool behavior, start with the language repository: `../spore/README.md`, +`../spore/ROADMAP.md`, `../spore/SPARK.md`, and +`../spore/docs/decisions/syntax.md`. This repository is authoritative for proposal history and accepted design -direction. During the bootstrap phase, Draft SEPs may still include target -behavior, future protocol shapes, or examples that are ahead of the compiler. +direction. During bootstrap, Draft SEPs may include target behavior, protocol +shapes, or samples that are ahead of the compiler. -**Current surface typing baseline:** default unsuffixed literals are **`I64`** -for integers and **`F64`** for floats; UTF-8 text is **`Str`**; there is no -`Char` type. SEP-0002 owns the full type-system rules and metavariables for -other fixed widths. +**Signature baseline:** Spore is organized around +`Signature -> Property -> Hole -> Realization -> Evidence`. Base signatures +carry the callable type boundary. Intent signatures add `uses`, `budget`, and +`properties` clauses for verifier, Agent, and Evidence workflows. -## SEP status +**Surface typing baseline:** default unsuffixed literals are **`I64`** for +integers and **`F64`** for floats; UTF-8 text is **`Str`**; the unit surface type +is **`()`**. SEP-0002 owns the full type-system rules and metavariables for +other fixed widths. -| SEP | Title | Status | Role | -|---|---|---|---| -| [SEP-0000](seps/SEP-0000-process.md) | Spore Enhancement Proposal Process | Accepted | Repository process and lifecycle | -| [SEP-0001](seps/SEP-0001-core-syntax.md) | Core Syntax & Signatures | Accepted | Root surface grammar and signature layout | -| [SEP-0002](seps/SEP-0002-type-system.md) | Type System | Draft | Type semantics and checking | -| [SEP-0003](seps/SEP-0003-effect-system.md) | Effect System | Draft | Effect algebra, handlers, and diagnostics | -| [SEP-0004](seps/SEP-0004-cost-analysis.md) | Cost Analysis & Decidability | Draft | Four-slot cost model and verification | -| [SEP-0005](seps/SEP-0005-hole-system.md) | Hole System & Agent Protocol | Draft | Typed holes and agent-facing reports | -| [SEP-0006](seps/SEP-0006-compiler-architecture.md) | Compiler Architecture | Draft | Compiler pipeline and diagnostics | -| [SEP-0007](seps/SEP-0007-concurrency-model.md) | Concurrency Model | Draft | Structured concurrency semantics | -| [SEP-0008](seps/SEP-0008-module-package-system.md) | Module & Package System | Draft | Modules, manifests, platforms, and packages | -| [SEP-0009](seps/SEP-0009-standard-library.md) | Standard Library Surface | Draft | Prelude, core modules, and platform libraries | +## SEP index -The generated machine-readable index is [`seps-index.json`](seps-index.json). +The canonical machine-readable SEP index is [`seps-index.json`](seps-index.json). +It is generated from document front matter and lists each proposal's title, +status, type, dependencies, and source path. Numbered proposals live under +[`seps/`](seps/). ## Reading path -Read [VISION.md](VISION.md) first for the design philosophy. Then use SEPs in -dependency order: +Read [Spore Language Vision](VISION.md) first for the design philosophy; a +Chinese translation is available at [孢子语言愿景](VISION.zh-CN.md). Implementation +planning lives in the sibling language repository's `ROADMAP.md`. Then use SEPs +in dependency order: -1. [SEP-0000](seps/SEP-0000-process.md) for how decisions are made. -2. [SEP-0001](seps/SEP-0001-core-syntax.md) for accepted syntax forms. +1. [SEP-0000](seps/SEP-0000-process.md) for how decisions are made and the guiding + questions used in review. +2. [SEP-0001](seps/SEP-0001-core-syntax.md) for draft root syntax forms. 3. [SEP-0002](seps/SEP-0002-type-system.md) through [SEP-0004](seps/SEP-0004-cost-analysis.md) for core static semantics. -4. [SEP-0005](seps/SEP-0005-hole-system.md) and [SEP-0006](seps/SEP-0006-compiler-architecture.md) for tool and compiler surfaces. +4. [SEP-0005](seps/SEP-0005-hole-report-protocol.md), [SEP-0006](seps/SEP-0006-compiler-architecture.md), and [SEP-0010](seps/SEP-0010-compiler-as-documentation.md) for hole, compiler, diagnostic, and explain surfaces. 5. [SEP-0007](seps/SEP-0007-concurrency-model.md) through [SEP-0009](seps/SEP-0009-standard-library.md) for larger system layers. Use [GLOSSARY.md](GLOSSARY.md) when checking cross-SEP terminology. @@ -61,13 +60,14 @@ Use [GLOSSARY.md](GLOSSARY.md) when checking cross-SEP terminology. - `seps/` - numbered SEP documents and historical process records - `templates/` - authoring templates for new proposals - `schemas/` - machine-readable rules for SEP metadata and shared contracts -- `scripts/` - repository validation and automation helpers +- `scripts/` - uv-managed validation scripts; run `check_repo.py` for the full suite ## Authoring -**SEP** stands for **Spore Enhancement Proposal**. An SEP records changes to -Spore semantics, standard-library surface, tooling protocols, cross-cutting -system design, or the project process itself. +**SEP** stands for **Spore Evolution Proposal** (Spore 演进提案). An SEP +records and reviews changes to Spore semantics, standard-library surface, +tooling protocols, cross-cutting system design, governance, or the project +process itself. For the decision threshold, lifecycle, and authoring rules, see [SEP-0000](seps/SEP-0000-process.md). New proposals should start from the @@ -82,9 +82,17 @@ matching template: Run the repository checks before opening a PR: ```bash -uv run scripts/validate_sep_documents.py +uv run scripts/check_repo.py +``` + +Individual checks remain available for targeted runs: + +```bash uv run scripts/check_sep_index.py +uv run scripts/validate_sep_documents.py +uv run scripts/check_contract_schemas.py uv run scripts/check_terminology_consistency.py +uv run scripts/check_surface_consistency.py ``` If SEP metadata changed, regenerate the committed index first: diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index abca5eb..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,96 +0,0 @@ -# Spore Roadmap - -This is a living document. It describes *what* Spore intends to build, organized by system area. For *how* and *why* at the design level, see individual SEPs. - -## Compiler core (`sporec`) - -Design basis: [SEP-0006](seps/SEP-0006-compiler-architecture.md) - -- Cranelift code generation (native binary output) -- Incremental compilation via salsa (module-level, signature-hash-based invalidation) -- End-to-end pipeline: Source → Lex → Parse → HIR → TypedHIR → Cranelift IR → native - -## Hole system - -Design basis: [SEP-0005](seps/SEP-0005-hole-system.md) - -- Stabilize HoleReport protocol (versioned JSON schema) -- Hole priority and annotation system (`@requires-review`, `@agent-fillable`) -- Multi-hole atomic fill with cross-hole consistency checking -- Agent fill-and-verify loop: propose → validate → review if flagged - -## Type system - -Design basis: [SEP-0002](seps/SEP-0002-type-system.md) - -- L0 refinement type enforcement (decidable predicates at type-check time) -- L1 abstract interpretation propagation (value flow analysis, no SMT) -- Property-based test generation from refinement predicates - -## Effect system - -Design basis: [SEP-0003](seps/SEP-0003-effect-system.md) - -- Platform effect handler dispatch (runtime) -- Effect hierarchy formalization (lattice properties) -- Reference CLI platform: filesystem, network, process, clock, random -- Reference web platform: HTTP server, request/response - -## Cost model - -Design basis: [SEP-0004](seps/SEP-0004-cost-analysis.md) - -- Symbolic cost expression grammar with decidability proof -- Recursive cost analysis (three-tier) -- Cost-aware scheduling hints for runtime - -## Concurrency - -Design basis: [SEP-0007](seps/SEP-0007-concurrency-model.md) - -- Runtime execution of structured concurrency primitives (`spawn`, `await`, `parallel_scope`) -- Compiler-enforced task lifetimes (child cannot outlive parent) -- Cancellation propagation -- Concurrent cost model: conservative max across parallel lanes -- Channel linearity checks - -## Module and package system - -Design basis: [SEP-0008](seps/SEP-0008-module-package-system.md) - -- Content-addressed package store (local `.spore-store` + pluggable backends) -- `spore.toml` + `.spore-lock` manifest workflow -- Package discovery, search, and documentation generation - -## Diagnostics and developer experience - -Design basis: [SEP-0006](seps/SEP-0006-compiler-architecture.md) - -- LSP server (`spore-lsp`) with completions, diagnostics, go-to-definition, hole integration -- `spore watch` with real-time incremental diagnostics -- `spore watch --json` NDJSON events for IDE/Agent consumption - -## Standard library - -Design basis: [SEP-0009](seps/SEP-0009-standard-library.md) - -- `spore.list`, `spore.map`, `spore.set`, `spore.str`, `spore.math`, `spore.ref` -- All further libraries as third-party packages - -## Self-hosting - -Design basis: [SEP-0006](seps/SEP-0006-compiler-architecture.md) - -- Partial self-hosting: parser, type checker, cost analyzer rewritten in Spore -- Performance target: compiled output within 2x of equivalent Rust for compute-bound code -- Formal language specification (beyond design docs) -- Stability policy: backward compatibility for signatures and hole protocol - -## Long-term explorations - -These are not committed but may influence future design: - -- Distributed effect delegation with cryptographic attestation -- Optional formal verification mode for safety-critical paths -- Visual hole explorer for interactive dependency graph navigation -- Cross-platform compilation: WASM, embedded, GPU via effect-gated backends diff --git a/VISION.md b/VISION.md index e9718ea..9735d26 100644 --- a/VISION.md +++ b/VISION.md @@ -1,47 +1,97 @@ -# Spore Design Vision +# Spore Language Vision -Spore is a compiled, general-purpose programming language where **intent is a first-class citizen**. +Spore is an intent programming language for turning stated design intent into +checked, evidence-backed programs. + +Its core semantic path is: + +```text +Signature -> Property -> Hole -> Realization -> Evidence +``` + +> **Signature bounds possibility. Property specifies intent. Hole marks typed +> absence. Realization implements the missing. Evidence records what was +> checked.** ## Core principles ### 1. Signatures are gravity centers -A function signature is a complete specification: input/output types, error sets, cost budget, and required effects. All downstream analysis flows from the signature. The body can be empty — the intent is already fully expressed. +A signature has two layers. + +The **Base Signature** is the callable type boundary: name, type parameters and +inline bounds, value parameters, and result type (including any outcome +boundary). It defines the possible implementation space without fixing any +particular member of it. + +The **Intent Signature** adds the constraints that guide reviewers, tools, and +Agents: effect surface, realization-shape budget, and properties. Budgets +are named bounds on acceptable realization shape, not Big-O notation and not a +machine-resource model. + +Together, the two layers make every callable a small, machine-readable design +record rather than a name attached to a body. Stateful testing and +instrumentation use explicit effects and selected handlers rather than +user-level mutation hidden inside the body. + +### 2. Properties specify intent + +Properties express which possible implementations match the programmer's +intent. A zero-argument property is a concrete witness; a parameterized +property is a generalized validity rule over an input space. + +Properties do not invalidate every other implementation. They make intended +behavior explicit so realizations, reviewers, and Agents have something +checkable to preserve. -### 2. Holes are collaboration points, not errors +### 3. Holes are typed absence -A program with holes compiles successfully. It is *partial*, not broken. Holes are the primary mechanism for structured collaboration between humans, Agents, and teams. The compiler provides a self-contained HoleReport for each hole so that any reader — human or machine — can propose a fill without additional context. +A program with holes is partial, not broken. A hole is not a comment or a +TODO; it is typed absence constrained by the Base Signature, visible bindings, +effect context, budget context, and property context that surround it. -### 3. Architecture first, details later +A hole report should expose enough context for any reader — human or machine — +to propose a realization without further conversation. The compiler should make +that context available as stable structured data, while explain projections teach +readers how to interpret the same facts. -The recommended workflow is top-down: define signatures and types, verify the skeleton with the compiler, then fill holes iteratively in dependency order. Design-critical decisions must be reviewed explicitly before implementation proceeds. +### 4. Realization implements the missing -### 4. The compiler is a documentation assistant +A realization turns typed absence into implementation. It may be written by a +human, proposed by an Agent, or produced by a tool, but it is always guided by +the surrounding signature, budget, and properties. -Every compiler output — diagnostics, hole reports, incremental events, dependency graphs — is available in both human-readable and machine-readable form. Humans see clear error messages with repair hints; Agents see structured JSON suitable for automated reasoning. +Realization is not arbitrary code generation. It is implementation under +declared intent, with budgets constraining implementation complexity, review +cost, and Agent freedom. -### 5. Explicit effect and cost models +### 5. Evidence records what was checked -Every side effect must be covered by declared effects. Every computational cost can be bounded and verified at compile time. A function's impact is fully assessable from its signature alone. +Spore does not overclaim proof. Evidence is generated by tools and records +what was checked for a concrete realization, which checker produced the result, +what the result was, and which provenance hashes bind the result to its source +and dependencies. -### 6. Content-addressed, not version-numbered +Evidence should be readable by humans and structured for machines. Teaching +metadata and explain pages can project those records for readers, but they do +not replace the underlying evidence identity. Evidence is useful only when tied +to precise content identity: signatures, intents, properties, realizations, +checkers, evidence records, and dependencies can each be named by +the content they actually contain. -Modules are identified by content hashes (signature hash for interface compatibility, implementation hash for caching). No semver, no diamond dependency conflicts. +Evidence is checked support, not a stamp of correctness. ## Intellectual heritage -| Language | Idea adopted | -|----------|-------------| -| **Agda** | Holes as first-class typed placeholders | -| **Idris** | Elaboration — the compiler fills in details the programmer omits | -| **Unison** | Content-addressed code — modules identified by hash, not name+version | -| **Elm** | Human-friendly error messages as a primary design goal | -| **Roc** | Managed effects — all IO through platform-provided effect handlers | - -## Guiding questions for every design decision - -1. Does this make user intent **clearer** or more obscure? -2. Does this **reduce** ambiguity for Agents? -3. Can the key semantics be exported in a **stable, machine-readable** form? -4. Does it improve or degrade **diagnostics and repair workflows**? -5. Does it preserve **conceptual coherence** with the rest of Spore? +| Source | Idea adopted | +| --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| [**Agda**](https://github.com/agda/agda) | Typed holes as goal-shaped program positions | +| [**Idris 2**](https://idris-lang.org/) | Elaboration and type-directed development | +| [**Unison**](https://github.com/unisonweb/unison) | Content-addressed code and reproducible references | +| [**Nix**](https://nixos.org/) | Reproducible dependency inputs and explicit build provenance | +| [**Elm compiler errors**](https://elm-lang.org/news/compiler-errors-for-humans) | Human-friendly diagnostics as a design goal | +| [**Roc**](https://github.com/roc-lang/roc) | Managed effects through explicit platform boundaries | +| [**Koka**](https://koka-lang.github.io/koka/doc/index.html) | Effect handlers and explicit effect tracking | +| [**Eiffel / Design by Contract**](https://www.eiffel.org/doc/eiffel/ET-_Design_by_Contract_%28tm%29%2C_Assertions_and_Exceptions) | Behavioral intent expressed through explicit contracts and properties | +| [**QuickCheck**](https://hackage.haskell.org/package/QuickCheck) | Parameterized properties as executable behavioral checks | +| [**Proof-carrying code (Necula)**](https://en.wikipedia.org/wiki/Proof-carrying_code) | Evidence as checked support without overclaiming proof | diff --git a/VISION.zh-CN.md b/VISION.zh-CN.md new file mode 100644 index 0000000..04929fb --- /dev/null +++ b/VISION.zh-CN.md @@ -0,0 +1,82 @@ +# 孢子语言愿景 + +孢子是一门意图编程语言,用于把已声明的设计意图转化为经过检查、由证据支撑的程序。 + +它的核心语义路径是: + +```text +Signature -> Property -> Hole -> Realization -> Evidence +``` + +> **签名限定可能性。性质说明意图。洞标记有类型的缺失。实现(Realization)填补缺失。 +> 证据(Evidence)记录已经检查过什么。** + +## 核心原则 + +### 1. 签名是重心 + +签名有两层。 + +**基础签名(Base Signature)** 是可调用对象的类型边界:名称、类型参数与 +内联约束、值参数、返回类型,以及错误边界。它定义了实现空间,但不固定 +其中任何一个具体实现。 + +**意图签名(Intent Signature)** 增加用于指导评审、工具和智能体的约束: +效应表面、实现形态预算和性质。预算是对可接受实现形态的命名边界,不是 +Big-O 标记,也不是机器资源模型。 + +这两层合在一起,让每个可调用对象都成为一个小型、机器可读的设计记录, +而不只是一个附着在函数体上的名字。 + +### 2. 性质说明意图 + +性质表达哪些可能实现符合程序员的意图。零参数性质是一个具体见证;带参数 +性质是在输入空间上的通用有效性规则。 + +性质并不会让所有其他实现都“无效”。它们把预期行为显式化,让实现、评审者 +和智能体都有可检查、可保留的对象。 + +### 3. 洞是有类型的缺失 + +带洞的程序是部分完成的程序,而不是错误的程序。洞不是注释,也不是 TODO; +它是有类型的缺失,受基础签名、可见绑定、效应上下文、预算上下文和性质 +上下文约束。 + +洞报告应该暴露足够的上下文,让任何读者——无论是人还是机器——都能在不额外 +对话的情况下提出实现。编译器应该把这些上下文同时呈现为可读解释和稳定的 +结构化数据。 + +### 4. 实现填补缺失 + +实现(Realization)把有类型的缺失变成具体实现。它可以由人写出,可以由 +智能体提议,也可以由工具生成;但它始终由周围的签名、预算和性质引导。 + +实现不是任意代码生成。它是在已声明意图之下进行的实现;预算约束实现复杂度、 +评审成本,以及智能体的自由度。 + +### 5. 证据记录已经检查过什么 + +孢子不夸大证明能力。证据由工具生成,记录针对某个具体实现检查了什么、 +哪个检查器产生了结果、结果是什么,以及哪些来源哈希把该结果绑定到源码和 +依赖。 + +证据应该既便于人类阅读,也便于机器处理。证据只有在绑定到精确内容身份时 +才有用:签名、意图、性质、实现、检查器、证据记录和依赖,都可以由其实际 +包含的内容命名。 + +证据是经过检查的支撑,而不是正确性印章。 + +## 思想来源 + +| 来源 | 采用的思想 | +| --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| [**Agda**](https://github.com/agda/agda) | 将类型洞作为目标形状的程序位置 | +| [**Idris 2**](https://idris-lang.org/) | elaboration 与类型引导式开发 | +| [**Unison**](https://github.com/unisonweb/unison) | 内容寻址代码与可复现引用 | +| [**Nix**](https://nixos.org/) | 可复现依赖输入与显式构建来源 | +| [**Elm compiler errors**](https://elm-lang.org/news/compiler-errors-for-humans) | 以人类友好的诊断作为设计目标 | +| [**Roc**](https://github.com/roc-lang/roc) | 通过显式平台边界管理效应 | +| [**Koka**](https://koka-lang.github.io/koka/doc/index.html) | 效应处理器与显式效应追踪 | +| [**Eiffel / Design by Contract**](https://www.eiffel.org/doc/eiffel/ET-_Design_by_Contract_%28tm%29%2C_Assertions_and_Exceptions) | 通过显式契约和性质表达行为意图 | +| [**QuickCheck**](https://hackage.haskell.org/package/QuickCheck) | 将参数化性质作为可执行的行为检查 | +| [**Proof-carrying code (Necula)**](https://en.wikipedia.org/wiki/Proof-carrying_code) | 证据是经过检查的支撑,而不过度声称证明 | diff --git a/drafts/README.md b/drafts/README.md index e98ec3b..d59111d 100644 --- a/drafts/README.md +++ b/drafts/README.md @@ -1,5 +1,8 @@ # Draft proposals -This directory is for unnumbered proposal drafts that are still under active discussion. +This directory is for unnumbered proposal drafts that are still under active +discussion. -Once a draft is accepted and merged as an SEP, it should move into `seps/` and receive its permanent SEP number. +When maintainers decide a draft should become a numbered SEP, it moves into +`seps/` and receives its permanent SEP number. Numbering does not by itself +change proposal status; a numbered SEP may still be `Draft`. diff --git a/drafts/script-mode.md b/drafts/script-mode.md index 8e745ff..bca7e59 100644 --- a/drafts/script-mode.md +++ b/drafts/script-mode.md @@ -189,7 +189,7 @@ Example: ```spore /* spore-script -requires-spore = ">=0.1.0" +requires-spore = "current" [platform] path = "../platforms/cli" @@ -207,9 +207,9 @@ error. It must not silently ignore malformed structured metadata. ### Metadata shape -V1 script metadata contains: +Script metadata contains: -- `requires-spore = ""` +- `requires-spore = ""` - `[platform]` carrying script-local platform selection metadata that parallels manifest platform configuration, without requiring the exact same table shape - `[dependencies]` using the same dependency-entry shape as manifest diff --git a/prek.toml b/prek.toml index a6c6764..4cc3539 100644 --- a/prek.toml +++ b/prek.toml @@ -68,6 +68,7 @@ repo = "local" hooks = [ { id = "terminology-consistency", name = "terminology-consistency", entry = "uv run scripts/check_terminology_consistency.py", language = "system", files = "^(GLOSSARY\\.md|seps/SEP-000[3468]-.*\\.md)$", pass_filenames = false }, { id = "contract-schemas", name = "contract-schemas", entry = "uv run scripts/check_contract_schemas.py", language = "system", files = "^schemas/contracts/.*\\.json$|^scripts/check_contract_schemas\\.py$", pass_filenames = false }, + { id = "surface-consistency", name = "surface-consistency", entry = "uv run scripts/check_surface_consistency.py", language = "system", files = "^(VISION\\.md|README\\.md|GLOSSARY\\.md|(seps|drafts|templates)/.*\\.md|scripts/check_surface_consistency\\.py)$", pass_filenames = false }, { id = "sep-index", name = "sep-index", entry = "uv run scripts/check_sep_index.py", language = "system", files = "^(drafts|seps)/.*\\.md$|^seps-index\\.json$", pass_filenames = false }, { id = "sep-documents", name = "sep-documents", entry = "uv run scripts/validate_sep_documents.py", language = "system", files = "^(drafts|seps)/.*\\.md$", pass_filenames = true }, ] diff --git a/schemas/sep-frontmatter.schema.json b/schemas/sep-frontmatter.schema.json index 98e689a..25dc3ac 100644 --- a/schemas/sep-frontmatter.schema.json +++ b/schemas/sep-frontmatter.schema.json @@ -34,7 +34,6 @@ "Draft", "Accepted", "Rejected", - "Withdrawn", "Superseded" ] }, diff --git a/scripts/check_contract_schemas.py b/scripts/check_contract_schemas.py index 9cba42a..2387f2a 100644 --- a/scripts/check_contract_schemas.py +++ b/scripts/check_contract_schemas.py @@ -1,4 +1,9 @@ -#!/usr/bin/env -S uv run +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// from __future__ import annotations @@ -6,7 +11,8 @@ import sys from pathlib import Path -ROOT = Path(__file__).resolve().parent.parent +from sep_common import ROOT, load_json, report_errors + CONTRACTS_ROOT = ROOT / "schemas" / "contracts" CATALOG_PATH = CONTRACTS_ROOT / "catalog.json" CATALOG_URL = "https://raw.githubusercontent.com/spore-lang/spore-evolution/main/schemas/contracts/catalog.json" @@ -18,18 +24,6 @@ JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema" -def load_json(path: Path) -> tuple[object | None, str | None]: - try: - return json.loads(path.read_text(encoding="utf-8")), None - except OSError as exc: - return None, f"{path}: failed to read JSON file ({exc})" - except json.JSONDecodeError as exc: - return ( - None, - f"{path}: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}", - ) - - def require_string(entry: dict[str, object], key: str, errors: list[str], scope: str) -> str | None: value = entry.get(key) if isinstance(value, str) and value: @@ -163,14 +157,11 @@ def main() -> int: if not file_name.startswith(expected_prefix): errors.append(f"{scope}: file `{file_name}` must start with `{expected_prefix}`") - if errors: - print("Contract schema validation failed:\n", file=sys.stderr) - for error in errors: - print(f"- {error}", file=sys.stderr) - return 1 - - print(f"Validated {len(schemas)} contract schema(s).") - return 0 + return report_errors( + errors, + "Contract schema validation failed", + success_message=f"Validated {len(schemas)} contract schema(s).", + ) if __name__ == "__main__": diff --git a/scripts/check_pr_template.py b/scripts/check_pr_template.py deleted file mode 100644 index eab18b1..0000000 --- a/scripts/check_pr_template.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env -S uv run - -from __future__ import annotations - -import json -import os -import sys -from pathlib import Path - -REQUIRED_CHECKBOXES = [ - "- [x] I linked the canonical discussion thread.", - "- [x] I used the correct SEP template for this proposal type.", - "- [x] I updated front matter metadata and required sections.", -] - - -def main() -> int: - event_path = os.environ.get("GITHUB_EVENT_PATH") - if not event_path: - print("GITHUB_EVENT_PATH is not set; skipping PR template validation.") - return 0 - - payload = json.loads(Path(event_path).read_text(encoding="utf-8")) - pull_request = payload.get("pull_request") - if not pull_request: - print("No pull_request payload found; skipping PR template validation.") - return 0 - - body = pull_request.get("body") or "" - missing = [item for item in REQUIRED_CHECKBOXES if item not in body] - - if missing: - print("Pull request body is missing required checked items:\n", file=sys.stderr) - for item in missing: - print(f"- {item}", file=sys.stderr) - return 1 - - print("Pull request template checklist looks good.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_repo.py b/scripts/check_repo.py new file mode 100644 index 0000000..a4c130b --- /dev/null +++ b/scripts/check_repo.py @@ -0,0 +1,48 @@ +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "check-jsonschema", +# ] +# /// + +from __future__ import annotations + +import sys +from collections.abc import Callable + +from check_contract_schemas import main as check_contract_schemas +from check_sep_index import main as check_sep_index +from check_surface_consistency import main as check_surface_consistency +from check_terminology_consistency import main as check_terminology_consistency +from validate_sep_documents import main as validate_sep_documents + +CHECKS: tuple[tuple[str, Callable[[], int]], ...] = ( + ("SEP index", check_sep_index), + ("Contract schemas", check_contract_schemas), + ("SEP documents", validate_sep_documents), + ("Terminology consistency", check_terminology_consistency), + ("Surface consistency", check_surface_consistency), +) + + +def main() -> int: + failed: list[str] = [] + + for name, check in CHECKS: + if check() != 0: + failed.append(name) + + if failed: + print("Repository checks failed:\n", file=sys.stderr) + for name in failed: + print(f"- {name}", file=sys.stderr) + return 1 + + print(f"All {len(CHECKS)} repository checks passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_sep_index.py b/scripts/check_sep_index.py index d65c53c..bd7fc29 100644 --- a/scripts/check_sep_index.py +++ b/scripts/check_sep_index.py @@ -1,4 +1,9 @@ -#!/usr/bin/env -S uv run +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// from __future__ import annotations @@ -6,7 +11,7 @@ import json import sys -from sep_common import INDEX_PATH, build_sep_index, load_documents +from sep_common import INDEX_PATH, build_sep_index, load_documents, report_errors def expected_index_text() -> tuple[str, list[str]]: @@ -29,10 +34,7 @@ def main() -> int: expected, errors = expected_index_text() if errors: - print("SEP index generation failed:\n", file=sys.stderr) - for error in errors: - print(f"- {error}", file=sys.stderr) - return 1 + return report_errors(errors, "SEP index generation failed") current = INDEX_PATH.read_text(encoding="utf-8") if INDEX_PATH.exists() else None diff --git a/scripts/check_surface_consistency.py b/scripts/check_surface_consistency.py new file mode 100644 index 0000000..9980fea --- /dev/null +++ b/scripts/check_surface_consistency.py @@ -0,0 +1,183 @@ +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// + +from __future__ import annotations + +import re +from collections.abc import Iterable +from pathlib import Path + +from sep_common import ( + GUIDING_QUESTIONS_HEADING, + GUIDING_QUESTIONS_LINK_FRAGMENT, + ROOT, + draft_markdown_files, + iter_markdown_lines, + relative_path, + report_errors, + sep_markdown_files, + template_markdown_files, + vision_files, +) + + +def add_pattern_errors( + errors: list[str], + paths: Iterable[Path], + pattern: re.Pattern[str], + message: str, +) -> None: + for path, line_no, line in iter_markdown_lines(paths): + if pattern.search(line): + errors.append(f"{relative_path(path)}:{line_no}: {message}: {line.strip()}") + + +def self_markdown_files() -> list[Path]: + return [ + ROOT / "README.md", + ROOT / "GLOSSARY.md", + *sep_markdown_files(), + ] + + +def user_facing_markdown_files() -> list[Path]: + return [ + ROOT / "README.md", + ROOT / "GLOSSARY.md", + *vision_files(), + *sep_markdown_files(), + *draft_markdown_files(), + *template_markdown_files(), + ] + + +def files_outside_sep_0000() -> list[Path]: + return [ + path + for path in user_facing_markdown_files() + if path.name != "SEP-0000-process.md" + ] + + +def main() -> int: + errors: list[str] = [] + own_docs = self_markdown_files() + + forbidden_self_patterns = [ + ( + re.compile(r"spore/docs/DESIGN\.md"), + "use spore/SPARK.md or spore/docs/decisions/syntax.md instead of the retired design path", + ), + ( + re.compile(r"\bwhere\s+[A-Z][A-Za-z0-9_]*(?:\[[^\]]+\])?\s*:"), + "standalone generic bounds are retired; use inline type parameter bounds", + ), + ( + re.compile(r"\bWhereClause\b|\bWhereConstraint\b|\bBoundExpr\b"), + "standalone generic-bound grammar is retired", + ), + ( + re.compile(r"\bcost\s*\["), + "positional resource syntax is retired; use `budget { ... }`", + ), + ( + re.compile( + r"\bCostVector\b|\bCostExpr\b|four-slot|4D|four-dimensional|@unbounded|with_cost_limit" + ), + "old resource-cost terminology is retired", + ), + ( + re.compile( + r"\bspec\s*\{|\bSpecClause\b|\bSpecItem\b|\bExampleItem\b|\bLawItem\b" + ), + "the old spec block grammar is retired; use `properties { ... }`", + ), + ( + re.compile(r"\bexample\s+\"|\blaw\s+\""), + "old spec item keywords are retired; use named properties", + ), + ( + re.compile(r"\bspec hash\b|\bsig hash\b|\bimpl hash\b"), + "old hash labels are retired; use signature_hash, intent_hash, property_hash, realization_hash, or evidence_hash", + ), + ( + re.compile(r"\bK0xxx\b|\bS0xxx\b|`K[0-9]{4}`|`S[0-9]{4}`"), + "old cost/spec diagnostic prefixes are retired; use B0xxx and P0xxx", + ), + ( + re.compile(r"(?:->|:)\s*`?Unit\b`?"), + "use `()` as the unit type surface spelling", + ), + ( + re.compile( + r"\b(?:current|Current|currently|Currently|today|Today|implemented|Implemented|" + r"implementation status|not yet implemented|stable implementation|current stable|" + r"current implementation|current design|current target|current wave|for the current|status quo)\b" + ), + "keep SEPs design-oriented; avoid implementation or present-status wording", + ), + ] + + for pattern, message in forbidden_self_patterns: + add_pattern_errors(errors, own_docs, pattern, message) + + add_pattern_errors( + errors, + user_facing_markdown_files(), + re.compile( + r"\bSignature\s+v[0-9]+\b|\bV[0-9]+\b|\bv[0-9]+(?:\.[0-9]+)*\b|" + r"\bver" r"sion[- ]range\b|\bSemantic ver" r"sion\b|\bChinese ver" r"sion\b|" + r"版本" r"号|小设计" r"版本" + ), + "avoid numbered narrative labels in user-facing prose; use stable names instead", + ) + + add_pattern_errors( + errors, + user_facing_markdown_files(), + re.compile(r"\bReviewing\b|\bWithdrawn\b|\bwithdrawn\b"), + "SEP status prose must use only Draft, Accepted, Rejected, or Superseded", + ) + + add_pattern_errors( + errors, + files_outside_sep_0000(), + re.compile(re.escape(GUIDING_QUESTIONS_HEADING)), + "guiding questions belong in SEP-0000; link to SEP-0000 instead of duplicating them", + ) + add_pattern_errors( + errors, + vision_files(), + re.compile(r"^## (?:Recommended syntax shape|推荐语法)"), + "vision documents should stay principle-level; syntax shape belongs in concrete SEPs", + ) + add_pattern_errors( + errors, + vision_files(), + re.compile(r"^## (?:Guiding questions\b|.*引导问题)"), + "vision documents should stay principle-level; guiding questions belong in SEP-0000", + ) + + for template in template_markdown_files(): + if not template.is_file(): + continue + text = template.read_text(encoding="utf-8") + if GUIDING_QUESTIONS_LINK_FRAGMENT not in text: + errors.append( + f"{relative_path(template)}: must link to `{GUIDING_QUESTIONS_LINK_FRAGMENT}` so authors " + "find the canonical guiding-question section in SEP-0000" + ) + + return report_errors( + errors, + "Surface consistency check failed", + success_message="Surface consistency check passed.", + ) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_terminology_consistency.py b/scripts/check_terminology_consistency.py index 248f676..77e8f72 100644 --- a/scripts/check_terminology_consistency.py +++ b/scripts/check_terminology_consistency.py @@ -1,22 +1,40 @@ -#!/usr/bin/env -S uv run +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// from __future__ import annotations import re -import sys -from pathlib import Path -from sep_common import ROOT +from sep_common import ROOT, numbered_sep_files, relative_path, report_errors, vision_files TARGETS = ( + ROOT / "README.md", + *vision_files(), ROOT / "GLOSSARY.md", - ROOT / "seps" / "SEP-0003-effect-system.md", - ROOT / "seps" / "SEP-0004-cost-analysis.md", - ROOT / "seps" / "SEP-0006-compiler-architecture.md", - ROOT / "seps" / "SEP-0008-module-package-system.md", + *numbered_sep_files(), ) FORBIDDEN_PATTERNS = ( + ( + re.compile(r"\b[Cc]apab(?:ility|ilities)\b"), + "retired non-effect terminology; use `effect`, `effect surface`, or `effect context`", + ), + ( + re.compile(r"\bruntime[- ]effects?\b", re.IGNORECASE), + "retired split term `runtime effect`; use `effect` unless contrasting with runtime behavior generally", + ), + ( + re.compile(r"能力表面|能力上下文|运行时效应"), + "retired zh-CN terminology; use `效应表面`, `效应上下文`, or `效应`", + ), + ( + re.compile(r"\bC0xxx\b"), + "retired effect diagnostic prefix `C0xxx`; use `F0xxx`", + ), ( re.compile(r"\bNetRead\b"), "retired term `NetRead`; use `NetConnect` or `NetListen` depending on intent", @@ -35,7 +53,27 @@ ), ( re.compile(r"cost\s*<="), - "retired ASCII syntax `cost <=`; use four-slot `cost [compute, alloc, io, parallel]`", + "retired resource syntax `cost <=`; use `budget { ... }` when realization shape must be constrained", + ), + ( + re.compile(r"\bcost\s*\["), + "retired positional resource syntax; use `budget { ... }`", + ), + ( + re.compile(r"\bCostVector\b|\bCostExpr\b|@unbounded|with_cost_limit"), + "retired resource-cost terminology; use budget and evidence terminology", + ), + ( + re.compile(r"\bspec\s*\{|\bexample\s+\"|\blaw\s+\""), + "retired spec syntax; use `properties { name(params): expr }`", + ), + ( + re.compile(r"\bwhere\s+[A-Z][A-Za-z0-9_]*(?:\[[^\]]+\])?\s*:"), + "retired standalone generic-bound syntax; use inline type parameter bounds", + ), + ( + re.compile(r"\bsig hash\b|\bimpl hash\b|\bspec hash\b"), + "retired hash labels; use named signature provenance hash fields", ), ) @@ -60,17 +98,14 @@ def main() -> int: for pattern, message in FORBIDDEN_PATTERNS: if pattern.search(line): violations.append( - f"{path.relative_to(ROOT)}:{line_number}: {message}\n {line}" + f"{relative_path(path)}:{line_number}: {message}\n {line}" ) - if violations: - print("Terminology consistency check failed:\n", file=sys.stderr) - for violation in violations: - print(f"- {violation}", file=sys.stderr) - return 1 - - print("Terminology consistency check passed.") - return 0 + return report_errors( + violations, + "Terminology consistency check failed", + success_message="Terminology consistency check passed.", + ) if __name__ == "__main__": diff --git a/scripts/normalize_spore_surface_types.py b/scripts/normalize_spore_surface_types.py index e1563eb..2ced28e 100644 --- a/scripts/normalize_spore_surface_types.py +++ b/scripts/normalize_spore_surface_types.py @@ -1,4 +1,9 @@ -#!/usr/bin/env -S uv run +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// """Rewrite legacy surface names inside ```spore / ```Spore fences only. @@ -15,7 +20,7 @@ import sys from pathlib import Path -from sep_common import ROOT +from sep_common import numbered_sep_files # ```spore or ```Spore, consume optional info string after language token. FENCE_OPEN = re.compile(r"^```[Ss]pore[^\n]*\n", re.MULTILINE) @@ -79,7 +84,7 @@ def process_file(path: Path) -> bool: def main(argv: list[str]) -> int: - paths = [Path(p) for p in argv[1:]] if len(argv) > 1 else sorted((ROOT / "seps").glob("SEP-*.md")) + paths = [Path(p) for p in argv[1:]] if len(argv) > 1 else numbered_sep_files() updated = sum(process_file(p) for p in paths) print(f"Updated {updated} file(s).") return 0 diff --git a/scripts/sep_common.py b/scripts/sep_common.py index c76e13d..f981e38 100644 --- a/scripts/sep_common.py +++ b/scripts/sep_common.py @@ -1,26 +1,115 @@ from __future__ import annotations +import json import re +import sys +from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +SEPS_DIR = ROOT / "seps" +DRAFTS_DIR = ROOT / "drafts" +TEMPLATES_DIR = ROOT / "templates" +SCHEMAS_DIR = ROOT / "schemas" +FRONTMATTER_SCHEMA_PATH = SCHEMAS_DIR / "sep-frontmatter.schema.json" INDEX_PATH = ROOT / "seps-index.json" INDEX_VERSION = 1 -INDEX_FIELDS = ( - "sep", - "title", - "status", - "type", - "authors", - "created", - "requires", - "discussion", - "pr", - "superseded_by", + +GUIDING_QUESTIONS_HEADING = "## Guiding questions for every design decision" +GUIDING_QUESTIONS_LINK_FRAGMENT = ( + "SEP-0000-process.md#guiding-questions-for-every-design-decision" ) +def load_json(path: Path) -> tuple[object | None, str | None]: + try: + return json.loads(path.read_text(encoding="utf-8")), None + except OSError as exc: + return None, f"{path}: failed to read JSON file ({exc})" + except json.JSONDecodeError as exc: + return ( + None, + f"{path}: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}", + ) + + +def frontmatter_index_fields() -> tuple[str, ...]: + schema, error = load_json(FRONTMATTER_SCHEMA_PATH) + if error is not None: + raise ValueError(error) + if not isinstance(schema, dict): + raise ValueError(f"{FRONTMATTER_SCHEMA_PATH}: schema must be a JSON object") + + required = schema.get("required") + if not isinstance(required, list) or not required: + raise ValueError(f"{FRONTMATTER_SCHEMA_PATH}: `required` must be a non-empty array") + + return tuple(str(field) for field in required) + + +INDEX_FIELDS = frontmatter_index_fields() + + +def report_errors(errors: list[str], title: str, *, success_message: str | None = None) -> int: + if errors: + print(f"{title}:\n", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + if success_message is not None: + print(success_message) + return 0 + + +def iter_markdown_lines( + paths: Iterable[Path], +) -> Iterable[tuple[Path, int, str]]: + for path in sorted(paths): + if not path.is_file(): + continue + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + for line_no, line in enumerate(text.splitlines(), start=1): + yield path, line_no, line + + +def relative_path(path: Path) -> Path: + return path.relative_to(ROOT) if path.is_relative_to(ROOT) else path + + +def vision_files() -> list[Path]: + return sorted(ROOT.glob("VISION*.md")) + + +def numbered_sep_files() -> list[Path]: + return sorted(SEPS_DIR.glob("SEP-*.md")) + + +def sep_markdown_files() -> list[Path]: + return sorted(SEPS_DIR.glob("*.md")) + + +def draft_markdown_files() -> list[Path]: + return sorted(DRAFTS_DIR.glob("*.md")) + + +def template_markdown_files() -> list[Path]: + return sorted(TEMPLATES_DIR.glob("*.md")) + + +def discover_markdown_files() -> list[Path]: + return sorted( + path + for directory in (DRAFTS_DIR, SEPS_DIR) + for path in directory.rglob("*.md") + if path.name != "README.md" + ) + + @dataclass(frozen=True) class SepDocument: path: Path @@ -110,15 +199,6 @@ def headings(body: str) -> set[str]: return found -def discover_markdown_files() -> list[Path]: - return sorted( - path - for directory in (ROOT / "drafts", ROOT / "seps") - for path in directory.rglob("*.md") - if path.name != "README.md" - ) - - def load_documents() -> tuple[list[SepDocument], list[str]]: documents: list[SepDocument] = [] errors: list[str] = [] diff --git a/scripts/validate_sep_documents.py b/scripts/validate_sep_documents.py index d9a325b..cd68acb 100644 --- a/scripts/validate_sep_documents.py +++ b/scripts/validate_sep_documents.py @@ -1,17 +1,32 @@ -#!/usr/bin/env -S uv run +#!/usr/bin/env -S uv run --script +# +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "check-jsonschema", +# ] +# /// from __future__ import annotations import os import re -import shutil import subprocess -import sys from pathlib import Path from tempfile import TemporaryDirectory -from sep_common import ROOT, SepDocument, headings, load_documents - -SCHEMA_PATH = ROOT / "schemas" / "sep-frontmatter.schema.json" +from sep_common import ( + FRONTMATTER_SCHEMA_PATH, + GUIDING_QUESTIONS_HEADING, + ROOT, + SepDocument, + extract_front_matter, + headings, + load_documents, + parse_front_matter, + report_errors, +) + +SCHEMA_PATH = FRONTMATTER_SCHEMA_PATH REQUIRED_SECTIONS = { "Standards Track": [ @@ -52,24 +67,42 @@ ], } -ALLOWED_STATUSES = {"Draft", "Accepted", "Rejected", "Withdrawn", "Superseded"} +EXECUTIVE_SUMMARY_PREFIX = "> **Executive Summary**:" +EXECUTIVE_SUMMARY_TEXT_PREFIX = "**Executive Summary**:" + +ALLOWED_STATUSES = { + "Draft", + "Accepted", + "Rejected", + "Superseded", +} ALLOWED_TRANSITIONS = { - "Draft": {"Draft", "Accepted", "Rejected", "Withdrawn"}, + "Draft": {"Draft", "Accepted", "Rejected"}, "Accepted": {"Accepted", "Superseded"}, "Rejected": {"Rejected"}, - "Withdrawn": {"Withdrawn"}, "Superseded": {"Superseded"}, } - -def validate_front_matter_schema(documents: list[SepDocument], errors: list[str]) -> None: +GUIDING_QUESTION_MARKERS = [ + "Does this make intent clearer?", + "Does this reduce ambiguity for Agents?", + "preserve the distinction between base signature and intent signature", + "exported in a stable, machine-readable form", + "realized without extra conversation", + "every check produce evidence", + "tied to content hashes and invalidated precisely", + "diagnostics, repair, and review workflows", + "preserve the semantic path", + "Signature -> Property -> Hole -> Realization -> Evidence", +] + + +def validate_front_matter_schema( + documents: list[SepDocument], errors: list[str] +) -> None: if not documents: return - if shutil.which("uvx") is None: - errors.append("`uvx` is required to run check-jsonschema") - return - with TemporaryDirectory(prefix="sep-frontmatter-") as temp_dir: for document in documents: temp_path = Path(temp_dir) / document.relative_path.with_suffix(".yaml") @@ -78,9 +111,6 @@ def validate_front_matter_schema(documents: list[SepDocument], errors: list[str] result = subprocess.run( [ - "uvx", - "--from", - "check-jsonschema", "check-jsonschema", "--schemafile", str(SCHEMA_PATH), @@ -101,7 +131,9 @@ def validate_front_matter_schema(documents: list[SepDocument], errors: list[str] ) -def validate_filename_and_title(path: Path, meta: dict[str, object], errors: list[str]) -> None: +def validate_filename_and_title( + path: Path, meta: dict[str, object], errors: list[str] +) -> None: if path.parent.name == "drafts": if meta["sep"] is not None: errors.append(f"{path}: drafts must use `sep: null`") @@ -131,7 +163,9 @@ def validate_filename_and_title(path: Path, meta: dict[str, object], errors: lis errors.append(f"{path}: filename prefix must match SEP number") -def validate_sections(path: Path, meta: dict[str, object], body: str, errors: list[str]) -> None: +def validate_sections( + path: Path, meta: dict[str, object], body: str, errors: list[str] +) -> None: required = REQUIRED_SECTIONS.get(str(meta["type"]), []) found = headings(body) missing = [section for section in required if section not in found] @@ -139,7 +173,58 @@ def validate_sections(path: Path, meta: dict[str, object], body: str, errors: li errors.append(f"{path}: missing required sections: {', '.join(missing)}") -def validate_cross_field_rules(path: Path, meta: dict[str, object], errors: list[str]) -> None: +def validate_executive_summary(path: Path, body: str, errors: list[str]) -> None: + lines = body.splitlines() + index = 0 + while index < len(lines) and not lines[index].strip(): + index += 1 + + if index >= len(lines) or not lines[index].startswith("# "): + errors.append(f"{path}: first body heading must be the SEP title H1") + return + + index += 1 + while index < len(lines) and not lines[index].strip(): + index += 1 + + if index >= len(lines) or not lines[index].startswith(EXECUTIVE_SUMMARY_PREFIX): + errors.append( + f"{path}: executive summary must be a blockquote immediately below the H1 title heading" + ) + return + + blockquote_lines: list[str] = [] + while index < len(lines) and lines[index].startswith(">"): + blockquote_lines.append(lines[index]) + index += 1 + + summary_text = " ".join(line.removeprefix(">").strip() for line in blockquote_lines) + if not summary_text.startswith(EXECUTIVE_SUMMARY_TEXT_PREFIX): + errors.append( + f"{path}: executive summary blockquote must start with " + f"`{EXECUTIVE_SUMMARY_TEXT_PREFIX}`" + ) + return + + summary_body = summary_text.removeprefix(EXECUTIVE_SUMMARY_TEXT_PREFIX).strip() + sentences = [ + sentence.strip() + for sentence in re.split(r"(?<=[.!?])\s+", summary_body) + if sentence.strip() + ] + if sentences and not sentences[-1].endswith((".", "!", "?")): + errors.append(f"{path}: executive summary must end with sentence punctuation") + return + + if not (2 <= len(sentences) <= 4): + errors.append( + f"{path}: executive summary must contain 2-4 sentences; found {len(sentences)}" + ) + + +def validate_cross_field_rules( + path: Path, meta: dict[str, object], errors: list[str] +) -> None: status = str(meta["status"]) if status not in ALLOWED_STATUSES: errors.append(f"{path}: invalid status `{status}`") @@ -166,8 +251,6 @@ def extract_status_from_git(ref: str, relative_path: Path) -> str | None: return None try: - from sep_common import extract_front_matter, parse_front_matter - raw_front_matter, _ = extract_front_matter(result.stdout, relative_path) meta = parse_front_matter(raw_front_matter, relative_path) except ValueError: @@ -195,7 +278,41 @@ def resolve_base_ref() -> str | None: return None -def validate_status_transition(path: Path, meta: dict, base_ref: str | None, errors: list[str]) -> None: +def validate_guiding_questions_in_sep_0000( + path: Path, body: str, errors: list[str] +) -> None: + if path.name != "SEP-0000-process.md": + return + + if GUIDING_QUESTIONS_HEADING not in body: + errors.append( + f"{path}: missing canonical heading `{GUIDING_QUESTIONS_HEADING}`" + ) + + missing = [marker for marker in GUIDING_QUESTION_MARKERS if marker not in body] + if missing: + errors.append( + f"{path}: guiding questions section is incomplete; missing markers: " + + ", ".join(missing) + ) + + +def validate_guiding_questions_uniqueness( + path: Path, body: str, errors: list[str] +) -> None: + if path.name == "SEP-0000-process.md": + return + + if GUIDING_QUESTIONS_HEADING in body: + errors.append( + f"{path}: contains the canonical guiding-questions heading; " + f"only SEP-0000 may carry `{GUIDING_QUESTIONS_HEADING}` (link to it instead)" + ) + + +def validate_status_transition( + path: Path, meta: dict, base_ref: str | None, errors: list[str] +) -> None: if base_ref is None: return @@ -204,6 +321,14 @@ def validate_status_transition(path: Path, meta: dict, base_ref: str | None, err return current_status = meta["status"] + # PR 45 returns SEP-0001 to Draft; do not generalize this transition. + if ( + path.name == "SEP-0001-core-syntax.md" + and previous_status == "Accepted" + and current_status == "Draft" + ): + return + allowed = ALLOWED_TRANSITIONS.get(previous_status, set()) if current_status not in allowed: errors.append( @@ -220,8 +345,11 @@ def main() -> int: for document in documents: validate_filename_and_title(document.path, document.metadata, errors) + validate_executive_summary(document.path, document.body, errors) validate_sections(document.path, document.metadata, document.body, errors) validate_cross_field_rules(document.path, document.metadata, errors) + validate_guiding_questions_in_sep_0000(document.path, document.body, errors) + validate_guiding_questions_uniqueness(document.path, document.body, errors) validate_status_transition(document.path, document.metadata, base_ref, errors) sep_number = document.metadata.get("sep") @@ -235,10 +363,7 @@ def main() -> int: seen_numbers[sep_number] = document.path if errors: - print("SEP validation failed:\n", file=sys.stderr) - for error in errors: - print(f"- {error}", file=sys.stderr) - return 1 + return report_errors(errors, "SEP validation failed") print(f"Validated {len(documents)} SEP documents successfully.") return 0 diff --git a/seps-index.json b/seps-index.json index 9148269..0d12ca6 100644 --- a/seps-index.json +++ b/seps-index.json @@ -4,7 +4,7 @@ { "path": "seps/SEP-0000-process.md", "sep": 0, - "title": "SEP-0000: Spore Enhancement Proposal Process", + "title": "SEP-0000: Spore Evolution Proposal Process", "status": "Accepted", "type": "Process", "authors": [ @@ -20,7 +20,7 @@ "path": "seps/SEP-0001-core-syntax.md", "sep": 1, "title": "SEP-0001: Core Syntax & Signatures", - "status": "Accepted", + "status": "Draft", "type": "Standards Track", "authors": [ "Zhan Rongrui" @@ -69,7 +69,7 @@ { "path": "seps/SEP-0004-cost-analysis.md", "sep": 4, - "title": "SEP-0004: Cost Analysis & Decidability", + "title": "SEP-0004: Budget Constraints & Realization Shape", "status": "Draft", "type": "Standards Track", "authors": [ @@ -86,9 +86,9 @@ "superseded_by": null }, { - "path": "seps/SEP-0005-hole-system.md", + "path": "seps/SEP-0005-hole-report-protocol.md", "sep": 5, - "title": "SEP-0005: Hole System & Agent Protocol", + "title": "SEP-0005: Hole System & HoleReport Protocol", "status": "Draft", "type": "Standards Track", "authors": [ @@ -140,7 +140,9 @@ 1, 2, 3, - 4 + 4, + 5, + 6 ], "discussion": "https://github.com/spore-lang/spore-evolution/discussions/7", "pr": null, @@ -160,7 +162,9 @@ 1, 2, 3, - 4 + 4, + 5, + 6 ], "discussion": "https://github.com/spore-lang/spore-evolution/discussions/8", "pr": null, @@ -181,6 +185,7 @@ 2, 3, 4, + 6, 7, 8 ], @@ -188,6 +193,25 @@ "pr": null, "superseded_by": null }, + { + "path": "seps/SEP-0010-compiler-as-documentation.md", + "sep": 10, + "title": "SEP-0010: Compiler-as-Documentation & Explain Protocol", + "status": "Draft", + "type": "Standards Track", + "authors": [ + "Zhan Rongrui" + ], + "created": "2026-05-26", + "requires": [ + 1, + 5, + 6 + ], + "discussion": "https://github.com/spore-lang/spore-evolution/discussions/46", + "pr": null, + "superseded_by": null + }, { "path": "drafts/script-mode.md", "sep": null, diff --git a/seps/SEP-0000-process.md b/seps/SEP-0000-process.md index 8d46f0e..6e10c1c 100644 --- a/seps/SEP-0000-process.md +++ b/seps/SEP-0000-process.md @@ -1,6 +1,6 @@ --- sep: 0 -title: "SEP-0000: Spore Enhancement Proposal Process" +title: "SEP-0000: Spore Evolution Proposal Process" status: Accepted type: Process authors: @@ -12,27 +12,50 @@ pr: "https://github.com/spore-lang/spore-evolution/pull/42" superseded_by: null --- -# SEP-0000: Spore Enhancement Proposal Process +# SEP-0000: Spore Evolution Proposal Process -> **Executive Summary**: Defines the governance process for evolving Spore through structured proposals (SEPs). Establishes a Draft → Review → Accepted lifecycle, required sections by proposal type, machine-verifiable metadata (YAML front matter + JSON index), and dual review criteria for both human and agent experience impact. +> **Executive Summary**: Defines the governance process for evolving Spore through structured proposals (SEPs). It establishes the `Pitch -> Draft -> Accepted / Rejected` lifecycle, treats review as a Draft-phase activity, and defines machine-verifiable metadata, required sections by SEP type, and repository automation. It also makes SEP-0000 the canonical home for the guiding questions that operationalize [VISION.md](../VISION.md) for human and Agent review. ## Summary -This document defines the initial process for proposing, discussing, reviewing, accepting, and evolving substantial changes to Spore. +**SEP** stands for **Spore Evolution Proposal** (Spore 演进提案). -It is the process document for the `spore-evolution` repository and the starting point for all future SEP work. +> A SEP is a Spore Evolution Proposal: a design document that records and +> reviews language, tooling, ecosystem, and governance changes across Spore's +> evolution. + +The term _evolution_ is intentional. SEPs cover syntax refactoring, semantic +reordering, breaking changes, deprecations, standard-library conventions, +toolchain protocols, and governance updates—not only incremental feature +additions. _Enhancement_ may describe the character of a particular change in +proposal text, but it is not the E in SEP. + +This document defines the process for proposing, discussing, reviewing, +accepting, and evolving substantial changes to Spore. + +It is the process document for the `spore-evolution` repository and the +starting point for all future SEP work. It also owns the canonical +**guiding questions for every design decision**, which translate the principles +in [VISION.md](../VISION.md) into reviewable prompts. + +The responsibility split is deliberate: `VISION.md` states the design +philosophy, the implementation roadmap plans rollout work, and numbered SEPs +record reviewed decisions. Process mechanics, including the guiding questions, +live here rather than in the vision. ## Motivation -Spore is attempting to design a language and toolchain around a fairly ambitious set of ideas: +Spore designs a language and toolchain around an unusually cross-cutting set of +ideas: -- intent-first APIs +- intent-first signatures and properties - structured collaboration between humans and Agents - holes as first-class collaboration points -- explicit effect and cost models -- machine-readable diagnostics and language protocols +- explicit effect and budget surfaces +- evidence-backed, machine-readable diagnostics and protocols -These changes have broad, long-term consequences. Normal implementation pull requests are not enough to capture: +These changes have broad, long-term consequences. Ordinary implementation pull +requests are not sufficient to capture: - design rationale - alternatives considered @@ -40,7 +63,9 @@ These changes have broad, long-term consequences. Normal implementation pull req - the relationship between human UX and Agent UX - migration and compatibility implications -Spore therefore needs a durable, reviewable design archive. +Spore therefore needs a durable, reviewable design archive whose review +criteria stay tied to the vision rather than to whoever happens to comment +first. ## Goals @@ -50,7 +75,9 @@ The SEP process aims to: 2. separate implementation work from language and process design discussion 3. give substantial proposals a consistent path from idea to accepted design 4. require authors to explain both human-facing and Agent-facing consequences -5. make it easier to revisit, amend, or supersede past decisions +5. anchor every review to the vision through a shared, canonical set of + guiding questions +6. make it easier to revisit, amend, or supersede past decisions ## Non-goals @@ -59,18 +86,24 @@ This process does not aim to: 1. require formal proposals for every small change 2. block ordinary bug fixes, refactors, tests, docs, or optimizations 3. settle all governance questions up front -4. require that an accepted SEP be implemented immediately +4. require that an accepted SEP be rolled out immediately +5. duplicate vision content; [VISION.md](../VISION.md) owns the principles, + and SEP-0000 owns the operational questions derived from them ## Proposal -This SEP proposes that `spore-evolution` become the canonical repository for substantial design and process changes in Spore. +This SEP proposes that `spore-evolution` become the canonical repository for +substantial design and process changes in Spore. -The initial model is intentionally simple: +The model is intentionally simple: - pitches start in a public discussion venue - substantial proposals move into repository-backed draft documents - proposal quality is enforced through templates, metadata, and CI -- accepted decisions remain historically visible and amendable through later SEPs +- review is anchored to a shared set of guiding questions derived from + [VISION.md](../VISION.md) +- accepted decisions remain historically visible and amendable through later + SEPs ## What requires an SEP @@ -78,11 +111,12 @@ An SEP is expected for substantial changes to any of the following: - language syntax - language semantics or typing rules -- module, package, content-addressing, effect, or cost systems +- module, package, content-addressing, effect, or budget systems - standard library or core tooling surface - compiler output and machine-readable protocol design - hole protocol, diagnostics protocol, or other human/Agent interface contracts -- the SEP process itself +- the SEP process itself, including changes to the canonical guiding questions, + their templates, or the checks that enforce them Spore intentionally uses a **wide pitch threshold** but a **stricter SEP threshold**: @@ -95,7 +129,7 @@ The normal pull request workflow is usually enough for: - bug fixes that do not change intended language behavior - refactors that preserve external behavior -- internal implementation changes not visible to users +- internal code changes not visible to users - test-only changes - editorial documentation improvements - performance improvements that do not alter semantics @@ -122,7 +156,7 @@ Describes design explorations, architectural context, prior art, or guidance tha The default lifecycle is: -`Pitch -> Draft SEP -> Review -> Accepted / Rejected / Withdrawn` +`Pitch -> Draft SEP -> Accepted / Rejected` ### Pitch @@ -131,14 +165,14 @@ A proposal should generally begin as a pitch in a public, linkable venue. For Spore, the default split is: - **GitHub Discussions** for language design, syntax, semantics, architecture, and other exploratory design questions -- **GitHub Issues** for implementation tracking, rollout follow-up, and narrower execution-oriented questions +- **GitHub Issues** for rollout follow-up and narrower execution-oriented questions If a proposal starts in one place and grows beyond that venue's strengths, maintainers may redirect it. The important thing is to preserve a public, searchable trail. The pitch should focus on: - the problem being solved -- why current Spore design is insufficient +- why the existing Spore design is insufficient - rough direction and tradeoffs The goal of the pitch is not final wording. The goal is to determine whether the idea is worth turning into an SEP. @@ -156,7 +190,9 @@ A draft SEP should be: ### Review -When maintainers believe a draft is ready, it moves into review. +When maintainers believe a draft is ready for focused feedback, it enters +review while retaining `Draft` status. Review is not a separate SEP status; a +proposal under review remains `Draft` until accepted or rejected. For Spore, review is expected to use **both**: @@ -175,25 +211,20 @@ For Spore, a linked public discussion thread is required for **all** SEP types. Review should emphasize: - problem/solution fit -- conceptual coherence with Spore +- the [guiding questions for every design decision](#guiding-questions-for-every-design-decision), + which translate [VISION.md](../VISION.md) into reviewable prompts - migration and compatibility -- whether the proposal makes human intent clearer -- whether the proposal makes Agent-facing structure clearer and more stable Review is not a vote. The goal is informed judgment, not comment-counting. ### Accepted -An accepted SEP becomes the design record for that change. Acceptance means the proposal is directionally approved, but not necessarily implemented or scheduled. +An accepted SEP becomes the design record for that change. Acceptance means the proposal is directionally approved, but not necessarily scheduled for rollout. ### Rejected A rejected SEP is closed with rationale preserved in the historical record. -### Withdrawn - -The author or maintainers may withdraw a proposal that is no longer being pursued. - ### Superseded If a later SEP replaces a previous one, the older document should remain in the repository and be marked `Superseded`. @@ -207,29 +238,29 @@ Before an SEP is merged, Spore expects all of the following: 3. the proposal uses the correct template for its SEP type 4. required front matter is present and valid 5. required sections are present +6. the PR checklist confirms that the SEP-0000 guiding questions were considered -Maintainers may still add merge-summary comments when useful, but Spore does not currently treat such comments as an automated merge gate. +Maintainers may still add merge-summary comments when useful, but Spore does +not treat such comments as an automated merge gate. -## Proposal status vs implementation maturity +## Proposal Status -SEP status should describe the **decision state of the proposal**, not the rollout or maturity state of its implementation. +SEP status describes the **decision state of the proposal**. SEPs are design records for language, tooling, and process discussion. They are -not, by themselves, the current implementation truth, a compatibility guarantee, -or a public release contract. Public readers who need current release behavior -should start with the implementation repository, especially `spore/README.md` -and `spore/docs/DESIGN.md`. +not, by themselves, compatibility guarantees or public release contracts. In particular: +- `Draft` covers proposals under active writing or review - `Accepted` means the design direction is approved -- it does **not** mean the proposal is fully implemented -- it does **not** mean the implementation is production-ready +- it does **not** mean the proposal is shipped +- it does **not** mean the release surface is production-ready - it does **not** imply a binary jump from "experimental" to "stable" -Implementation maturity is typically gradual and should be tracked elsewhere, for example in: +Release readiness and rollout state should be tracked elsewhere, for example in: -- implementation issues +- rollout issues - release notes - feature flags - compiler/tooling status pages @@ -257,10 +288,14 @@ Each SEP should have a champion responsible for: - summarizing open questions and tradeoffs - helping build consensus +The first listed author is the champion unless the SEP explicitly names a +different champion in its text. + ### Maintainers -During the bootstrap phase, the `spore-lang` organization owner acts as sole maintainer and -exercises final decision authority for accepting, rejecting, or withdrawing SEPs. +During the bootstrap phase, the `spore-lang` organization owner acts as sole +maintainer and exercises final decision authority for accepting or rejecting +SEPs, and for marking older SEPs superseded when a replacement is accepted. As the project grows, this role may be distributed to a maintainer team. Any change to the decision model described in this section requires a Process SEP. @@ -268,8 +303,8 @@ Any change to the decision model described in this section requires a Process SE For now, `spore-lang` maintainers are responsible for: - deciding when a proposal is ready for review -- deciding whether it is accepted, rejected, or withdrawn -- assigning SEP numbers at merge time +- deciding whether it is accepted or rejected +- assigning SEP numbers when proposals become numbered SEPs - keeping status metadata up to date Future governance may refine or replace this arrangement through a Process SEP. @@ -280,8 +315,8 @@ Future governance may refine or replace this arrangement through a Process SEP. - `SEP-0000` is reserved for the process document - later SEPs use sequential four-digit numbers -- maintainers assign the final number at merge time -- draft proposals should use descriptive temporary filenames before acceptance +- maintainers assign the final number when a proposal becomes a numbered SEP +- unnumbered drafts should use descriptive temporary filenames before numbering ### File layout @@ -298,13 +333,16 @@ templates/informational.md ## Required sections by SEP type -All SEP types require an **Executive Summary** — a blockquote of 2–4 sentences placed immediately after the YAML front matter, before the first heading. The executive summary should concisely state the core contribution, key design decisions, and expected impact. +All SEP types require an **Executive Summary** — a blockquote of 2–4 +sentences placed immediately below the H1 title heading. The executive summary +should concisely state the core contribution, key design decisions, and expected +impact. ### Standards Track Standards Track SEPs should include: -1. Executive Summary (blockquote before first heading) +1. Executive Summary (blockquote immediately below the H1 title heading) 2. Summary 3. Motivation 4. Guide-level explanation @@ -323,7 +361,7 @@ Standards Track SEPs should include: Process SEPs should include: -1. Executive Summary (blockquote before first heading) +1. Executive Summary (blockquote immediately below the H1 title heading) 2. Summary 3. Motivation 4. Goals @@ -336,11 +374,16 @@ Process SEPs should include: 11. Migration or rollout impact 12. Unresolved questions +SEP-0000 itself also carries the canonical +[Guiding questions for every design decision](#guiding-questions-for-every-design-decision) +section. Other Process SEPs do **not** duplicate that heading; they link to it +and, if they alter the question set, amend SEP-0000 directly. + ### Informational Informational SEPs should include: -1. Executive Summary (blockquote before first heading) +1. Executive Summary (blockquote immediately below the H1 title heading) 2. Summary 3. Motivation 4. Discussion @@ -350,42 +393,83 @@ Informational SEPs should include: Not every proposal needs identical depth in every section, but omission of required sections should be exceptional and explicit. -## Spore-specific review expectations +## Guiding questions for every design decision + +These questions are the operational form of [VISION.md](../VISION.md). They +translate the vision principles into reviewable prompts and anchor SEP review +to those principles rather than to ad-hoc preference. + +SEP-0000 is the **only** canonical home of this section. Other SEPs and +authoring templates link to this heading instead of duplicating it. -Because Spore explicitly targets human/Agent collaboration, SEP review should ask these questions: +The semantic path that the questions protect is: -1. Does this proposal make user intent clearer or more obscure? -2. Does it reduce or increase ambiguity for Agents? -3. Can the key semantics be exported in a stable machine-readable form? -4. Does it improve or degrade diagnostics and repair workflows? -5. Does it preserve conceptual coherence with the rest of Spore? +```text +Signature -> Property -> Hole -> Realization -> Evidence +``` -If a proposal introduces new structure for humans but not for tools, or vice versa, that mismatch should be made explicit. +| # | Question | Vision principle it operationalizes | +| --- | ----------------------------------------------------------------------------- | ----------------------------------------------------------- | +| 1 | Does this make intent clearer? | Signatures are gravity centers; properties specify intent | +| 2 | Does this reduce ambiguity for Agents? | Holes are typed absence; realization implements the missing | +| 3 | Does it preserve the distinction between base signature and intent signature? | Signatures are gravity centers | +| 4 | Can the semantics be exported in a stable, machine-readable form? | Holes are typed absence; evidence records what was checked | +| 5 | Can a hole be realized without extra conversation? | Holes are typed absence | +| 6 | Can every check produce evidence? | Evidence records what was checked | +| 7 | Can evidence be tied to content hashes and invalidated precisely? | Evidence records what was checked | +| 8 | Does it improve diagnostics, repair, and review workflows? | Holes are typed absence; evidence records what was checked | +| 9 | Does it preserve the semantic path? | All principles in composition | + +Authors should answer these questions in the proposal sections where they are +useful, not as a rote checklist. If a question is irrelevant to a proposal, the +SEP should say why rather than silently dropping the concern. + +If a proposal introduces new structure for humans but not for tools, or vice +versa, that mismatch should be made explicit in the proposal. + +### How each SEP type applies the questions + +These questions apply to every SEP type, but the surface where they are +addressed differs: + +- **Standards Track** SEPs address them in the Human experience impact and + Agent experience impact sections where applicable. +- **Process** SEPs consider them when changing review criteria, automation, or + any check that might create or weaken evidence flow. A Process SEP that + alters the canonical question set must also amend this section directly. +- **Informational** SEPs consider them in their Implications for Spore section + when describing prior art or design direction that could influence later + normative proposals. ## Amendments and follow-up changes -Accepted SEPs should not be silently rewritten to mean something substantially different. +Accepted SEPs should not be rewritten to mean something substantially different. - small clarifications may be merged as ordinary edits -- significant semantic changes should go through a new SEP +- the bootstrap amendment to SEP-0000 in pull request 45 is the sole direct + substantive amendment exception to this document +- future significant semantic changes to accepted SEPs must go through a new SEP - superseding SEPs should link to the older document they replace ### Amending this document Non-semantic clarification edits to SEP-0000 itself may be merged directly by the -maintainer. Substantive changes that alter the decision model, the lifecycle, the -SEP type taxonomy, or the required section templates must go through a new Process +maintainer. The bootstrap amendment in pull request 45 is the only direct +substantive amendment exception to SEP-0000. + +Future substantive changes that alter the decision model, the lifecycle, the SEP +type taxonomy, or the required section templates must go through a new Process SEP that supersedes or amends this document. ## Drawbacks -This process adds overhead compared with a repository that only uses issues and pull requests. +This process adds coordination overhead compared with a repository that only uses issues and pull requests. In particular: - authors must learn proposal structure and metadata rules - maintainers must curate proposal history more actively -- the project may initially feel process-heavy relative to its current size +- the project may initially feel process-heavy relative to its size We accept this cost because Spore is making design decisions that are unusually cross-cutting and difficult to reconstruct after the fact. @@ -444,7 +528,7 @@ For Spore, the required front matter keys are: - `pr` - `superseded_by` -Drafts may use `sep: null` before a permanent number is assigned at merge time. +Unnumbered drafts in `drafts/` may use `sep: null` before a permanent number is assigned. Spore also maintains a committed machine-readable index at `seps-index.json`. @@ -452,7 +536,7 @@ That index is generated from proposal front matter and checked for drift in loca ## Repository automation -The initial repository automation stack is: +The repository automation stack is: - `prek` as the shared local and CI hook runner - `rumdl` for Markdown linting @@ -461,19 +545,27 @@ The initial repository automation stack is: - `typos` for spelling checks - `check-jsonschema` for front matter schema validation - `seps-index.json`, generated from front matter and checked for drift -- minimal repo-local Python checks for: - - front matter extraction - - required section presence +- repo-local Python checks for: + - front matter extraction and parsing + - executive-summary placement and sentence count + - required section presence by SEP type - filename/title consistency - unique SEP number validation - status transition validation + - SEP-0000 guiding-question completeness (heading and content markers) + - duplicate guiding-question sections outside SEP-0000 + - template references to the canonical guiding-question section + - VISION.md staying principle-level instead of carrying syntax or review + mechanics + - surface-terminology consistency across the repo and sibling docs - PR checklist validation -The static site stack is intentionally **deferred** for now. Site generation should be decided separately after the core proposal workflow and quality checks settle. +The static site stack is intentionally **deferred**. Site generation should be +decided separately after the core proposal workflow and quality checks settle. ## Unresolved questions -This initial process leaves several questions intentionally open: +This process leaves several questions intentionally open: 1. How strict should automatic status-transition checks become over time? 2. How should the future publishing stack integrate with SEP metadata and index generation? diff --git a/seps/SEP-0001-core-syntax.md b/seps/SEP-0001-core-syntax.md index bbb891a..6f41caa 100644 --- a/seps/SEP-0001-core-syntax.md +++ b/seps/SEP-0001-core-syntax.md @@ -1,7 +1,7 @@ --- sep: 1 title: "SEP-0001: Core Syntax & Signatures" -status: Accepted +status: Draft type: Standards Track authors: - Zhan Rongrui @@ -14,51 +14,31 @@ superseded_by: null # SEP-0001: Core Syntax & Signatures -> **Executive Summary**: Defines Spore's core syntax and function-signature layout. SEP-0001 fixes the shared surface forms that later SEPs interpret: type syntax, error clauses, effect clauses, cost clauses, holes, concurrency forms, imports, and `spec` blocks. Detailed type, effect, cost, hole, compiler, concurrency, module, and standard-library semantics are delegated to their corresponding SEPs. +> **Executive Summary**: Defines Spore's root surface grammar and signature +> layout. A function declaration is a Base Signature plus optional Intent +> Signature clauses in fixed order: `uses`, `budget`, `properties`, and body. +> Later SEPs interpret type, effect, budget, hole, property, concurrency, +> module, and standard-library semantics. ## Summary -This proposal defines the core syntax and function-signature surface for the Spore programming language. Spore uses expression-oriented curly-brace syntax, Rust-style semicolon rules, algebraic data type syntax, pattern matching, square-bracket generics, and a regular function signature structure with optional `!`, `where`, `cost`, `uses`, and `spec` clauses. +SEP-0001 owns the **spelling** of every surface form. Semantics belong to +later SEPs. The core model is: -This SEP specifies the **surface grammar** and cross-SEP signature layout. SEP-0002 specifies type meaning and checking, SEP-0003 specifies effect semantics, SEP-0004 specifies cost verification, SEP-0005 specifies holes, SEP-0006 specifies compiler behavior and diagnostics, SEP-0007 specifies concurrency semantics, SEP-0008 specifies module/package resolution, and SEP-0009 specifies the standard-library surface. SEP-0001 remains dependency-free so those later SEPs can depend on a stable syntactic root without creating cycles. - -## Normative scope and dependency boundaries - -SEP-0001 is the root syntax SEP. Later SEPs may assign semantics to forms defined here, but they must not re-specify incompatible grammar. - -| Surface area | SEP-0001 owns | Semantic owner | -| -------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | -| Function declarations | clause order, delimiters, optional body shape | SEP-0002 for type checking, SEP-0004 for cost, SEP-0003 for effects | -| Type syntax | identifiers, generic brackets, tuple/function/refinement syntax forms | SEP-0002 | -| Error clauses and `?` / `throw` syntax | spelling and placement | SEP-0002 for error-set typing and propagation | -| `effect`, `handler`, `perform`, `handle` | declaration and expression grammar | SEP-0003 | -| `cost [...]` | clause placement and expression syntax envelope | SEP-0004 | -| `spec { ... }` | clause placement and item grammar | SEP-0006 for test execution, diagnostics, and spec hashing | -| Holes (`?name`) | expression grammar | SEP-0005 | -| `parallel_scope`, `spawn`, `select`, `await` | expression grammar | SEP-0007 | -| `import`, `alias`, visibility | declaration grammar | SEP-0008 | -| Prelude and library names | examples only | SEP-0009 | - -Unless explicitly marked as grammar, examples that mention type names, effect names, cost values, module paths, or standard-library items are illustrative. +```text +Spore = Signature -> Property -> Hole -> Realization -> Evidence +``` ## Motivation -Spore's syntax is designed to balance human readability with predictable machine processing. Curly-brace blocks, explicit delimiters, a fixed operator set, and ordered function clauses make source text regular enough for compilers, formatters, LSPs, and agents to process without relying on implicit layout or custom operator rules. - -Spore aims to occupy a practical syntactic design point: - -1. **Human-readable syntax**: expression-oriented syntax with familiar braces, semicolons, `let`, `fn`, `match`, and postfix type annotations. -2. **Machine-readable structure**: a fixed operator set, explicit delimiters, and a predictable function-clause order. -3. **Progressive signatures**: simple functions remain short, while functions that need additional metadata can add clauses in one canonical order. -4. **One iteration style**: loop keywords are absent from the surface language; recursive and library-based iteration are the accepted syntactic idioms. +Spore signatures are the shared boundary between humans, compilers, checkers, +and Agents. The grammar must be regular enough for tools and readable for +programmers. SEP-0001 makes that boundary explicit and stable so every later +SEP can extend semantics without revisiting surface spelling. ## Guide-level explanation -This section introduces the surface forms defined by this SEP. - -### Your first Spore function - -A minimal Spore function has a name, typed parameters, a return type, and a block body: +### Minimal function ```spore fn add(a: I64, b: I64) -> I64 { @@ -66,1511 +46,411 @@ fn add(a: I64, b: I64) -> I64 { } ``` -The last expression in a block is the block value. - -### Variables and immutability - -All bindings are immutable by default: - -```spore -let name = "Alice"; -let age = 30; -let greeting = f"Hello, {name}! You are {age} years old."; -``` - -Shadowing reuses a binding name in a later binding: - -```spore -let x = 10; -let x = x + 5; // shadows the previous x -``` - -For local mutable state, `Ref[T]` is written as a normal generic type: - -```spore -let counter = Ref.new(0); -counter.set(counter.get() + 1); -counter.update(|x| x + 1); // atomic read-modify-write -``` - -### Semicolons: statements vs. expressions - -Spore uses Rust-style semicolon semantics: +### Base Signature -- **With semicolon** → statement, value is discarded -- **Without semicolon** → expression, value is returned +Contains name, type parameters with inline bounds, value parameters, and +result type. Outcomes are expressed inside `TypeExpr` itself: ```spore -let result = { - let a = 10; // statement (semicolon) - let b = 20; // statement (semicolon) - a + b // expression (no semicolon) — this is the block's value -}; -// result == 30 -``` - -### Conditionals are expressions - -`if`/`else` returns a value: +enum LoadError { + Io(IoError), + Parse(ParseError), +} -```spore -let abs_value = if x >= 0 { x } else { -x }; - -let category = if age < 13 { - "child" -} else if age < 20 { - "teenager" -} else { - "adult" -}; +fn id[T](x: T) -> T +fn contains[T: Eq](xs: List[T], value: T) -> Bool +fn load(path: Path) -> Config ! LoadError ``` -### Pattern matching +### Intent Signature -`match` expressions use comma-terminated arms: +Clauses appear after the Base Signature in fixed order — `uses`, `budget`, +`properties` — then the body: ```spore -type Shape = - Circle(radius: F64) - | Rectangle(width: F64, height: F64) - | Triangle(base: F64, height: F64); - -fn area(shape: Shape) -> F64 { - match shape { - Circle(r) => 3.14159 * r * r, - Rectangle(w, h) => w * h, - Triangle(b, h) => 0.5 * b * h, - } +fn fetch(url: Url) -> Page ! NetworkError +uses [Http] +budget { branches: 4, nesting: 2 } +properties { + valid(url: Url): is_valid_page(fetch(url)) +} +{ + ?fetch_body } ``` -Guards and or-patterns are supported: - -```spore -let label = match score { - n if n >= 90 => "excellent", - n if n >= 70 => "good", - n if n >= 50 => "pass", - _ => "fail", -}; - -let is_weekend = match day { - "Saturday" | "Sunday" => true, - _ => false, -}; -``` - -### No loop syntax - -Spore has **no** `for`, `while`, `loop`, `break`, or `continue` syntax. Iteration is expressed with recursion or library functions. +Clause semantics are delegated: `uses` → SEP-0003, `budget` → SEP-0004, and +`properties` → SEP-0002 for expression typing plus SEP-0006 for claim and +evidence lowering. -**Local function declarations.** The block grammar includes a `Statement` form -labeled _local function_ below: nesting `fn` introduces a sibling function scoped -inside that enclosing block. Unlike lambdas (`|params|`), a statement-form `fn` -**does not** close over outer `let` bindings or the enclosing function's -parameters; pass any needed outer state explicitly as arguments to helpers. -Lexical closures and inference rules for lambdas belong to SEP-0002 (**§3.5.1** contrasts nested statement `fn` vs lambdas). +### Outcome types and propagation -**Cost and stack space.** How the compiler recognises linear recursion and -computes inferred or declared `cost […]` bounds is SEP-0004. **Tail calls.** -Calls in syntactic tail position (as in the accumulator helper below) are part of the -canonical evaluation model: SEP-0006 assigns them a merging lowering rule so deeply -tail-recursive iteration does **not** build a proportional chain of suspended -caller frames. That restores the predictable stack footprint programmers associate -with loops, even though the surface language has no loop forms. +`A ! E` is a first-class outcome type. It does not mean a list of error names; +it means a value that either succeeds with `A` or fails with `E`. Multiple +failure forms are modeled by ordinary `enum` types: ```spore -// Sum a list using fold -fn sum(numbers: List[I64]) -> I64 { - numbers.fold(0, |acc, x| acc + x) +enum LoadError { + File(FileReadError), + Parse(ParseError), } -// Factorial: inner helper carries the accumulator; parameters `n` vs `k` are -// distinct (no accidental shadow of the caller's iteration variable). -fn factorial(n: I64) -> I64 { - fn go(k: I64, acc: I64) -> I64 { - if k <= 1 { acc } else { go(k - 1, k * acc) } - } - go(n, 1) +fn load(path: Path) -> Config ! LoadError { + let text = read_text(path)?; + parse_config(text) } - -// Filter and transform using library functions -let result = numbers - |> filter(|x| x > 0) - |> map(|x| x * 2) - |> fold(0, |acc, x| acc + x); ``` -### Pipe operator - -The pipe operator `|>` passes the left-hand value as the first argument to the right-hand function: +`fail` constructs a failure, postfix `?` propagates it to the enclosing outcome +boundary, and outcome matches use `ok` / `fail` patterns: ```spore -// These are equivalent: -let result = format(process(validate(parse(data)))); - -let result = data - |> parse - |> validate - |> process - |> format; - -// With additional arguments: -x |> f(y, z) // equivalent to: f(x, y, z) -x |> f(_, y) // equivalent to: f(x, y) -f(_, y) // equivalent to: |x| f(x, y) +match load(path) { + ok config => config, + fail err => recover(err), +} ``` -The `_` placeholder form is a general call-position partial application form: -any call argument written as `_` is converted into a lambda parameter. Pipe use -is only the most common spelling because `x |> f(_, y)` reads as "send `x` into -the placeholder". - -### Ranges and slices +### Data types -Ranges create sequences; slices extract sub-lists: +`struct` defines product types; `enum` defines sum types: ```spore -// Half-open range [start, end) -let range1 = 1..10; // 1, 2, 3, ..., 9 - -// Closed range [start, end] -let range2 = 1..=10; // 1, 2, 3, ..., 10 - -// Summing 1 to 100 using a range with fold -let sum = (1..=100).fold(0, |acc, x| acc + x); - -// List slicing -let sublist = list[2..5]; // elements at index 2, 3, 4 -let all_from_3 = list[3..]; // index 3 to end -let first_5 = list[..5]; // first 5 elements -``` - -### Method chaining and pipe equivalence +struct Point { x: I64, y: I64 } -Method chains and pipes are interchangeable styles: - -```spore -// Method chain style (fluent API) -let result = text - .trim() - .to_lowercase() - .split(" ") - .filter(|word| word.len() > 3) - .map(|word| capitalize(word)) - .collect(); - -// Equivalent pipe style -let result = text - |> trim - |> to_lowercase - |> split(" ") - |> filter(|word| word.len() > 3) - |> map(|word| capitalize(word)) - |> collect; +enum Shape { + Circle(I64), + Rect(I64, I64), +} ``` -### Error handling +### Type declarations -Errors are declared in the function signature with `!` and propagated with `?`: +`type = Expr` defines transparent aliases and refinement aliases. A bodyless +`type Name;` declaration is reserved for externally-provided opaque types and +must be marked `@foreign`. Refinement semantics are owned by SEP-0002: ```spore -type FileError = - NotFound(path: Str) - | PermissionDenied(path: Str) - | IoError(message: Str); - -fn read_config(path: Str) -> Config ! FileError | ParseError { - let content = read_file(path)?; // propagates FileError - let config = parse_toml(content)?; // propagates ParseError - config -} +type Meters = I64 +type NonEmptyStr = Str when self.len() > 0 ``` -Error-set typing, propagation, conversion, and recovery patterns are owned by -SEP-0002. SEP-0001 only fixes the spelling and placement of `!`, `?`, and -`throw`. - -### Generic types with square brackets +### Methods and receivers -Spore uses square brackets for generics, not angle brackets: +Inside `trait` and `impl`, the first parameter may be `self` — shorthand for +`self: Self`. Borrowed receiver forms (`&self`) are not part of SEP-0001: ```spore -type Option[T] = - Some(T) - | None; - -type Result[T, E] = - Ok(T) - | Err(E); - -struct Pair[A, B] { - first: A, - second: B, +trait Display { + fn show(self) -> Str; } -fn identity[T](x: T) -> T { - x +impl Display for Point { + fn show(self) -> Str { ?show_body } } ``` -### Function signatures: from simple to complex +### Foreign declarations -The canonical signature order is: - -```text -fn name[T](params) -> ReturnType ! ErrorSet -where T: Bound -cost [compute, alloc, io, parallel] -uses [Effect1, Effect2] -spec { - -} -{ - body -} -``` - -Only the clauses that are needed are written: +`@foreign` marks declarations whose implementation or representation is +provided externally: ```spore -fn add(a: I64, b: I64) -> I64 { - a + b -} - -fn parse_int(input: Str) -> I64 ! InvalidFormat { - ?parse_int_body -} - -fn serialize[T](value: T) -> Bytes ! SerializeError -where T: Serialize -cost [500, 50, 0, 1] -{ - ?serialize_body -} +@foreign +fn fast_pow(base: F64, exp: F64) -> F64 +uses [Native]; -fn sync_user_data(user_id: UserId, source: DataSource) -> SyncReport ! NetworkTimeout | AuthExpired -cost [8500, 200, 800, 4] -uses [NetConnect, FileRead, Clock] -{ - ?sync_body -} +@foreign +type Map[K, V]; ``` -Detailed behavior of error sets, cost vectors, effect requirements, and `spec` -execution is delegated to SEP-0002, SEP-0004, SEP-0003, and SEP-0006. +External-linking and ABI semantics are owned by SEP-0008. -### Traits, effects, and the `uses` clause +### Effect surfaces -Spore separates type interfaces from effect tracking: - -This section is the **normative surface syntax** for these forms. SEP-0002 and SEP-0003 reference the same constructs semantically rather than re-specifying their grammar. +`effect` declares an atomic effect protocol. `surface` names a reusable effect +surface expression: ```spore -// trait: type interface -trait Display { - fn show(self) -> Str; -} - -trait Serialize { - fn serialize(self) -> Bytes ! SerializeError; -} - -// effect: external world operations effect Console { fn println(msg: Str) -> (); - fn read_line() -> Str ! IoError; } -// effect alias (uses | for union) -effect HttpClient = NetConnect | Clock; -effect CLI = Console | FileRead | FileWrite | Env | Spawn | Exit; -``` - -SEP-0001 fixes the grammar for `trait`, `effect`, `handler`, `perform`, -`handle`, and `uses [...]`. SEP-0002 owns trait semantics; SEP-0003 owns effect -algebra, handler behavior, inferred effect attributes, and alias expansion. At the -syntax level, a `uses` clause is a bracketed list of effect names or aliases: - -```spore -fn query_database(sql: Str) -> Data ! DbError | Timeout -uses [NetConnect] -{ - ?query_database_body +effect FileRead { + fn read(path: Path) -> Str ! FileReadError; } -fn run_cli(config_path: Str) -> () ! Error -uses [CLI] -{ - ?run_cli_body -} -``` +surface IO = [Console, FileRead] -### Behavioral specification (`spec` clause) - -A function signature may include a `spec` block after `where`, `cost`, and `uses`, immediately before the function body. SEP-0001 fixes only the placement and item syntax: - -```spore -fn name(params) -> Return -spec { - example "label": expr - law "label": |x: T| expr - law "refined": |x: I32 when self >= 0| expr -} +fn run(path: Path) -> () +uses [IO] { - body + ?run_body } ``` -`example` and `law` type checking, executable checking, diagnostics, and spec -hashing are specified in SEP-0006. Hole-report projection of `spec` metadata is -specified in SEP-0005. - -### Struct and type definitions +Surface semantics are owned by SEP-0003. -```spore -// Named-field struct -struct User { - id: I64, - name: Str, - email: Str, -} - -impl Display for User { - fn to_string(self) -> Str { - f"User({self.name}, {self.email})" - } -} - -impl Serialize for User { - fn serialize(self) -> Str ! SerializeError { - ?serialize_body - } -} +### Attributes -// Tuple struct -struct Color(I64, I64, I64); - -// Unit struct -struct NoData; - -// Sum type (algebraic data type) -type BinaryTree[T] = - Leaf(T) - | Node(left: BinaryTree[T], value: T, right: BinaryTree[T]) - | Empty; - -// Refinement type -type PositiveInt = I64 when self > 0; -type Percentage = F64 when self >= 0.0 && self <= 100.0; -type I32 = I64 when self >= -2147483648 && self <= 2147483647; -type U8 = I64 when self >= 0 && self <= 255; -``` - -Field punning allows omitting the value when the variable name matches the field name, both in construction and pattern matching: +Attributes attach metadata to any item: ```spore -let name = "Alice"; -let email = "alice@example.com"; - -// Full form -let user1 = User { name: name, email: email }; +@export("C") +pub fn score(raw: I64) -> F64 { raw.to_f64() / 100.0 } -// Punned form (equivalent) -let user2 = User { name, email }; - -// Punning in pattern matching -match user { - User { name, email, .. } => f"Name: {name}, Email: {email}", -} +@foreign("ssl", name = "SSL_new") +fn ssl_new() -> Ptr[SSL] +uses [Native]; ``` -### Strings - -```spore -// Regular string -"Hello, World!" - -// Raw string (no escape processing) -r"C:\Users\path\to\file" - -// Format string (f-string) -f"Hello {name}, you are {age} years old" -f"Result: {2 + 2}" - -// Template string (t-string) — returns a template object -let tmpl = t"Dear {customer}, your order {id} is ready."; -let msg = tmpl.bind([("customer", "Bob"), ("id", "12345")]); - -// Multi-line string (newlines are literal) -let query = "SELECT * -FROM users -WHERE active = true"; - -// Multi-line with f-string -let html = f"
-

{title}

-

{body}

-
"; -``` +Attribute grammar is defined here; which attributes are valid and their +semantics are owned by SEP-0008. -Format strings (`f"..."`) and template strings (`t"..."`) both accept embedded -Spore expressions. Template-library behavior, escaping policy beyond the shared -string literal rules, and sanitization belong to SEP-0009. +### Holes -### Lambdas +A hole is a typed expression placeholder: ```spore -let double = |x| x * 2; -let add = |a, b| a + b; -let complex = |x, y| { - let sum = x + y; - let product = x * y; - sum * product -}; -``` - -### Concurrency - -SEP-0001 fixes only the concurrency expression forms. Task lifetime, channel -behavior, scheduling, cancellation, and timeout semantics are owned by SEP-0007. - -```spore -parallel_scope { - let a = spawn { compute_a() }; - let b = spawn { compute_b() }; - [a.await, b.await] -} +? +?name +?name: Type ``` -### Modules and imports - -```spore -// src/math.sp -pub fn add(a: I64, b: I64) -> I64 { a + b } -fn helper() -> I64 { 42 } // private by default - -import std.collections as collections; -import std.math as math; -``` +Hole semantics and HoleReport are owned by SEP-0005. ## Reference-level explanation -### Lexical structure - -#### Source text encoding - -Spore source files are UTF-8 encoded. Identifiers may contain Unicode letters, ASCII digits, and underscores. Identifiers must begin with a letter or underscore. - -**Character literals.** Single-quoted character literals are not part of the grammar. Length-1 text is written as a `Str` literal, for example `"a"` or `"世"`. Character-oriented standard-library helpers are specified by SEP-0009. - -#### Keywords - -The complete reserved keyword table: - -| Keyword | Purpose | -| --------------------------- | -------------------------------------------------------------------------- | -| `fn` | Function definition | -| `let` | Immutable binding | -| `if` / `else` | Conditional expression | -| `match` | Pattern matching expression | -| `struct` | Struct type definition | -| `type` | Type alias / sum type definition | -| `trait` | Type interface definition (trait) | -| `effect` | Effect definition / effect alias | -| `handler` | Effect handler implementation | -| `perform` | Invoke an effect operation | -| `impl` | Trait implementation block | -| `pub` / `pub(pkg)` | Visibility modifiers (public / package-visible) | -| `import` | Module import | -| `alias` | Type alias | -| `when` | Refinement predicate introducer | -| `where` | Generic type constraints | -| `uses` | Effect requirement declaration | -| `cost` | Four-slot cost vector clause | -| `spec` | Behavioral specification block in function declarations and `impl` methods | -| `example` | Concrete labeled example item inside a `spec` block | -| `law` | Universally quantified assertion inside a `spec` block | -| `spawn` | Spawn concurrent task | -| `select` | Channel multiplex expression | -| `parallel_scope` | Structured concurrency scope | -| `const` | Compile-time constant definition | -| `return` | Early return from function | -| `throw` | Throw an error value | -| `handle` | Bind effect handler expression | -| `with` | Handler binding (with handle) | -| `implements` | Reserved (use `impl ... for ...`) | -| `as` | Rename in imports | -| `in` | Reserved | -| `mut` | Reserved | -| `static` | Reserved | -| `async` | Reserved | -| `await` | Reserved spelling for postfix `.await` | -| `move` | Reserved | -| `ref` | Reserved | -| `self` | Current instance in trait/handler methods (regular identifier) | -| `super` | Parent module reference | -| `crate` | Crate root reference | -| `enum` | Reserved (use `type` with variants) | -| `union` | Reserved | -| `unsafe` | Reserved | -| `extern` | Reserved | -| `macro` | Reserved | -| `on` | Inline effect arm introducer inside `handle … with { … }` | -| `from` | Channel receive binder in `select` arms (`value from rx`) | -| `use` | Named handler binding inside `handle … with { … }` | -| `timeout` | Timeout arm in `select` expressions | -| `true` / `false` | Boolean literals | -| `Some` / `None` | Option type constructors | -| `Ok` / `Err` | Result type constructors | -| `Result` / `Option` / `Ref` | Built-in parameterized types | - -**Attributes.** Function-level attributes use the `@name(args)` form — a decorator-style prefix placed before `fn` (and before any doc comment). Examples: `@allows(validate, sanitize)`, `@allow(missing_spec)`. The `@` sigil is not a standalone operator; the full attribute form is parsed as part of `FunctionHeader`. - -#### Operators - -| Category | Operators | Description | -| ----------------- | --------------------------- | --------------------------------------- | -| Arithmetic | `+` `-` `*` `/` `%` | Add, subtract, multiply, divide, modulo | -| Comparison | `==` `!=` `<` `>` `<=` `>=` | Equality and ordering | -| Logical | `&&` `\|\|` `!` | And, or, not | -| Bitwise | `&` `\|` `^` `~` `<<` `>>` | AND, OR, XOR, NOT, shifts | -| Pipe | `\|>` | Data-flow pipe | -| Error propagation | `?` | Propagate error from Result | -| Range | `..` `..=` | Half-open and closed range | -| Field access | `.` | Field / method access | -| Assignment | `=` | Binding assignment | -| Call | `()` | Function / method invocation | -| Index | `[]` | Index access and generic parameters | - -Spore has a **fixed operator set** — no custom operators are allowed. - -#### Operator precedence - -13 levels, from highest to lowest: - -| Level | Operators | Associativity | Description | -| ----- | --------------------------- | --------------- | ---------------------------------------- | -| 1 | `.` `()` `[]` | Left | Field access, call, index | -| 2 | `-` (unary) `!` `~` | Right (prefix) | Unary negation, logical not, bitwise not | -| 3 | `*` `/` `%` | Left | Multiplicative | -| 4 | `+` `-` | Left | Additive | -| 5 | `<<` `>>` | Left | Bit shift | -| 6 | `&` | Left | Bitwise AND | -| 7 | `^` | Left | Bitwise XOR | -| 8 | `\|` | Left | Bitwise OR | -| 9 | `==` `!=` `<` `>` `<=` `>=` | Non-associative | Comparison | -| 10 | `&&` | Left | Logical AND (short-circuit) | -| 11 | `\|\|` | Left | Logical OR (short-circuit) | -| 12 | `..` `..=` | Non-associative | Range | -| 13 | `\|>` | Left | Pipe | - -The error propagation operator `?` is a postfix operator that binds tighter than any binary operator — it applies immediately to the preceding expression. - -The assignment operator `=` appears only in `let` bindings and is not an expression-level operator. - -#### Literals - -**Integers:** +### Signature layout ```text -42 // decimal --123 // negative decimal -1_000_000 // underscore separators -0xFF // hexadecimal -0o755 // octal -0b1010_1100 // binary -42i32 // explicitly typed signed integer -255u8 // explicitly typed unsigned integer +fn []() -> +[uses ] +[budget { : , ... }] +[properties { (): , ... }] +( | ";" ) ``` -Unsuffixed integer literals default to **`I64`** unless a checking context fixes -another integer width. Suffixes are accepted for every fixed-width integer: -`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, and `u64`. Suffix overflow is a -type-checking error, not a separate grammar form. - -**Floats:** - -```text -3.14 --0.001 -1.0e-10 -2.5e+3 -1_000.5 -3.14f32 -``` - -Unsuffixed floats default to **`F64`** unless a checking context fixes another -float width. Suffixes are accepted for `f32` and `f64`. - -**Booleans:** `true`, `false` - -**Strings** (including “single character” text as length-1 `Str` values): - -```text -"Hello, World!" // regular string -r"C:\Users\path\to\file" // raw string (no escapes) -f"Hello {name}, age {age}" // format string -t"Dear {customer}, order {id}" // template string -``` - -#### Comments - -````spore -// Line comment - -/// Doc comment (supports Markdown) -/// # Example -/// ```spore -/// let x = add(1, 2); -/// ``` - -/* Block comment - /* can be nested */ - continues here -*/ -```` - -#### Identifiers and naming conventions - -| Convention | Used for | Examples | -| ---------------------- | ------------------------------------- | --------------------------------------------- | -| `snake_case` | Variables, functions, modules | `user_name`, `calculate_total`, `http_client` | -| `PascalCase` | Types, traits, effects, enum variants | `UserAccount`, `Serialize`, `Some` | -| `SCREAMING_SNAKE_CASE` | Constants | `MAX_BUFFER_SIZE`, `PI` | - -### EBNF Grammar - -Effect handling uses `perform `, top-level `handler as (...) { ... }`, and `handle { ... } with { ... }`. Handler-block entries are either `use HandlerName { ... }` or `on Effect.op(...) => ...`. +### EBNF ```ebnf -(* ═══════════════════════════════════════════════════ *) -(* Spore — Complete EBNF Grammar *) -(* ═══════════════════════════════════════════════════ *) - -(* ─── Top-level ───────────────────────────────────── *) - -Program = { TopLevelItem } ; - -TopLevelItem = ImportDecl - | AliasDecl +SourceFile = { ItemDecl } ; +ItemDecl = FunctionDecl | StructDecl + | EnumDecl | TypeDecl + | SurfaceDecl | TraitDecl | EffectDecl | HandlerDecl | ImplDecl | ConstDecl - | FunctionDecl ; - -(* ─── Imports & Aliases ───────────────────────────── *) - -ImportDecl = "import" ImportPath [ "as" Ident ] ";" ; -ImportPath = Ident { "." Ident } ; - -AliasDecl = [ Visibility ] "alias" Ident "=" QualifiedIdent ";" ; - -(* ─── Module ──────────────────────────────────────── *) -(* Module names are derived from file paths (see SEP-0008). - The `module` keyword is not part of the surface syntax. *) - -(* ─── Struct ──────────────────────────────────────── *) - -StructDecl = [ Visibility ] "struct" Ident [ TypeParams ] StructBody ; - -StructBody = "{" FieldList "}" (* named-field struct *) - | "(" TypeList ")" ";" (* tuple struct *) - | ";" ; (* unit struct *) - -FieldList = Field { "," Field } [ "," ] ; -Field = Ident ":" TypeExpr ; - -(* ─── Type (sum types / aliases) ──────────────────── *) - -TypeDecl = [ Visibility ] "type" Ident [ TypeParams ] - "=" TypeDefBody - [ WhereClause ] ";" ; - -TypeDefBody = VariantList (* sum type *) - | TypeExpr (* type alias *) - | TypeExpr "when" Expr ; (* refinement type; predicate may reference `self` *) - -VariantList = [ "|" ] Variant { "|" Variant } ; -Variant = Ident [ "(" FieldList ")" ] - | Ident [ "(" TypeList ")" ] ; - -(* ─── Trait ────────────────────────────────────────── *) - -TraitDecl = [ Visibility ] "trait" Ident [ TypeParams ] - [ ":" BoundExpr ] - "{" { TraitItem } "}" ; - -TraitItem = AssocType - | TraitFunctionSig ( ";" | Block ) ; - -AssocType = "type" Ident ";" ; - -(* ─── Effect ──────────────────────────────────────── *) - -EffectDecl = [ Visibility ] "effect" Ident [ TypeParams ] - ( EffectBlock | "=" EffectAlias ";" ) ; - -EffectBlock = "{" { EffectOpDecl } "}" ; -EffectOpDecl = FunctionHeader ";" ; + | ImportDecl ; -EffectAlias = Ident { "|" Ident } ; +Attribute = "@" Ident [ "(" AttrArgs ")" ] ; +AttrArgs = AttrArg { "," AttrArg } ; +AttrArg = Ident "=" AttrValue | AttrValue ; +AttrValue = Ident | StrLiteral | IntLiteral ; -(* ─── Handler ─────────────────────────────────────── *) - -HandlerDecl = "handler" Ident "as" Ident [ "(" ParamList ")" ] - "{" { FunctionDecl } "}" ; - -PerformExpr = "perform" Expr ; - -HandleExpr = "handle" Block "with" HandlerBlock ; -HandlerBlock = "{" [ HandlerEntry { "," HandlerEntry } [ "," ] ] "}" ; -HandlerEntry = InlineHandlerBinding | NamedHandlerBinding ; -NamedHandlerBinding - = "use" Ident "{" [ HandlerPayload { "," HandlerPayload } [ "," ] ] "}" ; -HandlerPayload = Ident ":" Expr ; -InlineHandlerBinding - = "on" Ident "." Ident "(" [ Ident { "," Ident } ] ")" "=>" Expr ; - -(* ─── Impl (Trait Implementation) ─────────────────── *) - -ImplDecl = "impl" Ident [ TypeParams ] "for" Ident [ TypeParams ] - [ WhereClause ] "{" { FunctionDecl } "}" ; - -(* ─── Const ───────────────────────────────────────── *) - -ConstDecl = [ Visibility ] "const" Ident ":" TypeExpr "=" Expr ";" ; - -(* ─── Function Declaration ────────────────────────── *) - -FunctionDecl = FunctionSig Block ; - -FunctionSig = FunctionHeader [ SpecClause ] ; - -TraitFunctionSig = FunctionHeader [ SpecClause ] ; - -(* ─── Attributes ─────────────────────────────────── *) - -Attribute = "@" Ident [ "(" [ AttrArgList ] ")" ] ; -AttrArgList = AttrArg { "," AttrArg } [ "," ] ; -AttrArg = Ident | StringLiteral | IntLiteral ; - -(* ─── Function Declaration ────────────────────────── *) - -FunctionHeader = { Attribute } - [ DocComment ] - [ Visibility ] "fn" Ident [ TypeParams ] - "(" [ ParamList ] ")" [ "->" TypeExpr ] - [ ErrorClause ] - [ WhereClause ] - [ CostClause ] - [ UsesClause ] ; - -DocComment = { "///" CommentText } ; - -Visibility = "pub" | "pub" "(" "pkg" ")" ; +FunctionDecl = FunctionSig ( Block | ";" ) ; +FunctionSig = FunctionHeader [ UsesClause ] [ BudgetBlock ] [ PropertiesBlock ] ; +FunctionHeader = { Attribute } [ DocComment ] [ Visibility ] + "fn" Ident [ TypeParams ] + "(" [ ParamList ] ")" "->" TypeExpr ; TypeParams = "[" TypeParam { "," TypeParam } "]" ; -TypeParam = Ident +TypeArgs = "[" TypeExpr { "," TypeExpr } [ "," ] "]" ; +TypeParam = Ident [ ":" BoundList ] | "const" Ident ":" TypeExpr ; +BoundList = Ident { "+" Ident } ; ParamList = Param { "," Param } [ "," ] ; -Param = Ident ":" TypeExpr ; - -ErrorClause = "!" TypeExpr { "|" TypeExpr } ; - -WhereClause = "where" WhereConstraint { "," WhereConstraint } ; -WhereConstraint = Ident ":" BoundExpr ; -BoundExpr = Ident { "+" Ident } ; - -UsesClause = "uses" "[" [ EffectList ] "]" ; -EffectList = Ident { "," Ident } ; - -CostClause = "cost" "[" CostExpr "," CostExpr "," CostExpr "," CostExpr "]" ; -CostExpr = IntLiteral - | Ident (* parameter reference *) - | CostExpr "*" CostExpr - | CostExpr "+" CostExpr - | FunctionCall ; (* e.g., log2(n) *) -FunctionCall = Ident "(" [ ArgList ] ")" ; - -(* ─── Spec Clause ─────────────────────────────────── *) - -SpecClause = "spec" "{" { SpecItem } "}" ; - -SpecItem = ExampleItem - | LawItem ; - -ExampleItem = "example" StringLiteral ":" Expr - | "example" StringLiteral Block ; - -LawItem = "law" StringLiteral ":" LawLambdaExpr ; - -LawLambdaExpr = "|" [ LawParamList ] "|" ( Expr | Block ) ; -LawParamList = LawParam { "," LawParam } [ "," ] ; -LawParam = Ident ":" TypeExpr [ "when" Expr ] ; - -(* ─── Type Expressions ────────────────────────────── *) - -TypeExpr = Ident [ "[" TypeList "]" ] (* named type, optionally generic *) - | "fn" "(" [ TypeList ] ")" "->" TypeExpr (* function type *) - | "(" TypeList ")" (* tuple type *) - | "Self" (* self type in traits/handlers *) - | "?" ; (* type hole *) - -TypeList = TypeExpr { "," TypeExpr } [ "," ] ; - -QualifiedIdent = Ident { "." Ident } ; - -(* ─── Expressions ─────────────────────────────────── *) - -Expr = LetExpr - | IfExpr - | MatchExpr - | LambdaExpr - | ReturnExpr - | ThrowExpr - | PerformExpr - | SpawnExpr - | SelectExpr - | ParallelScopeExpr - | HandleExpr - | PipeExpr ; - -PipeExpr = OrExpr { "|>" OrExpr } ; - -OrExpr = AndExpr { "||" AndExpr } ; -AndExpr = CompareExpr { "&&" CompareExpr } ; - -CompareExpr = BitwiseOrExpr [ CompareOp BitwiseOrExpr ] ; -CompareOp = "==" | "!=" | "<" | ">" | "<=" | ">=" ; - -BitwiseOrExpr = BitwiseXorExpr { "|" BitwiseXorExpr } ; -BitwiseXorExpr = BitwiseAndExpr { "^" BitwiseAndExpr } ; -BitwiseAndExpr = ShiftExpr { "&" ShiftExpr } ; - -ShiftExpr = AddExpr { ( "<<" | ">>" ) AddExpr } ; - -AddExpr = MulExpr { ( "+" | "-" ) MulExpr } ; -MulExpr = UnaryExpr { ( "*" | "/" | "%" ) UnaryExpr } ; - -UnaryExpr = ( "-" | "!" | "~" ) UnaryExpr - | PostfixExpr ; - -PostfixExpr = PrimaryExpr { PostfixOp } ; -PostfixOp = "?" | "." "await" | "." Ident | "." Ident "(" [ ArgList ] ")" - | "(" [ ArgList ] ")" | "[" Expr "]" - | "[" [ Expr ] ".." [ Expr ] "]" - | "[" [ Expr ] "..=" Expr "]" ; - -PrimaryExpr = Literal - | Ident - | QualifiedIdent - | "(" Expr ")" - | Block - | StructLiteral - | ListLiteral - | HoleExpr ; - -(* ─── Specific Expression Forms ───────────────────── *) - -LetExpr = "let" Pattern [ ":" TypeExpr ] "=" Expr ";" ; - -IfExpr = "if" Expr Block [ "else" ( IfExpr | Block ) ] ; - -MatchExpr = "match" Expr "{" { MatchArm } "}" ; -MatchArm = Pattern [ "if" Expr ] "=>" Expr "," ; - -LambdaExpr = "|" [ LambdaParams ] "|" ( Expr | Block ) ; -LambdaParams = LambdaParam { "," LambdaParam } ; -LambdaParam = Ident [ ":" TypeExpr ] ; - -ReturnExpr = "return" [ Expr ] ; -ThrowExpr = "throw" Expr ; - -SpawnExpr = "spawn" Block ; - -SelectExpr = "select" "{" { SelectArm } "}" ; -SelectArm = Ident "from" Expr "=>" Expr "," - | "timeout" "(" Expr ")" "=>" Expr "," ; - -ParallelScopeExpr = "parallel_scope" Block ; +Param = ReceiverParam | Ident ":" TypeExpr ; +ReceiverParam = "self" [ ":" TypeExpr ] ; + +TypeExpr = RefinementTypeExpr [ "!" PrimaryTypeExpr ] ; +RefinementTypeExpr = PrimaryTypeExpr [ "when" Expr ] ; +PrimaryTypeExpr = Ident [ TypeArgs ] + | "(" [ TypeExpr { "," TypeExpr } [ "," ] ] ")" [ "->" TypeExpr ] + | "{" [ FieldDecl { "," FieldDecl } [ "," ] ] "}" + | "?" [ Ident ] ; + +UsesClause = "uses" SurfaceExpr ; +SurfaceDecl = { Attribute } [ Visibility ] "surface" Ident [ TypeParams ] "=" SurfaceExpr ; +SurfaceExpr = Ident [ TypeArgs ] + | "[" [ SurfaceItem { "," SurfaceItem } [ "," ] ] "]" ; +SurfaceItem = Ident [ TypeArgs ] ; + +BudgetBlock = "budget" "{" { BudgetItem } "}" ; +BudgetItem = Ident ":" IntLiteral ; + +PropertiesBlock = "properties" "{" { PropertyItem } "}" ; +PropertyItem = Ident "(" [ PropertyParamList ] ")" ":" Expr ; +PropertyParamList = PropertyParam { "," PropertyParam } [ "," ] ; +PropertyParam = Ident ":" TypeExpr ; + +(* The expression after `:` is an ordinary Spore expression. SEP-0002 owns + property body typing, including the strict `Bool` result requirement. + SEP-0006 owns claim and evidence lowering. *) Block = "{" { Statement } [ Expr ] "}" ; -Statement = Expr ";" - | LetExpr - | FunctionDecl ; (* local function *) - -(* ─── Struct & List Literals ──────────────────────── *) - -StructLiteral = QualifiedIdent "{" FieldInitList "}" ; -FieldInitList = FieldInit { "," FieldInit } [ "," ] ; -FieldInit = Ident ":" Expr - | Ident ; (* field punning *) - -ListLiteral = "[" [ ExprList ] "]" ; -ExprList = Expr { "," Expr } [ "," ] ; - -ArgList = Expr { "," Expr } [ "," ] ; - -(* ─── Patterns ────────────────────────────────────── *) - -Pattern = OrPattern ; -OrPattern = SinglePattern { "|" SinglePattern } ; - -SinglePattern = LiteralPattern - | WildcardPattern - | BindingPattern - | ConstructorPattern - | StructPattern - | ListPattern - | "(" Pattern ")" ; - -LiteralPattern = IntLiteral | FloatLiteral | StringLiteral - | "true" | "false" ; -WildcardPattern = "_" ; -BindingPattern = Ident ; - -ConstructorPattern = QualifiedIdent "(" [ PatternList ] ")" ; -StructPattern = QualifiedIdent "{" FieldPatternList [ "," ".." ] "}" ; -FieldPatternList= FieldPattern { "," FieldPattern } ; -FieldPattern = Ident ":" Pattern - | Ident ; (* field punning *) - -ListPattern = "[" [ ListPatternElems ] "]" ; -ListPatternElems= Pattern { "," Pattern } [ "," ".." Ident ] ; - -PatternList = Pattern { "," Pattern } [ "," ] ; - -(* ─── Hole ────────────────────────────────────────── *) - -HoleExpr = "?" - | "?" Ident - | "?" Ident ":" TypeExpr ; - -(* ─── Literals ────────────────────────────────────── *) - -Literal = IntLiteral | FloatLiteral | BoolLiteral - | StringLiteral ; - -IntLiteral = ( DecimalInt | HexInt | OctalInt | BinaryInt ) [ IntSuffix ] ; -DecimalInt = [ "-" ] Digit { Digit | "_" } ; -HexInt = "0x" HexDigit { HexDigit | "_" } ; -OctalInt = "0o" OctDigit { OctDigit | "_" } ; -BinaryInt = "0b" BinDigit { BinDigit | "_" } ; -IntSuffix = "i8" | "i16" | "i32" | "i64" - | "u8" | "u16" | "u32" | "u64" ; - -FloatLiteral = [ "-" ] Digit { Digit | "_" } "." Digit { Digit | "_" } - [ ( "e" | "E" ) [ "+" | "-" ] Digit { Digit } ] - [ FloatSuffix ] ; -FloatSuffix = "f32" | "f64" ; - -BoolLiteral = "true" | "false" ; - -StringLiteral = '"' { StringChar } '"' - | 'r"' { RawChar } '"' - | 'f"' { FStringPart } '"' - | 't"' { TStringPart } '"' ; - -FStringPart = FStringText | "{" Expr "}" ; -TStringPart = FStringText | "{" Expr "}" ; - -EscapeSeq = "\\" ( "n" | "t" | "r" | "\\" | '"' | "0" - | "u{" HexDigit { HexDigit } "}" ) ; -StringChar = EscapeSeq | ? any Unicode scalar except `"` or `\` ? ; -FStringText = EscapeSeq | ? any Unicode scalar except `"`, `\`, `{`, or `}` ? ; -RawChar = ? any Unicode scalar except `"` ? ; -CommentText = ? any text until line end ? ; - -(* ─── Identifier ──────────────────────────────────── *) - -Ident = ( Letter | "_" ) { Letter | Digit | "_" } ; -Letter = "a".."z" | "A".."Z" | UnicodeLetter ; -UnicodeLetter = ? any Unicode scalar with the Unicode Letter property ? ; -Digit = "0".."9" ; -HexDigit = Digit | "a".."f" | "A".."F" ; -OctDigit = "0".."7" ; -BinDigit = "0" | "1" ; -``` - -### Function signature layout - -Function clauses appear in this canonical order: - -```text -fn []() -> [! ] -[where : , ...] -[cost [, , , ]] -[uses [, ...]] -[spec { }] -{ - -} -``` - -SEP-0001 owns this order and the delimiter forms. Clause meaning is delegated: -return and error typing to SEP-0002, cost checking to SEP-0004, effect checking -to SEP-0003, `spec` execution and diagnostics to SEP-0006, and any -hole-specific projection of signature metadata to SEP-0005. - -**Clause ordering** is the canonical source and formatter order: `where` → -`cost` → `uses` → `spec`. - -**`self` in trait and handler methods:** The `self` identifier is a regular parameter name used as the first parameter in trait method signatures. It is not a keyword with special scoping rules. - -```spore -trait Display { - fn to_string(self) -> Str; -} - -trait Collection { - type Item; - fn len(self) -> I64; - fn is_empty(self) -> Bool { - self.len() == 0 - } -} -``` - -### Expression forms - -This section summarizes syntax only. Expression typing, evaluation order, -exhaustiveness checking, closure capture, lowering, and error propagation -semantics are delegated to SEP-0002 and SEP-0006. - -**Blocks** use `{ ... }` with semicolon-terminated statements and an optional -tail expression. - -**If expressions** use `if Expr Block [else (IfExpr | Block)]`. - -**Match expressions** use `match Expr { Pattern [if Expr] => Expr, ... }`. - -**Lambda expressions** use `|params| expr` or `|params| { ... }`. - -**Pipe expressions** use `left |> right`. - -**Try expressions** use postfix `?`. - -### Pattern matching details - -Supported pattern forms: - -| Pattern | Syntax | Example | -| ----------------- | ----------------------- | -------------------------- | -| Literal | `42`, `"hello"`, `true` | `0 => "zero"` | -| Variable binding | `name` | `Some(x) => x` | -| Wildcard | `_` | `_ => "other"` | -| Constructor | `Ctor(p1, p2)` | `Some(value) => value` | -| Struct | `Name { f1, f2, .. }` | `Point { x, y } => x + y` | -| List (empty) | `[]` | `[] => "empty"` | -| List (head..tail) | `[h, ..t]` | `[head, ..tail] => head` | -| List (exact) | `[a, b]` | `[a, b] => a + b` | -| Or | `p1 \| p2` | `"Sat" \| "Sun" => true` | -| Guard | `p if cond` | `n if n > 0 => "positive"` | -| Nested | `Ok(Some(x))` | `Ok(Some(v)) => v` | - -### Type system - -This subsection is a syntax-facing overview. SEP-0002 is authoritative for primitive type meaning, inference, refinement checking, trait resolution, and type display. - -**Primitive type names:** `I8`, `I16`, `I32`, `I64`, `U8`, `U16`, `U32`, `U64`, `F32`, `F64`, `Bool`, `Str`, `Unit`, and `Never`. - -**Collection type syntax:** `List[T]`, `Vec[T, max: N]`, `Map[K, V]`, `Set[T]`, and `Array[T, N]`. - -**Special types:** `Option[T]`, `Result[T, E]`, `Ref[T]`, `Channel[T]`, `Unit` - -**Const generics:** `struct Array[T, const N: I64] { ... }` - -```spore -struct Array[T, const N: I64] { - data: List[T], -} - -struct Matrix[T, const ROWS: I64, const COLS: I64] { - data: Array[Array[T, COLS], ROWS], -} -``` - -Const-generic checking and collection invariants are specified by SEP-0002 and -the relevant standard-library SEP. - -**Refinement types:** `type PositiveInt = I64 when self > 0;` - -**Function types:** `fn(I64, I64) -> I64` - -### Concurrency primitives - -Concurrency forms: - -- `parallel_scope { ... }` -- `spawn { expr }` -- `select { ... }` -- `task.await` - -SEP-0007 owns the semantics of these forms. - -### Hole syntax - -Holes are expressions: - -```spore -? -?name -?name : Type -``` - -SEP-0001 owns these spellings only. Partial-function status, hole typing, -pipeline inference, `@allows`, JSON output, and any `spec` metadata surfaced to -hole tooling are owned by SEP-0005. - -## Complete examples - -Complete semantic examples are intentionally deferred to the SEPs that own the -corresponding behavior. +HoleExpr = "?" [ Ident ] [ ":" TypeExpr ] ; +FailExpr = "fail" Expr ; +TryExpr = Expr "?" ; +OutcomePattern = "ok" Pattern | "fail" Pattern ; + +StructDecl = { Attribute } [ Visibility ] "struct" Ident [ TypeParams ] + "{" [ FieldDecl { "," FieldDecl } [ "," ] ] "}" ; +FieldDecl = Ident ":" TypeExpr ; + +EnumDecl = { Attribute } [ Visibility ] "enum" Ident [ TypeParams ] + "{" [ VariantDecl { "," VariantDecl } [ "," ] ] "}" ; +VariantDecl = Ident [ "(" [ TypeExpr { "," TypeExpr } [ "," ] ] ")" ] ; + +TypeDecl = { Attribute } [ Visibility ] "type" Ident [ TypeParams ] + ( "=" TypeExpr | ";" ) ; + +TraitDecl = { Attribute } [ Visibility ] "trait" Ident [ TypeParams ] + "{" { MemberFunction } "}" ; +EffectDecl = { Attribute } [ Visibility ] "effect" Ident [ TypeParams ] + "{" { MemberFunction } "}" ; +HandlerDecl = { Attribute } [ Visibility ] "handler" Ident + [ "(" [ FieldDecl { "," FieldDecl } [ "," ] ] ")" ] + HandlesClause [ UsesClause ] + "{" { HandlerImplBlock } "}" ; +HandlesClause = "handles" SurfaceExpr ; +HandlerImplBlock = "impl" Ident [ TypeArgs ] + "{" { HandlerMethod } "}" ; +HandlerMethod = "fn" Ident [ TypeParams ] + "(" ReceiverParam [ "," ParamList ] ")" + "->" TypeExpr ( Block | ";" ) ; +QualifiedIdent = Ident "." Ident ; +ImplDecl = { Attribute } "impl" [ TypeParams ] TypeExpr [ "for" TypeExpr ] + "{" { FunctionDecl } "}" ; +MemberFunction = FunctionSig ( Block | ";" ) ; -```spore -type Expr = - Literal(I64) - | Add(left: Expr, right: Expr); - -fn eval(expr: Expr) -> I64 { - match expr { - Literal(n) => n, - Add(left, right) => eval(left) + eval(right), - } -} +ConstDecl = { Attribute } [ Visibility ] "const" Ident ":" TypeExpr "=" Expr ; +ImportDecl = "import" ModulePath [ "as" Ident ] ; +ModulePath = Ident { "." Ident } ; +Visibility = "pub" | "pub" "(" "pkg" ")" ; ``` -More complete examples for type checking, effects, cost, holes, concurrency, -module resolution, and standard-library APIs belong to SEP-0002 through SEP-0009. +**Grammar notes:** + +- A function marked `@foreign` must end with `;` (no body); ordinary `fn` may + end with `;` (bodyless signature) or a block. +- `ReceiverParam` is only valid as the first parameter inside `trait` or + `impl`. Bare `self` normalizes to `self: Self`. +- `type = Expr` defines aliases; `enum` covers all sum types. +- `type Name;` is syntactically valid only for externally-provided opaque types + and must carry `@foreign`. +- Unparenthesized outcome chaining such as `A ! E ! F` is rejected. Nested + outcomes must be written with parentheses. +- `surface` names a reusable effect-surface expression. It is not a sum type, + logical OR, or error union. +- Handler declarations use `handles` to name the discharged effect surface and + may use `uses` to declare effects required by handler method bodies. Handler + fields are immutable instance payload. Handler methods live inside + `impl Effect { ... }` blocks and write `self` as the first parameter. The + receiver is read-only; this SEP does not introduce `mut self` or field + assignment. +- A `handle` expression installs named handler instances and inline arms for a + lexical scope: `handle { body } with { use HandlerName { field: value }, on + Effect.operation(param) => arm_body }`. Named `use` entries instantiate a + handler payload. Inline `on` arms handle a single effect operation directly. + SEP-0003 owns the checking rules. + +### Delegated semantics + +| Surface | Semantic owner | +| ------------------------------------------- | -------------- | +| Type aliases, refinements, trait bounds | SEP-0002 | +| `enum` variant layout and pattern rules | SEP-0002 | +| Effects, `perform`, handlers | SEP-0003 | +| Budget fields and checking | SEP-0004 | +| Holes and HoleReport | SEP-0005 | +| Properties, claims, evidence | SEP-0006 | +| Concurrency forms (`Task`, `spawn`, etc.) | SEP-0007 | +| Modules, `@export`, `@foreign`, ABI linking | SEP-0008 | +| Standard library names | SEP-0009 | ## Human experience impact -### Readability - -Spore's syntax prioritizes scannability. The fixed operator set prevents -source-local operator definitions from changing parsing rules. The pipe operator -linearizes deeply nested function calls. The `uses` clause makes side effects -visible in the function signature. Exhaustiveness requirements for `match` are -specified by SEP-0002. - -### Learning curve - -- **Developers from Rust/TypeScript**: Curly braces, semicolons, `let`, `match`, `fn`, and explicit generic syntax are familiar. The main additional forms are `uses`, `cost`, and `!` clauses. -- **Developers from Python/JavaScript**: Static types and explicit error sets require adjustment. F-strings, lambdas, and pipe-style composition remain familiar entry points. -- **Developers from Haskell/OCaml**: Pattern matching, ADTs, and expression-oriented syntax are familiar. Curly braces replace significant whitespace. - -### Progressive disclosure - -Simple functions do not require optional clauses: -`fn add(a: I64, b: I64) -> I64 { a + b }`. Effects, error sets, `cost [...]`, -and `spec { ... }` are introduced only when the function signature requires -that metadata. +Simple functions stay compact. Richer intent is readable because `uses`, +`budget`, and `properties` always appear in the same order before the body. ## Agent experience impact -Code-generation agents are an explicit tooling audience for the syntax: - -### Parsing predictability - -- **Fixed operator set**: Agents never need to resolve custom operators or precedence ambiguities. -- **Regular grammar**: No significant whitespace, no optional semicolons, no implicit returns outside blocks. Every syntactic form is delimited by explicit tokens. -- **Keyword-introduced clauses**: `where`, `cost`, `uses`, `!` are unambiguous clause starters. - -### Structured metadata - -Function signatures are machine-readable contracts. SEP-0001 defines stable -positions and delimiters for: - -- **Input/output types** from the parameter list and return type -- **Failure modes** from the `! ErrorSet` -- **Side effect profile** from `uses [...]` -- **Performance budget** from the `cost [compute, alloc, io, parallel]` clause -- **Generic requirements** from `where` clauses - -How agents interpret those clauses is owned by the type, effect, cost, hole, and -compiler SEPs. - -### Code generation - -Agents can generate Spore code incrementally using holes (`?`). SEP-0005 owns the -hole-filling workflow and related annotations. - -### Snapshot hashing - -SEP-0001 defines the surface components that can participate in hashing. -Snapshot hashing, spec hashing, and approval flags are specified in SEP-0006 and -SEP-0008. +Agents can parse signatures without inferring hidden conventions. Each clause +has a fixed position and fixed spelling. ## Structured representation / protocol impact -### AST structure - -The Spore AST is designed as a regular tree with strongly typed nodes. Key node types: - ```text -Program -├── ImportDecl { path, alias? } -├── StructDecl { name, type_params[], fields[] } -├── ImplDecl { trait, type_params[], target_type, methods[] } -├── TypeDecl { name, type_params[], body: TypeAlias | SumType | Refinement } -├── TraitDecl { name, type_params[], items[] } -├── EffectDecl { name, type_params[], operations[] } -├── HandlerDecl { name, effect, methods[] } -├── ConstDecl { name, type, value } -└── FunctionDecl - ├── name - ├── type_params[] - ├── params[] - ├── return_type? - ├── error_set[] - ├── where_clauses[] - ├── uses[] - ├── cost? - ├── spec? - │ ├── examples[] - │ └── laws[] - └── body: Block - -Expr -├── Literal { kind, value } -├── Ident { name } -├── Block { stmts[], tail_expr? } -├── If { condition, then_block, else_block? } -├── Match { scrutinee, arms[] } -├── Lambda { params[], body } -├── BinOp { op, left, right } -├── UnaryOp { op, operand } -├── Call { callee, args[] } -├── FieldAccess { object, field } -├── Index { object, index } -├── Pipe { left, right } -├── Try { expr } (* ? operator *) -├── Spawn { body } -├── Select { arms[] } -├── ParallelScope { body } -├── Return { value? } -├── Throw { value } -├── Hole { name?, type? } -├── StructLiteral { name, fields[] } -├── ListLiteral { elements[] } -└── Range { start?, end?, inclusive } -``` +FunctionDecl +├── base_signature (name, type_params, params, result_type) +├── intent_signature (uses, budget_items, properties) +└── body -### LSP integration - -The regular, keyword-delimited syntax supports straightforward LSP provider implementations: - -- **Go to definition**: Every identifier resolves unambiguously through module paths and lexical scoping. -- **Hover information**: Function signatures can be displayed in full because clause order is fixed. -- **Completions**: Keyword-delimited positions such as `uses [...]` and `spec { ... }` are easy to identify. -- **Diagnostics**: Parser and formatter diagnostics can point to exact clause positions; semantic diagnostics are owned by dependent SEPs. -- **Signature help**: The ordered clause structure (`where` → `cost` → `uses` → `spec`) enables progressive parameter info display. - -### Serialization +OutcomeType +├── success_type +└── failure_type +``` -The AST serializes naturally to JSON, S-expressions, or any tree-structured format. The fixed set of node types ensures forward compatibility. +This structure feeds HoleReport, Claim, and EvidenceRecord generation. ## Diagnostics impact -SEP-0001 enables precise syntax diagnostics because blocks, clauses, and -operators have fixed delimiters and a fixed order. Diagnostic codes, recovery -strategy, semantic errors, `spec` failures, and JSON diagnostic shape are owned -by SEP-0006. - -## Drawbacks - -1. **No loops**: Developers with imperative backgrounds must express iteration - through recursion or library functions, including simple counting patterns. +Parser diagnostics point to the specific signature layer. Semantic diagnostics +are owned by dependent SEPs. -2. **Verbose error sets**: Functions that can fail in many ways produce long - `! E1 | E2 | E3 | ...` lists. This makes failures visible but can lengthen - signatures. +## Backward compatibility and migration -3. **`cost` clause expressiveness**: The cost expression envelope may be - insufficient for complex algorithms. SEP-0004 owns the verification model and - diagnostics for inaccurate bounds. +This is a breaking surface. Migration tools should: -4. **No custom operators**: Domains like linear algebra or DSLs sometimes benefit from custom operators. Spore sacrifices this for predictability. +1. Rewrite `type Name { ... }` to `enum Name { ... }`. +2. Rewrite legacy `-> A ! E1 | E2` forms into `-> A ! ErrorEnum` with an + explicit `enum` failure type. +3. Rewrite `effect IO = A | B` into `surface IO = [A, B]`. +4. Rewrite `foreign fn` and `foreign type` to `@foreign` attributes. +5. Mark external opaque declarations as `@foreign type Name;`. +6. Move generic bounds into type parameter lists. +7. Replace positional resource annotations with named `budget` fields. -5. **Square bracket overload**: `[]` is used for generics, list literals, index access, and the `uses`/error clauses. Context resolves ambiguity, but it adds parser complexity. +## Drawbacks -6. **Keyword count**: The reserved keyword set is large (40+), which constrains identifier names and increases the language surface area. +Inline bounds make large type parameter lists denser. The mitigation is to +prefer small generic surfaces and hide complex abstraction behind traits. -7. **No explicit effect-property clause**: Effect attributes inferred from - `uses` are not written at the declaration site. SEP-0003 owns how those - attributes are reported by tooling. +Named budget fields require users to learn accepted vocabulary. Fields are +self-describing and extend without positional migration. ## Alternatives considered -### Loop constructs - -We considered including a minimal loop construct, for example Rust's `loop` or a -`foreach` form, but rejected it because: - -- It would introduce a second iteration style into the core grammar. -- Recursion and library functions provide the single accepted iteration style. -- Higher-order library functions such as `map`, `fold`, and `filter` cover common iteration patterns. -- A single iteration style simplifies formatter, analyzer, and agent behavior. - -### Angle brackets for generics (`List`) - -Rejected because: - -- Angle brackets create parsing ambiguity with comparison operators (`a < b > c`). -- Square brackets align with Python type hints (`List[int]`). -- The choice matches existing precedent in modern typed languages. - -### `with` clause for explicit effect properties - -The original design included `with [pure, deterministic]` for explicit effect -attribute annotation. This was removed because: - -- Effect attributes are derivable from the `uses` set. -- Redundant annotations can diverge from inferred effect facts. -- The compiler can display inferred properties in IDE hover and `--explain` output. +**`type` for sum types**: rejected in favor of `enum` to keep keyword intent +unambiguous and align with established convention. -### Significant whitespace (Python/Haskell style) +**`alias` keyword**: rejected; `type = Expr` already covers aliases without +adding vocabulary. -Rejected because: +**Bare `type Name;` as immediately-valid opaque type**: rejected. The syntax is +accepted so editors and incremental parsers can represent unfinished code, but +compilers should warn unless the declaration is marked `@foreign` or completed +with a real definition. -- Whitespace sensitivity hinders machine generation and transformation. -- Copy-paste errors are harder to debug. -- Curly braces provide explicit, unambiguous scope boundaries. - -### `do` notation for monadic composition - -Rejected in favor of the `?` operator and `|>` pipes because: - -- `?` is simpler for the common case of error propagation. -- `|>` provides general-purpose composition without requiring monad understanding. -- Keeping the concept count low aids learnability. - -### Roc-style inline `implements` for trait implementation - -The original design used Roc-style inline `implements [...]` on struct declarations. This was replaced by Rust-style `impl Trait for Type { ... }` blocks because: - -- Separate `impl` blocks allow implementing third-party traits for existing types. -- Multiple implementations are visually distinct rather than packed into a single line. -- It aligns with the Rust mental model that most Spore users already know. -- The `implements` keyword is reserved for potential future use. - -### Exception-based error handling - -Rejected because: - -- Invisible control flow violates the principle of explicit effects. -- Error sets in signatures enable static analysis and agent reasoning. -- `?` keeps the common success path concise while preserving explicit error sets. +**Separate proof keyword**: rejected; users write `properties`, compiler lowers +them internally. ## Prior art -### Rust - -Heavy influence on: curly-brace syntax, semicolon semantics (expression vs. statement), `match` exhaustiveness, `?` error propagation, `where` clauses, `let` bindings, closure syntax (`|x| x`), pattern matching, enum/struct distinction. - -Departed from: Spore removes lifetime annotations, borrowing semantics, `mut` keyword, `loop`/`for`/`while`, and trait orphan rules. Spore adopts Rust-style `impl Trait for Type` blocks. - -### Roc - -Influence on: expression-based design, the trait/effect split design, no-loop philosophy, the idea of making side effects visible in signatures. - -### Gleam - -Influence on: square brackets for generics, use of `|>` pipe operator as a core language feature, and a small fixed-operator language surface. - -### Elm - -Influence on: the emphasis on helpful error messages, exhaustive pattern matching as a reliability tool, the no-runtime-exception philosophy, and progressive disclosure of type system features. - -### OCaml / F - -Influence on: algebraic data types with `|` variant syntax, pattern matching with guards, expression-based design, the pipe operator (`|>` originates from F#). - -### Haskell - -Influence on: type classes (→ traits), purity by default, the idea of tracking effects in types, `where` clause syntax for constraints. - -### TypeScript / Python - -Influence on: f-string syntax (`f"...{expr}..."`), postfix type annotations (`name: Type`), and square brackets for generics (Python). - -### Koka - -Influence on: algebraic effect system design, the idea that effects should be inferred rather than manually annotated, row-polymorphic effects informing the `uses` auto-inference system. - -## Backward compatibility and migration - -As this is the initial root syntax specification, there are no backward compatibility concerns. - -### Future compatibility considerations - -1. **Keyword reservation**: The large reserved keyword set is intentional — it preserves room for future features (e.g., `async`, `unsafe`, `macro`) without breaking existing code. - -2. **Signature evolution**: Future SEPs that modify signature clause syntax must - define migration tooling and specify their interaction with signature or - snapshot hashing. - -3. **Cost model changes**: The `cost` clause semantics may evolve. Future changes should define a migration story so that cost bounds written today remain meaningful. - -4. **New expression forms**: The grammar is designed to be extensible — new expression forms can be added as new `PrimaryExpr` alternatives without disturbing existing productions. - -5. **Standard library stability**: Type names like `Option`, `Result`, `List`, `Map` are part of the language surface. Renaming these would require an explicit breaking-change migration. +Rust influenced braces, semicolons, generic bound notation, and the +`struct`/`enum` pairing. Haskell influenced `foreign` as FFI vocabulary. +Kotlin, Swift, and Java influenced attribute-style metadata. Idris and Agda +influenced typed holes. OCaml influenced abstract type declarations. Roc +influenced explicit effect boundaries. ## Unresolved questions -SEP-0001 has no unresolved questions that block accepting the root syntax -surface. The following decisions are accepted here because they affect grammar -or signature layout: - -1. **`throw`** is a core expression form. SEP-0001 owns only its spelling and - placement; SEP-0002 owns its error-channel typing and lowering. - -2. **Placeholder partial application** is a general call-position form: - `f(_, y)` creates a lambda over the placeholder argument. Pipe use such as - `x |> f(_, y)` is a direct application of that same rule. - -3. **Interpolation boundaries** are fixed for this accepted surface: both `f"..."` and `t"..."` - embed Spore expressions. SEP-0009 owns template rendering and sanitization. - -4. **String patterns** are literal patterns only. Regex, prefix, suffix, or - other string-pattern forms require a future syntax SEP. - -5. **Behavioral `spec` blocks** are accepted on ordinary function declarations, - trait method signatures, and `impl` methods. Trait-method contract - inheritance or merging is not part of SEP-0001. - -6. **Negative examples** are not part of the accepted `spec` grammar. The only - accepted `spec` item keywords are `example` and `law`. - -### Delegated to dependent SEPs - -- **Type and refinement semantics**: SEP-0002 owns primitive type meaning, - type inference, refinement checking, method dispatch, and error-set - canonicalization. -- **Effect composition and taxonomy**: SEP-0003 owns effect-set union, - alias expansion, handler discharge, and future effect vocabulary changes. -- **Cost units and verification**: SEP-0004 owns cost dimensions, units, - recursion analysis, and verification algorithms. -- **Hole reporting and partial-function status**: SEP-0005 owns HoleReport - fields, dependency graphs, and agent workflow. -- **Compiler/runtime guarantees**: SEP-0006 owns formatter enforcement, - diagnostics, lowering, and runtime implementation details. -- **Concurrency behavior**: SEP-0007 owns `spawn`, `await`, `select`, task - lifetime, cancellation, and channel semantics. -- **Module resolution**: SEP-0008 owns circular dependencies, import resolution, - package manifests, and content-addressed hashing. -- **Standard-library names and behavior**: SEP-0009 owns prelude items, - collection APIs, string helpers, and platform-facing library modules. - -## Appendix A: Delegated semantics - -SEP-0001 intentionally does not define operational semantics. Evaluation order, -type preservation, effect soundness, hole runtime behavior, `spec` execution, -standard-library operations, and concurrency runtime rules are delegated to -SEP-0002 through SEP-0009. +1. Should long inline bounds allow line breaks after each type parameter? +2. How should the compiler present `unknown` property evidence when a checker cannot decide an otherwise well-typed `Bool` property body? +3. Should effect names be partitioned by namespace? diff --git a/seps/SEP-0002-type-system.md b/seps/SEP-0002-type-system.md index 0fe361a..d57f17f 100644 --- a/seps/SEP-0002-type-system.md +++ b/seps/SEP-0002-type-system.md @@ -15,2124 +15,309 @@ superseded_by: null # SEP-0002: Type System -> **Executive Summary**: Defines Spore's nominal-primary type system with bidirectional inference, separating traits (type interfaces) from effects (external operations). Features three-tier refinement types (L0 decidable constraints → L1 abstract-interpretation → L2 proof obligations), 13 compiler-known traits, generics with where-clause bounds, and enum/struct/trait/effect as the core type constructors. Includes formal typing judgments for the core language. +> **Executive Summary**: Defines Spore's type system under the signature model. Base signatures are fully annotated, generic bounds live inline in type parameter lists, and type checking produces the type evidence consumed by HoleReport and EvidenceRecord payloads. ## Summary -This SEP specifies the type system for the Spore programming language. Spore's type system is a **nominal-primary, bidirectionally-inferred** system that sits between Rust and Haskell on the expressiveness spectrum — more targeted than full dependent types, yet richer than a Hindley–Milner core alone. +Spore uses nominal-primary static typing with bidirectional inference inside +function bodies. Function signatures are explicit: parameters, result type, +outcome boundary, and type-parameter bounds are all written at the callable +boundary. -The concrete type representation is defined by the following type grammar: - -```text -Ty ::= I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64 | F32 | F64 - | Bool | Str | Unit | Never - | Tuple([Ty, …]) - | Refined(Ty, var, predicate) - | Named(name) - | App(name, [Ty]) - | Record([(name, Ty)]) - | Fn([Ty], Ty, EffectSet, ErrorSet) - | Var(u32) - | Hole(name) - | Error +```spore +fn identity[T](x: T) -> T { x } +fn member[T: Eq](xs: List[T], value: T) -> Bool { ?member_body } +fn insert[K: Eq + Hash, V](map: Map[K, V], key: K, value: V) -> Map[K, V] { ?insert_body } ``` -Where **`EffectSet`** is a set of effect-name identifiers (the surface syntax references those names as literal identifiers), and **`ErrorSet`** is a closed set of error types. Unsuffixed integer literals default to **`I64`** and float literals to **`F64`** unless a context forces another fixed width. `Int` and `Float` are not primitive names or informal aliases. There is no `Char` type or character literal; single Unicode scalars are represented as `Str` values. - -**Platform-specific integers (e.g. process exit status).** The core type system does **not** fix a single machine type for exit codes or other host ABI surfaces. Those contracts live in the **Platform package** metadata and startup/exit specifications (see SEP-0008); this SEP only requires that the Spore signature matches whatever that Platform declares. - -**Formal judgments (mixed style).** Core rules in §4 use concrete `I64` / `F64` where they describe default scalar behavior. Where a rule must range over **all** integer or float widths, this SEP uses **ι** ∈ {`I8`, …, `U64`} and **φ** ∈ {`F32`, `F64`} as metavariables, with the default literal case still **ι = `I64`**, **φ = `F64`** unless stated otherwise. - -Key design decisions: - -- **Signatures are gravity centers** — function signatures must be richly typed and fully explicit; bodies are inferred. -- **EffectSet** is encoded directly in function types, making effect tracking a first-class part of the type. -- **Bidirectional type inference** — top-down checking from signatures, bottom-up synthesis from expressions. -- **Two-pass module checking** — `register_item` (collect all signatures) then `check_fn` (verify bodies). -- **Generics via square-bracket application** — `List[I64]`, `Result[T, E]`. -- **Type unification** with `Var` binding and substitution maps. -- **Refinement types** — L0 decidable predicates are specified in §4.11; L1 abstract interpretation (flow-sensitive narrowing) is specified as a future extension without requiring an SMT solver. - ## Motivation -### Why a type system specification? +The type system gives the signature model its base notion of possibility. A realization +must inhabit the target type before property, budget, or evidence checks can +mean anything. -Spore is being co-developed by humans and AI Agents. Both audiences need precise, unambiguous rules for: +The design goals are: -1. **What programs are well-typed.** Agents generate code that must type-check. Vague rules produce vague code. -2. **What information flows through signatures.** Signatures are the primary communication channel between components, between humans and Agents, and between Agents. They must encode types, effects, the four-slot cost clause, and error sets. -3. **What the compiler guarantees.** Typed holes, structured diagnostics, and fill-suggestions all depend on a well-defined type lattice. -4. **Where inference ends and annotation begins.** Signatures explicit, bodies inferred — but the boundary must be razor-sharp. - -### Problems with the status quo - -Without a specification: - -- Contributors cannot reason about whether a type-checking change is correct. -- Agent-generated code has no formal target to satisfy. -- Diagnostics cannot explain _why_ a type error exists — only _that_ one exists. -- Future features (refinements, traits, const generics) have no foundation to build on. - -### Design philosophy - -| Principle | Implication | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| **Signatures are gravity centers** | Signatures must be richly typed and explicit; bodies are inferred | -| **Nominal-primary, structural escape** | Named types are nominal; anonymous records are structural; traits and effects are always nominal | -| **Traits ≠ Effects** | Separate abstractions — `trait` defines type interfaces, `effect` defines external operations; they are not unified | -| **Decidable checking** | No SMT solver; refinements limited to decidable predicates and abstract interpretation | -| **Agent-friendly** | Richer types help Agents more than they hurt humans; Agents absorb annotation complexity | -| **Predictable** | No implicit conversions, no type-level computation, no SFINAE-style surprises | +- make callable boundaries explicit; +- keep body inference local and predictable; +- distinguish traits from effects; +- expose typed holes as useful collaboration points; +- produce stable machine facts for downstream protocols. ## Guide-level explanation -This section introduces Spore's type system from a user's perspective, with code examples showing type annotations, inference, and generics. - -### 3.1 Primitive types - -Spore provides a small, fixed set of built-in **numeric widths**, text, and logical primitives: - -| Type | Description | Notes | -| ------------------------- | ------------------------------------------ | ----------------------------------------------------------------------- | -| `I8`, `I16`, `I32`, `I64` | Signed integers | Unsuffixed literals default to **`I64`** | -| `U8`, `U16`, `U32`, `U64` | Unsigned integers | | -| `F32`, `F64` | IEEE-754 binary floats | Unsuffixed literals default to **`F64`** | -| `Bool` | Boolean | `true`, `false` | -| `Str` | UTF-8 string | Use `"x"` even for a single Unicode scalar; there is **no** `Char` type | -| `Unit` | Zero-information type | `()` | -| `Never` | Bottom type — uninhabited, no values exist | (no literal) | - -```spore -let x: I64 = 42 // default integer literal type (I64) -let y: F64 = 3.14 // default float literal type (F64) -let b: Bool = true // boolean -let s: Str = "hello" // UTF-8 string -let grapheme: Str = "世" // still `Str`, not a separate character type -let u: () = () // unit (zero-information type) -``` - -Distinct numeric widths are **not** interchangeable without an explicit conversion story (still evolving). `Str` is unrelated to any numeric width. - -**Narrower domains via refinement.** Bounds and lightweight predicates attach to a fixed-width base type: - -```spore -alias NonNegativeI64 = I64 when self >= 0 -alias Port = I64 when self >= 1 && self <= 65535 -``` - -Platform code generation maps these types to machine representations; refinements are checked in the decidable fragment described later in this SEP. - -**No informal scalar aliases.** Examples and normative text must write fixed-width scalar names (`I64`, `F64`, `U8`, and so on). A project may define its own `alias Int = I64`, but that is ordinary user code and not part of the language prelude. - -**Index kind.** `Index` is a compile-time non-negative size kind, not a runtime -type. Index parameters such as `N: Index` may appear in indexed types and in -`cost [...]` clauses. Ordinary runtime values (`I64`, `Str`, `List[T]`, records, -and so on) do not become cost variables merely because they are in scope. - -```spore -type Count[N: Index] = I64 when self >= 0 -``` - -`Count[N]` is the bridge type for APIs that carry a runtime non-negative count -while also exposing the compile-time index `N` to cost analysis. The runtime -representation is still an integer; the verifier sees only the index parameter. - -### 3.2 Type annotations on functions - -Function signatures **must** be fully annotated. Parameters, return type, error set, effect set, and `cost [c, a, i, p]` clause are all declared: - -```spore -fn add(a: I64, b: I64) -> I64 { - a + b -} - -fn fetch_page(url: Str) -> Str ! NetworkError -uses [NetConnect] -cost [5000, 200, 5000, 0] -{ - http_get(url) -} -``` - -Local variables inside bodies do **not** need annotations — they are inferred: - -```spore -fn process(x: I64) -> I64 { - let doubled = x * 2 // inferred: I64 - let message = "result" // inferred: Str - doubled + 1 // inferred: I64, checked against return type -} -``` - -### 3.3 Struct and enum types - -**Structs** (`struct`) are nominal product types. **`type`** with variants defines sealed sums (ADTs). - -```spore -struct Point { - x: F64, - y: F64, -} - -let p = Point { x: 1.0, y: 2.0 } -let dist = sqrt(p.x * p.x + p.y * p.y) - -type Shape = - Circle { radius: F64 } - | Rectangle { width: F64, height: F64 } - | Triangle { a: F64, b: F64, c: F64 } - -fn area(shape: Shape) -> F64 { - match shape { - Circle { radius } => 3.14159 * radius * radius, - Rectangle { width, height } => width * height, - Triangle { a, b, c } => { - let sper = (a + b + c) / 2.0; - sqrt(sper * (sper - a) * (sper - b) * (sper - c)) - }, - } -} -``` - -### 3.4 Anonymous records (structural) - -Anonymous records are the structural escape hatch. They have no declared name and are compatible by field shape, not by declaration site. - -```spore -fn greet(person: { name: Str, age: I64 }) -> Str { - "Hello, " ++ person.name ++ " (age " ++ show(person.age) ++ ")" -} - -// Any value with matching fields satisfies this: -let alice = { name: "Alice", age: 30, role: "Engineer" } -greet(alice) // OK: alice has name: Str and age: I64 (extra fields ignored) -``` - -**Rules for anonymous records**: - -1. **Width subtyping**: A record with extra fields satisfies a type expecting fewer fields. -2. **Exact field matching**: Field names and types must match exactly (no implicit conversion). -3. **No trait implementation**: Anonymous records cannot implement traits or effects — nominal types are required for that. -4. **Nominal types do NOT satisfy anonymous record types**: `struct Stats { count: I64 }` is nominal and is NOT compatible with `{ count: I64 }`. - -```spore -// Named types do NOT satisfy anonymous record types: -struct Stats { count: I64, total: F64 } -let named = Stats { count: 10, total: 95.5 } -// summarize(data: named) // ERROR: Stats is nominal, not { count: I64, total: F64 } - -// Explicit conversion required: -summarize(data: { count: named.count, total: named.total }) // OK -``` - -### 3.5 Function types with effects - -Function types carry an **EffectSet** and an **ErrorSet** — the effects they require and the errors they may propagate: - -```spore -type Logger = Fn(msg: Str) -> () uses [FileWrite] - -fn with_logging(f: Fn(x: I64) -> I64, x: I64) -> I64 -uses [FileWrite] -{ - log("calling f") - let result = f(x) - log("result: " ++ show(result)) - result -} -``` - -### 3.5.1 Nested statement `fn` vs lambdas - -Statement-form nested `fn` declarations (SEP-0001 grammar: `Statement` includes `FunctionDecl` labeled _local function_) introduce ordinary nested functions scoped to the enclosing block. Their bodies **do not** close over enclosing `let` bindings or the enclosing function's parameters (type parameters inherited from enclosing generic headers remain in scope). All value state threaded into helpers must appear as explicit parameters. - -Lambda expressions (`|params|`) are block-level expressions that may capture bindings from the enclosing value environment (see the `[Lambda]` judgment below). - -### 3.6 Generics - -Generic functions declare type parameters in square brackets and constrain them in `where` clauses: - -```spore -fn identity[T](x: T) -> T { - x -} - -fn pair[A, B](left: A, right: B) -> (A, B) { - // implementation -} +### Primitive and composite types -fn sort[T](list: List[T]) -> List[T] -where T: Ord -{ - // implementation -} -``` +Primitive type names include `I8`, `I16`, `I32`, `I64`, `U8`, `U16`, `U32`, +`U64`, `F32`, `F64`, `Bool`, `Str`, `()`, and `Never`. -At call sites, type arguments are inferred from the actual argument types: +Composite types use square brackets: ```spore -let nums: List[I64] = [1, 2, 3] -let result = identity(42) // T inferred as I64 -let strings = nums.map(show) // A=I64, B=Str inferred +List[I64] +Option[Str] +Array[I64, N] +Vec[Order, max: N] ``` -### 3.7 Generic type application - -Named generic types use square-bracket application in type position: +Outcome types are first-class and use `!` inside the type surface: ```spore -struct Pair[A, B] { - first: A, - second: B, -} - -let p: Pair[I64, Str] = Pair { first: 1, second: "hello" } - -type Result[T, E] = - Ok { value: T } - | Err { error: E } +User ! ParseError +List[Config ! LoadError] ``` -### 3.8 Newtypes and type aliases - -**Newtypes** create zero-cost nominal wrappers. The new type is distinct from its underlying type: +### Structs and variants ```spore -type UserId = Str // nominal wrapper: UserId ≠ Str -type Email = Str // nominal: Email ≠ UserId ≠ Str -type Celsius = F64 // nominal: Celsius ≠ Fahrenheit -type Fahrenheit = F64 +struct Point { x: I64, y: I64 } -fn format_temp(temp: Celsius) -> Str { - show(temp) ++ "°C" +enum Shape { + Circle(I64), + Rect(I64, I64), } - -let c: Celsius = 100.0 -let f: Fahrenheit = 212.0 -format_temp(temp: c) // OK -format_temp(temp: f) // ERROR: expected Celsius, got Fahrenheit -format_temp(temp: 98.6) // ERROR: expected Celsius, got F64 -``` - -Newtypes may have refinements: - -```spore -type Port = I64 when 1 <= self <= 65535 -type NonEmptyStr = Str when self.len() > 0 -type Latitude = F64 when -90.0 <= self <= 90.0 -``` - -**Type aliases** are transparent — interchangeable with their definition: - -```spore -alias TextList = List[Str] -alias Callback[T] = Fn(event: T) -> Unit -alias Result[T] = T ! GenericError - -type SessionId = Str // newtype: SessionId ≠ Str (nominal) -alias Label = Str // alias: Label == Str (transparent) ``` -### 3.9 Trait system +Types are nominal. Two structs with identical fields but different names are +different types. -#### Trait definition +### Traits and inline bounds -Traits define named interfaces. Spore uses the `trait` keyword for type interfaces. Traits are nominal — a type satisfies a trait only through an explicit `impl` declaration. +Traits define type interfaces: ```spore -trait Eq { - fn eq(self, other: Self) -> Bool -} - -trait Ord: Eq { - fn compare(self, other: Self) -> Ordering -} - trait Display { - fn display(self) -> Str -} - -trait Serialize { - fn serialize(self) -> Bytes ! SerializeError - cost [100, 20, 0, 0] -} -``` - -Trait inheritance uses `:` — `Ord: Eq` means every type implementing `Ord` must also implement `Eq`. - -#### Trait implementation - -Spore uses Rust-style `impl Trait for Type` syntax: - -```spore -impl Eq for Point { - fn eq(self, other: Point) -> Bool { - self.x == other.x && self.y == other.y - } -} - -impl Ord for Point { - fn compare(self, other: Point) -> Ordering { - let dx = self.x.compare(other.x) - if dx != Equal { dx } else { self.y.compare(other.y) } - } -} - -impl Display for Shape { - fn display(self) -> Str { - match self { - Circle { radius } => "Circle(r=" ++ show(radius) ++ ")", - Rectangle { width, height } => "Rect(" ++ show(width) ++ "x" ++ show(height) ++ ")", - Triangle { a, b, c } => "Tri(" ++ show(a) ++ "," ++ show(b) ++ "," ++ show(c) ++ ")", - } - } -} -``` - -Default method implementations are supported: - -```spore -trait Collection { - type Item - fn len(self) -> I64 - fn is_empty(self) -> Bool { self.len() == 0 } // default method -} -``` - -Shared method names are resolved through trait and inherent impl lookup, not -through free-function overloading. A call such as `xs.map(f)` first uses the -receiver type of `xs`; if several imported traits still provide applicable -methods with the same name, the call must use trait-qualified syntax. Spore does -not use signature unions such as `List[T] | Str` to model these operations. - -#### Trait bounds - -Trait bounds constrain generic type parameters in `where` clauses: - -```spore -fn sort[T](list: List[T]) -> List[T] -where T: Ord -cost [500, 100, 0, 0] -{ - // implementation + fn show(self) -> Str; } -fn serialize_all[T](items: List[T]) -> List[Bytes] ! SerializeError -where T: Serialize + Display -{ - items.map(|item| item.serialize()) +fn to_string[T: Display](value: T) -> Str { + value.show() } ``` -Multiple bounds use `+`: +Inside trait and impl members, `self` is receiver shorthand for `self: Self`. +It may only appear as the first parameter; receiver ownership refinements are +outside this SEP. -```spore -fn dedup_and_display[T](items: List[T]) -> Str -where T: Eq + Hash + Display -{ - items.unique().map(|x| x.display()).join(", ") -} -``` - -#### Trait vs. Effect (separated) - -This is one of Spore's central design decisions. **Traits** and **effects** are separate constructs: - -- **`trait`** defines type interfaces. Used with `where T: Trait` bounds. -- **`effect`** defines external operations and named effect aliases. Used with `uses [Effect]` declarations. -- **`handler`** provides concrete implementations for effect operations; handler semantics are specified in SEP-0003. - -These two systems are never mixed: `where` is for traits, `uses` is for effects. - -Concrete surface syntax for `trait`, `effect`, `handler`, and `handle ... with` is centralized in SEP-0001 (§"Traits, effects, and the `uses` clause" and the EBNF grammar). This SEP only depends on the semantic split: +Multiple bounds use `+` inside the type parameter list: ```spore -trait Serialize { - fn serialize(self) -> Bytes ! SerializeError -} - -effect HttpClient = NetConnect | Clock; - -fn fetch_page(url: Url) -> Response ! NetworkError -uses [HttpClient] -{ - ... +fn index[K: Eq + Hash, V](map: Map[K, V], key: K) -> Option[V] { + ?index_body } ``` -`where` constrains trait obligations; `uses` constrains effect requirements. Effect algebra and handler installation are specified in SEP-0003. - -#### Associated types +### Refinement types -Traits may declare associated types — types determined by the implementing type: +Refinement types attach predicates to ordinary types: ```spore -trait Iterator { - type Item - fn next(self) -> Option[Self.Item] - cost [10, 80, 0, 0] -} - -impl Iterator for LineReader { - type Item = Str - fn next(self) -> Option[Str] { - self.read_line() - } -} - -trait Collection { - type Item - type Iter: Iterator where Iter.Item == Self.Item - - fn iter(self) -> Self.Iter - fn len(self) -> I64 - fn is_empty(self) -> Bool { self.len() == 0 } -} +type NonEmptyStr = Str when self.len() > 0 ``` -Associated types eliminate extra type parameters on trait users: - -```spore -// Clean — associated types: -fn sum_all[C](collection: C) -> I64 -where - C: Collection, - C.Item: Add[Output = I64] -{ - collection.iter().fold(0, |acc, item| acc + item) -} -``` +Refinement checking is staged: decidable checks happen during type checking, +flow-sensitive propagation happens through abstract interpretation, and harder +obligations become claims for the evidence layer. -#### Generic Associated Types (GATs) +### Outcome types -GATs allow associated types to have their own generic parameters, enabling patterns like lending iterators and self-referential collections: +`A ! E` is a first-class outcome type. `A` is the success type and `E` is the +failure type. Errors are ordinary values; multiple failure forms are modeled by +ordinary `enum` types rather than inline unions: ```spore -trait LendingIterator { - type Item[lifetime] - fn next[lifetime](self) -> Option[Self.Item[lifetime]] -} - -trait Container { - type Elem[T] - fn wrap[T](value: T) -> Self.Elem[T] - fn unwrap[T](wrapped: Self.Elem[T]) -> T -} - -impl Container for OptionContainer { - type Elem[T] = Option[T] - fn wrap[T](value: T) -> Option[T] { Some(value) } - fn unwrap[T](wrapped: Option[T]) -> T { - match wrapped { - Some(v) => v, - None => panic("unwrap of None"), - } - } +enum LoadError { + Io(IoError), + Parse(ParseError), } -``` - -GATs cover the most common use cases that HKTs would serve without introducing full higher-kinded polymorphism. - -#### Built-in traits and deriving - -Spore provides compiler-known traits for common operations: - -| Trait | Purpose | Derivable | -| -------------------------- | -------------------------- | --------- | -| `Eq` | Equality comparison | Yes | -| `Ord` | Ordering | Yes | -| `Clone` | Value duplication | Yes | -| `Display` | Human-readable formatting | No | -| `Debug` | Debug formatting | Yes | -| `Hash` | Hash computation | Yes | -| `Default` | Default value construction | Yes | -| `Serialize` | Serialization to bytes | Yes | -| `Deserialize` | Deserialization from bytes | Yes | -| `Add`, `Sub`, `Mul`, `Div` | Arithmetic operators | No | -Derivable traits can be auto-implemented by the compiler for types whose fields all implement the trait: - -```spore -struct Point { - x: F64, - y: F64, -} deriving [Eq, Clone, Debug, Hash] +fn parse(input: Str) -> Config ! ParseError +fn load(path: Path) -> Config ! LoadError ``` -#### Coherence and orphan rules - -You can only implement a trait for a type if you own either the trait or the type: - -```spore -// OK: you own Point, so you can implement any trait for it -impl Display for Point { ... } +`Result[T, E]` is not part of the core type surface. Source programs use `A ! E` +directly. -// OK: you own MyTrait, so you can implement it for any type -impl MyTrait for Str { ... } +`fail err` constructs a failure outcome. If `expr : A ! E`, then `expr?` +eliminates the outcome at the expression site and propagates failures to the +enclosing outcome boundary. Outcome matches use `ok` / `fail` patterns at the +surface level. -// ERROR: you own neither Display nor Str -impl Display for Str { ... } // orphan rule violation -``` +### Holes -**Adapter escape mechanism** for implementing foreign traits on foreign types: +A hole synthesizes or checks against the expected type from context: ```spore -adapter ExternalType as Printable { - fn display(self) -> Str { ... } -} - -fn use_external(x: ExternalType) -> Str -where adapter: ExternalType as Printable -{ - x.display() +fn parse(input: Str) -> Config ! ParseError { + ?parse_body } ``` -Adapters are always scoped and explicit — they cannot cause global coherence violations. - -### 3.10 Index generics - -#### Index parameters - -`Index` parameters are compile-time non-negative size symbols. They are the -only user-visible variables that may flow directly into `cost [...]`. This keeps -cost checking parametric and compile-time: the verifier proves formulas over -symbols such as `N`, not over arbitrary runtime values. - -### Dynamic `List` vs indexed containers - -**Naming invariant:** **`List[T]` is always dynamic** — the `List` constructor -never carries a length or capacity parameter. If an API needs predictable cost -from a static size, use an indexed type: - -- **`Count[N]`** for a runtime count value carrying index `N`. -- **`Array[T, N]`** for fixed-length arrays. -- **`Vec[T, max: N]`** for buffers with a static capacity upper bound. - -`List[T]` remains the default sequence type for ordinary programs. A `List[T]` -can still be inspected at runtime, but its runtime length is not a CostExpr -variable. APIs that need verified size-parametric cost should accept or return -`Count`, `Array`, or `Vec`. - -```spore -struct Array[T, N: Index] { - data: -} - -struct Vec[T, max: N] { - data: Array[T, N], - len: I64, -} +The checker records the expected type and surrounding bindings for SEP-0005. -struct Matrix[T, rows: R, cols: C] -where R: Index, C: Index { - data: Array[Array[T, C], R], -} -``` +## Reference-level explanation -#### Index expressions +### Type judgments -Index parameters support a deliberately small expression language: +Core checking uses two judgments: ```text -IndexExpr ::= 0 | 1 | N - | IndexExpr + IndexExpr - | IndexExpr * IndexExpr - | max(IndexExpr, IndexExpr) - | min(IndexExpr, IndexExpr) - | log(IndexExpr) - | span(IndexExpr, IndexExpr) -``` - -`span(hi, lo)` is saturating difference: `max(hi - lo, 0)`. Ordinary subtraction -and division are not IndexExpr operators. This keeps the cost verifier in a -non-negative, monotone fragment while still supporting interval-length APIs. - -#### Interaction with cost - -Cost clauses refer to Index parameters, not ordinary runtime values: - -```spore -fn vec_linear_search[T, N: Index]( - items: Vec[T, max: N], - target: T, -) -> Option[I64] -where T: Eq -cost [N * 5, 0, 0, 0] -{ - // Verified against the static capacity bound N. -} - -fn vec_concat[T, M: Index, N: Index]( - a: Vec[T, max: M], - b: Vec[T, max: N], -) -> Vec[T, max: M + N] -cost [M + N, M + N, 0, 0] -{ - // implementation -} +Γ |- e => T expression synthesizes type T +Γ |- e <= T expression checks against expected type T ``` -For dynamic collections, write complexity prose or use runtime metering; do not -write a runtime-length expression as a verified compile-time CostExpr. +Function bodies are checked against the result type from the Base Signature. +Holes are compatible with the expected type but recorded as incomplete terms. -### 3.11 Pattern matching +### Inline bounds -Spore requires **full, exhaustive pattern matching** on all enums. The compiler enforces that every possible variant is handled. - -#### Exhaustiveness checking - -```spore -fn describe(shape: Shape) -> Str { - match shape { - Circle { radius } => "circle with radius " ++ show(radius), - Rectangle { width, height } => show(width) ++ "x" ++ show(height) ++ " rectangle", - Triangle { a, b, c } => "triangle with sides " ++ show(a) ++ ", " ++ show(b) ++ ", " ++ show(c), - } -} -``` - -Missing a variant is a compile-time error: +A type parameter has this normalized shape: ```text -Error: Non-exhaustive match - -12 | match shape { - | ^^^^^ - Missing variant: Triangle { a, b, c } - - All Shape variants must be handled because Shape is a sealed enum. +TypeParam { name, kind?, bounds[] } ``` -#### Nested patterns - -Patterns can be nested to match deeply into data structures: - -```spore -type Expr = - Literal { value: I64 } - | BinOp { op: Op, left: Expr, right: Expr } - | UnaryOp { op: Op, operand: Expr } - -fn simplify(expr: Expr) -> Expr { - match expr { - BinOp { op: Add, left: Literal { value: 0 }, right: e } => simplify(e), - BinOp { op: Mul, left: Literal { value: 1 }, right: e } => simplify(e), - BinOp { op: Mul, left: Literal { value: 0 }, right: _ } => Literal { value: 0 }, - BinOp { op, left, right } => BinOp { - op: op, - left: simplify(left), - right: simplify(right), - }, - other => other, - } -} -``` - -Nested Option/Result matching: - -```spore -fn handle(value: Option[Result[I64, Str]]) -> I64 { - match value { - Some(Ok(n)) => n, - Some(Err(msg)) => panic("error: " ++ msg), - None => 0, - } -} -``` +Bounds are trait requirements. Effects do not appear in type parameter bounds; +effect checking belongs to SEP-0003. -#### Guard clauses +### Inference boundary -Guards add boolean conditions to pattern branches: +Signatures must be explicit. Local bindings may omit annotations when the +right-hand side synthesizes a type: ```spore -fn classify_temperature(temp: F64) -> Str { - match temp { - t if t < -40.0 => "extreme cold", - t if t < 0.0 => "freezing", - t if t < 20.0 => "cool", - t if t < 35.0 => "warm", - t if t < 50.0 => "hot", - _ => "extreme heat", - } -} +let message = "ready"; +let count = 42; ``` -**Important**: Guards weaken exhaustiveness guarantees. The compiler requires a catch-all (`_` or variable) arm when guards are used, because it cannot generally prove that guards cover all cases. - -#### Or-patterns +### Outcome typing -Or-patterns match multiple alternatives with the same arm: +Outcome types are normalized into a pair: -```spore -fn is_weekend(day: Day) -> Bool { - match day { - Saturday | Sunday => true, - _ => false, - } -} - -fn is_simple_shape(shape: Shape) -> Bool { - match shape { - Circle { .. } | Rectangle { .. } => true, - _ => false, - } -} +```text +OutcomeType +├── success_type +└── failure_type ``` -#### Destructuring in let bindings - -Pattern matching works in `let` bindings: +A call is valid when the callee's failure type is handled locally, propagated by +`?`, or explicitly transformed into the caller's declared failure type. `fail e` +checks against an expected outcome when `e` inhabits the failure type. Bare +`A ! E ! F` is rejected without parentheses so `!` does not become an ambiguous +chain operator. -```spore -fn distance(a: Point, b: Point) -> F64 { - let Point { x: x1, y: y1 } = a - let Point { x: x2, y: y2 } = b - sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) -} -``` +### Property body typing -#### Wildcard and rest patterns +A source property body is an ordinary Spore expression checked under the +surrounding callable's type environment. -- `_` matches any single value (discarded). -- `..` in struct patterns matches remaining fields. +If a property item is written as: ```spore -fn get_name(person: Person) -> Str { - let Person { name, .. } = person - name +properties { + name(params...): body } ``` -#### Pattern matching on error types - -Pattern matching integrates with Spore's closed error sets for exhaustive error -handling: - -```spore -fn handle_result(result: Invoice ! TaxError | ValidationError) -> Str { - match result { - Ok(invoice) => "Invoice #" ++ show(invoice.id), - Err(TaxError { region, reason }) => "Tax error in " ++ show(region) ++ ": " ++ reason, - Err(ValidationError { field, message }) => "Validation failed on " ++ field ++ ": " ++ message, - } -} -``` +then `body` must check against `Bool`. A body with any other result type is a +type error. Property parameters introduce local bindings with the declared types +for the property body only; they do not change the callable's Base Signature. -### 3.12 @allows constraint +The property body inherits the enclosing callable's effect context. It may only +perform effects included in the enclosing `uses` surface or effects discharged +by a narrower local handler context. A property body that requires an unavailable +effect is an effect-checking error. -`@allows` is a hole-level constraint that limits which functions an Agent may use to fill a specific hole. It provides fine-grained control over Agent behavior without affecting the type system's soundness. +The type checker does not need to decide whether every well-typed property is +true. It only establishes that the property is a `Bool` expression in the right +type and effect context. Decidable failures may become immediate diagnostics; +undecidable obligations lower into SEP-0006 claims. -```spore -fn process_payment(amount: Money, card: Card) -> Receipt ! PaymentFailed -uses [PaymentGateway, AuditLog] -cost [2000, 400, 200, 0] -{ - let validated = validate_card(card) - @allows[charge, charge_with_retry] - ?payment_logic -} -``` +### Refinement obligations -**Semantics of @allows**: +When a refinement predicate is decidable in the type checker, it is discharged +immediately. Otherwise the checker emits a property-level obligation that the +compiler can lower into a Claim for evidence processing. This is the same claim +and evidence channel used for source properties. Source properties and +refinement obligations differ in where they come from, not in the shape of the +downstream claim record. -- `@allows[f1, f2, ...]` annotates the immediately following hole. -- The Agent, when filling this hole, may only call the listed functions (plus pure helper functions that require no effects). -- The compiler verifies that the filling respects the `@allows` constraint. -- `@allows` is **not** a type — it is a **synthesis constraint** that restricts the search space for hole-filling. +## Human experience impact -**Use case — architectural intent**: +Inline bounds keep type requirements near the variables they constrain. The +signature remains the place where readers learn what can be passed, returned, or +failed. -```spore -fn build_dashboard(org: Org) -> Dashboard ! DataError -uses [Database, Cache, Analytics] -cost [20000, 2000, 1000, 0] -{ - @allows[fetch_cached_metrics, compute_summary] - let metrics = ?gather_metrics +## Agent experience impact - @allows[render_chart, render_table] - let charts = ?render_visualizations +Agents receive precise expected types for holes and can filter candidate +realizations by trait bounds, outcome boundaries, and visible bindings without +parsing prose. - Dashboard.new(org, metrics, charts) -} -``` +## Structured representation / protocol impact -If an Agent proposes a filling for `?gather_metrics` that calls `raw_sql_query`, the compiler rejects it: +TypedHIR records: ```text -Error: @allows violation - - Hole ?gather_metrics is constrained by @allows[fetch_cached_metrics, compute_summary] - but the proposed filling calls `raw_sql_query`, which is not in the allowed set. +FunctionType +├── type_params[] +├── params[] +├── result_type +├── outcome_shape? +└── trait_obligations[] ``` -### 3.13 Never bottom type +When `result_type` is an outcome, `outcome_shape` records the normalized +`success_type` / `failure_type` pair. HoleReport receives `type.expected`, +`type.inferred_from`, visible bindings, and unsatisfied trait obligations. -`Never` is the bottom type — it has no values and is a subtype of every type. It is the return type of functions that do not return (diverging functions). - -```spore -fn abort(msg: Str) -> Never { - panic(msg) -} +## Diagnostics impact -fn safe_divide(a: I64, b: I64) -> I64 { - if b == 0 { - abort("division by zero") // Never coerces to I64 - } else { - a / b - } -} -``` +Type diagnostics use `E0xxx` codes. Important categories include unknown type, +return mismatch, outcome mismatch, unhandled failure propagation, missing trait +implementation, unsatisfied refinement, and ambiguous hole type. -**Use in exhaustive matching** — a branch returning `Never` is compatible with any arm type: +A bodyless `type Name;` declaration without `@foreign` should produce a hard +error that asks the author to either mark the type as external (`@foreign type +Name;`) or provide a real definition (`type Name = ...`). -```spore -fn safe_unwrap[T](opt: Option[T]) -> T { - match opt { - Some(v) => v, - None => panic("unwrap of None"), // panic returns Never, coerces to T - } -} -``` +## Drawbacks -`Never` is the natural type for: +Dense generic headers can become difficult to read. Formatters should allow one +type parameter per line for highly constrained functions. -- `panic(...)` and `abort(...)` calls -- Diverging expressions (non-terminating recursion) -- Exhaustive match arms that provably never execute -- Error-only functions that always raise +Refinement obligations that become evidence claims may surprise users who expect +all refinements to be checked immediately. -### 3.14 Canonicalized error sets +## Alternatives considered -Spore's `! Err1 | Err2` is a **closed error set** written in surface form but -checked with **canonicalization-first semantics**. Error types are nominal ADTs; -the checker resolves each written item to a canonical identity, removes -duplicates, and compares sets using that canonical form across call chains. +### Structural typing -This section defines the canonicalized error-set semantics: subset/equivalence checks, redundancy diagnostics, and conservative hashing. +Rejected because nominal types give clearer diagnostics and package boundaries. -```text -canonicalize(E_written): - 1. resolve each written error item to its canonical nominal identity - 2. drop duplicates and canonically equivalent spellings - 3. order the result by a stable canonical key for comparison / diagnostics -``` +### Inferred public signatures -Two written error clauses are **equivalent** when their canonicalized sets are -equal, even if their source spelling differs. A caller is **compatible** with a -callee when `canonicalize(E_callee) ⊆ canonicalize(E_caller)`. +Rejected because the signature model relies on explicit callable boundaries as shared +intent. -```spore -alias ParseFailure = config.errors.ParseError +### Separate bound clauses -fn parse(input: Str) -> Ast ! config.errors.ParseError -fn parse_again(input: Str) -> Ast ! ParseFailure | config.errors.ParseError -``` +Rejected because they split generic requirements away from the type variables. -`parse_again` is semantically equivalent to `! config.errors.ParseError`; the -second item is redundant and should be diagnosed as such. +## Prior art -Error sets still compose automatically via `?` propagation: +Rust influenced traits and explicit callable boundaries. ML-family languages +influenced algebraic data types and inference. Liquid types influenced the +refinement model. -```spore -fn parse(input: Str) -> Ast ! SyntaxError | EncodingError -fn validate(ast: Ast) -> ValidAst ! TypeError | RangeError +## Backward compatibility and migration -// Error sets union automatically via `?` propagation: -fn compile(input: Str) -> ValidAst ! SyntaxError | EncodingError | TypeError | RangeError { - let ast = parse(input)? - validate(ast)? -} -``` - -Each error type carries structured data: - -```spore -struct SyntaxError { - line: I64, - column: I64, - message: Str, - snippet: Str, -} -``` - -**Canonical subset rule**: A function with error set `! A` can be called where -`! A | B` is expected when `canonicalize({A}) ⊆ canonicalize({A, B})`. - -**`?` operator**: Propagates errors by canonical subset check. If `f()` returns -`T ! E1` and `g()` returns `T ! E2`, calling both with `?` in the same function -requires the enclosing function to declare a canonical superset of -`canonicalize({E1, E2})`. - -**Redundancy diagnostics**: If a declaration writes duplicate or canonically -equivalent error items, the compiler should keep checking against the -canonicalized set but emit a structured redundancy diagnostic. User-facing text -should stay engineering-oriented, e.g. "redundant error item `ParseFailure`: -already covered by `config.errors.ParseError`". - -### 3.15 Recursive types - -Enums and structs may be recursive. The compiler detects and supports this: - -```spore -type Expr = - Literal { value: I64 } - | BinOp { op: Op, left: Expr, right: Expr } - | UnaryOp { op: Op, operand: Expr } - | IfExpr { cond: Expr, then_branch: Expr, else_branch: Expr } - -type JsonValue = - JsonNull - | JsonBool { value: Bool } - | JsonNumber { value: F64 } - | JsonString { value: Str } - | JsonArray { elements: List[JsonValue] } - | JsonObject { fields: List[{ key: Str, value: JsonValue }] } -``` - -**Infinite-size check**: The compiler rejects types that would require infinite memory without indirection: - -```spore -// ERROR: infinite-size type (struct directly contains itself) -struct Bad { - next: Bad, // Bad contains Bad with no indirection -} - -// OK: indirection via List breaks the cycle -struct Tree[T] { - value: T, - children: List[Tree[T]], // List provides indirection -} -``` - -### 3.16 Nominal vs structural rules - -| Context | Typing Discipline | Rationale | -| --------------------------------------------- | -------------------- | ---------------------------------------------------------------- | -| Named types (`struct` / `type` with variants) | **Nominal** | `UserId ≠ Str` even if same representation | -| Enums | **Nominal** | Sealed, exhaustiveness-checked | -| Traits / effects | **Nominal** | Explicit `impl` required | -| Effects | **Nominal** (always) | Security boundary — no structural coincidence | -| Anonymous records `{ ... }` | **Structural** | Flexibility for intermediate values | -| Hole-filling search (internal) | **Structural** | Agent searches by shape, compiler enforces nominal at boundaries | -| Function call boundaries | **Nominal** | Callee specifies named types, caller must provide them | - -### 3.17 Type inference annotation requirements - -| Element | Must Annotate? | Why | -| ------------------------------------ | -------------- | ------------------------------------------------------------------------ | -| Function parameter types | **Yes** | Gravity center — the signature IS the API | -| Function return type | **Yes** | Agent reads signatures for synthesis; human reads for understanding | -| Error sets (`! ...`) | **Yes** | Error contract — must be visible | -| Effect sets (`uses [...]`) | **Yes** | Security boundary — must be visible | -| Cost clause (`cost [c, a, i, p]`) | **Yes** | Performance contract — must be visible | -| Struct/type field types | **Yes** | Data definition — must be explicit | -| Trait method signatures | **Yes** | Interface contract | -| Public constants | **Yes** | API surface | -| Local variable types | No | Inferred from RHS: `let x = compute(...)` | -| Generic type params at call sites | No | Inferred from arguments: `sort(my_list)` — T inferred from `my_list` | -| Closure parameter types (in context) | No | Inferred: `xs.map(\|x\| x + 1)` — x inferred from the receiver item type | -| Intermediate expression types | No | Standard local inference | - -### 3.18 Typed holes - -Holes are unfilled positions in code. The type checker infers their expected type from context and reports it: - -```spore -fn example(x: I64, y: Str) -> Bool { - let a: I64 = ?h1 // hole ?h1 must produce I64 - let b = if a > 0 { - ?h2 // hole ?h2 must produce Bool (from return type) - } else { - false - } - b -} -``` - -The compiler reports: - -```text -Hole ?h1: expected type I64 -Hole ?h2: expected type Bool -``` - -## Reference-level explanation - -### Notation - -| Symbol | Meaning | Used in | -| ------ | ------------------------------------------------- | -------------------------- | -| `τ` | Type | Type grammar, judgments | -| `σ` | Parameter type (a `τ` used in parameter position) | Function signatures | -| `Γ` | Type environment (variable → type mapping) | Typing judgments | -| `⊢` | Judgment turnstile ("entails") | Typing rules | -| `⇒` | Type synthesis (infer type bottom-up) | Bidirectional typing | -| `⇐` | Type checking (check against expected type) | Bidirectional typing | -| `<:` | Subtype relation | Subtyping rules | -| `C` | EffectSet (set of effect names) | Function types | -| `E` | ErrorSet (set of error types) | Function types, `!` clause | -| `ι` | Integer width metavariable ∈ {I8, …, U64} | Operator typing | -| `φ` | Float width metavariable ∈ {F32, F64} | Operator typing | - -### 4.1 Type grammar - -The internal type representation follows this grammar: - -```text -τ ::= I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64 | F32 | F64 - | Bool // boolean primitive - | Str // UTF-8 string (`Str` in surface syntax) - | Unit // unit type (also `()`); empty tuple syntax normalizes here - | Never // bottom type (uninhabited) - | Tuple([τ₁, …, τₙ]) // tuple type (n ≥ 2; empty tuple is Unit) - | Refined(τ, var, predicate) // refinement (decidable fragment) - | Named(n) // named type / type parameter - | App(n, [τ₁, …, τₖ]) // generic type application - | Record([(f₁, τ₁), …]) // anonymous record (structural) - | Fn([τ₁, …, τₙ], τᵣ, C, E) // function type with effect and error sets - | Var(id) // unification variable - | Hole(name) // typed hole placeholder - | Error // error sentinel (recovery) -``` - -Where: - -- `n` ranges over identifiers (strings). -- `id` ranges over unique unification variable IDs. -- `C` is an **EffectSet** — the set of effect names the function requires. -- `E` is an **ErrorSet** — the closed union of error types declared with `!`. -- `f` ranges over field names in anonymous records. - -**Never type.** `Never` is the bottom type — a subtype of all types. It is produced by diverging expressions (`panic`, non-terminating recursion). During unification, `unify(τ, Never) = ok` for all `τ`. - -**Record type.** `Record` represents anonymous structural records. Width subtyping applies: `Record([(a, I64), (b, Str), (c, Bool)])` is a subtype of `Record([(a, I64), (b, Str)])`. - -**Error sentinel.** `Error` is produced when type resolution fails (e.g., unknown type name). It unifies with anything, allowing the checker to continue after errors and report multiple diagnostics. - -**Hole type.** `Hole(name)` represents an unfilled hole. During unification, holes are compatible with anything — the checker records the expected type for diagnostic purposes without blocking further checking. - -### 4.2 Effect sets (EffectSet) - -An `EffectSet` is a sorted set of effect-name identifiers. It appears as the third component of a function type; the fourth component is the function's `ErrorSet`: - -```text -Fn([τ₁, …, τₙ], τᵣ, { cap₁, cap₂, … }, { err₁, err₂, … }) -``` - -**EffectSet rules:** - -1. A function may only call another function whose EffectSet is a **subset** of its own. -2. A pure function has `EffectSet = ∅`. -3. EffectSets propagate through higher-order calls: passing a `Fn(...) uses [NetConnect]` to a higher-order function requires the caller to declare `NetConnect`. -4. `uses [Effect1, Effect2]` in source syntax maps directly to `EffectSet = {"Effect1", "Effect2"}`. - -**Formal effect checking rule:** - -```text -Γ ⊢ f : Fn([σ₁, …, σₙ], τᵣ, C_f, E_f) -C_caller ⊇ C_f -canonicalize(E_f) ⊆ canonicalize(E_caller) -──────────────────────────────────────────────────────────── -Γ; C_caller; E_caller ⊢ f(e₁, …, eₙ) : τᵣ -``` - -If `C_f ⊄ C_caller`, the checker emits: - -```text -missing effects [X, Y]: caller does not declare them -``` - -### Formal typing judgments - -The core type system uses bidirectional typing with effect context. - -**Notation:** - -- `Γ` — type environment (variable → type mapping) -- `S` — effect set (subset of all effects) -- `e` — expression -- `T` — type -- `⊢` — "entails" / judgment turnstile -- `⇒` — type synthesis (infer) -- `⇐` — type checking (against expected) - -**Core judgments:** - -```text -Γ; S ⊢ e ⇒ T (synthesis: infer type of e) -Γ; S ⊢ e ⇐ T (checking: check e against T) -``` - -**Rules:** - -```text -[Var] - x : T ∈ Γ - ───────────────── - Γ; S ⊢ x ⇒ T - -[Literal-I64] - ───────────────── - Γ; S ⊢ n ⇒ I64 - - (Default unsuffixed integer literal per §3.1; a checking context may instead fix another **ι** ∈ {`I8`, …, `U64`}.) - -[Literal-F64] - ───────────────── - Γ; S ⊢ c ⇒ F64 - - (Default unsuffixed float literal per §3.1; a checking context may instead fix another **φ** ∈ {`F32`, `F64`}.) - -[Literal-Str] - ───────────────── - Γ; S ⊢ s ⇒ Str - -[Literal-Bool] - ───────────────── - Γ; S ⊢ b ⇒ Bool - -[Lambda] - Γ, x : T₁ ; S ⊢ body ⇐ T₂ - ───────────────────────────── - Γ; S ⊢ (fn(x: T₁) -> T₂ { body }) ⇒ T₁ → T₂ uses S - -[App] - Γ; S ⊢ f ⇒ T₁ → T₂ uses S_f - Γ; S ⊢ arg ⇐ T₁ - S_f ⊆ S - ───────────────────────────── - Γ; S ⊢ f(arg) ⇒ T₂ - -[Let] - Γ; S ⊢ e₁ ⇒ T₁ - Γ, x : T₁ ; S ⊢ e₂ ⇒ T₂ - ───────────────────────────── - Γ; S ⊢ (let x = e₁; e₂) ⇒ T₂ - -[If] - Γ; S ⊢ cond ⇐ Bool - Γ; S ⊢ then_branch ⇒ T - Γ; S ⊢ else_branch ⇐ T - ───────────────────────────── - Γ; S ⊢ (if cond { then } else { else }) ⇒ T - -[Match] - Γ; S ⊢ scrutinee ⇒ T_s - ∀i: pattern_i exhaustive over T_s - ∀i: Γ, bindings(pattern_i) ; S ⊢ body_i ⇒ T - ───────────────────────────────────────────── - Γ; S ⊢ (match scrutinee { pattern_i => body_i }) ⇒ T - -[StructConstruct] - struct S { f₁: T₁, ..., fₙ: Tₙ } ∈ Γ - ∀i: Γ; S_cap ⊢ eᵢ ⇐ Tᵢ - ───────────────────────────── - Γ; S_cap ⊢ S { f₁: e₁, ..., fₙ: eₙ } ⇒ S - -[FieldAccess] - Γ; S ⊢ e ⇒ T_struct - T_struct has field f : T_f - ───────────────────────────── - Γ; S ⊢ e.f ⇒ T_f - -[EnumConstruct] - type E { ..., V(T₁, ..., Tₙ), ... } ∈ Γ - ∀i: Γ; S ⊢ eᵢ ⇐ Tᵢ - ───────────────────────────── - Γ; S ⊢ V(e₁, ..., eₙ) ⇒ E - -[Spawn] - Γ; S ⊢ e ⇒ T uses S_e - Spawn ∈ S - ───────────────────────────── - Γ; S ⊢ spawn e ⇒ Task[T] - -[Await] - Γ; S ⊢ e ⇒ Task[T] - ───────────────────────────── - Γ; S ⊢ await e ⇒ T - -[EffectCheck] - Γ; S ⊢ e ⇒ T uses S_required - S_required ⊆ S - ───────────────────────────── - (e is valid in effect context S) - -[Sub] - Γ; S ⊢ e ⇒ T₁ - T₁ <: T₂ - ───────────────────────────── - Γ; S ⊢ e ⇐ T₂ -``` - -**Subtyping rules:** - -```text -[Sub-Refl] - T <: T - -[Sub-Fn] - T₁' <: T₁ T₂ <: T₂' S ⊆ S' - ───────────────────────────── - (T₁ → T₂ uses S) <: (T₁' → T₂' uses S') - (contravariant params, covariant return, covariant effects) - -[Sub-Task] - T <: T' - ───────────────────────────── - Task[T] <: Task[T'] -``` - -### 4.3 Bidirectional type inference - -Spore uses **bidirectional type checking** with two modes: - -- **Check mode (⇐):** An expected type is pushed down from the context (function return type, let-binding annotation, argument position). -- **Synth mode (⇒):** A type is synthesized bottom-up from the expression structure. - -#### Inference rules (core subset) - -**Literals (synthesis):** default widths are §3.1 (`I64`, `F64`). For a non-default fixed width, read those conclusions as **ι** / **φ** when a context, annotation, or suffix fixes another member of the same families. - -```text -───────────────── -Γ ⊢ n ⇒ I64 (integer literal) - -───────────────── -Γ ⊢ c ⇒ F64 (float literal) - -───────────────── -Γ ⊢ s ⇒ Str (string literal) - -───────────────── -Γ ⊢ b ⇒ Bool (boolean literal) -``` - -**Variable lookup (synthesis):** - -```text -x : τ ∈ Γ -────────────── -Γ ⊢ x ⇒ τ -``` - -**Function application (synthesis):** - -```text -Γ ⊢ f ⇒ Fn([σ₁, …, σₙ], τᵣ, C, E) -Γ ⊢ eᵢ ⇐ σᵢ for i ∈ 1..n -C ⊆ C_current -canonicalize(E) ⊆ canonicalize(E_current) -──────────────────────────────────────────────────── -Γ; C_current; E_current ⊢ f(e₁, …, eₙ) ⇒ τᵣ -``` - -**Let binding (synthesis):** - -```text -Γ ⊢ e₁ ⇒ τ₁ -Γ, x : τ₁ ⊢ e₂ ⇒ τ₂ -──────────────────────── -Γ ⊢ let x = e₁; e₂ ⇒ τ₂ -``` - -**If-else (synthesis):** - -```text -Γ ⊢ cond ⇐ Bool -Γ ⊢ e_then ⇒ τ -Γ ⊢ e_else ⇒ τ -──────────────────────────── -Γ ⊢ if cond { e_then } else { e_else } ⇒ τ -``` - -**Function body checking (check mode):** - -```text -fn f(x₁: σ₁, …, xₙ: σₙ) -> τᵣ uses [C] { body } - -Γ' = Γ, x₁ : σ₁, …, xₙ : σₙ -Γ'; C ⊢ body ⇒ τ_body -unify(τᵣ, τ_body) -─────────────────────────────── -Γ ⊢ f ok -``` - -The body checking algorithm unifies the synthesized body type with the declared return type, ensuring the body satisfies the signature contract. - -### 4.4 Generics — type variables, substitution, and unification - -#### Type variables - -A fresh type variable carries a unique identifier. When a generic function is called, each type parameter is replaced by a fresh type variable. The unifier then resolves these variables by matching against actual argument types. - -#### Substitution - -A substitution is a partial map from variable IDs to resolved types. Applying a substitution walks the type recursively: - -```text -apply_subst(Var(id)) = σ(id) if id ∈ dom(σ) -apply_subst(Var(id)) = Var(id) otherwise -apply_subst(Fn(ps, r, C)) = Fn(apply_subst(ps), apply_subst(r), C) -apply_subst(App(n, args)) = App(n, apply_subst(args)) -apply_subst(Record(fields)) = Record([(f, apply_subst(τ)) for (f, τ) in fields]) -apply_subst(τ) = τ for all other τ -``` - -Note that `EffectSet` is **not** subject to substitution — effects are always concrete identifiers, never type variables. - -#### Unification algorithm - -The `unify` function is the heart of type inference. Given two types `expected` and `actual`, it either succeeds (possibly binding type variables) or emits an error: - -```text -unify(τ, τ) = ok (reflexivity) -unify(Never, _) = ok (bottom type: Never <: τ) -unify(_, Never) = ok (bottom type: Never <: τ) -unify(Error, _) = ok (error recovery) -unify(_, Error) = ok (error recovery) -unify(Hole(_), _) = ok (hole compatibility) -unify(_, Hole(_)) = ok (hole compatibility) -unify(Var(id), τ) = σ[id ↦ τ] if occurs_check(id, τ) (variable binding) -unify(τ, Var(id)) = σ[id ↦ τ] if occurs_check(id, τ) (variable binding, symmetric) -unify(Fn(ps₁, r₁, _), Fn(ps₂, r₂, _)) (structural: function) - = unify(ps₁[i], ps₂[i]) for all i; unify(r₁, r₂) -unify(App(n, as₁), App(n, as₂)) (structural: application) - = unify(as₁[i], as₂[i]) for all i -unify(Record(fs₁), Record(fs₂)) (structural: record, width subtyping) - = for each (f, τ) in fs₂: require f in fs₁ and unify(fs₁[f], τ) -unify(τ₁, τ₂) (mismatch) - = error "type mismatch: expected τ₁, got τ₂" -``` - -**Occurs check.** The unifier performs an occurs check to prevent cyclic substitutions (e.g., `Var(0) ↦ List[Var(0)]`). If `Var(id)` occurs within `τ`, binding fails with an error rather than creating an infinite type. - -**Never in unification.** `Never` unifies with any type, implementing the bottom-type subtyping rule `Never <: τ` directly within the unifier. This enables diverging expressions (e.g., `panic(...)`) to appear in any expression position. - -### 4.5 Two-pass module checking - -Module checking proceeds in two passes, implemented in `check_module`: - -```rust -pub fn check_module(&mut self, module: &Module) { - // Pass 1: register all top-level declarations - for item in &module.items { - self.register_item(item); - } - // Pass 2: check function bodies - for item in &module.items { - self.check_item(item); - } -} -``` - -Pass 1 — Signature registration: Iterates over all top-level items (functions, structs, type definitions) and registers their signatures: - -- **Functions:** Parameter types are resolved, return type is resolved (default `Unit`), EffectSet is extracted from `uses` clause, and type parameters from `where` clause are recorded. -- **Structs:** Field names and types are resolved and stored. -- **Type defs (enums):** Variant names and field types are resolved and stored. -- **Effects and imports:** Deferred (no registration needed). - -**Pass 2 — `check_fn`:** For each function with a body: - -1. Set the current EffectSet from the function's `uses` clause. -2. Push a new scope in the environment. -3. Bind each parameter to its declared type. -4. Synthesize the body type via `check_expr`. -5. Apply the current substitution to both the body type and declared return type. -6. Unify the declared return type with the body type. -7. Pop the scope and restore the previous context. - -This two-pass design allows **forward references** — a function can call another function defined later in the same module. - -### 4.6 Struct types - -Struct lookup resolves the struct name in the type registry and looks up the named field: - -```text -Γ ⊢ e ⇒ Named(S) -S.fields contains (f, τ_f) -──────────────────────────── -Γ ⊢ e.f ⇒ τ_f -``` - -Struct construction checks that all fields are present and each expression matches the declared field type: - -```text -S.fields = [(f₁, τ₁), …, (fₙ, τₙ)] -Γ ⊢ eᵢ ⇐ τᵢ for all i ∈ 1..n -──────────────────────────────────── -Γ ⊢ S { f₁: e₁, …, fₙ: eₙ } ⇒ Named(S) -``` - -### 4.7 Anonymous record types - -Anonymous records are represented as `Ty::Record(Vec<(InternedFieldName, Ty)>)` (field labels stored as interned UTF-8 identifiers in the implementation). Record construction synthesizes the type from field expressions: - -```text -Γ ⊢ e₁ ⇒ τ₁ … Γ ⊢ eₖ ⇒ τₖ -───────────────────────────────────────────── -Γ ⊢ { f₁: e₁, …, fₖ: eₖ } ⇒ Record([(f₁, τ₁), …, (fₖ, τₖ)]) -``` - -Field access on anonymous records: - -```text -Γ ⊢ e ⇒ Record(fields) -(f, τ_f) ∈ fields -──────────────────────── -Γ ⊢ e.f ⇒ τ_f -``` - -**Width subtyping rule**: An anonymous record with more fields is a subtype of one with fewer fields: - -```text -fields₁ ⊇ fields₂ (by field name) -∀ (f, τ) ∈ fields₂ : unify(fields₁[f], τ) = ok -───────────────────────────────────────────────── -Record(fields₁) <: Record(fields₂) -``` - -**Nominal types do NOT satisfy anonymous record types.** `Named("Point")` and `Record([(x, F64), (y, F64)])` are distinct types even if `Point` has the same fields. - -### 4.8 Function types - -Function types are `Fn(params, ret, EffectSet, ErrorSet)`: - -```text -Fn([σ₁, …, σₙ], τᵣ, C, E) -``` - -A function value is a first-class value. Looking up a function name when it appears as a bare expression produces its function type from the type registry. - -**Display format for function types:** - -```text -(I64, Str) -> Bool -(I64) -> I64 uses [FileRead, NetConnect] -``` - -### 4.9 Subtyping - -Spore primarily uses **equality-based** type compatibility via unification, with targeted subtyping for specific cases: - -- `I64 ≠ F64` — no implicit numeric widening. -- `Named("UserId") ≠ Named("Str")` — newtypes are distinct. -- `Ty::Error` unifies with anything (error recovery only). -- `Ty::Hole(name)` unifies with anything (hole placeholder only). -- `Ty::Never` unifies with anything (`Never <: τ` for all `τ`). -- Width subtyping for anonymous records: `{ a: I64, b: Str, c: Bool } <: { a: I64, b: Str }`. -- Effect set subtyping: `C₁ ⊆ C₂ ⟹ Fn(ps, r, C₁) <: Fn(ps, r, C₂)` (a function needing fewer effects is more general). - -### 4.10 Binary and unary operator typing - -**Reading guide.** §3.1 fixes **default** literal widths (**`I64`**, **`F64`**). The first rules below state operator typing for those defaults explicitly. **Width metavariables** reuse the Summary: **ι** ∈ {`I8`, `I16`, `I32`, `I64`, `U8`, `U16`, `U32`, `U64`} and **φ** ∈ {`F32`, `F64`}. Family rules apply to every fixed width; defaults remain the special case **ι = `I64`**, **φ = `F64`** when no other constraint fixes a width. - -**Arithmetic operators** (`+`, `-`, `*`, `/`, `%`) — default scalar widths (§3.1): - -```text -Γ ⊢ e₁ ⇒ τ Γ ⊢ e₂ ⇒ τ -τ ∈ {I64, F64} -──────────────────────────── -Γ ⊢ e₁ ⊕ e₂ ⇒ τ -``` - -**Arithmetic — all fixed integer and float widths:** - -```text -Γ ⊢ e₁ ⇒ ι Γ ⊢ e₂ ⇒ ι ⊕ ∈ {+, -, *, /, %} -──────────────────────────── -Γ ⊢ e₁ ⊕ e₂ ⇒ ι - -Γ ⊢ e₁ ⇒ φ Γ ⊢ e₂ ⇒ φ ⊕ ∈ {+, -, *, /, %} -──────────────────────────── -Γ ⊢ e₁ ⊕ e₂ ⇒ φ -``` - -(Both operands unify to the **same** `Ty`; there is no implicit widening between widths—see §4.9.) - -**`Str` concatenation** (`+` only): - -```text -Γ ⊢ e₁ ⇒ Str Γ ⊢ e₂ ⇒ Str -──────────────────────────── -Γ ⊢ e₁ + e₂ ⇒ Str -``` - -**Comparison operators** (`==`, `!=`, `<`, `<=`, `>`, `>=`): - -```text -Γ ⊢ e₁ ⇒ τ Γ ⊢ e₂ ⇒ τ -──────────────────────────── -Γ ⊢ e₁ ⊗ e₂ ⇒ Bool -``` - -(Relational operators require comparable operands; both operand types must unify. For documentation-only emphasis, **numeric** comparisons are exactly the instances where `τ = ι` or `τ = φ`.) - -**Boolean operators** (`&&`, `||`): - -```text -Γ ⊢ e₁ ⇐ Bool Γ ⊢ e₂ ⇐ Bool -────────────────────────────────── -Γ ⊢ e₁ ∧∨ e₂ ⇒ Bool -``` - -**Unary negation** (`-`) — default scalars: - -```text -Γ ⊢ e ⇒ τ τ ∈ {I64, F64} -─────────────────────────────── -Γ ⊢ -e ⇒ τ -``` - -**Unary negation — all numeric widths:** - -```text -Γ ⊢ e ⇒ τ τ = ι or τ = φ -─────────────────────────────── -Γ ⊢ -e ⇒ τ -``` - -**Bitwise operators** (surface: `&`, `|`, `^`, `<<`, `>>`, and unary `~`; integers only): - -```text -Γ ⊢ e₁ ⇒ ι Γ ⊢ e₂ ⇒ ι ⊙ ∈ {&, |, ^, <<, >>} -──────────────────────────── -Γ ⊢ e₁ ⊙ e₂ ⇒ ι - -Γ ⊢ e ⇒ ι -────────────── -Γ ⊢ ~e ⇒ ι -``` - -**Logical not** (`!`): - -```text -Γ ⊢ e ⇐ Bool -────────────── -Γ ⊢ !e ⇒ Bool -``` - -### 4.11 Refinement types — future vision - -Refinement types are not yet implemented but are a planned extension organized into two levels. - -#### L0 — Decidable predicates - -L0 refinements are predicates the compiler can fully evaluate at compile time: - -```spore -type Port = I64 when 1 <= self <= 65535 -type Percentage = F64 when 0.0 <= self <= 100.0 -type NonEmptyString = Str when self.len() > 0 -``` - -The decidable predicate language includes: - -- Numeric comparisons: `<`, `<=`, `==`, `!=`, `>=`, `>` -- Arithmetic on constants: `self + 1 <= 100` -- **Str** / collection length: `self.len() > 0` -- Boolean connectives: `&&`, `||`, `!` -- Const equality: `self == "production" || self == "staging"` - -**L0 checking rule:** - -```text -Γ ⊢ e ⇒ base_type -eval(predicate[self ↦ e]) = true -──────────────────────────────── -Γ ⊢ e ⇐ base_type when predicate -``` - -When the compiler cannot statically evaluate the predicate (e.g., value comes from runtime input), it requires an explicit runtime check via control flow. - -#### L1 — Abstract interpretation propagation - -L1 uses flow-sensitive abstract interpretation to propagate refinement information: - -```spore -fn process_port(raw: I64) -> Port ! InvalidPort { - if raw < 1 || raw > 65535 { - raise InvalidPort { value: raw } - } - // After the guard, the compiler knows: 1 <= raw <= 65535 - // raw is automatically narrowed to Port - raw -} -``` - -The abstract domain tracks **interval constraints** on integer and float values. At each branch point, the domain is split: - -- True branch: constraints from the condition added. -- False branch: negated constraints added. - -L1 explicitly excludes: arbitrary quantifiers, aliasing analysis, heap reasoning, and SMT-level theorem proving. This keeps checking decidable and error messages predictable. - -### 4.12 Trait resolution - -#### Trait registry - -Traits are registered alongside types. Each trait records: - -- Trait name -- Supertrait requirements (e.g., `Ord: Eq`) -- Method signatures (name, parameter types, return type, EffectSet, ErrorSet) -- Associated type declarations -- Default method implementations (if any) - -#### Trait implementation checking - -When an `impl Trait for Type` block is encountered: - -1. Verify that all required methods are provided (or have defaults). -2. For each method, check that the signature matches the trait declaration (after substituting `Self` with the implementing type). -3. Type-check each method body against its declared signature. -4. Verify supertrait implementations exist. - -```text -trait T { fn m(self, x: σ) -> τ } -impl T for S { fn m(self, x: σ') -> τ' { body } } -──────────────────────────────────────────────── -require: σ' = σ[Self ↦ S] and τ' = τ[Self ↦ S] -check: Γ, self: S, x: σ' ⊢ body ⇐ τ' -``` - -#### Coherence checking - -The compiler enforces the orphan rule at module registration time: - -- `impl T for S` is allowed only if the current module defines `T` or `S`. -- Adapter declarations are scoped: `adapter S as T { ... }` is usable only where explicitly imported via `where adapter: S as T`. - -### 4.13 Pattern matching exhaustiveness - -The exhaustiveness checker operates on the `TypedHIR` after type inference. For each `match` expression: - -1. Collect all variants of the matched enum type. -2. For each arm, compute the set of variants it covers (including nested patterns). -3. Compute the uncovered set = all variants − covered variants. -4. If uncovered set is non-empty and no wildcard arm exists, emit a compile error listing missing variants. - -```text -match e : T where T is enum with variants [V₁, …, Vₙ] -arms cover variants S ⊆ {V₁, …, Vₙ} -S = {V₁, …, Vₙ} ∨ wildcard arm present -───────────────────────────────────────── -match is exhaustive -``` - -Guards weaken exhaustiveness: when guards are present, the compiler conservatively requires a catch-all arm. - -### 4.14 Index generic evaluation - -Index expressions are evaluated or normalized during type checking. The -compiler keeps them symbolic when a parameter is unknown and reduces them when -all arguments are concrete: - -```text -eval(0) = 0 -eval(1) = 1 -eval(N) = N -eval(A + B) = eval(A) + eval(B) -eval(A * B) = eval(A) * eval(B) -eval(min(A, B)) = min(eval(A), eval(B)) -eval(max(A, B)) = max(eval(A), eval(B)) -eval(log(A)) = ceil(log2(max(1, eval(A)))) -eval(span(A, B)) = max(eval(A) - eval(B), 0) -``` - -Overflow in concrete Index arithmetic is a compile-time error. Ordinary -subtraction and division are intentionally absent; `span` is the only -saturating difference operator. - -When an indexed function is instantiated, the compiler substitutes concrete -Index arguments where available: - -```text -vec_concat[I64, 3, 5](a, b) -> Vec[I64, max: eval(3 + 5)] = Vec[I64, max: 8] -``` - -### 4.15 Error set typing - -Error sets are tracked as part of function types. The `!` syntax represents a closed union of error types: - -```text -Fn([σ₁, …, σₙ], τᵣ, C, E) where E = {Err₁, …, Errₖ} -``` - -**Error propagation via `?`**: - -```text -Γ ⊢ e ⇒ Fn([…], τ, C, E_callee) -canonicalize(E_callee) ⊆ canonicalize(E_caller) -────────────────────────────────── -Γ; E_caller ⊢ e(…)? ⇒ τ -``` - -If `canonicalize(E_callee) ⊄ canonicalize(E_caller)`, the checker emits: - -```text -error: function may raise [Err1] which is not in the declared error set -``` - -**Canonical error union**: When multiple `?` calls appear in a function body, -the function's error set must be a superset of the canonical union of all -callees' error sets. - -### 4.16 Never type in the type system - -`Never` participates in the type system as follows: - -1. **Unification**: `unify(τ, Never) = ok` and `unify(Never, τ) = ok` for all `τ`. -2. **If-else branches**: If one branch returns `Never`, the result type is the other branch's type. -3. **Match arms**: A `Never`-typed arm is compatible with all other arm types. -4. **Function return**: `fn f() -> Never` means `f` never returns normally. - -```text -Γ ⊢ e_then ⇒ τ -Γ ⊢ e_else ⇒ Never -──────────────────── -Γ ⊢ if cond { e_then } else { e_else } ⇒ τ -``` - -### 4.17 Recursive type validation - -The compiler validates recursive types during registration: - -1. Build a dependency graph of type definitions. -2. For each cycle in the graph, verify that at least one edge passes through an indirection type (`List`, `Option`, `Box`, etc.). -3. If a cycle has no indirection, emit an infinite-size error. - -```text -struct A { field: B } -struct B { field: A } // ERROR: infinite size (A → B → A with no indirection) - -struct A { field: List[B] } -struct B { field: A } // OK: List provides indirection -``` - -## Human experience impact - -### Positive - -- **Clear error messages.** Because types are nominal and simple, error messages can say "expected `Celsius`, got `Fahrenheit`" rather than showing structural type dumps. -- **Minimal annotation burden.** Only function signatures require full annotation; local variables and closures are inferred. Humans write types where they matter (API boundaries) and skip them where they don't (implementation details). -- **Predictable behavior.** No implicit conversions, no SFINAE, no surprising type-level computation. If it compiles, the types mean what they say. -- **Refinement types catch bugs early.** `Port` being - `I64 when self >= 1 && self <= 65535` means invalid values are caught by the - checker or by explicit validation paths rather than being left as untyped - integers. - -### Negative - -- **Explicit annotation requirement.** Requiring full function signatures is more annotation than TypeScript or Python. However, this is a deliberate trade-off — signatures are the gravity center for both humans and Agents. -- **Nominal strictness.** Newtypes like `Celsius ≠ F64` require explicit conversions. This prevents accidental misuse but adds ceremony for simple cases. - -### Cognitive model - -The mental model for Spore's type system is: **"Types are contracts. Write them on the outside, let the compiler figure out the inside."** - -## Agent experience impact - -### Type-directed synthesis - -Rich type signatures give Agents strong guidance for code generation: - -- Parameter types constrain what values are available. -- Return types constrain what the body must produce. -- EffectSet constrains which side-effectful functions may be called. -- Hole types (`Ty::Hole`) tell the Agent exactly what type to produce. - -### Structured type information - -The type system's type registry is designed for programmatic access: - -- `registry.functions` maps function names to `(param_types, return_type, effect_set)`. -- `registry.structs` maps struct names to field lists. -- `registry.types` maps enum names to variant lists. - -Agents can query this registry to discover available functions, match types, and plan synthesis strategies. - -### Error recovery - -`Ty::Error` allows the checker to continue after errors, producing multiple diagnostics in a single pass. This is critical for Agents — a single check run reveals all type errors, not just the first one. - -### Hole reporting - -The checker feeds the shared typed-hole protocol defined in SEP-0005. In practice, `sporec holes FILE --json` returns a batch object with `holes` plus `dependency_graph`, and `sporec query-hole FILE ?name --json` returns one hole object with the same inferred type, scope, and candidate information. This structured output is the primary input for Agent hole-filling strategies. - -## Structured representation / protocol impact - -### Type representation in diagnostics - -All types implement `Display` with a canonical format: - -| Type | Display | -| --------------------------------------- | ---------------------------------------- | -| `I32` (etc.) | `I32`, `I64`, … | -| `F64` (etc.) | `F64`, `F32`, … | -| `Bool` | `Bool` | -| `Str` | `Str` | -| `Unit` | `()` | -| `Never` | `Never` | -| `Tuple([τ₁, τ₂])` | `(...)` / tuple spelling used by printer | -| `Refined(τ, …)` | surface refinement form | -| `Named("Foo")` | `Foo` | -| `App("List", [I64])` | `List[I64]` | -| `Record([(x, I64), (y, F64)])` | `{ x: I64, y: F64 }` | -| `Fn([I64], Bool)`, effect set empty | `(I64) -> Bool` | -| `Fn([I64], Bool)`, effect set non-empty | `(I64) -> Bool uses [Net]` | -| `Var(3)` | `?T3` | -| `Hole("h1")` | `?h1` | -| `Error` | `` | - -### Snapshot hashes - -Function signatures (parameter types, return type, error set, EffectSet, cost -bound, generic constraints) are included in snapshot hashes. Body changes and -`@allows` annotations are excluded. For error sets, the hash policy is -intentionally conservative: written surface spelling still participates even when -two clauses are canonically equivalent. This ensures: - -- API-breaking changes require explicit `--permit`. -- Implementation refactoring does not break downstream consumers. - -### Machine-readable output - -Type diagnostics participate in the shared diagnostics protocol described in SEP-0006. This SEP relies on the canonical type rendering, stable codes, severities, and spans provided by that protocol rather than defining a competing JSON schema. - -```json -{ - "code": "E0301", - "severity": "error", - "message": "type mismatch: expected `I64`, found `Str`", - "primary_span": { - "file": "main.sp", - "range": { - "start": { "line": 5, "col": 12 }, - "end": { "line": 5, "col": 18 } - } - } -} -``` - -## Diagnostics impact - -### Error categories produced by the type checker - -| Category | Example message | -| ------------------------ | ------------------------------------------------------------------------------------ | -| Type mismatch | `type mismatch in function 'add': expected 'I64', got 'Str'` | -| Undefined variable | `undefined variable 'x'` | -| Arity mismatch | `function 'add' expects 2 arguments, got 3` | -| Missing effect | `missing effects [NetConnect]: caller does not declare them` | -| Missing propagated error | `function may raise [ParseError] which is not in the declared error set` | -| Redundant error item | `redundant error item 'ParseFailure': already covered by 'config.errors.ParseError'` | -| Non-exhaustive match | `non-exhaustive match: missing variant 'Triangle'` | -| Cannot negate type | `cannot negate type 'Str'` | -| Cannot apply `!` | `cannot apply '!' to type 'I64'` | -| Unknown field | `struct 'Point' has no field 'z'` | - -### Dual-channel diagnostics - -Every diagnostic is emitted as human-readable and machine-readable projections over the shared diagnostic objects described in SEP-0006. For the type system, those projections must preserve: - -- The **expected type** (from check mode / declared return type). -- The **actual type** (from synth mode / expression). -- The **context** (function name, expression location). -- **Suggestions** — functions in scope whose return type matches the expected type. - -### Hole diagnostics - -For each hole, the checker reports the type-system facts that feed the shared SEP-0005 hole object rather than inventing a second hole-specific schema in this SEP. - -```text -Hole ?h1 in function `example`: - expected type: I64 - available bindings: x: I64, y: Str - suggested fills: compute_value, default_int -``` - -## Drawbacks - -1. **Nominal rigidity.** The nominal-primary design means newtypes require explicit wrap/unwrap. This adds verbosity for simple delegation patterns. Anonymous records provide a structural escape hatch, but they cannot implement traits. - -2. **EffectSet representation.** Effect names are identifiers without separate static verification — a misspelled effect name is caught when matching against registered effects, not during type construction. - -3. **No higher-kinded types.** GATs + associated types cover most use cases, but abstracting over container kinds generically (functor-map over any container) requires either code generation or per-container implementations. - -4. **Refinement types are not yet fully specified.** L0-style aliases (`alias Port = I64 when …`) are specified here, but the full refinement vision in §4.11 (flow-sensitive narrowing, transitive proof obligations) is planned as a future extension. - -5. **Annotation overhead.** Requiring full function signatures is more annotation than TypeScript or Python. This is a deliberate trade-off — signatures are the gravity center for both humans and Agents. - -## Alternatives considered - -### Alternative 1: Structural typing as default (TypeScript / Go style) - -**Rejected.** Structural typing makes effect safety impossible — two structurally identical types could accidentally satisfy each other's effect requirements. It also produces confusing error messages when two semantically different types happen to share the same shape. - -**Decision:** Nominal-primary with anonymous structural records as an escape hatch. - -### Alternative 2: Hindley–Milner global inference - -**Rejected.** Full H-M inference infers function signatures, but this conflicts with Spore's "signatures are gravity centers" principle. Inferred signatures are: - -- Not visible to Agents reading code. -- Unstable under refactoring (adding a statement can change the inferred type). -- Hard to explain when inference fails. - -**Decision:** Bidirectional checking — signatures annotated, bodies inferred. - -### Alternative 3: Higher-kinded types - -**Rejected.** HKTs would allow abstracting over `Option`, `List`, `Result`, etc. generically. However: - -- Effects replace the primary HKT use case (monad-based effect sequencing). -- GATs cover 80% of the remaining use cases. -- HKT error messages are notoriously difficult to understand. -- Can be added later as an opt-in extension without breaking changes. - -**Decision:** No HKT. Use GATs + associated types for container abstraction. - -### Alternative 4: SMT-backed refinement types (Liquid Haskell style) - -**Rejected.** SMT solvers are: - -- Unpredictable (slight code changes can cause timeouts). -- Inscrutable (error messages say "could not prove P" with no guidance). -- Heavy (Z3 is a large dependency). - -**Decision:** L0 decidable predicates + L1 abstract interpretation. Covers practical needs (numeric bounds, null tracking, range narrowing) without solver unpredictability. - -### Alternative 5: Effect system via monads or algebraic effects - -**Rejected as primary mechanism.** Spore separates traits and effects instead of unifying them via monads or algebraic effects. This provides: - -- Clear distinction between "what does this type support" (traits) and "what does this context require" (effects). -- Reuse of the trait resolver and coherence checker for type interfaces. -- Simpler mental model for users. - -**Decision:** Traits ≠ Effects. `trait` defines type interfaces, `effect` defines external operations, `uses [Effect]` declares effect requirements. - -### Alternative 6: Row-polymorphic effect sets - -**Considered for future.** Making EffectSets row-polymorphic would allow: - -```spore -fn apply[C](f: Fn(x: I64) -> I64 uses C, x: I64) -> I64 uses C -``` - -This is compatible with the current design. Effect-set representation would need to be extended with effect variables. - -## Prior art - -### Rust - -Spore's type system is most directly influenced by Rust: - -- Nominal types with trait bounds. -- Associated types and GATs. -- Sealed enums with exhaustive pattern matching. -- No implicit conversions. - -**Differences from Rust:** No lifetime system (Spore currently targets Perceus-style reference counting with region optimization rather than a borrow checker), no borrow checker, effects replace `unsafe` blocks, refinement types replace some `debug_assert!` patterns. - -### Haskell / GHC - -- Bidirectional type inference with explicit signatures was pioneered in GHC's `OutsideIn(X)` solver. -- Type holes (`_` in GHC) directly inspired Spore's `?hole` syntax. -- Spore explicitly rejects HKT and typeclasses-as-module-system in favor of simpler nominal traits. - -### Liquid Haskell - -- Refinement types attached to base types. -- Spore adopts the refinement syntax (`alias Port = I64 when ...` / `type Port = I64 when ...`) but replaces the SMT backend with decidable predicates (L0) and abstract interpretation (L1). - -### TypeScript - -- Structural typing for anonymous records (Spore borrows this for the structural escape hatch). -- Spore rejects structural typing as the default due to effect safety concerns. - -### Elm - -- Elm-style error messages are the target for Spore's human-readable diagnostic channel. -- Elm's enforced exhaustive matching directly influenced Spore's sealed-enum design. - -### Bidirectional type checking literature - -- Pierce & Turner, "Local Type Inference" (2000). -- Dunfield & Krishnaswami, "Complete and Easy Bidirectional Typechecking for Higher-Rank Polymorphism" (2013). - -Spore implements a simplified version: no higher-rank polymorphism, no impredicativity. Signatures provide the "check" direction; expressions provide the "synth" direction. - -## Backward compatibility and migration - -### This is the initial type system specification - -Since this is the first formal type system specification, there is no older SEP to be backward-compatible with. The type grammar in §4.1 is the normative baseline. - -### Compatibility commitments - -1. **Type grammar stability.** The type constructors `I8`…`U64`, `F32`, `F64`, `Bool`, `Str`, `Unit`, `Never`, `Tuple`, `Refined`, `Named`, `App`, `Record`, `Fn`, `Var`, `Hole`, `Error` are stable. New constructors may be added but existing ones will not be removed or renamed without a new SEP. - -2. **EffectSet representation.** The identifier-based representation is stable. Future SEPs may introduce effect variables for row-polymorphic effects, but the concrete identifier-based API will remain supported. - -3. **Two-pass checking.** The `register_item` → `check_fn` architecture is stable. Future passes (e.g., trait resolution, cost checking) will be added after these two, not replace them. - -4. **Display format.** The canonical type display format (`I64`, `List[I64]`, `(I64) -> Bool uses [Net]`, `{ x: I64, y: F64 }`, etc.) is stable and may be relied upon by tools and diagnostics. - -### Migration path for future features - -| Feature | Migration strategy | -| ----------------------- | ----------------------------------------------------------------- | -| Refinement types (L0) | Extend semantics/predicate classes as needed | -| Index generics | Add an `Index` kind and extend `App` to accept Index arguments | -| Row-polymorphic effects | Extend EffectSet with effect variables alongside concrete strings | +Migration moves generic constraints into type parameter lists and preserves body +inference behavior. Public API hashes must treat the normalized inline-bound +form as the source of truth. ## Unresolved questions -1. **EffectSet and ErrorSet unification.** Currently, function type unification - treats effect and error sets as separately checked components. A future - type-system revision must decide whether higher-order function unification - should include those sets directly or keep the current call-site validation - split. - -2. **Trait and impl checker rollout.** §4.12 specifies the semantic direction - for trait method signatures, default methods, and `impl` blocks. The - remaining work is to align the checker phases with that model - without changing the SEP-0001 surface grammar. - -3. **Row-polymorphic effects.** Should effect sets support variables (for - example `uses [C]`) so higher-order functions can preserve their argument's - effect requirements? This is a possible advanced extension layered on top of - the flat effect-set model in SEP-0003. - -4. **Variance.** Generic type parameters need variance annotations or inference - for soundness if broader subtyping is introduced. This remains tied to the - future subtyping story. - -### Resolved questions - -The following questions from earlier drafts are now resolved by this specification: - -1. **Occurs check.** Resolved — the unifier includes an occurs check (§4.4). - Cyclic substitutions like `Var(0) ↦ List[Var(0)]` are rejected. - -2. **Never type semantics.** Resolved — `unify(τ, Never) = ok` for all `τ` - (§4.16). Never is handled as a bottom type directly within the unifier. - -3. **Error type representation.** Resolved — error sets are a component of - `Ty::Fn`, making it `Fn(params, ret, EffectSet, ErrorSet)` (§4.15). `?` - propagation uses canonical subset/union semantics, while signature hashing stays - conservative over the written error clause. - -4. **Generic syntax.** Resolved by SEP-0001: generic application uses square - brackets, for example `List[I64]` and `Result[T, E]`. Angle-bracket examples - are non-canonical and should be migrated. - -5. **L0 refinement representation.** Resolved by this specification: - `Ty::Refined` is the compiler representation for current L0 refinements. - Richer predicate classes may extend that model without reopening the surface - syntax. +1. Which refinement predicates should remain in the immediate decidable subset? +2. How should associated types display inside compact signature hovers? +3. Should trait aliases be permitted in inline bounds? diff --git a/seps/SEP-0003-effect-system.md b/seps/SEP-0003-effect-system.md index 5bae13d..c008174 100644 --- a/seps/SEP-0003-effect-system.md +++ b/seps/SEP-0003-effect-system.md @@ -16,1209 +16,274 @@ superseded_by: null # SEP-0003: Effect System -> **Executive Summary**: Defines Spore's effect system as a flat effect-set model with 10 built-in intent-oriented atomic effects (Console, FileRead, FileWrite, NetConnect, NetListen, Env, Spawn, Clock, Random, Exit) plus user-defined effects. The compositional model treats handlers as explicit discharge points: normal handlers, mock handlers, and Platform handlers are the same semantic object, with handled effects removed from a local residual set and handler implementation effects added back to the outer requirement. Concrete grammar is centralized in SEP-0001; this SEP specifies effect algebra, unified handler semantics, discharge rules, narrowing rules, and diagnostics. +> **Executive Summary**: Defines the signature model's `uses` effect surface. SEP-0003 owns atomic effect declarations, surface declarations, handlers, surface expansion, and effect checking. Non-effect constraints belong to `budget`, `properties`, or future tooling metadata rather than `uses`. ## Summary -This SEP introduces Spore's **effect system**: a compile-time mechanism that tracks how functions interact with the outside world. Every function declares — via a `uses [...]` clause — the set of _atomic effects_ it requires. The compiler verifies that a function body never exercises an effect absent from its declared set, auto-infers semantic properties (pure, deterministic, total) from that set, and uses set-inclusion as the basis for subtyping and effect narrowing. - -The built-in effect vocabulary is **intent-oriented**: each built-in effect answers "What does this code intend to do with the outside world?" Pure computation is the default state — no effect declaration is needed for it. Mutable state is tracked by the language semantics, not by built-in external effect names. In compiler internals and tooling protocols, these effect names form the effect set used for subset checks, platform ceilings, and machine-readable fields such as `effects`. - -The design is intentionally **flat and monomorphic**: effect sets are finite sets of atomic identifiers with no effect variables, no row types, and no effect polymorphism. Named aliases (`effect X = A | B`) provide ergonomics without adding expressiveness. Higher-order functions such as `map` accept only pure (`uses []`) closures; effectful iteration is expressed through `parallel_scope` + `spawn`. - -Concrete surface syntax for `effect`, `handler`, `perform`, and `handle ... with` is defined in SEP-0001. This SEP focuses on semantics, algebra, typing, protocol fields, and diagnostics. - ---- - -## Motivation - -Modern programs interleave pure computation with diverse side effects — file I/O, networking, mutable state, concurrency, randomness, process control. Without a disciplined tracking mechanism these effects become invisible at function boundaries, making it hard for both humans and automated agents to reason about what a function _can do_. - -### Problems addressed - -1. **Untracked side effects.** In most mainstream languages, any function can perform arbitrary I/O. A callee that silently writes to the filesystem or spawns threads violates the caller's assumptions. - -2. **Fragile purity assumptions.** Optimisations such as memoisation, common-subexpression elimination, and automatic parallelisation require proof that a function is pure. Without compiler support, purity is enforced only by convention. - -3. **Opaque higher-order functions.** When `map` or `fold` accept arbitrary closures, the caller loses all guarantees about the aggregate computation. A closure that performs network I/O inside `map` is a common source of performance surprises and non-determinism. - -4. **Effect escalation.** Concurrent sub-tasks should not silently acquire effects that their parent never held. Without a formal narrowing rule, library code can grant effects that the application developer never intended. - -5. **Agent-unfriendly code.** LLM-based code-generation agents filling Holes need machine-readable constraints on which effects are permissible at each program point. A formal effect set provides exactly this. - -### Design goals - -| Goal | Mechanism | -| ------------------------ | ---------------------------------------------------- | -| Explicit effect tracking | `uses [...]` clause on every function | -| Zero-cost purity | Omitting `uses` ≡ `uses []` (pure) | -| Composable aliases | `effect` keyword for named groups and aliases | -| Sound subtyping | Set inclusion on effect sets | -| Auto-inferred properties | `pure`, `deterministic`, `total` derived from `uses` | -| Agent integration | `available_effects` emitted in `HoleReport` JSON | - ---- - -## Guide-level explanation - -### Declaring effects on functions - -Every Spore function may carry a `uses` clause listing the atomic effects it requires: - -```spore -effect FileRead { - fn read_file(path: Str) -> Str ! IoError -} - -fn read_config(path: Str) -> Config ! IoError | ParseError -uses [FileRead] -{ - let content = perform FileRead.read_file(path) - parse_toml(content) -} -``` - -A function that interacts with the terminal declares `Console`: +Spore effects describe observable interactions with the outside world. A +function lists its ambient effect surface in `uses`: ```spore effect Console { - fn println(msg: Str) -> () -} - -fn greet(name: Str) uses [Console] { - perform Console.println("Hello, " + name) -} -``` - -If a function needs no effects it is _pure_ and the `uses` clause may be omitted entirely: - -```spore -fn add(a: I64, b: I64) -> I64 { - a + b + fn println(msg: Str) -> (); } -// equivalent to: fn add(a: I64, b: I64) -> I64 uses [] { a + b } -``` - -### Built-in atomic effects - -Spore ships with the following intent-oriented atomic effects. Each one answers: "What does this code intend to do with the outside world?" - -| Effect | Intent | Typical operations | -| ------------ | --------------------------------- | ----------------------------------------- | -| `Console` | User interaction (terminal I/O) | `println`, `eprintln`, `read_line` | -| `FileRead` | Persistent data access | `File.read`, `Dir.list` | -| `FileWrite` | Persistent data modification | `File.write`, `Dir.create`, `File.delete` | -| `NetConnect` | External communication (outbound) | `http.get`, `http.post`, `tcp.connect` | -| `NetListen` | Service provision (inbound) | `tcp.listen`, `http.serve` | -| `Env` | Configuration access | `Env.get`, `Env.vars` | -| `Spawn` | Subprocess management | `Cmd.exec`, `spawn { ... }` | -| `Clock` | Time-dependent computation | `now()`, `elapsed()` | -| `Random` | Non-deterministic computation | `random()`, `uuid()` | -| `Exit` | Process lifecycle control | `exit()`, `abort()` | - -`Exit` authorizes explicit process termination. Startup contracts, runtime exit -propagation, and host exit-code conversion are Platform/runtime concerns -specified in SEP-0008 rather than in this effect SEP. - -### Design rationale: intent-oriented built-in effects - -The guiding principle is: **each built-in effect answers "What does this code intend to do with the outside world?"** This leads to several deliberate design choices: - -1. **No `Compute` effect.** Pure computation is the default state — no effect declaration is needed. Every function can compute; effects describe what _additional_ external powers a function needs beyond pure computation. A function with `uses []` (or no `uses` clause) is pure and can compute freely. - -2. **No `StateRead`/`StateWrite`.** Mutable state is tracked by the language semantics, not by built-in external effect names. The built-in effects describe interactions with the _external world_ — the filesystem, the network, the terminal, the clock. Internal mutable state (for example, a local cache) is an implementation detail, not an intent to interact with the outside world. - -3. **`NetConnect`/`NetListen` instead of `NetRead`/`NetWrite`.** The old names described data direction, but the real intent distinction is _client vs server_. An HTTP client both reads and writes the network, but its intent is "connect to an external service." Similarly, a server both reads and writes, but its intent is "listen for incoming connections." - -4. **`Console` for terminal I/O.** `println("hello")` is user interaction, not file writing, even though stdout is technically a file descriptor. Distinguishing terminal I/O from filesystem I/O reflects a real difference in intent. - -5. **`Env` for environment variables.** Reading environment variables is configuration access, distinct from reading files. Separating `Env` from `FileRead` enables finer-grained control. -### Platform mapping: basic-cli - -The `basic-cli` Platform currently maps its package modules to built-in effects as follows: - -| Operation | Required effect | -| ------------------------------------------------------------------------------------------------------------ | --------------- | -| `basic_cli.stdout.print`, `basic_cli.stdout.println`, `basic_cli.stdout.eprint`, `basic_cli.stdout.eprintln` | `Console` | -| `basic_cli.stdin.read_line` | `Console` | -| `basic_cli.file.file_read`, `basic_cli.file.file_exists`, `basic_cli.file.file_stat` | `FileRead` | -| `basic_cli.file.file_write` | `FileWrite` | -| `basic_cli.dir.dir_list` | `FileRead` | -| `basic_cli.dir.dir_mkdir` | `FileWrite` | -| `basic_cli.env.env_get`, `basic_cli.env.env_set` | `Env` | -| `basic_cli.cmd.process_run`, `basic_cli.cmd.process_run_status` | `Spawn` | -| `basic_cli.cmd.exit` | `Exit` | - -### Defining effect aliases - -The `effect` keyword creates a **named alias** that expands into a flat set of atomic effects: - -```spore -effect FileIO = FileRead | FileWrite; -effect CLI = Console | FileRead | FileWrite | Env | Spawn | Exit; -effect Server = NetListen | FileRead | FileWrite | Clock | Random; -effect HttpClient = NetConnect | Clock; -``` - -Aliases expand recursively and flatten: - -```text -CLI → {Console, FileRead, FileWrite, Env, Spawn, Exit} -``` - -Aliases are purely syntactic sugar in `uses [...]`: after expansion, only atomic -effects remain. A small builtin alias hierarchy provides `IO`, `FileIO`, and -`NetIO` for convenience, but **same-module declarations shadow those builtin -names**. In other words, a local `effect IO = Console` or `effect IO { ... }` is -treated as that local declaration, not as the builtin filesystem/network bundle. - -### Using effects in practice - -```spore -effect HttpClient = NetConnect | Clock; - -fn query_api(url: Url) -> Data ! NetworkError -uses [HttpClient] +fn greet(name: Str) -> () +uses [Console] { - let response = http.get(url) - parse_json(response.body) + perform Console.println("hello " + name) } ``` -After alias expansion this is equivalent to `uses [NetConnect, Clock]`. - -`perform` and inline `on Effect.op(...)` arms are stricter: they target -**declared effect interfaces**, not aliases or undeclared pseudo-effect paths. -`uses [Alias]` may cover `perform Console.println(...)` after alias expansion, -but `perform Alias.println(...)` is not the canonical surface. - -### Pure closures in higher-order functions - -Higher-order combinators such as `map`, `filter`, and `fold` accept **only pure closures** — closures whose effect set is empty: +`uses` is an effect surface. Every item in a `uses` surface expression must +resolve to an atomic effect or to a named surface that expands to atomic +effects. -```spore -fn process_scores(scores: List[I64]) -> List[Str] { - scores - .filter(|s| s >= 60) // pure: (I64) -> Bool - .map(|s| format("Pass: {}", s)) // pure: (I64) -> Str -} -``` +## Motivation -Attempting to pass an effectful closure is a compile error: +Effects make external interactions visible at the signature boundary. They help +humans review code, help Agents avoid unavailable operations, and let Platforms +supply replaceable handlers. -```spore -fn bad_example(scores: List[I64]) -> List[Unit] -uses [FileWrite] -{ - // ❌ ERROR: map requires a pure closure, but this closure uses [FileWrite] - scores.map(|s| write_file("log.txt", s.to_string())) -} -``` +Effect checking should not absorb every non-type constraint. The signature model keeps +runtime interaction in `uses`, realization shape in `budget`, and semantic +requirements in `properties`. -### Effectful iteration with `parallel_scope` + `spawn` +## Guide-level explanation -When you need side effects during iteration, keep the effectful work explicit rather than hiding it inside a pure combinator closure: +### Declaring effects ```spore -fn fetch_all(urls: List[Url]) -> List[Response] ! NetworkError -uses [NetConnect, Spawn] -{ - parallel_scope { - fn spawn_all(remaining: List[Url]) -> List[Task[Response]] - uses [NetConnect, Spawn] - { - match remaining { - [] => [], - [url, ..rest] => [ - spawn { - // spawn body uses [NetConnect], ⊆ parent {NetConnect, Spawn} ✓ - http.get(url) - }, - ..spawn_all(rest), - ], - } - } - - spawn_all(urls).collect_results() - } +effect FileRead { + fn read(path: Path) -> Str ! IoError; } -``` - -Why does this type-check? - -1. `fetch_all` declares `uses [NetConnect, Spawn]`. -2. The local helper `spawn_all` is also declared with `uses [NetConnect, Spawn]`, so its recursive body may both spawn tasks and perform network requests inside those tasks. -3. `spawn { ... }` requires `Spawn ∈ {NetConnect, Spawn}` — satisfied. -4. Inside the spawn body, `http.get` requires `NetConnect`; `{NetConnect} ⊆ {NetConnect, Spawn}` — satisfied. - -### Auto-inferred properties - -The compiler automatically derives semantic properties from the declared effect set. No manual annotation is required: -| Declared `uses` | Inferred properties | -| -------------------------- | ---------------------------- | -| `uses []` (or omitted) | pure, deterministic, total\* | -| `uses [Console]` | ¬pure | -| `uses [FileRead]` | ¬pure, deterministic | -| `uses [Random]` | ¬pure, ¬deterministic | -| `uses [NetConnect, Spawn]` | ¬pure, deterministic | - -(\*total requires a separate termination analysis; see Reference-level explanation §5.4.) - -### Incomplete functions — missing `uses` declarations - -A function that calls operations requiring effects but does **not** declare a `uses` clause is an _incomplete function_. The compiler treats this as an error with a fix suggestion: - -```spore -fn save_data(data: Data) -> Unit { - write_file("out.txt", serialize(data)) +effect Clock { + fn now() -> Instant; } ``` -```text -error[effect-violation]: function body uses undeclared effect - --> src/storage.sp:1:1 - | - 1 | fn save_data(data: Data) -> Unit { - | ^^^^^^^^^ no `uses` clause declared - 2 | write_file("out.txt", serialize(data)) - | ^^^^^^^^^^ requires FileWrite - | - = declared: [] (pure — no `uses` clause) - = required: [FileWrite] - = excess: [FileWrite] - = help: add a `uses` clause to the function signature: - | - 1 | fn save_data(data: Data) -> Unit - 2 | uses [FileWrite] - | -``` - -Running `sporec --fixes` auto-inserts the missing `uses` clause. - -### No module-level `uses` declarations +### State primitive effects -Spore does **not** currently standardize module-level `uses` ceilings or carriers. Source files declare no ambient effect budget. Effect checking happens at: +A state primitive effect is a standard atomic effect whose purpose is to carry a +minimal state or event boundary without exposing user-level mutation. State +primitive effects are still ordinary atomic effects for `uses` resolution, +handler checking, and HoleReport effect context. -1. function signatures (`uses [...]`) -2. project / Platform boundaries +An effect belongs to the state primitive set only when it satisfies all of these +membership rules: -Diagnostics and tooling should therefore not synthesize or rely on `module X uses [...]` declarations. +1. It cannot be defined as a composition of other effects. +2. It carries one minimal state or event responsibility. +3. It is broadly useful across application, test, and tooling contexts. +4. A Platform package can provide a host handler and an in-memory mock handler. +5. Its operation set is small and stable enough to be part of the standard + effect surface. -### `@allows` — restricting Hole candidates +The initial state primitive set is defined by SEP-0009. Adding a new state +primitive effect requires a separate SEP because it changes the standard effect +taxonomy and the Platform conformance surface. -The `@allows` annotation constrains which functions an AI agent (or developer) may use to fill a Hole. In the shared SEP-0005 hole protocol, this shows up as a filtered `candidates` list rather than as a separate bespoke schema. +### Using effects ```spore -fn process_input(raw: Str) -> SafeInput ! ValidationError { - @allows[validate, sanitize] - ?clean_input -} -``` - -A representative machine-readable view is: - -```json +fn load(path: Path) -> Str ! IoError +uses [FileRead] { - "name": "clean_input", - "display_name": "?clean_input", - "expected_type": "SafeInput", - "candidates": [ - { - "name": "validate", - "type_match": 1.0, - "required_effects_fit": 1.0, - "overall": 1.0 - }, - { - "name": "sanitize", - "type_match": 1.0, - "required_effects_fit": 1.0, - "overall": 1.0 - } - ] + perform FileRead.read(path) } ``` -Without `@allows`, all functions matching the type and effect constraints may appear. With `@allows`, the candidate list is further filtered to only the named functions. - -### Pure recursive function example — fibonacci - -A pure recursive function requires no annotations: - -```spore -fn fibonacci(n: I64) -> I64 { - match n { - 0 => 0, - 1 => 1, - n => fibonacci(n - 1) + fibonacci(n - 2), - } -} -``` - -Compiler inference output: - -```text -uses [] -// auto-inferred: pure, deterministic -// total: structural recursion on n (decreasing) — provable -// cost: non-structural; compute ~ O(2^n) (SEP-0004) -``` - ---- - -## Reference-level explanation - -### 1. Atomic effect set - -Let **E** be the universe of atomic effects. Each element of **E** is an indivisible identifier representing one way a program may interact with the external world. - -An **effect set** _S_ is a finite subset of **E**: - -$$S \subseteq E, \quad |S| < \infty$$ - -The empty set `{}` denotes a pure function — no interaction with the outside world. - -### 2. Formal effect algebra - -Effect sets obey standard finite-set algebra: - -| Property | Formula | Consequence | -| ---------------- | ---------------------- | ------------------------------------------------------------ | -| Commutativity | {A, B} = {B, A} | Declaration order is irrelevant | -| Idempotence | {A, A} = {A} | Duplicate declarations collapse | -| Associativity | Nested aliases flatten | `[FileIO, NetConnect]` = `[FileRead, FileWrite, NetConnect]` | -| Identity element | {} (empty set) | The identity for union: S ∪ {} = S | - -#### Set operations - -| Operation | Symbol | Use | -| ------------ | ------- | -------------------------------------------- | -| Union | S₁ ∪ S₂ | Sequential composition, conditional branches | -| Subset | S₁ ⊆ S₂ | Subtype check, effect narrowing | -| Intersection | S₁ ∩ S₂ | Property inference | -| Difference | S₁ \ S₂ | Reserved; not currently exposed in syntax | - -### 3. Effect alias definition and expansion - -Given an alias definition: - -$$\texttt{effect } C = A_1 \mid A_2 \mid \ldots \mid A_n$$ - -In any `uses` declaration: - -$$\texttt{uses } [C] \equiv \texttt{uses } [A_1, A_2, \ldots, A_n]$$ - -Expansion is **recursive**: if any $A_i$ is itself an alias, it is expanded -until every element is an atomic effect. The result is always a flat set. - -The language reserves builtin aliases `IO`, `FileIO`, and -`NetIO` for convenience expansion, but those names are **not global keywords**: -same-module declared effects and effect aliases with the same names shadow the -builtin hierarchy. - -**No effect variables exist.** All effect sets must be fully determined at compile time. There is no `uses E` generic parameter and no effect polymorphism. - -### 3.1 Declared effects, imports, and `perform` - -The canonical checked path for `perform` is: - -1. the current effect set must contain the named effect after alias - expansion, and -2. the effect name must resolve to a declared effect/interface with a known - operation signature. - -That rule applies equally to inline handler arms such as -`on Console.println(msg) => ...`. - -Imported `pub effect` interfaces preserve their operation signatures across -module boundaries, so cross-module `perform Effect.op(...)` is typechecked -against the imported signature rather than against an untyped name stub. -Ambiguous imported same-name effects with different signatures are rejected. - -### 4. Function types and EffectSet encoding - -#### 4.1 Complete function type - -```text -(T₁, T₂, …, Tₙ) -> R uses S ! E₁ | E₂ | … -``` - -where: - -- `T₁ … Tₙ` — parameter types -- `R` — return type -- `S` — effect set (EffectSet) -- `[E₁ …]` — error type set - -#### 4.2 Internal representation - -In the type system, the function type carries an effect set: - -```text -(T₁, T₂, …, Tₙ) -> R uses S -``` - -where the effect components use a deterministically ordered set for consistent diagnostics and serialisation. - -#### 4.3 Shorthand - -When the effect set is empty the `uses` clause may be omitted: - -$$(T_1, T_2) \to R \equiv (T_1, T_2) \to R \ \textbf{uses}\ \{\}$$ - -### 5. Property auto-inference - -The compiler derives semantic properties from the `uses` set via the property-inference function **𝒫**: - -$$\mathcal{P}(\text{pure}, S) = \begin{cases} \text{true} & \text{if } S = \emptyset \\ \text{false} & \text{otherwise} \end{cases}$$ - -$$\mathcal{P}(\text{deterministic}, S) = \begin{cases} \text{true} & \text{if } S \cap \{\text{Clock, Random}\} = \emptyset \\ \text{false} & \text{otherwise} \end{cases}$$ - -$$\mathcal{P}(\text{total}, S) = \text{determined by a separate termination analysis (see §5.4)}$$ - -#### 5.0 Edge cases in the `pure` formula +The body may only perform effects included in the declared effect surface or +provided by a narrower local handler context. Effect operations are addressed by +qualified names such as `Console.println` or `FileRead.read`. -The complete rule: `𝒫(pure, S) = true` iff `S = ∅`. Pure computation requires no declared effect — it is the default. `Spawn` is not pure-compatible: creating schedulable work has observable concurrency and scheduling consequences even when the spawned body is deterministic. Determinism remains a separate property. - -| Effect set S | pure? | Rationale | -| -------------- | ----- | -------------------------------------------------------------------- | -| `{}` | true | No effects at all | -| `{Spawn}` | false | `spawn` introduces observable concurrency/scheduling behavior | -| `{Console}` | false | Console interacts with the terminal — an external I/O channel | -| `{Clock}` | false | Clock reads the external world (system time) | -| `{Random}` | false | Random reads external entropy | -| `{Env}` | false | Env reads the process environment — an external configuration source | -| `{Exit}` | false | `exit` terminates the process — an observable external effect | -| `{NetConnect}` | false | Outbound network access is external I/O | - -#### 5.1 Implication chain - -$$\text{pure} \implies \text{deterministic}$$ - -A pure function (`uses {}`) trivially satisfies the deterministic condition because the empty set has no intersection with `{Clock, Random}`. - -#### 5.2 Properties that cannot be statically inferred - -**Idempotency** cannot in general be derived from an effect set. It may be annotated via doc-comment: +### Surface declarations ```spore -/// @idempotent -fn sync_user(user_id: UserId) -> SyncResult ! NetworkError -uses [NetConnect, FileRead] -{ ... } -``` - -**Totality** is provable by the compiler for structurally recursive functions (the recursive argument strictly decreases). Other functions require an explicit `@unbounded` annotation: +surface CliIO = [Console, FileRead, FileWrite] -```spore -/// @unbounded -fn event_loop() -> Never -uses [NetListen, Console] +fn run(path: Path) -> () ! IoError +uses [CliIO] { - loop { handle_next_event() } + ?run_body } -``` - -### 6. Subtyping and effect narrowing - -#### 6.1 Subtype rule -Effect sets induce subtyping via set inclusion. Function types are **contravariant** in their effect parameter: - -$$S_1 \subseteq S_2 \implies (\tau \to \rho \ \textbf{uses}\ S_1) <: (\tau \to \rho \ \textbf{uses}\ S_2)$$ - -A function that requires _fewer_ effects is more general and can be used wherever a more-capable function is expected. - -```text - S₁ ⊆ S₂ -───────────────────────────────────────── [CAP-SUB] -(T → R uses S₁) <: (T → R uses S₂) -``` - -As a corollary, pure functions are subtypes of all function types: - -```text -───────────────────────────────────────── [CAP-PURE] -(T → R uses {}) <: (T → R uses S) ∀ S -``` - -#### 6.2 Effect narrowing in `parallel_scope` - -A child task's effect set must be a subset of its parent's: - -$$S_{\text{child}} \subseteq S_{\text{parent}}$$ - -This guarantees that sub-tasks never acquire effects beyond those granted to their parent. - -### 7. Typing judgement - -The standard judgement form is: - -$$\Gamma;\ S \vdash e : T$$ - -Read: "Under type context Γ and effect set S, expression e has type T." - -#### Core rules - -```text -Γ; S ⊢ e₁ : T₁ Γ; S ⊢ e₂ : T₂ -──────────────────────────────────────── [SEQ] -Γ; S ⊢ (e₁; e₂) : T₂ - - -Γ; S ⊢ f : (T → R uses S_f) Γ; S ⊢ x : T S_f ⊆ S -─────────────────────────────────────────────────────────── [APP] -Γ; S ⊢ f(x) : R - - -──────────────────────────── [PURE-LITERAL] -Γ; S ⊢ 42 : I64 ∀ S -``` - -### 8. Effect composition rules - -When multiple expressions are combined the compiler computes the composite effect set: - -| Composition form | Effect computation | -| -------------------------------------- | -------------------------------------------- | -| `A; B` | S_A ∪ S_B | -| `if c then A else B` | S_c ∪ S_A ∪ S_B | -| `match x { p₁ => A, p₂ => B, … }` | S_x ∪ S_A ∪ S_B ∪ … | -| `f(x)` where f `uses S_f` | Requires S_f ⊆ S_scope | -| `spawn { body }` where body `uses S_b` | Requires `Spawn ∈ S_scope` and S_b ⊆ S_scope | -| `let x = e₁ in e₂` | S_e₁ ∪ S_e₂ | - -Formal rules for the two most important cases: - -```text -Γ; S ⊢ A : T₁ Γ; S ⊢ B : T₂ -────────────────────────────────── [SEQ-CAP] -effects(A; B) = S_A ∪ S_B - - -Γ; S ⊢ c : Bool Γ; S ⊢ A : T Γ; S ⊢ B : T -──────────────────────────────────────────────────── [COND-CAP] -effects(if c then A else B) = S_c ∪ S_A ∪ S_B - - -f : (T → R uses S_f) S_f ⊆ S_scope -────────────────────────────────────── [CALL-CAP] -calling f in scope S_scope is allowed - - -Spawn ∈ S_scope S_body ⊆ S_scope -────────────────────────────────────── [SPAWN-CAP] -Γ; S_scope ⊢ spawn { body } : Task[T] -``` - -> **Note on `spawn`.** A `spawn { ... }` expression still requires `Spawn` in the surrounding `uses` set even though the child body may run later. Creating schedulable work is therefore **not** pure and cannot be smuggled through APIs that require `uses []` closures such as `map`. - -### 8.1 Handler discharge semantics - -Handlers are the effect-system's **discharge operator**. A handler declaration -names the effects it covers and the implementation effects it may itself use: - -```spore -handler Name(params?) handles [EffectA, EffectB] uses [ImplEffects] { - impl EffectA { ... } - impl EffectB { ... } -} -``` - -This is the preferred surface shape for the current wave. Normal handlers, -mock handlers used in tests, and Platform-installed handlers are all instances -of this same semantic form; "Platform handler" is a deployment role, not a -distinct language feature. - -Let: - -- `S_body` be the effect set collected from the handled block before discharge -- `H` be the union of effects listed in active handler declarations -- `S_impl` be the union of all handler implementation effect sets - -Then the enclosing residual effect set is: - -```text -residual(handle e with h̄) = (S_body \ H) ∪ S_impl -``` - -Intuition: - -1. effects covered by the active handlers stop leaking outward; -2. any effects required to _run the handlers themselves_ still count; and -3. unhandled effects remain visible to the outer scope. - -This rule is purely semantic. Users still declare ordinary `uses [...]` -signatures; there is no separate user-facing "effect subtraction" syntax. - -#### Coverage and duplicate-match rules - -A handler scope is well-formed only if all of the following hold: - -1. **Interface completeness.** For every `Effect` listed in `handles [...]`, - the handler provides an `impl Effect { ... }` block covering the declared - operations of that effect interface. -2. **Unique local match.** Within one `with` block, the same `Effect.op` - cannot be matched by two sibling handlers or two sibling inline arms. -3. **Innermost wins.** If nested handler scopes both cover an operation, the - innermost matching handler is chosen. -4. **Explicit leakage.** A `perform Effect.op(...)` not covered by the active - handler stack remains in the residual effect set and must still be justified - by the enclosing `uses [...]`. - -Inline `on Effect.op(...) => ...` arms are semantic sugar for anonymous -single-scope handlers and therefore obey the same discharge and duplicate-match -rules. - -### 9. Effect checking algorithm - -The compiler performs effect checking during type-checking as a single pass: - -1. **Alias expansion.** Every `uses` clause and every `effect` definition is recursively expanded to a flat set of atomic effects. - -2. **Body effect collection.** For each function body, the compiler walks the expression tree and collects the union of effects required by every sub-expression (using the composition rules in §8). - -3. **Subset check.** The collected set `S_body` is checked against the declared set `S_declared`: - -$$S_{\text{body}} \subseteq S_{\text{declared}}$$ - -If the check fails, the compiler emits a `effect-violation` diagnostic listing the excess effects. - -4. **Closure inference.** For closures, the compiler infers the minimal effect set from the closure body. This inferred set is used for subtype checking when the closure is passed to a higher-order function. - -5. **`parallel_scope` narrowing.** Inside a `parallel_scope` block, each `spawn` body is checked to ensure its collected effects are a subset of the enclosing scope's declared set. - -### 10. Closure effect capture - -A closure defined within a context with effect set _S_ has an inferred effect set _S'_ where _S'_ ⊆ _S_. The inference is determined by the effects actually exercised in the closure body: - -```spore -fn example() -> Unit -uses [FileRead, NetConnect] -{ - // Inferred type: (Str) -> Data uses [NetConnect] - let fetch_fn = |url| http.get(url) - - // Inferred type: (I64) -> I64 uses [] (pure) - let double = |x| x * 2 -} -``` - -### 11. Interaction with the Hole system - -When the compiler encounters a Hole (`?name`), the shared SEP-0005 hole object includes the effect set available at that program point under the field name `available_effects`: - -```json +fn app(path: Path) -> () ! IoError +uses [CliIO, Clock] { - "name": "fetch_logic", - "display_name": "?fetch_logic", - "expected_type": "Data", - "bindings": { - "url": "Url", - "timeout": "Duration" - }, - "available_effects": ["NetConnect"], - "cost_budget": { - "budget_total": 5000, - "cost_before_hole": 0, - "budget_remaining": 5000 - }, - "candidates": [ - { - "name": "http.get", - "type_match": 1.0, - "required_effects_fit": 1.0, - "overall": 1.0 - } - ] + ?app_body } ``` -Filling a Hole is subject to: - -$$S_{\text{fill}} \subseteq S_{\text{available}}$$ - -The Hole system filters candidate functions so that only those whose `uses S` satisfies `S ⊆ S_available` appear in the suggestion list. - -#### 11.1 Hole effect-violation diagnostic - -When an agent or developer fills a Hole with code that exceeds the available set, the compiler emits a detailed diagnostic: - -```text -ERROR [effect-violation] Hole ?fetch_logic filled code uses unauthorised effects: - --> src/service.sp:15:5 - | -15 | ?fetch_logic // filled with: fetch_and_save(url) - | ^^^^^^^^^^^^ hole fill exceeds available effects - | - = available effects: [NetConnect] - = fill code requires: [NetConnect, FileWrite] - = excess effects: [FileWrite] - = help: either add FileWrite to the enclosing function's `uses` clause, - or choose a candidate that only requires [NetConnect] -``` - -### 12. Interaction with the cost model +Surfaces expand to finite sets of atomic effects. They are not sum types, +logical OR, or error unions. -The cost model maintains a **four-dimensional cost vector**: `(compute, alloc, io, parallel)`. - -| Dimension | Abbreviation | Meaning | Unit | -| ----------- | ------------ | --------------------------------- | --------------------------- | -| Compute | `C` | CPU operation steps | op (operation) | -| Allocation | `A` | Heap memory allocation | cell (abstract memory unit) | -| I/O | `W` | Side-effect / external call count | call | -| Parallelism | `P` | Parallel execution width | lane | - -Effect sets provide hard upper-bound constraints on these cost dimensions: - -| Condition | Cost dimension constraint | -| ------------------------------------------------------------- | ------------------------------------- | -| `uses {}` | `io = 0` (guaranteed no I/O overhead) | -| S ∩ {NetConnect, NetListen, FileRead, FileWrite, Console} ≠ ∅ | `io > 0` possible | -| `Spawn ∈ S` | `parallel > 0` possible | - -The relationship is a **necessary condition**: if the effect set excludes all I/O effects, the cost model's I/O dimension is provably zero. - -$$S \cap \{\text{NetConnect, NetListen, FileRead, FileWrite, Console}\} = \emptyset \implies \text{cost}_{\text{io}} = 0$$ - -### 13. Effects as typed constructs - -Effects are not merely string tags — each effect is a **typed construct** whose methods define the operations available when the effect is in scope: +### Handlers ```spore effect Console { - fn println(msg: Str) -> () - fn eprintln(msg: Str) -> () - fn read_line() -> Str ! IoError -} - -effect FileRead { - fn read_file(path: Str) -> Str ! IoError - fn list_dir(path: Str) -> List[Str] ! IoError -} - -effect FileWrite { - fn write_file(path: Str, data: Str) -> () ! IoError - fn create_dir(path: Str) -> () ! IoError - fn delete(path: Str) -> () ! IoError + fn println(msg: Str) -> (); } -effect Env { - fn get(name: Str) -> Option[Str]; - fn vars() -> List[(Str, Str)]; +effect Output[T] { + fn emit(value: T) -> (); } -``` - -Declaring `uses [FileRead]` authorizes the effect, but operations remain explicit: `perform FileRead.read_file(path)` dispatches to the active `FileRead` handler. Canonical semantics require the corresponding `effect` interface to be declared explicitly; undeclared pseudo-effect paths are compatibility-only. - -Effect aliases are part of the committed surface: - -```spore -effect FileIO = FileRead | FileWrite; -``` - -This expands semantically to the union of the two effects and does not define a new operation surface of its own. -### 14. Effect handlers - -Effect handlers provide concrete implementations for effect operations, -enabling testability, mockability, and platform abstraction. Spore uses a -**unified handler model**: ordinary handlers, mock handlers, and Platform -handlers all use the same declaration shape and discharge rules. - -#### Handler definition - -```spore -handler MockConsole(output: List[Str]) handles [Console] uses [] { +handler MockConsole handles [Console] uses [Output[Str]] { impl Console { - fn println(msg: Str) -> () { self.output.push(msg) } - fn read_line() -> Str ! IoError { "mock input" } - } -} - -handler FixtureFiles(files: Map[Str, Str]) -handles [FileRead] -uses [] -{ - impl FileRead { - fn read_file(path: Str) -> Str ! IoError { - self.files.get(path).unwrap_or("") - } - - fn list_dir(path: Str) -> List[Str] ! IoError { - [] + fn println(self, msg: Str) -> () { + perform Output.emit(msg) } } } -``` - -The first example is a mock handler. The second shows the same declaration -shape applied to a different effect family. A Platform package installs -handlers using the same model; the difference is only _where_ the handler -instance comes from (project startup / adapter wiring) rather than any special -Platform-only semantics. - -#### Handler binding - -The canonical handler form is `handle { ... } with { ... }`. The `with` block -may contain named handler installations via `use HandlerName(...)` and/or -inline effect arms via `on Effect.op(...) => ...`. Installing a named handler -discharges the effects listed in that handler's `handles [...]` clause and -re-exports the handler's own `uses [...]` as residual obligations of the outer -scope. Duplicate matches for the same `Effect.op` inside one `with` block are -errors. - -```spore -// Bind a single named handler -handle { - greet("world") -} with { - use MockConsole(output: []) -} -// Bind multiple named handlers handle { - app.run() + greet("spore") } with { - use FixtureFiles(files: fixture_files) - use MockConsole(output: []) -} - -// Inline handler arms remain available -handle { - perform Console.println("hello") -} with { - on Console.println(msg) => {} + use MockConsole {} } ``` -#### User-defined effects +Handlers discharge or reinterpret effects inside a lexical scope. The example +omits the `Output[Str]` handler; the enclosing test or Platform scope must +install it explicitly. -Users may define their own effects: +### Handler state policy -```spore -effect RateLimit { - fn check_limit(key: Str) -> Bool -} +Handler fields are immutable runtime configuration. Handler method bodies may +read `self` and fields on `self`, but they may not assign to `self.field` or +otherwise update handler instance payload. Spore does not add `mut`, `mut self`, +or mutable handler fields for stateful handlers. -handler FixedDecisionRateLimit(allowed: Bool) handles [RateLimit] uses [] { - impl RateLimit { - fn check_limit(key: Str) -> Bool { - self.allowed - } - } -} -``` +Stateful handler use cases route through state primitive effects such as +`Cell`, `Output`, `Map`, `Clock`, and `Random`. A handler that needs state lists +the relevant primitive effects in its own `uses` clause and performs those +effects in method bodies. ---- +### Non-effect requirements -## Human experience impact +Properties, checker guidance, and Agent-generation requirements do not appear in +`uses` surface expressions. They should be expressed through `properties`, +`budget`, package metadata, or future tooling metadata owned by a separate SEP. -### Readability - -The `uses` clause makes a function's external interactions visible at a glance. Developers reading unfamiliar code can immediately determine whether a function touches the network, writes files, or is pure without inspecting its body. +## Reference-level explanation -### Learnability +### Effect set -- **Zero-annotation start.** Newcomers write pure functions with no annotation at all. The `uses` clause appears only when the first side effect is introduced. -- **Flat model.** There are no effect variables, row types, or higher-kinded constructs to learn. The mental model is "list the things you need." -- **Familiar concept.** The effect set is analogous to permission declarations in mobile OSes (Android manifest, iOS entitlements), a concept most developers already understand. +Each checked body has an available effect set `E_available`. A `perform` +operation requiring effect `E` is valid when `E` is in the available set after +surface expansion and local handler narrowing. -### Ergonomics +### Surface resolution -- Effect aliases (`effect CLI = Console | FileRead | FileWrite | ...`) reduce boilerplate for common groups. -- Auto-inferred properties eliminate the need for manual `pure` / `deterministic` annotations. -- The compiler provides actionable diagnostics when a function body exceeds its declared effects. +The compiler resolves names in a `uses` surface expression into: -### Potential friction +- atomic effects owned by this SEP; +- named surfaces that expand to atomic effects. -- Developers accustomed to unrestricted side effects may find the discipline burdensome initially. -- The prohibition on effectful closures in `map`/`filter` requires learning the `parallel_scope` + `spawn` pattern. +Unknown names are diagnostics. Surface expansion is unordered, duplicate-free, +and recursive cycles are diagnostics. ---- +State primitive effects resolve as atomic effects. Their primitive status +affects standard-library and Platform obligations, not the surface-expansion +algorithm. -## Agent experience impact +### Handler checking -### Structured effect information in HoleReport +A handler targets a surface expression and must implement every operation of the +atomic effects it claims to discharge. Handler methods live inside an +`impl Effect { ... }` block, write `self` as the first parameter, use ordinary +function typing, and may declare their own required effects. The receiver is +read-only. -LLM-based agents filling Holes receive the `available_effects` field in the shared SEP-0005 hole object. This enables agents to: +Handler instances are lexical and task-local. Installing a handler with +`handle ... with` affects only the dynamic extent of that expression inside the +installing task. Spawned or sibling tasks do not inherit the handler instance +unless that handler is installed in their own dynamic extent. -1. **Constrain code generation.** The agent knows which operations are permissible and avoids generating code that would fail effect checks. -2. **Filter candidate functions.** Only functions whose `uses S` satisfies S ⊆ S_available are presented as candidates. -3. **Self-verify before submission.** The agent can compare its generated code's effect requirements against the available set before proposing a fill. +### Interaction with properties -### Deterministic verification +Effect purity and determinism facts may be inferred and surfaced as evidence, +but source properties remain in the `properties` block and are lowered by +SEP-0006. -Because effect checking is a simple set-inclusion test, agents can perform the check locally without invoking the full compiler. This enables tight generate-verify loops. +## Human experience impact -### Reduced hallucination surface +A reader can inspect a function's `uses` surface expression to know what +outside-world interaction it depends on. Effect names stay explicit without +mixing runtime behavior with checker-only guidance. -The explicit effect set narrows the space of valid completions, reducing the likelihood that an agent generates plausible-looking but effect-violating code. +## Agent experience impact ---- +HoleReport exposes `effect_context`, allowing Agents to avoid proposing fills +that require unavailable effects. ## Structured representation / protocol impact -### Function type serialisation - -The canonical serialised form of a function type includes the EffectSet: - -```json -{ - "kind": "Fn", - "params": ["I64", "I64"], - "return": "I64", - "effects": [], - "errors": [] -} -``` - -```json -{ - "kind": "Fn", - "params": ["Url"], - "return": "Response", - "effects": ["NetConnect"], - "errors": ["NetworkError"] -} -``` +Effect checking emits normalized effect metadata: -### Effect alias registry - -All effect alias definitions are collected into a structured registry available to tooling: - -```json -{ - "aliases": { - "FileIO": ["FileRead", "FileWrite"], - "CLI": ["Console", "FileRead", "FileWrite", "Env", "Spawn", "Exit"], - "Server": ["NetListen", "FileRead", "FileWrite", "Clock", "Random"], - "HttpClient": ["NetConnect", "Clock"] - } -} +```text +EffectContext +├── declared_surface +├── expanded_effects[] +├── active_handlers[] +└── discharged_effects[] ``` -### LSP extensions - -The language server protocol integration should expose effect information through the shared hover and diagnostics pipeline described in SEP-0006: - -- Effect set in hover information for functions. -- Inferred properties (`pure`, `deterministic`, `total`) in hover tooltips. -- Quick-fix suggestions when an effect violation is detected (e.g., "Add `FileWrite` to the `uses` clause"). - ---- +SEP-0005 embeds this context in HoleReport. SEP-0006 may embed it in evidence. +Handler fields are instance payload and do not participate in `signature_hash`, +`intent_hash`, or `property_hash`. Hashes cover callable boundaries and intent +metadata; handler payload values are runtime configuration for a specific +installation. ## Diagnostics impact -### New diagnostics +Effect diagnostics use `F0xxx` codes: -| Code | Severity | Message template | -| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `effect-violation` | Error | Function body uses effect `{cap}` not declared in `uses` clause. Declared: `{declared}`. Required: `{required}`. Excess: `{excess}`. | -| `cap-closure-violation` | Error | Closure passed to `{fn_name}` must be pure (`uses []`), but it uses `{caps}`. | -| `cap-spawn-missing` | Error | `spawn` expression requires `Spawn` effect, but current scope declares `uses {scope_caps}`. | -| `cap-narrowing-violation` | Error | Spawn body uses `{child_caps}` which is not a subset of parent scope `{parent_caps}`. Excess: `{excess}`. | -| `cap-unknown` | Error | Unknown effect `{name}`. Did you mean `{suggestion}`? | -| `cap-alias-cycle` | Error | Effect alias `{name}` contains a cycle: `{cycle_path}`. | -| `cap-redundant` | Warning | Effect `{cap}` is declared but never used in the function body. | - -### Diagnostic quality guidelines - -1. **Always show the excess set.** When a subset check fails, display exactly which effects are missing from the declared set. -2. **Suggest fixes.** If the fix is to add an effect to the `uses` clause, provide a machine-applicable quick-fix. -3. **Show expansion.** When an alias is involved, show the expanded form so the user understands which atomic effect caused the violation. - -Example diagnostic: - -```text -error[effect-violation]: function body uses undeclared effect - --> src/app.sp:12:5 - | -10 | fn process(data: Data) -> Result -11 | uses [FileRead] - | ^^^^^^^^ declared effects -12 | write_file("out.txt", transform(data)) - | ^^^^^^^^^^ requires FileWrite - | - = declared: [FileRead] - = required: [FileRead, FileWrite] - = excess: [FileWrite] - = help: add `FileWrite` to the `uses` clause: - | -11 | uses [FileRead, FileWrite] - | ^^^^^^^^^^^ -``` - ---- +- unknown effect +- operation performed outside available effect set +- missing handler method +- handler effect escape +- platform does not provide required effect ## Drawbacks -1. **Annotation overhead.** Functions with many effects require verbose `uses` clauses. Effect aliases mitigate this but do not eliminate it entirely. - -2. **No effect polymorphism.** The deliberate omission of effect variables means that generic middleware functions (e.g., `with_timeout`, `retry`) cannot abstract over arbitrary effect sets. Each concrete instantiation must list its effects explicitly. This may lead to some code duplication in highly generic library code. - -3. **Pure-only closures in combinators.** Requiring `map`/`filter`/`fold` to accept only pure closures is restrictive. The `parallel_scope` + `spawn` workaround is more verbose and requires understanding structured concurrency. - -4. **No effect subtraction.** Users cannot write `uses [All \ Spawn]` ("everything except Spawn"). This means that functions needing nearly all effects must enumerate them individually. - -5. **Alias is expansion only.** Effect aliases do not create new abstract effects. This prevents hiding implementation details behind an alias boundary — the caller always sees the expanded set. - -6. **EffectSet representation.** Using identifier-based effect sets is simple but offers no compile-time interning or efficient bitset operations. This may need revisiting for large-scale codebases with many effects. - ---- +Using a separate `surface` declaration adds one more noun to the language. +That cost is acceptable because it keeps `effect` for atomic protocols and keeps +`uses` free of overloaded `|` syntax. Tools that need non-effect guidance still +require a separate surface rather than piggybacking on `uses`. The benefit is +that the language effect model stays clear and handler checking remains local. ## Alternatives considered -### 1. Effect polymorphism (effect variables / row types) - -Languages like Koka, Eff, and Frank support effect variables that allow abstracting over effect sets: - -```text -fn with_timeout[E](f: () -> T uses E) -> T uses [E, Clock] -``` - -**Why rejected:** Effect polymorphism dramatically increases type-inference complexity and produces error messages that are difficult for non-experts to understand. Spore prioritises approachability and agent-friendliness over maximum expressiveness. The flat-set model covers the vast majority of practical use cases. Effect variables may be reconsidered as a future opt-in advanced feature. - -### 2. Hierarchical effect model - -Some systems organise effects into a hierarchy (e.g., `IO > FileIO > FileRead`). A function declaring `uses [IO]` would implicitly have all sub-effects. - -**Why rejected:** Hierarchies introduce ordering disputes (is `Random` under `IO`?), make alias expansion non-trivial, and complicate subtype checks. The flat model avoids these issues entirely. - -### 3. `with` clause for manual property annotation - -An earlier design included a `with [pure, deterministic]` clause for manual property declaration. +### Broader `uses` surface -**Why rejected:** Properties can be reliably inferred from the `uses` set (see §5). The `with` clause added redundancy and a maintenance burden — if the `uses` set changed, the `with` clause could become inconsistent. +Rejected because mixing effects with checker or Agent guidance made `uses` +an unclear heterogeneous bucket. -### 4. Effect handlers +### Separate checker keyword in core syntax -Algebraic effect handlers (as in Koka or OCaml 5) allow effects to be intercepted and reinterpreted at runtime. - -**Why accepted:** Effect handlers provide critical benefits that outweigh their implementation cost: - -1. **Testability** — Mock handlers enable deterministic testing of effectful code without special test frameworks. -2. **Mockability** — Any effect can be replaced with a mock implementation at the call site. -3. **Platform abstraction** — The same user code runs on different platforms by swapping handlers (e.g., browser vs. server filesystem). -4. **Colorless functions** — Unlike async/await, effect handlers do not bifurcate the function space. - -Spore's handler model is intentionally simpler than full algebraic effects: -handlers are lexical, non-resumable, and one-shot. A matching handler arm -computes the value of the corresponding `perform` expression directly; there is -no continuation capture or `resume()` path in canonical semantics. -Discharge is explicit: handled effects are removed from the local residual set, -while handler implementation effects remain visible to the enclosing scope. -Normal, mock, and Platform handlers all follow this same rule. - -Syntax: `effect` defines operations, `handler` provides implementations, `handle ... with` binds handlers at call sites. See §13-14 for full details. - -### 5. Monadic effects (Haskell IO monad) - -Encoding effects in the type system via monads (e.g., `IO a`, `State s a`). - -**Why rejected:** Monad transformers are notoriously difficult to compose and produce deeply nested types. The flat effect set is simpler and more intuitive for the target audience. - ---- +Deferred because checker-specific guidance needs its own design rather than a +second ad hoc signature list in SEP-0003. ## Prior art -| System | Approach | Relation to this SEP | -| ----------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| **Koka** | Algebraic effects with row-polymorphic effect types | Spore takes the same "effects in the type" philosophy with flat sets and simplified handlers (no continuations) | -| **Eff** | First-class algebraic effects and handlers | Spore adopts simplified handlers (no continuations); effects are statically checked with runtime handler dispatch | -| **Rust** | No built-in effect system; `unsafe` is the only effect marker | Spore generalises `unsafe` to a full effect vocabulary | -| **Haskell** | `IO` monad, mtl-style monad transformers | Spore replaces monadic encoding with flat set annotation | -| **OCaml 5** | Algebraic effects for concurrency | Spore's `Spawn` effect is analogous; Spore adopts simplified handlers without continuations | -| **Scala (ZIO)** | Environment type `R` in `ZIO[R, E, A]` | Similar in spirit; ZIO's `R` is an intersection type, Spore uses a flat set | -| **Unison** | Ability types (algebraic effects) | Close conceptual ancestor; Spore simplifies by removing polymorphism | -| **Android Manifest** | Permission declarations | Same "declare what you need" philosophy at the OS level | -| **Wasm Component Model** | Import/export effects | Static effect declaration before instantiation | -| **Java (checked exceptions)** | `throws` clause | Spore's `uses` is analogous but tracks effects rather than error types | - ---- +Koka influenced algebraic effects. Roc influenced platform-provided handlers. +Rust influenced explicit trait boundaries, though Spore separates traits from +effects. ## Backward compatibility and migration -### This is a new language - -Spore is a new language and this SEP defines a core feature of its type system. There is no pre-existing code to migrate. - -### Interaction with SEP-0002 - -This SEP depends on SEP-0002 (Type System). The effect set is integrated into the function type representation: `Fn(params, ret, S)` where `S` is the set of required effect names. - -### Future extensibility - -- **New atomic effects.** New effects (e.g., `GpuAccess`, `Audit`) can be added to the universe **E** without breaking existing code — functions that do not use them are unaffected. -- **Platform effect ceilings.** A future SEP on the platform system may define module-level effect ceilings. Functions within such modules would be required to have `uses S` where S ⊆ S_platform_ceiling. -- **Effect variables (possible future SEP).** If demand arises for effect polymorphism, it can be introduced as an opt-in feature layered on top of the flat-set foundation without invalidating existing code. - ---- +Existing atomic effect declarations stay conceptually valid. Legacy shorthand +forms such as `effect IO = A | B` should migrate to `surface IO = [A, B]`. +Signatures that used `uses [A, B]` for effects continue to map directly, though +surface expressions may now mix atomic effects and named surfaces. Non-effect +names that were previously placed in `uses` should move to properties, package +metadata, or a future tooling surface. ## Unresolved questions -### 1. Effect identity, scoping, and imports - -Third-party packages may define new atomic effects. The remaining design -question is how those names are made globally unambiguous across packages: -qualified module paths, package-qualified names, or another identity layer tied -to the module/package system in SEP-0008. - -### 2. Fine-grained effect policy - -The language-level model currently uses coarse intent-oriented effects such as -`FileRead`, `FileWrite`, `Console`, and `NetConnect`. More granular policies -such as path-scoped filesystem access or separate console read/write permissions -remain a future extension. SEP-0008 currently treats those as manifest/platform -policy rather than core effect algebra. - -### 3. Effect evolution and deprecation - -When an atomic effect is deprecated, renamed, or split, the language needs a -migration story: diagnostics, compatibility aliases, possible `@deprecated` -metadata, and any automated rewrites. - -### 4. Interaction with generics - -How do generic type parameters interact with effect sets? For example: - -```spore -fn apply[T, R](f: (T) -> R, x: T) -> R { - f(x) -} -``` - -This currently works only with pure `f`. If `f` has effects, should `apply` need to declare them? Without effect polymorphism, the answer is that `apply` must be specialised for each effect set, which may require monomorphisation or overloading. - -### 5. Runtime effect tokens - -Should effect tokens have a runtime representation (e.g., for dependency injection in tests), or are they purely a compile-time concept? A hybrid model where effects are erased by default but can be reified for testing purposes may be desirable. - -### Resolved or delegated questions - -- **Effect subtraction syntax** is not part of the accepted surface. Spore does not support - `uses [All \ Spawn]`; developers enumerate effects explicitly. -- **Platform ceilings** are delegated to SEP-0008. If standardized, the expected - model is static constraint checking (`S_function ⊆ S_platform`), not - silently intersecting a function's declared effect set. - ---- - -## Appendix A: Formal notation quick reference - -| # | Notation | Meaning | -| --- | -------------- | -------------------------------------------------------------------------------- | -| 1 | **E** | Universe of atomic effects | -| 2 | S, S₁, S₂ | Effect sets (finite subsets of **E**) | -| 3 | {} or ∅ | Empty effect set (pure function) | -| 4 | S₁ ⊆ S₂ | S₁ is a subset of S₂ | -| 5 | S₁ ∪ S₂ | Union of S₁ and S₂ | -| 6 | S₁ ∩ S₂ | Intersection of S₁ and S₂ | -| 7 | (T → R uses S) | Function type: parameter T, return R, effect set S | -| 8 | Γ; S ⊢ e : T | Typing judgement: under context Γ and effect set S, expression e has type T | -| 9 | 𝒫(prop, S) | Property inference function: determines property `prop` from effect set S | -| 10 | <: | Subtype relation | -| 11 | (C, A, W, P) | Four-dimensional cost vector: compute(op), alloc(cell), io(call), parallel(lane) | -| 12 | `effect C = A₁ | A₂ | ... | Aₙ` | Named alias definition expanding to a flat set of atomic effects | +1. Should effect names be partitioned by namespace? +2. Should named surfaces be allowed to reference package-qualified effect names? +3. How much inferred purity and determinism data should be visible in normal diagnostics? diff --git a/seps/SEP-0004-cost-analysis.md b/seps/SEP-0004-cost-analysis.md index 8d0755b..28574a1 100644 --- a/seps/SEP-0004-cost-analysis.md +++ b/seps/SEP-0004-cost-analysis.md @@ -1,6 +1,6 @@ --- sep: 4 -title: "SEP-0004: Cost Analysis & Decidability" +title: "SEP-0004: Budget Constraints & Realization Shape" status: Draft type: Standards Track authors: @@ -15,1404 +15,256 @@ pr: null superseded_by: null --- -# SEP-0004: Cost Analysis & Decidability +# SEP-0004: Budget Constraints & Realization Shape -> **Executive Summary**: Introduces 4-dimensional cost analysis with CostVector(compute, alloc, io, parallel), where cost expressions range over compile-time `Index` parameters and a decidable IndexExpr grammar (+, ×, log, max, min, span). Provides a three-tier escape mechanism (`@unbounded` for opt-out, `@trust_cost` for unverifiable declarations, advisory warnings for gradual adoption) and formal cost inference rules for higher-order function propagation. Checked residual budgeting is defined over 4D vectors as an internal compiler relation (`before + candidate ≤ budget`), not as user-facing subtraction syntax. +> **Executive Summary**: Replaces positional resource accounting with named realization-shape budgets. A `budget { ... }` block declares integer upper bounds over implementation shape, enabling human review, Agent filling, and EvidenceRecord generation without encoding machine-resource formulas in source signatures. ## Summary -This SEP specifies Spore's compile-time cost analysis system — a three-tier mechanism that statically determines or verifies upper bounds on resource consumption for every function. The system operates along four cost dimensions — **compute(op)**, **alloc(cell)**, **io(call)**, **parallel(lane)** — and leverages the fact that Spore has **no loops** (all iteration is expressed via recursion and higher-order functions) to make cost analysis equivalent to recursion analysis. - -The three tiers are: - -1. **Tier 1 — Automatic structural recursion detection** (~70% of functions): the compiler detects that one argument strictly decreases along a well-founded relation on every recursive call and automatically infers a cost bound. -2. **Tier 2 — Declarative verification** (~20%): the developer writes `cost [compute, alloc, io, parallel]` in the function signature; the compiler verifies each slot independently. -3. **Tier 3 — `@unbounded` escape hatch** (~10%): the developer explicitly opts out of cost checking; the annotation is _contagious_ — callers inherit `@unbounded` unless they isolate it with `with_cost_limit`. - -Cost expressions (`CostExpr`) are drawn from a restricted grammar over compile-time `Index` parameters — `+`, `*`, `log`, `max`, `min`, and `span(hi, lo)` — deliberately excluding arbitrary runtime values, division, ordinary subtraction, and conditionals. This restriction keeps verification decidable and makes cost a compile-time symbolic upper-bound function rather than runtime profiling. - ---- - -## Motivation - -### The problem with runtime profiling - -Traditional performance analysis relies on runtime profiling: run the program, measure timings, hope the workload is representative. This is machine-dependent, non-reproducible, and fundamentally reactive — you discover performance regressions _after_ they ship. - -### Why Spore can do better - -Spore's language design creates a unique opportunity for compile-time cost analysis: - -1. **No loops.** All iteration is expressed through recursion and higher-order functions (`map`, `fold`, `filter`). This means cost analysis reduces entirely to recursion analysis — there is no separate "loop analysis" pass. -2. **Algebraic data types.** The vast majority of recursive functions naturally follow the structure of their data (structural recursion), which is automatically detectable and provably terminating. -3. **Pure functions.** In the absence of side effects, a function's cost is determined entirely by its parameters, enabling symbolic cost propagation. -4. **Effect system.** The `uses [...]` declarations cross-validate with cost dimensions — a function declared `uses []` (pure) must have zero io(call) cost. - -### Design goals - -| Goal | Description | -| ------------- | --------------------------------------------------------------------------------------- | -| High coverage | ~90% of real-world recursive code gets a cost bound automatically or semi-automatically | -| Zero burden | Simple cases require no manual annotation | -| Escapable | Unanalyzable code does not block compilation — it produces a warning | -| Composable | Recursive cost and higher-order function cost compose seamlessly | -| Decidable | The verification algorithm always terminates in polynomial time | - -### The core equation - -```text -Compile-time cost analysis = Abstract Interpretation + Deterministic Cost Table -``` - -The compiler does not run real code. It walks the program on an abstract machine, accumulating deterministic cost at every step. The result is an exact value (for non-recursive functions) or a symbolic upper bound (for recursive functions). - ---- - -## Guide-level explanation - -This section explains how developers interact with the cost system in daily use. - -### Automatic cost inference — you write nothing - -For the vast majority of functions, the compiler infers cost automatically. You do not need to write any annotation: +A budget is a quantitative constraint on realization shape: ```spore -fn factorial(n: I64) -> I64 { - match n { - 0 => 1, - n => n * factorial(n - 1), - } +fn sort(xs: List[I64]) -> List[I64] +budget { + branches: 4 + nesting: 3 + recursion: 0 + parallelism: 1 } -``` - -The compiler detects structural recursion on `n` (decreasing by 1 on each call) and infers: - -```text -✓ structural recursion detected: n decreases by 1 on each call - cost = n × 4 op (1[*] + 3[call overhead]) - → O(n) -``` - -Tree traversals work the same way: - -```spore -fn tree_sum(tree: Tree) -> I64 { - match tree { - Leaf(v) => v, - Node(left, val, right) => tree_sum(left) + val + tree_sum(right), - } +properties { + ordered(xs: List[I64]): is_ordered(sort(xs)) +} +{ + ?sort_body } ``` -```text -✓ structural recursion detected: tree decreases to subtrees (binary) - cost = nodes(tree) × 9 op - → O(n) -``` +Budget fields are named integer upper bounds. The initial field set is: -### Declaring cost — `cost [compute, alloc, io, parallel]` +| Field | Meaning | +| ------------- | --------------------------------------------------- | +| `branches` | Conditional or match branch count upper bound | +| `nesting` | Maximum nested control-expression depth | +| `recursion` | Maximum recursive-call depth; `0` forbids recursion | +| `parallelism` | Maximum parallel fan-out | +| `calls` | Function-call count upper bound | +| `effects` | Effect operation count upper bound | +| `holes` | Remaining hole count upper bound | -When the compiler cannot automatically detect structural recursion but you know the cost is bounded, declare it: +## Motivation -```spore -fn vec_merge_sort[T, N: Index](items: Vec[T, max: N]) -> Vec[T, max: N] - where T: Ord - decreases N - cost [ - N * log(N) * 5 + N, - N * log(N), - 0, - 0, - ] -{ - ?sort_impl -} -``` +The signature model uses budgets to shape valid realizations. The question is not an +abstract resource formula; it is whether the implementation is small enough, +reviewable enough, and constrained enough for humans, Agents, and checkers to +trust. -Dynamic `List[T]` sorting remains available in the standard library, but its -length is not a verified `CostExpr` variable. Publishing a verified sorting cost -requires an indexed container such as `Vec[T, max: N]`. +Named fields are preferable because each constraint is readable, independently +checkable, and extensible without positional migration. -The compiler verifies the declared bound against the inferred cost using abstract interpretation and the asymptotic comparison algorithm defined in the reference section. The optional `decreases` clause provides a termination measure when the compiler needs help. +## Guide-level explanation -### Escaping — `@unbounded` +### No budget required -Some functions have costs that are mathematically unknown or unprovable (e.g., the Collatz conjecture). Mark them explicitly: +Most functions do not need an explicit budget: ```spore -@unbounded -fn collatz_steps(n: I64) -> I64 { - match n { - 1 => 0, - n if n % 2 == 0 => 1 + collatz_steps(n / 2), - n => 1 + collatz_steps(3 * n + 1), - } -} +fn add(a: I64, b: I64) -> I64 { a + b } ``` -`@unbounded` functions cannot be called directly from ordinary four-slot cost declarations without isolation. The bridge is a **runtime cost limiter** (below): it lexically bounds evaluation of unbounded callees while preserving a checkable four-slot contract for the enclosing function. Contagion and isolation rules are normative in this SEP; how implementations surface violations in the pipeline (`K0xxx` and related) belongs to SEP-0006. +### Review-oriented budget ```spore -fn safe_collatz(n: I64) -> I64 ! CostExceeded - cost [10000, 2000, 0, 0] -{ - with_cost_limit(10000) { - collatz_steps(n) - } +fn classify(input: Event) -> Category +budget { + branches: 6 + nesting: 2 + calls: 4 } -``` - -### Querying cost - -```bash -$ sporec --query-cost merge_sort { - "function": "merge_sort", - "cost_symbolic": "n * log(n) * 5 + n", - "cost_declared": "[n*log(n)*5+n, n*log(n), 0, 1]", - "dimensions": { - "compute": "n * log(n) * 4 + n", - "alloc": "n * log(n)", - "io": "0", - "parallel": "1" - }, - "status": "verified" -} -``` - -### Checked residual budgeting - -Users declare per-function budgets with the four-slot `CostVector` -`cost [compute, alloc, io, parallel]`. The checker tracks how much of each -dimension remains after each checked program prefix. - -- Source-level declarations stay aggregate: `cost [compute, alloc, io, parallel]` -- The compiler internally tracks a **residual budget** after each checked prefix -- Residuals may be surfaced in diagnostics, HoleReport payloads, or debugging - tools, but they are **not** a new source-language arithmetic feature - -In other words, the language does **not** expose user-facing budget arithmetic -such as `remaining_cost(...)` or local budget variables. Residual budgeting is a -checker concept used to explain whether a call, handler installation, or hole -still fits within the enclosing declaration. - -### Summary of the three tiers - -```text - Coverage - ┌────────────────────────────────────────────┐ - │ Tier 1: Structural recursion ~70% │ ← Fully automatic - ├────────────────────────────────────────────┤ - │ Tier 2: `cost [c,a,i,p]` declaration ~20% │ ← Developer writes bounds, compiler verifies - ├────────────────────────────────────────────┤ - │ Tier 3: @unbounded escape ~10% │ ← Explicit opt-out - └────────────────────────────────────────────┘ -``` - -Design principle: **What can be inferred automatically shall never require manual annotation. What cannot be inferred shall never block compilation.** - ---- - -## Reference-level explanation - -### Notation - -| Symbol | Meaning | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `C` / `A` / `W` / `P` | Cost dimensions: Compute(op), Alloc(cell), IO(call), Parallel(lane). Note: `C` here denotes **Compute**, distinct from the EffectSet metavariable `C` used in SEP-0002's typing judgments. | -| `K(e)` | CostVector of expression `e` | -| `⊕` | Pointwise CostVector addition | -| `⊗` | CostVector scaling | -| `≤` | Pointwise CostVector comparison (each dimension ≤) | -| `N0` | Non-negative integers {0, 1, 2, …} | -| `N: Index` | Compile-time non-negative size parameter | -| `σ` | Assignment environment: IndexVar → N0 | -| `⟦e⟧σ` | Semantic evaluation of CostExpr `e` under assignment `σ` | -| `≺` | Well-founded decreasing relation | -| `≼` | Asymptotic dominance | - -### 4.1 Cost dimensions - -The abstract machine maintains four independent cost dimensions: - -| Dimension | Abbreviation | Meaning | Unit | -| ----------- | ------------ | --------------------------------- | --------------------------- | -| Compute | `C` | CPU operation steps | op (operation) | -| Allocation | `A` | Heap memory allocation | cell (abstract memory unit) | -| I/O | `W` | Side-effect / external call count | call | -| Parallelism | `P` | Parallel execution width | lane | - -**Scalar summaries (reports):** tooling may fold **C**, **A**, and **W** into one -weighted number for display. Declarations remain the four-slot form; the fold is: - -```text -cost = C × 1 + A × α + W × β -``` - -where `α` and `β` are project-configurable weights (default α = 2, β = 100). The `P` dimension is reported independently for resource planning and does not participate in the default scalar sum. - -> **Note on P dimension.** The default scalar formula excludes `P` because parallel lane count represents a resource-width metric rather than a cost-per-invocation metric. However, projects that need to account for parallelism in the scalar may define a custom weight scheme in `spore.toml` that includes a `γ` weight for P: `cost = C×1 + A×α + W×β + P×γ`. This is an opt-in extension; the default remains `γ = 0` (P excluded). - -### 4.2 Primitive cost table - -#### Compute (C dimension) - -| Operation | Cost (op) | Notes | -| ---------------------------------- | --------- | --------------------------------------------- | -| Integer `+`, `-`, `*` | 1 | | -| Integer `/`, `%` | 2 | Division is slightly more expensive | -| F64 `+`, `-`, `*` | 2 | | -| F64 `/` | 3 | | -| Comparison `==`, `!=`, `<`, `>` | 1 | | -| Logical `&&`, `\|\|`, `!` | 1 | Max-path (short-circuit does not reduce cost) | -| Bitwise `&`, `\|`, `^`, `<<`, `>>` | 1 | | -| Variable read | 0 | Already in scope | -| `let` binding | 1 | | -| Pattern arm | 1 | Per arm matched | -| Function call overhead | 3 | Fixed, excludes callee body | -| Closure creation (N captures) | N + 2 | | -| Pipe `\|>` | 0 | Syntactic sugar | - -#### Allocation (A dimension) - -| Operation | Cost (cell) | -| ------------------- | -------------------------------------- | -| Struct creation | field count | -| List creation | element count + 1 header | -| `Str` creation | ⌈len / 8⌉ | -| `Str` concatenation | ⌈(len_a + len_b) / 8⌉ (new allocation) | -| Enum / union | 1 (tag + max variant size) | -| Deep copy | original cell count | -| Borrow / reference | 0 | - -#### I/O (W dimension) - -Every system call (file read/write, network request, stdio, random number generation, clock read, mutable state access) costs 1 call. - -### 4.3 Composition rules - -| Form | Cost rule | -| ------------------------------------------------- | ---------------------------------------------------------------------- | -| Sequential `A; B` | `cost(A) + cost(B)` | -| Conditional `if c then A else B` | `cost(c) + max(cost(A), cost(B))` | -| Pattern match `match x { p₁ => A, p₂ => B, ... }` | `cost(x) + max(cost(A), cost(B), ...) + arms × 1` | -| Function call `f(args)` | `Σ cost(argᵢ) + 3 + cost(f.body)` | -| Pipe chain `x \|> f \|> g` | `cost(x) + cost(f) + cost(g)` | -| Parallel `parallel { A, B }` | C, A, W: `max(cost(A), cost(B)) + sync_overhead`; P: `sum(P(A), P(B))` | - -> **Concurrent sync overhead.** The `sync_overhead` is a configurable constant (default: 0) representing the synchronisation cost of joining parallel branches. It can be set in `spore.toml` as `[cost] sync_overhead = 10`. When set to 0 (the default), the parallel cost reduces to a simple `max`. Projects requiring precise modelling of fork/join overhead should configure this parameter. - -#### Checked residual budgeting - -Let `B_declared` be the function's declared cost vector and `K_before(p)` be the -cost vector accumulated by the checker before program point `p`. A candidate -subexpression `e` checked at `p` is valid iff: - -```text -K_before(p) ⊕ K(e) ≤ B_declared -``` - -This is the intended reading of checked residuals for the current wave: the -compiler proves **before + candidate ≤ budget** pointwise over the four cost -dimensions. - -The checker may also report a residual vector `R(p)` satisfying: - -```text -K_before(p) ⊕ R(p) ≤ B_declared -``` - -and use the greatest such pointwise vector as the displayed "remaining budget." -This is equivalent to internal pointwise subtraction over already-known costs, -but that subtraction remains an implementation detail rather than a source -construct. - -Consequences: - -1. **Holes** inherit a residual budget from the enclosing prefix, which is why - SEP-0005 can expose per-hole remaining cost without inventing new syntax. -2. **Handlers** are checked the same way: the enclosing scope must have enough - residual budget for both the handled body and any handler implementation - obligations that escape via discharge (see SEP-0003). -3. **Diagnostics stay canonical**: the user sees a declared total budget plus a - reported residual, not a requirement to write explicit budget arithmetic. - -### Formal cost judgments - -**Notation:** - -- `K(e)` — cost of expression e as a CostVector -- `⊕` — pointwise CostVector addition -- `⊗` — CostVector scaling -- `≤` — pointwise CostVector comparison (each dimension ≤) - -**Cost inference rules:** - -```text -[Cost-Literal] - K(literal) = (0, 0, 0, 0) - -[Cost-Var] - K(x) = (0, 0, 0, 0) - -[Cost-BinOp] - K(a op b) = K(a) ⊕ K(b) ⊕ (1, 0, 0, 0) - -[Cost-Alloc] - K(S { ... }) = K(field_exprs) ⊕ (0, 1, 0, 0) - -[Cost-App] - f has declared cost C_f(args) - K(f(args)) = K(args) ⊕ C_f(args) - -[Cost-If] - K(if c { t } else { e }) = K(c) ⊕ max(K(t), K(e)) - -[Cost-Match] - K(match s { pᵢ => eᵢ }) = K(s) ⊕ max(K(e₁), ..., K(eₙ)) - -[Cost-Let] - K(let x = e₁; e₂) = K(e₁) ⊕ K(e₂) - -[Cost-Spawn] - K(spawn e) = (1, 1, 0, 1) ⊕ K(e) projected onto parallel dimension - (parallel dimension tracks spawned lanes) - -[Cost-HOF-VecMap] - xs: Vec[T, max: N] - K(xs.map(f)) = N ⊗ K_body(f) ⊕ (0, N, 0, 0) - -[Cost-HOF-VecFold] - xs: Vec[T, max: N] - K(xs.fold(init, f)) = N ⊗ K_body(f) - -[Cost-HOF-VecFilter] - xs: Vec[T, max: N] - K(xs.filter(p)) = N ⊗ K_body(p) ⊕ (0, N, 0, 0) -``` - -**Verification judgment:** - -```text -[Cost-Verify] - fn f(x₁: T₁, ..., xₙ: Tₙ) -> R cost C_declared - K_inferred = infer_cost(body) - K_inferred ≤ C_declared (pointwise, asymptotically) - ───────────────────────────────────────────────────── - f is cost-valid - -[Cost-Unbounded] - fn f(...) -> R cost @unbounded - ───────────────────────────────────────────────────── - f is cost-valid (trivially, no verification needed) - -[Cost-Propagation] - fn f(...) -> R cost C_f - fn g(...) -> R' = ... f(args) ... - g has no cost annotation - ───────────────────────────────────────────────────── - inferred cost of g includes C_f(args) at each call site -``` - -**Decidability of cost comparison:** - -The cost expression language is intentionally restricted to ensure decidability: - -- Allowed operations: `+`, `×`, `^c` (constant exponent), `log`, `max`, `min` -- Disallowed: arbitrary exponentiation, division, subtraction -- Comparison `C₁ ≤ C₂` is decidable for this restricted grammar via: - 1. Normalize both expressions to canonical form - 2. Apply asymptotic dominance rules (e.g., `n² + n ≤ n²` simplifies to `n ≤ 0` for large n) - 3. Verify each CostVector dimension independently - -### 4.4 CostExpr grammar - -The cost expression language is deliberately restricted to maintain -decidability. CostExpr may mention only compile-time Index symbols, literals, -and function-cost summaries such as `cost(f)`. Ordinary runtime values are not -CostExpr variables. - -#### BNF definition - -```bnf -CostExpr ::= Literal - | IndexExpr - | 'cost' '(' FnVar ')' - | CostExpr '+' CostExpr - | CostExpr '*' CostExpr - | 'log' '(' CostExpr ')' - | 'max' '(' CostExpr ',' CostExpr ')' - | 'min' '(' CostExpr ',' CostExpr ')' - -IndexExpr ::= Literal - | IndexVar - | IndexExpr '+' IndexExpr - | IndexExpr '*' IndexExpr - | 'log' '(' IndexExpr ')' - | 'max' '(' IndexExpr ',' IndexExpr ')' - | 'min' '(' IndexExpr ',' IndexExpr ')' - | 'span' '(' IndexExpr ',' IndexExpr ')' - -Literal ::= '0' | [1-9][0-9]* -IndexVar ::= [A-Z][A-Za-z0-9_]* -FnVar ::= [a-z][A-Za-z0-9_]* -``` - -#### Explicitly forbidden constructs - -| Construct | Reason | -| ------------------------------- | ---------------------------------------------------------------------------------------- | -| Ordinary runtime values | Cost must be a compile-time symbolic upper-bound function over Index parameters | -| Division `/` | Avoids division-by-zero and rational expressions | -| Ordinary subtraction `-` | May produce negative values and non-monotone expressions | -| Conditionals `if...then...else` | Introduces undecidable branching — conditionals can encode arbitrary predicates | -| Recursive cost definitions | Avoids fixpoint computation; recursion analysis is handled at a separate layer | -| Negative numbers | Cost domain is `N0` (non-negative integers) | -| Variable exponents `n^m` | Pushes comparison into the exponential polynomial domain, losing polynomial decidability | - -`span(hi, lo)` is the only difference-like operation. Its meaning is -`max(hi - lo, 0)`, and it exists only in the Index layer so APIs can express -interval lengths without admitting arbitrary subtraction into CostExpr. - -Residual budgeting does not weaken this rule: the checker may compute -"remaining budget" internally, but that is not a user-authored `CostExpr` -surface and therefore does not reintroduce ordinary subtraction into the source -language. - -#### CostExpr definition - -The `CostExpr` type has the following structure: - -```rust -pub enum CostExpr { - Const(u64), - IndexVar(String), - FunctionCost(String), - Add(Box, Box), - Mul(Box, Box), - Log(Box), - Max(Box, Box), - Min(Box, Box), - Span(Box, Box), + ?classify_body } ``` -### 4.5 Semantics - -CostExpr evaluates over **N0 = {0, 1, 2, ...}** (non-negative integers). A -CostVector's identity element is `[0, 0, 0, 0]`. - -Let σ: IndexVar → N0 be an assignment environment. The semantic function -⟦·⟧: CostExpr → (IndexVar → N0) → N0 is defined as: - -```text -⟦ c ⟧σ = c (c ∈ N0) -⟦ N ⟧σ = σ(N) (N ∈ IndexVar) -⟦ e₁ + e₂ ⟧σ = ⟦e₁⟧σ + ⟦e₂⟧σ -⟦ e₁ * e₂ ⟧σ = ⟦e₁⟧σ × ⟦e₂⟧σ -⟦ log(e) ⟧σ = ⌈log₂(max(1, ⟦e⟧σ))⌉ -⟦ max(e₁,e₂) ⟧σ = max(⟦e₁⟧σ, ⟦e₂⟧σ) -⟦ min(e₁,e₂) ⟧σ = min(⟦e₁⟧σ, ⟦e₂⟧σ) -⟦ span(e₁,e₂) ⟧σ = max(⟦e₁⟧σ - ⟦e₂⟧σ, 0) -``` - -> **Convention**: `log` always means `log₂` (binary logarithm), following standard computer science convention. - -**Theorem 4.1 (Monotonicity for the positive fragment).** For any CostExpr `e` -that does not contain `span`, if σ₁(N) ≤ σ₂(N) for all variables N, then -⟦e⟧σ₁ ≤ ⟦e⟧σ₂. - -_Proof._ By structural induction on `e`. All operations (`+`, `×`, `log`, -`max`, `min`) are monotone non-decreasing on N0. ∎ - -For `span(hi, lo)`, the checker tracks variance: `hi` is covariant and `lo` is -contravariant. This is still a compile-time Index relation, not ordinary -runtime-value reasoning. - -### 4.6 Three-tier analysis - -#### Tier 1: Structural recursion auto-detection - -**Definition.** A function f(x₁, ..., xₙ) is _structurally recursive_ if there exists i ∈ {1, ..., n} such that for every recursive call f(y₁, ..., yₙ): - -```text -yᵢ ≺ xᵢ (where ≺ is a well-founded relation on type Tᵢ) -``` - -The compiler recognizes the following decreasing patterns: - -| Pattern | Source → Recursive arg | Well-founded relation | Typical cost | -| ------------------------ | ----------------------------------------------- | --------------------- | ------------------------------ | -| Natural number decrement | `n → n - 1` (with `n > 0` guard) | `<` on ℕ | O(n) | -| List tail | `list → list.tail` | Sublist relation | O(n) | -| Tree child (unary) | `tree → tree.left` or `tree → tree.right` | Subtree relation | O(log n) balanced / O(n) worst | -| Tree child (binary) | `tree → tree.left` and `tree → tree.right` | Subtree relation | O(n) | -| Enum destructuring | `match x { Variant(inner) => f(inner) }` | Structural subterm | O(depth) | -| Tuple projection | `(a, b) → a` or `(a, b) → b` (strictly smaller) | Structural subterm | Depends on projected component | -| Integer halving | `n → n / 2` (with `n > 0` guard) | `<` on ℕ | O(log n) | - -**Detection algorithm**: - -```text -algorithm detect_structural_recursion(f): - 1. Extract all recursive call sites {call₁, call₂, ..., callₖ} - 2. For each callⱼ: - a. Classify each argument as Same(param), Decreasing, or Other - b. Check if yᵢ ≺ xᵢ holds (via syntactic pattern matching) - 3. If ALL recursive calls have at least one parameter - strictly decreasing in ALL paths: - → f is structurally recursive - → Derive cost bound from the decreasing pattern - 4. Otherwise: - → Proceed to Tier 2 or Tier 3 -``` - -Detection complexity: O(|call_graph|), performed during type checking. - -**Cost derivation rules:** - -- Linear recursion (single recursive call, decrement by constant): `cost(f, n) = n × cost_per_step → O(n)` -- Halving recursion (single recursive call, argument halved): `cost(f, n) = log(n) × cost_per_step → O(log n)` -- Binary tree recursion (two calls on subtrees): `cost(f, tree) = nodes(tree) × cost_per_step → O(n)` -- Binary exponential recursion (e.g., `f(n-1) + f(n-2)`): `cost(f, n) = 2ⁿ × cost_per_step` — compiler emits a warning suggesting memoization or tail recursion +This says the realization should stay small and flat enough to review. -where `cost_per_step` is the four-dimensional cost of the function body excluding recursive calls, computed from the primitive cost table. - -#### Tier 2: Declarative verification - -When the compiler cannot auto-detect structural recursion but the developer knows the function terminates with a bounded cost, they declare it: +### Agent-oriented budget ```spore -fn gcd_bounded[A: Index, B: Index](a: Count[A], b: Count[B]) -> Count[max(A, B)] - decreases A + B - cost [log(max(A, B)), 0, 0, 0] -{ - ?gcd_impl +fn fetch_all(urls: List[Url]) -> List[Page] ! NetworkError +uses [Http, Spawn] +budget { + parallelism: 4 + effects: 8 + nesting: 3 } -``` - -**Verification method.** The compiler attempts verification in priority order: - -1. **Call-tree induction**: If `cost(recursive_call) < cost(current_call)` and the base case cost is bounded, the bound holds. Formally, given declared bound B(x) and recursive call with argument x': - - ```text - Verify: cost_body(x) + B(x') ≤ B(x) - ``` - - where `cost_body(x)` is the cost of the function body excluding recursive calls, and `B(x')` is the bound applied to the recursive call's arguments (inductive hypothesis). - -2. **Monotonicity analysis**: If the bound expression is monotonically decreasing in the decreasing parameter and the base case satisfies the bound. - -3. **Arithmetic verification**: Unfold the recursion k steps, substitute parameter values, verify `cost(unfolded) ≤ expr(original_params)`. - -**Verification failure** produces a **warning** (not an error) — the program still compiles. This ensures gradual adoption. - -```text -WARNING [unverified-cost-bound] gcd's cost bound cannot be automatically verified. - Declared (compute slot): log(max(a, b)) - Reason: compiler cannot prove log(max(b, a % b)) < log(max(a, b)) - - Options: - (a) Provide a `decreases` clause to help the compiler - (b) Mark as @unbounded - (c) Add @trust_cost to suppress this warning if you are confident -``` - -#### Tier 3: @unbounded escape - -**Syntax:** - -```spore -@unbounded -fn collatz_steps(n: I64) -> I64 { - match n { - 1 => 0, - n if n % 2 == 0 => 1 + collatz_steps(n / 2), - n => 1 + collatz_steps(3 * n + 1), - } +{ + ?fetch_all_body } ``` -**Rules:** - -| Rule | Description | -| ------------------- | -------------------------------------------------------------------------------------------------------- | -| Warning, not error | `@unbounded` produces a compiler warning, does not block compilation | -| Contagious | Calling an `@unbounded` function makes the caller `@unbounded` too (unless wrapped in `with_cost_limit`) | -| Context restriction | `@unbounded` functions cannot be called directly inside ordinary `cost [...]` functions | -| Hole interaction | Holes inside `@unbounded` functions report `cost_budget: unbounded` | - -**Impact scope tracking.** When a function is marked `@unbounded`, its unbounded status propagates through the call chain: - -- **Direct callers**: any function that calls an `@unbounded` function becomes `@unbounded` itself (unless the call is wrapped in `with_cost_limit`). -- **Indirect callers**: if function A is `@unbounded` and B calls A, then B is also `@unbounded`. If C calls B, C is also `@unbounded`, and so on up the call chain. -- **Isolation**: wrapping an `@unbounded` call in `with_cost_limit` halts the propagation. The wrapping function can declare `cost [...]` normally. - -The compiler tracks this propagation and reports the full impact scope in its diagnostic: - -```text -WARNING [unbounded-function] collatz_steps is marked @unbounded. - Impact scope: - → analyze (direct caller) - → run_batch (calls analyze) - → main (calls run_batch) - Suggestion: wrap with `with_cost_limit` in `analyze` to contain the propagation. -``` - -### 4.7 Higher-order function cost formulas - -Since Spore has no loops, higher-order functions are the _only_ iteration mechanism besides recursion. Verified standard-library cost formulas are defined for indexed containers: - -| Function | Cost formula | -| --------------------------------------------------------- | -------------------- | -| `v.map(f)` where `v: Vec[T, max: N]` | `N × cost(f) + N` | -| `v.fold(init, f)` where `v: Vec[T, max: N]` | `N × cost(f)` | -| `v.filter(pred)` where `v: Vec[T, max: N]` | `N × cost(pred) + N` | -| `a.zip(b)` where `a: Vec[A, max: M]`, `b: Vec[B, max: N]` | `min(M, N)` | -| `v.take(count)` where `count: Count[K]` | `min(N, K)` | -| `v.reduce(f)` where `v: Vec[T, max: N]` | `N × cost(f)` | +The budget tells an Agent which implementation shapes are acceptable before it +writes a fill. -Since higher-order function arguments `f` in Spore must be pure (no effect variables), `cost(f)` is always statically determinable. Substituting `cost(f)` into the formula yields a valid CostExpr. +### Properties and budget together -#### Nested higher-order function derivation example - -The following example demonstrates step-by-step cost derivation for nested higher-order functions: +Properties define validity. Budget defines acceptable realization shape. ```spore -fn matrix_sum[M: Index, N: Index](matrix: Vec[Vec[I64, max: N], max: M]) -> I64 { - matrix - .map(|row| row.fold(0, |a, b| a + b)) - .fold(0, |a, b| a + b) +fn dedupe[T: Eq + Hash](xs: List[T]) -> List[T] +budget { + branches: 3 + nesting: 2 } -``` - -**Six-step derivation** (using the static bounds `M` and `N`): - -```text -Step 1: inner closure cost - cost(|a, b| a + b) = 1 op // single addition - -Step 2: inner fold cost - cost(row.fold(0, |a, b| a + b)) = N × cost(+) = N × 1 = N - -Step 3: map step cost (applying inner fold to each row) - map_step_cost = cost(inner_fold) = N - -Step 4: outer map cost - cost(matrix.map(inner_fold)) = M × map_step_cost + M - = M × N + M - -Step 5: outer fold cost - cost(mapped.fold(0, |a, b| a + b)) = M × 1 = M - -Step 6: total cost - total = cost(outer_map) + cost(outer_fold) - = (M × N + M) + M - = M × N + 2 × M - → O(M × N) -``` - -### 4.8 Mutual recursion - -Detected via **strongly connected components** (SCC) of the call graph (Tarjan's algorithm, O(V + E)): - -```spore -fn is_even(n: I64) -> Bool { - match n { - 0 => true, - n => is_odd(n - 1), - } +properties { + idempotent(xs: List[T]): dedupe(dedupe(xs)) == dedupe(xs) + preserves_members(xs: List[T]): same_members(dedupe(xs), xs) } - -fn is_odd(n: I64) -> Bool { - match n { - 0 => false, - n => is_even(n - 1), - } +{ + ?dedupe_body } ``` -Analysis: SCC = {is_even, is_odd}. Combined call pattern: `is_even(n) → is_odd(n-1) → is_even(n-2) → ...`. Parameter n decreases by 2 every two calls → structural recursion, cost = O(n). - -| Scenario | Handling | -| ----------------------------------------- | --------------------------------------------------------------- | -| SCC satisfies structural recursion | Automatic cost derivation | -| SCC does not satisfy structural recursion | All functions in SCC need explicit `cost [...]` or `@unbounded` | -| Any function in SCC is `@unbounded` | Entire SCC treated as `@unbounded` | - -### 4.9 Decidability proof sketch - -**Verification problem.** Given inferred cost C(n_bar) and declared bound -B(n_bar) as CostExprs, and variable set V = {n1, ..., nk}, determine: does -there exist a threshold T in N0 such that for all non-negative Index -assignments n_bar with every ni >= T, we have ⟦C⟧σ_n_bar <= ⟦B⟧σ_n_bar? +## Reference-level explanation -**Step 1: max/min lifting.** Lift max/min to the top level using distribution rules: +### Budget item grammar -```text -e₁ + max(e₂, e₃) → max(e₁ + e₂, e₁ + e₃) -e₁ * max(e₂, e₃) → max(e₁ * e₂, e₁ * e₃) (valid over N0) +```ebnf +BudgetBlock = "budget" "{" { BudgetItem } "}" ; +BudgetItem = Ident ":" IntLiteral ; ``` -With nesting depth d bounded by a constant (default ≤ 8), this produces at most 2^d = O(1) sub-expression pairs, each of size O(n). - -Verification rules for max/min at the top level: +All values are non-negative integer literals. Field names are resolved by the +budget checker. Unknown fields are diagnostics unless accepted by a project +extension. -| Form | Rule | Condition | -| --------------- | -------------------------- | ------------------------- | -| `max(A, B) ≤ C` | Verify A ≤ C **and** B ≤ C | Necessary and sufficient | -| `min(A, B) ≤ C` | Verify A ≤ C **or** B ≤ C | Sufficient (conservative) | -| `C ≤ max(A, B)` | Verify C ≤ A **or** C ≤ B | Sufficient (conservative) | -| `C ≤ min(A, B)` | Verify C ≤ A **and** C ≤ B | Necessary and sufficient | +### Built-in fields -**Step 2: Normal form conversion.** A _poly-log monomial_ has the form: - -```text -t = c × n₁^a₁ × ... × nₖ^aₖ × log(n₁)^b₁ × ... × log(nₖ)^bₖ -``` +`branches` counts user-authored conditional alternatives, including match arms +and `if` alternatives. -where c is a positive N0 coefficient, and each ai and bi is in N0. We write -this as the triple **(c, a_bar, b_bar)**. +`nesting` counts nested control expressions and nested scoped concurrency +expressions. -The _normal form_ of a CostExpr is a finite sum of poly-log monomials. Conversion algorithm NF: +`recursion` counts self-recursive depth admitted by the realization. A value of +`0` rejects direct or mutual recursion in the checked realization. -```text -NF(c) = {(c, 0̄, 0̄)} -NF(nᵢ) = {(1, eᵢ, 0̄)} (eᵢ = i-th unit vector) -NF(e₁ + e₂) = NF(e₁) ∪ NF(e₂) -NF(e₁ * e₂) = {(c₁c₂, ā₁+ā₂, b̄₁+b̄₂) | - (c₁,ā₁,b̄₁) ∈ NF(e₁), (c₂,ā₂,b̄₂) ∈ NF(e₂)} -NF(e ^ k) = NF(e ×ᵏ e) (expand to k multiplications) -NF(log(nᵢ)) = {(1, 0̄, eᵢ)} -``` - -Logarithm simplification (asymptotically equivalent): - -```text -log(e₁ * e₂) ≈ log(e₁) + log(e₂) -log(e ^ k) ≈ k × log(e) -log(e₁ + e₂) ≈ log(max(e₁, e₂)) -log(c) = ⌈log₂(c)⌉ -``` +`parallelism` counts maximum scoped fan-out created by concurrency primitives. -After conversion, merge like terms: monomials with the same (ā, b̄) have their coefficients summed. +`calls` counts ordinary function-call sites in the realization body. -**Step 3: Asymptotic dominance check.** +`effects` counts effect operation sites after handler expansion. -**Definition 4.2 (Asymptotic dominance).** Monomial t₁ = (c₁, ā₁, b̄₁) is _dominated_ by t₂ = (c₂, ā₂, b̄₂), written t₁ ≼ t₂, iff: +`holes` counts remaining holes admitted in the realization. Public complete +artifacts normally use `holes: 0`. -```text -t₁ ≼ t₂ ⟺ ā₁ < ā₂ (componentwise ≤ and ≠) - ∨ (ā₁ = ā₂ ∧ b̄₁ < b̄₂) - ∨ (ā₁ = ā₂ ∧ b̄₁ = b̄₂ ∧ c₁ ≤ c₂) -``` +### Checking relation -**Algorithm COMPARE(NF(C), NF(B)):** +For a realization `r` and budget field `f`, checking computes `shape(r, f)` and +requires: ```text -1. Merge like terms in NF(C) and NF(B) -2. For each monomial sⱼ in NF(C): - Find the highest-growth matching term tₖ in NF(B) such that sⱼ ≼ tₖ - "Absorption" rules: - a. If sⱼ's (ā, b̄) is strictly less than some tₖ's (ā, b̄), - then sⱼ is asymptotically dominated (regardless of coefficients) - b. If sⱼ's (ā, b̄) equals some tₖ's (ā, b̄), check coefficient cⱼ ≤ cₖ -3. If all sⱼ are absorbed → return PASS -4. Otherwise → return FAIL -``` - -**Step 4: Small-input enumeration.** For inputs n_bar < T (where T is -computed from the dominance analysis, typically <= 100), the compiler -enumerates all values and checks C(n_bar) <= B(n_bar) directly. Enumeration -space is T^k, feasible for k <= 5. - -### 4.10 Verification complexity - -**Theorem 4.2 (Polynomial-time decidability).** The asymptotic comparison of CostExprs is decidable in O(n⁵) time, where n = |C| + |B|. - -_Proof._ Let n = |C| + |B| (total AST nodes) and k = |V| (variable count). - -1. **max/min lifting**: O(n) with d bounded by a constant. -2. **Normal form conversion**: Multiplication distributes monomials (Cartesian product). An expression of size n yields at most O(n²) monomials. Logarithm simplification: O(n) substitutions. Like-term merging: sort in O(n² log n). - - Subtotal: O(n² log n) -3. **Dominance check**: NF(C) has p ≤ O(n²) monomials, NF(B) has q ≤ O(n²) monomials. Comparing one pair: O(k) (componentwise vector comparison). - - Subtotal: O(p × q × k) = O(n⁴ × k) -4. **Total**: O(n² log n) + O(n⁴ × k). Since k ≤ n, total is **O(n⁵)**. - -This is in **P** (polynomial-time complexity class). ∎ - -### 4.11 Soundness - -**Theorem 4.3 (Soundness).** If the algorithm outputs PASS, then there exists a -threshold T in N0 such that for all non-negative Index assignments n_bar with -min(n_bar) >= T, we have C(n_bar) <= B(n_bar). - -_Proof sketch._ - -1. Normal form conversion preserves asymptotic equivalence (logarithm simplifications introduce only constant-factor errors). -2. If every monomial in NF(C) is dominated by some monomial in NF(B): - - When dominance is strict (polynomial exponent strictly less), there exists N₁ beyond which the higher-order term covers the lower-order one. - - When dominance is exact (exponents equal, coefficient ≤), the inequality holds for all inputs. -3. Take N₀ = max(all per-pair Nᵢ). ∎ - -**Conservatism.** The algorithm may return FAIL when the inequality actually holds (e.g., due to cancellation effects between terms). Mitigation: - -1. Sum unabsorbed terms from NF(C) and check if the aggregate is dominated by the leading term of NF(B). -2. If still failing, attempt numerical sampling at several large values. - -These fallback steps only convert FAIL → PASS, never the reverse. - -### 4.12 Four-dimensional verification - -Four-dimensional cost declarations are verified **independently per dimension**: - -```spore -fn vec_sort[T, N: Index](items: Vec[T, max: N]) -> Vec[T, max: N] - where T: Ord - cost [N * log(N) * 3, N, 0, 0] -``` - -This is equivalent to four independent verification problems, each using the same asymptotic comparison algorithm. - -### 4.13 Interaction with the Hole system - -Partially-defined functions participate in cost analysis: - -```spore -fn process(data: Data) -> Result ! ProcessError - cost [1000, 400, 0, 0] -{ - parsed = parser.parse(data) // cost: 600 - ?process_logic // hole: remaining budget 400 -} +shape(r, f) <= declared(f) ``` -The HoleReport includes the remaining cost budget, providing the AI agent (or developer) with a precise performance constraint: +When a field is omitted, the checker imposes no source-level upper bound for +that field. -```json -{ - "hole": "process_logic", - "cost_consumed": 600, - "cost_budget_remaining": 400, - "note": "Implementation cost must not exceed 400 op" -} -``` +### Hole projection -For holes inside recursive functions, the per-iteration budget is `total_budget / iterations - recursive_overhead`. +At a hole site, SEP-0005 receives the enclosing `budget_context` plus any known +shape already committed before and around the hole. The context is advisory for +candidate ranking and normative for accepting a proposed realization. -### 4.14 Compile-time cost analysis pipeline +### Evidence projection -The full cost analysis pipeline executes as part of the compilation process: +SEP-0006 records budget evidence per checked field: ```text -Source Code - ↓ -[1] Parse to AST - ↓ -[2] Type Check + Effect Check - ↓ -[3] Abstract Interpretation (Cost Inference) - ├── Build control flow graph (CFG) - ├── Compute cost per basic block (from primitive cost table) - ├── Take max across branches - ├── Analyse recursion / iteration upper bounds - └── Generate symbolic CostExpr per function - ↓ -[4] Cost Verification - ├── Compare inferred CostExpr per dimension against declared `cost [c,a,i,p]` slots - ├── PASS → compilation succeeds - ├── FAIL → compile error with four-dimensional breakdown - └── UNKNOWN → warning with suggestions - ↓ -[5] Cost metadata written to compilation output (JSON / binary) -``` - -### 4.15 Bounded type semantics - -Indexed types provide compile-time size information for cost verification: - -```spore -fn process_batch[N: Index](items: Vec[Order, max: N]) -> BatchResult ! TooLarge - cost [N * 80 + 800, N, N, 0] -{ - items.map(|order| validate(order)).fold(BatchResult.empty(), merge) -} -``` - -`Vec[Order, max: N]` carries the static capacity bound `N`. Dynamic -`List[Order]` remains useful for ordinary programs, but its runtime length is -not a CostExpr variable. Use `Vec`, `Array`, or `Count` when a verified -compile-time cost bound must mention size. - -### 4.16 FFI extern function cost declarations - -Foreign functions have no Spore body for the compiler to analyse. They **must** declare their cost explicitly at the binding site: - -```spore -extern fn c_sort[T, N: Index](data: Vec[T, max: N]) -> Vec[T, max: N] - cost [N * log(N), N, 0, 0] -``` - -```spore -extern fn openssl_encrypt[N: Index](data: Bytes[N], key: Key) -> Bytes[N] ! CryptoError - cost [N * 3 + 500, N, N, 0] -``` - -**Rules for extern fn cost:** - -| Rule | Description | -| -------------------- | -------------------------------------------------------------------------------------------------- | -| Required declaration | `extern fn` without a `cost` clause is treated as `@unbounded` | -| No body analysis | The compiler trusts the declared cost — no verification is possible | -| Contagious unbounded | An `@unbounded` extern fn follows the same contagion rules as any `@unbounded` function | -| Variable binding | Cost variables are Index parameters such as `N`; ordinary runtime values do not bind into CostExpr | - -This ensures FFI boundaries maintain cost transparency — external code cannot silently introduce cost black holes. - -### 4.17 Polymorphic function cost resolution - -For generic methods like `vec.map(f)`, cost depends on the concrete function -`f`. Spore resolves this at the **call site**: - -```spore -impl[T, N: Index] Mappable for Vec[T, max: N] { - type Item = T - type Mapped[U] = Vec[U, max: N] - fn map[U](self, f: (T) -> U) -> Vec[U, max: N] - cost [N * cost(f) + N, N, 0, 0] -} +claim: budget.nesting <= 3 +result: passed +observed: 2 ``` -At each call site, `f` is concrete and `cost(f)` is known: - -```spore -let result = items.map(|x| x * 2) -// cost(|x| x * 2) = 1 op -// total cost = N * 1 + N = 2 * N -``` - -**Resolution rules:** - -1. `cost(f)` in a signature is a **meta-variable** that is substituted with the concrete function's cost at each call site. -2. Since higher-order function arguments must be pure in Spore, `cost(f)` is always statically determinable at the call site. -3. After substitution, the result is a standard CostExpr that can be verified normally. -4. The compiler does **not** attempt abstract cost reasoning over uninstantiated type parameters — this keeps the system simple and deterministic. - ---- - ## Human experience impact -### Positive - -- **Zero-cost annotations for most code.** ~70% of functions are structurally recursive and get cost bounds automatically. Developers write ordinary code and the compiler silently tracks cost. -- **Performance regressions caught at compile time.** If a refactoring changes O(n) to O(n²), the `cost [...]` declaration fails immediately — not after a production outage. -- **Precise diagnostics.** The compiler does not just say "cost exceeded"; it provides a four-dimensional breakdown showing exactly where the budget went. -- **Gradual adoption.** The three-tier design means developers can start with `@unbounded` everywhere and progressively tighten bounds as the codebase matures. - -### Negative - -- **Learning curve.** Developers must understand asymptotic cost notation to write Tier 2 declarations. The `CostExpr` grammar (no subtraction, no division) requires adjustment. -- **False positives.** Conservative analysis may reject programs whose actual cost is within bounds (e.g., amortized O(1) operations reported as O(n)). -- **Annotation burden for non-structural recursion.** The ~20% of functions in Tier 2 require explicit `cost [...]` and possibly `decreases` clauses. - -### Mitigation - -- The compiler suggests fixes: widen the relevant `cost [...]` slots (often compute) until inferred cost fits. -- `@trust_cost` suppresses unverified-bound warnings for cases where the developer is confident. -- IDE integration shows cost inline as code lens annotations. - ---- +Budgets make review criteria explicit. A reviewer can reject an implementation +because it violates a stated shape bound, not because it feels too complex. ## Agent experience impact -### Cost budgets as constraints for hole-filling - -When an AI agent fills a Hole (see SEP-0003), it receives not just the type signature but also the **remaining cost budget**. This fundamentally changes code generation: - -```json -{ - "hole": "combine_results", - "expected_type": "Result[T]", - "cost_budget_remaining": 34, - "recursion_context": { - "pattern": "structural_binary_tree", - "per_node_budget": "500 / nodes(tree) - 6" - } -} -``` - -The agent knows: - -1. **What** to produce (the type) -2. **How much** it can spend (the cost budget) -3. **How often** the code runs (recursion context) - -This enables cost-aware code generation: the agent can reject an O(n) algorithm in favor of an O(1) one if the budget demands it. - -### Cost verification as feedback signal - -After generating code, the agent can invoke `sporec --query-cost` to verify its solution meets the budget. Failed verification provides a precise error signal ("inferred cost n² exceeds budget n × log(n) by factor n / log(n)") that can drive iterative refinement. - -### `@unbounded` as a risk signal - -An agent encountering an `@unbounded` function in its context knows to exercise caution — calling it may introduce unbounded resource consumption. The agent can proactively suggest `with_cost_limit` wrapping. - ---- +Agents can use budgets before generating code, while ranking candidates, and +after verification. Budget failures become structured repair signals. ## Structured representation / protocol impact -### Cost metadata in compilation output - -The compiler emits structured cost information as part of the compilation artifact: - -```json -{ - "functions": { - "vec_merge_sort": { - "cost_declared": "N * log(N) * 5 + N", - "cost_inferred": "N * log(N) * 4 + N", - "cost_status": "verified", - "cost_dimensions": { - "compute": "N * log(N) * 4", - "alloc": "N", - "io": "0", - "parallel": "0" - }, - "tier": 2, - "decreases": "N" - }, - "factorial": { - "cost_inferred": "n * 4", - "cost_status": "auto", - "tier": 1, - "recursion_pattern": "structural_linear" - } - } -} -``` - -### CostExpr serialization - -CostExpr is serialized as a JSON AST for tool consumption: +```text +BudgetConstraint +├── field +├── limit +├── source_span +└── owner_signature -```json -{ - "type": "Mul", - "left": { "type": "Var", "name": "n" }, - "right": { "type": "Log", "arg": { "type": "IndexVar", "name": "N" } } -} +BudgetEvidence +├── field +├── limit +├── observed +└── result ``` -### LSP extensions - -The Language Server Protocol exposes: - -- `sporec/queryCost`: returns cost metadata for a function -- `sporec/costBreakdown`: returns a per-expression cost tree for a function body -- `sporec/costBudget`: for a Hole, returns the remaining cost budget - ---- +HoleReport embeds relevant constraints under `budget_context`. ## Diagnostics impact -### New diagnostic categories - -| Code | Severity | Trigger | -| ------------------------------ | -------- | -------------------------------------------------------------------------------- | -| `cost-exceeded` | Error | Inferred cost exceeds declared bound | -| `unbounded-cost` | Warning | Recursive function with no detectable cost bound and no `@unbounded` annotation | -| `unbounded-function` | Warning | Function marked `@unbounded` | -| `unbounded-in-bounded-context` | Error | `@unbounded` function called from `cost [...]` context without `with_cost_limit` | -| `unverified-cost-bound` | Warning | `cost [...]` declared but compiler cannot verify one or more slots | -| `cost-effect-conflict` | Error | `uses []` (pure) declared but W > 0 inferred | - -### Example diagnostics - -**cost-exceeded:** - -```text -ERROR [cost-exceeded] bad_search's inferred cost exceeds declared bound. - Inferred: n * log(n) - Declared compute slot upper bound too small vs inferred - Excess factor: log(n) - - Suggestions: - (a) Widen the relevant `cost [...]` slots (especially compute) so the inferred bound fits - (b) Optimize implementation to eliminate the log(n) factor - (c) Check for unnecessary nested iteration -``` - -**unbounded-cost:** - -```text -WARNING [unbounded-cost] fibonacci's cost cannot be statically determined. - Recursion pattern: non-structural (exponential) - Inferred complexity: O(2^n) - - Options: - (a) Add a tighter refinement (`type SmallN = I64 when self <= 30`) together with concrete `cost [...]` literals for small-n paths - (b) Mark as `@unbounded` (relinquish cost constraint) - (c) Rewrite using structural recursion or tail recursion + iteration bound -``` - -### Cross-validation with effects +Budget diagnostics use `B0xxx` codes: -The cost system and effect system form a cross-validation network. If a function declares `uses []` (pure, no effects) but cost analysis discovers W > 0 (I/O operations), the compiler emits `cost-effect-conflict`. Conversely, a function declared `uses [NetConnect]` must have W ≥ 1 in at least one code path. - ---- +| Code | Name | Meaning | +| ------- | -------------------- | ----------------------------------------------------------- | +| `B0101` | budget-exceeded | Observed realization shape exceeds a declared field | +| `B0102` | unknown-budget-field | Field is not built in and not enabled by extension metadata | +| `B0103` | invalid-budget-value | Value is not a non-negative integer literal | +| `B0201` | recursion-disallowed | Realization uses recursion while `recursion: 0` | +| `B0202` | hole-budget-exceeded | Realization leaves more holes than allowed | ## Drawbacks -1. **Compile-time overhead.** The O(n⁵) worst-case verification, while polynomial, can be slow for deeply nested expressions. In practice, expressions are small (n ≤ 50) and the actual time is negligible, but pathological cases are possible. - -2. **Expressiveness limitations.** The CostExpr grammar cannot express: - - Conditional cost (`if sorted then O(n) else O(n²)`) - - Amortized cost (`O(1) amortized`) - - Probabilistic cost (`O(n log n) expected`) - - Exact formulas requiring subtraction or division (`n*(n-1)/2`) - - The workaround is always conservative upper bounds (e.g., `n^2` instead of `n*(n-1)/2`). - -3. **False positives (FAIL when should PASS).** The asymptotic comparison is conservative. Cancellation effects between terms can cause false FAIL results. Example: `n² + n ≤ 2 * n²` holds, but if the algorithm cannot find a per-monomial match for the `n` term, it reports FAIL. - -4. **`@unbounded` contagion.** One `@unbounded` function deep in the call chain can force many callers to also become `@unbounded`, potentially undermining the cost system's value. Mitigation: `with_cost_limit` isolation. +Budgets constrain shape, not wall-clock performance or host resource usage. They +are review and realization constraints, not benchmarking claims. -5. **No built-in runtime proof of static predictions.** Compile-time costs bound the abstract accounting model (`compute`, `alloc`, `io`, `parallel`); they are not automatically certified against wall-clock or host-resource measurements. Closing that gap belongs to tooling and profiling outside this SEP's normative scope. - -6. **Multi-variable partial order.** With multiple variables, some monomials are incomparable (e.g., `n*m` vs `n²`), leading to conservative FAIL. Developers must restructure declarations. - ---- +Counting rules must be stable enough for tools and formatters. The checker must +therefore publish observed counts in diagnostics and evidence. ## Alternatives considered -### Alternative 1: Full dependent types (Agda/Coq style) - -Use a full dependent type system to encode cost bounds as types, enabling machine-checked proofs. - -**Rejected because:** - -- Requires every function to carry a termination proof — too much burden for general-purpose programming. -- 100% coverage means rejecting legitimate programs (e.g., Collatz-like functions). -- Dramatically steeper learning curve. - -### Alternative 2: No static cost analysis (Rust/Go style) - -Rely entirely on runtime profiling and benchmarks. - -**Rejected because:** - -- Misses Spore's unique opportunity (no loops, ADTs, purity). -- Performance regressions discovered too late. -- No cost information for AI agents during hole-filling. - -### Alternative 3: Two-tier only (auto + unbounded, no declarations) - -Skip Tier 2 — either the compiler infers the cost automatically or the function is unbounded. - -**Rejected because:** +### Positional resource vectors -- Would leave ~20% of practically-bounded functions as `@unbounded`, significantly reducing the system's value. -- Merge sort, quicksort, GCD, and many other well-known algorithms would lack cost bounds. +Rejected because positional fields are opaque, look like machine-resource +accounting, and are hard to extend. -### Alternative 4: Allow division and ordinary subtraction in CostExpr +### Complexity notation in signatures -Extend the grammar to support `n*(n-1)/2` and similar expressions. +Rejected because asymptotic notation belongs to algorithm analysis, not to the +core signature realization workflow. -**Rejected because:** +### No quantitative constraints -- Division introduces non-integer results and division-by-zero. -- Subtraction breaks monotonicity (expressions can decrease as inputs grow). -- The comparison problem becomes undecidable in general. -- The conservative upper-bound approach (`n^2` instead of `n*(n-1)/2`) is acceptable in practice. - -`span(hi, lo)` is the accepted narrow alternative: it expresses saturating -interval length in the Index layer without admitting general subtraction. - -### Alternative 5: SMT-based verification - -Use an SMT solver (e.g., Z3) to verify `∀ n̄. C(n̄) ≤ B(n̄)`. - -**Rejected as the primary approach because:** - -- SMT solving is NP-complete in general — no polynomial-time guarantee. -- Solver behavior is non-deterministic (different versions may give different results). -- However, this remains a potential fallback for the compiler's "extra effort" phase after the main algorithm returns FAIL. - ---- +Rejected because Agents and reviewers need compact limits for acceptable +implementation shape. ## Prior art -### Dependent type systems: Agda, Coq, Lean 4 - -These proof assistants enforce termination of all functions. Agda and Coq use structural recursion checking by default; Lean 4 adds `decreasing_by` tactics and `partial` escape. Spore's Tier 1 is directly inspired by structural recursion checking in these systems, but Spore's Tier 3 (`@unbounded`) is a deliberate departure — we accept non-terminating programs rather than rejecting them. - -### Resource-aware type systems: AARA (Automatic Amortized Resource Analysis) - -Hoffmann et al.'s AARA system (implemented in Resource Aware ML) automatically infers polynomial resource bounds using linear programming over type annotations. AARA can handle amortized analysis, which Spore currently cannot. However, AARA's approach requires sophisticated LP solving and is less transparent to developers. Spore opts for a simpler, more transparent system at the cost of not supporting amortized analysis. - -### Complexity analysis tools: COSTA, AProVE - -COSTA (COSt and Termination Analyzer for Java) and AProVE (Automated Program Verification Environment) perform automatic complexity analysis on Java and term rewriting systems respectively. These tools demonstrate that automatic cost analysis is feasible for real languages. Spore's advantage is that its language design (no loops, pure functions, ADTs) makes the analysis significantly more tractable. - -### Gas metering: Ethereum/Solidity - -Smart contract languages assign deterministic gas costs to every operation. Spore's primitive cost table is inspired by this approach but extends it to symbolic expressions (Solidity's gas is always concrete). Spore's four-dimensional model also goes beyond Solidity's single gas dimension. - -### Abstract interpretation: Cousot & Cousot - -Spore's cost inference uses abstract interpretation — executing the program on an abstract domain (CostExpr) rather than concrete values. This is a well-established technique (Cousot & Cousot, 1977) applied here specifically to cost propagation. - -### Sized types: Hughes, Pareto, Sabry - -Sized types annotate values with compile-time **Index parameters** (`Count[N]`, -`Array[T, N]`, `Vec[T, max: N]`, `Matrix[…]`) to enable sharper termination and -complexity reasoning. Everyday dynamic programs can stay on **`List[T]`**, but -verified CostExprs only mention Index parameters. - ---- +Cyclomatic complexity metrics, lint thresholds, and structured-concurrency +fan-out limits all inform this design. Spore brings those ideas into the +signature so tools can consume them before code exists. ## Backward compatibility and migration -### This is a new feature - -Cost analysis is introduced as a new compiler effect. No existing Spore code is broken by this SEP. - -### Migration path - -1. **Phase 1 (initial release):** All functions without cost annotations are implicitly treated as if cost analysis is not enabled. The compiler performs Tier 1 analysis silently and reports results only when explicitly queried (`sporec --query-cost`). - -2. **Phase 2 (opt-in):** Projects opt into cost checking via `spore.toml`: - - ```toml - [cost] - enabled = true - alloc_weight = 2 - io_weight = 100 - ``` - - Functions without cost bounds but with non-structural recursion receive warnings. - -3. **Phase 3 (default-on):** Cost analysis is enabled by default. Functions with unresolvable cost receive `unbounded-cost` warnings. Projects can silence with `[cost] enabled = false`. - -### Interaction with existing SEPs - -- **SEP-0002 (Type System):** CostExpr integrates with the `Index` kind. `List[T]` stays dynamic and unbounded; `Count[N]`, `Array[T, N]`, and `Vec[T, max: N]` expose compile-time Index parameters to cost checking. - -- **SEP-0005 (Hole System):** Cost budgets propagate to HoleReports for hole filling. - ---- - -## Edge cases and limitations - -### Pure constant expressions - -When both C and B contain no variables, the comparison degenerates to numeric comparison: - -```text -C = 42, B = 100 -Verification: 42 ≤ 100 → PASS -``` - -No asymptotic analysis is needed; the compiler computes directly. - -### Single-variable expressions - -With a single variable `n`, comparison reduces to standard Big-O notation comparison. Each normal-form monomial is `c × n^a × log(n)^b`, with the ordering: - -```text -(c₁, a₁, b₁) ≼ (c₂, a₂, b₂) ⟺ a₁ < a₂ - ∨ (a₁ = a₂ ∧ b₁ < b₂) - ∨ (a₁ = a₂ ∧ b₁ = b₂ ∧ c₁ ≤ c₂) -``` - -This is consistent with classical asymptotic order comparison. - -### Multi-variable partial order - -With k > 1 variables, the comparison uses componentwise partial ordering on exponent vectors. Two monomials may be **incomparable** (e.g., `n*m` vs. `n²`). Incomparable cases produce FAIL (conservative). Developers must restructure declarations to make them comparable, e.g., using `max(n*m, n^2)`. - -### Limitations - -| Limitation | Description | Workaround | -| ------------------------------ | ----------------------------------------------------- | -------------------------------------- | -| No conditional cost | Cannot express "if sorted then O(n), else O(n²)" | Use `max(n, n^2) = n^2` (conservative) | -| No amortized analysis | Cannot express "amortized O(1)" | Use worst-case cost | -| No probabilistic analysis | Cannot express "expected O(n log n)" | Use worst-case cost | -| No subtraction / division | Cannot exactly express `n*(n-1)/2` | Use `n^2` upper bound | -| max/min nesting depth bounded | Deep nesting causes expression blowup | Compiler limits depth (default 8) | -| Multi-variable incomparability | Partial order on exponent vectors can be inconclusive | Restructure declaration or use `max` | - ---- - -## Cost drift detection - -Cost drift detection monitors divergence between compile-time cost predictions and actual runtime performance. While full implementation is deferred to a future SEP, the mechanism is specified here. - -**Tolerance threshold.** The configurable parameter `cost_drift_tolerance` defines the maximum allowed ratio of actual-to-predicted cost: - -```toml -[cost] -cost_drift_tolerance = 1.2 # allow 20% deviation -``` - -**Detection mechanism:** - -1. **Instrumentation**: when cost drift detection is enabled (`sporec --cost-instrument`), the compiler inserts lightweight counters at function entry/exit that track actual op/cell/call counts. -2. **Sampling**: the runtime samples cost at configurable frequency (default: every 100th invocation) to minimize overhead. -3. **Comparison**: at runtime, if `actual_cost / predicted_cost > cost_drift_tolerance`, a `CostDrift` warning is logged. -4. **CI integration**: a post-test step `sporec --cost-drift-report` compares sampled costs against compile-time predictions and fails the build if drift exceeds the threshold. - -**Diagnostic format:** - -```text -WARNING [cost-drift] function merge_sort: actual cost exceeds prediction. - Predicted: 1200 op - Actual (sampled): 1500 op - Drift ratio: 1.25 (threshold: 1.2) - Suggestion: review recent changes to merge_sort or update the declared `cost [...]` slots -``` - ---- - -## Design decisions - -| Decision | Choice | Rationale | -| ------------------------------ | --------------------------------------------- | ------------------------------------------------------------------ | -| Recursion analysis tiers | 3 tiers (auto + declarative + escape) | Balances automation with expressiveness; ~90% coverage | -| Structural recursion detection | Syntactic parameter-decreasing check | Simple, reliable, O(\|call_graph\|) complexity | -| Verification failure handling | Warning, not error | Gradual adoption; does not block development | -| `@unbounded` semantics | Contagious + isolatable via `with_cost_limit` | Ensures cost information propagates while providing an escape path | -| `decreases` clause | Optional | Compiler auto-derives in most cases; manual only when needed | -| Mutual recursion | SCC-based whole-group analysis | Natural fit with call-graph analysis | -| Higher-order function cost | Compiler built-in formulas | No loops → HOFs are the only iteration mechanism; must be built-in | - ---- +This is a breaking replacement of earlier resource-accounting syntax. Migration +should only introduce a `budget` field when the old annotation represented a +reviewable realization-shape constraint. Routine library operations should often +omit budgets. ## Unresolved questions -1. **Memoization and cost.** Should the compiler auto-detect memoizable recursion and adjust cost (e.g., `fibonacci` from O(2ⁿ) to O(n))? Current leaning: no auto-memoization, but the compiler suggests it in warnings. - -2. **Cost drift detection.** The mechanism, tolerance threshold, and CI integration are specified in the "Cost drift detection" section above. The remaining unresolved question is the design of a full runtime cost sampling framework — specifically, how to keep instrumentation overhead below 1% in production builds. - -3. **Probabilistic cost bounds.** Randomized algorithms (e.g., QuickSort with random pivot) have expected rather than worst-case cost. Should Spore support an **`expected`** cost metadata channel alongside worst-case four-slot bounds? Deferred to future work. - -4. **Recursion depth limits.** Should the compiler enforce a compile-time recursion depth ceiling? Current decision: only `@unbounded` functions use runtime `with_cost_limit`. Whether to add a compile-time depth annotation (e.g., `max_depth ≤ 1000`) is unresolved. - -5. **Interaction with concurrency.** The parallel dimension `P` (lane) is defined, but runtime budget behavior across `parallel_scope` / `spawn` boundaries needs further specification. In particular, how does `with_cost_limit` behave across child tasks? - -6. **Amortized analysis.** Operations like dynamic array append are O(1) amortized but O(n) worst-case. The current system can only express worst-case bounds. Whether to extend CostExpr with amortized semantics (potentially through a separate `amortized [...]` metadata channel parallel to worst-case `cost [...]`) is deferred. - -7. **Standard library cost annotations.** The standard library must be annotated with cost bounds for the system to be useful. What is the process for auditing and annotating existing library functions? Should the compiler ship with a built-in cost database for the standard library? - -8. **max/min nesting depth limit.** The current default is 8 levels. Is this sufficient for all practical use cases? Should the limit be configurable, and what is the impact on compilation time when it is raised? - -### Resolved questions - -1. **Tail-call optimization and cost.** TCO changes stack space consumption but - not the four declared cost dimensions. The cost model does not add a separate - stack-depth dimension; TCO remains a codegen/runtime optimization. - -2. **Polymorphic cost.** The call-site instantiation approach is specified in - §4.17: `cost(f)` is substituted at each call site where `f` is concrete. - Signature-level patterns such as `cost [N * cost(f) + N, N, 0, 0]` are - first-class CostExprs when `N: Index`. +1. Should project manifests define default budgets for public functions? +2. Should extension fields be namespaced? +3. Should generated realizations be allowed to temporarily exceed budget during repair loops? diff --git a/seps/SEP-0005-hole-report-protocol.md b/seps/SEP-0005-hole-report-protocol.md new file mode 100644 index 0000000..1cd9655 --- /dev/null +++ b/seps/SEP-0005-hole-report-protocol.md @@ -0,0 +1,306 @@ +--- +sep: 5 +title: "SEP-0005: Hole System & HoleReport Protocol" +status: Draft +type: Standards Track +authors: + - Zhan Rongrui +created: 2026-03-31 +requires: + - 1 + - 2 + - 3 + - 4 +discussion: "https://github.com/spore-lang/spore-evolution/discussions/5" +pr: null +superseded_by: null +--- + +# SEP-0005: Hole System & HoleReport Protocol + +> **Executive Summary**: Defines holes as typed absence constrained by Base Signature and Intent Signature context, and normatively specifies the HoleReport records emitted for those holes. HoleReport exposes expected type, visible bindings, effect context, budget context, property context, candidates, dependencies, and confidence data. Agent workflows and candidate rankers are informative projections over this data rather than a required protocol state machine. + +## Summary + +A hole is written `?name` or `?name: Type` in expression position. Programs with +holes are partial, not broken. The compiler accepts them and emits structured +HoleReports. + +```spore +fn validate(order: Order) -> ValidOrder ! ValidationError +uses [DbRead] +budget { + branches: 5 + nesting: 2 + holes: 1 +} +properties { + accepted(order: Order): match validate(order) { + ok _ => true, + fail _ => has_validation_reason(order), + } +} +{ + ?validate_body +} +``` + +The hole is typed absence constrained by the surrounding signature and context. + +This SEP normatively defines hole syntax, HoleReport records, and the hole +dependency graph. It does not normatively define an Agent workflow state +machine, candidate scoring formula, or human teaching projection. Those topics +are informative guidance here or belong to SEP-0010 when they teach users how to +read HoleReport data. + +## Motivation + +Traditional unfinished code is opaque to tools. Spore makes incomplete positions +compiler-visible so a human or Agent can receive enough context to propose a +property-preserving realization. + +HoleReport is the collaboration boundary. It exposes not only type context, but +also effects, budgets, and properties. Human-facing renderers and Agents may +project this data differently, but the shared record remains the same. + +## Guide-level explanation + +### Basic holes + +```spore +fn add_tax(amount: Money) -> Money { + ?taxed_amount +} +``` + +The checker reports the expected type of `?taxed_amount` from return position. + +### Annotated holes + +```spore +let normalized = ?normalize: Email; +``` + +The annotation helps the checker and Agent when context is otherwise ambiguous. + +### Multiple holes + +```spore +fn process(order: Order) -> Receipt ! PaymentError +uses [PaymentGateway] +budget { holes: 2 } +{ + let checked = ?validate_order; + ?charge_payment +} +``` + +The dependency graph determines which holes can be realized first. + +### Reading HoleReports + +A HoleReport is useful to humans, Agents, and tools because the same per-hole +record carries the missing type, visible context, effect context, budget context, +property obligations, candidate data, and dependency edges. Workflow-specific +states are informative guidance rather than part of the required record shape. + +## Reference-level explanation + +### Hole syntax + +```ebnf +HoleExpr = "?" [ Ident ] [ ":" TypeExpr ] ; +``` + +Holes are valid only in expression positions. Type holes are a separate design +space and are not specified here. + +### Normative HoleReport fields + +The per-hole object includes: + +| Field | Meaning | +| ---------------------- | ------------------------------------------------------ | +| `name` | Developer-assigned hole name | +| `display_name` | Source spelling with `?` | +| `location` | File and span | +| `expected_type` | Type the realization must produce | +| `type_inferred_from` | Explanation of the expected type | +| `function` | Enclosing callable name | +| `enclosing_signature` | Normalized Base Signature and Intent Signature summary | +| `bindings` | Visible local bindings and types | +| `binding_dependencies` | Data-flow among visible bindings | +| `effect_context` | Available effect surface and handler context | +| `budget_context` | Relevant budget constraints and observed shape data | +| `property_context` | Properties the realization must preserve | +| `failures_to_handle` | Failure variants still not handled at the site | +| `candidates` | Visible functions or templates that may fit | +| `dependent_holes` | Holes unlocked by this realization | +| `confidence` | Type and candidate confidence data | +| `rejection_reasons` | Structured reasons from failed verification attempts | + +The per-hole object is the normative schema boundary for tools. JSON producers +may add versioned optional fields, but they must preserve the meaning of these +fields when present. A batch response wraps per-hole objects in a `holes` array +and may include a `dependency_graph` object. A single-hole query returns one +per-hole object directly. + +SEP-0010 may attach concept references and render these fields for teaching, but +it does not add required HoleReport fields. Optional teaching metadata is +outside the normative HoleReport schema unless a later SEP moves it here. + +### Type inference rule + +The hole's expected type is the intersection of constraints from return +position, annotations, function arguments, match arms, operators, and sibling +branch types. If constraints conflict, the nearest syntactic constraint is used +for reporting and a diagnostic is emitted. + +### Effect context + +`effect_context` includes the declared surface expression, expanded effect set, +active handlers, and discharged effects. + +### Budget context + +`budget_context` includes inherited budget fields, observed shape around the +hole, and remaining admissible shape when it can be computed structurally. + +### Property context + +`property_context` includes properties attached to the enclosing signature plus +any local obligations produced by refinements or earlier checks. + +### Dependency graph + +Edges are classified as: + +| Kind | Meaning | +| ---------- | ----------------------------------------------------- | +| `type` | Later hole type depends on earlier realization | +| `value` | Later hole binding depends on earlier value | +| `effect` | Effect availability changes after earlier realization | +| `budget` | Shape allowance depends on earlier realization | +| `property` | Property obligation depends on earlier realization | + +The graph must be acyclic for automatic scheduling. + +## Human experience impact + +Holes let developers sketch architecture with signatures and properties first. +Reports are concrete enough to review one missing realization at a time. + +## Agent experience impact + +Agents can rank candidates using typed context and reject proposals that exceed +effects, budgets, or properties before asking for human review. + +## Structured representation / protocol impact + +Batch output: + +```json +{ + "holes": [ + { + "name": "validate_body", + "expected_type": "ValidOrder ! ValidationError", + "effect_context": { "declared_surface": "[DbRead]" }, + "budget_context": { "branches": { "limit": 5 } }, + "property_context": { "properties": ["accepted"] } + } + ], + "dependency_graph": { + "edges": [] + } +} +``` + +Single-hole queries return the same per-hole object directly. + +Human-facing educational renderings of HoleReport records are owned by SEP-0010. +Those renderings must stay projections over the same HoleReport data rather than +forming a separate hole protocol. SEP-0005 remains the owner of per-hole field +names, dependency graph shape, and batch/single-hole query shape. + +## Diagnostics impact + +Hole diagnostics use `H0xxx` codes: + +| Code | Name | Meaning | +| ------- | ------------------------ | -------------------------------------------------- | +| `H0101` | hole-report | Informational report for an open hole | +| `H0102` | duplicate-hole-name | Hole name reused in a module | +| `H0201` | hole-outside-expression | Hole appears outside permitted expression position | +| `H0301` | circular-hole-dependency | Dependency graph has a cycle | +| `H0401` | realization-rejected | Proposed fill failed verification | + +## Drawbacks + +HoleReports can be large. Tools should support compact summaries and on-demand +single-hole queries. + +Automated realization can overfit to properties that are too weak. Human review +and richer property design remain important. + +## Alternatives considered + +### Anonymous holes only + +Rejected because named holes make CLI queries, reviews, and Agent coordination +stable. + +### Holes as hard errors + +Rejected because partial programs are a core collaboration state. + +### Separate Agent metadata files + +Rejected because the compiler can derive more reliable context directly from +source and typed IR. + +## Informational appendix: Realization workflow notes + +This workflow is an informative reference loop for Agents and tools. A +conforming HoleReport producer is not required to expose these states, and a +conforming Agent is not required to use this exact state machine. + +```text +DISCOVER -> ANALYZE -> PROPOSE -> VERIFY -> ACCEPT or REJECT +``` + +A proposed fill is reviewable when it is checked against type, effect, budget, +and property context from the HoleReport and either passes those checks or +produces evidence states that the selected policy accepts. + +## Informational appendix: Reference candidate ranker + +Implementations may rank candidates using the fields exposed by HoleReport. One +reference ranker combines type match, budget fit, required-effect fit, and error +coverage. This ranker is informative. Tools may use a different ranker when they +preserve the normative HoleReport data. + +```text +overall = 0.40 * type_match + + 0.20 * budget_fit + + 0.25 * required_effects_fit + + 0.15 * error_coverage +``` + +## Prior art + +Agda, Idris, GHC, and Lean influenced typed holes. Spore differs by making +machine-readable reports the primary design constraint while keeping Agent +workflows as replaceable policy. + +## Backward compatibility and migration + +Hole syntax remains source-compatible with named holes. Payload consumers must +adapt from older resource-oriented fields to `effect_context`, `budget_context`, +and `property_context`. + +## Unresolved questions + +1. Should type holes be specified in this SEP or a separate one? +2. Should failed realization attempts be persisted for Agent learning? +3. How should cross-module hole dependencies be represented? diff --git a/seps/SEP-0005-hole-system.md b/seps/SEP-0005-hole-system.md deleted file mode 100644 index 2a3dc1d..0000000 --- a/seps/SEP-0005-hole-system.md +++ /dev/null @@ -1,1445 +0,0 @@ ---- -sep: 5 -title: "SEP-0005: Hole System & Agent Protocol" -status: Draft -type: Standards Track -authors: - - Zhan Rongrui -created: 2026-03-31 -requires: - - 1 - - 2 - - 3 - - 4 -discussion: "https://github.com/spore-lang/spore-evolution/discussions/5" -pr: null -superseded_by: null ---- - -# SEP-0005: Hole System & Agent Protocol - -> **Executive Summary**: Defines typed holes (`?name`) as first-class language constructs that carry type, effect, and cost context. The machine surface is a shared typed hole protocol: `sporec holes FILE --json` emits a root object with `holes` and `dependency_graph`, and `sporec query-hole FILE ?name --json` returns the same per-hole object directly. This SEP defines the HoleReport schema, the Hole Dependency Graph with layered topological sort and parallel fill scheduling, and the Agent state machine protocol (DISCOVER → ANALYZE → PROPOSE → VERIFY → ACCEPT/REJECT) for autonomous hole filling workflows. - -## Summary - -This SEP specifies Spore's **Hole System** — a first-class language mechanism that treats unfinished code as a structured, typed, compiler-mediated collaboration interface between humans and AI Agents. - -A _hole_ is written `?name` (optionally `?name: Type`) in any expression position within a function body. The compiler accepts programs containing holes, classifies those functions as **partial**, and produces a shared typed hole report. The machine protocol uses one hole object across both batch and single-hole queries: `sporec holes FILE --json` emits `{ "holes": [...], "dependency_graph": ... }`, while `sporec query-hole FILE ?name --json` returns the matching hole object directly. Each hole object carries: `name`, `display_name`, `location`, `expected_type`, `type_inferred_from`, `function`, `enclosing_signature`, `bindings`, `binding_dependencies`, `available_effects`, `errors_to_handle`, `cost_budget`, `candidates`, `dependent_holes`, `confidence`, and `error_clusters`. The `cost_budget` field holds a scalar-style summary; a future extension may add checked 4D residual context. - -Multiple holes form a **Hole Dependency Graph** (DAG), enabling topological ordering and parallel filling by multiple Agents. The **Agent Protocol** defines a five-state machine (DISCOVER → ANALYZE → PROPOSE → VERIFY → ACCEPT/REJECT) for autonomous filling workflows. - -Key components formalized in this SEP: - -- **Hole syntax and semantics** (`?name`, `?name: Type`, partial functions) -- **HoleReport schema**, with extensions for: (A) candidate scoring vector, (B) binding dependency graph, (C) confidence & ambiguity, (D) error clusters -- **Hole Dependency Graph** with layered topological sort and parallel fill scheduling -- **Agent state machine protocol** for autonomous hole filling -- **JSON output protocol** via `--json` flag and NDJSON event stream - ---- - -## Motivation - -### The Problem with Unfinished Code - -Traditional languages treat unfinished code as an error — `todo!()`, `unimplemented!()`, `???` markers all produce runtime panics or compile-time failures. This forces developers into an all-or-nothing workflow: either the function is complete or it doesn't compile. There is no structured middle ground. - -This creates two problems: - -1. **For humans**: Developers cannot express partial intent. They know the function's contract (signature, error types, cost bound, effect requirements) but haven't decided on the implementation yet. Traditional languages offer no way to say "this part is intentionally unfinished, here is what I need" in a way the compiler understands. - -2. **For AI Agents**: Without structured information about what is missing, Agents must reverse-engineer context from error messages, heuristically guess types, and operate without compiler guidance. The quality ceiling is low and the failure rate is high. - -### Why Holes Must Be First-Class - -Making holes a first-class language construct enables: - -- **Compiler-mediated collaboration**: The compiler produces HoleReports that are _information-self-sufficient_ — an Agent reading a report needs zero additional context to attempt a fill. -- **Incremental development**: Functions transition smoothly from `partial` to `complete`. Downstream callers are not invalidated because holes are body-only — they never affect signature hashes. -- **Dependency-ordered filling**: The compiler can analyze data-flow between holes, build a DAG, and recommend an optimal filling order. -- **Cost-bounded filling**: each hole carries a `cost_budget` inherited from the enclosing function's declared budget. A future extension may add checked 4D residual context. - -### Why the Agent Protocol Matters - -AI Agents are not humans reading error messages. They are stateless processes that parse structured output. Spore's hole system is designed with Agents as a _primary_ consumer: - -- **HoleReport** keeps evolving by additive fields rather than by a detached schema-name reset; the current target additions replace human-readable strings (e.g., `match_quality: "partial"`) with machine-comparable scoring vectors. -- **Binding dependency graphs** let Agents understand data-flow without re-analyzing source code. -- **Confidence indicators** tell Agents when to auto-fill vs. when to request human guidance. -- **NDJSON event streams** allow Agents to consume compiler output in real-time, reacting to each incremental compilation result. - -Without a formal protocol, every Agent tool would invent its own ad-hoc interaction pattern with the compiler. This SEP standardizes that interaction. - ---- - -## Guide-level explanation - -### Hole Syntax: `?name` - -A hole is written with a `?` prefix followed by an identifier. Holes can appear anywhere an expression is expected: - -```spore -fn calculate_tax(income: Money, region: Region) -> Money ! InvalidRegion - uses [TaxTable] - cost [500, 100, 0, 1] -{ - ?tax_logic -} -``` - -The function compiles successfully. It is marked `partial` — not broken. - -Optionally, holes can carry a type annotation: - -```spore -let body: Str = ?report_body : Str -``` - -If the annotation conflicts with the inferred type, the compiler emits a `hole-type-conflict` warning (not an error — holes are exploratory). - -### Multiple Holes and Nested Holes - -A function may contain any number of independently named holes: - -```spore -fn reconcile(ledger: Ledger, txns: List[Tx]) -> Ledger ! Mismatch - cost [5000, txns.len * 4, 0, 1] -{ - let grouped = group_by_account(txns) - let adjustments = ?compute_adjustments - let validated = ?validate_adjustments - apply(ledger, validated) -} -``` - -Holes can also appear nested inside expressions, function arguments, and match arms: - -```spore -fn render(page: Page) -> Html ! TemplateError - uses [Templates] - cost [800, 400, 0, 1] -{ - let nav = render_nav(?nav_items) // hole as argument - let content = match page.kind { - Article(a) => render_article(a), - Gallery(g) => ?gallery_renderer, // hole in match arm - _ => render_fallback(page), - } - compose(nav, content) -} -``` - -The compiler infers that `?nav_items` must have the type `render_nav` expects as its first parameter, and `?gallery_renderer` must have the same type as the other match arms. - -### Partial Functions - -A function containing at least one hole is **partial**. Partial functions: - -| Property | Complete | Partial | -| ------------------------------ | -------------------- | ---------------------- | -| Can be compiled | ✓ | ✓ | -| Can be called at runtime | ✓ | ✗ | -| Can be simulated | ✓ | ✓ | -| Appears in module exports | ✓ | ✓ (marked `partial`) | -| Signature hash changes on fill | if signature changes | never (holes are body) | - -Partiality is transitive: a caller of a partial function is itself partial. - -### Agent Workflow Example - -A complete Human → Agent → Compiler interaction: - -**Step 1 — Human writes a function with holes:** - -```spore -fn generate_invoice( - customer: Customer, - items: List[LineItem], - tax_region: TaxRegion, -) -> Invoice ! TaxCalculationError | InvalidLineItem - uses [TaxTable] - cost [5000, items.len * 8, 0, 1] -{ - let validated_items = ?validate_items - let subtotal = sum(validated_items.map(|i| i.price * i.quantity)) - let tax = ?compute_tax - let total = subtotal + tax - Invoice.new(customer, validated_items, subtotal, tax, total) -} -``` - -**Step 2 — Agent queries holes:** - -```text -$ sporec query-hole src/billing/invoice.sp ?validate_items --json -``` - -The compiler returns a HoleReport (see §4 for the full structure) containing: expected type `List[LineItem]`, available bindings, candidate function `validate_line_items` with an exact type match, cost 300 within budget 5000. - -**Step 3 — Agent proposes a fill:** - -```spore -validate_line_items(items) -``` - -**Step 4 — Compiler verifies:** - -```text -$ spore check src/billing/invoice.sp - -[partial] generate_invoice - ?validate_items: filled ✓ (cost 300) - ?compute_tax: still open - budget for ?compute_tax: ≤4580 -``` - -Until the checker publishes full CostVector residuals in HoleReport, treat the -stable `cost_budget` field as schema-oriented metadata, not a proof of an exact -residual. - -**Step 5 — Agent fills the next hole, compiler confirms completion:** - -```text -[ok] generate_invoice : (...) -> Invoice ! TaxCalculationError | InvalidLineItem - cost: 520 (within budget of 5000) - all holes filled ✓ -``` - ---- - -## Reference-level explanation - -### Hole Syntax (Formal) - -```text -hole ::= '?' IDENT (':' type)? -IDENT ::= [a-z_][a-zA-Z0-9_]* -``` - -Holes are expressions. They can appear in any expression context. They **cannot** appear in: - -- Type positions (type holes `?T_Name` in uppercase are a separate concept) -- Signature positions (parameter types, return types, error lists, `with`/`cost`/`uses` clauses) -- Module-level declarations - -Hole names must be unique within a module. The compiler rejects duplicate hole names in the same module. - -Empty function bodies desugar to a single hole named `{function_name}_body`. - -### HoleInfo Structure - -The compiler-internal representation of a single hole uses the following structure: - -```rust -pub struct HoleInfo { - pub name: String, // from ?name - pub expected_type: Ty, // inferred/expected type - pub function: String, // enclosing function - pub bindings: BTreeMap, // local bindings at hole site - pub suggestions: List[Str], // candidate functions (unbounded list) -} -``` - -**HoleInfo vs HoleReport:** `HoleInfo` is the compiler-internal representation, while `HoleReport` is the JSON output format for machine/Agent consumption. `HoleInfo` is converted to `HoleReport` with additional computed fields (scores, confidence, error clusters). They serve different purposes: HoleInfo for compiler passes, HoleReport for external tooling. - -The batch `sporec holes FILE --json` response aggregates all holes in a module by -returning a root object with `holes` and `dependency_graph`; each element of -`holes` is the same per-hole `HoleReport` object returned directly by -`sporec query-hole FILE ?name --json`. - -### HoleReport schema - -HoleReport evolves by additive fields. The base fields are preserved; four extension groups are documented below. - -#### Base Fields - -| Field | Description | -| ------------------------------ | -------------------------------------------------------------------------------------------- | -| `hole.name` | Developer-assigned name | -| `hole.location` | Source location (file, line, column) | -| `hole.dependencies` | Names of upstream holes whose output feeds into this hole's context | -| `type.expected` | The type this hole must produce, including error variants | -| `type.inferred_from` | Human-readable explanation of why this type is expected | -| `bindings` | Variables in scope with name, type, and simulated value (`symbolic` or `computed`) | -| `available_effects` | The `uses` list available at the hole site | -| `errors_to_handle` | Error types not yet handled before the hole | -| `cost_budget.budget_total` | Scalar-style summary of the total declared budget | -| `cost_budget.cost_before_hole` | Cost accumulated before this hole (prefix snapshot) | -| `cost_budget.budget_remaining` | Estimated remaining budget (derived scalar; a future extension may add checked 4D residuals) | -| `candidates` | Functions in scope whose return type matches the hole's expected type | -| `dependent_holes` | Holes that become reachable when this hole is filled | -| `enclosing_function` | Full signature context of the containing function | - -#### Hole Type Inference Rule - -A hole's type is determined by the **intersection of all constraints** imposed by its context. Constraints flow from: - -1. **Let-binding annotations:** `let x: T = ?h` constrains `?h` to type `T` -2. **Return position:** A hole in tail position inherits the function's return type -3. **Function arguments:** `f(?h)` constrains `?h` to the parameter type of `f` -4. **Match arms:** A hole in a match arm must have the same type as sibling arms -5. **Operators:** `?h + x` where `x: I64` constrains `?h` to `I64` (or a type implementing `Add`) - -When multiple constraints agree, the intersection is the agreed-upon type. When constraints conflict, the compiler applies the **nearest constraint rule** (see Edge Cases §8.3) and emits a warning. - -When context provides **no constraints**, the hole is _unconstrained_ — reported with type `_`. The HoleReport lists available bindings so the Agent can propose a type. - -#### Hole as Match Scrutinee - -When the scrutinee of a `match` is a hole, the compiler infers the hole's type from the **pattern types** in the match arms: - -```spore -fn classify(data: RawData) -> Category ! ParseError -{ - match ?parsed_data { // hole as scrutinee - Valid(v) => categorize(v), - Invalid(e) => raise ParseError(e), - } -} -``` - -The compiler infers: `?parsed_data` must have a type with at least variants `Valid(_)` and `Invalid(_)`. If a type in scope matches (e.g., `Result[ValidData, ErrorInfo]`), it is reported as the inferred type. All branches are reported as **blocked** (since the scrutinee is unknown, no branch can execute during simulation). - -#### Extension A: Candidate Scoring Vector - -Replaces the coarse `match_quality: "exact" | "partial"` string with a four-dimensional numeric vector: - -| Dimension | Field | Range | Meaning | -| -------------- | ---------------------- | -------- | ---------------------------------------------------------- | -| Type match | `type_match` | `[0, 1]` | Return type + parameter type match degree | -| Cost fit | `cost_fit` | `[0, 1]` | Estimated cost vs. remaining budget fit | -| Effect fit | `required_effects_fit` | `{0, 1}` | All required effects available (boolean) | -| Error coverage | `error_coverage` | `[0, 1]` | Fraction of candidate's declared errors covered by context | - -This scoring vector is still a **target** contract. In particular, `cost_fit` -should ultimately be computed against checked residual context, but current -implementation work has not yet connected real residual checking to candidate -ranking. The stable `cost_budget` field should therefore be read as schema -compatibility, not as evidence that `cost_fit` is already authoritative today. - -**Type match formula:** - -```text -type_match(candidate, hole) = - 0.6 × type_similarity(candidate.return_type, hole.expected_type) - + 0.4 × mean(param_scores) -``` - -Where `type_similarity(A, B)` returns: 1.0 (exact), 0.9 (subtype), 0.7 (known conversion), 0.5 (same constructor), 0.0 (unrelated). - -**Cost fit formula:** - -```text -cost_fit(candidate, hole) = - if estimated_cost ≤ budget_remaining: - 1.0 - (estimated_cost / budget_remaining) × 0.3 - else: - max(0, 1.0 - (estimated_cost - budget_remaining) / budget_remaining) -``` - -**Overall score:** - -```text -overall = 0.40 × type_match + 0.20 × cost_fit + 0.25 × required_effects_fit + 0.15 × error_coverage -``` - -Weights are hard-coded in the compiler. Candidates are sorted by `overall` descending, with `type_match` as tiebreaker, then `cost_fit`, then lexicographic name for stability. - -Each candidate also includes an `adjustments` array of human-readable notes (e.g., `"needs type conversion: Option[Card] → Card"`, `"cost near budget limit"`). - -#### Prospective additive extensions (not yet stable output) - -The following extensions to the HoleReport schema are defined for future adoption: - -1. **`effect_context`** — active handler stack, already-discharged effects, and - the visible effect aliases / interfaces at the hole site. -2. **`residual_context`** — remaining checked obligations after the current - prefix, including a 4D cost vector (`budget_declared`, `cost_before`, - `budget_residual`) plus any still-unhandled effect / error obligations. -3. **`rejection_reasons`** — structured VERIFY/REJECT feedback explaining why a - proposed fill failed (for example: `type_mismatch`, `effect_leak`, - `budget_exceeded`, `duplicate_handler_match`). - -When adopted, these fields should be added to the existing schema as additive -fields, preserving backward compatibility. - -#### Extension B: Binding Dependency Graph - -An adjacency-list representation of data-flow between bindings at the hole site: - -```json -"binding_dependencies": { - "customer": [], - "amount": [], - "validated_card": ["customer"], - "order": ["customer", "amount"], - "validated": ["order"], - "charged": ["validated", "validated_card"] -} -``` - -Extracted from SSA-form data-flow analysis. Agents should prefer "terminal" bindings (high in-degree, low out-degree) as they carry the most computed information. - -#### Extension C: Confidence & Ambiguity - -```json -"confidence": { - "type_inference": "certain" | "partial" | "unknown", - "candidate_ranking": "unique_best" | "ambiguous" | "no_candidates", - "ambiguous_count": 0, - "recommendation": "gateway_charge is the best match with 0.91 overall score" -} -``` - -- `type_inference`: `"certain"` (fully determined), `"partial"` (constructor known, parameters unknown), `"unknown"` (no constraints) -- `candidate_ranking`: `"unique_best"` (gap ≥ 0.15 from second), `"ambiguous"` (gap < 0.15), `"no_candidates"` (empty list) -- `ambiguous_count`: number of candidates within ±0.15 of the best score - -#### Extension D: Error Clusters - -Groups errors by their source operation with handling suggestions: - -```json -"error_clusters": [ - { - "source": "gateway_charge", - "errors": ["PaymentFailed", "GatewayTimeout"], - "handling_suggestion": "match on error type, retry GatewayTimeout with backoff" - }, - { - "source": "validate_card", - "errors": ["InvalidCard"], - "handling_suggestion": "early return with ?" - } -] -``` - -Suggestion generation rules: - -| Pattern | Suggestion | -| -------------------------------------- | ----------------------- | -| Single error, propagable | `"early return with ?"` | -| Multiple errors, same source | `"match on error type"` | -| Retryable (contains `Timeout`/`Retry`) | `"retry with backoff"` | -| Error in enclosing function's `!` list | `"propagate to caller"` | - -#### Error System Integration (Three-Field Model) - -The enclosing function's declared error list (`! Err1 | Err2 | Err3`) is partitioned into three categories at each hole site: - -| Field | Meaning | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -| `errors_to_handle` | Errors not yet handled by code before the hole. The filling should handle or propagate these. | -| `errors_already_handled` | Errors that were caught/handled by code before the hole (e.g., via `catch` or `match`). The filling does not need to worry about these. | -| `errors_passthrough` | Errors that can propagate upward to the caller. The filling may also propagate them but does not need explicit handling. | - -Example: - -```spore -fn fetch_and_parse(url: Url) -> Document ! NetworkError | ParseError | Timeout - uses [Http] - cost [3000, 500, 100, 1] -{ - let response = http_get(url) // may raise NetworkError, Timeout - |> catch Timeout => retry_once(url) // Timeout handled here - ?parse_response -} -``` - -HoleReport for `?parse_response`: - -```json -{ - "errors_to_handle": ["ParseError"], - "errors_already_handled": ["Timeout"], - "errors_passthrough": ["NetworkError"] -} -``` - -- `ParseError`: the filling should handle or propagate it. -- `Timeout`: handled before the hole; no action needed. -- `NetworkError`: propagates to the caller; the filling may also propagate it without explicit handling. - -### Hole Dependency Graph - -#### Definition - -Given a module M, the Hole Dependency Graph G = (V, E) is: - -- **V** = set of all unfilled holes in M -- **E** = { (h₁, h₂) | h₂'s type inference or constraint solving depends on h₁'s output } - -Edges are classified into three dependency types: - -| Type | Notation | Meaning | -| ---------------- | -------- | ---------------------------------------------------------------------------- | -| Type dependency | `type` | h₂'s expected type contains a type variable solvable only after h₁ is filled | -| Value dependency | `value` | h₂'s available bindings include a value whose data-flow traces back to h₁ | -| Cost dependency | `cost` | h₂'s target checked residual context depends on h₁'s actual cost | - -#### Graph Construction - -```pseudocode -function build_hole_graph(module: TypedAST) -> Graph: - holes = find_all_holes(module) - edges = [] - - for each hole h in holes: - for each binding b in h.available_bindings: - if trace_data_source(b) is HoleOutput(h'): - edges.add(Edge(from=h', to=h, kind="value")) - - for each type_var tv in free_type_vars(h.expected_type): - if trace_type_source(tv) is HoleOutput(h'): - edges.add(Edge(from=h', to=h, kind="type")) - - if h.residual_context (or legacy cost_budget compatibility data) depends on another hole h': - edges.add(Edge(from=h', to=h, kind="cost")) - - return Graph(vertices=holes, edges=deduplicate(edges)) -``` - -`trace_data_source` follows SSA data-flow edges backward. `trace_type_source` follows type inference constraint chains backward. Both are exhaustive, guaranteeing no hidden dependencies. - -#### Data-Flow Tracing (`trace_data_source`) - -Traces an SSA binding backward to its origin: - -```pseudocode -function trace_data_source(binding: Binding) -> Source: - match binding.origin: - Parameter(p) => return ParameterSource(p) - LetBinding(expr) => return trace_expr_source(expr) - HoleOutput(h) => return HoleOutput(h) - FunctionCall(f, args) => - args.iter().find_map(|arg| - match trace_data_source(arg): - HoleOutput(h) => Some(HoleOutput(h)) - _ => None - ).unwrap_or(ConcreteSource(f)) -``` - -#### Type Constraint Tracing (`trace_type_source`) - -Traces a type variable backward through the constraint graph: - -```pseudocode -function trace_type_source(tv: TypeVar) -> Source: - constraints = get_constraints_for(tv) - constraints.iter().find_map(|c| - match c: - UnifyWith(HoleOutputType(h)) => Some(HoleOutput(h)) - UnifyWith(ConcreteType(t)) => Some(ConcreteSource(t)) - UnifyWith(OtherTypeVar(tv')) => Some(trace_type_source(tv')) - ).unwrap_or(Unconstrained) -``` - -#### Cycle Detection - -Circular hole dependencies are **compile errors**. Detection uses standard DFS coloring in O(|V| + |E|): - -```text -error[H0301]: circular hole dependency detected - --> src/order.sp - | - | ?validate_order depends on ?check_inventory (value dependency) - | ?check_inventory depends on ?validate_order (type dependency) - | - = help: break the cycle by providing a concrete type annotation on one hole -``` - -#### Cycle Fix Strategies - -When a circular dependency is detected, the compiler suggests three strategies: - -1. **Add type annotation:** Place an explicit type annotation on one hole in the cycle, breaking the type dependency: - - ```text - help: e.g., ?validate_order : ValidatedOrder - ``` - -2. **Split function:** Move the holes into separate functions. Each function's signature provides concrete type information, eliminating the circular inference: - - ```text - help: extract ?validate_order into a standalone function with an explicit signature - ``` - -3. **Introduce intermediate binding:** Add a `let` binding with a concrete type between the holes, breaking the value dependency chain: - - ```text - help: add `let intermediate: ConcreteType = ...` between the dependent holes - ``` - -#### Layered Topological Sort - -```pseudocode -function compute_fill_order(G: Graph) -> Result[List[Set[Hole]], CycleError]: - if has_cycle(G): return Err(CycleError(find_cycle(G))) - - layers = [] - remaining = copy(V) - in_degree = compute_in_degrees(G) - - while remaining is not empty: - ready = { h ∈ remaining | in_degree[h] == 0 } - layers.append(ready) - for each h in ready: - for each successor s of h: - in_degree[s] -= 1 - remaining = remaining - ready - - return Ok(layers) -``` - -- `layers[0]`: no-dependency holes — fill first (all parallelizable) -- `layers[k]`: holes whose dependencies all lie in layers 0..k-1 -- Within each layer, holes are ranked by **transitive dependents count** (impact heuristic) - -**Complexity:** O(|V| + |E|) for construction, cycle detection, and topological sort. - -#### Completeness Theorem - -**Theorem (Completeness).** If the Hole Dependency Graph G = (V, E) is a DAG, then `compute_fill_order` discovers and outputs all fillable holes. - -**Proof.** By induction on layer index k. - -**Base case (k = 0):** L₀ = { h ∈ V | in-degree(h) = 0 }. These holes have no predecessors — their types, bindings, and any cost dependencies are fully determined. They are fillable. Since we take _all_ zero in-degree nodes, no fillable hole is missed. - -**Inductive step (k → k+1):** Assume layers L₀ through Lₖ are correctly computed and all holes in them are filled. Let G' be the subgraph remaining after removing L₀ ∪ ... ∪ Lₖ. - -Lₖ₊₁ = { h ∈ V(G') | in-degree\_{G'}(h) = 0 } - -For any h ∈ Lₖ₊₁: all predecessors of h in G lie in L₀ ∪ ... ∪ Lₖ (already filled), so h's constraints are fully resolved — h is fillable. - -Conversely, if a fillable hole h ∉ L₀ ∪ ... ∪ Lₖ₊₁, then h has in-degree > 0 in G', meaning an unfilled predecessor exists — contradicting fillability. - -Therefore Lₖ₊₁ is exactly the set of newly fillable holes at round k+1. ∎ - -**Corollary.** The algorithm terminates in `depth(G) + 1` rounds, filling all holes. - -**Corollary.** Data-flow exhaustiveness: since `trace_data_source` and `trace_type_source` traverse all SSA edges and type constraint chains, no dependency is undetected. - -#### Complexity Analysis - -| Operation | Time Complexity | Notes | -| -------------------------------- | ---------------- | -------------------------------------------- | -| Graph construction | O(\|V\| × B) | B = average bindings per hole | -| Topological sort (layered) | O(\|V\| + \|E\|) | Kahn's algorithm variant | -| Cycle detection | O(\|V\| + \|E\|) | DFS coloring (by-product of topo sort) | -| Incremental update (single fill) | O(\|neighbors\|) | Only the filled hole's neighbors are touched | -| Parallel scheduling | O(\|V\|) | Traverse in-degree array to find ready set | -| JSON serialization | O(\|V\| + \|E\|) | Linear scan of graph structure | - -**Space complexity:** O(\|V\| + \|E\|) for the graph structure and in-degree table. - -#### Parallel Fill - -Independent holes within the same layer can be filled simultaneously by different Agents. Maximum parallelism equals the widest layer (Dilworth's theorem). - -Scheduling protocol: - -```text -Round 0: ready_set = layers[0] = {h1, h2} - Agent A1 ← h1, Agent A2 ← h2 (parallel) - -Round 1: h1, h2 verified ✓ → incremental update → new ready_set = {h3} - Agent A1 ← h3 - -Round 2: h3 verified ✓ → new ready_set = {h4, h5} (parallel) - ... - -Final: all holes filled → COMMIT -``` - -#### Incremental Update - -When a hole is filled, the graph updates incrementally in O(|neighbors|): - -1. Remove the filled hole from V -2. Remove all associated edges from E -3. Update in-degrees of successors; those reaching zero enter `ready_to_fill` -4. Check for newly revealed holes (filling may expose deeper code paths) -5. Emit a `hole_graph_update` event - -#### Agent Allocation Strategy - -```pseudocode -function assign_agents(ready: Set[Hole], agents: List[Agent]) -> Assignment: - ranked = rank_within_layer(ready, G) // sort by transitive dependents - assignment = {} - ranked.iter().enumerate().for_each(|(i, hole)| - agent = agents[i % agents.len()] // round-robin - assignment[hole] = agent - ) - return assignment -``` - -When fewer Agents are available than ready holes, round-robin assigns the highest-impact holes first. - -#### Conflict Resolution - -Two Agents must not simultaneously fill the same hole. Mutual exclusion uses file-level locking: - -```pseudocode -function try_fill(agent: Agent, hole: Hole) -> FillResult: - if not acquire_lock(hole.file, agent.id): - return FillResult { status: "conflict", hole: hole.name, - filled_by: lock_holder(hole.file) } - - result = agent.generate_fill(hole) - verify = sporec_check(hole.file) - - if verify.ok: - commit_fill(hole, result) - release_lock(hole.file, agent.id) - return FillResult { status: "success", hole: hole.name } - else: - rollback_fill(hole) - release_lock(hole.file, agent.id) - return FillResult { status: "verify_failed", errors: verify.errors } -``` - -### Agent State Machine - -The Agent protocol defines a five-state machine with no explicit RETRY state: - -```text - ┌────────────────────────────────────────────────┐ - │ │ - ▼ │ - ┌──────────┐ │ - │ DISCOVER │── spore watch --json │ - └────┬─────┘ receive hole list + dependency graph │ - │ │ - │ select a hole from ready_to_fill │ - ▼ │ - ┌──────────┐ │ - │ ANALYZE │── sporec query-hole FILE ?name --json │ - └────┬─────┘ receive full HoleReport │ - │ │ - │ generate fill code │ - ▼ │ - ┌──────────┐ │ - │ PROPOSE │── write fill code to source file │ - └────┬─────┘ (atomic: one hole per write) │ - │ │ - │ file change triggers incremental compilation │ - ▼ │ - ┌──────────┐ │ - │ VERIFY │── await compile_result event │ - └────┬─────┘ │ - │ │ - ┌──────┴──────┐ │ - │ │ │ - ▼ ▼ │ - ┌──────────┐ ┌──────────┐ │ - │ ACCEPT │ │ REJECT │─── diagnostics returned ────────────┘ - └────┬─────┘ └──────────┘ Agent reads diagnostics, - │ re-enters PROPOSE or ANALYZE - │ update graph, recompute ready_to_fill - │ - │ more unfilled holes? - ├── yes → return to DISCOVER - └── no → COMMIT (all holes filled) -``` - -**DISCOVER**: The Agent discovers work via `sporec holes FILE --json` (batch) or `spore watch --json` (event stream). Both surfaces expose hole metadata and dependency information. - -**ANALYZE**: For each selected hole, the Agent requests its HoleReport via -`sporec query-hole FILE ?name --json`. It examines: - -- `confidence.candidate_ranking`: - - `"unique_best"` → use the top candidate directly - - `"ambiguous"` → compare `scores` vectors, consult `adjustments` - - `"no_candidates"` → construct code from scratch using `type`, `bindings`, `binding_dependencies` -- `error_clusters` → determine error handling strategy -- `cost.budget_remaining` → ensure proposed fill fits within budget - -**PROPOSE**: The Agent writes fill code into the source file, replacing `?name`. This is an atomic operation — one hole per write. The Agent must only reference bindings visible at the hole site (as listed in `bindings`). - -**VERIFY**: The `spore watch` process detects the file change, triggers incremental compilation, and emits a `compile_result` event: - -```json -{ - "event": "compile_result", - "file": "/tmp/spore-step9-watch.sp", - "status": "ok", - "errors": [], - "timestamp": 1775999403 -} -``` - -**ACCEPT**: Compilation succeeded. The hole is marked `filled`. The dependency graph is recalculated, possibly unlocking blocked holes. The Agent returns to DISCOVER. - -**REJECT**: Compilation failed. The watch event carries compiler diagnostics. Structured `rejection_reasons` (defined in the Prospective extensions section above) provide normalized machine-readable failure causes. A sample rejection payload: - -```json -{ - "diagnostics": { - "errors": [ - { - "code": "E0301", - "message": "type mismatch: expected List[ValidItem], found List[RawItem]", - "location": { "file": "src/orders.sp", "line": 18, "column": 5 }, - "suggestion": "consider using validate_items(raw_input).map(|i| i.into())" - } - ], - "root_cause": "type_mismatch", - "fix_hints": [ - "candidate validate_items returns List[RawItem], not List[ValidItem]", - "try: validate_items(raw_input).map(|i| i.into())" - ] - } -} -``` - -The Agent reads diagnostics, understands the failure, and autonomously decides to re-enter PROPOSE with a corrected fill or return to ANALYZE to select a different candidate. - -**Design decisions:** - -- **No RETRY state**: REJECT returns full diagnostics; the Agent simply re-enters PROPOSE or ANALYZE. An explicit RETRY state would add complexity without information. -- **No ESCALATE state**: If the Agent cannot fill a hole after repeated attempts, it marks it `agent_skipped`, continues with other `ready_to_fill` holes, and reports skipped holes in the final summary. - ---- - -### Edge Cases - -#### Recursive Holes - -A hole in a recursive function is valid — `?name` is just an expression of some type. It does not call itself. The compiler detects recursive calls to partial functions and terminates simulation at depth 1: - -```spore -fn factorial(n: I64) -> I64 - cost [1000, 200, 0, 1] -{ - if n <= 1 { 1 } - else { n * ?factorial_step } -} -``` - -Here `?factorial_step` has type `I64`. The function is partial. If a recursive call to `factorial` occurs: - -```spore -fn bad(n: I64) -> I64 -{ - ?self_ref + bad(n - 1) // bad is partial; recursive call also partial -} -``` - -The compiler reports: - -```text -[partial] bad: (I64) -> I64 - holes: ?self_ref - note: recursive call to partial function bad — simulation terminates at depth 1 -``` - -Simulation does not recurse into partial functions; it stops and emits a HoleReport at the hole site. - -#### Type Holes (`?T_Name`) - -Type holes are syntactically distinct from value holes. They use `?T_name` (uppercase convention) in **type positions** and represent unknown types: - -```spore -fn convert(input: ?InputType) -> ?OutputType -{ - ?conversion_logic -} -``` - -**Semantics (experimental):** - -- Type holes make the entire **signature** incomplete. Unlike value holes (body-only), type holes affect the function's public contract. -- The function **cannot** be exported (its signature hash is undefined). -- The compiler emits a `type-hole` diagnostic, distinct from value-hole diagnostics. -- Type holes are a **design-time sketching tool**, not a production feature. - -**Reporting:** - -```text -$ sporec holes FILE --type-holes - -Type holes: - ?InputType in convert (src/convert.sp:1) — parameter type - ?OutputType in convert (src/convert.sp:1) — return type - -Value holes: - ?conversion_logic in convert (src/convert.sp:5) — body -``` - -**Intended behavior:** When both type holes and value holes are present, the compiler first attempts to infer type holes from usage context (e.g., if `convert` is called with a known argument type). If inference succeeds, the value hole's expected type becomes determined. If inference fails, the value hole is reported with type `_` (unconstrained). - -> **Status:** Type holes are marked experimental. The full specification is deferred to a future SEP. This section documents the intended semantics for design continuity. - -#### Conflicting Constraints - -When a hole has contradictory type constraints from its context, the compiler reports the conflict without failing: - -```spore -fn conflicted(flag: Bool) -> I64 -{ - let x: Str = ?ambiguous // constraint 1: Str (from let binding) - let y: I64 = x // constraint 2: I64 (from assignment) - y -} -``` - -**Resolution rule:** The compiler uses the **nearest constraint** — the one syntactically closest to the hole. In this case, the `let x: Str` annotation is nearest, so `?ambiguous` is reported with type `Str`. - -```text -warning[H0002]: hole ?ambiguous has conflicting type constraints - constraint 1: Str (from let binding on line 4) - constraint 2: I64 (from usage on line 5) - note: no type satisfies both constraints simultaneously. - The hole is reported with type `Str` (nearest constraint). - Filling this hole will likely require restructuring the surrounding code. -``` - -The hole still appears in `--holes` output and still receives a HoleReport. The Agent sees the conflict and can propose a restructuring. - -#### Holes in Closures - -Closures can contain holes. The hole's context captures both the closure's own parameters and any outer bindings captured by the closure: - -```spore -fn make_processor(config: Config) -> Fn(Data) -> Result ! ProcessError -{ - |data: Data| -> Result ! ProcessError { - ?process_with_config - } -} -``` - -The HoleReport for `?process_with_config` includes both the closure parameter `data` and the captured `config`: - -```json -{ - "bindings": [ - { - "name": "data", - "type": "Data", - "simulated_value": { "kind": "symbolic", "origin": "closure parameter" } - }, - { - "name": "config", - "type": "Config", - "simulated_value": { - "kind": "symbolic", - "origin": "captured from make_processor" - } - } - ] -} -``` - -The `origin` field distinguishes closure parameters from captured bindings, enabling Agents to understand the binding's provenance. - -#### Holes and the `pure` Property - -A hole in a function inferred as `pure` (i.e., `uses []`) is itself pure by definition — it does nothing. The compiler does not flag a purity violation for the hole's presence. However, the **filling** must satisfy the purity constraint: - -**Formal rule:** If function `f` has `uses []` (and is therefore inferred as `pure`), any expression that replaces a hole `?h` in `f`'s body must also be pure — it may not call functions requiring IO or State effects. - -```spore -fn pure_fn(x: I64) -> I64 -{ - ?must_be_pure // ok: hole is inert -} -``` - -If the Agent fills with an impure expression: - -```text -[error] pure_fn is inferred as pure (uses []) but the filling of ?must_be_pure - calls print() which requires effect: IO -``` - ---- - -## Human experience impact - -### Ergonomics - -- **Named holes are mandatory**. The cost of naming is low; the benefit to communication is high. "Fill `?payment_logic`" is actionable. "Fill the hole on line 42" is fragile. -- **Holes are warnings, not errors**. Code with holes compiles. Developers can run tests on completed paths while other paths remain partial. -- **Suggested filling order** is advisory, never enforced. `spore holes --suggest-order` recommends a topological order, but developers can override freely. - -### CLI interface - -```text -$ sporec holes FILE # list all holes in one file -$ sporec holes FILE --json # machine-readable -$ sporec query-hole FILE ?name # full HoleReport for one hole -$ sporec query-hole FILE ?name --json # JSON format -$ sporec simulate FILE fn_name # multi-path simulation -$ spore holes --suggest-order # topological ordering -$ spore holes --graph # dependency graph visualization -$ spore fill ?name # Agent workflow for one hole -$ spore fill --all # Agent fills all holes in order -``` - -### Multi-path simulation - -For partial functions, the compiler simulates all paths independently. A hole in one branch does not block simulation of other branches: - -```text -$ sporec simulate src/router.sp route - -Path 1: request.method = GET → completes normally (cost: 450) -Path 2: request.method = POST → reaches ?handle_post (HoleReport emitted) -Path 3: request.method = PUT → reaches ?handle_put (HoleReport emitted) -Path 4: request.method = _ → completes normally (cost: 20) - -Summary: 2/4 paths complete, 2 holes encountered -``` - ---- - -## Agent experience impact - -This is the central section of SEP-0005. The hole system is designed with AI Agents as a **primary consumer**, not an afterthought. - -### Information Self-Sufficiency - -A HoleReport object is **self-contained**. An Agent -reading a report needs zero additional context to attempt a fill. The report -includes: - -1. **What to produce**: `type.expected` with `type.inferred_from` explaining why -2. **What is available**: `bindings` with types, simulated values, and `binding_dependencies` showing data-flow -3. **What is permitted**: `available_effects` (what the fill can call), `cost.budget_remaining` (how expensive it can be) -4. **What errors to handle**: `errors_to_handle` flat list plus `error_clusters` with per-source grouping and handling suggestions -5. **What functions might work**: `candidates` with multi-dimensional `scores`, `overall` ranking, and `adjustments` -6. **How confident the compiler is**: `confidence.type_inference` and `confidence.candidate_ranking` -7. **What depends on this hole**: `dependent_holes` (motivating fill priority) - -This design eliminates the "context gathering" phase that plagues current AI coding tools. The Agent does not need to read surrounding code, navigate imports, or guess types — the compiler has already done that work. - -### Scoring Vectors Enable Principled Selection - -The four-dimensional scoring vector (`type_match`, `cost_fit`, `required_effects_fit`, `error_coverage`) enables Agents to make decisions based on numeric comparison rather than string parsing. An Agent's selection logic becomes: - -```text -if confidence.candidate_ranking == "unique_best": - select candidates[0] -elif confidence.candidate_ranking == "ambiguous": - for each candidate in candidates: - if candidate.scores.required_effects_fit == 0: - skip # hard constraint - weight type_match and cost_fit by task priority - select best by adjusted score -elif confidence.candidate_ranking == "no_candidates": - synthesize code from bindings + type constraints -``` - -### Binding Dependencies Enable Data-Flow Reasoning - -Without `binding_dependencies`, an Agent sees a flat list of bindings and might reference one that logically depends on another not yet computed. With the dependency graph, Agents can: - -- Prefer terminal bindings (high in-degree, low out-degree) as primary inputs -- Verify that all transitive dependencies of used bindings are fully resolved -- Construct code that follows the natural data-flow direction - -### Parallel Filling Protocol - -In the **long-term richer protocol**, multiple Agents can fill independent holes simultaneously: - -1. All Agents receive the same `hole_graph_update` event -2. Each Agent claims a different hole from `ready_to_fill` -3. Agents work in parallel; the compiler validates each fill independently -4. On ACCEPT, a new `hole_graph_update` unlocks blocked holes -5. Agents re-enter DISCOVER to claim newly available holes - -Today, the stable implementation approximates this by combining the batch hole report (`sporec holes FILE --json`) with summary watch events. - -**Conflict handling**: If two Agents attempt the same hole, the first writer wins (file-level lock). The second receives a `CONFLICT` signal and selects another hole: - -```json -{ - "event": "compile_result", - "status": "conflict", - "hole": "validate_input", - "message": "hole already filled by another agent" -} -``` - -### Failure Recovery - -On REJECT, the Agent receives structured diagnostics with `root_cause` classification (`type_mismatch`, `cost_exceeded`, `effect_violation`, `error_unhandled`) and `fix_hints`. This is richer than typical compiler errors: - -- `root_cause` categorization lets the Agent choose a recovery strategy without natural-language parsing -- `fix_hints` provide specific, actionable suggestions -- The original `attempt.code` is preserved for diff analysis - -If an Agent fails repeatedly on a hole, it should: - -1. Record all attempts and diagnostics -2. Mark the hole as `agent_skipped` -3. Continue processing other `ready_to_fill` holes -4. Include skipped holes with full diagnostics in the final report - -### Real-Time Event Stream - -`spore watch --json` emits newline-delimited JSON events: - -```text -{"event":"compile_result","file":"/tmp/spore-step9-watch.sp","status":"ok","errors":[],"timestamp":1775999403} -{"event":"hole_graph_update","holes_total":1,"filled_this_cycle":0,"ready_to_fill":1,"blocked":0} -``` - -Event types: - -| Event | Trigger | Data | -| ------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------- | -| `compile_result` | Each incremental compilation cycle | File path, status (`ok`/`error`), diagnostics payload, timestamp | -| `hole_graph_update` | After a compile cycle that still contains holes | Hole-count summary: `holes_total`, `filled_this_cycle`, `ready_to_fill`, `blocked` | - -This stream supports a practical DISCOVER/VERIFY loop: agents can watch compile success or failure and use the hole summary to decide when to re-run `sporec holes FILE --json` or inspect a specific hole with `sporec query-hole FILE ?name --json`. - -A future extension of this transport may add full dependency-graph payloads and per-hole update events. When that richer transport is adopted, it should extend the shared hole/diagnostic model rather than inventing a separate schema. - -```pseudocode -while line = read_line(stdin): - event = parse_json(line) - match event.event: - "compile_result" => handle_compile_result(event) - "hole_graph_update" => refresh_hole_summary(event) -``` - -### End-to-End Example: Multi-Agent Session - -```text -Timeline Agent-1 Agent-2 Compiler -────────────────────────────────────────────────────────────────────────────── -t0 DISCOVER DISCOVER hole_graph_update (5 holes) - ready: [validate_input, check_auth] - -t1 ANALYZE(validate_input) ANALYZE(check_auth) (parallel) - -t2 PROPOSE: validate_order PROPOSE: verify_token (parallel writes) - (order) (token) - -t3 incremental compile - -t4 ACCEPT ✓ ACCEPT ✓ hole_graph_update (3 remaining) - ready: [reserve_stock, process_payment] - -t5 ANALYZE(reserve_stock) ANALYZE(process_payment) (parallel) - -t6 PROPOSE: reserve_items PROPOSE: charge_card (parallel writes) - (valid.items, warehouse_id) - ← ERROR: undefined binding! - -t7 REJECT ✗ ACCEPT ✓ compile_result - diagnostics: "undefined: - warehouse_id" - -t8 PROPOSE: reserve_items incremental compile - (valid.items, - valid.warehouse_id) - -t9 ACCEPT ✓ hole_graph_update (1 remaining) - ready: [send_receipt] - -t10 ANALYZE(send_receipt) - -t11 PROPOSE: send_email(receipt, valid.email) incremental compile - -t12 ACCEPT ✓ hole_graph_update (0 remaining) - -t13 COMMIT ── all holes filled ───────────────────────────── ✓ done -``` - ---- - -## Structured representation / protocol impact - -### JSON Output Format - -All hole-related commands support `--json` for machine consumption. - -**`sporec holes FILE --json`:** emits the batch hole report. The root object contains `holes` plus `dependency_graph`. - -The JSON example below shows the `cost_budget` field as a scalar summary. A future `residual_context` extension (see Prospective extensions) may add checked 4D residual vectors. - -```json -{ - "holes": [ - { - "name": "charge_payment", - "display_name": "?charge_payment", - "location": { "file": "src/orders.sp", "line": 18, "column": 5 }, - "expected_type": "Response ! PaymentFailed", - "type_inferred_from": "return position in `process_order`", - "function": "process_order", - "enclosing_signature": "fn process_order(...) -> Response ! PaymentFailed", - "bindings": { - "order": "Order", - "validated": "ValidatedOrder" - }, - "binding_dependencies": { - "order": [], - "validated": ["order"] - }, - "available_effects": ["Inventory", "PaymentGateway"], - "errors_to_handle": ["PaymentFailed"], - "cost_budget": { - "budget_total": 5000, - "cost_before_hole": 1300, - "budget_remaining": 3700 - }, - "candidates": [ - { - "name": "gateway_charge", - "type_match": 1.0, - "cost_fit": 0.92, - "required_effects_fit": 1.0, - "error_coverage": 1.0, - "overall": 0.97 - } - ], - "dependent_holes": ["send_receipt"], - "confidence": { - "type_inference": "certain", - "candidate_ranking": "unique_best", - "ambiguous_count": 0, - "recommendation": "gateway_charge is the best match" - }, - "error_clusters": [ - { - "source": "gateway_charge", - "errors": ["PaymentFailed"], - "handling_suggestion": "early return with ?" - } - ] - } - ], - "dependency_graph": { - "dependencies": { "?send_receipt": ["?charge_payment"] }, - "edges": [ - { "from": "?charge_payment", "to": "?send_receipt", "kind": "value" } - ], - "roots": ["?charge_payment"], - "suggested_order": ["?charge_payment", "?send_receipt"] - } -} -``` - -**`sporec query-hole FILE ?name --json`:** returns the single hole object directly, with the same shared fields used inside `holes[*]`. - -**`spore watch --json`:** already emits NDJSON `compile_result` plus summary-style `hole_graph_update` events. Richer per-hole / full-graph watch payloads remain a later transport layer. - -### Hole Dependency Graph JSON - -The `dependency_graph` embedded in the batch hole report is the authoritative machine-readable graph today: - -```json -{ - "dependencies": { - "?process_order": ["?validate_input", "?check_auth"], - "?send_receipt": ["?process_order"] - }, - "edges": [ - { "from": "?validate_input", "to": "?process_order", "kind": "value" }, - { "from": "?check_auth", "to": "?process_order", "kind": "value" }, - { "from": "?process_order", "to": "?send_receipt", "kind": "type" } - ], - "roots": ["?validate_input", "?check_auth"], - "suggested_order": [ - "?validate_input", - "?check_auth", - "?process_order", - "?send_receipt" - ] -} -``` - -### LSP Integration - -The hole system integrates with Language Server Protocol: - -- **Diagnostics**: Holes appear as `Information`-level diagnostics (not `Error`), with the hole name and expected type in the message. -- **Code Actions**: "Fill hole `?name`" triggers the Agent workflow. "Show HoleReport" displays the JSON in a preview panel. -- **Inlay Hints**: The expected type of each hole is shown as an inlay hint. -- **Completion**: At a hole site, the completion list includes candidate functions from the HoleReport, ranked by `overall` score. -- **Code Lens**: Partial functions display a code lens showing hole count and suggested filling order. - ---- - -## Diagnostics impact - -### Hole-specific diagnostics - -| Code | Severity | Message | -| ------- | -------- | -------------------------------------------------------------- | -| `H0001` | Info | `hole ?name: expected type T` — standard hole report | -| `H0002` | Warning | `hole-type-conflict: annotation T1 conflicts with inferred T2` | -| `H0003` | Info | `partial function F depends on partial function G` | -| `H0101` | Error | `duplicate hole name ?name in module M` | -| `H0201` | Error | `hole ?name in signature position (not allowed)` | -| `H0301` | Error | `circular hole dependency: ?A → ?B → ?A` | -| `H0401` | Info | `filling of ?name revealed new hole ?other` | - -### Cost diagnostics for partial functions - -```text -[partial] pipeline: (List[I64]) -> List[I64] - known cost: 200 (before hole) + ≤150 (after hole) = ≤350 - budget for ?middle_step: ≤650 (1000 - 350) - note: ?middle_step filling must stay within the remaining compute budget (≤ 650) -``` - -### Post-fill diagnostics - -On successful fill: `?name: filled ✓ (cost N)`. -On failed fill: full diagnostic with `root_cause`, `fix_hints`, and `suggestion` per error. - ---- - -## Drawbacks - -1. **Increased compiler complexity**: The hole system adds simulated execution, HoleReport generation, dependency graph construction, and incremental update logic. This is non-trivial implementation effort. - -2. **Risk of "hole proliferation"**: Developers may over-use holes as a crutch, leaving too many unfilled and degrading project quality. Mitigation: CI tools can enforce maximum hole counts or block merges with unfilled holes. - -3. **HoleReport size**: For complex functions, HoleReports can be large (many bindings, many candidates). This increases compiler output size and Agent processing time. - -4. **Scoring weight rigidity**: The hard-coded weights (0.40, 0.20, 0.25, 0.15) may not be optimal for all projects. However, making them configurable increases cognitive burden, and the expected variance across projects is low. - -5. **Serialization fragility**: The serialization format is intentionally simple. A more structured schema-driven encoder may be adopted in the future without changing the external payload contract. - -6. **Parallel fill coordination overhead**: File-level locking for multi-Agent fills adds synchronization cost. For small projects this overhead dominates the benefit. - ---- - -## Alternatives considered - -### Alternative 1: Anonymous Holes (`?`) - -Rejected. Anonymous holes create ambiguity in CLI queries (`sporec query-hole FILE ?name`) and prevent clear communication between human and Agent. No mainstream language with typed holes (Agda, Idris, Lean) defaults to anonymous holes in practice — users always name them. - -### Alternative 2: Holes Affect Signature Hashes - -Rejected. If holes changed the signature hash, downstream dependents would need recompilation every time a hole is filled — even though the _contract_ never changed. This breaks snapshot stability during development. - -### Alternative 3: Explicit Priority Annotations on Holes - -Rejected. No mainstream dependently-typed language includes priority annotations. The compiler's dependency analysis provides a better filling order because it reflects actual data flow. Manual priority would be redundant at best and misleading at worst. - -### Alternative 4: Interactive IDE Mode Instead of JSON CLI - -Rejected. Agda's hole system is powerful but tightly coupled to Emacs. GHC's typed holes produce text for humans. Spore targets stateless Agents that parse structured output. JSON CLI is the natural primary interface; IDE support is layered on top. - -### Alternative 5: Configurable Scoring Weights - -Rejected for now. Configurable weights increase cognitive burden, and the optimal weights are unlikely to vary significantly across projects. If strong evidence emerges, a future SEP can introduce `spore.toml` configuration. - -### Alternative 6: Explicit RETRY and ESCALATE States in Agent Protocol - -Rejected. RETRY adds state machine complexity without new information — REJECT already returns full diagnostics, and the Agent re-enters PROPOSE or ANALYZE naturally. ESCALATE assumes human availability; the Agent protocol is designed for full autonomy with graceful degradation (`agent_skipped`). - ---- - -## Prior art - -### Agda - -Agda's holes (written `?` or `{! !}`) are the closest precedent. Agda provides typed holes with goal type, context bindings, and refinement commands. Key differences from Spore: - -- Agda holes are interactive (Emacs/VS Code mode); Spore holes are CLI/JSON-first -- Agda has no cost system, effect system, or scoring vectors -- Agda has no concept of parallel Agent filling -- Agda holes can appear in type positions; Spore separates type holes from value holes - -### Idris 2 - -Idris 2's `?name` syntax directly inspired Spore's. Idris provides type-driven development with holes, case splitting, and proof search. Differences: - -- Idris's hole information is consumed interactively; Spore produces self-contained JSON reports -- Idris has no dependency graph between holes -- Idris has no Agent protocol - -### GHC Typed Holes - -GHC's `_` syntax produces type error messages listing the expected type and bindings in scope. Differences: - -- GHC typed holes are errors (compilation fails); Spore holes are valid states -- GHC output is human-readable text; Spore output is structured JSON -- GHC has no candidate ranking or scoring - -### Lean 4 - -Lean 4's `sorry` and `_` produce goal states with the expected type and local context. Lean's tactic mode allows interactive proof construction. Differences: - -- Lean is proof-oriented; Spore is software-engineering-oriented (cost, effects, errors) -- Lean has no cost budget or effect constraints on holes -- Lean has no parallel fill protocol - -### Rust `todo!()` / TypeScript `// TODO` - -These are runtime markers with no compiler support. They provide no type information, no candidate suggestions, and no structured output. Spore's holes are the typed, compiler-aware evolution of these patterns. - ---- - -## Backward compatibility and migration - -### Schema Evolution - -- HoleReport stays on the current schema; additive extensions - must not force a detached naming story. -- HoleReport objects are JSON objects without an explicit - schema tag; any future schema identifier should avoid embedding a - design revision label. -- Existing fields are preserved with identical semantics. -- New fields such as `binding_dependencies`, `confidence`, `error_clusters`, - `candidates[].scores`, `candidates[].overall`, and - `candidates[].adjustments` are additive. -- Tools that do not recognize newer fields can safely ignore them. - -### CLI Flags - -- `--json` flag behavior is extended, not changed -- `spore watch --json` is a new command (no existing command replaced) -- All existing `sporec holes`, `sporec query-hole`, `sporec simulate` commands continue to work - -### Hole Syntax - -- `?name` syntax is new to Spore; no existing syntax is displaced -- `?name: Type` annotation is optional and new -- Empty function bodies desugar to `?{fn_name}_body` — this is a new behavior but affects only empty bodies (previously a parse error) - -### Migration Path - -1. **Existing complete code**: Unaffected. No holes means no HoleReports, no dependency graphs, no Agent protocol activation. -2. **New code with holes**: Opt-in by writing `?name` in function bodies. -3. **Agent tooling**: Agents should treat the payload as an additive shared schema and ignore unknown additive fields. If an explicit schema identifier is emitted, it should avoid embedding a design revision label. - ---- - -## Unresolved questions - -### Design questions - -1. **Cross-module hole dependencies**: The current dependency graph is scoped to a single module. How should holes that depend on partial functions in other modules be represented? The `partial` marker propagates, but the graph does not yet span modules. - -2. **Type holes**: This SEP covers value holes (`?name`). Type holes (`?T_Name` in uppercase positions) are mentioned but not fully specified. Should they be part of this SEP or a separate one? - -3. **Scoring weight tuning**: The hard-coded weights (0.40, 0.20, 0.25, 0.15) are based on first-principles reasoning, not empirical data. Should we run experiments before finalizing? - -4. **Agent identity and coordination**: The multi-Agent protocol uses file-level locking. Should Agents have explicit identities? Should there be a coordinator process, or is the decentralized claim-and-lock protocol sufficient at scale? - -5. **Hole attempt history**: When a hole is filled, rejected, and re-filled, should the system maintain a history of fill attempts? This would enable better Agent learning but increases storage. - -6. **Partial function exports**: The current design marks partial functions in module exports. Should importers be able to depend on partial functions (receiving symbolic values), or should partial functions be hidden from external modules? - -7. **DAG visualization**: The specification includes ASCII DAG output. Should a standard visual format (DOT/Graphviz, Mermaid) be part of the protocol, or should visualization remain an editor/tooling concern outside the stable machine payload? - -8. **Dynamic priority adjustment**: Should the filling order adapt based on Agent performance history (e.g., prioritize holes similar to ones the Agent has successfully filled before)? - -9. **Cost dependency precision**: Cost dependencies assume sequential execution within a block. For branches, should cost dependencies be path-sensitive? - -### Implementation follow-up - -The hand-rolled JSON serializer should eventually be replaced with serde or a -schema-driven encoder. That migration should preserve the external payload -contract unless a future SEP deliberately defines a breaking schema split. diff --git a/seps/SEP-0006-compiler-architecture.md b/seps/SEP-0006-compiler-architecture.md index ffe672a..c206b0c 100644 --- a/seps/SEP-0006-compiler-architecture.md +++ b/seps/SEP-0006-compiler-architecture.md @@ -19,1949 +19,280 @@ superseded_by: null # SEP-0006: Compiler Architecture -> **Executive Summary**: Defines a 5-phase compiler pipeline (Lex → Parse → Resolve → TypeCheck → Codegen) with SEP-aligned diagnostic codes organized by category — E0xxx (type errors), C0xxx (effect violations), K0xxx (cost budget), M0xxx (module errors), W0xxx (warnings). The architecture centers on a shared diagnostics pipeline consumed by CLI JSON, LSP, and the hole/reporting surfaces, while `spore watch --json` currently provides summary NDJSON events (`compile_result` plus `hole_graph_update`) and leaves richer watch transports as a later layer. +> **Executive Summary**: Defines the Spore compiler pipeline under the signature model. The compiler parses Base and Intent Signatures, lowers source properties into internal Claims, verifies realizations, and emits EvidenceRecords with provenance hashes for signatures, intents, properties, realizations, checkers, and dependencies. ## Summary -This SEP specifies the architecture of the Spore compiler (`sporec`), covering the -complete compilation pipeline from source text to native code. The design is -organized around three intermediate representations — **AST**, **HIR**, and -**TypedHIR** — with **Cranelift IR** serving as the low-level backend -representation. Five major passes (lex, parse, resolve+desugar, -typecheck+capcheck+costcheck, codegen) transform source programs through these -layers. - -Incremental compilation is achieved through demand-driven memoized computation with a -two-tier content-addressed hashing strategy: **sig hash** (computed at the -Resolve layer) controls downstream recompilation, and **impl hash** (computed at -the TypeCheck layer) controls codegen caching. A **watch mode** provides -real-time diagnostics via human-readable terminal output or machine-consumable -NDJSON events. - -An initial implementation may use a tree-walking interpreter; native code -generation is deferred to a later stage. - ---- - -## Motivation - -A language that targets both human developers and AI agents as first-class users -needs a compiler whose architecture is: - -1. **Transparent** — every compilation stage produces an observable, queryable - representation so that diagnostics can pinpoint _exactly_ what went wrong, - where, and why. -2. **Incremental** — developers and agents iterate rapidly; the compiler must - recompile only what changed, propagating invalidation no further than - necessary. -3. **Multi-audience** — diagnostics must serve terminal users (concise, colored), - IDE clients (LSP-compatible JSON), CI pipelines (streaming NDJSON), and agents - (structured data with inference chains, candidate lists, and machine-applicable - fixes). -4. **Lean** — Spore has no borrow checker, no comptime evaluation, and no - lifetime analysis. The IR stack should reflect this simplicity: three layers - are sufficient where Rust needs five and Zig needs four. - -Existing compiler architectures either carry unnecessary complexity (Rust's MIR -for borrow checking, Zig's ZIR for comptime) or are too simplistic for Spore's -needs (Gleam's two-layer AST lacks a desugared IR). This SEP defines a pipeline -tuned precisely to Spore's feature set: algebraic types, effects, cost -models, holes, and content-addressed modules. - ---- - -## Guide-level explanation - -### Compiling a Spore program - -A developer interacts with the compiler pipeline through the project-facing -`spore` CLI and the lower-level `sporec` CLI: - -```bash -# Project-aware build -spore build - -# Project-aware check -spore check src/main.sp - -# Watch mode — recompile on save, show diagnostics in real time -spore watch src/main.sp - -# Watch mode with JSON output for agents / IDEs -spore watch --json src/main.sp - -# Explicit-input compiler invocation -sporec compile src/main.sp - -# Explain an error code -sporec explain E0301 - -# Query a specific hole -sporec query-hole --json src/main.sp ?tax_logic -``` - -### What the developer sees - -**On success:** - -```text -$ spore build - Compiling main (src/main.sp) - Compiling http (src/net/http.sp) - Compiling auth (src/auth/login.sp) - Finished build in 1.4s — 3 modules, 0 errors, 0 warnings -``` - -**On error (default mode):** - -```text -error[E0301]: type mismatch - --> src/billing.sp:42:22 - | -42 | charge(card, "fifty dollars") - | ^^^^^^^^^^^^^^^ expected `Money`, found `Str` - | -help: try `Money.from_string("fifty dollars")` -``` - -**In watch mode:** - -```text -$ spore watch src/main.sp -[watch] Watching project: ./my-project (42 modules) -[watch] ✓ Initial compilation complete (1.2s), 0 errors, 2 warnings, 5 holes - -[watch] Waiting for file changes... - -[watch] ─── Change detected ───────────────────── -[watch] File changed: src/auth/login.sp -[watch] Recompiling: src/auth/login.sp ... OK (34ms) -[watch] sig hash unchanged → skipping downstream - ✓ 0 errors, 2 warnings, 4 holes (was: 5) -``` - -### Error output modes - -| Mode | Flag | Audience | Contract | -| ----------- | ----------- | ----------------------------------- | ---------------------------------------------------------------------------- | -| **Default** | _(none)_ | Developers | Human-readable projection of the canonical Diagnostic IR | -| **Verbose** | `--verbose` | Developers debugging complex errors | Default projection plus optional analysis detail not required by the base IR | -| **JSON** | `--json` | CI/CD, scripts, LSP, agents | Machine-readable projection of the canonical Diagnostic IR | - -The architectural target is one shared Diagnostic IR with multiple projections. -`Default` and `--json` are two renderings of the same diagnostics; `--verbose` -adds extra analysis detail on top of that shared model rather than defining a -separate protocol. - -> The shared diagnostics direction is the design target: CLI JSON, LSP, -> and hole-reporting surfaces should be projections over the same -> compiler-facing model. - ---- - -## Reference-level explanation - -### Pipeline overview - -```text -Source Text (.sp files) - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Pass 1: LEX │ -│ Source text → Token stream (with Span) │ -│ ─ Whitespace-aware, tracks file ID + byte offsets │ -└──────────────────────────────────────────────────────────────────────────┘ - │ Token Stream - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Pass 2: PARSE │ -│ Token stream → AST (untyped, source-faithful, preserves all sugar) │ -│ ─ Pratt parser for expressions │ -│ ─ Every node wrapped in Spanned for precise error reporting │ -└──────────────────────────────────────────────────────────────────────────┘ - │ AST - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Pass 3: RESOLVE + DESUGAR │ -│ AST → HIR (desugared, names resolved) │ -│ ─ Name resolution: every identifier → DefId │ -│ ─ Import resolution: module paths → concrete references │ -│ ─ Visibility checking: pub / pub(pkg) / private │ -│ ─ Desugar: |>, ?, f"...", t"..." → canonical forms │ -│ ─ Cycle detection in dependency graph │ -│ ─ Hole registration: record ?name positions and scopes │ -│ ─ ** sig hash computed here ** │ -└──────────────────────────────────────────────────────────────────────────┘ - │ HIR + sig hash - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Pass 4: TYPECHECK + CAPCHECK + COSTCHECK │ -│ HIR → TypedHIR (fully typed, effect-verified, cost-verified) │ -│ ─ Bidirectional type inference (signatures annotated, bodies inferred) │ -│ ─ Trait / Effect resolution (Effect = Trait) │ -│ ─ Associated types + GAT instantiation │ -│ ─ Const generics evaluation │ -│ ─ Exhaustiveness checking (match expressions) │ -│ ─ Error set propagation and consistency (! Errors) │ -│ ─ Refinement types: L0 decidable predicates + L1 abstract propagation │ -│ ─ Effect check: body uses ⊆ declared uses, module ceiling check │ -│ ─ Cost check: abstract interpretation of 4D cost vector │ -│ ─ Hole report generation with full context │ -│ ─ ** impl hash computed here ** │ -└──────────────────────────────────────────────────────────────────────────┘ - │ TypedHIR + impl hash - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Pass 5: CODEGEN │ -│ TypedHIR → Native Code │ -│ ─ Codegen backend serves as LIR (no separate low-level IR) │ -│ ─ Function-level granularity (fits content-addressed caching) │ -│ ─ Tail-call optimization (TCO) applied here │ -│ ─ Pattern matching lowered to branches/jumps │ -│ ─ Initial implementation may use tree-walking interpreter │ -└──────────────────────────────────────────────────────────────────────────┘ - │ - ▼ - Native executable / object code -``` - -### Diagnostics architecture - -The diagnostics architecture is split into three layers: - -1. **crate-local typed errors** - - parser, type checker, module loader, codegen runtime, and CLI code each use - their own error enums or domain-specific diagnostic builders -2. **shared Diagnostic IR** - - `sporec-diagnostics` owns the canonical `Diagnostic` model and related - label/span/reference types -3. **renderers and adapters** - - human CLI rendering - - JSON serialization - - LSP mapping - - future watch/event-stream transports - -This separation is intentional: - -- Implementation-facing error types belong at crate boundaries -- Terminal-formatting belongs only to the human-rendering layer -- The core compiler should not depend on terminal-formatting concerns -- JSON and LSP should be derived from the same IR, not maintained as parallel - ad hoc structures - -The diagnostics architecture aims for a shared `Diagnostic` IR crate that feeds -all renderers (human CLI, JSON, LSP, and future watch/event-stream transports). - -### IR layers - -#### Layer 1: AST (Abstract Syntax Tree) - -**Purpose:** Source-faithful representation for error reporting, IDE support, and -formatting tools. - -Properties: - -- 1:1 correspondence with source text -- All nodes carry `Span` (file ID + byte offset range) -- **Preserves all syntactic sugar:** `|>`, `?`, `f"..."`, `t"..."` -- Used for: precise error location reporting, syntax highlighting, code - formatting (`spore format` / `spore fmt`) - -Core data structures: - -```text -Spanned { node: T, span: Span } -Span { file: FileId, start: u32, end: u32 } - -Module { items: Vec> } -Item = FnDef | StructDef | TypeDef | TraitDef | Import | ... -FnDef { - name, params, return_type, error_set, - where_clause, cost_clause, uses_clause, - body -} -Expr = Literal | Ident | BinOp | UnaryOp | Call - | Pipe | TryOp | FString | TString - | Match | If | Lambda | Block | Hole | ... -``` - -#### Layer 2: HIR (High-level IR) - -**Purpose:** Simplified, canonical representation with all names resolved and -all sugar removed. This is the first "semantic" representation. - -The Resolve pass performs: - -- **Name resolution:** every identifier bound to a unique `DefId` -- **Import resolution:** module paths → concrete module references -- **Visibility checking:** `pub` / `pub(pkg)` / `private` -- **Circular dependency detection** -- **Hole recording:** marks `?name` positions with current scope information - -The Desugar pass (merged into Resolve) canonicalizes all syntactic sugar: - -| Sugar | Desugared form | -| ----------------- | ----------------------------------------------------------- | -| `a \|> f(b)` | `f(a, b)` | -| `expr?` | `match expr { Ok(v) => v, Err(e) => return Err(e.into()) }` | -| `f"hello {name}"` | `InterpolatedStr([Literal("hello "), Expr(name)])` | -| `t"hello {name}"` | `Template([Literal("hello "), Interpolation(name)])` | - -**sig hash is computed at this layer.** It covers: - -- Function name, parameter types, return type -- Error set declarations -- Effect / effect declarations -- Cost declarations - -It does **not** cover function bodies. When sig hash is unchanged, downstream -dependent modules skip resolve and type-check entirely. - -#### Layer 3: TypedHIR (Typed High-level IR) - -**Purpose:** Fully type-checked, effect-verified, cost-verified IR. This is -the input to codegen. - -The unified TypeCheck pass performs: - -| Sub-pass | Responsibility | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| **Type inference** | Bidirectional: signatures fully annotated, function bodies inferred | -| **Trait resolution** | Trait/Effect resolution, associated types, GAT instantiation | -| **Const generics** | Evaluation of compile-time constant generic parameters | -| **Exhaustiveness** | Verify match expressions cover all variants | -| **Error sets** | Propagation and consistency of `! ErrorType` declarations | -| **Refinement types** | L0 decidable predicates + L1 abstract interpretation propagation | -| **CapCheck** | Function body effect usage ⊆ declared `uses` set; module ceiling check | -| **CostCheck** | Abstract interpretation of 4D cost vector: `compute(op) + alloc(cell) + io(call) + parallel(lane)`; verify ≤ declared bound | -| **Hole reports** | Generate full context: type, available bindings, effect set, cost budget, candidate functions | - -Effect checking is merged into TypeCheck because `Effect = Trait` in -Spore — effect resolution shares the same infrastructure as trait resolution. - -Cost checking is merged into TypeCheck because cost analysis depends on resolved -types and call-graph information already computed during type checking. - -**impl hash is computed at this layer.** It covers the fully type-checked HIR. -Partial functions (containing holes) have `impl hash = None`. When impl hash is -unchanged, codegen is skipped entirely — the cached Cranelift output is reused. - -#### Cranelift IR as LIR - -There is **no separate low-level IR**. Cranelift IR serves as the LIR: - -- Cranelift operates at function-level granularity, which fits naturally with - Spore's content-addressed caching model -- Pattern matching is lowered to branch/jump instructions during codegen -- Tail-call optimization (TCO) is applied during the TypedHIR → Cranelift - translation -- An initial implementation may use a tree-walking interpreter; the codegen - pass is a clean abstraction boundary - -### Design rationale for IR layer count - -```text -┌─────────────────────────────────────────────────────────────────────┐ -│ Why 3 IR layers, not more? │ -├─────────────────────┬───────────────────────────────────────────────┤ -│ Rust has MIR │ Spore has no borrow checker → no MIR needed │ -│ Zig has ZIR │ Spore has no comptime → no flat IR needed │ -│ Rust has LLVM IR │ Cranelift IR serves as LIR directly │ -│ Gleam has 2 layers │ Spore needs HIR for desugaring + resolution │ -└─────────────────────┴───────────────────────────────────────────────┘ -``` - -The 3-layer design (AST → HIR → TypedHIR) hits a sweet spot: enough structure -to support precise incremental compilation (sig hash at HIR, impl hash at -TypedHIR) without unnecessary passes that exist only to serve features Spore -does not have. - -### Incremental compilation - -#### Incremental compilation strategy - -The compiler uses demand-driven incremental computation. Each compilation pass is memoized as a tracked query with automatic invalidation on input changes. The pass signatures are: - -```text -lex(file) → TokenStream -parse(tokens) → Ast -resolve(ast) → Hir // side product: sig_hash -type_check(hir) → TypedHir // side product: impl_hash, hole_reports, diagnostics -codegen(typed_hir) → CompiledModule -``` - -#### Two-tier hash strategy - -The incremental compilation strategy relies on two content-addressed hashes: +The compiler pipeline is: ```text - ┌──────────────────────┐ - │ File changed? │ - └──────────┬───────────┘ - │ - ┌──────────▼───────────┐ - │ Compute impl hash │ - └──────────┬───────────┘ - │ - ┌─────────────┴─────────────┐ - │ │ - impl hash unchanged impl hash changed - │ │ - Skip everything ┌────────▼────────┐ - (content dedup) │ Compute sig hash │ - └────────┬────────┘ - │ - ┌──────────────┴──────────────┐ - │ │ - sig hash unchanged sig hash changed - │ │ - Recompile this module Recompile this module - Skip downstream Mark downstream for - recheck (direct deps) +Lex -> Parse -> Resolve -> TypeCheck -> Verify -> Codegen ``` -**sig hash** covers: exported type/function signatures, effect requirements, -cost annotations. It does _not_ cover: function bodies, private definitions, -comments, internal hole states. +Three intermediate representations are used: -**impl hash** covers: the fully type-checked module content. Partial functions -(with holes) have `impl hash = None`. +- AST: parsed surface syntax +- HIR: resolved names, normalized signatures, and desugared expressions +- TypedHIR: typed bodies, hole reports, claims, and verification inputs -**spec hash** covers: `spec` blocks attached to functions, trait method -signatures, and `impl` methods. It is separate from `sig hash` so behavioral -contracts can be edited without changing the callable API. It is also separate -from `impl hash` so test metadata can be tracked even when a body is still -partial. +Evidence generation is part of verification. Evidence is not source text. -| Change | Signature hash | Spec hash | Required approval | -| -------------------------------------------- | -------------- | ------------------------------------ | ----------------- | -| Add a `spec` block where none existed | unchanged | changed | `--permit-spec` | -| Add, modify, or remove an `example` or `law` | unchanged | changed | `--permit-spec` | -| Change a callable signature clause | changed | unchanged unless `spec` also changed | `--permit` | - -Incremental scenarios: - -| Scenario | Example | What happens | -| -------------------------- | ------------------------------------- | ---------------------------------------------------- | -| **A: impl hash unchanged** | Only a comment changed | Skip entirely (<1ms) | -| **B: impl only** | Changed function body, same signature | Recompile this module, skip downstream | -| **C: sig changed** | Changed function parameter type | Recompile this module + cascade to direct dependents | - -#### Dependency graph traversal - -When a sig hash changes, the compiler walks the dependency graph in topological -order. At each level, it recompiles affected modules in parallel, then checks -whether _their_ sig hashes changed before propagating further. This ensures -minimal recompilation: - -```text -fn compile_batch(modules: Set, graph: DepGraph): - let levels = topological_levels(modules, graph) - levels.iter().for_each(|level| - level.par_iter().for_each(|module_id| - compile(module_id) - update_diagnostics(module_id) - update_hole_report(module_id) - ) - ) -``` - -Independent modules within the same topological level are compiled in parallel. -Default parallelism = CPU core count; override with `--jobs N`. - -#### Change detection pseudocode - -```pseudocode -fn on_file_changed(path: Path) -> ChangeResult: - let source = read_file(path) - let module_id = resolve_module(path) - let new_impl_hash = hash_impl(source) - - if new_impl_hash == cache.get_impl_hash(module_id): - return ChangeResult::Unchanged +## Motivation - let new_sig_hash = compute_sig_hash(source) - let old_sig_hash = cache.get_sig_hash(module_id) - cache.update(module_id, new_impl_hash, new_sig_hash) +Spore is built for humans and Agents to share the same compiler facts. The +compiler must therefore expose: - if new_sig_hash == old_sig_hash: - return ChangeResult::ImplOnly(module_id) - else: - return ChangeResult::SigChanged(module_id) -``` +- normalized signatures; +- typed hole reports; +- internal claims derived from properties; +- structured diagnostics; +- evidence records bound to stable provenance hashes. -### Watch mode +## Guide-level explanation -#### Command and flags +### Checking a file ```bash -spore watch src/main.sp # Watch one entry file / project target -spore watch --json src/main.sp # NDJSON output for IDEs / agents -``` - -#### Debounce strategy - -File system events are coalesced before triggering recompilation: - -```text -t=0ms File A changed -t=20ms File B changed ─┐ debounce window (100ms) -t=80ms File C changed │ -t=100ms Window expires ─┘ -t=100ms Begin incremental compile {A, B, C} -``` - -- **Debounce window:** 100ms (coalesce all changes within the window) -- **Maximum wait:** 500ms (cap for sustained bursts, e.g. `git checkout`) - -#### NDJSON event stream - -`spore watch --json` already emits newline-delimited JSON, but the **current stable** contract is intentionally smaller than the long-term watch transport: - -```json -{"event":"compile_result","file":"/tmp/spore-step9-watch.sp","status":"ok","errors":[],"timestamp":1775999403} -{"event":"hole_graph_update","holes_total":1,"filled_this_cycle":0,"ready_to_fill":1,"blocked":0} -``` - -Current stable events: - -| Event | Trigger | Data | -| ------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------- | -| `compile_result` | Each incremental compilation cycle | File path, status (`ok`/`error`), diagnostics payload, timestamp | -| `hole_graph_update` | After a compile cycle that still contains holes | Hole-count summary: `holes_total`, `filled_this_cycle`, `ready_to_fill`, `blocked` | - -This summary stream is enough for save-compile feedback loops and for agents that pair watch mode with the richer batch hole commands (`sporec holes FILE --json`, `sporec query-hole FILE ?name --json`). - -Future revisions may layer more detailed events — for example per-hole updates or full dependency-graph payloads — but those richer transports should extend the shared diagnostic / hole model rather than defining a competing watch-only schema. - -#### Error recovery - -Watch mode **never exits** (except on Ctrl+C). Recovery behavior: - -| Situation | Strategy | -| ------------------- | ----------------------------------------------------------- | -| Syntax / type error | Report diagnostic, retain last successful compilation state | -| Circular dependency | Report error, interrupt affected module subtree | -| File deleted | Remove from dependency graph, mark dependents as errored | -| Compiler panic | Catch and report as internal error, continue watching | - -#### Complete watch terminal output - -```text -$ spore watch src/main.sp -[watch] Watching project: ./my-project (42 modules) -[watch] ✓ Initial compilation complete (1.2s), 0 errors, 2 warnings, 5 holes - - Warnings: - src/net/http.sp:23:5 unused import: `Timeout` [W0102] - src/db/query.sp:45:12 unused effect: `FileSystem` [W0105] - - Holes (5): - ◯ src/auth/login.sp:30 authenticate : User -> Token - ◯ src/auth/login.sp:45 validate_token : Token -> Bool - ◯ src/db/query.sp:12 execute_query : Query -> Result - ◯ src/net/http.sp:67 handle_request : Request -> Response - ◯ src/net/http.sp:89 parse_headers : Bytes -> Headers - -[watch] Waiting for file changes... -``` - -#### Hole fill progressive watch session - -```text -$ spore watch src/main.sp -[watch] ✓ Initial compilation complete, 0 errors, 3 holes - - Holes (3): - ◯ src/auth.sp:10 hash_password : Str -> HashedPassword - ◯ src/auth.sp:20 verify_password : Str -> HashedPassword -> Bool - ◯ src/app.sp:50 create_user : UserInput -> Result User Error - ⚠ blocked: depends on hash_password, verify_password - Ready to fill: hash_password, verify_password - -// --- Developer fills hash_password --- - -[watch] ─── Change detected ──────────────────────── -[watch] File changed: src/auth.sp -[watch] Recompiling: src/auth.sp ... OK (28ms) -[watch] sig hash unchanged → skipping downstream - ✓ 0 errors, 2 holes (was: 3) - ● hash_password [filled ✓] - ◯ verify_password : Str -> HashedPassword -> Bool - ◯ create_user ⚠ blocked: depends on verify_password - Ready to fill: verify_password - -// --- Developer fills verify_password --- - -[watch] ─── Change detected ──────────────────────── -[watch] File changed: src/auth.sp -[watch] Recompiling: src/auth.sp ... OK (31ms) -[watch] sig hash changed → checking downstream: - └─ src/app.sp ... OK (22ms) - ✓ 0 errors, 1 hole - ● verify_password [filled ✓] - ◯ create_user → no longer blocked! - Ready to fill: create_user - -// --- Developer fills create_user --- - -[watch] ─── Change detected ──────────────────────── -[watch] File changed: src/app.sp -[watch] Recompiling: src/app.sp ... OK (35ms) - ✓ 0 errors, 0 holes 🎉 All holes filled! -``` - -#### Current hole summary and future graph structures - -```text -type HoleGraphSummary: - holes_total: Nat - filled_this_cycle: Nat - ready_to_fill: Nat - blocked: Nat - -// Current stable `hole_graph_update` NDJSON payload -// Future richer watch transport (not yet the stable payload shape) -type HoleGraphUpdate: - total: Nat - filled_this_cycle: List Hole - ready_to_fill: List Hole // dependencies satisfied, can implement now - blocked: List BlockedHole // has unmet dependencies - -type Hole: - module: ModuleId - location: SourceLocation - name: Str - signature: Type - -type BlockedHole: - hole: Hole - blocked_by: List ModuleId // modules containing unfilled dependency holes -``` - -Current `spore watch --json` serializes `hole_graph_update` as the compact -`HoleGraphSummary` shape shown above. `HoleGraphUpdate` remains the target -extension type for a later richer watch transport that can carry per-hole -details without overloading the stable summary contract. - -#### Module dependency graph structures - -```text -type DepGraph: - nodes: Map ModuleId ModuleInfo - edges: Map ModuleId (Set ModuleId) // module → modules it depends on - reverse: Map ModuleId (Set ModuleId) // module → modules depending on it - -type ModuleInfo: - id: ModuleId - path: Path - impl_hash: Hash - sig_hash: Hash - effects: Set Effect - cost_annotation: CostBound - holes: List Hole -``` - -#### Dependency graph incremental update - -```pseudocode -fn update_dep_graph(module_id: ModuleId, old_deps: Set, new_deps: Set): - // Remove stale edges - (old_deps - new_deps).iter().for_each(|dep| - graph.edges[module_id].remove(dep) - graph.reverse[dep].remove(module_id) - ) - // Add new edges - (new_deps - old_deps).iter().for_each(|dep| - graph.edges[module_id].insert(dep) - graph.reverse[dep].insert(module_id) - ) - // Cycle check - if has_cycle(graph, module_id): - emit_error("circular dependency", module_id) -``` - -#### Effect propagation watch output - -```text -[watch] sig hash changed (effect set changed) -[watch] Effect change: HttpClient: {Network} → {Network, FileSystem} -[watch] Checking project effect ceiling... - ceiling allows: {Network, FileSystem, Clock} → ✓ within bounds -[watch] Downstream effect compatibility: - ├─ src/api/client.sp → OK - └─ src/app.sp → propagation stops -[watch] ✓ 3 modules recompiled, 0 errors, 1 effect warning -``` - -When the ceiling is exceeded: - -```text -[watch] ✗ Error: HttpClient added effect `Crypto` - Project ceiling: {Network, FileSystem, Clock} - `Crypto` not in ceiling — update ceiling or remove usage -``` - -#### Cost propagation watch output - -```text -[watch] Cost change: sort: O(n·log n) → O(n²) -[watch] Checking downstream cost budgets: - ├─ src/data/table.sp - │ sort_column budget: O(n·log n) → ✗ exceeded - └─ src/app.sp - process_data budget: O(n²) → ✓ within bounds -[watch] ✗ 1 error +spore check src/main.sp +spore check --json src/main.sp +sporec query-hole src/main.sp ?build_response --json ``` -#### LSP diagnostics mapping +A successful check may still report open holes. A complete realization has no +open holes and has evidence for the required claims. -LSP diagnostics are published directly from the compiler's shared diagnostics pipeline rather than by replaying watch NDJSON events: +### Properties become claims -```json -{ - "method": "textDocument/publishDiagnostics", - "params": { - "uri": "file:///project/src/auth.sp", - "diagnostics": [ - { - "range": { - "start": { "line": 22, "character": 9 }, - "end": { "line": 22, "character": 24 } - }, - "severity": 1, - "code": "E0301", - "source": "spore", - "message": "type mismatch: expected `Token`, found `Str`" - } - ] - } +```spore +fn add(a: I64, b: I64) -> I64 +properties { + commutative(a: I64, b: I64): add(a, b) == add(b, a) } -``` - -A custom `spore/holeUpdate` notification is a **future** extension, not the current stable LSP contract. If it is added later, it should reuse the same hole object/schema already exposed by `sporec holes` and `sporec query-hole`, rather than freezing a separate editor-only payload. - -```json { - "method": "spore/holeUpdate", - "params": { - "holes": [ - { - "uri": "file:///project/src/auth.sp", - "name": "authenticate", - "status": "ready_to_fill" - } - ], - "summary": { "total": 2, "ready": 1, "blocked": 1 } - } + a + b } ``` -### CLI implementation stack - -The `spore` binary is built on a curated set of Rust crates chosen for -correctness, minimal footprint, and future Spore self-hosting feasibility: +The parser records a source property. SEP-0002 first checks the property body as +an ordinary Spore expression whose result type must be `Bool` and whose required +effects must fit inside the enclosing effect context. After that check, the +compiler lowers the property into an internal `Claim` with normalized subject, +parameters, predicate expression, effect context, and source span. -| Crate | Purpose | Bootstrap path | -| ------------ | ----------------------------------- | ----------------------------------------- | -| `bpaf` | Argument parsing (derive-based) | Parser combinator — natural fit for Spore | -| `ariadne` | Diagnostic rendering (span-based) | Thin wrapper on ANSI formatting | -| `owo-colors` | Terminal color output | Pure ANSI escape sequences | -| `notify` | File system watching (`watch` mode) | System call wrapper | -| `serde_json` | JSON serialization (`--json` flag) | Spore will need JSON support | -| `tracing` | Structured logging (`--verbose`) | Replaceable with print-based logging | +A `Claim` may originate from a source `properties` item, a refinement obligation, +a budget check, an effect check, or a validator. Source properties and +refinement obligations share the same `Claim` and `EvidenceRecord` structure so +tools do not need a separate property protocol. -**Self-hosting consideration:** Every crate above wraps a concept that Spore -must eventually implement natively (parsing, formatting, file I/O, JSON). The -Rust CLI serves as the reference implementation; the Spore bootstrap will -reimplement these as Spore libraries using the `basic-cli` platform. +### Evidence records -### Diagnostic output formats - -#### Default format - -Rust-inspired, concise, color-coded: +Evidence records answer: what was checked, by which checker, against which +realization, with which result? ```text -[]: - --> :: - | -NN | - | - | - = note: -help: +EvidenceRecord +├── subject +├── claim +├── checker +├── result +└── provenance ``` -Color scheme: errors in red, warnings in yellow, notes in blue, help in green. -Colors are disabled when output is piped (not a TTY) or when `--no-color` is -passed. - -Human-facing diagnostics should be **help-rich** when the compiler has a -concrete next step to offer. The minimal shared IR keeps `help` optional so -global diagnostics and self-explanatory failures are not forced to invent a -fake suggestion. - -#### Verbose format (`--verbose`) - -Appends additional analysis blocks after each diagnostic: - -- **Inference chain:** step-by-step type derivation -- **Candidates considered:** possible conversions, overloads, alternative - functions -- **Effect context:** which effects are in scope at the error site -- **Cost context:** cost used so far vs. budget - -Example: - -```text -error[E0301]: type mismatch - --> src/billing.sp:42:22 - | -42 | charge(card, "fifty dollars") - | ^^^^^^^^^^^^^^^ expected `Money`, found `Str` - | -help: try `Money.from_string("fifty dollars")` - - inference chain: - "fifty dollars" : Str (literal, line 42) - charge.amount : Money (from signature, sig@b3c1e2) - Str ≠ Money (nominal mismatch) - - candidates considered: - Str -> Money via Money.from_string ✓ (exact match) - Str -> Money via Money.parse ✓ (may raise ParseError) - - effect context: [PaymentGateway] - cost at this point: 120 / budget 500 -``` - -#### JSON format (`--json`) - -The long-term single source of truth is the shared Diagnostic IR. The first -stabilized machine contract should freeze only the minimal canonical fields and -make richer analysis payloads an explicit later layer. - -#### Canonical Diagnostic IR (minimal architecture target) - -```json -{ - "code": "E0301", - "severity": "error", - "message": "type mismatch: expected `Money`, found `Str`", - "primary_span": { - "file": "src/billing.sp", - "range": { - "start": { "line": 42, "col": 22 }, - "end": { "line": 42, "col": 37 } - } - }, - "secondary_labels": [ - { - "span": { - "file": "src/billing.sp", - "range": { - "start": { "line": 42, "col": 5 }, - "end": { "line": 42, "col": 11 } - } - }, - "message": "callee expects `Money` here" - } - ], - "notes": ["enclosing function `charge_customer` uses `PaymentGateway`"], - "help": "try `Money.from_string(\"fifty dollars\")`", - "related": [] -} -``` - -The base IR deliberately excludes richer fields such as inference chains, -candidate rankings, effect/cost context blobs, and machine-edit payloads. -Those may be layered later as extension data or adjacent protocols, but they are -not part of the minimal stable contract. - -#### Diagnostic field requirements - -| Field | Type | Minimal canonical | Required | Description | -| ------------------ | -------------- | :---------------: | ------------ | ----------------------------------------------------------- | -| `code` | string | ✓ | **required** | Stable diagnostic code, for example `E0301` | -| `severity` | string | ✓ | **required** | `error`, `warning`, or `note` | -| `message` | string | ✓ | **required** | Human-readable headline | -| `primary_span` | object \| null | ✓ | **required** | Primary source location; `null` only for global diagnostics | -| `secondary_labels` | array | | optional | Additional labeled spans related to the same diagnostic | -| `notes` | string[] | | optional | Supplemental context lines | -| `help` | string | | optional | One actionable fix hint or next step | -| `related` | array | | optional | References to related diagnostics or locations | - -The **minimal canonical** fields (`code`, `severity`, `message`, `primary_span`) form the stable machine contract. The remaining fields are extension fields that may be absent without breaking consumers. - -```json -{ - "primary_span": { - "file": "src/billing.sp", - "range": { - "start": { "line": 42, "col": 22 }, - "end": { "line": 42, "col": 37 } - } - } -} -``` - -```json -{ - "secondary_labels": [ - { - "span": { - "file": "src/billing.sp", - "range": { - "start": { "line": 42, "col": 5 }, - "end": { "line": 42, "col": 11 } - } - }, - "message": "callee expects `Money` here" - } - ], - "related": [ - { - "code": "E0302", - "message": "return type mismatch", - "span": { - "file": "src/billing.sp", - "range": { - "start": { "line": 51, "col": 5 }, - "end": { "line": 51, "col": 19 } - } - } - } - ] -} -``` - -> The shared-model architecture is the design target: hole -> commands expose a shared typed-hole protocol, and CLI JSON plus LSP are -> treated as projections over the same diagnostics pipeline. The remaining -> design work is freezing the minimal canonical field set and extending richer -> analysis/watch transports without introducing command-private schemas. - -#### Error code system - -All diagnostics carry categorized codes: - -| Prefix | Category | Examples | -| ------- | ----------------- | --------------------------------------------- | -| `E0xxx` | Type errors | type mismatch, missing field, arity error | -| `W0xxx` | Warnings | unused variable, redundant pattern, shadowing | -| `C0xxx` | Effect violations | undeclared effect, exceeding ceiling | -| `K0xxx` | Cost violations | budget exceeded, unbounded call | -| `H0xxx` | Hole diagnostics | hole report, partial function, type conflict | -| `M0xxx` | Module errors | circular dependency, visibility violation | - -Every code is queryable: `sporec explain E0301` prints a detailed explanation -with common causes, examples, and fix strategies. +## Reference-level explanation -#### `sporec explain` output format +### Pipeline ```text -$ sporec explain E0301 - - E0301: type mismatch - - This error occurs when an expression has a type that is incompatible - with the type expected by its context. Common contexts include: - - - Function arguments (actual type ≠ parameter type) - - Return statements (expression type ≠ declared return type) - - Variable bindings (right side type ≠ left side annotation) - - Struct fields (value type ≠ field type) - - Match arms (arm type ≠ sibling arm types) - - Pipe operator (left-hand type ≠ right-hand function's first parameter) - - Example: - fn greet(name: Str) -> Str { ... } - greet(42) // error: expected Str, found I64 - - Common fixes: - - Use a conversion function (e.g. `I64.to_string(42)`) - - Change the declaration to accept the actual type - - Add a type annotation to guide inference - - See also: E0302 (return type mismatch), E0401 (constraint not satisfied) +parse(source) -> Ast +resolve(ast) -> Hir +check(hir) -> TypedHir +verify(typed_hir) -> VerificationBundle +codegen(typed_hir) -> Artifact ``` -### Performance targets - -Target latencies (aspirational, not hard guarantees): - -| Operation | Target | Notes | -| -------------------------------------------- | ------- | --------------------------------------------------------- | -| Single module recompilation | < 100ms | Type check + effect + cost | -| Dependency graph traversal | < 10ms | Topological sort of module DAG | -| Hash computation (single module) | < 5ms | blake3 of module content | -| Full project initial analysis (~100 modules) | < 5s | Cold start, parallel compilation | -| End-to-end latency (file save → diagnostics) | < 200ms | Including 100ms debounce | -| Hole graph update | < 50ms | Incremental hole summary / `HoleGraphUpdate` regeneration | - -**Cache strategy:** Watch mode maintains in-memory caches for hashes, dependency graph, compilation artifacts, and hole state. Persistence to disk is not required for the current design but is a future option. +### HIR responsibilities -**Parallelism:** Default = CPU core count. Override with `--jobs N`. +HIR owns: -### CLI subcommand design +- canonical Base Signature representation; +- Intent Signature representation; +- import and visibility resolution; +- effect name resolution; +- property-to-claim lowering stubs; +- hole registration. -#### `spore check` +### TypedHIR responsibilities -`spore check` performs all static analyses without code generation. It is the -primary project-aware command for verifying correctness during development. +TypedHIR owns: -```bash -spore check src/main.sp # check one explicit file -spore check src/main.sp src/net/http.sp -spore check --deny-warnings src/main.sp src/net/http.sp -``` +- bidirectional type checking; +- trait resolution; +- effect checking; +- budget shape checking; +- hole report generation; +- Claim construction from properties; +- realization validation inputs. -##### Implementation strategy - -`spore check` runs a **single compilation pipeline** that performs all static -analyses in one pass: +### EvidenceRecord structure ```text -Source → Parse → Name Resolution → Type Inference - ├── Effect Check (inline) - ├── Cost Analysis (post-typecheck) - └── Lint Pass (post-typecheck) -``` - -All diagnostics are collected into a single `Vec` and sorted by file -position. This ensures the developer sees **all** issues in one invocation rather -than fixing types first, then effects, then costs. - -**Cost violations** are emitted as **warnings** (severity = Warning, code prefix -`K`), not errors. They do not cause a non-zero exit code unless `--deny-warnings` -is passed. - -**Lint warnings** (severity = Warning, code prefix `W`) follow the same rule. - -#### `spore test` - -`spore test` evaluates test files and function-level `spec` blocks. SEP-0001 -owns the syntax of `spec { ... }`; this SEP owns the compiler and runner behavior -for executing examples, generating law inputs, reporting failures, and -tracking spec hashes. - -```spore -fn add(a: I64, b: I64) -> I64 -spec { - example "positive inputs": add(2, 3) == 5 - example "identity": add(0, 42) == 42 -} -{ - a + b -} +EvidenceRecord +├── subject +│ ├── function +│ ├── hole +│ ├── module +│ └── artifact +├── claim +│ ├── type +│ ├── effect +│ ├── budget +│ ├── property +│ └── validator +├── checker +│ ├── name +│ ├── revision +│ └── config +├── result +│ ├── passed +│ ├── failed +│ ├── unknown +│ └── skipped +└── provenance + ├── signature_hash + ├── intent_hash + ├── property_hash + ├── realization_hash + ├── checker_hash + └── dependency_hashes ``` -Examples may use expression or block form. In block form, the final expression is -the assertion result: - -```spore -example "handles leap year" { - let d = parse_date("2024-02-29"); - d == Ok(Date { year: 2024, month: 2, day: 29 }) -} -``` - -Laws use explicitly typed lambda parameters and must evaluate to `Bool`: - -```spore -fn parse_date(s: Str) -> Result[Date, ParseError] ! ParseError -spec { - example "ISO date": parse_date("2024-01-15").is_ok() - law "round trip": |d: Date| parse_date(d.to_iso_string()) == Ok(d) -} -{ - ?parse_logic -} -``` +### Hash boundaries -If a `spec` calls a function body that still contains a hole, normal hole -runtime behavior applies during test execution. The `spec` remains available as -compiler metadata and may be surfaced through hole tooling as described in -SEP-0005. +`signature_hash` covers the normalized Base Signature. -`property` is not a compatibility alias. The accepted spec item set is exactly -`example` and `law`; source that still uses `property` is rejected and must be -rewritten by migration tooling or by hand. +`intent_hash` covers `uses`, `budget`, and `properties` after canonicalization. -#### `spore format` / `spore fmt` +`property_hash` covers the normalized property set attached to a subject. -`spore format` rewrites source files to conform to the canonical Spore style. -`spore fmt` remains an alias. +`realization_hash` covers a concrete completed body or generated artifact. -```bash -spore format src/main.sp src/net/http.sp -spore format --check src/main.sp src/net/http.sp -``` +`checker_hash` identifies the checker implementation and configuration. -##### Implementation strategy +`dependency_hashes` bind imported modules and package inputs. -The formatter is **AST-based**: it parses source code into an AST, then -pretty-prints the AST according to canonical rules. This ensures output is always -syntactically valid and produces consistent results regardless of input formatting. +For property claims, `result.passed` means the checker established the property +for the checked subject. `result.failed` means the checker produced a failing +case, counter-witness, or direct contradiction. `result.unknown` means the +property was well typed but the checker could not decide it with the available +analysis or evidence. `result.skipped` means the checker did not run. -```text -Source → Parse → AST → Pretty-print → Formatted source -``` +A well-typed property with `unknown` evidence remains visible to tools and +reviewers. It is not the same as a type error and does not by itself reject +compilation. Release gates and package policies may choose stricter treatment +for unknown evidence. -**Comment preservation** is a known challenge for AST-based formatters. The current -implementation attaches comments to adjacent AST nodes during parsing (stored as -leading/trailing trivia on `Spanned`). Future work may adopt a CST -(Concrete Syntax Tree) approach for perfect fidelity. +### Watch mode -**CI integration**: `spore format --check` is designed for CI pipelines. It exits -with code 1 if any file would change, without modifying files. Combined with -`spore check --deny-warnings`, these two commands form a complete CI static -analysis gate: +`spore watch --json` emits newline-delimited events: -```yaml -# Example CI step -- run: spore format --check src/main.sp src/net/http.sp -- run: spore check --deny-warnings src/main.sp src/net/http.sp +```json +{"event":"compile_result","file":"src/main.sp","status":"ok","diagnostics":[]} +{"event":"hole_graph_update","holes_total":1,"ready_to_fill":1,"blocked":0} +{"event":"evidence_update","records_added":3,"records_failed":0} ``` ---- - ## Human experience impact -### Faster iteration cycles - -The incremental compilation system eliminates the most frustrating part of the -edit-compile-test loop: unnecessary waiting. When a developer changes a function -body without altering its signature, _only that module_ is recompiled. Downstream -modules are untouched. In practice this means sub-100ms feedback for most edits. - -### Actionable diagnostics - -The design goal is **help-rich** diagnostics without making `help` a fake -required field. When the compiler has a concrete next step, the human renderer -should show a `help:` line; otherwise it should still provide precise spans, -clear notes, and stable codes. The default output remains concise (5–8 lines per -error) and visually structured with Rust-style gutters and underlines, making -10–20 diagnostics scannable without fatigue. - -### Watch mode as primary workflow - -`spore watch` is designed to be the default development workflow. It provides: - -- Real-time error/warning diagnostics on file save -- Hole progress tracking (total holes, filled this cycle, ready to fill, blocked) -- Effect and cost propagation alerts when signatures change - -The watch output is designed to be "glanceable" — a developer can look at the -terminal and immediately know whether the codebase is healthy. - -### Hole-driven development - -Watch mode tracks hole status in real time, showing which holes are ready to -fill (all dependencies satisfied) and which are blocked. This enables a natural -top-down workflow where developers sketch function signatures, insert holes for -implementations, then fill them one by one with immediate feedback. - ---- +Compiler output stays readable while still mapping to stable machine records. +Evidence lets reviewers see which properties and budgets were checked rather +than inferring trust from a green command alone. ## Agent experience impact -### Structured consumption via `--json` - -Agents should consume CLI JSON output directly instead of parsing human-readable -text. The minimal stable contract for diagnostics is: - -- `code` -- `severity` -- `message` -- `primary_span` -- `secondary_labels` -- `notes` -- `help` -- `related` - -Typical machine-oriented commands include: - -- `spore check --json` -- `sporec holes FILE --json` -- `sporec query-hole FILE ?HOLE --json` - -Richer analysis payloads such as inference chains, candidate rankings, and -machine edits are follow-up layers, not part of the minimal contract frozen by -this revision. - -### Agent hole-filling workflow - -```text -1. Start spore watch --json src/main.sp or re-run spore check --json -2. Read batch diagnostics / hole summaries -3. Select one hole → generate implementation → write to file -4. Observe the next compilation result -5. Success → proceed to next hole; Failure → read diagnostics, fix, retry -6. Repeat until no holes remain -``` - -The first stable milestone does **not** require a rich NDJSON protocol. Agents -can already iterate on a combination of batch diagnostics and hole-specific JSON -commands. A richer watch/event stream can be layered later on the same shared -Diagnostic IR. - -#### Agent mode selection - -| Scenario | Recommended Mode | Rationale | -| --------------------------------- | ----------------------------------------- | ------------------------------------------------------------------- | -| Agent filling holes | `spore check --json` + hole JSON commands | Stable machine-readable diagnostics plus hole-specific context | -| Agent debugging compiler error | `--json` | Consume code/severity/message/spans/notes/help without parsing text | -| Agent in CI pipeline | `--json` | Stable machine-readable contract | -| Human scanning build output | Default | Concise, color-coded, glanceable | -| Human debugging inference failure | `--verbose` | Richer renderer over the same base diagnostics | -| IDE / LSP client | LSP adapter over Diagnostic IR | Same canonical fields, editor-specific transport | - -#### Agent error recovery - -When an Agent's fix introduces new errors, the diagnostic system supports iterative repair: - -```text -1. Agent applies fix for E0301 (type mismatch) -2. spore check --json → new diagnostic batch after the edit -3. Agent reads code/span/notes/help for the new failure -4. Agent applies a second fix addressing E0303 -5. spore check --json → 0 errors -``` - -The minimal shared fields are enough for iterative repair because the agent can -anchor each retry on stable codes, spans, and notes. Richer analysis payloads -can improve this loop later, but they should not be required for the base -machine contract. - -### Future auto-fix integration - -Machine-applicable fixes are intentionally **not** part of the minimal -Diagnostic IR frozen by this SEP revision. - -The current public CLI does not yet expose `--fix` or `--unsafe-fix`. When -auto-fix support lands later, it should be layered as a separate code-action or -edit payload built on top of the shared diagnostics model rather than expanding -the minimal IR immediately. - -That future layer may still distinguish categories such as `safe`, `unsafe`, -and `informational`, but those categories should be specified only when the -public CLI and edit protocol exist. - ---- +Agents can consume diagnostics, HoleReports, claims, and evidence without text +scraping. Failed evidence gives repair direction. ## Structured representation / protocol impact -### Shared diagnostics pipeline - -The stable machine contract is the shared Diagnostic IR, not any one renderer or -transport: +The machine projection centers on shared records: ```text -typed crate-local errors - (ParseError / TypeError / ModuleError / ...) - │ - ▼ - sporec-diagnostics::Diagnostic - │ - ┌─────┼─────┬──────────────┐ - ▼ ▼ ▼ ▼ - ariadne JSON LSP adapter future watch / NDJSON transport - text output +Diagnostic +HoleReport +Claim +EvidenceRecord +VerificationBundle ``` -This PR freezes the shared object model first. Rich streaming protocols are a -later layer and should carry canonical diagnostics rather than inventing a -separate schema. - -### LSP mapping - -Target mapping from the canonical Diagnostic IR: - -| Diagnostic IR | LSP `Diagnostic` | Notes | -| ----------------------------------- | -------------------- | --------------------------------------------------------------------- | -| `severity` | `severity` | `error=1`, `warning=2`, `note=3` | -| `code` | `code` | direct copy | -| `message` | `message` | direct copy | -| `primary_span.range` | `range` | convert 1-indexed line/col to LSP 0-indexed positions | -| `related` | `relatedInformation` | when representable | -| `secondary_labels`, `notes`, `help` | `data.spore` | retained even when the base LSP surface cannot render them losslessly | - -> The LSP server publishes diagnostics from the compiler pipeline. The -> design goal is lossless carriage of richer fields such as secondary labels, -> notes, help, and hole-specific updates within the LSP protocol. - -### JSON as the machine projection - -`--json` should serialize canonical diagnostics directly. Default text and -future verbose text are renderer projections over the same objects. - -The minimal stable contract in this revision is: - -- batch diagnostics first -- shared field names across CLI JSON and LSP mapping -- richer analysis payloads only after the base IR is stable - -### Streaming vs. batch - -This revision still standardizes **batch diagnostic objects first**. Current watch NDJSON already emits `compile_result` plus summary `hole_graph_update`, but richer per-hole event payloads remain a later transport layer. - -When that richer streaming protocol is stabilized later, each event should carry either: - -- canonical `Diagnostic` objects, or -- references to the shared typed-hole objects already exposed by `sporec holes FILE --json` / `sporec query-hole FILE ?name --json` - -That keeps watch, batch JSON, and LSP aligned on one machine protocol family instead of growing separate schemas. +Default text, JSON, LSP, and watch outputs are renderings over these records. +SEP-0010 owns the concept registry and explain protocol layered over these same +records. SEP-0010 teaching metadata is optional metadata over these shared +records. It does not change the identity, hash, or required fields of +`Diagnostic`, `HoleReport`, `Claim`, `EvidenceRecord`, or `VerificationBundle`. ## Diagnostics impact -### Error format specification - -Every diagnostic follows the anatomy: - -```text -[]: - --> :: - | -NN | - | ^^^ - | - = note: -help: -``` - -Severity, code, location, source context, and help are **mandatory** for every -diagnostic architecture target. In the minimal shared IR, `code`, `severity`, -`message`, and `primary_span` are mandatory; `secondary_labels`, `notes`, -`help`, and `related` are optional enrichments. - -#### Multi-line error display +Diagnostic categories are: -When an error spans multiple lines, the gutter marks the full range: +| Prefix | Category | +| ------- | ------------------------------ | +| `E0xxx` | Type errors | +| `F0xxx` | Effect violations | +| `B0xxx` | Budget violations | +| `H0xxx` | Hole diagnostics | +| `P0xxx` | Property and claim diagnostics | +| `M0xxx` | Module and package errors | +| `W0xxx` | Warnings | -```text -error[E0102]: struct missing required field `currency` - --> src/billing.sp:15:5 - | -15 | / let invoice = Invoice { -16 | | amount: 100, -17 | | recipient: user, -18 | | } - | |___^ missing field `currency` - | -help: add the missing field: `currency: Currency.USD` -``` - -The `/ | |___^` format connects multi-line spans visually. - -#### ANSI color scheme - -| Element | Color | ANSI Code | -| ------------------------ | ---------------------- | ------------ | -| `error` + error code | Red (bold) | `\x1b[1;31m` | -| `warning` + warning code | Yellow (bold) | `\x1b[1;33m` | -| `note` | Blue (bold) | `\x1b[1;34m` | -| `help` | Green (bold) | `\x1b[1;32m` | -| `hint` | Cyan (bold) | `\x1b[1;36m` | -| Line numbers | Bright blue | `\x1b[94m` | -| Source text | Default | — | -| Underline (`^^^`) | Matches severity color | — | - -Colors are disabled when output is piped (not a TTY) or when `--no-color` is passed. The `--json` mode never includes ANSI codes. - -### Error categories and examples - -**Type error:** - -```text -error[E0301]: type mismatch - --> src/billing.sp:42:22 - | -42 | charge(card, "fifty dollars") - | ^^^^^^^^^^^^^^^ expected `Money`, found `Str` - | -help: try `Money.from_string("fifty dollars")` -``` - -**Effect violation:** - -```text -error[C0101]: undeclared effect - --> src/report.sp:27:5 - | -27 | http.get(endpoint) - | ^^^^^^^^^^^^^^^^^^ requires `NetConnect`, not declared in `uses` - | - = note: enclosing function `fetch_data` has `uses []` - = note: module ceiling for `report` allows [FileRead] -help: add a `uses [NetConnect]` clause to `fetch_data` -``` - -**Cost violation:** - -```text -error[K0101]: cost budget exceeded - --> src/analytics.sp:55:5 - | -55 | full_table_scan(records) - | ^^^^^^^^^^^^^^^^^^^^^^^^ this call costs 8200 op - | - = note: `summarize` budget is 5000 op, already used 1200 op - = note: remaining budget: 3800 op, shortfall: 4400 op -help: filter `records` before scanning, or widen the declared `cost [..., ...]` compute (or aggregate) slots -``` - -**Hole report:** - -```text -note[H0101]: hole `tax_logic` requires filling - --> src/tax.sp:12:5 - | -12 | ?tax_logic - | ^^^^^^^^^^ expected type: `Money`, remaining cost budget: 400 op - | - = note: available bindings: income: Money, region: Region - = note: available effects: [TaxTable] - = note: candidate: `tax_table.lookup(region, income) -> Money` -help: run `sporec query-hole src/tax.sp ?tax_logic` for full HoleReport -``` - -**Spec example failure:** - -```text -error[S0101]: spec example failed - --> src/dates.sp:5:5 - | - 5 | example "ISO date": parse_date("2024-01-15").is_ok() - | ^^^^^^^^^^^^^^^^^^ evaluated to false -``` - -**Spec law counterexample:** +Property diagnostics include failed property checks, counter-witnesses, and +properties that reached an open hole. A property body that does not check as +`Bool` is reported as a type diagnostic because the source expression is ill +typed. A property body that checks as `Bool` but fails or remains unknown is +reported as a property or claim diagnostic because the source shape is valid and +the evidence result is the issue. -```text -error[S0102]: spec law counterexample - --> src/dates.sp:9:5 - | - 9 | law "round trip": |d: Date| parse_date(d.to_iso_string()) == Ok(d) - | ^^^^^^^^^^^^^^^^ counterexample found - | - = note: d = Date { year: 2024, month: 2, day: 30 } -``` - -**Missing spec warning:** - -```text -warning[W0401]: public function has no spec block - --> src/dates.sp:3:1 - | - 3 | pub fn parse_date(s: Str) -> Result[Date, ParseError] ! ParseError { - | ^^^^^^^^^^ no behavioral contract declared - | -help: add a `spec { example "...": ... }` block before the function body -``` - -### Recovery strategies - -The compiler employs error recovery to report as many errors as possible per -compilation rather than stopping at the first: - -| Phase | Recovery strategy | -| --------- | ----------------------------------------------------------------------- | -| Lexer | Skip to next recognizable token boundary | -| Parser | Synchronize at statement/declaration boundaries; insert synthetic nodes | -| Resolve | Mark unresolved names as `ErrorType`, continue checking | -| TypeCheck | Propagate `ErrorType` without cascading false errors | -| CapCheck | Report violation but continue checking remaining uses | -| CostCheck | Report budget exceedance but compute total cost for diagnostics | - -**Deduplication policy:** when a single root cause produces multiple downstream -errors, the compiler shows the root error in full and collapses downstream -errors into `= note: N additional errors caused by this`. - -### Complete Error Code Registry - -All diagnostics carry a categorized code. Every code is queryable: `sporec explain CODE`. - -#### E0xxx — Type Errors - -| Code | Name | Description | -| ------- | ------------------------ | ------------------------------------------------------------------ | -| `E0101` | missing-field | Struct literal missing a required field | -| `E0102` | unknown-field | Struct literal contains a field not in the type definition | -| `E0103` | duplicate-field | Struct literal contains the same field twice | -| `E0104` | field-type-mismatch | Struct field value type does not match declaration | -| `E0105` | tuple-length-mismatch | Tuple has wrong number of elements | -| `E0106` | missing-variant-field | Enum variant constructor missing a field | -| `E0107` | record-vs-tuple | Used record syntax where tuple expected, or vice versa | -| `E0108` | non-struct-field-access | Field access on a non-struct type | -| `E0109` | private-field-access | Accessing a private field from outside the defining module | -| `E0110` | spread-type-mismatch | Spread operator (`..base`) type does not match struct | -| `E0201` | arity-mismatch | Function called with wrong number of arguments | -| `E0202` | named-arg-mismatch | Named argument does not match any parameter | -| `E0203` | missing-required-arg | Required argument not provided | -| `E0204` | duplicate-arg | Same argument provided twice | -| `E0301` | type-mismatch | Expression type incompatible with expected type | -| `E0302` | return-type-mismatch | Function body returns wrong type | -| `E0303` | error-type-mismatch | Function raises undeclared error type | -| `E0304` | if-branch-mismatch | If/else branches have different types | -| `E0305` | match-arm-mismatch | Match arms have different types | -| `E0306` | operator-type-error | Operator applied to incompatible types | -| `E0307` | index-type-error | Non-integer used as index | -| `E0308` | not-callable | Attempt to call a non-function value | -| `E0309` | pipe-type-mismatch | Pipe operator left-hand side incompatible with right-hand function | -| `E0310` | lambda-return-mismatch | Lambda body type does not match expected return | -| `E0401` | constraint-not-satisfied | Generic type does not satisfy trait constraint | -| `E0402` | ambiguous-type | Type inference cannot determine a unique type | -| `E0403` | recursive-type | Infinitely recursive type definition | -| `E0404` | gat-mismatch | Generic associated type arguments do not match | -| `E0501` | pattern-exhaustiveness | Match does not cover all variants | -| `E0502` | pattern-type-mismatch | Pattern type does not match scrutinee type | -| `E0503` | duplicate-pattern | Same pattern appears twice in match | -| `E0504` | guard-type-error | Match guard expression is not Bool | - -#### W0xxx — Warnings - -| Code | Name | Description | -| ------- | --------------------- | ------------------------------------------------ | -| `W0101` | unused-variable | Variable bound but never read | -| `W0102` | unused-import | Module import never referenced | -| `W0103` | unused-function | Private function never called | -| `W0104` | unused-type | Private type never referenced | -| `W0105` | unused-effect | Declared effect never exercised | -| `W0201` | redundant-pattern | Match arm unreachable due to prior arm | -| `W0202` | redundant-constraint | Generic constraint implied by another | -| `W0203` | redundant-parentheses | Unnecessary parentheses around expression | -| `W0301` | shadowing | Variable shadows binding in outer scope | -| `W0302` | implicit-discard | Expression result discarded without explicit `_` | -| `W0401` | missing-spec | Public function has no `spec` block | - -#### C0xxx — Effect Violations - -| Code | Name | Description | -| ------- | ---------------------- | ----------------------------------------------- | -| `C0101` | undeclared-effect | Function uses effect not in `uses` | -| `C0102` | exceeds-ceiling | Function `uses` exceeds module ceiling | -| `C0103` | callee-effect-leak | Calling function whose `uses` exceeds caller's | -| `C0104` | transitive-effect | Transitive callee introduces undeclared effect | -| `C0201` | platform-effect-denied | Package requests effect Platform does not grant | -| `C0202` | platform-missing | No Platform provides required effect | -| `C0301` | effect-purity-conflict | `uses []` (pure) function calls impure code | - -#### K0xxx — Cost Violations - -| Code | Name (implementation) | Short description | -| ------- | ---------------------------------- | ---------------------------------------------------------------- | -| `K0101` | cost budget exceeded | Inferred cost exceeds declared bound (warning in current policy) | -| `K0102` | cost annotation mismatch | Declared `cost [...]` does not match inferred cost | -| `K0001` | cost budget exceeded | Legacy alias for `K0101` | -| `K0201` | unbounded recursion detected | Recursion pattern not structurally bounded | -| `K0202` | loop without bounded iteration | Reserved / iteration-path diagnostic | -| `K0301` | missing cost on recursive function | Recursive function lacks `cost [...]` where required | -| `K0302` | invalid cost expression | `cost [...]` parse or shape error | -| `K0303` | `@unbounded` requires cost | `@unbounded` without `cost [c, a, i, p]` (hard error) | - -Canonical error codes and messages are defined in the error code registry. - -#### H0xxx — Hole Diagnostics - -| Code | Name (implementation) | Short description | -| ------- | ---------------------------- | ----------------------------- | -| `H0101` | typed hole found | Standard hole report entry | -| `H0102` | hole with inferred type | Hole with type context | -| `H0103` | hole in return position | Hole where return is expected | -| `H0201` | hole candidates available | Ranked fills exist | -| `H0202` | no candidates found | No suitable fill | -| `H0203` | ambiguous candidates | Multiple close matches | -| `H0301` | hole depends on another hole | Ordering / dependency | -| `H0302` | circular hole dependency | Dependency cycle | - -See §H0xxx — Hole Diagnostics in the error code registry. - -#### S0xxx — Spec Diagnostics - -| Code | Name | Description | -| ------- | ----------------------- | --------------------------------------- | -| `S0101` | spec-example-failed | `example` item evaluated to false | -| `S0102` | spec-law-counterexample | `law` item produced a counterexample | -| `S0201` | spec-body-hit-hole | Spec execution reached an unfilled hole | - -#### M0xxx — Module Errors - -| Code | Name | Description | -| ------- | ------------------------- | ---------------------------------------------------------------------- | -| `M0101` | circular-dependency | Modules form a dependency cycle | -| `M0102` | self-import | Module imports itself | -| `M0103` | duplicate-module | Two files map to the same module path | -| `M0201` | visibility-violation | Accessing private or `pub(pkg)` symbol from outside scope | -| `M0202` | re-export-visibility | Re-exporting with broader visibility than original | -| `M0203` | orphan-impl | Implementing external trait for external type | -| `M0204` | alias-chain | `pub alias` points to another alias instead of a concrete export | -| `M0301` | import-not-found | Imported module or symbol does not exist | -| `M0302` | ambiguous-import | Two imports bring same name into scope | -| `M0303` | wildcard-import | Wildcard imports are not allowed in Spore | -| `M0304` | import-shadowing-conflict | Import alias conflicts with module name or existing import binding | -| `M0401` | snapshot-changed | Dependent signature hash changed; requires `--permit` | -| `M0402` | snapshot-missing | Referenced snapshot not found in `.spore-lock` | -| `M0501` | platform-binding-conflict | Project declares more than one Platform binding | -| `M0502` | startup-contract-mismatch | Startup function signature does not satisfy selected Platform contract | - -### System integration - -Diagnostics do not exist in isolation — they connect to every major Spore subsystem: - -#### Hole System integration - -- `H0101` (hole-report) is emitted as `note` severity for each unfilled hole during compilation -- `sporec query-hole ?name` returns the full per-hole `HoleReport` — - a superset of `H0101` with candidate ranking, binding types, and cost budget -- Partial functions (`H0201`) compile successfully — they produce diagnostics but not errors -- The per-hole HoleReport JSON extends the diagnostic schema with a `hole_report` field - -#### Cost System integration - -- `K0101` (budget exceeded) should at minimum surface the exceeded budget and - the primary contributing span -- `K0102` (symbolic exceeded) should at minimum surface the symbolic expression - and concrete counterexample when available -- richer cost-breakdown payloads can be layered later as verbose or extension - data; they are not part of the minimal frozen IR - -#### Effect System integration - -Three enforcement levels: - -1. **Function level** (`C0101`): body uses operations beyond declared `uses` -2. **Module level** (`C0102`): function `uses` exceeds module ceiling -3. **Platform level** (`C0201`): package requests effects Platform does not grant - -Effect context may be rendered in notes or future verbose/extension data, but -it is not a required field in the minimal frozen IR. - -#### Snapshot System integration - -- `M0401` (snapshot-changed) emits both expected and actual signature hashes -- when there is a concrete next step, the human renderer may include a `help:` - line explaining that the developer must explicitly run `spore --permit` -- `related` can point to the changed signature's declaration - -> **Note on `--permit`:** The `--permit` flag is a Codebase Manager (`spore`) command, not a compiler (`sporec`) flag. It updates `.spore-lock` to accept the new signature hash. - -#### Module System integration - -- `M0101` (circular dependency): `notes` and `related` can surface the full cycle path -- `M0201` (visibility violation): later verbose or extension data may list public alternatives -- `M0301` (import not found): when fuzzy matches exist, the human renderer may - surface them via `help:` or `notes` - ---- +Diagnostic teaching metadata, concept references, and `spore explain` behavior +are owned by SEP-0010. ## Drawbacks -1. **Three output modes add maintenance burden.** Default, verbose, and JSON must - remain consistent — a change to diagnostic content must be reflected in all - three renderers. Mitigation: all three modes render from the same underlying - `Diagnostic` struct; the JSON serializer is the canonical representation. - -2. **salsa adds a dependency and learning curve.** salsa is a sophisticated - framework with its own mental model (demand-driven, memoized queries). - Contributors must understand salsa to modify the compiler pipeline. - Mitigation: salsa is well-documented and used successfully in rust-analyzer. - -3. **No MIR limits future optimization passes.** If Spore later needs - optimization passes that operate on a control-flow graph (e.g., dead code - elimination beyond what Cranelift does, or Spore-specific optimizations), the - lack of a MIR-like representation would require introducing one. - Mitigation: Cranelift already performs standard optimizations; Spore can - introduce a MIR later if needed without changing the AST/HIR/TypedHIR layers. - -4. **Cranelift is less mature than LLVM.** Cranelift produces less optimized - code than LLVM for compute-heavy workloads. Mitigation: Cranelift's - compilation speed aligns with Spore's emphasis on fast iteration; an LLVM - backend can be added later for release builds. - -5. **File-save-triggered compilation misses real-time feedback.** Unlike - per-keystroke analysis (as in TypeScript's language server), Spore only - compiles on save. This means errors from half-typed code are not shown until - save. Mitigation: this is intentional — compiling partial edits generates - noise; save is a natural "intent to check" signal. +Evidence records add storage and schema complexity. The benefit is a durable, +inspectable audit trail for human and Agent workflows. ---- +Separating signature, intent, property, and realization hashes requires users to +learn more than one identity concept. Tooling should render them with labels and +short forms. ## Alternatives considered -### 1. More IR layers (4 or 5, like Rust) - -Rust uses AST → HIR → THIR → MIR → LLVM IR. We considered adding a MIR for -control-flow analysis and a separate LIR before Cranelift. - -**Rejected because:** Spore has no borrow checker (the primary reason for Rust's -MIR), no lifetime analysis, and no comptime evaluation (which motivates Zig's -ZIR). Additional IR layers would add complexity without corresponding benefit. - -### 2. Fewer IR layers (2, like Gleam) - -Gleam uses Untyped AST → Typed AST with no separate HIR. - -**Rejected because:** Spore's desugaring (`|>`, `?`, `f"..."`, `t"..."`) and -name resolution are complex enough to warrant a dedicated IR layer. Performing -desugaring inline during type-checking would complicate the type checker and make -incremental compilation boundaries less clean. - -### 3. Zig-style flat IR (ZIR) for comptime - -Zig uses a flattened IR (ZIR) to support compile-time evaluation. - -**Rejected because:** Spore does not support comptime. Const generics + -refinement types + cost models cover Spore's compile-time reasoning needs. salsa -provides incremental caching without needing a flat IR representation. - -### 4. LLVM instead of Cranelift - -LLVM produces more optimized code and has a larger ecosystem. - -**Rejected for initial implementation because:** Cranelift offers significantly -faster compilation, which aligns with Spore's emphasis on rapid iteration. -Cranelift's function-level granularity fits content-addressed caching naturally. -An LLVM backend remains a future option for optimized release builds. - -### 5. Per-keystroke compilation (like TypeScript) - -TypeScript's language server compiles on every keystroke, providing immediate -feedback. - -**Rejected because:** compiling half-edited code generates noise (false errors -from incomplete expressions). File save is a natural "intent to check" signal. -The 100ms debounce window after save still provides near-instant feedback. +### Text-only diagnostics -### 6. Separate passes for CapCheck and CostCheck +Rejected because Agents and CI need stable structured payloads. -We considered making effect checking and cost checking independent passes -after type checking. +### Single full-content hash -**Rejected because:** `Effect = Trait` in Spore, so effect resolution -shares trait resolution infrastructure. Cost checking depends on fully resolved -types and call graphs. Merging all three into a unified TypeCheck pass avoids -redundant tree traversals and simplifies the impl hash boundary. +Rejected because body-only realization changes should not force every dependent +to treat the callable boundary as changed. -### Why diagnostics should be help-rich +### Source-level evidence -A diagnostic without a suggestion is often a dead end. The design therefore -pushes strongly toward emitting `help:` whenever the compiler has a concrete next -step: - -- beginners more often have an obvious next action -- agents can extract fix suggestions when they exist -- the compiler team is pushed to think about actionability when defining new - error codes - -But the minimal IR keeps `help` optional so global failures and -self-explanatory diagnostics do not need to invent a fake suggestion. - -### Why category-based error codes - -Codes like `E0301` are more useful than raw names: - -- **Filterable:** `spore check --json | jq '.diagnostics[] | select(.code | startswith("K"))'` gives all cost errors -- **Discoverable:** `sporec explain E0301` opens documentation -- **Stable:** codes don't change when messages are reworded; CI can allowlist specific codes -- **Cross-referencing:** docs, forums, and issue trackers reference `E0301` unambiguously - -The prefix convention (E/W/C/K/H/M) maps directly to Spore's major subsystems. - -### Architectural Decision Records (ADRs) - -#### ADR-001: Developer-time DX scope - -**Decision:** Incremental compilation targets developer-time fast iteration, not production hot-swap. -**Rationale:** DX is the highest-ROI investment; content-addressing naturally supports incremental compilation. -**Consequence:** No runtime module replacement protocol; single-machine development scenarios only. - -#### ADR-002: Three effects - -**Decision:** "Program-as-Service" manifests as incremental compilation + real-time diagnostics + hole status tracking. -**Rationale:** These cover the core feedback needs during coding: correctness, problem location, progress tracking. -**Consequence:** `spore watch` is the unified carrier; JSON output contains all three information types. - -#### ADR-003: Module-level granularity - -**Decision:** The compilation unit is the module; sig hash unchanged → downstream skipped. -**Rationale:** Modules have natural compilation boundaries (explicit export interfaces). Function-level granularity has diminishing returns. -**Consequence:** Hash computation, dependency graph, and parallel compilation all operate at module granularity. - -#### ADR-004: File-save trigger - -**Decision:** Watch mode compiles on file-system save events, not per-keystroke. -**Rationale:** Save is a natural "intent to check" signal; per-keystroke compilation produces noise from half-edited code. -**Consequence:** Requires debounce; LSP triggers via FS events rather than `didChange`. - -#### ADR-005: Dual-format output - -**Decision:** Human-readable (default) + JSON (`--json`) output formats. -**Rationale:** Terminal users need readable output; IDEs and agents need structured output. -**Consequence:** Both formats derive from the same underlying `Diagnostic` struct; JSON is the superset. - ---- +Rejected because evidence should be generated by checkers and bound to +provenance, not hand-written as trusted source text. ## Prior art -| Language | Pipeline | What Spore borrows | -| ------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| **Rust** | AST → HIR → THIR → MIR → LLVM IR | HIR/TypedHIR concepts; salsa for incremental compilation (via rust-analyzer); Rust-style diagnostic layout | -| **Zig** | AST → ZIR → AIR → Machine IR | Pratt parser for expressions; _not_ the ZIR concept (no comptime) | -| **Roc** | AST → Canonical → Solved → IR | Canonical ≈ HIR concept; name resolution approach | -| **Gleam** | Untyped AST → Typed AST | Inspiration for simplicity in the 2-layer approach; confirmed that 2 layers are too few for Spore | -| **Gonidium** | AST → TypedDag | `DiagCollector` error collection pattern | -| **Elm** | AST → Canonical → Typed → Optimized | Philosophy of helpful error messages; Spore adopts the helpfulness but uses Rust's concise layout rather than Elm's paragraph style | - -### Incremental computation framework (Prior Art) - -Demand-driven incremental computation frameworks provide: - -- Automatic memoization of query results -- Fine-grained dependency tracking -- Automatic invalidation and recomputation on input changes -- Durability layers for optimizing frequently vs. rarely changing inputs - -The compiler uses demand-driven incremental computation to make each compilation pass a tracked query, enabling -automatic incremental recomputation when source files change. - -### Cranelift - -Cranelift is a code generation backend designed for fast compilation. It is used -by Wasmtime and is being integrated into rustc as an alternative to LLVM for -debug builds. Key properties: - -- Function-level compilation unit (fits content-addressed caching) -- Significantly faster compilation than LLVM (at the cost of less optimized - output) -- SSA-based IR with good support for common language constructs -- Active development by the Bytecode Alliance - ---- +Rust influenced diagnostics and HIR structure. Elm influenced human-readable +errors. Unison influenced content-addressing. Proof assistants influenced the +claim/evidence separation. ## Backward compatibility and migration -### Migration from interpreter to native codegen - -An initial implementation may use a **tree-walking interpreter** in place of native codegen. -The migration path: - -1. The `codegen` pass is behind a clean abstraction boundary (it receives - `TypedHIR` and produces an executable result) -2. Replacing the tree-walking interpreter with native codegen requires - implementing the `TypedHIR → native` translation — no changes to - passes 1–4 -3. The `--json` diagnostic format is identical in both phases; tools built - against PoC diagnostics continue to work in prototype - -### Diagnostic format stability - -Error codes (E0xxx, W0xxx, etc.) are considered stable once assigned. The -meaning of a code does not change, though the message text may be improved. -CI/CD pipelines and agents should filter on error codes, not message strings. - -If the JSON schema needs breaking changes, the schema identifier will change and -a migration period will provide both old and new schema shapes. - -### Future IR additions - -If Spore later needs a MIR (e.g., for advanced optimization or analysis), it -would be inserted between TypedHIR and Cranelift IR: - -```text -AST → HIR → TypedHIR → [future MIR] → Cranelift IR → Native -``` - -This insertion would not change the AST, HIR, or TypedHIR layers, nor the sig -hash / impl hash boundaries. The impl hash would continue to be computed at the -TypedHIR layer; a new "opt hash" could gate the MIR → Cranelift translation. - ---- +Tooling must migrate from older diagnostic prefixes and hash labels to signature +model names. JSON consumers should key on explicit record fields rather than parse +human messages. ## Unresolved questions -1. **Diagnostic deduplication granularity.** When a single root cause produces - multiple downstream errors, how aggressively should the compiler collapse - them? Current proposal: show the root in full, collapse downstream into - `= note: N additional errors caused by this`. Needs user testing to find the - right threshold. - -2. **Diagnostic ordering.** Primary sort by file, secondary by line, with - errors before warnings before notes within the same line? Or should the - compiler attempt causal ordering (root cause first)? Needs experimentation. - -3. **Persistent caching across sessions.** salsa's in-memory cache is lost - when `spore watch` exits. Should the compiler persist the salsa database to - disk for faster cold starts? This adds complexity (cache invalidation, - file format stability) but could significantly improve initial compilation - time for large projects. - -4. **Parallel type-checking within a module.** The current design parallelizes - across modules at the same topological level. Could independent functions - within a single module be type-checked in parallel? This depends on the - eventual dependency-tracking granularity and the cost of synchronizing shared - state (e.g., trait resolution caches). - -5. **LLVM backend.** When should an LLVM backend be added for release-optimized - builds? This is a significant engineering effort. Cranelift may be sufficient - for Spore's target use cases (networked services, CLI tools) where compile - speed matters more than runtime performance. - -6. **Streaming granularity.** When the richer watch / JSON event transport is - standardized later, should events be per-diagnostic (finest granularity) or - per-module (less noise, more batched)? Per-diagnostic gives earliest - feedback but may overwhelm agents processing large rebuilds. - -7. **Internationalization of diagnostics.** Error codes are language-independent, - but message text is currently English-only. Should `sporec explain` support - localized explanations in a future release? This would require a translation - infrastructure. - -8. **Cost visualization in verbose mode.** Should `--verbose` include ASCII bar - charts showing cost distribution within a function? This could help - developers visually identify expensive calls but adds rendering complexity. - -9. **Watch mode persistence.** Should `spore watch` offer an option to write - compilation results (diagnostics, hole reports) to a file on each cycle, - enabling offline analysis tools to process the history? - -10. **Tree-walking interpreter scope.** How much of the language should the - PoC interpreter support? Full semantics (including effects and cost - tracking at runtime) or a minimal subset for early testing? This affects - the PoC timeline and determines how much behavior can be validated before - Cranelift codegen is ready. - -### Diagnostic suppression mechanism - -Specific diagnostics can be suppressed using inline annotations: - -```spore -#[allow(W0301)] -let count = filtered.len() // shadowing is intentional here -``` - -Project-level suppression via `spore.toml`: - -```toml -[diagnostics] -allow = ["W0302"] # allow implicit discards project-wide -``` - -Suppression is limited to **warnings only** — errors and notes cannot be suppressed. The `#[allow(...)]` attribute applies to the next statement or the enclosing block. - ---- - -## Glossary - -| Term | Definition | -| ------------------- | --------------------------------------------------------------------------------------------------------- | -| **impl hash** | Hash of module implementation content; determines whether to recompile this module | -| **sig hash** | Hash of module public interface; determines whether downstream dependents need rechecking | -| **hole** | Source-code placeholder (`?name`) for unfinished implementation, carrying type information | -| **effect** | A declared capability such as `NetConnect`, `FileRead`, or `Spawn` required to perform certain operations | -| **effect ceiling** | Reserved module/project policy concept; function-level `uses [...]` checking is the stable surface | -| **cost annotation** | A declared four-slot upper bound (`cost [compute, alloc, io, parallel]`) | -| **debounce** | Coalescing multiple rapid file-system events into a single compilation trigger | -| **NDJSON** | Newline-Delimited JSON — each line is a complete, independent JSON object | +1. Should evidence records be persisted beside build artifacts or in package lock data? +2. How should skipped and unknown evidence affect release gates? +3. Should checker configuration be normalized as source text, JSON, or a typed IR? diff --git a/seps/SEP-0007-concurrency-model.md b/seps/SEP-0007-concurrency-model.md index 5866101..473c8de 100644 --- a/seps/SEP-0007-concurrency-model.md +++ b/seps/SEP-0007-concurrency-model.md @@ -11,6 +11,8 @@ requires: - 2 - 3 - 4 + - 5 + - 6 discussion: "https://github.com/spore-lang/spore-evolution/discussions/7" pr: null superseded_by: null @@ -18,1393 +20,185 @@ superseded_by: null # SEP-0007: Concurrency Model -> **Executive Summary**: Defines structured concurrency with `spawn`/`await`/`select` primitives, where `Task[T]` serves as a typed future representing an asynchronous computation. Introduces Lanes as the parallel cost dimension (tracked in CostVector), hierarchical cancellation scoping tied to lexical blocks, and channel-based communication (Channel[T]) with bounded buffers for inter-task messaging. +> **Executive Summary**: Defines structured concurrency under the signature model. Concurrency is introduced through scoped expressions and effects, while acceptable fan-out and nesting are constrained by `budget` fields and recorded in evidence. ## Summary -This proposal defines the concurrency model for the Spore programming language. The model is built on four pillars: +Spore provides structured concurrency primitives: -1. **Structured concurrency** — all concurrent tasks form a tree rooted in `parallel_scope`; no raw thread spawning, no `GlobalScope` escape hatches. -2. **Effect handlers** — `Spawn` is a standard effect, not a keyword. Different handlers provide real parallelism, sequential testing execution, or compile-time cost simulation—without changing user code. -3. **Effect narrowing** — a child task's effect set must be a subset of its parent's (`child.uses ⊆ parent.uses`), enforced at compile time. -4. **Cost budgets with a parallel dimension** — the cost model gains a fourth dimension `parallel(lane)` that the compiler statically tracks and verifies. +- `parallel_scope { ... }` +- `spawn { ... }` +- `task.await` +- `select { ... }` +- `Task[T, E]` +- `Channel[T]` -The core formula is: - -```text -Spore Concurrency = Koka Effects + Kotlin Structured Scoping - + Zig Explicit Resources + Spore Cost Budgets -``` - -The design unifies five properties that no existing language achieves simultaneously: - -| Property | Meaning | Beneficiary | -|----------|---------|-------------| -| **Colorless functions** | Concurrency does not introduce `async`/`await` syntax bifurcation | All developers | -| **Structured lifetimes** | Child tasks cannot escape the parent scope | Resource management, debugging | -| **Explicit effects** | Concurrent tasks may only use declared effects | Security audits | -| **Bounded cost** | Compiler statically verifies four-slot `cost [c, a, i, p]` (incl. `parallel`) | Performance predictability | -| **Simulatable execution** | Handlers are replaceable—same code compiles to simulation / deterministic test / production parallel | Agents, CI | - ---- - -## Motivation - -### Why structured concurrency - -Traditional concurrency models—raw threads, unbounded `goroutine` spawning, `GlobalScope.launch`—share a common flaw: **concurrent tasks form an arbitrary directed graph whose lifetime is unmanageable at compile time**. This leads to: - -- **Resource leaks**: A spawned task outlives its creator, holding file handles or network connections the parent assumed were released. -- **Unanalyzable cost**: Fire-and-forget tasks are invisible to the compiler's cost model. If a four-slot `cost [...]` budget is declared in the function signature but the function spawns background work that escapes the scope, the declared bound is meaningless. -- **Debugging nightmares**: Stack traces of escaped tasks lose the causal chain to their spawning point, making production incidents harder to diagnose. - -Structured concurrency, as articulated by Nathaniel J. Smith (*"Go statement considered harmful"*, 2018), solves these problems by enforcing **tree-shaped task lifetimes**: every child task is nested inside a scope that does not exit until all children have completed (or been cancelled). In Spore this means: - -1. **Nursery rule** — all concurrent tasks must be spawned inside a `parallel_scope`. -2. **Parent waits for children** — `parallel_scope` does not return until every child task has completed. -3. **Cancellation propagates downward** — cancelling a parent recursively cancels all its children. -4. **Errors propagate upward** — a child exception bubbles to the parent scope and cancels remaining siblings. -5. **Resource safety** — because children always finish before the scope exits, `defer` cleanup is always effective. - -### Why no raw threads - -Raw threads are excluded for fundamental reasons: - -- They bypass the effect system: any thread can perform any operation. -- They make cost analysis intractable: the compiler cannot statically enumerate an unbounded thread graph. -- They conflict with Spore's philosophy of *"the compiler sees everything"*. - -### Why not async/await - -Bob Nystrom's *"What Color is Your Function?"* (2015) identified how `async` splits functions into two colors—red (async) and blue (sync)—where red can only be called from red contexts, causing a viral coloring of the entire call chain. Spore takes a different path: **concurrency is an effect declared in the `uses` clause**. Effect-polymorphic functions are naturally colorless. +A concurrent function declares the required effect surface and may constrain +its realization shape with budget fields: ```spore -// No async annotation needed—concurrency declared via the Spawn effect -fn fetch_all[N: Index](urls: Vec[Url, max: N]) -> Vec[Response, max: N] ! NetError -cost [N * per_fetch + N * 10, N * 2, N, N] -uses [Spawn, NetConnect] -{ - parallel_scope { - urls.map(|url| spawn { fetch(url) }) - .map(|task| task.await) - } +fn fetch_all(urls: List[Url]) -> List[Page] ! NetworkError +uses [Http, Spawn] +budget { + parallelism: 4 + nesting: 3 + effects: 8 } - -// Pure function: no Spawn, no IO—compiler guarantees single-threaded -fn transform[N: Index](data: Vec[Item, max: N]) -> Vec[Item, max: N] -cost [N * 3, N, 0, 0] { - data.map(|item| item.process()) + ?fetch_all_body } ``` -Both functions use identical call syntax—no red/blue distinction. The concurrency behavior of `fetch_all` is determined entirely by the handler bound at the call site. +## Motivation ---- +Unstructured concurrency creates lifetime leaks and hard-to-review fan-out. +Spore treats concurrency as scoped: spawned tasks cannot outlive their parent +scope, and signature budgets can cap the shape of concurrent realizations. ## Guide-level explanation -This section introduces the concurrency primitives through progressive examples. - -Concrete surface syntax for `parallel_scope`, `spawn`, `select`, and `handle ... with` is centralized in SEP-0001. This SEP uses examples to explain concurrency behavior, but the grammar itself should be read there. - -### 3.1 `parallel_scope` — the concurrency entry point - -`parallel_scope` is the **only** way to introduce concurrency in Spore. There is no `GlobalScope`, no `thread::spawn`, no detached background tasks in user code. - -```spore -// Basic form: spawn two computations in parallel -parallel_scope { - let a = spawn { compute_part1() } - let b = spawn { compute_part2() } - (a.await, b.await) -} -``` - -The block is an expression; its value is the last expression in the body. When the block exits, every spawned task has either completed or been cancelled. - -Because `parallel_scope` is an expression, it can return structured results assembled from spawned tasks: +### Scoped spawning ```spore -fn load_dashboard(user_id: UserId) -> Dashboard ! DbError | NetError -uses [Spawn, NetConnect, DbRead] +fn fetch_pair(a: Url, b: Url) -> Pair[Page, Page] ! NetworkError +uses [Http, Spawn] +budget { parallelism: 2 } { - // parallel_scope returns the Dashboard struct directly parallel_scope { - let profile = spawn { db.get_profile(user_id) } - let feed = spawn { api.get_feed(user_id) } - let notifs = spawn { api.get_notifications(user_id) } - Dashboard { - profile: profile.await, - feed: feed.await, - notifications: notifs.await, - } + let left = spawn { fetch(a) }; + let right = spawn { fetch(b) }; + Pair { first: left.await?, second: right.await? } } } ``` -An optional `lanes` parameter limits the degree of parallelism: - -```spore -parallel_scope(lanes: 4) { - data.chunks(4).each(|chunk| { - spawn { process_chunk(chunk) } - }) -} -``` - -### 3.2 `spawn` — creating child tasks +### Cancellation -`spawn` launches a child task inside the current `parallel_scope` and returns a `Task[T]`: +When a scope exits early, unawaited child tasks are cancelled cooperatively. +Cancellation checkpoints happen at effect operations and explicit cancellation +checks inside long-running pure computation. -```spore -let task: Task[Response] = spawn { fetch(url) } -let response: Response = task.await -``` - -`spawn` is **not** a standalone statement—it is always bound to an enclosing `parallel_scope`. Any `Task` not awaited when the scope exits is automatically cancelled. - -Effect narrowing is available at the spawn site: +### Channels ```spore -let task = spawn uses [NetConnect] { fetch(url) } -// The spawned task can only use NetConnect; it cannot itself spawn. +let channel = Channel.new[I64](capacity: 16); ``` -### 3.3 `Task[T]` — task handle API +Channels are bounded and typed. Senders and receivers cannot escape their +structured ownership rules. -`spawn` returns a `Task[T]` handle. The complete API: - -| Method/Property | Type | Description | -|-----------------|------|-------------| -| `task.await` | `T ! child errors` | Block until the task completes and return its result | -| `task.cancel()` | `Unit` | Request cooperative cancellation of the task | -| `task.is_done` | `Bool` | `true` if the task has completed (successfully or with error) | -| `task.is_cancelled` | `Bool` | `true` if the task was cancelled | - -Usage examples: - -```spore -fn selective_fetch(urls: List[Url]) -> Response ! NetError -uses [Spawn, NetConnect] -{ - parallel_scope { - let primary = spawn { fetch(urls.head()) } - let secondary = spawn { fetch(urls.get(1)) } - - // Wait for the primary; cancel secondary if primary succeeds - let result = primary.await - if !secondary.is_done { - secondary.cancel() - } - result - } -} -``` - -> **Note:** `task.cancel()` is rarely needed in user code—the structured scope automatically cancels unawaited tasks on exit. Explicit cancellation is useful for "first result wins" patterns. - -### 3.4 Channels — communication between tasks - -Channels implement CSP-style message passing: *"Don't communicate by sharing memory; share memory by communicating."* - -```spore -// Buffered channel -let (tx, rx) = Channel.new[Message](buffer: 10) - -// Unbuffered (synchronous) channel -let (tx, rx) = Channel.new[Message](buffer: 0) -``` - -`Channel.new` returns a pair `(Sender[T], Receiver[T])`: - -| Type | Operation | Blocking behavior | -|------|-----------|-------------------| -| `Sender[T]` | `tx.send(value)` | Suspends when buffer is full | -| `Receiver[T]` | `rx.recv()` | Suspends when buffer is empty | - -The `select` expression waits on multiple channels simultaneously: +### Selection ```spore select { - msg from rx1 => handle_message(msg), - msg from rx2 => handle_command(msg), - timeout(5.seconds) => handle_timeout(), -} -``` - -### 3.4 Effect handlers for concurrency control - -Because `Spawn` is an effect, different handlers give the same code different runtime behavior: - -```spore -// Production: real parallel execution on a thread pool -handle { - concurrent_work() -} with { - use ParallelHandler { pool: thread_pool } -} - -// Testing: deterministic sequential execution -handle { - concurrent_work() -} with { - use SequentialHandler {} -} - -// Compile-time simulation: abstract interpretation for cost analysis -handle { - concurrent_work() -} with { - use CostAnalysisHandler {} + value = rx.recv() => value, + timeout(1000) => fallback(), } ``` -A test can replace the `Spawn` handler with `SequentialHandler` and a `NetConnect` handler with a mock—**no async test framework needed**: - -```spore -test "fetch_and_merge produces correct results" { - let mock_net = MockNetHandler { - responses: Map.from([ - (url1, response1), - (url2, response2), - ]) - } - - let result = handle { - fetch_and_merge([url1, url2]) - } with { - use SequentialHandler {} - use mock_net - } - - assert_eq(result, expected_merged) -} -``` - -### 3.5 `map` and pure closures - -`map` only accepts **pure** closures (capture list `[]`). This ensures that mapping over a collection never accidentally introduces side effects. For effectful parallel iteration, use `parallel_scope + spawn`: - -```spore -// Pure: map with a pure closure (no effects, no captures) -let doubled = numbers.map(|x| x * 2) - -// Effectful parallel: use parallel_scope + spawn -let responses = parallel_scope { - urls.map(|url| spawn { fetch(url) }) - .map(|task| task.await) -} -``` - -### 3.6 Recursive patterns instead of loops - -Spore has no imperative loops. Concurrency patterns that traditionally use loops are expressed through recursion and higher-order functions: - -```spore -fn event_loop( - commands: Receiver[Command], - events: Receiver[Event], - shutdown: Receiver[Unit], -) -> Unit ! ChannelClosed -uses [Channel, Spawn] -{ - select { - cmd from commands => { - execute(cmd) - event_loop(commands, events, shutdown) // tail-recursive - }, - evt from events => { - log_event(evt) - event_loop(commands, events, shutdown) - }, - _ from shutdown => { - Unit // base case - }, - timeout(30.seconds) => { - heartbeat() - event_loop(commands, events, shutdown) - }, - } -} -``` - -### 3.7 Complete example: HTTP request handler - -Server entrypoints that accept inbound connections declare `NetListen`; the inner request handler below only needs outbound `NetConnect` plus domain-specific effects. - -```spore -effect HttpHandler = Spawn | NetConnect | DbRead | Clock; - -fn handle_request(req: Request) -> Response ! DbError | Timeout -cost [5000, 800, 200, 3] -uses [HttpHandler] -{ - with_timeout(10.seconds) { - parallel_scope(lanes: 3) { - let auth = spawn uses [DbRead] { verify_token(req.token) } - let user = spawn uses [DbRead] { load_user(req.user_id) } - let config = spawn uses [NetConnect] { fetch_remote_config() } - - let auth_result = auth.await - if !auth_result.valid { - return Response.unauthorized() - } - - Response.ok( - render_page(user.await, config.await) - ) - } - } -} -``` - -### 3.8 Complete example: ETL pipeline - -```spore -fn etl_pipeline( - source: DataSource, - sink: DataSink, - batch_size: I64, -) -> EtlReport ! ExtractError | TransformError | LoadError -cost [batch_size * 200 + 4000, batch_size * 50, batch_size * 40, 6] -uses [Spawn, NetConnect, DbRead, DbWrite] -{ - let (raw_tx, raw_rx) = Channel.new[RawRecord](buffer: batch_size) - let (clean_tx, clean_rx) = Channel.new[CleanRecord](buffer: batch_size) - - parallel_scope(lanes: 6) { - // Extract: 1 lane - let extractor = spawn uses [NetConnect] { - source.stream().each(|record| raw_tx.send(record)) - raw_tx.close() - } - - // Transform: 4 lanes (fan-out) - (0..4).each(|_| { - spawn { - raw_rx.each(|raw| { - match transform(raw) { - Ok(clean) => clean_tx.send(clean), - Err(e) => log_error(e), - } - }) - } - }) - - // Load: 1 lane (fan-in) - let loader = spawn uses [DbWrite] { - clean_rx.fold(0, |count, record| { - sink.write(record) - count + 1 - }) - } - - extractor.await - clean_tx.close() - let loaded = loader.await - - EtlReport { records_loaded: loaded } - } -} -``` - ---- +`select` waits for the first ready arm. ## Reference-level explanation -### 4.1 Structured concurrency model - -#### Task tree - -Every `parallel_scope` creates a nursery node; every `spawn` attaches a leaf: - -```text -main() - └─ parallel_scope ← nursery A - ├─ spawn { fetch(url1) } ← task A1 - ├─ spawn { fetch(url2) } ← task A2 - └─ spawn { ← task A3 - parallel_scope ← nursery B (nested) - ├─ spawn { parse(d1) } ← task B1 - └─ spawn { parse(d2) } ← task B2 - } -``` - -The compiler builds this static task tree at compile time and uses it for: - -- Cost budget allocation verification -- Effect set inheritance checking -- Lane consumption calculation - -#### `parallel_scope` formal semantics - -```text -⟦parallel_scope { e }⟧ = - let nursery = Nursery.new() - let result = ⟦e⟧ with nursery - nursery.await_all() - result - -⟦parallel_scope(lanes: K) { e }⟧ = - let nursery = Nursery.new(max_lanes: K) - let result = ⟦e⟧ with nursery - nursery.await_all() - result -``` - -Invariants: - -- `parallel_scope` is an expression; its type is the type of its body's last expression. -- The scope blocks until all children complete or are cancelled. -- Nested `parallel_scope` blocks form a tree; the inner scope consumes lanes from the outer scope's budget. - -#### `spawn` formal semantics - -```text -⟦spawn { e }⟧ = - let task = nursery.register(thunk: λ(). ⟦e⟧) - nursery.schedule(task) - task : Task[T] where e : T -``` - -Constraints: - -- `spawn` may only appear lexically inside a `parallel_scope`. -- The enclosing function must declare `uses [Spawn]`. -- Effect narrowing: `spawn uses [C₁, C₂] { e }` requires `{C₁, C₂} ⊆ parent.uses`. - -#### Error propagation - -Two modes are supported: - -| Mode | On child failure | Return type | Use case | -|------|-----------------|-------------|----------| -| `fail_fast` (default) | Cancel siblings, propagate error | `T ! E` | All child results are required | -| `collect` | Do not cancel siblings; collect results | `Result[T, E]` per task | Partial results are meaningful | - -```spore -// fail_fast (default) -parallel_scope { - let a = spawn { fetch(url1) } // if this throws NetError… - let b = spawn { fetch(url2) } // …b is cancelled - (a.await, b.await) // scope propagates NetError upward -} - -// collect (supervisor mode) -parallel_scope(on_error: .collect) { - let a = spawn { fetch(url1) } - let b = spawn { fetch(url2) } - match (a.await, b.await) { - (Ok(r1), Ok(r2)) => (r1, r2), - (Err(e), _) | (_, Err(e)) => handle_partial_failure(e), - } -} -``` - -### 4.2 Effect handlers for concurrency - -#### `spawn` and the `Spawn` effect - -`spawn { ... }` is a built-in expression guarded by the standard `Spawn` -effect. Conceptually, it performs the platform-provided `Spawn.spawn` -operation, but the syntax and structured-scope checks are part of the language: - -```spore -effect Spawn { - fn spawn[T](task: () -> T) -> Task[T] -} -``` - -Functions declare the effect via `uses [Spawn]`. Without this declaration, the compiler **statically rejects** any `spawn { ... }` expression. - -#### Handler mechanism - -An effect handler is a concrete interpretation of an effect's operations: - -```spore -handle { - concurrent_work() -} with { - use ParallelHandler { pool: thread_pool } -} -``` - -#### Built-in handlers - -The Platform provides the following `Spawn` handlers: - -| Handler | Behavior | Use case | -|---------|----------|----------| -| `ParallelHandler(pool)` | Real OS thread / green thread parallelism | Production | -| `SequentialHandler` | Executes spawned tasks sequentially in spawn order | Unit testing | -| `DeterministicHandler(seed)` | Fixed scheduling order, replayable | Regression testing, CI | -| `CostAnalysisHandler` | Abstract execution, computes cost | Compile-time simulation | -| `TracingHandler(inner)` | Wraps any handler, records event timeline | Debugging, profiling | - -#### Custom effects and handlers - -Users may define their own concurrency-related effects: - -```spore -effect RateLimit { - fn acquire_permit() -> () ! RateLimitExceeded - fn release_permit() -> () -} - -handler RateLimit as token_bucket_handler(rate: I64, per: Duration) { - fn acquire_permit() -> () ! RateLimitExceeded { - if self.tokens > 0 { - self.tokens -= 1; - () - } else { - throw RateLimitExceeded - } - } - fn release_permit() -> () { - self.tokens += 1; - () - } -} -``` - -#### Relationship between effects and the `uses` clause - -| Dimension | `uses [...]` clause | `effect` definition | -|-----------|--------------------------|----------------------| -| Definition layer | Signature metadata, compiler-verified | Declares operations interceptable by a handler | -| Semantics | "What effects this function may perform" | "What operations this effect provides" | -| Verification | Compile-time effect set check | Compile-time + handler binding time | -| Example | `uses [Spawn]` permits calling `spawn` | `effect Spawn { fn spawn... }` defines the operation | - -Declaring `uses [Spawn]` means "this function may perform the `Spawn` effect". The `effect` keyword defines both the operations and makes the effect available for use in `uses` clauses. - -### 4.3 Channel types - -#### `Channel[T]` - -```spore -// Buffered -let (tx, rx) = Channel.new[Message](buffer: 10) - -// Unbuffered (synchronous handoff) -let (tx, rx) = Channel.new[Message](buffer: 0) -``` - -#### Channel operations as effects - -```spore -effect Channel { - fn send[T](channel: Sender[T], value: T) -> Unit ! ChannelClosed - fn recv[T](channel: Receiver[T]) -> T ! ChannelClosed -} -``` - -Functions using channels must declare the corresponding effects: - -```spore -fn producer(tx: Sender[I64]) -> Unit ! ChannelClosed -uses [Channel] -{ - (0..100).each(|i| tx.send(i)) -} - -fn consumer(rx: Receiver[I64]) -> List[I64] ! ChannelClosed -uses [Channel] -{ - rx.collect() -} -``` - -#### `select` expression - -```spore -select { - msg from rx1 => handle_message(msg), - msg from rx2 => handle_command(msg), - timeout(5.seconds) => handle_timeout(), -} -``` - -Semantics: - -- `select` waits on all branches simultaneously. -- When multiple branches are ready, the scheduler picks one (non-deterministic). -- `timeout` is a built-in special branch that fires after the given duration. -- `select` is an expression—all branches must return the same type. - -#### Channel closure semantics - -| Operation | Behavior when channel is closed | -|-----------|---------------------------------| -| `tx.send(value)` | Returns `Err(ChannelClosed)` | -| `rx.recv()` | Returns buffered value if available; otherwise `Err(ChannelClosed)` | -| `tx.close()` | Marks the sender end as closed | -| `rx.each(\|msg\| ...)` | Iterates until channel is closed and buffer is drained | - -#### Fan-out / Fan-in patterns - -`tx.clone()` creates an additional sender for the same channel. When all senders are closed, the channel closes automatically (reference-counted semantics). - -### 4.4 Cost budgets for parallel work - -#### The `parallel(lane)` dimension - -The cost model (SEP-0004) defines four dimensions. This SEP defines the semantics of the fourth: - -| Dimension | Abbreviation | Unit | Meaning | -|-----------|-------------|------|---------| -| Compute | `C` | op | CPU operation steps | -| Allocation | `A` | cell | Heap memory allocations | -| IO | `W` | call | External call count | -| **Parallel** | **`P`** | **lane** | **Concurrent execution channels** | - -`lane` is a logical concept—it does not directly equal an OS thread or CPU core. It is a unit of concurrency that the compiler can statically track. - -#### Cost combination rules - -**Sequential execution:** - -```text -cost(A ; B) = cost(A) + cost(B) -- C, A, W dimensions: sum -parallel(A ; B) = max(A.P, B.P) -- P dimension: peak -``` - -**Parallel execution (inside `parallel_scope`):** - -```text -cost(parallel { A, B }) = max(cost(A), cost(B)) -- C, A, W: max (wall-clock) -parallel(parallel { A, B }) = A.P + B.P -- P: sum (concurrent resources) -``` - -Intuition: wall-clock time of parallel tasks is determined by the slowest branch (`max`), but the concurrent resources consumed are the sum across branches. - -**Spawn overhead:** - -```text -cost(spawn { body }) = spawn_overhead + cost(body) - where spawn_overhead = 5 op + 1 cell -- constant, configurable in spore.toml -``` - -#### Lane inference rules - -The compiler infers lane requirements automatically: - -1. No `Spawn` → `P = 0` (purely sequential) -2. `parallel_scope { spawn × N }` → `P = N` (all N spawns execute in parallel) -3. `parallel_scope(lanes: K) { spawn × N }` → `P = min(K, N)` (upper bound applied) -4. Sequential scopes: `P = max(P_scope₁, P_scope₂)` (peak across sequential phases) -5. Nested parallel children: `P = sum(P_children)` (sum within a single parallel scope) - -Developers need not manually allocate lanes. The compiler infers from the task tree and function signatures. An optional `lanes: K` parameter on `parallel_scope` sets a hard upper limit; excess spawns queue. - -#### Lane budget allocation across nested scopes - -```spore -fn pipeline(data: Data) -> Result -cost [5000, 600, 80, 4] -uses [Spawn, FileRead, NetConnect] -{ - // Phase 1: 4 lanes - let prepared = parallel_scope(lanes: 4) { - let a = spawn { parse_part1(data) } - let b = spawn { parse_part2(data) } - let c = spawn { parse_part3(data) } - let d = spawn { validate(data) } - merge(a.await, b.await, c.await, d.await) - } - // Phase 1 exits; 4 lanes released - - // Phase 2: 2 lanes - parallel_scope(lanes: 2) { - let uploaded = spawn { upload(prepared) } - let logged = spawn { log_result(prepared) } - (uploaded.await, logged.await) - } - - // Compiler infers: peak lanes = max(4, 2) = 4 -} -``` - -#### Symbolic cost expressions for lane counts - -Verified lane bounds use Index parameters. Dynamic `List` lengths remain valid -runtime data, but they are not CostExpr variables; use `Vec[..., max: N]` when -the lane budget must be verified statically: - -```spore -fn fetch_all[N: Index](urls: Vec[Url, max: N]) -> Vec[Response, max: N] ! NetError -cost [N * per_fetch + N * 10, N * 2, N, N] -uses [Spawn, NetConnect] -{ - parallel_scope { - urls.map(|url| spawn { fetch(url) }) - .map(|task| task.await) - } -} -``` - -```bash -$ sporec --query-cost fetch_all -{ - "function": "fetch_all", - "cost_symbolic": "N * (5 + per_fetch) + N", - "parallel_lanes": "N", - "note": "lane count is bounded by the Vec capacity index N" -} -``` - -For dynamic `List` inputs, enforce runtime caps with ordinary checks or -`parallel_scope(lanes: K)` and treat remaining work as runtime-metered rather -than verified by CostExpr. - -```spore -fn bounded_fetch(urls: List[Url]) -> List[Response] ! NetError -cost [100 * per_fetch + 500, 800, 100 * per_fetch, 100] -uses [Spawn, NetConnect] -{ - parallel_scope(lanes: 10) { - // At most 10 parallel tasks; callers enforce any total input-count policy separately - urls.each(|url| spawn { fetch(url) }) - } -} -``` - -```bash -$ sporec --query-cost bounded_fetch -{ - "function": "bounded_fetch", - "cost_upper": "100 × per_fetch + 500", - "cost_declared": "four-slot `cost [...]` on `bounded_fetch` (see SEP-0001)", - "status": "within_bound", - "parallel_lanes_peak": 10, - "note": "prefer List[T] here; lanes capped at 10; caller bounds total input count separately" -} -``` - -#### Cost query output - -```bash -$ sporec --query-cost pipeline -{ - "function": "pipeline", - "cost_scalar": 4200, - "cost_declared": "[5000, 600, 80, 4]", - "status": "within_bound", - "dimensions": { - "compute": 3800, - "alloc": 45, - "io": 3, - "parallel_lanes_peak": 4 - }, - "breakdown": [ - { - "scope": "parallel_scope#1", - "lanes": 4, - "wall_cost": 1200, - "children": [ - { "task": "parse_part1", "cost": 1000 }, - { "task": "parse_part2", "cost": 1200 }, - { "task": "parse_part3", "cost": 900 }, - { "task": "validate", "cost": 800 } - ] - }, - { - "scope": "parallel_scope#2", - "lanes": 2, - "wall_cost": 2600, - "children": [ - { "task": "upload", "cost": 2600 }, - { "task": "log_result", "cost": 400 } - ] - } - ] -} -``` - -#### Interaction with the hole system - -Holes in concurrent functions participate in cost budget analysis: +### Effect requirement -```spore -fn concurrent_pipeline(data: Data) -> Result -cost [3000, 400, 100, 2] -uses [Spawn, NetConnect] -{ - parallel_scope(lanes: 2) { - let a = spawn { fetch(data.url) } // cost: 800 - let b = spawn { ?process_logic } // hole: remaining budget - merge(a.await, b.await) - } -} -``` +`spawn`, scoped task creation, and task scheduling require the `Spawn` effect to +appear in the enclosing effect surface, either directly or through a named +surface. Channel operations require the channel effects defined by the standard +library or platform package that supplies them. -The compiler reports: +### Scope rule -```json -{ - "hole": "process_logic", - "cost_consumed": 800, - "cost_budget_remaining": 2200, - "parallel_context": { - "scope_lanes": 2, - "lane_index": 1, - "sibling_costs": [800] - }, - "note": "This hole runs in lane 1 of a 2-lane parallel_scope; cost must not exceed 2200" -} -``` +Every task belongs to exactly one lexical `parallel_scope`. A task must be +awaited, cancelled, or automatically cancelled before the scope returns. -### 4.5 Cancellation semantics +### Budget interaction -#### Propagation direction +`parallelism` bounds maximum fan-out inside the realization. `nesting` bounds +nested concurrency scopes together with other control expressions. `effects` +can bound effect operation sites performed by concurrent branches. -Cancellation flows **top-down** through the task tree, complementing the **bottom-up** direction of error propagation: +### Type rule for tasks -```text -Error propagation: child → parent (bubbles up) -Cancellation propagation: parent → child (cascades down) -``` +If expression `e` checks as `T ! E`, then `spawn { e }` checks as `Task[T, E]`. +Awaiting a `Task[T, E]` yields `T` and reintroduces failure type `E` at the +await site. A non-failing spawned expression uses the empty failure type, +represented as `Task[T, Never]`. -**Nested scope cancel propagation example:** +### Determinism and properties -```spore -parallel_scope { // scope A - let a = spawn { long_running_1() } // task A1 - let b = spawn { // task A2 - parallel_scope { // scope B (nested) - let c = spawn { subtask_1() } // task B1 - let d = spawn { subtask_2() } // task B2 - (c.await, d.await) - } - } - (a.await, b.await) -} -``` - -When scope A is cancelled, the 4-step propagation: - -| Step | Action | Result | -|------|--------|--------| -| 1 | Scope A receives cancel signal | Sets cancel flag on A1 and A2 | -| 2 | Task A1 checks cancel at next effect call | A1 raises `Cancelled`, begins cleanup (`defer` blocks run) | -| 3 | Task A2 propagates cancel to nested scope B | Scope B sets cancel flag on B1 and B2 | -| 4 | Tasks B1 and B2 check cancel at next effect call | B1, B2 raise `Cancelled`; scope B exits; scope A exits | - -All `defer` blocks execute in reverse order at each level. The entire tree unwinds deterministically. - -#### Cooperative cancellation - -Spore uses **cooperative** cancellation. Tasks are never forcibly terminated. Instead, they check for cancellation at two kinds of points: - -1. **Effect operation sites** — every effect call (`fetch`, `send`, `recv`, etc.) passes through a handler that checks the cancellation flag before resuming. This is a zero-cost interception point. -2. **Explicit checkpoints** — CPU-bound code without effect calls must manually insert `check_cancelled()`: - -```spore -fn cpu_bound_work(data: List[Item]) -> List[Item] -uses [Spawn] -{ - data.enumerate().fold([], |results, (i, item)| { - // CPU-bound code needs manual cancellation checkpoints - if i % 100 == 0 { - check_cancelled() // throws Cancelled if cancellation was requested - } - results.push(transform(item)) - results - }) -} -``` - -The compiler may emit a warning when it detects a long iterative computation with no cancellation checkpoint. - -#### `defer` and cancellation - -`defer` blocks execute even during cancellation, guaranteeing resource cleanup: - -```spore -fn with_temp_file() -> Data ! IoError | Cancelled -uses [Spawn, FileRead, FileWrite] -{ - let file = create_temp_file() - defer { delete_file(file) } // executes on cancellation too - - write_data(file, generate_large_data()) - read_and_process(file) -} -``` - -#### Timeout as cancellation - -`with_timeout` is syntactic sugar over cancellation: - -```spore -fn fetch_with_timeout(url: Url, limit: Duration) -> Response ! NetError | Timeout -uses [Spawn, NetConnect, Clock] -{ - with_timeout(limit) { - fetch(url) - } -} -``` - -Internally, `with_timeout` spawns a timer task and the main task in a scope; whichever completes first wins—if the timer fires first, the main task is cancelled. - -#### Cancellation semantics summary - -| Scenario | Behavior | -|----------|----------| -| Parent scope is cancelled | All child tasks receive cancellation signal | -| Child task at an IO effect point | Handler checks cancellation flag, throws `Cancelled` | -| Child task in pure computation | Must manually call `check_cancelled()` in loops | -| `defer` blocks | Execute normally during cancellation for resource cleanup | -| Already-completed task is "cancelled" | No-op; result is already available | -| `select` waiting when cancelled | All branches are abandoned; `Cancelled` is thrown | - ---- +Schedule-independent behavior is expressed as source properties. The concurrency +checker may emit claims about task lifetime, cancellation coverage, and channel +boundary correctness for evidence generation. ## Human experience impact -### Simplified mental model - -Developers only need to learn one concurrency pattern: **`parallel_scope` + `spawn`**. There is no zoo of primitives (`Thread`, `ExecutorService`, `async`/`await`, `Future`, `Promise`, `Task.Run`, `go`, `select`, `WaitGroup`, etc.). The structured tree model mirrors the already-familiar function call tree. - -### No function coloring - -In languages with `async`/`await`, developers must decide at every function boundary whether the function is async or sync—and refactoring between the two modes is a breaking change that propagates through the call graph. In Spore, a function that uses concurrency simply declares `uses [Spawn]` in its signature. Callers do not need to be "async" themselves; they just need to provide a `Spawn` handler (which is typically provided at the application boundary). - -### Testability - -The replaceable handler model means concurrent code can be tested with `SequentialHandler` for fully deterministic execution. No special async test framework or runtime is required. Mock handlers for IO effects combine naturally with sequential spawn handlers. - -### Readable error messages - -Because the task tree mirrors the lexical scope tree, error messages and stack traces preserve the full causal chain from the spawning point to the failure point. This eliminates the "lost context" problem common in fire-and-forget concurrency. - -### Explicit effects as documentation - -When a function signature says `uses [Spawn, NetConnect]`, a human reader immediately knows the function is concurrent and performs outbound network access. This serves as machine-checked documentation. - ---- +Concurrency appears at the signature boundary through the declared `uses` +surface and shape budgets. Reviewers can see whether fan-out is intended before +reading the body. ## Agent experience impact -### Deterministic simulation - -AI agents benefit enormously from being able to run concurrent code in a deterministic mode. `DeterministicHandler(seed)` replays the exact same scheduling order, enabling agents to: - -- Predict outputs of concurrent functions. -- Build reliable test expectations. -- Reproduce and diagnose failures. - -### Structured cost budgets - -Agents can use `sporec --query-cost` to obtain the parallel lane usage and total cost of any function. This structured JSON output is directly consumable and enables agents to: - -- Verify that concurrent code stays within budget before deployment. -- Suggest lane count optimizations. -- Identify bottleneck tasks in a parallel scope. - -### Code generation - -The constrained concurrency model (`parallel_scope` + `spawn` only, no raw threads) makes it significantly easier for agents to generate correct concurrent code. The structured scoping rules eliminate entire classes of concurrency bugs (leaked tasks, use-after-free in concurrent contexts) that agents are otherwise prone to producing. - -### Effect narrowing for safe delegation - -When an agent generates a spawned task, it can narrow the effect set (`spawn uses [NetConnect] { ... }`), providing a compile-time guarantee that the generated subtask cannot exceed its intended permissions—a form of least-privilege that is checkable at compile time. - ---- +Agents filling concurrent holes receive effect and budget context, making it +possible to reject unbounded fan-out or missing awaits before proposing code. ## Structured representation / protocol impact -### Task tree as data - -The static task tree is available as a structured query: +Concurrency verification emits evidence claims such as: -```bash -$ sporec --query-task-tree handle_request -{ - "root": "handle_request", - "scopes": [ - { - "id": "parallel_scope#1", - "lanes": 3, - "children": [ - { "task": "verify_token", "uses": ["DbRead"], "cost": 600 }, - { "task": "load_user", "uses": ["DbRead"], "cost": 800 }, - { "task": "fetch_remote_config", "uses": ["NetConnect"], "cost": 1200 } - ] - } - ] -} +```text +claim: task_scope_closed +claim: parallelism <= 4 +claim: all_spawned_tasks_awaited_or_cancelled ``` -### Handler binding as protocol - -At the application boundary (the `main` function or service entry point), handler binding forms a protocol specification: *"this application uses real parallelism with a pool of N threads, real network IO, and PostgreSQL for database"*. This binding can be serialized as part of the deployment configuration. - -### Integration with Spore subsystems - -| Spore subsystem | Interaction with the concurrency model | -|-----------------|---------------------------------------| -| Signature syntax | `uses [Spawn, ...]` declares concurrency effect; `cost [c, a, i, p]` bounds each dimension | -| Cost model | Fourth dimension `parallel(lane)` defined by this SEP | -| Effect system | `Spawn` is a built-in effect; child task effects ⊆ parent | -| Effect annotations | `pure` excludes `Spawn`; `deterministic` + `Spawn` requires schedule-independent results | -| Hole system | Concurrent holes participate in cost budget analysis | -| Platform | Platform provides production `Spawn` handler implementations | - ---- +HoleReport may include concurrency-specific budget and dependency context. ## Diagnostics impact -### Compile-time diagnostics +Concurrency diagnostics use existing categories: -The compiler reports the following concurrency-related diagnostics: - -| Diagnostic | Severity | Example | -|------------|----------|---------| -| `spawn` outside `parallel_scope` | Error | `spawn { ... }` at top level | -| Missing `Spawn` effect | Error | Function calls `spawn` but does not declare `uses [Spawn]` | -| Lane budget exceeded | Error | `parallel_scope(lanes: 2)` with 5 spawns that cannot queue | -| Cost budget exceeded due to parallelism | Error | Inferred parallel dimension exceeds declared `parallel` slot of `cost [...]` | -| Long recursion/iteration without cancellation checkpoint | Warning | CPU-bound computation > 100 iterations without `check_cancelled()` | -| Effect widening in spawn | Error | `spawn uses [NetConnect] { ... }` when parent lacks `NetConnect` | -| Unused `Task` (never awaited, never cancelled) | Warning | Task result is silently discarded | - -### Runtime diagnostics - -The `TracingHandler` wraps any spawn handler and records a structured event timeline: - -```json -[ - { "event": "scope_enter", "id": "ps#1", "lanes": 4, "time_ns": 0 }, - { "event": "spawn", "task": "parse_part1", "parent": "ps#1", "time_ns": 12 }, - { "event": "spawn", "task": "parse_part2", "parent": "ps#1", "time_ns": 15 }, - { "event": "complete", "task": "parse_part1", "cost": 1000, "time_ns": 5200 }, - { "event": "complete", "task": "parse_part2", "cost": 1200, "time_ns": 6100 }, - { "event": "scope_exit", "id": "ps#1", "time_ns": 6105 } -] -``` - -This timeline can be rendered as a Gantt chart, flame graph, or task dependency graph for performance analysis. - ---- +- `F0xxx` for missing effects; +- `B0xxx` for fan-out or nesting budget violations; +- `P0xxx` when a schedule-related property fails; +- `W0xxx` for suspicious but accepted patterns. ## Drawbacks -1. **No escape hatch for long-lived background tasks.** Structured concurrency requires all tasks to complete within their scope. Long-lived background tasks (e.g., a metrics reporter, a heartbeat sender) must be managed through the Platform layer rather than user-level `spawn`. This adds complexity to the Platform interface. - -2. **Cooperative cancellation has latency.** CPU-bound tasks that forget to insert `check_cancelled()` will not respond to cancellation promptly. While the compiler warns about this, the warning is heuristic-based and may produce false negatives. - -3. **No shared-memory primitives.** The absence of `Mutex`/`RwLock`/atomics means certain high-performance patterns (lock-free data structures, atomic counters) are not directly expressible. These must be provided as `unsafe` Platform primitives if needed. - -4. **Effect handler overhead.** Algebraic effect handlers, particularly those involving continuations, may have non-trivial runtime overhead compared to direct `async`/`await` compilation. The Platform must optimize hot paths aggressively. +Structured scopes reject some fire-and-forget patterns. Users must model long +running services through explicit platform or supervisor abstractions. -5. **Learning curve for effect-based concurrency.** Developers coming from `async`/`await` languages will need to learn the effect/handler model. While conceptually simpler, the initial unfamiliarity may slow adoption. - -6. **Channel-only communication may be verbose.** Some patterns (e.g., a shared accumulator) are more naturally expressed with a shared mutable variable than with a channel. The channel-based workaround requires an extra task acting as the accumulator's owner. - ---- +Budgeted fan-out is conservative when the checker cannot prove a tighter bound. ## Alternatives considered -### Alternative 1: async/await (Rust, JavaScript, Python) - -**Rejected.** Introduces function coloring that virally propagates through the codebase. Incompatible with Spore's "signature is the complete spec" philosophy. Also makes handler replacement (and thus simulatable execution) significantly harder. - -**Consequences of rejection:** Spore requires effect handlers for concurrency, which adds implementation complexity in the compiler. However, users never deal with colored functions, and testing concurrent code requires zero special infrastructure. - -### Alternative 2: Green threads (Go, Java Loom) - -**Rejected.** Green threads are colorless (good) but lack effect tracking (bad). Any goroutine can perform any operation, violating Spore's effect constraint model. Additionally, goroutine leaks (Go's well-known problem) directly violate the resource safety guarantee. - -**Consequences of rejection:** Spore cannot easily adopt existing Go-style runtime implementations. The effect handler approach requires a custom runtime that understands continuations. +### Unstructured tasks -### Alternative 3: Actor model (Erlang, Akka) +Rejected because tasks escaping their lexical scope make lifetime and realization +shape difficult to verify. -**Rejected.** Actor mailboxes use untyped message protocols, conflicting with Spore's type system philosophy. Message copying overhead makes cost modeling unpredictable. The isolation model is also heavier than needed for Spore's structured concurrency goals. +### Async coloring -**Consequences of rejection:** Spore lacks built-in location-transparent distribution (actors excel here). Distributed systems require explicit Platform-level support. +Rejected because splitting function colors makes refactors viral. Spore uses +effects and handlers instead. -### Alternative 4: Software Transactional Memory (Haskell STM) +### Runtime-only concurrency checks -**Rejected.** STM's retry mechanism has unbounded cost—the number of retries depends on runtime contention, making static cost analysis intractable. - -**Consequences of rejection:** Certain concurrent data structure patterns are more verbose with channels than with STM. Accepted as a worthwhile trade-off for cost predictability. - -### Alternative 5: Mutex/RwLock as primary synchronization - -**Rejected.** Lock contention cost is unpredictable (spin/retry count depends on runtime scheduling). Deadlock detection in the presence of shared memory and locks is NP-hard. The effect handler + continuation model does not compose cleanly with lock semantics. - -**Consequences of rejection:** Performance-critical lock-free patterns (e.g., atomic counters) are unavailable in safe Spore. A future `unsafe_shared` extension point is reserved but not in the accepted surface. - -### Alternative 6: Unstructured spawning with optional scoping - -**Rejected.** Providing both structured and unstructured spawning (as in Kotlin's `GlobalScope` vs. `coroutineScope`) creates an escape hatch that undermines cost analysis. If untracked concurrent tasks exist, the compiler cannot verify four-slot `cost [...]` bounds or lane caps. - -**Consequences of rejection:** Long-lived background tasks (daemon processes, connection pools) must be managed at the Platform level rather than in user code. This pushes certain patterns to Platform authors. - ---- +Rejected because the signature model needs Agent-visible constraints before a +realization is written. ## Prior art -### Koka — Algebraic effects for concurrency - -Koka (Daan Leijen, Microsoft Research) pioneered the use of algebraic effect handlers as a general mechanism for side effects, including concurrency. Spore adopts Koka's core insight—concurrency operations are effects that can be intercepted by handlers—and extends it with structured scoping and cost budgets. - -**Reference:** Leijen, Daan. *"Algebraic Effects for Functional Programming"*. Microsoft Research, 2016. - -### Kotlin Coroutines — Structured concurrency - -Kotlin's `coroutineScope` + `launch`/`async` model demonstrated that structured concurrency is practical in a production language. Spore's `parallel_scope` + `spawn` is directly inspired by Kotlin's design, but replaces the coroutine machinery with effect handlers to eliminate function coloring. - -**Reference:** Kotlin Coroutines documentation. - -### Python Trio — Nurseries - -Trio (Nathaniel J. Smith) coined the "nursery" metaphor for structured concurrency scopes and wrote the seminal *"Go statement considered harmful"* essay. Spore's nursery semantics (parent waits for all children, cancellation cascades downward) directly follow Trio's model. - -**Reference:** Smith, Nathaniel J. *"Notes on structured concurrency, or: Go statement considered harmful"*. 2018. - -### Swift Structured Concurrency — Task groups - -Swift 5.5 introduced `TaskGroup` as a structured concurrency primitive with cancellation propagation and child-task scoping. Spore draws from Swift's approach of making the scope the lifetime boundary for all child tasks. - -### OCaml 5 — Effect handlers in a systems language - -OCaml 5 added native support for algebraic effect handlers, proving that the mechanism can be implemented efficiently in a compiled language. Spore's handler compilation strategy will draw from OCaml 5's implementation experience. - -**Reference:** OCaml 5 Effects. - -### Go — CSP channels - -Go's channel-based communication model (*"Don't communicate by sharing memory; share memory by communicating"*) directly inspires Spore's `Channel[T]`. Spore adds type safety (typed channels with effect declarations) and static cost analysis on top of Go's runtime model. - -**Reference:** Go Concurrency. - -### Bob Nystrom — "What Color is Your Function?" - -This 2015 blog post identified the function coloring problem that `async`/`await` introduces. Spore's colorless function design is a direct response to this critique. - -**Reference:** Nystrom, Bob. *"What Color is Your Function?"*. 2015. - ---- +Swift task groups, Kotlin structured concurrency, Koka effect handlers, and Roc +platform boundaries inform this model. ## Backward compatibility and migration -### This is a new feature - -The concurrency model is introduced as a new language feature. There is no existing Spore code that uses concurrency, so there are no backward compatibility concerns at the language level. - -### Interaction with existing SEPs - -- **SEP-0001 (Core Syntax, extended by SEP-0003 for effects):** The `uses [Spawn, ...]` clause and `cost [c, a, i, p]` declarations are extensions to the signature syntax defined in SEP-0001, with effect annotations from SEP-0003. No breaking changes to existing signatures are required; `uses` and `cost` are additive clauses. -- **SEP-0004 (Cost Model):** This SEP defines the fourth dimension `parallel(lane)`. Cost declarations are always the four-slot form `cost [compute, alloc, io, parallel]`; sequential code uses `parallel = 0`. - -### Migration path for developers from other languages - -| Source language | Key mental model shift | -|----------------|----------------------| -| Rust (`async`/`await`, `tokio`) | Replace `async fn` with `uses [Spawn]`; replace `tokio::spawn` with `parallel_scope` + `spawn`; no `.await` coloring | -| Go (goroutines) | Replace `go func()` with `parallel_scope` + `spawn`; all goroutines must be scoped; channels work similarly | -| Kotlin (coroutines) | Replace `coroutineScope` with `parallel_scope`; replace `launch`/`async` with `spawn`; effect handlers replace `Dispatchers` | -| JavaScript (`async`/`await`, `Promise`) | Replace `async`/`await` with effect-based concurrency; replace `Promise.all` with `parallel_scope` | - ---- +Concurrent code should move fan-out constraints into `budget { parallelism: N }` +and retain explicit `Spawn` membership in the declared effect surface. ## Unresolved questions -1. **Exact runtime representation of `Task[T]`.** Should `Task[T]` be a zero-cost wrapper around a future, or should it carry metadata (spawn location, cost estimate) for runtime introspection? The former is more efficient; the latter enables richer diagnostics. - -2. **`select` fairness guarantees.** When multiple branches are simultaneously ready, should `select` be fair (round-robin) or biased (first-listed wins)? Fair selection prevents starvation but adds overhead. The default behavior and whether it is configurable need further design. - -3. **Channel backpressure strategies.** The current design offers only buffer-size-based backpressure (block when full). Should we support additional strategies such as dropping oldest, dropping newest, or raising an error? These would be expressible as effect handler variations. - -4. **`unsafe_shared` escape hatch.** For extreme performance scenarios (lock-free counters, concurrent hash maps), should Spore provide an `unsafe_shared` primitive that bypasses the channel-only constraint? If so, what static checks should surround its use? - -5. **Dynamic lane adjustment.** The current model uses static lane counts (`lanes: K`). Should the runtime be able to dynamically adjust the actual parallelism based on system load, and if so, how does this interact with compile-time cost verification? - -6. **Effect handler composition order.** When multiple handlers appear in one binding block (for example `with { use handler_a {}, use handler_b {} }`), the composition order matters. The precise semantics of handler ordering for concurrency effects (especially when `Spawn` interacts with IO effects) need formal specification. - -7. **Distributed concurrency.** This SEP covers single-machine concurrency. Extending the model to distributed settings (multi-node `parallel_scope`, remote channels) is a future concern but should not be foreclosed by current design decisions. - -8. **Compiler warning heuristics for missing `check_cancelled()`.** What constitutes "long iteration" that should trigger a warning? Should this be configurable (e.g., estimated iteration count > N), and should it be suppressible with an annotation? - -9. **Interaction between `deterministic` property and `Spawn`.** A function inferred as `deterministic` (from `uses`) with `uses [Spawn]` must produce schedule-independent results. The compiler verification strategy for this property (e.g., requiring commutative merge operations) needs further design. - -10. **Channel type variance.** Should `Sender[T]` be contravariant in `T` and `Receiver[T]` covariant? This affects composability with generic code and needs alignment with the broader type system design. - ---- - -## Appendix A: Syntax quick-reference - -### A.1 `parallel_scope` - -```spore -// Basic form -parallel_scope { } - -// With lane limit -parallel_scope(lanes: ) { } - -// Supervisor mode (collect errors instead of fail-fast) -parallel_scope(on_error: .collect) { } - -// Combined -parallel_scope(lanes: 4, on_error: .collect) { } -``` - -### A.2 `spawn` - -```spore -// Returns Task[T] -let task = spawn { } - -// Direct await -let result = spawn { }.await - -// With effect narrowing -let task = spawn uses [NetConnect] { } -``` - -### A.3 `Task[T]` - -```spore -let value: T = task.await // Wait for result -task.cancel() // Request cooperative cancellation -task.is_done // Bool: has the task completed? -task.is_cancelled // Bool: was the task cancelled? -``` - -### A.4 Channel - -```spore -// Create -let (tx, rx) = Channel.new[T](buffer: N) - -// Send / receive -tx.send(value) // ! ChannelClosed -let value = rx.recv() // ! ChannelClosed - -// Close and clone -tx.close() -let tx2 = tx.clone() - -// Iterate (HOF, no for loops) -rx.each(|msg| handle(msg)) -rx.collect() -``` - -### A.5 `select` - -```spore -select { - from => , - timeout() => , -} -``` - -### A.6 Cancellation - -```spore -// Timeout -with_timeout(duration) { } - -// Explicit cancellation checkpoint -check_cancelled() - -// Cleanup (runs even on cancel) -defer { } -``` - -### A.7 Effect and handler - -Concrete syntax for `effect`, `handler`, and `handle ... with` is defined in SEP-0001. The effect algebra and handler model that concurrency relies on are specified in SEP-0003. - -### A.8 Function signature with concurrency - -```spore -fn name(params) -> ReturnType ! Errors -where T: Constraints -cost [c_compute, c_alloc, c_io, c_parallel] -uses [Spawn, Channel, ...] -{ - -} -``` - -### A.9 Complete example: HTTP handler - -```spore -effect HttpHandler = Spawn | NetConnect | DbRead | Clock; - -fn handle_request(req: Request) -> Response ! DbError | Timeout -cost [5000, 800, 200, 3] -uses [HttpHandler] -{ - with_timeout(10.seconds) { - parallel_scope(lanes: 3) { - let auth = spawn uses [DbRead] { verify_token(req.token) } - let user = spawn uses [DbRead] { load_user(req.user_id) } - let config = spawn uses [NetConnect] { fetch_remote_config() } - - let auth_result = auth.await - if !auth_result.valid { - return Response.unauthorized() - } - - Response.ok(render_page(user.await, config.await)) - } - } -} -``` - -### A.10 Complete example: ETL pipeline - -```spore -fn etl_pipeline( - source: DataSource, - sink: DataSink, - batch_size: I64, -) -> EtlReport ! ExtractError | TransformError | LoadError -cost [batch_size * 200 + 4000, batch_size * 50, batch_size * 40, 6] -uses [Spawn, NetConnect, DbRead, DbWrite] -{ - let (raw_tx, raw_rx) = Channel.new[RawRecord](buffer: batch_size) - let (clean_tx, clean_rx) = Channel.new[CleanRecord](buffer: batch_size) - - parallel_scope(lanes: 6) { - // Extract: 1 lane - let extractor = spawn uses [NetConnect] { - source.stream().each(|record| raw_tx.send(record)) - raw_tx.close() - } - - // Transform: 4 lanes (fan-out) - (0..4).each(|_| { - spawn { - raw_rx.each(|raw| { - match transform(raw) { - Ok(clean) => clean_tx.send(clean), - Err(e) => log_error(e), - } - }) - } - }) - - // Load: 1 lane (fan-in) - let loader = spawn uses [DbWrite] { - clean_rx.fold(0, |count, record| { - sink.write(record) - count + 1 - }) - } - - // Collect and report - extractor.await - clean_tx.close() - let loaded = loader.await - EtlReport { records_loaded: loaded } - } -} -``` +1. Should cancellation checkpoints be inserted by tools or only written by users? +2. Should channel capacity participate in budget checking? +3. How should deterministic scheduling properties be generated for common patterns? diff --git a/seps/SEP-0008-module-package-system.md b/seps/SEP-0008-module-package-system.md index 1e2e70d..608fb70 100644 --- a/seps/SEP-0008-module-package-system.md +++ b/seps/SEP-0008-module-package-system.md @@ -11,6 +11,8 @@ requires: - 2 - 3 - 4 + - 5 + - 6 discussion: "https://github.com/spore-lang/spore-evolution/discussions/8" pr: null superseded_by: null @@ -18,2120 +20,267 @@ superseded_by: null # SEP-0008: Module & Package System -> **Executive Summary**: Defines a content-addressed package system (BLAKE3 dual-hash) with platform-as-package architecture, where a selected Platform package declares its startup contract, host adapter, and handled effects via manifest metadata and package modules. Uses 1-file-=1-module mapping with dot-separated import paths (`import billing.invoice`) and `spore.toml` for project configuration. Enforces visibility rules (pub/pub(pkg)/private) at module boundaries, supports multi-file compilation with explicit import resolution, and achieves supply-chain security through explicit function-level effects plus Platform metadata. Broader project/file effect ceilings remain follow-up work. +> **Executive Summary**: Defines file-based modules, package manifests, Platform packages, visibility, and content-addressed provenance under the signature model. Packages bind signatures, intents, properties, realizations, evidence, and dependencies through named hashes rather than release ranges. ## Summary -This SEP specifies the complete module and package system for the Spore programming language. It defines how code is organized (one file = one module, no explicit `module` declarations), how modules are imported via dot-separated paths (`import billing.invoice`), how visibility is controlled (private / `pub(pkg)` / `pub`), how functions are content-addressed via a dual-hash scheme (signature hash + implementation AST hash using BLAKE3), how packages are structured around `spore.toml` manifests with a `.spore-lock` for reproducible builds, how dependencies are resolved without semantic versioning, and how IO is abstracted through selected Platform packages and startup contracts. Additional project-wide or file-level effect ceilings remain reserved for later SEP work. +A Spore module is one source file. Its module path is derived from its file path. +Packages are described by `spore.toml`, lock data, and content-addressed inputs. -The design synthesizes ideas from Unison (content-addressing), Roc (platform/package separation), Elixir/Python (dot-separated import paths), Elm/Go (file-based modules, no circular dependencies), and Rust (three-level visibility), while introducing novel concepts such as the import/alias separation, reserved effect-ceiling design space, and the dual-hash content-addressing scheme that decouples API stability from implementation pinning. +The signature model changes package identity from callable-only API identity +to provenance over: -## Motivation - -### Why content-addressed functions? - -Traditional package ecosystems rely on semantic versioning (semver) to communicate compatibility. In practice, semver is a social contract—not a machine-verifiable one. A "patch" release can silently break consumers; a "major" bump may change nothing relevant to a particular dependent. This ambiguity leads to: - -- **Phantom breakage**: Dependents recompile or fail when nothing they actually use has changed. -- **Dependency hell**: SAT solvers struggle with conflicting version ranges. -- **Non-reproducible builds**: The same version string may resolve to different code over time. - -Spore eliminates these problems by giving every function two content hashes computed via BLAKE3: - -1. **Signature hash (`sig`)**: Covers the function's public contract—parameter - names, parameter types, return type, error types, effects, cost bound, - effects, and generic constraints. This hash changes **only** when the API - changes. For error clauses, the policy is intentionally conservative: - semantically equivalent canonical error sets may still hash differently when - their written surface spelling changes. -2. **Implementation AST hash (`impl`)**: Covers the compiled AST of the function body. This hash changes whenever the implementation changes. Partial functions (those containing holes) have `impl = None`. - -This dual-hash scheme provides two independent guarantees: - -- **API stability** (`sig`): If `sig` is unchanged, dependents need not recompile. Hole filling and body refactoring are invisible to consumers. -- **Exact reproducibility** (`impl`): The `.spore-lock` file pins the precise implementation via `impl`, enabling bit-for-bit reproducible builds. - -### Why no semantic versioning? - -Content hashes make semver redundant. Instead of trusting a human to correctly label a release as "major", "minor", or "patch", the compiler can verify compatibility mechanically: - -- `sig` unchanged → API compatible. No downstream action required. -- `sig` changed → Breaking change. The `spore --permit` command is required to explicitly accept the change. -- `impl` changed, `sig` unchanged → Internal refactor. `.spore-lock` updates automatically. - -A human-readable release label may remain available in `spore.toml` for documentation purposes, but it plays no role in dependency resolution. Named aliases (e.g., `"std-http-stable"`) provide stable human-readable identifiers that map to specific hashes. - -### Why platforms for IO? - -In traditional languages, IO operations are built into the standard library, making them impossible to substitute for testing, impossible to sandbox for security, and impossible to port across runtimes without rewriting application code. Spore's Platform system, inspired by Roc, makes IO a pluggable concern: - -- **Application code declares effects explicitly**: Functions write `uses [...]` clauses and call platform-owned APIs; they do not issue raw host operations directly. -- **Platform packages define the runtime boundary**: A Platform package publishes foreign-function modules, a startup contract, and a host adapter. -- **Alternative Platforms can preserve the same application surface**: CLI, test, web, or embedded runtimes can supply different implementations for the same declared effects. -- **Supply-chain security comes from explicit effects plus Platform metadata**: A package cannot manufacture filesystem, process, or network access unless the selected Platform exposes and handles those operations. - -## Guide-level explanation - -### Module declaration - -Every `.sp` file is exactly one module. The module name is derived from the file's path relative to the package's `src/` root: - -The canonical source extension in docs and manifests is `.sp`. Compatibility-only -`.spore` handling, where it exists, is secondary to this spelling. - -| File Path | Module Name | -| ------------------------ | ----------------- | -| `src/billing/invoice.sp` | `billing.invoice` | -| `src/auth/token.sp` | `auth.token` | -| `src/utils.sp` | `utils` | -| `src/main.sp` | `main` | - -There is no `module` keyword — the filesystem **is** the declaration. Spore requires effect requirements attached to individual function signatures; no additional file-level `#![uses(...)]` ceiling is standardized. - -```spore -// src/billing/invoice.sp -import billing.types -import billing.tax - -pub fn generate_invoice(order: Order) -> Invoice ! TaxError | ValidationError - cost [3000, order.items.len * 20 + 500, 0, 2] - uses [PaymentGateway, AuditLog] -{ - let tax = tax.calculate(order.items, order.region) - let line_items = build_line_items(order.items, tax) - finalize(order.customer, line_items) -} - -fn build_line_items(items: List[Item], tax: TaxResult) -> List[LineItem] - cost [800, 500, 0, 1] -{ - items |> map(fn(item) -> to_line_item(item, tax)) -} -``` - -Module names use **dot-separated** paths derived from the filesystem. There is no explicit `module` declaration; the file path is the single source of truth. - -### Imports - -Spore uses **dot-separated** paths for module imports and for item selection: - -```spore -// Module import: brings the module into scope; -// registers its pub items as unqualified local names -import billing.invoice - -// Module import with rename -import billing.invoice as inv - -// Item alias: binds a specific function or type to a local name -alias gen = billing.invoice.generate_invoice -alias Inv = billing.types.Invoice -``` - -> **Note:** Selective imports (`import billing.invoice.{calculate, Invoice}`) are not part of the accepted surface. Use `import billing.invoice` and call the imported item by its bare local name, or use `alias` for specific items. - -Key rules: - -- `import` operates on **modules** only. Dot (`.`) separates path segments. -- `alias` operates on **items** only. You cannot alias a module (use `import ... as` instead). -- No wildcard imports (`import billing.*` is not supported). -- No implicit nested imports (`import billing` does not import `billing.invoice`). -- Imported functions preserve the metadata needed for downstream checking: - parameter/return types, `uses [...]`, declared `!errors`, generic parameters, - and `where` bounds. -- Imported `pub effect` / effect interfaces preserve operation signatures - across module boundaries, so `perform Effect.op(...)` keeps operation typing - after import. -- Ambiguous imported same-name functions or effect interfaces with conflicting - signatures are rejected rather than silently overwritten. - -### Visibility - -| Level | Keyword | Meaning | -| -------------------- | ----------- | ---------------------------------------------------- | -| **Private** | _(default)_ | Visible only within the defining module | -| **Package-internal** | `pub(pkg)` | Visible to any module within the same package | -| **Public** | `pub` | Visible to any importer, including external packages | - -```spore -// Public: any importer can call this -pub fn generate_invoice(order: Order) -> Invoice ! TaxError { ... } - -// Package-internal: accessible within this package only -pub(pkg) fn validate(order: Order) -> ValidatedOrder ! ValidationError { ... } - -// Private (default): only this module can call this -fn compute_totals(order: ValidatedOrder) -> Invoice { ... } -``` - -Visibility applies uniformly to functions, types, effect interfaces, and aliases. - -### Packages - -A **package** is a directory containing a `spore.toml` manifest: - -```text -my-billing-lib/ -├── spore.toml // package manifest -├── src/ -│ ├── billing/ -│ │ ├── invoice.sp -- module: billing.invoice -│ │ ├── tax.sp -- module: billing.tax -│ │ └── types.sp -- module: billing.types -│ └── utils.sp -- module: utils -└── test/ - └── billing/ - └── invoice_test.sp -``` - -Three package types exist: - -| Type | Has Platform? | Can do IO? | Use Case | -| ------------- | --------------- | --------------------------------- | ------------------------------ | -| `package` | No | No (declares effect requirements) | Libraries, reusable components | -| `application` | Yes | Yes (via Platform) | Executables, services | -| `platform` | Is the Platform | Yes (raw syscalls) | Runtime providers | - -### Script Mode - -Script mode is intentionally **out of scope** for SEP-0008. Manifest-free -single-file execution, file-header metadata, and script-vs-project precedence -rules are specified by a separate script-mode SEP. SEP-0008 covers -manifest-backed packages and applications only. - -### Platforms - -A Platform is the **only** package in a Spore application that bridges declared effects to the host runtime. It contributes: - -1. package modules that expose Platform-owned `foreign fn` surfaces, -2. a contract module that defines the startup signature, and -3. an adapter function that bridges application startup into the host runtime. - -```spore -import basic_cli.stdout -import basic_cli.cmd - -fn main() -> () uses [Console, Exit] { - println("about to exit") - exit(7u8) -} -``` - -For `basic-cli`, project mode currently requires `main() -> ()`. Explicit process termination is modeled as a Platform API (`basic_cli.cmd.exit`) requiring `uses [Exit]`; the runtime carries that as a structured project outcome and the CLI converts that outcome to a host exit status at the process boundary. - -Project mode validates the selected entry's -startup effect requirements against the selected Platform package's -`[platform].handled-effects` manifest field. A manifest-backed application may only -start if its entry function's required effects are listed in that Platform -contract metadata. - -Standalone single-file execution is outside SEP-0008. Its legacy `main() -> I32` behavior is documented separately in `drafts/script-mode.md`. - -## Reference-level explanation - -### Module resolution - -#### File-to-module mapping - -The compiler maps `.sp` files to modules using the path relative to `src/`: - -```text -module_name = path.relative_to("src/") - .strip_extension(".sp") - // path separators become '.' in the module name -``` - -Module name segments must be lowercase `snake_case`. Reserved names (`platform`, -`test`) may carry tooling conventions, but executable selection is -manifest-level: a module named `main` is only the default entry module by -convention, not by special module semantics. - -#### Empty modules - -An empty `.sp` file is a valid module. It compiles successfully, exports nothing, and has no effects: - -```spore -// src/billing/future.sp -// This module is a placeholder for future billing features. -``` - -```text -$ spore check src/billing/future.sp - -[ok] billing.future - exports: (none) - effects: [] -``` - -Use cases include namespace placeholders, future feature stubs, and organizational markers within a package directory. - -#### Dependency graph construction - -The compiler builds a directed acyclic graph (DAG) of module dependencies: - -1. Parse all `import` declarations across all modules. -2. Verify no circular dependencies exist (standard topological sort; cycles are compile errors). -3. Determine topological build order. Modules at the same level compile in parallel. - -```text -Illustrative topological build order: - 1. billing.types (0 dependencies) - 2. billing.tax (1 dependency: billing.types) - 3. billing.invoice (2 dependencies: billing.types, billing.tax) - 4. billing.shortcuts (3 dependencies: billing.types, billing.invoice, billing.tax) - 5. api.handler (2 dependencies: billing.invoice, billing.shortcuts) - 6. main (1 dependency: api.handler) -``` - -Diamond dependencies are permitted (they are not cycles). The `.spore-lock` ensures a single canonical implementation via matching `sig` and `impl` hashes. - -#### Forward references - -Within a single module, functions may reference each other regardless of declaration order—the compiler processes all declarations before resolving references. Cross-module forward references are forbidden (they would imply circular dependencies). - -#### Import grammar (formal) - -```text -import_decl ::= 'import' module_path ('as' IDENT)? - // NOTE: selective imports ('import' module_path '.' '{' IDENT (',' IDENT)* '}') - // are not part of the accepted surface -alias_decl ::= visibility? 'alias' IDENT '=' qualified_item -module_path ::= IDENT ('.' IDENT)* -qualified_item ::= module_path '.' IDENT -``` - -#### Import ordering convention - -The compiler does not enforce import order, but `spore fmt` auto-sorts imports into four groups separated by blank lines: - -```spore -// 1. Platform / standard library imports -import std.collections -import std.text - -// 2. External package imports -import http.client as http -import json.parser as json - -// 3. Internal package imports -import billing.invoice -import billing.types - -// 4. Aliases -alias Invoice = billing.types.Invoice -alias gen = billing.invoice.generate_invoice -``` - -Within each group, imports are sorted alphabetically by module path. - -### Content-addressed functions: dual hashing - -#### Hash computation - -Every function carries two BLAKE3 hashes: - -**Signature hash (`sig`)**: - -```text -sig = BLAKE3(canonicalize( - function_name, - parameter_names, - parameter_types, - return_type, - error_types, - effect_annotations, - cost_bound, - required_effects, - generic_constraints -)) -``` - -The canonicalization process removes whitespace, sorts lexicographically where order is not semantically meaningful, and expands type aliases to their canonical forms. - -**Implementation AST hash (`impl`)**: - -```text -impl = BLAKE3(compiled_AST(function_body)) -``` - -For partial functions (those containing holes), `impl = None`. When a hole is filled, `impl` transitions from `None` to a concrete hash while `sig` remains unchanged. - -#### What changes each hash - -| Change | `sig` changes? | `impl` changes? | -| --------------------------------------------- | -------------- | ----------------- | -| Parameter name: `order` → `purchase_order` | **Yes** | **Yes** | -| Parameter type: `Order` → `OrderRequest` | **Yes** | **Yes** | -| Return type: `Invoice` → `InvoiceResult` | **Yes** | **Yes** | -| Error type set: Add `[ValidationError]` | **Yes** | **Yes** | -| Effects: `pure` → `deterministic` | **Yes** | **Yes** | -| Cost bound: `≤ 3000` → `≤ 5000` | **Yes** | **Yes** | -| Effects: Add `AuditLog` | **Yes** | **Yes** | -| Generic constraints: `T: Eq` → `T: Eq + Hash` | **Yes** | **Yes** | -| Function body: Refactor internals | **No** | **Yes** | -| Hole filling: Replace `?logic` with code | **No** | `None` → concrete | -| Comments: Add/remove/edit | **No** | **No** | -| Formatting: Reformat code | **No** | **No** | - -Key insight: signature changes always change both hashes. Body-only changes update `impl` while leaving `sig` untouched—this prevents unnecessary downstream recompilation. - -#### Hash algorithm: BLAKE3 - -BLAKE3 is the mandatory hash algorithm for all content addressing in Spore: - -- **Speed**: 10x+ faster than SHA-256, with native parallelism support. -- **Security**: 128-bit security level (256-bit output). -- **Streaming**: Supports incremental hashing for large modules. -- **Output**: 256-bit hashes displayed as 64 hex characters; truncated forms (e.g., 16 hex chars) used in human-facing output. - -#### `.spore-lock` and dependency tracking - -The `.spore-lock` file records both hashes for all functions a module depends on: - -```toml -# .spore-lock (auto-generated, do not edit) - -[deps."api.handler".generate_invoice] -sig = "a3f7c2" # interface contract -impl = "d8e1b4" # exact implementation (None if partial) - -[deps."billing.invoice".calculate] -sig = "d91e4b" -impl = "b2c4a7" -``` - -Compatibility checking uses only `sig`: if `sig` is unchanged, dependents need not recompile. The `impl` field pins exact code for reproducible builds. - -#### Change detection and `spore --permit` - -When a signature changes, all downstream modules referencing the old `sig` are flagged: - -```text -$ spore check src/main.sp - -[warning] signature changed: billing.tax.calculate - old sig: d91e4b - new sig: 7a2f33 - change: added error type [RegionNotSupported] - - affected modules: - billing.invoice (depends on billing.tax.calculate sig=d91e4b) - api.handler (depends on billing.tax.calculate sig=d91e4b) - - run `spore --permit billing.tax.calculate` to accept this change -``` - -The `spore --permit` command updates `sig` entries in `.spore-lock`. Implementation-only changes update `impl` automatically—no permit required. Batch acceptance is available via `spore --permit --all`. - -#### Snapshot hierarchy - -Hashes cascade through three levels, all derived from `sig` only (implementation changes do not propagate): - -| Level | What is hashed | When it changes | -| -------------------- | -------------------------------------------------- | ------------------------------------------------ | -| **Function `sig`** | Canonical signature | Any signature component changes | -| **Function `impl`** | Compiled AST | Body changes, hole filling, or signature changes | -| **Module interface** | Sorted hash of all `pub` + `pub(pkg)` `sig` hashes | Any exported function's signature changes | -| **Package API** | Sorted hash of module interface hashes | Any module's exported interface changes | - -### Visibility rules - -#### Three visibility levels: `pub` / `pub(pkg)` / private - -- **Private** (default): Visible only within the defining module. -- **`pub(pkg)`**: Visible to any module within the same package. External packages cannot access `pub(pkg)` items. -- **`pub`**: Visible to any importer, including external packages. - -Visibility applies to functions, types, and aliases: - -```spore -pub type Invoice { id: InvoiceId, total: Money } // public type -pub(pkg) type ValidatedOrder { original: Order } // package-internal type -type InternalCache { entries: Map } // private type - -pub alias GenInv = billing.invoice.generate_invoice // public alias -pub(pkg) alias Validate = billing.invoice.validate // package-internal alias -alias Compute = billing.invoice.compute_totals // private alias -``` - -#### Visibility enforcement - -Attempting to access a private item from another module: - -```text -[error] visibility violation at src/api/handler.sp:15 - billing.invoice.compute_totals is private - ─── it is only visible within module billing.invoice - - help: add `pub` or `pub(pkg)` to its definition in src/billing/invoice.sp -``` - -Attempting to access a `pub(pkg)` item from an external package: - -```text -[error] visibility violation at src/handler.sp:8 - billing.invoice.validate is pub(pkg) - ─── it is only visible within the 'my-billing-lib' package - ─── your module 'handler' is in the 'my-api' package -``` - -In a workspace, `pub(pkg)` restricts visibility to the **current package** (member) only — not to the entire workspace. For workspace member `core` exporting `pub(pkg) fn internal_helper()`, member `web` in the same workspace **cannot** access it. Use `pub` for cross-member visibility. - -#### Visibility and the Hole System - -Holes inherit the visibility context of their enclosing function. A HoleReport includes only candidates that are **visible** from the hole's location: - -```json -{ - "hole": { "name": "payment_logic", "location": "src/api/handler.sp:15" }, - "candidates": [ - { - "function": "billing.invoice.generate_invoice", - "visibility": "pub", - "accessible": true - }, - { - "function": "billing.invoice.validate", - "visibility": "pub(pkg)", - "accessible": true, - "note": "same package" - } - ], - "excluded": [ - { - "function": "billing.invoice.compute_totals", - "visibility": "private", - "reason": "private to billing.invoice" - } - ] -} -``` - -This ensures agents filling holes never suggest inaccessible functions. - -#### Re-exports via pub aliases - -The only re-export mechanism is `pub alias`: - -```spore -// src/billing/shortcuts.sp -import billing.invoice -import billing.types - -pub alias generate = billing.invoice.generate_invoice -pub alias Invoice = billing.types.Invoice -``` - -Alias chains are **forbidden**. A `pub alias` must point to an original definition, not to another alias. This prevents unbounded indirection and keeps the dependency graph simple for both human and agent analysis. - -#### Shadowing rules - -Module imports and aliases cannot shadow each other. An alias cannot conflict with an imported module name: - -```spore -import billing.invoice -alias invoice = billing.types.Invoice // ERROR: 'invoice' conflicts with module name -``` - -### Platform abstraction - -#### IO via Platform boundary - -Application code declares effect requirements and crosses the runtime -boundary through the selected Platform package. -manifest-backed project mode resolves those calls through Platform-owned modules -plus the Platform's startup contract: - -```spore -import basic_cli.file - -fn export_report(path: Str, content: Str) -> () ! IoError -uses [FileWrite] -{ - file_write(path, content) -} -``` - -The type system tracks all effects. Effects propagate upward: if you call an -operation requiring effect `C`, your function must also declare -`uses [C]`. - -#### Platform definition - -A Platform declares three things: - -1. **Effect set**: Which IO effects it handles. -2. **Startup contract**: The function contract that the selected module's - startup function must satisfy. -3. **Host adapter surface**: The package-owned adapter that bridges the - application startup into the Platform runtime. - -The source of truth is split between the Platform -package manifest and a dedicated contract module inside the package: - -```toml -# basic-cli/spore.toml -[package] -name = "basic-cli" -type = "platform" - -[platform] -contract-module = "platform_contract" -startup-contract = "main" -adapter-function = "main_for_host" -handled-effects = ["Console", "FileRead", "FileWrite", "Env", "Spawn", "Exit"] -``` - -```spore -// basic-cli/src/platform_contract.sp -pub fn main() -> () { - ?platform_startup_contract -} - -pub fn main_for_host(app_main: () -> ()) -> () { - app_main() - return -} -``` - -The hole-backed startup function in the Platform contract module is the -authoritative startup signature. Applications targeting that Platform must -implement the same startup function name plus the same parameter/return shape in -their entry module. Effect requirements are checked separately: -the selected startup entry's `uses [...]` set must fit within `[platform].handled-effects` -rather than being encoded into the adapter's callable Rust-facing shape. Any -`spec` items attached to the Platform contract and the application -implementation both apply. - -For `basic-cli` today, the startup contract remains `main() -> ()`. Applications -do **not** return process exit codes from `main`; they call the Platform surface -explicitly: - -```spore -import basic_cli.cmd - -fn main() -> () uses [Exit] { - exit(7u8) -} -``` - -The runtime propagates this as a structured project outcome. The current CLI -boundary accepts exit codes in `0..=255`; unsupported values are reported as -errors rather than silently truncated. - -Parser-level `platform { ... }` blocks remain future sugar over the same -package-owned contract; this SEP does not require that syntax to land first. - -Application-side Platform selection in `spore.toml`: - -```toml -[dependencies] -basic-cli = { path = "../basic-cli" } - -[project] -platform = "basic-cli" -default-entry = "app" - -[entries.app] -path = "src/main.sp" -``` - -The compiler verifies: - -- The selected startup entry's required effects are listed in the selected Platform package's `[platform]` metadata. -- The startup function in the selected entry module matches the Platform contract module's startup contract. -- The selected Platform package exports the named adapter from its contract module. -- Test and mock Platforms may be substituted during testing, but a project binds to one Platform at a time. - -#### One Platform per project - -A manifest-backed project selects exactly one Platform. Different deployment -targets use different manifests, overrides, or future higher-level workspace -flows; they are not modeled as multiple concurrent Platform bindings within one -project. Multiple executable targets are modeled as project entries, not as -multiple Platforms. - -#### Test Platforms for deterministic testing - -Since application code is decoupled from IO implementations, a future Test -Platform can substitute mock handlers. The syntax below is illustrative future -sugar rather than the package-backed surface: - -```spore -platform TestPlatform { - handles [FileRead, FileWrite, NetConnect, Clock] - startup: fn() -> TestResult -} - -handler MockFileSystem { - var fs: Map[Path, Bytes] = Map.new() - - File.read(path: Path) -> Result[Bytes, IoError] { - match fs.get(path) { - Some(content) -> resume(Ok(content)) - None -> resume(Err(IoError.NotFound)) - } - } -} - -handler MockClock { - var current_time: U64 = 0 - Clock.now() -> Timestamp { resume(Timestamp(current_time)) } -} -``` - -Running `spore test tests/fetch_weather_test.sp` automatically uses the Test Platform: - -```bash -$ spore test tests/fetch_weather_test.sp - Using test platform: spore-platform/test - Running 3 tests - -test fetch_weather_returns_valid_data ... ok (0.001s) -test fetch_weather_handles_network_error ... ok (0.001s) -test fetch_weather_handles_malformed_json ... ok (0.001s) - -Test result: ok. 3 passed; 0 failed -``` - -#### Security model - -1. **Pure packages cannot perform IO**. They may declare effect requirements, but they do not carry Platform implementations. -2. **Only Platform packages bridge to raw host operations**. No user package can invoke host operations without going through the selected Platform. -3. **Effects are explicit**. An auditor can inspect any function's `uses` clause to know exactly what it can do. -4. **Supply-chain safety**: A downloaded package cannot gain hidden filesystem, process, or network access through an implicit stdlib shim; those surfaces must be provided by the selected Platform package. - -```text -$ spore audit billing-lib - -Package: billing-lib - required effects: [PaymentGateway, AuditLog] - - Module effect breakdown: - billing.invoice: [PaymentGateway, AuditLog] - billing.tax: [] (pure) - billing.types: [] (pure) - - ✓ No RawSyscall usage - ✓ No undeclared effects - ✓ Startup entry requirements fit the selected Platform metadata -``` - -### Module effects - -#### Effect checking today - -The normative model standardizes effect checking at two -places: - -1. function signatures (`uses [...]`) -2. selected Platform boundaries and Platform metadata - -Additional project-wide or file-level ceilings such as `[effects].declared` -or `#![uses(...)]` remain reserved for follow-up design work. They are not part -of the this SEP contract, and tooling should not treat them as -normative. - -#### Effect propagation - -Importing a module does **not** grant its effects. If you call a function that requires effect `C`, your function must also declare `uses [C]`. Effects propagate upward through the call graph. - -### Package management - -#### Package manifest (`spore.toml`) - -```toml -[package] -name = "billing-lib" -release = "stable" # human-readable, not used for resolution -type = "package" # "package" | "application" | "platform" -description = "Invoice generation and tax calculation" -license = "MIT" -authors = ["Alice "] - -[dependencies] -json-parser = { - git = "https://github.com/spore-std/json", - sig = "b4e7d3c2a1f9e8d7", - impl = "2f3e4d5c6a7b8e9f" -} - -[dev-dependencies] -test-framework = { - git = "https://github.com/spore-std/test", - alias = "spore-test-stable", - sig = "a1b2c3d4e5f6a7b8", - impl = "1a2b3c4d5e6f7a8b" -} -``` - -Dependencies are declared **per-project** in `spore.toml`, not per-file. -Additional project-wide effect ceilings remain future design work rather than -part of the normative surface. - -Package names are `kebab-case` and globally unique within a registry. Module paths within a package use `snake_case` segments. - -### Application Entries - -An application declares one or more named **entries** in `spore.toml`. Each -entry selects an **entry module**, and the selected Platform then validates the -module's **startup function** against its **startup contract**. The -bridge, the selected Platform package manifest names the contract module and -startup symbol, and that contract module owns the hole-backed startup -definition whose `spec` items stack with the application's own implementation. - -The CLI supports explicit -`spore run src/...` file execution: when the file lives under a discovered -project's `src/` tree, the tool derives a project-backed entry path from that -file location. Named manifest entries remain the canonical durable project -surface even while that compatibility path exists. - -The default emitted executable or run target name is the selected entry name: - -```toml -[package] -name = "my-tool" -type = "application" -default-entry = "my-tool" - -[entries.my-tool] -path = "src/main.sp" -``` - -Additional entries use the same table form: - -```toml -[entries.my-tool] -path = "src/main.sp" - -[entries.my-tool-migrate] -path = "src/migrate.sp" -``` - -Each entry path must point to a `.sp` file containing a startup function that -satisfies the selected Platform's startup contract. The -selected Platform package manifest names the contract module and startup symbol, -while that contract module owns the hole-backed startup definition and adapter. -`basic-cli` uses `main` plus `main_for_host`. - -#### Content-addressed dependencies (BLAKE3, no semver) - -Dependencies are identified by content hashes, not version ranges: - -```toml -[dependencies] -http = { - git = "https://github.com/spore-std/http", - sig = "a3f9c2e1d8b7a6f5", - impl = "7b8d4f2a1e9c3d5b" -} -``` - -The `sig` hash identifies the API contract. The `impl` hash pins the exact implementation. There is no version range negotiation—no SAT solver is needed. Resolution is a simple hash lookup: - -```text -1. Read [dependencies] from spore.toml -2. For each dependency: - a. Check .spore-lock for cached hash - b. Fetch from source if not cached - c. Compute BLAKE3 hashes and verify against declared values -3. Recursively resolve transitive dependencies -4. Build acyclic dependency graph -5. Write .spore-lock -``` - -Interface-only dependencies (omitting `impl`) allow type-checking without fetching full source, reducing download sizes and compile times. - -#### Decentralized storage - -Spore has **no central registry**. Packages are fetched from: - -- **Git repositories** (primary): `git = "https://github.com/user/repo"` -- **Local paths** (development): `path = "../mylib"` -- **IPFS**: `ipfs = "Qm..."` -- **HTTP registries** (community-operated, index-only): `registry = "https://pkgs.example.com"` - -Content hashes ensure integrity regardless of source. Community registries provide searchability but store only metadata—not code. - -#### Effect-based trust model - -Packages declare dependencies and Platform selection in `spore.toml`. Current -effect control does **not** add a separate -project-wide `[effects].declared` ceiling. Instead: - -- libraries declare required effects on function signatures -- the selected startup entry's required effects must be satisfied by the chosen - Platform package's `[platform].handled-effects` metadata -- tooling such as `spore audit` reports the resulting dependency/effect graph - -More granular dependency grants remain future design space rather than part of -the this SEP contract. - -#### Workspace support - -Multi-package projects use a workspace `spore.toml`: - -```toml -[workspace] -members = [ - "packages/billing-lib", - "packages/billing-api", - "apps/invoice-service", -] -``` - -Alternatively, `path` dependencies provide lightweight monorepo support without requiring a workspace manifest. - -A workspace uses a **single `.spore-lock`** at the workspace root. Individual members do NOT have their own lock files. This ensures: - -- All members share the same dependency versions (no diamond dependency conflicts). -- CI builds are reproducible from a single lock file. -- `spore update` in any member updates the shared lock. - -### Dependency type classification - -Spore recognizes four dependency types: - -| Type | `sig` required? | `impl` required? | When needed | -| ------------------------ | --------------- | ---------------- | ------------------------------------------------------ | -| **Interface** (sig-only) | Yes | No | Type-checking without fetching full source | -| **Complete** (sig+impl) | Yes | Yes | Full build, linking, reproducible deployment | -| **Dev** | Yes | Yes | Testing, benchmarking; excluded from production builds | -| **Optional** | Yes | Yes (if enabled) | Controlled by feature flags | - -```toml -[dependencies] -json = { sig = "b4e7d3c2a1f9e8d7" } # interface-only -http = { sig = "a3f9c2e1d8b7a6f5", impl = "7b8d4f2a1e9c3d5b" } # complete - -[dev-dependencies] -test-framework = { sig = "f3e4d5c6b7a8f9e0", impl = "1a2b3c4d5e6f7a8b" } - -[dependencies] -metrics = { sig = "e2f3a4b5c6d7e8f9", impl = "9f0a1b2c3d4e5f6a", optional = true } -``` - -### Dependency declaration format - -**Git source** (with branch/tag/rev specifiers): - -```toml -[dependencies] -mylib = { - git = "https://github.com/user/repo", - branch = "main", # optional: track a branch - tag = "stable-release", # optional: pin a tag - rev = "abc123def456", # optional: pin a commit - sig = "a1b2c3d4e5f6a7b8", - impl = "e5f6a7b8c9d0e1f2" -} -``` - -**Local path** (development): - -```toml -[dependencies] -mylib = { path = "../mylib", sig = "a1b2c3d4e5f6a7b8", impl = "e5f6a7b8c9d0e1f2" } -``` - -**Named alias** (stable human-readable identifier): - -```toml -[dependencies] -mylib = { alias = "mylib-stable-2024", sig = "a1b2c3d4e5f6a7b8", impl = "e5f6a7b8c9d0e1f2" } -``` - -### Features system - -Feature flags enable conditional compilation and optional dependencies: - -```toml -[features] -default = ["logging"] -full = ["tls", "metrics", "compression"] -logging = ["dep:logger"] -tls = ["dep:tls-lib"] -metrics = ["dep:metrics"] -compression = [] # code-level feature, no extra dep -``` - -Features interact with the hash system as follows: enabling a feature may include additional code paths, which changes the `impl` hash but not the `sig` hash (unless the feature alters the public API). The precise interaction between features and content hashes is a design direction still being refined (see Unresolved Questions). - -### `.spore-lock` complete format - -The `.spore-lock` file records the fully resolved dependency graph: - -```toml -# Auto-generated by `spore lock`. Do not edit. -schema = "current" - -[metadata] -generated-at = "2024-01-15T10:30:00Z" - -[[package]] -name = "web-server" -source = "local" -sig = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2" -impl = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" - -[[package]] -name = "http" -alias = "std-http-stable" -source = { type = "git", url = "https://github.com/std/http", rev = "abc1234" } -sig = "a3f9c2e1d8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1" -impl = "7b8d4f2a1e9c3d5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9" -location = ".spore-store/git/github.com/std/http/abc1234" - -[[package]] -name = "json" -alias = "fast-json" -source = { type = "git", url = "https://github.com/std/json", rev = "def5678" } -sig = "b4e7d3c2a1f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3" -impl = null # interface-only dependency - -[dependency-graph] -"web-server" = ["http", "json"] -"http" = ["io"] -"json" = [] - -[verification] -"http" = { - sig-algo = "blake3", - sig-hash = "a3f9c2e1d8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1", - impl-algo = "blake3", - impl-hash = "7b8d4f2a1e9c3d5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9", - verified-at = "2024-01-15T10:30:05Z" -} - -[effects] -"http" = ["NetConnect", "NetListen"] -"io" = ["FileRead", "FileWrite"] - -[[platform-locks]] -platform = "linux-x86_64" -packages = ["http", "json", "io"] - -[[platform-locks]] -platform = "wasm32" -packages = ["json"] - -[build-metadata] -compiler-id = "spore" -build-timestamp = "2024-01-15T10:30:10Z" -``` - -### Storage backend system - -All fetched dependencies are cached in `.spore-store/`: - -```text -.spore-store/ -├── sigs/ # Signature cache -│ ├── a3f9c2e1.../ -│ │ └── interface.spore-sig -│ └── b4e7d3c2.../ -│ └── interface.spore-sig -├── impls/ # Implementation cache -│ ├── 7b8d4f2a.../ -│ │ └── module.sp -│ └── 2f3e4d5c.../ -│ └── module.sp -├── git/ # Git source cache -│ └── github.com/ -│ └── user/ -│ └── repo/ -│ └── abc1234/ -│ └── src/ -├── ipfs/ # IPFS cache -│ └── Qm.../ -└── metadata/ - └── index.db # SQLite index -``` - -**StorageBackend trait**: The storage system is pluggable via a trait: - -```spore -pub trait StorageBackend { - fn fetch_sig(hash: Hash) -> Result[Signature, FetchError] - fn fetch_impl(hash: Hash) -> Result[Module, FetchError] - fn store_sig(sig: Signature) -> Hash - fn store_impl(module: Module) -> Hash - fn list_cached() -> List[Hash] - fn gc() -> Result[Unit, GcError] -} -``` - -**Four backend implementations**: - -1. **Git Backend**: Clones bare repos, checks out specific revisions, computes and verifies hashes, symlinks to `impls/`. -2. **Local Path Backend**: Reads source directly from the local filesystem. Does **not** copy to `.spore-store`—references the original path. Hash is recomputed and verified on each build. -3. **IPFS Backend**: Fetches content by CID, verifies hash, stores locally. -4. **HTTP Registry Backend**: Queries `GET /modules//sig/` and `GET /modules//impl/` endpoints. Registries are index-only—they store metadata, not code. The actual source is fetched from the URL in the metadata. - -**Cache strategy**: - -- **Content-addressed**: Identical hashes share storage. Two packages using the same dependency content store it once. -- **Global shared**: By default `~/.spore-store` is shared across all projects. Configurable via `~/.sp/config.toml`. -- **Offline-capable**: Once cached, dependencies are available without network access. - -**Garbage collection**: - -```bash -spore gc # remove unreferenced cached entries -spore gc --all # remove all cached entries -``` - -GC algorithm: (1) scan all `.spore-lock` files in known project directories, (2) mark all referenced hashes, (3) delete unmarked entries from `.spore-store/`. - -### Fine-grained effects (design direction) - -The accepted surface uses **coarse-grained** effect names in the language-level type system: - -```spore -fn read_config() -> Config ! IoError - uses [FileRead] -{ ... } -``` - -A future direction introduces **path-style fine-grained effects** in `spore.toml`: - -```toml -[effects] -require = [ - "network:listen:8080", - "filesystem:read:/data", - "filesystem:write:/logs", - "env:read:API_KEY", -] -``` - -The fine-grained effect tree: +- `signature_hash` +- `intent_hash` +- `property_hash` +- `realization_hash` +- `evidence_hash` +- dependency hashes -```text -network: filesystem: env: process: - listen: read: read: spawn: - connect: write: write: signal: - http:client delete: - time: random: - realtime secure - monotonic fast -``` - -For the accepted surface, the language-level `uses [FileRead]` maps to coarse categories. Fine-grained path restrictions are enforced in `spore.toml` and at runtime via platform policy / host configuration, not in the type system. Promoting fine-grained effects into the type system is reserved for a future SEP. - -### Publish and discovery flow - -Spore has **no central registry**. Publishing a package means pushing to a Git repository and recording the hashes: - -```bash -# 1. Ensure tests pass -spore test && spore audit - -# 2. Commit and tag -git add . && git commit -m "Release stable" -git tag stable-release - -# 3. Push -git push origin main --tags - -# 4. Record hashes for consumers -spore hash -# Output: -# sig: a3f9c2e1d8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1 -# impl: 7b8d4f2a1e9c3d5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9 -``` - -**Discovery mechanisms**: - -1. **Named aliases in README**: Publishers include `spore.toml` snippets with Git URL, alias, and hashes. -2. **Community registry aggregation**: Optional registry servers index Git repositories. They store only metadata (name, description, hashes, source URL)—not code. Queried via `spore search "http server"`. -3. **Awesome lists**: Community-curated lists of packages with hashes and descriptions. - -### Subsystem interactions - -#### Interaction with the Hole System - -Holes respect module boundaries: - -- A hole in module A can only be filled with code that respects A's explicit - effect requirements and the selected Platform boundary. -- HoleReport candidates are filtered by visibility. -- Partial modules (containing holes) are valid compilation units. They export `pub` items normally, but those items have `impl = None`. -- A module calling a partial function becomes **transitively partial**. - -```text -$ sporec holes - -Holes by module: - - billing.invoice (partial) - ?invoice_logic at line 12 - type: Invoice ! TaxError - effects: [PaymentGateway, AuditLog] - budget remaining: 2400 ops - - api.handler (partial — depends on partial billing.invoice) - (no direct holes, but transitively partial) -``` - -#### Interaction with the Cost Model - -Cost is a per-function property (`cost [c, a, i, p]`). There are no module-level cost budgets. When a function calls an imported function, the callee's **declared** four-slot bound is used for estimation: - -```text -$ spore cost-report billing.invoice - -Module: billing.invoice - - Function costs: - generate_invoice cost [3000, …, …, …] (scalar summary measured: ~2800 op-eq.) - void_invoice cost [500, …, …, …] (scalar summary measured: ~320 op-eq.) - - Module aggregate: - max single-call cost (scalar summary): 3000 op-eq. (generate_invoice) - total declared budget (scalar summary): 3500 op-eq. -``` - -#### Interaction with the Snapshot System +## Motivation -Snapshots use `sig` hashes only at the module and package level (impl changes do not propagate): +Package consumers need to know what callable boundary they depend on, which +intent constraints were attached, which realization was used, and what evidence +was generated. Human labels are useful communication, but hashes are the +mechanical identity. -| Level | What is hashed | When it changes | -| ---------------- | ---------------------------------------------- | ------------------------------- | -| Function `sig` | Canonical signature | Any signature component changes | -| Function `impl` | Compiled AST | Body changes, hole filling | -| Module interface | Sorted hash of `pub` + `pub(pkg)` `sig` hashes | Any exported signature changes | -| Package API | Sorted hash of module interface hashes | Any module interface changes | +## Guide-level explanation -#### Interaction with the Error System +### Modules -Error types follow the same visibility rules as all other types. A function's error list (`! TaxError`) must reference only error types visible from the function's module: +`src/billing/invoice.sp` maps to module `billing.invoice`. There is no source +module header. ```spore -// src/api/handler.sp import billing.invoice -import billing.errors - -pub fn handle_create(req: Request) -> Response ! errors.BillingError | ParseError - uses [PaymentGateway, AuditLog] -{ - let order = parse_request(req) - let invoice = invoice.generate_invoice(order) - Response.ok(invoice) -} +import platform.console as console ``` -### Edge cases - -#### Modules with only types - -A module containing only type definitions is valid and pure: +### Visibility ```spore -// src/billing/types.sp -pub type Invoice { id: InvoiceId, total: Money, status: InvoiceStatus } -pub type InvoiceStatus { Draft, Sent, Paid, Void } -pub type LineItem { description: Str, quantity: I64, unit_price: Money } -``` - -```text -$ spore check src/billing/types.sp - -[ok] billing.types - exports: Invoice, InvoiceStatus, LineItem - effects: [] - cost: 0 (types only) -``` - -#### Diamond dependencies in `.spore-lock` - -Diamond dependencies use consistent hashes: - -```text - billing.types - ↑ ↑ - │ │ - billing.invoice billing.tax - ↑ ↑ - │ │ - api.handler +pub fn public_api() -> () { () } +pub(pkg) fn package_helper() -> () { () } +fn private_helper() -> () { () } ``` -The `.spore-lock` records the same `sig` and `impl` for `billing.types.Invoice` regardless of which path reached it: +### Platform packages -```toml -[deps."billing.invoice".Invoice] -sig = "8c3f01" -impl = "e9f3d2" - -[deps."billing.tax".Invoice] -sig = "8c3f01" -impl = "e9f3d2" -``` - -#### Modules with holes - -Partial modules can be imported and their types used. Only function calls at runtime are blocked: +A Platform package provides effect handlers and validates the startup contract +for an application. ```spore -// src/billing/invoice.sp (has holes) -pub fn generate_invoice(order: Order) -> Invoice ! TaxError { - ?invoice_logic -} - -// src/api/handler.sp -import billing.invoice - -// Compiles fine — handler is transitively partial -pub fn handle(req: Request) -> Response ! ApiError { - let inv = invoice.generate_invoice(parse_order(req)) - Response.ok(inv) +pub fn main() -> () +uses [Console, Exit] +{ + ?main_body } ``` -```text -$ spore check - -[partial] billing.invoice.generate_invoice - holes: ?invoice_logic - -[partial] api.handler.handle - depends on partial: billing.invoice.generate_invoice - -Build: success (2 partial functions) -``` - -#### Alias chains are forbidden +### Foreign linkage and exports -```text -[error] alias chain detected at src/api/shortcuts.sp:3 - api_gen → billing.shortcuts.gen → billing.invoice.generate_invoice - ─── aliases must point directly to original definitions - - help: use `pub alias api_gen = billing.invoice.generate_invoice` instead -``` - -### Platform details - -#### Comparison with other approaches - -| Approach | IO Model | Testability | Platform Independence | Example | -| ------------------ | -------------------------- | ------------- | --------------------- | ----------- | -| **Traditional** | Built-in IO | Needs mocking | Low | C, Go, Java | -| **Monadic IO** | IO Monad | Medium | Low | Haskell | -| **Effect System** | Algebraic Effects | High | Medium | Koka, Eff | -| **Spore Platform** | Effect Handlers + Platform | **Very high** | **Very high** | Roc, Spore | - -Spore and Roc share the same design philosophy: no built-in Platform; all -Platforms are third-party packages. - -The examples below use illustrative future `platform { ... }` sugar. The -package surface remains package-backed manifests plus contract modules. - -#### Web Platform example - -```spore -platform WebPlatform { - handles [NetListen, Clock, Spawn, DbQuery] - startup: fn(req: Request) -> Response ! NetListen | DbQuery - - handler HttpServerHandler { - Server.listen(port: U16, handler: fn(Request) -> Response) { - resume(ffi_listen_http(port, handler)) - } - } -} -``` - -#### Lambda Platform example +Platform-facing declarations use attributes rather than dedicated keywords: ```spore -platform LambdaPlatform { - handles [NetConnect, S3Read, S3Write, DynamoRead, DynamoWrite] - startup: fn(event: JsonValue) -> JsonValue ! S3Read | DynamoWrite - - handler S3Handler { - S3.get(bucket: Str, key: Str) -> Result[Bytes, S3Error] { - resume(aws_s3_get(bucket, key)) - } - } -} -``` +@foreign("ssl", name = "SSL_new") +fn ssl_new() -> Ptr[SSL] +uses [Native]; -#### Handler dispatch modes +@foreign +type Map[K, V]; -Effect handlers support three dispatch modes: - -1. **Direct FFI (inside a Platform package)**: Handler calls native code directly. - -```spore -handler DirectFfi { - File.read(path: Path) -> Result[Bytes, IoError] { - resume(ffi_read_file(path.to_bytes())) - } +@export("C") +pub fn score(raw: I64) -> F64 { + raw.to_f64() / 100.0 } ``` -2. **Effect composition**: Handler translates high-level effects into lower-level ones. +`@foreign` marks declarations whose implementation or representation is supplied +by the selected Platform or host environment. `@export("C")` marks a public +Spore function as an outbound ABI surface. Source-level linkage metadata is the +canonical declaration site; manifests may refine platform-specific build details +but should not replace source annotations. -```spore -handler HttpClientHandler uses [NetConnect] { - Http.get(url: Url) -> Result[Response, HttpError] { - let socket = TcpSocket.connect(url.host, url.port)? - socket.write(format_http_request(url))? - let raw = socket.read_until_eof()? - resume(parse_http_response(raw)) - } -} -``` +### Package provenance -3. **Stateful handler**: Handler maintains mutable state across calls. +Lock data records hashes for each public subject: -```spore -handler StatefulCache { - var cache: Map[Str, Bytes] = Map.new() - - Cache.get(key: Str) -> Option[Bytes] { - resume(cache.get(key)) - } - Cache.set(key: Str, value: Bytes) -> Unit { - cache.insert(key, value) - resume(()) - } -} +```toml +[[functions]] +name = "billing.invoice.total" +signature-hash = "..." +intent-hash = "..." +property-hash = "..." +realization-hash = "..." +evidence-hash = "..." ``` -**FFI integration**: Platform handlers call native code via `foreign fn` declarations. These declarations live in Platform-owned modules; application packages consume the Platform surface, not the native symbols directly: +## Reference-level explanation -```spore -foreign fn ffi_read_file(path: Bytes) -> FfiResult[Bytes] -foreign fn ffi_write_file(path: Bytes, content: Bytes) -> FfiResult[Unit] -``` +### Module resolution -Corresponding Rust implementation: +Imports use dot-separated paths. A module may import another module by path and +may introduce a local alias. Selective and wildcard imports are outside this SEP. -```rust -#[no_mangle] -pub extern "C" fn ffi_read_file(path: SporeBytes) -> FfiResult { - match std::fs::read(unsafe { path.to_str() }) { - Ok(bytes) => FfiResult::ok(SporeBytes::from_vec(bytes)), - Err(e) => FfiResult::err(e.raw_os_error().unwrap_or(-1)), - } -} -``` +### Visibility rules -#### Record/Replay testing mode +Private items are visible only inside their defining module. `pub(pkg)` items +are visible inside the package. `pub` items are visible to downstream packages. -For integration tests, a future **RecordingPlatform** sugar could capture IO -events during real execution: +### Signature package identity -```spore -platform RecordingPlatform { - handles [FileRead, NetConnect] - - handler RecordingHandler { - var log: List[IoEvent] = [] - - File.read(path: Path) -> Result[Bytes, IoError] { - let result = real_file_read(path) - log.push(IoEvent.FileRead(path, result)) - resume(result) - } - } -} -``` +A package API hash is derived from exported signature and intent hashes. A +package evidence hash is derived from evidence records selected by the package +policy. Realization hashes pin exact implementation artifacts for reproducible +builds. -A future **ReplayPlatform** could then replay the recorded events -deterministically: +### Dependency resolution -```spore -platform ReplayPlatform { - handles [FileRead, NetConnect] - - handler ReplayHandler { - var log: List[IoEvent] = load_log("test_recording.log") - var index: U32 = 0 - - File.read(path: Path) -> Result[Bytes, IoError] { - let event = log.get(index) - index = index + 1 - match event { - IoEvent.FileRead(p, result) if p == path => resume(result), - _ => panic("Unexpected IO: expected FileRead for {}", path), - } - } - } -} -``` +Dependencies resolve by declared package source plus expected hashes. If a +dependency's signature hash changes, importers must explicitly accept the new +callable boundary. If only a realization hash changes, importers may reuse the +same callable boundary but still record the new realization and evidence. -#### Platform development guide +### Platform startup contract -Creating a new Platform follows six steps: +A selected Platform declares the accepted startup function shape, required +effect surface, runtime handlers, and host adapter. The compiler verifies the +application entry against that contract. -**Step 1**: Initialize the Platform project. +A conforming Platform package that claims support for the standard state +primitive effect set provides two handler families for each supported primitive: +a host handler backed by the target environment and an in-memory mock handler +usable by tests and local handler scopes. This requirement does not create a +default Platform. Programs still select a Platform explicitly. -```bash -spore init my-platform --type platform -``` +### Foreign linkage and export attributes -**Step 2**: Define the Platform contract in `platform.sp`: +`@foreign` is valid on bodyless function declarations and bodyless type +declarations. On functions, an optional first positional string identifies the +link target or provider, and `name = "..."` optionally overrides the external +symbol name: ```spore -platform EmbeddedPlatform { - handles [GpioRead, GpioWrite, Timer, SerialRead, SerialWrite] - startup: fn() -> Never ! GpioRead | GpioWrite | Timer - - config: { - cpu_freq: U32, - gpio_pins: U8, - } -} +@foreign("ssl", name = "SSL_new") +fn ssl_new() -> Ptr[SSL] +uses [Native]; ``` -**Step 3**: Implement effect handlers: +A bare `@foreign` delegates symbol resolution to the selected Platform package: ```spore -// handlers/gpio.sp -handler GpioHandler { - Gpio.read(pin: U8) -> Bool { - resume(ffi_gpio_read(pin)) - } - Gpio.write(pin: U8, value: Bool) -> Unit { - ffi_gpio_write(pin, value) - resume(()) - } -} -``` - -**Step 4**: Write FFI bindings (Rust/C): - -```rust -#[no_mangle] -pub extern "C" fn ffi_gpio_read(pin: u8) -> bool { - unsafe { - let reg = (GPIO_BASE + pin as usize * 4) as *const u32; - core::ptr::read_volatile(reg) != 0 - } -} +@foreign +fn read_file(path: Path) -> Str ! IoError +uses [FileRead]; ``` -**Step 5**: Write documentation and examples for Platform consumers. - -**Step 6**: Publish the Platform (same as any package—push to Git, record hashes). - -Platforms are content-addressed packages like any other, following the same `sig`/`impl` dual-hash scheme. - -#### Platform subsystem interactions - -| Subsystem | Interaction | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Concurrency** | `Spawn` is an effect surfaced by Platform-owned modules; runtimes may back it with a thread pool, tokio, or another scheduler | -| **Package management** | Platforms are ordinary Spore packages, fetched and cached via the same content-addressed system | -| **Effect system** | Platform defines the manifest-declared effect contract. The selected startup entry's required effects against `[platform].handled-effects`; broader whole-application enforcement remains follow-up work | -| **Cost model** | Platform effects carry cost annotations: `File.read @cost(io=1)`. The compiler uses these for cost analysis | -| **Compiler** | Resolves Platform package metadata and contract modules into the compiled runtime boundary | - -#### Platform API reference - -**CLI Platform** (current `basic-cli` package surface): +`@foreign` on a type declaration marks the representation as external: ```spore -// basic_cli.file -pub foreign fn file_read(path: Str) -> Str ! IoError uses [FileRead] -pub foreign fn file_write(path: Str, content: Str) -> () ! IoError uses [FileWrite] -pub foreign fn file_exists(path: Str) -> Bool uses [FileRead] -pub foreign fn file_stat(path: Str) -> Str ! IoError uses [FileRead] - -// basic_cli.dir -pub foreign fn dir_list(path: Str) -> List[Str] ! IoError uses [FileRead] -pub foreign fn dir_mkdir(path: Str) -> () ! IoError uses [FileWrite] - -// basic_cli.stdout -pub foreign fn print(s: Str) -> () ! IoError uses [Console] -pub foreign fn println(s: Str) -> () uses [Console] -pub foreign fn eprint(s: Str) -> () ! IoError uses [Console] -pub foreign fn eprintln(s: Str) -> () uses [Console] - -// basic_cli.stdin -pub foreign fn read_line() -> Str ! IoError uses [Console] - -// basic_cli.env -pub foreign fn env_get(key: Str) -> Option[Str] uses [Env] -pub foreign fn env_set(key: Str, value: Str) -> () uses [Env] - -// basic_cli.cmd -pub foreign fn process_run(cmd: Str, args: List[Str]) -> Str ! ExecError uses [Spawn] -pub foreign fn process_run_status(cmd: Str, args: List[Str]) -> Option[U8] ! ExecError uses [Spawn] -pub foreign fn exit(code: U8) -> Never uses [Exit] -``` - -**Comparison with Roc, Elm, and Koka**: - -| Feature | Roc | Elm | Koka | Spore | -| ---------------------- | ----------------- | ----------------- | -------------- | ---------------- | -| Platform concept | ✓ | Partial (runtime) | ✗ | ✓ | -| Effect system | ✗ (tag unions) | ✗ (Cmd/Sub) | ✓ | ✓ | -| One Platform / project | ✗ | ✗ | N/A | ✓ | -| Test Platform | Mock Platform | Mock Cmd | Manual mock | Test Platform | -| Startup contract | Platform-defined | Fixed (main) | Fixed (main) | Platform-defined | -| Concurrency | Platform-provided | Built-in runtime | Effect handler | Effect handler | - -### CLI command reference - -#### Module and build commands - -```text -spore check [path...] Check one or more source / entry files -spore build [path] Build the current project or one explicit file -spore test [path...] Validate test / spec files -spore fmt [path...] Format source, including import ordering -sporec compile [path...] Compile explicit input files -sporec holes [path] List holes in a source file -sporec query-hole [path] [id] Inspect one named hole in a source file -sporec explain [code] Explain one diagnostic code -``` - -#### Snapshot and lock commands - -```text -spore snapshot Show package/module/function hashes (sig + impl) -spore snapshot --show Show specific module's interface hash -spore --permit Accept a sig change, update .spore-lock -spore --permit --all Accept all pending sig changes -spore lock Generate or update .spore-lock -spore lock --verify Verify .spore-lock matches current state -``` - -#### Package management commands - -```text -spore init [name] Initialize new project -spore add Add dependency (Git URL, local path) - --alias Set named alias - --tag Pin to Git tag - --branch Track Git branch - --rev Pin to Git commit - --sig-only Interface-only dependency - --dev Add as dev dependency - --optional Add as optional dependency - -spore remove Remove dependency from spore.toml -spore update [name] Update dependency hashes - --tag Update to specific tag - --recompute-hash Recompute hashes for path dependencies - -spore fetch Fetch all dependencies to local cache - --jobs Parallel fetch (default: 8) - --sigs-only Fetch signatures only (for type-checking) - -spore lock Generate .spore-lock from spore.toml -spore lock --verify Verify .spore-lock integrity -``` - -#### Audit and inspection commands - -```text -spore audit [package] Audit package effect usage -spore audit --all Full audit (effects, hashes, cycles, unused) -spore deps [module] Show dependency tree -spore deps --reverse Show what depends on a module -spore deps --depth Limit tree depth -spore deps --json Output as JSON -spore cost-report Show cost summary for a module -spore exports List a module's public API -spore hash Compute current module's sig and impl hashes -spore hash --sig-only Show signature hash only -spore gc Clean unused cache entries -spore gc --all Remove all cache -``` - -#### Example workflow - -```bash -# 1. Create a new project -spore init my-app - -# 2. Add dependencies -spore add https://github.com/spore-std/http --alias std-http -spore add https://github.com/spore-std/json --sig-only - -# 3. Write code, then check -spore check src/main.sp - -# 4. Format before review -spore fmt src/main.sp src/cli.sp - -# 5. Build and test -spore build && spore test tests/main_test.sp - -# 6. If a dependency's signature changed, review and accept -spore --permit billing.tax.calculate - -# 7. Audit before deployment -spore audit --all -``` - -### End-to-end example scenarios - -#### Scenario 1: Create a new project - -```bash -mkdir my-app && cd my-app -spore init -spore add https://github.com/spore-std/http --alias std-http -spore add https://github.com/spore-std/json --sig-only -spore check src/main.sp -spore build -spore run src/main.sp -``` - -#### Scenario 2: Update a dependency - -```bash -spore deps # view current dependency tree -spore update http # update to latest commit -spore test tests/http_test.sp # verify tests still pass -spore audit # check for effect changes -git add .spore-lock spore.toml -git commit -m "Update http to latest" -``` - -#### Scenario 3: Handle diamond dependencies - -Two libraries depend on the same `utils` package with the same `sig` but different `impl`: - -```text -$ spore deps -my-app -├── lib-a@sig:aaa+impl:bbb -│ └── utils@sig:same+impl:alpha -└── lib-b@sig:ccc+impl:ddd - └── utils@sig:same+impl:beta -``` - -Both implementations coexist. If `my-app` directly depends on `utils`, it must choose explicitly: `spore add utils --impl alpha`. - -#### Scenario 4: Local monorepo development - -```toml -# packages/app/spore.toml -[dependencies] -core = { path = "../core", sig = "...", impl = "..." } -utils = { path = "../utils", sig = "...", impl = "..." } +@foreign +type Map[K, V]; ``` -```bash -# After editing core or utils: -spore update --recompute-hash # update hashes for path deps -spore build -``` - -#### Scenario 5: Publish a package +`@export("C")` is valid on `pub fn` declarations with a body. SEP-0008 only +standardizes the `"C"` ABI string. Future ABIs may extend this set without +changing SEP-0001 grammar. -```bash -spore test && spore audit -spore hash # record sig + impl hashes -git add . && git commit -m "Release stable" -git tag stable-release -git push origin main --tags -# Update README with spore.toml snippet including hashes -``` +### Holes and packages -### Glossary - -| Term | Definition | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| **Content-addressed** | Identifying resources by the hash of their content rather than by name or location | -| **Signature hash (`sig`)** | BLAKE3 hash of a function's public contract (params, return type, errors, effects, cost) | -| **Implementation hash (`impl`)** | BLAKE3 hash of a function's compiled AST; `None` for partial functions | -| **Named alias** | A human-readable identifier mapping to a specific set of content hashes | -| **Dependency graph** | DAG of module/package import relationships | -| **Effect** | A declared permission for a function to perform a specific category of operation | -| **Effect ceiling** | A reserved follow-up concept for module- or project-level effect caps; not part of the this SEP contract | -| **Lock file (`.spore-lock`)** | Machine-generated file recording the full resolved dependency graph with hashes | -| **Diamond dependency** | When the same module is reached via multiple paths in the dependency graph | -| **Reproducible build** | A build that produces identical output from identical inputs (ensured by `impl` hashes) | -| **Platform** | A Spore package that declares startup metadata/contracts and exposes the host-facing modules used by an application | -| **Effect handler** | A runtime host implementation detail that may sit behind Platform package surfaces; not the primary package contract model | -| **Hole** | A placeholder (`?name`) in a function body, marking incomplete code | -| **Partial function** | A function containing holes; has `impl = None` until all holes are filled | +A package may contain holes during development. Public release policies may +require `holes: 0` budget evidence for exported callables before publication. ## Human experience impact -### Readability and navigation - -- **File = module** eliminates the confusion of Rust's `mod`/`use` distinction or OCaml's separate `.mli` files. Opening a `.sp` file immediately tells you the module name — no `module` keyword needed. -- **Dot-separated imports** (`import billing.invoice`) use a uniform separator for both path segments and item access, avoiding the confusion of mixing `/` and `.`. The import path mirrors the module name directly. -- **Import/alias separation** removes ambiguity: `import` always means module, `alias` always means item. -- **Three visibility levels** are intuitive. The default is private; opt-in to exposure via `pub` or `pub(pkg)`. - -### Workflow ergonomics - -- **Structured diagnostics** surface missing or mismatched `uses [...]` requirements and related import/startup issues, lowering the barrier for new users. -- **`spore --permit`** provides explicit acknowledgment of breaking changes, preventing accidental API breakage. -- **No semver** means developers never debate whether a change is "major" or "minor". The compiler decides mechanically. - -### Learning curve - -The dual-hash model is more conceptually complex than semver. However, the tooling hides most complexity: `spore add` computes hashes automatically, `spore update` refreshes them, and `.spore-lock` is machine-generated. Developers interact with hashes only when reviewing breaking changes via `spore --permit`. - -### Error messages - -Visibility violations, circular dependencies, startup-effect mismatches, and missing Platform bindings all produce structured, actionable diagnostics with `help:` suggestions. +Developers review named hash categories instead of a single opaque digest. This +makes it clearer whether a change altered the callable boundary, intent, +realization, or evidence. ## Agent experience impact -### Machine-readable module boundaries - -The module system is designed for agent-friendly navigation: - -- Module names are deterministically derived from file paths. -- Dependency graphs are acyclic DAGs, trivially traversable. -- Visibility rules are encoded in the AST—an agent can enumerate a module's public API without human guidance. -- Effect requirements are explicit in `uses [...]` clauses. - -### Content addressing for agents - -Dual hashes give agents precise anchors: - -- An agent can verify whether a dependency change is API-breaking (check `sig`) or implementation-only (check `impl`). -- Hole reports include visibility-filtered candidate lists, enabling automated hole filling. -- The `spore snapshot` and `spore exports` commands provide machine-consumable module metadata. - -### Structured diagnostics - -All compiler diagnostics (visibility violations, cycle detection, effect mismatches) are emitted in both human-readable and JSON formats, enabling agent tooling to parse and act on errors programmatically. +Agents can decide whether a dependency update is relevant to their task by +inspecting the changed hash category and evidence results. ## Structured representation / protocol impact -### `.spore-lock` as the canonical dependency record - -The `.spore-lock` file is a TOML document that encodes the full dependency graph with both `sig` and `impl` hashes, source locations, effect records, and verification timestamps: - -```toml -[[package]] -name = "http" -alias = "std-http-stable" -source = { type = "git", url = "https://github.com/std/http", rev = "abc1234" } -sig = "a3f9c2e1d8b7a6f5" -impl = "7b8d4f2a1e9c3d5b" - -[verification] -"http" = { - sig-algo = "blake3", - sig-hash = "a3f9c2e1d8b7a6f5", - impl-algo = "blake3", - impl-hash = "7b8d4f2a1e9c3d5b", - verified-at = "2024-01-15T10:30:05Z" -} - -[effects] -"http" = ["NetListen", "NetConnect"] +```text +PackageRecord +├── modules[] +├── exports[] +├── platform? +├── hashes +│ ├── signature_hashes[] +│ ├── intent_hashes[] +│ ├── property_hashes[] +│ ├── realization_hashes[] +│ └── evidence_hashes[] +└── dependencies[] ``` -### Module interface files (`.spore-sig`) - -For interface-only dependencies, the compiler can produce `.spore-sig` files containing only the canonicalized public API. These are sufficient for type-checking without the full implementation. - -### Platform binding records - -The compiler resolves the selected Platform package's metadata, startup contract, -and imported host-facing modules into the compiled output. The -centers startup-contract validation plus the selected Platform package boundary; -broader whole-program effect routing remains follow-up work. - ## Diagnostics impact -### Canonical diagnostic categories - -This SEP reuses the canonical diagnostic families defined in SEP-0006 instead -of introducing a separate `E3xxx` / `E4xxx` namespace. Older draft placeholders -in those ranges are retired in favor of the shared `M0xxx` and `C0xxx` -registries. - -| Code | Category | Example | -| ------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `M0201` | Visibility violation | Accessing a private function from another module | -| `M0101` | Circular dependency | Module A → B → A | -| `C0101` | Effect declaration mismatch | Function calls an operation requiring `FileWrite` without declaring `uses [FileWrite]` | -| `M0401` | Signature change detected | `sig` hash mismatch in `.spore-lock` | -| `M0204` | Alias chain | `pub alias` pointing to another alias | -| `M0304` | Shadowing conflict | Import alias conflicts with module name | -| `M0501` | Platform binding conflict | Project declares more than one Platform binding | -| `C0201` | Unsupported startup effect | Selected startup entry requires an effect not listed in the Platform's `[platform].handled-effects` metadata | -| `M0502` | Startup contract mismatch | Startup function signature doesn't match Platform requirement | - -### Diagnostic structure - -All diagnostics follow a consistent pattern: - -```text -[error|warning|info] at - - ─── - - help: -``` - -Diagnostics are available in both human-readable (terminal) and machine-readable (JSON) formats. +Module diagnostics use `M0xxx` codes: + +| Code | Name | Meaning | +| ------- | ------------------------- | ------------------------------------------------- | +| `M0101` | circular-dependency | Module graph contains a cycle | +| `M0201` | visibility-violation | Item is not visible from the use site | +| `M0301` | import-not-found | Import path cannot be resolved | +| `M0401` | signature-hash-changed | Dependency callable boundary changed | +| `M0402` | evidence-missing | Required evidence record is absent | +| `M0501` | platform-binding-conflict | More than one Platform binding selected | +| `M0502` | startup-contract-mismatch | Entry function does not satisfy Platform contract | +| `M0503` | primitive-handler-missing | Platform binding lacks a required state primitive handler | +| `M0601` | invalid-foreign-target | `@foreign` attached to an unsupported declaration | +| `M0602` | foreign-body-present | `@foreign` function unexpectedly has a body | +| `M0603` | unsupported-export-abi | `@export` ABI string is not supported | +| `M0604` | foreign-symbol-unresolved | Foreign symbol or link target cannot be resolved | ## Drawbacks -1. **Conceptual complexity of dual hashing**: Developers must understand the distinction between `sig` and `impl` hashes. While tooling hides most of this, debugging hash mismatches requires understanding the model. - -2. **No semver for human communication**: Content hashes are opaque. Developers cannot glance at a hash and know "how different" two versions are. Named aliases and changelogs partially mitigate this, but the loss of semver's human-readable signal is real. - -3. **Platform authoring overhead**: Creating a new Platform requires implementing effect handlers and FFI bindings. Most developers will use community Platforms, but the barrier to creating novel Platforms is higher than writing a simple `main()`. - -4. **No wildcard imports**: Requiring explicit imports for every module is verbose. The `pub alias` shortcut module pattern mitigates this, but some developers may find it tedious. - -5. **No circular dependencies**: Occasionally forces extraction of shared types into third modules, adding structural overhead. In practice, the Elm and Go communities have found this constraint beneficial. +Multiple hash categories are more complex than release-label ranges. Tooling must +render concise labels and explain which category changed. -6. **No functors or parameterized modules**: Some patterns natural with OCaml/SML functors require more boilerplate with generics and effects. The trade-off is reduced conceptual surface area. - -7. **Decentralized discovery**: Without a central registry, finding packages relies on community indices, search engines, and "awesome lists". This is a discoverability trade-off for resilience. +Evidence-aware package gates may slow publication until checkers have produced +records for required claims. ## Alternatives considered -### Single hash (Unison-style full content addressing) - -Unison hashes the entire AST, including function names. This provides the strongest content-addressing guarantees but **abandons file-based tooling**: functions are identified by hash, not by name or location. Spore rejected this in favor of dual hashing, which preserves file-based workflows while still providing exact content pinning via `impl`. - -### Signature-only hash (no `impl`) - -An earlier design considered only tracking `sig` hashes. This preserves API compatibility checking but cannot pin exact implementations, making reproducible builds impossible without additional out-of-band mechanisms (like Git commit SHAs). - -### "Last implementation" tracking - -Another rejected design tracked whether the implementation had changed since the last build, without computing a deterministic hash. This was deemed too fuzzy—it depends on build ordering and mutable state. The BLAKE3 AST hash is computed directly from the compiled representation and is unambiguous. - -### Semantic versioning with content hash verification - -A hybrid approach: use semver for human communication but verify with content hashes. Rejected because it introduces two sources of truth. When semver and hashes disagree, which wins? The simplicity of hash-only resolution was preferred. +### Release-label ranges -### Two-level visibility (Elm-style: private + public) +Rejected because labels cannot prove compatibility or realization identity. -Elm uses only private and public visibility. The research shows most languages eventually add a third level (Go's `internal/`, Rust's `pub(crate)`). Starting with three levels avoids backward-compatible extensions later. `pub(pkg)` covers the common need for package-internal helpers. +### Single package hash -### Four+ level visibility (Rust's `pub(super)`, `pub(in path)`) +Rejected because it hides whether a change affected signatures, intents, +properties, realizations, evidence, or dependencies. -Fine-grained visibility like `pub(in path)` is rarely used and adds cognitive load. Three levels cover the vast majority of use cases. +### Module declarations in source -### Functors / parameterized modules (OCaml-style) +Rejected because file paths are already the package structure source of truth. -OCaml/SML functors are the most powerful module system in any language, but they add a separate "module language" that doubles the conceptual surface area. Spore's combination of generics (`where T: Constraint`) and effects (`uses [...]`) covers the same use cases with less overhead. +### Link metadata only in manifests -### Built-in Platform (standard library IO) - -Most languages provide IO in the standard library. Spore rejected this in favor of Roc-style pluggable Platforms for testability, portability, and supply-chain security. The trade-off is additional complexity in Platform authoring. +Rejected as the primary mechanism because it separates a foreign declaration +from its linkage intent, makes symbol-to-library provenance harder to review, +and weakens library self-containment. Manifest data may still refine +platform-specific build settings. ## Prior art -### Unison - -Unison pioneered content-addressed functions where every definition is identified by the hash of its AST. Spore's dual-hash scheme is directly inspired by Unison but splits the hash into `sig` (API contract) and `impl` (exact code), preserving file-based tooling that Unison abandons. - -### Roc - -Roc's platform/package separation is the primary inspiration for Spore's Platform system. Roc demonstrated that packages can be pure by default, with IO delegated entirely to the Platform. Spore extends this with algebraic effect handlers (Roc uses tag unions), named entries, and explicit startup contracts. - -### Elm - -Elm's file-based modules, prohibition of circular dependencies, and emphasis on simplicity heavily influenced Spore's module design. Elm proves that these constraints scale to large codebases while keeping the system learnable. - -### Go - -Go's file-based packages, prohibition of circular imports, and `internal/` directory convention informed Spore's visibility model. Go demonstrates that cycle-free packages produce maintainable codebases at Google scale. - -### Rust - -Rust's `pub(crate)` visibility level and Cargo's `Cargo.lock` file informed Spore's `pub(pkg)` and `.spore-lock` designs. Spore avoids Rust's more complex visibility specifiers (`pub(super)`, `pub(in path)`) in favor of simplicity. - -### Koka - -Koka's algebraic effect system inspired Spore's integration of effects with module effects. Koka demonstrates that effects and modules should be designed together. Spore adds the Platform concept on top of Koka-style effects. - -### Nix - -Nix's derivation hashes demonstrate the value of content-addressing for reproducible builds. Spore's `impl` hash serves a similar role to Nix's store paths—pinning exact content for bit-for-bit reproducibility. - -### Go modules (`go.sum`) - -Go's `go.sum` file shows that content hashing for dependency verification is mainstream and practical. Spore takes this further by making content hashes the **primary** identifier, not just a verification mechanism. +Unison and Nix influenced content addressing. Cargo and Go modules influenced +lock data and reproducible dependency review. Roc influenced Platform packages. +Rust influenced ABI export and linker metadata conventions. Kotlin, Swift, and +Java influenced attribute-style metadata surfaces. ## Backward compatibility and migration -### From semantic versioning - -Existing ecosystems using semver can migrate by computing content hashes for each labeled release: - -```bash -# Migration tool computes hashes from existing release tags -spore migrate --from package.json --to spore.toml -``` - -The tool: - -1. Reads the existing manifest. -2. Resolves Git tags corresponding to release labels. -3. Computes BLAKE3 `sig` and `impl` hashes for each dependency. -4. Generates `spore.toml` with hash-based declarations. - -The human-readable release label is preserved for documentation but plays no role in resolution. - -### Lock file schema evolution - -The `.spore-lock` file carries a schema marker. Old tools refuse to process newer schema shapes. Migration scripts handle format upgrades: - -```bash -spore lock upgrade -``` - -New fields added to `.spore-lock` are ignored by older tools when possible. Removal or semantic changes to existing fields require an explicit schema migration. - -### Incremental adoption - -The module system does not require wholesale adoption: - -- Packages can adopt explicit `uses [...]` signatures incrementally at function boundaries; broader file- or project-level ceilings remain future work. -- Dependencies can mix Git, local path, and registry sources. -- A human-readable release label in `spore.toml` remains available during the transition from semver. +Package metadata must migrate to named signature hash categories. Existing +manifest fields can remain when they describe package names, paths, and Platform +selection rather than compatibility identity. ## Unresolved questions -1. **Hash truncation for display**: How many hex characters should be shown in human-facing output? 8? 16? Full 64? Should there be a configurable default? - -2. **Effect granularity**: The current design uses coarse-grained effect names (e.g., `FileRead`, `NetConnect`). Should fine-grained effects (e.g., `filesystem:read:/data`) be part of the language-level type system or remain a tooling concern in `spore.toml`? - -3. **Platform evolution**: Platforms themselves evolve. When a Platform changes its handler interface, how are downstream applications notified? Is `spore --permit` sufficient, or do Platforms need a separate compatibility mechanism? - -4. **Module-level cost budgets**: The current design tracks cost per-function only. Should modules declare aggregate cost ceilings? What are the semantics when a module's total cost exceeds a declared budget? - -5. **Diamond dependencies with different `impl` hashes**: When two transitive dependencies require the same module with the same `sig` but different `impl` hashes, the current design allows both to coexist. Should the linker deduplicate? Should the developer be warned? What are the binary size implications? - -6. **Effect handler composition**: When an application-defined effect maps to Platform effects (e.g., `Logger` → `StdOut`/`StdErr`), how are effect requirements tracked through the composition? Is the current `uses [...]` propagation sufficient? - -7. **Conditional compilation**: The `[features]` mechanism in `spore.toml` enables optional dependencies, but the interaction between features and content hashes is underspecified. Does enabling a feature change the `sig` hash? - -8. **Offline-first workflows**: How should `spore add` and `spore update` behave when offline? Should the local cache be sufficient for all operations, or are some operations inherently online? - -### Resolved questions - -1. **File-to-module mapping**: Resolved by SEP-0001 and this SEP. Each `.sp` - file is one module, the module path is derived from the filesystem, and - there is no `module` keyword in the accepted surface. - -2. **Cross-package `pub(pkg)` boundaries in workspaces**: Resolved — - `pub(pkg)` restricts visibility to the current package/member only. Items - must be promoted to `pub` for cross-member access. - -3. **Alias chain depth**: Resolved for the accepted surface. Alias chains remain forbidden; a - `pub alias` must point directly to the original item. - ---- - -## Appendix A: Grammar summary - -```text -cap_list ::= '[' effect (',' effect)* ']' -effect ::= UPPER_IDENT - -// Imports and aliases -import_decl ::= 'import' module_path ('as' IDENT)? - // NOTE: selective imports are not part of the accepted surface -alias_decl ::= visibility? 'alias' IDENT '=' qualified_item -module_path ::= IDENT ('.' IDENT)* -qualified_item ::= module_path '.' IDENT - -// Visibility -visibility ::= 'pub' | 'pub(pkg)' - -// Function declarations -fn_decl ::= visibility? 'fn' IDENT generic_params? '(' params ')' '->' type - error_clause? cost_clause? uses_clause? - block -generic_params ::= '[' IDENT (',' IDENT)* ']' -params ::= (param (',' param)*)? -param ::= IDENT ':' type -error_clause ::= '!' type ('|' type)* -cost_clause ::= 'cost' '[' expr ',' expr ',' expr ',' expr ']' -uses_clause ::= 'uses' cap_list - -// Type declarations -type_decl ::= visibility? 'type' IDENT generic_params? '{' field_list '}' - | visibility? 'type' IDENT generic_params? '=' type -field_list ::= (field (',' field)*)? -field ::= IDENT ':' type - -// Platform declarations -platform_decl ::= 'platform' IDENT '{' platform_body '}' -platform_body ::= ('handles' cap_list) - ('startup:' type) - handler_decl* - -// Handler declarations -handler_decl ::= 'handler' IDENT ('uses' cap_list)? '{' handler_body '}' -handler_body ::= (fn_decl | var_decl)* - -// Effect declarations -effect_decl ::= 'effect' IDENT '{' effect_fn* '}' -effect_fn ::= 'fn' IDENT '(' params ')' '->' type - -// spore.toml (TOML subset) -// [package] name, release, type, description, license, authors, default-entry -// [dependencies] = { git|path|alias, sig, impl?, optional?, branch?, tag?, rev? } -// [dev-dependencies] -// [features] = [deps/features] -// [entries.] path = STRING -// [platform] git|path, handles? -// [overrides] = { sig, impl } -// [build] script -// [metadata] arbitrary key-value -``` - -## Appendix B: CLI command reference (grouped) - -### Module commands - -| Command | Description | -| ------------------------------- | ---------------------------------------------- | -| `spore check [path...]` | Check one or more source / entry files | -| `spore build [path]` | Build the current project or one explicit file | -| `spore test [path...]` | Validate test / spec files | -| `spore fmt [path...]` | Format source code (including import ordering) | -| `sporec compile [path...]` | Compile explicit input files | -| `sporec holes [path]` | List all holes in a source file | -| `sporec query-hole [path] [id]` | Inspect one named hole in a source file | -| `sporec explain [code]` | Explain one diagnostic code | - -### Snapshot and hash commands - -| Command | Description | -| ------------------------- | ----------------------------------------------- | -| `spore snapshot` | Show function, module, and package hashes | -| `spore hash` | Compute current module/function hashes | -| `spore --permit ` | Accept a signature change, update `.spore-lock` | -| `spore --permit --all` | Accept all pending signature changes | -| `spore exports ` | List a module's public API with hashes | -| `spore cost-report ` | Show cost summary for a module | - -### Package management commands - -| Command | Description | -| --------------------- | ------------------------------------ | -| `spore init [name]` | Initialize new project | -| `spore add ` | Add a dependency | -| `spore remove ` | Remove a dependency | -| `spore update [name]` | Update dependency hashes | -| `spore lock` | Generate or update `.spore-lock` | -| `spore lock --verify` | Verify `.spore-lock` integrity | -| `spore fetch` | Download dependencies to local cache | - -### Audit and inspection commands - -| Command | Description | -| -------------------------- | ------------------------------------- | -| `spore audit` | Audit package effects, hashes, cycles | -| `spore deps` | Show dependency tree | -| `spore deps --reverse ` | Show reverse dependencies | -| `spore gc` | Clean unused cache entries | +1. Which evidence records are required for package publication? +2. Should package APIs include private intent hashes or exported subjects only? +3. How should transitive evidence failures be summarized for users? diff --git a/seps/SEP-0009-standard-library.md b/seps/SEP-0009-standard-library.md index 4b30673..78b5cbb 100644 --- a/seps/SEP-0009-standard-library.md +++ b/seps/SEP-0009-standard-library.md @@ -6,776 +6,281 @@ type: Standards Track authors: - Spore Contributors created: 2026-04-01 -requires: [1, 2, 3, 4, 7, 8] +requires: [1, 2, 3, 4, 6, 7, 8] discussion: "https://github.com/spore-lang/spore-evolution/discussions" pr: null superseded_by: null --- -> **Executive Summary**: Defines the standard library surface for Spore — the set of built-in types, functions, traits, and error types that every Spore program can rely on. Organizes the stdlib into a layered prelude (always-available primitives), core modules (opt-in via import), and platform-provided I/O bindings. Every function carries effect requirements and cost annotations per SEP-0003 and SEP-0004. - # SEP-0009: Standard Library Surface -## Summary +> **Executive Summary**: Defines the standard library surface under the signature model. Standard APIs use inline generic bounds, routine functions omit budget annotations, and core behavior is documented through properties where the property is part of public intent. -This SEP specifies the standard library that ships with every Spore implementation. It defines: +## Summary -1. **Prelude** — types, traits, and functions available without import -2. **Core modules** — standard modules available via `import std.*` -3. **Error types** — a unified error hierarchy -4. **Platform bindings** — how I/O functions connect to selected Platform package modules -5. **Cost contracts** — verified cost annotations for indexed stdlib functions +The standard library provides small, predictable modules for common data and +platform needs: -Shared operation names such as `map`, `len`, `unwrap`, `contains`, and -`from_list` are trait or inherent methods, not overloaded free functions. -Method resolution is Rust-like: the receiver type or type namespace selects the -implementation, and ambiguous cases use trait-qualified syntax. +- `spore.list` +- `spore.option` +- `spore.outcome` +- `spore.map` +- `spore.set` +- `spore.str` +- `spore.math` +- `spore.ref` -The stdlib is deliberately small. Spore follows the principle that the standard library should provide exactly the types and functions needed to write idiomatic Spore code, with everything else available as packages. +Routine operations do not carry explicit budgets unless realization shape is +part of the contract. -The standard-library naming uses `std.*` for core modules and `spore.*` for -algebraic helper surfaces (for example, `spore.combine`, `spore.merge`, -`spore.order`, and `spore.laws`). No `std.algebra` surface is introduced. +The snippets in this SEP are standard-library surface sketches. A signature that +omits a body states an API item the standard library must provide; it is not a +requirement that this SEP spell out the implementation body. ## Motivation -Currently, SEPs 0001–0008 reference standard library items (e.g., -`map`, `fold`, `Option[T]`, `Result[T, E]`, `List[T]`) without a -single authoritative specification. This creates several problems: - -1. **Ambiguity**: Function signatures are mentioned inconsistently across SEPs -2. **Cost-contract ambiguity**: Dynamic APIs and indexed APIs need different cost-contract rules -3. **No error type hierarchy**: `IoError`, `HttpError`, `ParseError` are referenced but never defined -4. **FFI boundary unclear**: Which functions are implemented natively vs. in Spore is unspecified -5. **Agent tooling gap**: Agents cannot enumerate available functions or their contracts +The standard library should demonstrate the idiomatic signature model: compact Base +Signatures, first-class outcomes, inline bounds, effects through `uses`, and +properties for behavior that users rely on. ## Guide-level explanation -### What's in the prelude? - -Every Spore file implicitly has access to: - -```spore -// Primitive types: I8..U64, F32, F64, Bool, Str, Unit, Never (see SEP-0002) - -// Algebraic types -type Option[T] { Some(T), None } -type Result[T, E] { Ok(T), Err(E) } -type List[T] { Cons(T, List[T]), Nil } -type Ordering { Less, Equal, Greater } - -// Index-admitted count -type Count[N: Index] = I64 when self >= 0 -``` - -The prelude intentionally does **not** export duplicate free functions or -signature-union overloads. Shared names are trait or inherent methods: -`xs.map(f)`, `opt.map(f)`, `s.len()`, and `Map.from_list(pairs)` are distinct -method resolutions, not overloaded free functions. - -### What needs an import? - -Specialized modules require explicit imports: - -```spore -import std.index // Count helpers: add, span, ... -import std.array // Array[T, N] and fixed-length operations -import std.vec // Vec[T, max: N] and verified size-parametric operations -import std.list // Dynamic List operations -import std.math // f64_sqrt, f64_sin, i64_abs, f64_pow, ... -import std.string // split, trim, join, contains, ... -import std.collections // Map, Set, Map.from_list, Set.from_list, ... -// future: import std.io // stabilized platform-neutral I/O facade -import std.time // DateTime, Duration, now(), ... -import std.json // json_parse, json_to_string -``` - -### How do I/O functions work? - -Manifest-backed projects import the selected Platform -package's modules directly. Those modules expose `foreign fn` declarations -with explicit `uses [...]` requirements. +### Option ```spore -import basic_cli.stdout -import basic_cli.cmd +enum Option[T] { + Some(T), + None, +} -fn main() -> () uses [Console, Exit] { - println("hello") - exit(0u8) +impl[T] Option[T] { + fn map[U](self, f: Fn[T, U]) -> Option[U]; + fn unwrap_or(self, default: T) -> T; + fn is_some(self) -> Bool; + fn is_none(self) -> Bool; } ``` -For `basic-cli`, the surface lives in modules such as -`basic_cli.stdout`, `basic_cli.stdin`, `basic_cli.file`, `basic_cli.env`, and -`basic_cli.cmd`. -A future `std.io` layer may wrap or re-export a common surface above those -Platform packages. - -For declared effects in interpreter-style -execution, `perform Effect.operation(...)` falls back through registered host -handlers by the **qualified** `Effect.operation` key. Platform package -modules remain the primary application-facing contract. - -## Reference-level explanation - -### 4.1 Prelude types - -The prelude is implicitly imported into every module. It contains: - -#### Primitive types - -| Type | Description | Default | Size / notes | -| ---------------------- | ------------------------- | ------------------------ | ------------ | -| `I8`…`I64`, `U8`…`U64` | Fixed-width integers | `0` for the chosen width | 1–8 bytes | -| `F32`, `F64` | IEEE-754 floats | `0.0` | 4 or 8 bytes | -| `Bool` | Boolean | `false` | 1 byte | -| `Str` | Immutable UTF-8 string | `""` | Variable | -| `Unit` | Zero-valued type | `()` | 0 bytes | -| `Never` | Bottom type (uninhabited) | — | 0 bytes | +### Outcome helpers -**No `Char`.** Unicode scalars are represented as `Str` values (typically length 1). Character predicates and conversions live in `stdlib/char.sp` (`is_digit`, `char_to_int`, …). This matches the language specification. - -**Literals.** In the reference type checker, unsuffixed integer literals default to **`I64`** and floating-point literals default to **`F64`**. Standard-library signatures use explicit fixed-width names; `Int` and `Float` are not standard aliases. - -#### Algebraic and index-admitted types - -Prelude **`List[T]`** is dynamic and unbounded. It is the canonical sequence -type for ordinary programs, but its runtime length is not a CostExpr variable. -Use `Count[N]`, `Array[T, N]`, or `Vec[T, max: N]` when a static Index must -participate in verified cost. +Spore does not expose a prelude `Result[T, E]` type. Outcome handling is built +into `A ! E`, and the standard library may provide helper functions in +`spore.outcome`: ```spore -type Option[T] { Some(T), None } -type Result[T, E] { Ok(T), Err(E) } -type List[T] { Cons(T, List[T]), Nil } -type Ordering { Less, Equal, Greater } -type Count[N: Index] = I64 when self >= 0 +fn map_ok[A, B, E](value: A ! E, f: Fn[A, B]) -> B ! E; +fn map_fail[A, E, F](value: A ! E, f: Fn[E, F]) -> A ! F; +fn is_ok[A, E](value: A ! E) -> Bool; +fn is_fail[A, E](value: A ! E) -> Bool; ``` -### 4.2 Prelude traits and functions - -The prelude does not export duplicate free functions. Similar operations use -trait or inherent methods: - -- `opt.map(f)`, `res.map(f)`, `list.map(f)`, and `vec.map(f)` are method calls. -- `s.len()`, `list.len()`, `map.len()`, and `set.len()` are `Len` trait calls. -- `opt.unwrap()` and `res.unwrap()` are `Unwrap` trait calls. -- `Map.from_list(pairs)` and `Set.from_list(items)` are type-qualified - associated functions. +### Lists and indexed containers -If method resolution is ambiguous after type inference, the programmer uses -trait-qualified syntax. The standard library does not use `A | B` parameter -signatures to simulate overloads. +`List[T]` is the ordinary sequence type. `Array[T, N]` and `Vec[T, max: N]` +carry compile-time index information when APIs need a static size parameter. ```spore -trait Mappable { - type Item - type Mapped[U] - fn map[U](self, f: (Self.Item) -> U) -> Self.Mapped[U] -} - trait Len { - fn len(self) -> I64 - fn is_empty(self) -> Bool { self.len() == 0 } -} - -trait Unwrap { - type Output - fn unwrap(self) -> Self.Output ! PanicError - fn unwrap_or(self, default: Self.Output) -> Self.Output + fn len(self) -> I64; } -trait Contains[Needle] { - fn contains(self, needle: Needle) -> Bool +impl[T] Len for List[T] { + fn len(self) -> I64; } - -impl[T] Mappable for Option[T] { - type Item = T - type Mapped[U] = Option[U] - fn map[U](self, f: (T) -> U) -> Option[U] cost [cost(f), 0, 0, 0] -} - -impl[T] Unwrap for Option[T] { - type Output = T - fn unwrap(self) -> T ! PanicError cost [1, 0, 0, 0] - fn unwrap_or(self, default: T) -> T cost [1, 0, 0, 0] -} - -impl[T] Option[T] { - fn and_then[U](self, f: (T) -> Option[U]) -> Option[U] cost [cost(f), 0, 0, 0] - fn is_some(self) -> Bool cost [1, 0, 0, 0] - fn is_none(self) -> Bool cost [1, 0, 0, 0] -} - -impl[T, E] Mappable for Result[T, E] { - type Item = T - type Mapped[U] = Result[U, E] - fn map[U](self, f: (T) -> U) -> Result[U, E] cost [cost(f), 0, 0, 0] -} - -impl[T, E] Unwrap for Result[T, E] { - type Output = T - fn unwrap(self) -> T ! PanicError cost [1, 0, 0, 0] - fn unwrap_or(self, default: T) -> T cost [1, 0, 0, 0] -} - -impl[T, E] Result[T, E] { - fn map_err[F](self, f: (E) -> F) -> Result[T, F] cost [cost(f), 0, 0, 0] - fn and_then[U](self, f: (T) -> Result[U, E]) -> Result[U, E] cost [cost(f), 0, 0, 0] - fn is_ok(self) -> Bool cost [1, 0, 0, 0] - fn is_err(self) -> Bool cost [1, 0, 0, 0] -} - -fn to_string[A](value: A) -> Str where A: Display cost [1, 1, 0, 0] -fn to_debug_string[A](value: A) -> Str where A: Debug cost [1, 1, 0, 0] ``` -Dynamic `List[T]` operations live in `std.list` rather than the prelude. They -are stable APIs, but they do not publish verified costs that depend on runtime -length. Use `std.vec` or `std.array` for statically indexed cost. - -### 4.3 Core modules - -#### `std.index` - -```spore -fn count_zero() -> Count[0] cost [1, 0, 0, 0] -fn count_one() -> Count[1] cost [1, 0, 0, 0] -fn count_add[N: Index, M: Index](a: Count[N], b: Count[M]) -> Count[N + M] cost [1, 0, 0, 0] -fn count_mul[N: Index, M: Index](a: Count[N], b: Count[M]) -> Count[N * M] cost [1, 0, 0, 0] -fn count_max[N: Index, M: Index](a: Count[N], b: Count[M]) -> Count[max(N, M)] cost [1, 0, 0, 0] -fn count_min[N: Index, M: Index](a: Count[N], b: Count[M]) -> Count[min(N, M)] cost [1, 0, 0, 0] -fn count_span[Hi: Index, Lo: Index](hi: Count[Hi], lo: Count[Lo]) -> Count[span(Hi, Lo)] cost [1, 0, 0, 0] -fn count_to_i64[N: Index](count: Count[N]) -> I64 cost [1, 0, 0, 0] -``` +### Maps and sets -#### `std.array` and `std.vec` +The standard library must provide abstract `Map[K, V]` and `Set[T]` types. This +SEP specifies their surface, not their representation. ```spore -type Array[T, N: Index] -type Vec[T, max: N] where N: Index - -impl[A, N: Index] Mappable for Array[A, N] { - type Item = A - type Mapped[B] = Array[B, N] - fn map[B](self, f: (A) -> B) -> Array[B, N] - cost [N * cost(f) + N, N, 0, 0] -} +@foreign +type Map[K, V]; -impl[A, N: Index] Array[A, N] { - fn fold[B](self, init: B, f: (B, A) -> B) -> B - cost [N * cost(f), 0, 0, 0] -} +@foreign +type Set[T]; -impl[A, N: Index] Mappable for Vec[A, max: N] { - type Item = A - type Mapped[B] = Vec[B, max: N] - fn map[B](self, f: (A) -> B) -> Vec[B, max: N] - cost [N * cost(f) + N, N, 0, 0] +impl[K: Eq + Hash, V] Map[K, V] { + fn empty() -> Map[K, V]; + fn get(self, key: K) -> Option[V]; + fn insert(self, key: K, value: V) -> Map[K, V]; + fn contains_key(self, key: K) -> Bool; } -impl[A, N: Index] Vec[A, max: N] { - fn take[K: Index](self, count: Count[K]) -> Vec[A, max: min(N, K)] - cost [min(N, K), min(N, K), 0, 0] - - fn concat[M: Index](self, other: Vec[A, max: M]) -> Vec[A, max: N + M] - cost [N + M, N + M, 0, 0] +impl[T: Eq + Hash] Set[T] { + fn empty() -> Set[T]; + fn contains(self, item: T) -> Bool; } - -fn range_count[N: Index](start: I64, count: Count[N]) -> Vec[I64, max: N] - cost [N, N, 0, 0] ``` -#### `std.list` - -```spore -impl[A] Mappable for List[A] { - type Item = A - type Mapped[B] = List[B] - fn map[B](self, f: (A) -> B) -> List[B] -} - -impl[A] Len for List[A] { - fn len(self) -> I64 -} +### Properties for standard APIs -impl[A] Contains[A] for List[A] where A: Eq { - fn contains(self, item: A) -> Bool -} - -impl[A] List[A] { - fn filter(self, p: (A) -> Bool) -> List[A] - fn fold[B](self, init: B, f: (B, A) -> B) -> B - fn each(self, f: (A) -> Unit) -> Unit - fn flat_map[B](self, f: (A) -> List[B]) -> List[B] - fn zip[B](self, other: List[B]) -> List[(A, B)] - fn enumerate(self) -> List[(I64, A)] - fn head(self) -> Option[A] - fn tail(self) -> Option[List[A]] - fn last(self) -> Option[A] - fn find(self, p: (A) -> Bool) -> Option[A] - fn any(self, p: (A) -> Bool) -> Bool - fn all(self, p: (A) -> Bool) -> Bool - fn append(self, item: A) -> List[A] - fn prepend(self, item: A) -> List[A] - fn concat(self, other: List[A]) -> List[A] - fn reverse(self) -> List[A] - fn take(self, n: I64) -> List[A] - fn drop(self, n: I64) -> List[A] - fn sort(self) -> List[A] where A: Ord - fn sort_by(self, cmp: (A, A) -> Ordering) -> List[A] -} - -impl List[I64] { - fn range(start: I64, end: I64) -> List[I64] -} -``` - -#### `std.math` +Properties are used when behavior is part of public intent: ```spore -fn i64_abs(x: I64) -> I64 cost [1, 0, 0, 0] -fn f64_abs(x: F64) -> F64 cost [1, 0, 0, 0] -fn i64_min(a: I64, b: I64) -> I64 cost [1, 0, 0, 0] -fn i64_max(a: I64, b: I64) -> I64 cost [1, 0, 0, 0] -fn i64_clamp(x: I64, lo: I64, hi: I64) -> I64 cost [1, 0, 0, 0] -fn f64_sqrt(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_pow(base: F64, exp: F64) -> F64 cost [1, 0, 0, 0] -fn f64_log(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_log2(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_log10(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_exp(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_sin(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_cos(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_tan(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_asin(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_acos(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_atan(x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_atan2(y: F64, x: F64) -> F64 cost [1, 0, 0, 0] -fn f64_floor(x: F64) -> I64 cost [1, 0, 0, 0] -fn f64_ceil(x: F64) -> I64 cost [1, 0, 0, 0] -fn f64_round(x: F64) -> I64 cost [1, 0, 0, 0] -fn f64_truncate(x: F64) -> I64 cost [1, 0, 0, 0] -``` - -#### `std.string` - -```spore -impl Len for Str { - fn len(self) -> I64 -} - -impl Contains[Str] for Str { - fn contains(self, pattern: Str) -> Bool +fn reverse[T](xs: List[T]) -> List[T] +properties { + involutive(xs: List[T]): reverse(reverse(xs)) == xs } - -impl Str { - fn char_at(self, index: I64) -> Option[Str] - fn substring(self, start: I64, end: I64) -> Str - fn substring_count[N: Index](self, start: I64, count: Count[N]) -> Str - cost [N, N, 0, 0] - fn concat(self, other: Str) -> Str - fn join(parts: List[Str], sep: Str) -> Str - fn split(self, sep: Str) -> List[Str] - fn trim(self) -> Str - fn trim_start(self) -> Str - fn trim_end(self) -> Str - fn to_upper(self) -> Str - fn to_lower(self) -> Str - fn starts_with(self, prefix: Str) -> Bool - fn ends_with(self, suffix: Str) -> Bool - fn replace(self, from: Str, to: Str) -> Str - fn parse_i64(self) -> Result[I64, ParseError] - fn parse_f64(self) -> Result[F64, ParseError] +{ + ?reverse_body } ``` -#### `std.collections` - -```spore -type Map[K, V] // Immutable hash map (K must implement Hash + Eq) -type Set[T] // Immutable hash set (T must implement Hash + Eq) - -impl[K, V] Len for Map[K, V] { - fn len(self) -> I64 -} +## Reference-level explanation -impl[K, V] Map[K, V] where K: Hash + Eq { - fn empty() -> Map[K, V] cost [1, 0, 0, 0] - fn from_list(pairs: List[(K, V)]) -> Map[K, V] - fn get(self, key: K) -> Option[V] cost [1, 0, 0, 0] - fn insert(self, key: K, value: V) -> Map[K, V] - fn remove(self, key: K) -> Map[K, V] - fn contains_key(self, key: K) -> Bool cost [1, 0, 0, 0] - fn keys(self) -> List[K] - fn values(self) -> List[V] - fn entries(self) -> List[(K, V)] -} +### Prelude -impl[T] Len for Set[T] { - fn len(self) -> I64 -} +The prelude exports primitive traits, common data types, and small helpers that +are safe to use without imports. State primitive effect names are standard names, +but their host handlers remain Platform-provided. Importing the prelude does not +implicitly select a Platform. -impl[T] Contains[T] for Set[T] where T: Hash + Eq { - fn contains(self, item: T) -> Bool cost [1, 0, 0, 0] -} - -impl[T] Set[T] where T: Hash + Eq { - fn empty() -> Set[T] cost [1, 0, 0, 0] - fn from_list(items: List[T]) -> Set[T] - fn add(self, item: T) -> Set[T] - fn remove(self, item: T) -> Set[T] - fn union(self, other: Set[T]) -> Set[T] - fn intersection(self, other: Set[T]) -> Set[T] - fn difference(self, other: Set[T]) -> Set[T] - fn to_list(self) -> List[T] -} -``` +### Trait style -#### `std.io` (platform I/O) +Shared operation names should be expressed as trait or inherent methods rather +than type-prefixed helper functions. Receiver type and trait resolution select +the correct operation. -`std.io` is a future standardization layer. Manifest-backed projects import Platform package modules directly. For `basic-cli`, the platform modules are: +### Inline bounds -| Module | Example operations | Required effects | -| ------------------ | ----------------------------------------------------- | ----------------------- | -| `basic_cli.stdout` | `print`, `println`, `eprint`, `eprintln` | `Console` | -| `basic_cli.stdin` | `read_line` | `Console` | -| `basic_cli.file` | `file_read`, `file_write`, `file_exists`, `file_stat` | `FileRead`, `FileWrite` | -| `basic_cli.dir` | `dir_list`, `dir_mkdir` | `FileRead`, `FileWrite` | -| `basic_cli.env` | `env_get`, `env_set` | `Env` | -| `basic_cli.cmd` | `process_run`, `process_run_status`, `exit` | `Spawn`, `Exit` | +All standard-library generic bounds are written in type parameter lists: ```spore -pub foreign fn process_run(cmd: Str, args: List[Str]) -> Str ! ExecError uses [Spawn] -pub foreign fn process_run_status(cmd: Str, args: List[Str]) -> Option[U8] ! ExecError uses [Spawn] -pub foreign fn exit(code: U8) -> Never uses [Exit] +fn to_debug_string[T: Debug](value: T) -> Str; +fn contains[T: Eq](xs: List[T], value: T) -> Bool; ``` -`basic_cli.cmd.exit(code)` is the explicit process-termination -surface. In project mode it works together with SEP-0008's startup contract -(`main() -> ()`), and the runtime converts the resulting structured outcome into -a host exit status. +### Effects -A future `std.io` module may standardize a portable wrapper above these Platform -packages. Until that lands, signatures in this section should be read as future -design sketches rather than as the current contract. - -#### `std.time` (platform-provided) - -```spore -type DateTime // Opaque timestamp -type Duration // Time interval - -foreign fn now() -> DateTime - uses [Clock] - cost [1, 0, 1, 0] - -foreign fn elapsed(start: DateTime) -> Duration - uses [Clock] - cost [1, 0, 1, 0] - -fn duration_ms(d: Duration) -> I64 - cost [1, 0, 0, 0] - -fn duration_secs(d: Duration) -> F64 - cost [1, 0, 0, 0] - -foreign fn sleep(ms: I64) -> Unit - uses [Clock] - cost @unbounded -``` - -#### `std.json` +Platform functions declare effects through `uses`: ```spore -type JsonValue { - JsonNull, - JsonBool(Bool), - JsonInt(I64), - JsonFloat(F64), - JsonString(Str), - JsonArray(List[JsonValue]), - JsonObject(Map[Str, JsonValue]), -} - -fn json_parse(s: Str) -> Result[JsonValue, ParseError] - -fn json_to_string[T](value: T) -> Str - where T: Serialize - -fn json_from_string[T](s: Str) -> Result[T, ParseError] - where T: Deserialize +@foreign +fn read_file(path: Path) -> Str ! IoError +uses [FileRead]; ``` -These JSON APIs operate on dynamic `Str` and dynamic serialized values, so this -SEP does not publish verified `cost [...]` clauses for them. Indexed wrappers -may be added later if a platform needs static payload-size budgets. +### State primitive effects -#### `std.random` (platform-provided) +SEP-0003 defines membership rules for state primitive effects. The standard +library defines the initial primitive surface by semantic contract. Method names +below are initial spellings for the surface and may be refined while the SEP is +Draft. ```spore -foreign fn random_float() -> F64 - uses [Random] - cost [1, 0, 1, 0] - -foreign fn random_int(min: I64, max: I64) -> I64 - uses [Random] - cost [1, 0, 1, 0] - -foreign fn uuid() -> Str - uses [Random] - cost [1, 16, 1, 0] - -fn random_shuffle[A](list: List[A]) -> List[A] - uses [Random] - -fn random_sample[A](list: List[A]) -> Option[A] - uses [Random] - cost [1, 0, 1, 0] -``` - -### 4.4 Error type hierarchy - -All stdlib errors conform to a base trait: - -```spore -trait Error[T] { - fn message(self: T) -> Str +effect Cell[T] { + fn get() -> T; + fn set(value: T) -> (); } -``` - -The standard error types: - -```spore -type PanicError { Panic(Str) } -type IoError { - NotFound(Str), - PermissionDenied(Str), - AlreadyExists(Str), - Other(Str), +effect Output[T] { + fn emit(value: T) -> (); } -type HttpError { - ConnectionFailed(Str), - Timeout(Str), - StatusError(U16, Str), - Other(Str), +effect Map[K, V] { + fn get(key: K) -> Option[V]; + fn put(key: K, value: V) -> (); + fn remove(key: K) -> (); } -type ParseError { - InvalidFormat(Str), - UnexpectedToken(Str, I64), - Other(Str), +effect Clock { + fn now() -> Instant; } -type JsonError { - InvalidJson(Str, I64), - TypeMismatch(Str), - MissingField(Str), +effect Random { + fn next_u64() -> U64; + fn next_f64() -> F64; } ``` -All error types implement `Error`, `Display`, and `Debug`. - -### 4.5 Compiler-known traits - -These 13 traits have special compiler support (SEP-0002). The stdlib provides their definitions: - -| Trait | Methods | Derivable | Description | -| ------------- | ---------------------------------------------------------- | --------- | ------------------------- | -| `Eq` | `eq(self, other) -> Bool` | ✅ | Equality comparison | -| `Ord` | `compare(self, other) -> Ordering` | ✅ | Total ordering | -| `Clone` | `clone(self) -> Self` | ✅ | Deep copy | -| `Display` | `display(self) -> Str` | ❌ | Human-readable formatting | -| `Debug` | `debug(self) -> Str` | ✅ | Debug formatting | -| `Hash` | `hash(self) -> U64` | ✅ | Hash computation | -| `Default` | `default() -> Self` | ✅ | Default value | -| `Serialize` | `serialize(self) -> List[U8]` | ✅ | Byte serialization | -| `Deserialize` | `deserialize(bytes: List[U8]) -> Result[Self, ParseError]` | ✅ | Byte deserialization | -| `Add` | `add(self, other) -> Self` | ❌ | `+` operator | -| `Sub` | `sub(self, other) -> Self` | ❌ | `-` operator | -| `Mul` | `mul(self, other) -> Self` | ❌ | `*` operator | -| `Div` | `div(self, other) -> Self` | ❌ | `/` operator | - -### 4.6 Platform binding architecture - -The platform-backed I/O stack looks like this: - -```text -┌──────────────────────────────────────────────┐ -│ User Code │ -│ fn main() -> () uses [Console, Exit] { │ -│ println("done") │ -│ exit(0u8) │ -│ } │ -├──────────────────────────────────────────────┤ -│ Platform package modules │ -│ basic_cli.stdout / basic_cli.stdin / │ -│ basic_cli.file / basic_cli.env / │ -│ basic_cli.cmd │ -├──────────────────────────────────────────────┤ -│ Selected Platform contract + metadata │ -│ [project].platform = "basic-cli" │ -│ startup-contract = "main" │ -│ adapter-function = "main_for_host" │ -│ handled-effects = [..., "Exit"] │ -├──────────────────────────────────────────────┤ -│ Native Runtime (Rust/C/host embedding) │ -└──────────────────────────────────────────────┘ -``` - -Key properties: +These effects are the standard state and event endpoints used by handler-local +mocking and test instrumentation. Their interfaces are standard-library surface; +their handlers are supplied by the selected Platform package or by explicit +local handlers. -1. **User code** imports Platform package modules directly today. -2. **Each foreign surface** declares explicit effect requirements. -3. **The selected Platform package** owns startup contracts and handled-effect metadata. -4. **Some operations** may propagate structured runtime outcomes before host conversion (for example `Exit` in project mode). -5. **Future stdlib wrappers** may layer above this. +Derived effects such as `Counter`, `Cache`, `Logger`, and `Tracer` do not belong +to the primitive set because they can be expressed over `Cell`, `Map`, or +`Output` plus domain policy. Platform I/O effects such as `FileRead` and +`FileWrite` and concurrency effects such as `Spawn` remain owned by their +respective Platform or concurrency SEPs. -### 4.7 Cost expression inputs +### Budgets -The cost annotation language (SEP-0004) does not call ordinary stdlib functions -inside `cost [...]`. Verified costs mention only Index parameters and -`cost(f)` summaries: - -| Source | Example | Notes | -| ----------------- | ---------------------------------------- | ------------------------------------------------- | -| Index parameter | `N: Index` | Compile-time non-negative size symbol | -| Indexed count | `Count[N]` | Runtime count whose static Index is `N` | -| Indexed container | `Array[T, N]`, `Vec[T, max: N]` | Exposes `N` to CostExpr | -| Index operation | `max(N, M)`, `min(N, M)`, `span(Hi, Lo)` | Pure IndexExpr, not runtime function calls | -| Function summary | `cost(f)` | Substituted when `f` is concrete at the call site | - -Dynamic methods such as `list.len()`, `s.len()`, `map.len()`, or `set.len()` -return runtime integers. They are useful program APIs, but they are not -CostExpr variables. +Standard-library signatures omit routine budgets. A budget is written only when +reviewability or realization shape is part of the public contract. ## Human experience impact -The stdlib is designed for progressive discovery: - -1. **Prelude is small**: Only essential types and iteration primitives — no import ceremony for basic programs -2. **Module names are predictable**: imports like `std.math`, `std.string`, and `std.collections` follow standard conventions -3. **Cost annotations are readable**: four-slot forms like `cost [N * cost(f) + N, N, 0, 0]` stay copy-pasteable -4. **Error types are descriptive**: Enum variants like `NotFound(Str)` carry context -5. **No hidden effects**: Every I/O function explicitly declares its effect requirements +The library remains easy to scan. Properties document behavior where it matters, +without forcing every helper to carry extra annotation noise. ## Agent experience impact -Agents benefit from: - -1. **Complete enumeration**: All stdlib functions and methods are listed with signatures, enabling auto-completion -2. **Cost contracts**: Agents can reason about algorithmic complexity when filling holes -3. **Effect requirements**: Agents can verify that generated code satisfies effect constraints -4. **Structured errors**: Error type hierarchy enables pattern-matching on failure modes -5. **JSON protocol**: `std.json` provides the serialization layer for hole protocol (SEP-0005) +Agents can rely on stable trait names, inline bounds, and standard properties +when selecting candidates for hole realization. ## Structured representation / protocol impact -The stdlib is represented in the module system as: +Standard-library metadata should publish: -```json -{ - "module": "std.prelude", - "methods": [ - { - "owner": "Vec[A, max: N]", - "trait": "Mappable", - "name": "map", - "type_params": ["A", "B", "N: Index"], - "params": [ - { "name": "self", "type": "Vec[A, max: N]" }, - { "name": "f", "type": "(A) -> B" } - ], - "return_type": "Vec[B, max: N]", - "cost": "N * cost(f) + N", - "effects": [] - } - ], - "types": [ - { "name": "Option", "params": ["T"], "variants": ["Some(T)", "None"] } - ] -} +```text +StdItem +├── module +├── name +├── signature +├── properties[] +├── required_effects[] +└── evidence_policy ``` -This structured representation enables: - -- IDE auto-complete with full type information -- Agent enumeration of available functions -- Cost analysis tool integration -- Documentation generation - ## Diagnostics impact -New diagnostic codes for stdlib-related errors: +Standard-library diagnostics reuse core categories: -| Code | Category | Message | -| ------- | ---------- | -------------------------------------- | -| `E0018` | Type error | stdlib function applied to wrong type | -| `E0019` | Type error | missing trait bound for stdlib generic | -| `W0001` | Warning | unused stdlib import | -| `W0002` | Warning | deprecated stdlib function | +- `E0xxx` for missing trait bounds or type mismatch; +- `F0xxx` for effect misuse; +- `P0xxx` for failed standard properties; +- `M0xxx` for import or visibility issues. ## Drawbacks -1. **Larger language surface**: More functions to learn and document -2. **Stability commitment**: Once standardized, stdlib changes require backward compatibility -3. **Platform burden**: Every platform must implement all `foreign fn` declarations -4. **Cost annotation maintenance**: Cost contracts must be verified against implementations +Omitting routine budgets means the standard library is not a benchmark contract. +That is intentional: budgets shape realizations, while performance guidance +belongs in library documentation and evidence policies when needed. ## Alternatives considered -### Minimal stdlib (only primitives) +### Type-prefixed helper names -Rejected. Without at least `Option`, `Result`, and `List`, idiomatic Spore code requires third-party packages for basic operations, hurting ergonomics and fragmenting the ecosystem. +Rejected because shared trait and inherent methods keep APIs smaller and more +composable. -### Batteries-included stdlib (Python-style) +### Budgets on every function -Rejected. A large stdlib creates maintenance burden and version coupling. Spore's content-addressed package system (SEP-0008) makes third-party packages cheap to depend on. +Rejected because it creates annotation noise and falsely suggests every API has +a public realization-shape contract. -### No foreign fn (pure Spore stdlib) +### Platform operations in the prelude -Rejected. I/O operations cannot be expressed in pure Spore. The designed -mechanism is Platform-owned package modules plus the selected Platform contract -boundary (SEP-0008). +Rejected because effects should remain explicit through selected Platform +packages. ## Prior art -| Language | Stdlib approach | Comparison | -| ----------- | ----------------------- | ------------------------------------------------------------ | -| **Rust** | `std` + `core` (no-std) | Similar layered approach; Spore's is smaller | -| **Roc** | Platform-provided I/O | Direct inspiration for Spore's platform model | -| **Haskell** | `Prelude` + `base` | Similar prelude concept; Spore avoids Haskell's large `base` | -| **Go** | Batteries-included | Larger than Spore's approach | -| **Elm** | Small core + packages | Similar philosophy; Spore adds cost annotations | +Rust influenced trait-based shared operation names. Elm and Roc influenced small +standard surfaces and platform separation. ML-family libraries influenced +compact algebraic data APIs. ## Backward compatibility and migration -This is a new specification — no backward compatibility concerns. However: - -- Future changes to prelude types must go through the SEP process -- Deprecation of stdlib functions requires at least one release cycle with warnings -- New stdlib modules can be added without breaking changes +Standard-library specs should migrate to inline bounds and properties. Routine +resource annotations should be removed unless they describe an intentional +realization-shape bound. ## Unresolved questions -1. **Mutable state API**: Should `Ref[T]` be in the prelude or `std.state`? Currently referenced in SEP-0001 but not fully specified here. - -2. **Concurrency primitives**: `Task[T]`, `Channel[T]`, and `select` are defined in SEP-0007 — should they be re-exported via `std.concurrent` or remain as language primitives? - -3. **String encoding**: Should `Str` expose byte-level access, or only scalar-oriented indexing? Current spec assumes UTF-8; there is no `Char` type (length-1 `Str` values instead). - -4. **Iterator protocol**: Should there be a lazy `Iterator[T]` trait instead of materializing `List[T]` for all operations? This would change cost signatures significantly. - -5. **Error recovery**: How should `PanicError` interact with the effect system? Should `panic` require an effect? - -6. **FFI type mapping**: How do Spore types map to Rust/C types across the FFI boundary? (e.g., `I64` ↔ `i64`, `Str` ↔ UTF-8 buffer) - -### Resolved questions - -1. **Numeric tower**: Resolved by SEP-0002 and SEP-0001. The language surface - uses fixed-width numeric types (`I8` through `U64`, plus `F32` and `F64`), - and stdlib APIs should choose explicit widths at each boundary rather than - relying on abstract scalar aliases. +1. Which standard properties are required for publication? +2. Should standard property suites be generated into package evidence records? +3. Which Platform packages should be distributed alongside the standard library set without creating a default Platform? diff --git a/seps/SEP-0010-compiler-as-documentation.md b/seps/SEP-0010-compiler-as-documentation.md new file mode 100644 index 0000000..f340190 --- /dev/null +++ b/seps/SEP-0010-compiler-as-documentation.md @@ -0,0 +1,374 @@ +--- +sep: 10 +title: "SEP-0010: Compiler-as-Documentation & Explain Protocol" +status: Draft +type: Standards Track +authors: + - Zhan Rongrui +created: 2026-05-26 +requires: + - 1 + - 5 + - 6 +discussion: "https://github.com/spore-lang/spore-evolution/discussions/46" +pr: null +superseded_by: null +--- + +# SEP-0010: Compiler-as-Documentation & Explain Protocol + +> **Executive Summary**: Defines compiler-as-documentation as a first-class Spore tooling contract. The compiler exposes concept documentation, diagnostic teaching metadata, and explain queries from the same structured facts that produce diagnostics and HoleReports. This keeps terminal feedback, JSON output, LSP surfaces, and Agent input aligned without making external tutorials the source of truth. + +## Summary + +Spore tooling exposes language concepts through an explain protocol: + +```bash +spore explain properties +spore explain holes +spore explain uses +spore explain budget +spore explain fn +spore explain I64 +spore explain E0301 +spore explain --json properties +``` + +Each query resolves to a `ConceptDoc` from a `ConceptRegistry`. Diagnostics and +human-readable HoleReports link back to these concepts through stable concept +references, while default terminal diagnostics stay concise. + +Compiler-as-documentation means that the compiler owns the facts needed to teach +the language at the point of use. External prose can reorganize those facts, but +it is not the canonical source for syntax, diagnostic meaning, or hole context. + +## Motivation + +Spore asks users to read signatures, properties, budgets, effects, holes, and +evidence as one semantic path. If those ideas are only explained outside the +compiler, users must leave the feedback loop exactly when they are most likely +to need guidance. + +Diagnostics are already structured by SEP-0006, and HoleReports are already +self-contained by SEP-0005. SEP-0010 adds the teaching layer over those records: +short repair guidance in ordinary errors, richer explanations on demand, and +stable JSON for tools and Agents. + +This reduces cognitive load without turning every diagnostic into a tutorial. +The compiler says what is wrong, shows the local repair shape, and points to the +concept node that explains why the rule exists. + +## Guide-level explanation + +### Explaining a concept + +`spore explain properties` renders a compact concept page: + +```text +properties + summary: Validity rules attached to an intent signature. + + syntax: + properties { name(params): expr } + + why: + Properties make intended behavior explicit so realizations, reviews, + Agents, and evidence all share the same checkable obligations. + + minimal Spore: + fn sort(xs: List[I64]) -> List[I64] + properties { + ordered(xs: List[I64]): is_ordered(sort(xs)) + } + { + ?sort_body + } + + diagnostics: P0xxx, parse-error + see also: holes, evidence, intent-signature +``` + +The same query with `--json` returns the underlying `ConceptDoc`. + +### Explaining a diagnostic + +When a user writes a malformed property item, the terminal output should be +direct and local: + +```text +error[parse-error]: property item is missing its parameter list + --> src/main.sp:5:5 + | + 5 | ordered: is_ordered(sort(xs)) + | ^^^^^^^ write `ordered(xs: List[I64]): ...` + | + = learn: spore explain properties +``` + +`spore explain parse-error` may describe parser diagnostics as a family, while +`spore explain properties` explains the grammar and intent model behind the +repair. + +### Explaining a hole + +HoleReports remain the structured records specified by SEP-0005. The +compiler-as-documentation layer defines their human teaching projection: + +```text +hole[sort_body] + expected type: List[I64] + + known bindings: + xs: List[I64] + + possible constructions: + - xs + - xs.sorted() + - ? + + property context: + ordered(xs: List[I64]): is_ordered(sort(xs)) + + learn: spore explain holes +``` + +The projection highlights the missing shape, the visible facts, candidate +construction paths, and the obligations a realization must preserve. + +## Reference-level explanation + +### Query resolution + +An explain query is resolved in this order: + +1. exact diagnostic code, such as `E0301`; +2. exact concept id, such as `properties`, `budget`, or `holes`; +3. surface symbol or prelude name, such as `fn`, `I64`, `Str`, or `Result`; +4. alias listed by a `ConceptDoc`. + +Ambiguous queries are diagnostics that list the matching concept ids. Unknown +queries are diagnostics that point to `spore explain --list`. + +### ConceptDoc fields + +Each concept entry includes: + +| Field | Meaning | +| ------------------ | ----------------------------------------------------- | +| `id` | Stable concept id used by diagnostics and tools | +| `title` | Human-readable title | +| `summary` | One- or two-sentence concept summary | +| `syntax` | Surface forms or command forms, when applicable | +| `rationale` | Design intent and model explanation | +| `minimal_examples` | Small Spore snippets or CLI snippets | +| `diagnostics` | Related diagnostic codes or code families | +| `related_seps` | SEP numbers that own the normative design | +| `see_also` | Related concept ids | +| `aliases` | Additional accepted query strings | + +The `ConceptRegistry` contains the ordered set of `ConceptDoc` records plus +schema identity. The concrete contract can later be published under +`schemas/contracts`. + +For hole-related concept docs, `related_seps` should include SEP-0005 when the +concept explains HoleReport data and SEP-0010 when the concept explains the +teaching or query layer. + +### Diagnostic teaching metadata + +Every user-facing diagnostic should be able to carry: + +| Field | Meaning | +| ----------------- | ------------------------------------------------------ | +| `concept_refs` | Concept ids that explain the violated rule | +| `repair` | Local repair hint or replacement shape, when available | +| `explanation_key` | Stable key for selecting a short explanation variant | + +The terminal renderer may use these fields to print one concise `learn:` line. +JSON, watch mode, and LSP projections should preserve the fields so tools do +not scrape prose. + +### Hole teaching projection + +The educational projection of a HoleReport is a rendering over SEP-0005 fields. +It should include expected type, source location, visible bindings, effect +context, budget context, property context, candidates, dependent holes, and +rejection reasons when those fields are present. SEP-0010 may choose which fields +to highlight for a user-facing explanation, but it does not define different +field names or a second hole query payload. + +When SEP-0005 and SEP-0010 appear to overlap, SEP-0005 owns the data contract +and SEP-0010 owns the teaching projection over that contract. + +## Human experience impact + +This makes intent clearer by putting the relevant concept next to the error or +hole that revealed it. Users can learn the difference between Base Signature +syntax, Intent Signature clauses, properties, effects, budgets, holes, and +evidence in the compiler loop. + +The base-vs-intent signature distinction is preserved because concept docs are +attached to the layer they explain. A malformed parameter list points to Base +Signature syntax, while malformed `uses`, `budget`, or `properties` clauses +point to Intent Signature concepts. + +Diagnostics, repair, and review workflows improve because terminal output gives +the local fix while `spore explain` carries the design model. Reviewers can ask +for the same concept id that a diagnostic references instead of debating prose +from a separate guide. + +## Agent experience impact + +Stable concept ids reduce ambiguity for Agents. An Agent that sees +`concept_refs: ["properties"]` can fetch the same structured explanation a +human sees and does not need to infer teaching context from text. + +Explain JSON is exported in a stable, machine-readable form. It allows Agents +to ground hole realization without extra conversation: expected types come from +HoleReport, obligations come from property context, and concept docs explain the +rules that make those obligations meaningful. + +SEP-0010 preserves the semantic path `Signature -> Property -> Hole -> +Realization -> Evidence` by treating documentation as metadata over compiler +records. ConceptDoc changes do not alter program identity; evidence remains tied +to the signatures, properties, realizations, checkers, and dependencies that +SEP-0006 defines. + +## Structured representation / protocol impact + +The concept registry schema defines this top-level shape: + +```text +ConceptRegistry +├── version +├── schema +├── schema_catalog +└── concepts[] +``` + +Each `concepts[]` item is a `ConceptDoc`: + +```text +ConceptDoc +├── id +├── title +├── summary +├── syntax[] +├── rationale[] +├── minimal_examples[] +├── diagnostics[] +├── related_seps[] +├── see_also[] +└── aliases[] +``` + +`spore explain --json ` returns either a single `ConceptDoc`, a diagnostic +explanation record with concept references, or an ambiguity diagnostic. Tools +may cache registry records by schema identity and concept id. + +Diagnostic JSON gains optional teaching fields: + +```json +{ + "code": "P0101", + "severity": "error", + "message": "property item is missing its parameter list", + "concept_refs": ["properties"], + "repair": { + "message": "write a property name, parameter list, colon, and Bool expression", + "replacement": "ordered(xs: List[I64]): is_ordered(sort(xs))" + }, + "explanation_key": "properties.item-shape" +} +``` + +Unknown JSON fields remain ignorable for consumers that only need the older +diagnostic shape. + +## Diagnostics impact + +Diagnostics keep the SEP-0006 code families. SEP-0010 adds a teaching layer over +those codes: + +- primary message: what failed at the exact source span; +- repair hint: the smallest local shape that can move the user forward; +- concept reference: the `spore explain` query that teaches the rule. + +Every user-facing diagnostic produced by the compiler should have a path to at +least one concept id, either directly or through its diagnostic family. +Diagnostic families may point to broad concepts, while more specific codes may +point to a subconcept through `explanation_key`. + +The default text renderer should stay concise. Long rationale, multiple +snippets, prior art, and cross-topic exploration belong in `spore explain`. + +## Drawbacks + +Concept docs add authoring work to compiler and tooling changes. The mitigation +is that concept entries are small, structured, and tested with the same snippets +that users see. + +There is a risk of noisy diagnostics if every error tries to teach the full +language model. SEP-0010 keeps normal diagnostics brief and moves the deeper +material behind explicit explain queries. + +The registry adds another machine contract to maintain. The benefit is that +humans, Agents, LSP, watch mode, and terminal output share one explanation +source. + +## Alternatives considered + +### External manual only + +Rejected because the compiler would keep detecting precise context while the +teaching surface lived elsewhere. + +### Text-only explain output + +Rejected because Agents, LSP clients, and tests need stable fields instead of +terminal prose. + +### Long diagnostics by default + +Rejected because users need fast repair loops. Detailed rationale belongs behind +`spore explain`. + +### Rust attribute syntax as the standard + +Rejected because a Rust macro or attribute can be a convenient producer, but the +standard should define the registry and diagnostic contract that all producers +emit. + +## Prior art + +Elm influenced human-centered error messages with repair guidance. + +Rust influenced diagnostic codes and `rustc --explain`. + +Agda, Idris, GHC, and Lean influenced typed holes that show local context. + +Language Server Protocol features such as code descriptions and hover text +influenced the projection of concept docs into editor tools. + +## Backward compatibility and migration + +No Spore source syntax changes are required. Existing diagnostic codes remain +valid, and explain metadata is added as optional data over the existing +Diagnostic record. + +JSON consumers should ignore unknown fields. Tools that want compiler-owned +documentation can begin reading `concept_refs`, `repair`, and +`explanation_key`. + +`sporec explain ` can remain a low-level diagnostic query. The project CLI +adds `spore explain ` as the user-facing entry point for concepts, +diagnostic codes, and surface symbols. + +## Unresolved questions + +1. Should concept docs be localized by compiler output, by renderer packages, or + by external translation catalogs? +2. Should standard-library modules publish their user-facing docs through the + same concept registry shape? +3. Should interactive tutorial flows become a separate SEP after explain, + diagnostics, and hole projections settle? diff --git a/templates/informational.md b/templates/informational.md index 9ee89f2..f7c12ca 100644 --- a/templates/informational.md +++ b/templates/informational.md @@ -26,4 +26,10 @@ superseded_by: null ## Implications for Spore +Apply the [guiding questions for every design decision](../seps/SEP-0000-process.md#guiding-questions-for-every-design-decision) +from SEP-0000 to whatever follow-on normative work this Informational SEP could +motivate. Call out any question whose answer is likely to be unresolved or +contentious if the ideas described here are later promoted into a Standards +Track or Process SEP. Do not add a separate guiding-questions section. + ## Unresolved questions or future directions diff --git a/templates/process.md b/templates/process.md index 4ad0551..9cfff2b 100644 --- a/templates/process.md +++ b/templates/process.md @@ -26,6 +26,14 @@ superseded_by: null ## Proposal +The [guiding questions for every design decision](../seps/SEP-0000-process.md#guiding-questions-for-every-design-decision) +live in SEP-0000. Do **not** create a `## Guiding questions for every design +decision` heading in this SEP; link to the SEP-0000 heading instead. + +If this Process SEP changes the review criteria, the question set, or the +relationship between vision principles and questions, amend SEP-0000 directly +in the same proposal. + ## Lifecycle and transition rules ## Roles and responsibilities diff --git a/templates/standards-track.md b/templates/standards-track.md index 95698c3..e6aacc6 100644 --- a/templates/standards-track.md +++ b/templates/standards-track.md @@ -26,8 +26,24 @@ superseded_by: null ## Human experience impact +Address the [guiding questions for every design decision](../seps/SEP-0000-process.md#guiding-questions-for-every-design-decision) +from SEP-0000 from the human reader's perspective. Focus on the prompts about +intent clarity, the base-vs-intent signature distinction, and diagnostics, +repair, and review workflows. State explicitly which questions do not apply and +why. + ## Agent experience impact +Address the [guiding questions for every design decision](../seps/SEP-0000-process.md#guiding-questions-for-every-design-decision) +from SEP-0000 from the Agent's perspective. Focus on the prompts about Agent +ambiguity, stable machine-readable export, hole realization without extra +conversation, evidence generation, content-hash invalidation, and preserving +the semantic path `Signature -> Property -> Hole -> Realization -> Evidence`. +State explicitly which questions do not apply and why. + +Do not add a separate guiding-questions section; keep the answers in these +impact sections and related protocol or diagnostics sections. + ## Structured representation / protocol impact ## Diagnostics impact