Skip to content

feat(codegen): higher-order calls through computed and field callees#148

Merged
MelbourneDeveloper merged 1 commit into
mainfrom
feature/higher-order-calls
Jun 24, 2026
Merged

feat(codegen): higher-order calls through computed and field callees#148
MelbourneDeveloper merged 1 commit into
mainfrom
feature/higher-order-calls

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TLDR

Calls and iterator callbacks whose callee is an arbitrary expression — a chained application add3(1)(2)(3), a function held in a record field cfg.processor, or any computed call result — now compile and dispatch through the closure cell instead of failing with unsupported construct.

Details

For an FP language this closes a core gap: only a plain-identifier callee (or the special maker(args)(args) shape) lowered before; everything else bailed (indirect / higher-order call, or iterator callback must be a function name or lambda). The dispatch machinery (closure::cell_call) already existed — the missing piece was recovering the callee's signature.

  • crates/osprey-codegen/src/builder.rs — new callee_fn_type(expr) recovers the function Type an expression evaluates to in callee/callback position: a named function or function-typed local, a chained call (peeling one arrow per application, so add3(1)(2)(3) resolves), or a function-valued record field (resolving the owner from the bound value's type tag, else a unique field-name match).
  • crates/osprey-codegen/src/expr.rsgen_call's non-identifier callee now recovers a FnSig via callee_fn_type and dispatches through call_fn_valuecell_call; it still fails loudly when the callee is not a function value.
  • crates/osprey-codegen/src/iter.rscallback_of gains a Callback::Value variant: a computed/field callback is evaluated once to a closure handle and called per element through its cell, so map/filter/fold/forEach accept makeAdder(10) and cfg.processor as callbacks.

Out of scope (separately tracked): the UFCS method-call spelling obj.field(args), which the parser desugars to field(obj, args); function_composition_test already documents this. The capability is reachable today via let p = obj.field then p(x), or as an iterator callback.

Closes plan 0001; removes it from docs/plans/.

How Do The Automated Tests Prove It Works?

  • New codegen unit test higher_order_calls_through_computed_and_field_callees compiles a program with all three forms (chained 3-deep call, makeAdder(10) map callback, cfg.keep filter callback) and asserts the IR contains a closure cell-call (to { i8* }*) — i.e. each higher-order callee loads a function pointer from a cell rather than erroring.
  • Extended golden compiler/examples/tested/basics/function_composition_test.osp adds chain3: ${tri3(1)(2)(3)} computed: ${...} fieldcb: ${...} and asserts chain3: 6 computed: 36 fieldcb: 23 byte-for-byte (range(1,4) mapped through makeAdder(10) folds to 36; range(8,13) filtered by the field predicate n > 10 folds to 23).
  • Differential harness PASS=47 FAIL=0 NOEXP=0, FC_OK — the change is purely additive (it only fires where codegen previously bailed), so no existing example regresses.
  • Coverage holds: osprey-codegen 95.6% ≥ 95%. Full-workspace cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, and cargo test --workspace are clean.

Calls and iterator callbacks whose callee is an arbitrary expression — a
chained application (`add3(1)(2)(3)`), a function held in a record field
(`cfg.processor`), or any computed call result — previously failed loudly.
They now recover the callee's signature from the type table and dispatch
through the existing closure cell:

- builder.rs: `callee_fn_type` resolves the function `Type` of a callee
  expression (identifier / chained call / record-field access).
- expr.rs: the non-identifier callee in `gen_call` dispatches via the
  recovered `FnSig` instead of bailing; still fails loudly for non-functions.
- iter.rs: `callback_of` evaluates a computed/field callback once to a closure
  handle (`Callback::Value`) and calls it per element through its cell.

Closes plan 0001. Extends function_composition_test with a chained 3-deep
call, a computed iterator callback, and a record-field filter callback; adds
a codegen unit test for the three paths.
@MelbourneDeveloper MelbourneDeveloper merged commit 523a81e into main Jun 24, 2026
5 checks passed
@MelbourneDeveloper MelbourneDeveloper deleted the feature/higher-order-calls branch June 24, 2026 08:33
MelbourneDeveloper added a commit that referenced this pull request Jun 25, 2026
…ator, handler-owned effect state + resume front-end (#149)

## TLDR

Adds editor **variable hover** (declared/inferred type + `///` docs), a
single-source-of-truth **built-in function docs generator**,
**handler-owned mutable state** for algebraic effects, and the
parse/type **front-end for explicit `resume`** — all behind a green
`make ci`.

## Details

**LSP / editor — variable hover (`[LSP-HOVER]`)**
- `collect_all_symbols` now deep-walks every expression body (blocks,
match arms, lambdas, call args, string interpolations, handler `in {…}`
blocks), so a `let`/`mut` nested anywhere is discoverable — fixing "no
hover bubble on a variable inside an HTTP server block".
- Hover shows the declared type, or the **inferred** type for
unannotated bindings, via position-keyed `ProgramTypes.lets` /
`let_type` (new `crates/osprey-types/src/info.rs` + `check.rs`).
- Variables are documentable with `///` like functions: grammar
`doc_comment` on `let`, captured through syntax lowering into the AST
`doc` field, rendered as hover prose. The binding position anchors on
the name/keyword field, so a leading `///` never shifts it.
- lspkit is reused, not reimplemented; the one missing text primitive is
a documented shim in `text.rs` filed upstream as `Nimblesite/lspkit#2`.
Spec `0020` gains `[LSP-HOVER]`, `[LSP-HOVER-VARIABLES]`,
`[LSP-HOVER-DOCS]`, `[LSP-REUSE-LSPKIT]`.

**Built-in docs generator — `osprey --docs --docs-dir <dir>`**
- New `crates/osprey-cli/src/docs.rs` +
`crates/osprey-types/src/builtin_docs{,_lang,_sys}.rs`: the compiler's
type schemes are the single source of truth — one page per built-in plus
an index, with stale pages pruned. Regenerates 90
`website/src/docs/functions/*` pages so the website and editor hover
show identical signatures/types.

**Algebraic effects**
- **Handler-owned state (`[EFFECTS-HANDLER-STATE]`)**: handler arms
read/write a `mut` captured from the enclosing scope (promoted to a
shared heap cell; per-region env carried on the handler stack). Makes
the `State` effect fully usable. Runtime in `effects_runtime.c`; codegen
in `effects.rs` / `lower.rs` / `expr.rs`. Reference example
`compiler/examples/tested/effects/http_state_levels.osp`.
- **Explicit `resume` front-end**: grammar / AST / syntax-lowering +
Hindley-Milner typing (`resume` typed against the operation result;
rejected outside a handler arm). The continuation **runtime/codegen is
not yet wired** — `resume` parses and type-checks but is currently
rejected at code generation; spec `0017` and plan `0008` mark it
specified-but-not-yet-executable.

**Examples / runtime**
- `compiler/examples/statefulhttp/{server,tui}.osp` + README: a stateful
HTTP server and a terminal UI. Terminal/raw-mode additions in
`system_runtime.c`; HTTP handler return-type alignment in
`http_server_runtime.c`.

**VS Code extension**
- Debug-config synthesis extracted and unit-tested; server-command
resolution hardening; test harness/runner updates;
FEATURES/README/TESTING docs refreshed.

> Scope note: this branch is named `feature/higher-order-calls`, but
that feature already shipped in the merged PR #148. The changes here are
the **post-#148** work that accumulated on the branch (the four areas
above); they are unrelated to higher-order calls.

## How Do The Automated Tests Prove It Works?

- **Variable hover (Rust, `crates/osprey-lsp`)**:
`hover_on_a_let_binding_uses_the_name_and_type_form` (asserts `limit:
int`), `hover_on_a_local_let_shows_inferred_type_and_docs` (asserts
inferred `greeting: string` **and** the `///` doc prose),
`hover_on_a_documented_function_renders_its_docs`, and
`collect_all_symbols_descends_into_every_expression_form` (walks every
expression container).
- **Variable hover (VS Code e2e / vsix)**:
`vscode-extension/test/suite/extension.test.ts` — "CHUNKY: full
language-intelligence sweep…" asserts `radius: int` / `count: int` on
unannotated lets, and "CHUNKY: a `///`-documented variable hovers with
its type AND its docs" asserts a top-level **and** a block-nested
documented `let` hover with type + doc prose through a live
`executeHoverProvider` against the launched LSP.
- **Docs generator (Rust, `crates/osprey-cli/src/docs.rs`)**:
`generate_writes_a_page_per_builtin_plus_an_index`,
`prune_deletes_stale_pages_but_keeps_live_ones_and_the_index`,
`slugs_disambiguate_case_collisions_and_stay_unique`,
`slug_map_falls_back_to_a_numeric_suffix_when_the_type_slug_also_collides`,
`page_uses_scheme_types_and_omits_empty_sections`.
- **Handler-owned state**: the differential harness runs
`http_state_levels.osp` through `osprey --run` and matches
`.expectedoutput` byte-for-byte (part of `PASS=48 FAIL=0`).
- **Resume typing**: `osprey-types` `select_and_handler_expressions`
type-checks a `handle … resume(v + 1000) …` program (auto-unwrap of the
arithmetic `Result` into the operation-result slot).
- **Gates**: `make ci` green — clippy `-D warnings`, `cargo fmt
--check`, all 7 crate coverage thresholds (osprey-lsp 99.1%,
osprey-types 98.7%, osprey-cli 96.4%, osprey-codegen 95.6%, …),
vscode-extension 90.37% ≥ 89%, differential `PASS=48 FAIL=0 NOEXP=0` +
`FC_OK`.
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