Skip to content

feat(match): list patterns [head, ...tail] — the last production primitive#146

Merged
MelbourneDeveloper merged 7 commits into
mainfrom
feature/list-patterns
Jun 23, 2026
Merged

feat(match): list patterns [head, ...tail] — the last production primitive#146
MelbourneDeveloper merged 7 commits into
mainfrom
feature/list-patterns

Conversation

@MelbourneDeveloper

@MelbourneDeveloper MelbourneDeveloper commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Lands list patterns ([] / [a, b] / [head, ...tail] / [a, b, ...rest]) in match — the last unshipped production-app primitive — and fixes the dev Language Server, which never launched because its configured compiler path pointed at the C-runtime archive directory instead of the built binary (so hover and every LSP feature were dead).

Details

Feature — list patterns ([TYPE-LIST-PATTERNS])

  • Grammar: new list_pattern rule; scalar literals are split out of $.literal so [head, tail] can never collide with a two-element list literal. The index [ is now token.immediate, so a match-arm body (=> 0) no longer swallows the next arm's [...] as a postfix index.
  • AST: Pattern::List { elements, rest }.
  • Lowering (osprey-syntax), HM inference (osprey-types: unifies the scrutinee with List<E>, binds each element to E and ...rest to List<E>), free-variable analysis (osprey-codegen/freevars), and match codegen (osprey-codegen/pattern: length-guard ==/>=, then osprey_list_get / osprey_list_drop). Built over the pre-existing osprey_list_drop runtime.
  • Examples: lists/list_basics.osp gains a section covering all four forms + a recursive sumList (Result-unwrapping the overflow-checked +). Two failscompilation cases reject mid-list rest ([a, ...m, b]) and double rest ([...a, ...b]).
  • Documented limit: deep non-tail recursion is stack-bounded (no TCO yet) — fine for bounded recursive descent (JSON nesting); the examples stay shallow.

Fix — dev Language Server never launched (hover/def/refs/etc. all dead)

  • Root cause: .vscode/settings.json set osprey.server.compilerPath to …/compiler/bin/osprey. compiler/bin is the gitignored C-runtime archive directory (lib*_runtime.a), not a binary — so the client spawned a nonexistent file and died with ENOENT, leaving the LSP in startFailed. The compiler that make build produces is target/release/osprey. Repointed the setting there and corrected the explanatory comment block (//4//8, now noting compiler/bin is the C-runtime archive dir, NOT the binary).
  • Hardening (vscode-extension/client/src/extension.ts): new looksLikePath(); resolveServerCommand() now detects a configured path that looks like a path but doesn't exist on disk, emits a showWarningMessage + output-channel warning, and falls back to the bundled compiler then bare osprey on PATH instead of silently spawning a dead path. makeClientFailureHandling() centralises the client's initializationFailedHandler/errorHandler so a failed start is logged and surfaced rather than swallowed.

Fix — editor Run/Compile ran a stale compiler; VSIX now self-contained

  • The editor's Run/Compile commands previously execFile'd a bare osprey, which resolved to whatever stale global build sat on PATH (an old ANTLR-era binary that rejects [head, ...tail] with no viable alternative at input). Both now route through resolveServerCommand(context), so Run/Compile use the same version-matched compiler as the LSP.
  • make _vsix_bundle now stages target/release/osprey plus the libfiber_runtime.a / libhttp_runtime.a C-runtime archives into bin/<platform>/, and .vscodeignore re-includes !bin/**. find_runtime_lib searches for the archives beside the executable, so the bundled compiler can link --run from inside the installed extension — proven end-to-end by running the bundled ~/.vscode/extensions/nimblesite.osprey-*/bin/darwin-arm64/osprey from a neutral cwd against the golden output. make rebuild-install-vsix rebuilds all binaries (make build → C archives + release compiler + extension compile) before packaging.

Coverage & tests

  • vscode-extension gate raised to 89% in coverage-thresholds.json (measured 89.92%, 375/417 lines). The earlier toothless integration tests (if (result) assert; else log) passed even with hover broken — exactly how this bug shipped — so they were replaced with assertion-dense live-LSP tests run against the real built compiler.
  • Rust coverage is now gated per crate (floor 95%; osprey-ast 96, osprey-cli 95, osprey-codegen 95, osprey-lsp 97, osprey-syntax 95, osprey-types 98, osprey-runtime-sys 99) — make _coverage_check_rust enforces every crate individually, backed by new unit tests across osprey-codegen, osprey-syntax and osprey-types.

Cleanupdocs/plans/ removed (completed/superseded design docs, recoverable from git history); the four spec links that pointed at it were repointed.

How Do The Automated Tests Prove It Works?

List patterns

  • Differential golden suite (zsh crates/diff_examples.sh): PASS=47 FAIL=0 NOEXP=0, FC_OK. list_basics.osp byte-matches its .expectedoutputclassify over []/[10,20]/[1,2,3]/[7] yields empty/two(10,20)/many head=1 rest=2/one(7); sumList yields 6 and 55; restLen yields 1/0. list_pattern_middle_rest and list_pattern_double_rest are rejected (nonzero exit).
  • examples_compile::every_tested_example_compiles_to_ir and ::list_pattern_negative_cases_are_rejected (osprey-cli).
  • freevars::list_pattern_binds_prefix_and_rest_not_free proves head/tail/rest are bound, not captured by an enclosing closure.
  • cargo test --workspace green; make _coverage_check_rust passes every per-crate gate (osprey-ast 97.1, osprey-cli 96.0, osprey-codegen 95.6, osprey-lsp 99.0, osprey-runtime-sys 100, osprey-syntax 95.8, osprey-types 99.0).

LSP + editor fixvscode-extension suite (43 passing locally and in CI under xvfb), exercising the real language server against the freshly-built target/release/osprey:

  • CHUNKY: full language-intelligence sweep over one program — over a single 10-line program asserts hover (fn area(r) -> Unit at decl and call site, builtin listLength -> int/List, print -> Unit), go-to-definition (area→line 1, perimeter→line 2), references (area 3× on lines [1,4,5], perimeter 2× on [2,6]), 10 document symbols with correct kinds (Shape=Class, area/perimeter=Function, radius=Variable), signature help (1 param, activeParameter 0), and completion (declared names + fn/let/match/type).
  • CHUNKY: diagnostics lifecycle — broken source surfaces an Error diagnostic (source osprey, /syntax/i message, range), editing to valid clears it and hover returns, re-breaking re-raises it.
  • CHUNKY: list-pattern program and CHUNKY: hover + symbols work on the ACTUAL reported list_basics.osp file — hover/def/refs/symbols/completion over the exact classify match in the file from the original bug report.
  • Resolution unit tests: resolveServerCommand falls back and warns when the configured path is missing, keeps a bare command name without touching the filesystem, an existing configured path is still returned verbatim and never warns, looksLikePath distinguishes filesystem paths from bare commands.
  • Failure-handler unit tests: initializationFailedHandler logs, surfaces an error, and stops retrying; errorHandler.error/.closed; deactivate stops the running language client without throwing.
  • Regression guard (Committed Dev Settings Sanity): reads the committed .vscode/settings.json and asserts compilerPath is never the dead compiler/bin/osprey path, and that a checkout-local compilerPath resolves to a real, built binary ending in target/release/osprey.

CI — all 5 checks green on the head commit: Rust Compiler (fmt, clippy, test, differential), Test, Format, Build & Validate (lint + make test per-crate coverage + differential + vscode tests + build), Website E2E (Playwright), Windows Core Build & Smoke Test, Detect changed areas.

…imitive

Land list patterns in `match`, the final unshipped production-app primitive
(the blocker for an Osprey-native JSON parser). All four forms work:
`[]`, `[a, b]`, `[head, ...tail]`, `[a, b, ...rest]`, with `_` element
wildcards. Grammar + AST + lowering + Hindley-Milner inference + codegen,
built over the existing `osprey_list_drop` runtime. Implements
[TYPE-LIST-PATTERNS].

Fixes a real grammar ambiguity uncovered by the feature: a match-arm body
greedily swallowed the next arm's `[...]` as a postfix index. The index `[`
is now `token.immediate`, so `=> 0  [h, ...t] => …` parses as two arms.

Coverage push (example tests now count): an in-process `examples_compile`
harness compiles every tested example through parse→typecheck→codegen under
cargo-llvm-cov, plus assertion-loaded unit tests for cli/sandbox/freevars/
unify and the lspkit-based LSP server/wire/engine. crates 52% -> 92.9%
(threshold raised to 90). vscode-extension 52% -> ~81% (threshold 75).

Deslop duplication ratcheted 11.0% -> 9.5% (measured 9.20% after DRYing the
LSP test harness).

Removes docs/plans (completed + superseded design docs; recoverable from git
history) and repoints the four spec links that referenced them.
The vscode-extension test suite reached 90.00% line coverage (35 passing
tests covering activation, command handlers, the Shipwright version
handshake, and the binary-resolution helpers; the key unlock was forcing the
target `.osp` document active before driving the `osprey.compile`/`osprey.run`
commands, which `outputChannel.show()` otherwise steals). Gate set to 89
(measured − 1) to absorb macOS↔Linux line-coverage drift in the platform
branches, since `vscode-test` can't run locally to re-verify while VS Code is
open. Both project gates are now at the 90% target band (crates 92.9%/90,
vscode 90.0%/89).
…tched compiler

The Run/Compile commands shelled out to a bare `osprey` on PATH, which
resolved to a stale global install (v1.0.0) that predates list patterns —
so `[head, ...tail]` examples failed with parser errors in the editor even
though the freshly-built compiler handles them.

- Runner now resolves the compiler exactly as the language server does
  (user setting -> version-matched bundled binary -> PATH), so the VSIX
  runs against its own bundled `osprey`, never whatever is on PATH.
- `make _vsix_bundle` stages libfiber_runtime.a + libhttp_runtime.a beside
  the bundled compiler; find_runtime_lib locates archives next to the
  executable, so `--run` links without depending on /usr/local/lib.
- `.vscodeignore` explicitly ships bin/** so the binaries reach the VSIX.

Verified: bundled osprey runs list_basics.osp from a neutral cwd and
byte-matches the golden output (exit 0).
…sholds

The previous commit accidentally swept in an uncommitted per-crate rewrite
of _coverage_check_rust while its matching per-crate coverage-thresholds.json
was never committed. CI then ran per-crate logic against the single-key
`crates: 90` config, hunting for a crate literally named `crates`, finding no
lcov lines, and failing ("crates FAIL: no lines found in lcov.info").

Revert _coverage_check_rust to the aggregate gate that matches the committed
config (crates >= 90; measured 92.9%). The VSIX runtime-archive bundling from
the prior commit is unaffected. Per-crate gating can land later as its own
change, with the JSON and the raised per-crate coverage committed together.
The vscode-extension coverage gate (89%) regressed to 85.67% after the
resolveServerCommand hardening (looksLikePath + missing-path warn-fallback)
added branches that the committed tests never exercised.

- extension.test.ts: add unit tests for the new branches — missing configured
  path warns and falls back (the hover-killing ENOENT regression), bare PATH
  command returned verbatim, existing path returned verbatim, looksLikePath
  true/false; plus a Committed Dev Settings Sanity suite that locks
  .vscode/settings.json off the dead compiler/bin/osprey path.
- .vscode/settings.json: point osprey.server.compilerPath at the make-build
  output (target/release/osprey) instead of compiler/bin/osprey, which is the
  C-runtime archive dir and never holds the compiler binary. Required by the
  sanity suite above.
vscode-extension line coverage sat at 87.43% (< 89%): the language client's
initialization-failed and error/closed handlers, plus deactivate(), were never
exercised — they fire only on real LSP transport failures or shutdown, which
the integration suites can't deterministically induce.

Extract the three failure callbacks into the exported, injectable
makeClientFailureHandling (activate() spreads it into clientOptions — behaviour
unchanged) and unit-test each: init-failed logs+errors+returns false, error
Continues, closed Restarts. Add a deactivate test (runs last; stops the live
client started by earlier suites). Deterministic coverage of the failure paths,
no reliance on inducing a flaky LSP crash.
@MelbourneDeveloper MelbourneDeveloper merged commit 42c4650 into main Jun 23, 2026
5 checks passed
@MelbourneDeveloper MelbourneDeveloper deleted the feature/list-patterns branch June 23, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant