Skip to content

lowering: block-local functions carry their arity, so a partial application curries - #91

Merged
simontreanor merged 1 commit into
mainfrom
fix/local-let-arity
Aug 30, 2026
Merged

lowering: block-local functions carry their arity, so a partial application curries#91
simontreanor merged 1 commit into
mainfrom
fix/local-let-arity

Conversation

@simontreanor

Copy link
Copy Markdown
Owner

What

A function bound by a block-local let with parameters, then applied to fewer (or more) arguments than it takes, now curries exactly like a top-level function:

let addAll xs ys =
  let pair a b = a + b
  List.map (pair 10) ys

emits _pf_map(functools.partial(pair, 10), ys) and prints [11, 12]. Before, the same program passed pyfun check and died at runtime with TypeError: pair() missing 1 required positional argument, because lowering took arity only from the module-level table and a local name answered "unknown", which is emitted n-ary as written.

How

A block-scoped local_arities table lives beside the fold pass's local_fn_defs registry and follows the same scope discipline, so an entry can only mean "this name resolves to that local function here":

  • registered for let f a b = … and let f = fun a b -> … (parameter count of the binding or the lambda);
  • snapshotted and restored per block (save_local_scope/restore_local_scope), and around a computation expression body, which is its own Python function;
  • displaced by parameters and match-arm binders for the binder's extent (shadow_local_fns/unshadow_local_fns, now carrying both registries);
  • evicted when a non-function let or a CE binder rebinds the name.

One ordering detail: a function let registers its arity before its value is lowered, so a recursive local function can partially apply itself; a non-function let evicts after its value is lowered, since let pair = pair 1 reads the previous binding.

lower_application consults local_arities first. Parameters have no entry and stay n-ary, so nothing changes for a callee whose arity really is unknown. Over-application of a local function (k 1 2 x with a 2-ary k returning a function) now emits k(1, 2)(x) instead of a three-argument call.

Tests

tests/compile.rs: the issue's program (string-level and end-to-end), the lambda spelling, over-application, a parameter shadowing the local function, a match-arm binder shadowing it, a recursive local function partially applying itself, and a non-function rebinding evicting the arity.

Docs: the src/lowering/mod.rs header comment and a new "Arity for the currying lowering" section in INTERNALS.md.

Closes #84

…cation curries

A function bound by a block-local `let` with parameters (or `let f = fun a b
-> ...`) and then under-applied was emitted as a full Python call with too few
arguments, passing `pyfun check` and dying at runtime with a TypeError. Arity
came only from the module-level table, and a local name answered `None`.

Add a block-scoped `local_arities` table kept in step with the fold pass's
`local_fn_defs`: saved and restored per block (and around a CE body),
displaced by parameters and match-arm binders, evicted by a non-function
rebinding or a CE binder. A function `let` registers before its value lowers
so a recursive local function can partially apply itself; a non-function `let`
evicts after, since `let pair = pair 1` reads the previous binding.
`lower_application` consults the table first, so both partial application
(`functools.partial(pair, 10)`) and over-application (`k(1, 2)(x)`) of a
local function now lower like a top-level one. Parameters stay n-ary.
simontreanor added a commit that referenced this pull request Aug 30, 2026
…after #90, #91 and #93

Resolves the overlap between the lookup peephole (#93) and the Option/Result
ladder (#94): both hooks run in the match lowering, lookup first since it
needs no Option at all; the two new list helpers return the _None_ singleton;
tests whose scrutinee both passes rewrote now expect the combined output.
@simontreanor
simontreanor merged commit fd8b7e7 into main Aug 30, 2026
16 checks passed
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.

Partial application of a block-local function is emitted as a full call

1 participant