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
28 changes: 28 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,34 @@ kwargs-extern carries them through `functools.partial` (`openText` → `functool
mode="rt", encoding="utf-8")`), so a later application still supplies them; only a full (or over-)
application emits the direct `f(a, kw=v)` call.

**Caller-supplied keyword slots (`= target(kw = ...)`).** Python's API culture is optional keyword
arguments with defaults, and pinning a literal only covers the value that is fixed for every call. A
`...` in place of the literal makes the keyword's value come from the **caller** instead:

```
extern parseInt : string -> int -> int = int(base = ...) # parseInt "ff" 16 → int("ff", base=16)
extern openText : string -> string -> Seq string = builtins.open(mode = "rt", encoding = ...)
extern writeText : Path -> string -> string -> int = .write_text(encoding = ...)
```

The spelling is Python's own stub-file placeholder (`def get(url, timeout=...)`), lexed as one token.
The **binding rule** is positional and mirrors a Python call: the target takes the leading arguments
positionally and the `...` slots take the **trailing** ones, in the order the keywords are written;
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.

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
effects. Under-application still never drops anything, but it cannot use `functools.partial`, which has
no way to carry a keyword whose value has not arrived. An under-applied slot extern becomes a **lambda**
over the remaining arguments (`parseInt "ff"` → `lambda _pf_k0: int("ff", base=_pf_k0)`; a bare
`parseInt` → `lambda _pf_k0, _pf_k1: int(_pf_k0, base=_pf_k1)`). The arguments already supplied are
bound to temporaries first, so they evaluate at application time exactly as `functools.partial` would
have evaluated them, rather than once per later call; a receiver is bound the same way.

**Lists — the eager collection.** `List a` is a built-in type that **lowers to a
Python `list`** (a dynamic array), with literal syntax `[1, 2, 3]` (comma-separated, like Python and
like Pyfun records and tuples). The big-O is Python's, *not*
Expand Down
14 changes: 0 additions & 14 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,6 @@ Keep this a *forward-looking* backlog — do not let it grow back into a changel
§5.3: statically-known decoders deforest to direct dict/list access, byte-identical `Result`s, 2.8x
measured on a decode-dominated workload; dynamic shapes (`andThen`, decoder-as-value) keep the
interpreter.)
- **Caller-varying keyword arguments at the `extern` boundary** (M) — pinned kwargs (`DESIGN.md` §6)
accept only *literals fixed at the declaration*, so a call whose keyword value comes from the caller
(`requests.get(url, timeout = t)`) needs a separate extern per call shape. Python's API culture is
optional-kwargs-with-defaults, so this is the boundary friction a real user is likeliest to meet first,
and it is the one place where the declaration count scales with the *call shapes* used rather than with
the functions called. The shape is open and the bar is Pythonista familiarity, so offer the
alternatives before shipping one: a marked slot in the existing pinned list (`= requests.get(timeout = _)`, filled by the next argument) is the smallest
step and needs no new type machinery, since the slot consumes an ordinary arrow and only the emitted
call shape changes; a record-of-options argument types better but wants optional fields, which nothing
else in the language has. Distinct from the **`extern` stub generator** non-goal below: that one
automates *writing* signatures, this one makes a signature expressible at all. The complementary cost,
that every user re-derives the same wrapper lines independently, is the façade half of **Larger prelude
/ package manager** above.

- ~~Module-alias shadowing~~ **CLOSED 2026-07-27** — `import Ids` + any same-named binder (top-level
`let`, parameter, block `let` anywhere in the function, lambda parameter, match-pattern capture at
any level, native-CE binder) now emits `import ids as _pf_ids` at the affected sites
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

# Inside the compiler

- [Learning Rust through this compiler](internals/rust-primer.md)
- [The tour and its running example](internals/README.md)
- [Orientation](internals/00-orientation.md)
- [Lexing](internals/01-lexing.md)
Expand Down
Loading