compile record copy (m(field = e)) to match+reconstruct, enabling in-place update#909
Draft
TimWhiting wants to merge 5 commits into
Draft
compile record copy (m(field = e)) to match+reconstruct, enabling in-place update#909TimWhiting wants to merge 5 commits into
TimWhiting wants to merge 5 commits into
Conversation
Synthesize the copy constructor as one match on the copied value that reconstructs with either the given optional argument or the matched field, instead of per-field accessor calls as optional-argument defaults. After inlining at a call site the optional scrutinies fold away statically, leaving a single match+reconstruct that Perceus turns into an in-place field update when the value is unique. Simplify gains the missing pieces for that folding: a known-constructor match reduction that answers NoMatch on constructor-name mismatch regardless of pattern arity (one Unknown previously blocked branch selection), handles nullary polymorphic constructors and box applications against Box patterns, and A-normalizes non-total constructor arguments in let bindings so the constructor binding itself becomes total and a single-use binding can inline into its match. 10M single-field updates of a five-field struct: 62ms -> 11ms at -O2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TimWhiting
force-pushed
the
opt/copy-fbip
branch
from
July 12, 2026 20:55
03f0d62 to
d8db777
Compare
TimWhiting
marked this pull request as draft
July 12, 2026 21:11
Sink trailing let bindings into an irrefutable singleton match, and fold
matches on a scrutinee variable inside a branch that already destructured
it (enriching wildcard fields to fresh pattern variables as needed). A
record update m(a = m.a + 1) now compiles to a single destructure that
uses the matched field directly:
match m { Model(a,b,c,d,e) -> Model(a+1, b, c, d, e) }
test/parc/parc15 and parc16 exercise nested same-scrutinee matches; their
recorded core is regenerated (the inner match now folds before Parc).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matching can have effects in Koka: forcing a lazy value overwrites the cell in place, so a later match on the same variable can see a different constructor. Exclude thunk-constructor patterns from the known-scrutinee fold and from irrefutability (whnf constructors remain eligible: forcing is monotone). Values inside refs were already safe by construction: the fold only unifies matches on the same immutable binding, and a ref read is an application producing a fresh binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
|
Soundness note on the known-scrutinee fold (folding a nested match on an already-destructured variable): it relies on the scrutinee binding being immutable between the two matches, which holds in Koka core with one exception.
🤖 Generated with Claude Code |
…ation The constructor witness built by enrichConPattern reused the pattern's constructor name, whose instantiated type does not survive core (de)serialization: the core parser resolves constructors to their generic scheme, so a bare polymorphic Con application inlined cross-module became ill-typed core (flagged by --checkcore as 'expecting function type in application'; runtime was unaffected). Build the witness at the generic constructor scheme with an explicit type application derived from the pattern result type, and skip the fold in the (unexpected) case the instantiation cannot be recovered. test/parc/parc24 pins the typed witness shape (--showcoretypes) and runs with --checkcore, covering both the as-pattern nested match and the polymorphic record-copy path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
The record-update idiom
m(field = e)previously compiled (even at -O2, with the copy inlined) to one runtimeOptArg/NoOptArgscrutiny per field plus one borrowing accessor match per unchanged field — and on the unique path Perceusfreed the record and allocated a fresh one.It now compiles to a single destructure that Perceus turns into an in-place field update when the value is unique (falling back to dup+alloc when shared):
Three changes:
Copy synthesis (
Kind/Infer.synCopyCon): the copy constructor is one match on@thisreconstructing with either the given optional argument or the matched field, instead of per-field accessor calls as optional-argument defaults. No call-site-specialized copies: the definition inlines (InlineAlways) and the optional arguments — always literalOptArg/NoOptArgat a call site — fold away statically.Simplify, known-constructor folding (
kmatchPattern): answersNoMatchon constructor-name mismatch regardless of pattern arity (a singleUnknownpreviously blocked branch selection entirely), handles nullary polymorphic constructor scrutinees and@boxapplications against@Boxpatterns, and A-normalizes non-total constructor arguments in let bindings (val x = Con(f(e))⇒val y = f(e); val x = Con(y), same evaluation order) so the constructor binding is total and can inline into its match when used once (splitting only non-total arguments makes this a one-step fixpoint — total ones would inline back).Simplify, known scrutinee: trailing let bindings sink into an irrefutable singleton match, and a match on a scrutinee variable folds inside a branch that already destructured it (wildcard fields are enriched to fresh pattern variables as needed) — so the
m.aread inm(a = m.a + 1)becomes the pattern variable itself instead of a second borrow-match.10M single-field updates of a five-field struct: 62ms → 16ms, flat rss.
test/parc/parc22/parc23recorded core is regenerated and now pins the reuse shape (@con-fields-assignon the unique path inside@copy);parc15/parc16exercise nested same-scrutinee matches, which now fold before Parc, and are regenerated likewise. Full test suite passes.🤖 Generated with Claude Code