extern: caller-supplied keyword slots (kw = ...) - #21
Merged
Conversation
A practical 17-section guide to Rust using examples from the Pyfun compiler, for readers new to Rust who want to understand the codebase. Covers ownership, impl blocks, pattern matching, Result types, traits, generics, lifetimes, memory layout, closures, error handling, modules, deriving traits, smart pointers, iterators, and syntax fundamentals. Placed in docs/src/internals as a prerequisite to the numbered compiler tour chapters, since the compiler is written in Rust and readers may need a language primer alongside the architecture tour.
Pinned kwargs took only literals fixed at the declaration, so a call whose keyword value comes from the caller needed a separate extern per call shape. A `...` in place of the literal now takes the value from the caller, spelled as in a Python stub file and lexed as one token. The target takes the leading arguments positionally and the slots take the trailing ones in written order; pinned literals consume no argument, so the two mix freely on all three target forms. A slot claims one argument of the declared arrow, so a receiver-only or nullary extern has none to spare and is rejected with a diagnostic. Under-application cannot use functools.partial, which has no way to carry a keyword whose value has not arrived, so it becomes a lambda over the missing arguments. Already-supplied arguments (and a method extern's receiver) bind to temporaries first, keeping the evaluation timing functools.partial had. The type is untouched: a slot changes only where an argument lands in the emitted call, so inference, effects and arity are unchanged.
This was referenced Jul 28, 2026
Merged
simontreanor
added a commit
that referenced
this pull request
Jul 31, 2026
Two user-visible additions since 0.2.0 -- the `input` prelude builtin
(`string ->{io} string`, #19) and caller-supplied `extern` keyword slots
(`kw = ...`, #21) -- plus five fixes, four of them from the first program
written in Pyfun in anger:
* imported schemes reused the exporting module's type-variable ids, silently
blocking generalization in the importing module and cascading to its
consumers (#26 / #27)
* a recursive function of arity >= 2 could not perform any effect -- the shape
of every loop in a language with no `while` (#24 / #28)
* a module that only matched an imported `Option` emitted `None_` without
importing it (#25 / #29)
* user identifiers collided with Python keywords, builtins the emitter calls,
and modules an `extern` imports (#23 / #30)
* the local-binder module-alias shadowing residual (#18)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the Caller-varying keyword arguments backlog item added in #20.
The gap
Pinned kwargs (
DESIGN.md§6) accepted only literals fixed at the declaration, sorequests.get(url, timeout = t)with a caller-suppliedtneeded a separate extern per call shape. Python's API culture is optional-kwargs-with-defaults, which made this the boundary friction a real user was likeliest to meet first.The feature
A
...in place of the literal takes the keyword's value from the caller:The spelling is Python's own stub-file placeholder (
def get(url, timeout=...)), lexed as one token so. . .stays three dots.Binding rule. Positional, mirroring 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 the two mix freely:
m.f(a=1, b=..., c="x", d=...)at arity 3 emitsm.f(s, a=1, b=i, c="x", d=b).Rejections. 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
unitlowering drops, so both are diagnosed rather than silently mis-lowered.Under-application.
functools.partialcannot carry a keyword whose value has not arrived, so an under-applied slot extern becomes a lambda over the remaining arguments:Already-supplied arguments, and a method extern's receiver, bind to temporaries first, so they evaluate at application time exactly as
functools.partialwould have evaluated them rather than once per later call.Scope
The type is untouched. A slot changes only where an argument lands in the emitted call, so inference, effects and arity are unchanged, and
src/types/needed no edit.src/python_emitter/needed none either:PyExpr::CallKwalready held arbitrary expressions. Strictly additive, so every existing extern behaves as before.Also updated the tree-sitter grammar (
extern_slot), since editors using it would otherwise fail to parse a slot extern. The TextMate grammars only highlight theexternkeyword, so they need nothing.Tests
7 new in
tests/compile.rs(placement, mixed literal/slot ordering, receiver form, partial-to-lambda, eager evaluation of supplied args and receiver, the three rejections, one e2e), 4 new roundtrip cases, 1 new lexer test, 1 new tree-sitter corpus case.856 Rust tests pass, 42/42 tree-sitter corpus,
cargo fmt --checkandcargo clippy --all-targetsclean. Verified cross-module too: a slot extern binds to a lambda at module top level and a dependent module calls it n-ary.