Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,14 @@ concern: no Pyfun name is bound, the checker ignores it (`Item::ExternImport`),
emits nothing, and the emitted reference is always the full dotted target as written. The `extern`
prefix keeps it distinct from Pyfun's own file-module `import Geometry` (§6.1) — the module graph and
LSP are untouched. Receiver-reachable members can of course still use the instance-access form below
instead. A **nullary** extern (`unit -> a`,
e.g. `time.time`) applied to `()` lowers to a zero-argument call (`time.time()`), not `time.time(None)`
— the unit argument is evaluated for effects but dropped. The mirror image is a **thunk parameter**:
instead. A `unit` **parameter contributes no argument**, wherever it sits along the arrow: a
**nullary** extern (`unit -> a`, e.g. `time.time`) applied to `()` lowers to a zero-argument call
(`time.time()`), not `time.time(None)`; a leading unit before real parameters drops the same way
(`extern myAbs : unit -> int -> int = abs` lowers `myAbs () (-5)` to `abs(-5)`, never the split
`abs()(-5)`); and a mid-list unit simply vanishes from the call (`int -> unit -> int -> int = max`
emits `max(3, 5)`). One rule, three shapes. The dropped argument is still evaluated when it could
have effects, and a partial application closes over a lambda rather than `functools.partial`, which
could not drop a `unit` that has not yet arrived. The mirror image is a **thunk parameter**:
an extern parameter typed `unit -> a` (`asyncio.to_thread`'s function, an event handler with no
arguments) is a Pyfun function of one parameter, the unit value, that Python will call with none, so
the call site wraps that argument as `lambda: f(None)` (a literal `fun _ -> body` collapses to
Expand Down Expand Up @@ -576,8 +581,8 @@ positionally and the `...` slots take the **trailing** ones, in the order the ke
pinned literals consume no argument, so they may sit anywhere among the slots
(`m.f(a = 1, b = ..., c = "x", d = ...)` at arity 3 emits `m.f(s, a=1, b=i, c="x", d=b)`). A slot claims
one argument of the declared arrow, so the type must have one to spare: a receiver takes the first
argument, and a nullary extern's only argument is the `unit` that lowering drops, so both are rejected
with a diagnostic rather than silently mis-lowered.
argument and a `unit` parameter contributes none, so a type left with nothing for a slot to claim is
rejected with a diagnostic rather than silently mis-lowered.

A slot changes only *where* an argument lands in the emitted call, so like a pinned literal it stays
**invisible to the type** (`parseInt` is an ordinary `string -> int -> int`), to inference, and to
Expand Down
17 changes: 14 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,18 @@ play comes first; the browser target is last because it depends on the async dec
prelude a function body gets, and the CE's own binders join the frame stack, so a lambda nested
in the block classifies its captures against them too.

24. ~~**A leading `unit` parameter split the extern call**~~ **CLOSED 2026-08-31** (#123, found
wiring the keyboard pump: `unit -> int -> int = abs` emitted `abs()(-5)` — the nullary
zero-argument rule fired and the remaining arguments applied to its result — while a mid-list
unit travelled as `None` (`max(3, None, 5)`): three shapes, three meanings). Now **one rule: a
`unit` parameter contributes no argument, wherever it sits** — `abs(-5)`, `max(3, 5)`, and the
nullary call unchanged. Partial application closes over a lambda (a future unit is accepted and
ignored, which `functools.partial` cannot do), receiver methods and bare references drop units
the same way, and the parser's `...`-slot arithmetic counts only non-unit arguments — so the
extern the finding wanted, `unit -> (unit ->{io} unit) -> Thread =
threading.Thread(daemon = true, target = ...)`, now parses and emits the single call. The
mid-list `None` change is breaking in principle; nothing in the repo or lessons relied on it.

## Deferred (real features, no current demand — say the word and I'll scope it)

- **Fold-pass residual shapes** (S per slice, demand-driven) — Tier B shipped 2026-07-13 (local named
Expand Down Expand Up @@ -561,9 +573,8 @@ registry (PR to zed-industries/extensions), and consider shipping Helix indent/t
needs nothing installed, because Gradle is *not* on this machine and two releases running lost
time rediscovering that before reaching for a bare `gradle`. Approval is not instant: the plugins
API lists approved versions only, so it reads the previous one until moderation clears.
- **VS Code Marketplace** — accepted and live as `pyfun.pyfun`, at **0.7.0**; the **0.8.0 vsix is
packaged and attached to the release but not yet uploaded** (2026-08-30, gallery API reads
0.7.0). Verification takes minutes, during which the API keeps reading the
- **VS Code Marketplace** — accepted and live as `pyfun.pyfun`, at **0.8.0** (vsix uploaded through
the publisher web UI 2026-08-30). Verification takes minutes, during which the API keeps reading the
previous version and the publisher UI shows the pending one as "Verifying" — a lag, not a failed
upload. The **only surface that cannot be scripted**: the vsix is uploaded by hand through
the publisher web UI at `https://marketplace.visualstudio.com/manage/publishers/pyfun` (the CLI
Expand Down
Loading