Skip to content

lowering: Option/Result matches as isinstance ladders, slotted classes, nullary singletons (#87, #89) - #94

Merged
simontreanor merged 1 commit into
mainfrom
perf/option-nullary-representation
Aug 30, 2026
Merged

lowering: Option/Result matches as isinstance ladders, slotted classes, nullary singletons (#87, #89)#94
simontreanor merged 1 commit into
mainfrom
perf/option-nullary-representation

Conversation

@simontreanor

Copy link
Copy Markdown
Owner

Three changes to the emitted Python's representation of the built-in two-case types and field-less constructors, from the Scrabble move generator findings in #87 and #89. Nothing about the language changes; every change is a property of the output. DESIGN.md §5.5 is the contract, INTERNALS.md the mechanics.

What changed

Option/Result matches lower to an isinstance ladder. A match whose arms are Some p/None (or Ok p/Error e) with irrefutable payload patterns, optionally ending in a catch-all, now emits what a person writes:

def allowsLetter(checks, rc, l):
    _pf_t0 = _pf_map_try_find(rc, checks)
    if isinstance(_pf_t0, None_):
        return True
    else:
        s = _pf_t0._0
        return l in s

The last arm of an exhaustive match is a plain else and the defensive case _: raise is gone (the ladder's exhaustiveness rule, both constructors unguarded or an unguarded catch-all, is the checker's rule for this shape). Guarded arms in return position become their own if and fall through; a guarded match in value position, and any refutable payload pattern (case Some 0:), keep the match lowering. result {} / option {} use the same ladder per let!/do! and forward a failure as the value in hand (return r instead of rebuilding Error(e) or calling None_()). User ADTs still emit match/case.

Every emitted dataclass is slots=True (variants, records, Some/None_/Ok/Error/_Exception). Instances lose __dict__, which is the one visible interop change (vars(p) no longer works; dataclasses.asdict/replace, ==, hashing and < are unchanged and covered by e2e tests).

Nullary constructors are singletons. Each field-less variant and the prelude's None is built once right after its class (_Across = Across(), _None_ = None_()) and every use as a value loads that name; the prelude helpers return _None_ on a miss. Patterns still test the class (case Across():, isinstance(o, None_)), so a hand-built instance from Python matches and compares equal as before. If the program binds the singleton's name itself (let _Across = …), that constructor keeps the call form. Cross-module references load the exporting module's singleton (palette._Red), decided by the same rule on both sides; project modules import _None_ from _pyfun_rt alongside Some/None_.

Measurements

Micro-benchmark of the issue's shapes (an Option match on Map.tryFind and on List.findIndex, a nullary match and ==, one million iterations; emitted by main vs this branch, CPython 3.14.6, median of 5, identical output): 2.269s → 1.756s (23% faster).

bench/run.py (median of 5; these workloads have no Option matches on their hot path, so this is a regression check for slots=True and the singletons rather than a demonstration):

benchmark before (main) after hand-written baseline
expr_eval 1.265s (2.29x) 1.240s (2.27x) 0.547s
collatz 0.973s (0.99x) 0.980s (1.00x) 0.980s
map_build 0.700s (1.53x) 0.682s (1.51x) 0.452s

Outputs verified identical to the baselines. expr_eval's small gain is the slotted ADT classes.

Tests

New snapshot tests in tests/compile.rs for every ladder shape (return and value position, guards, tuple payload, Result, catch-all variable, the refutable and guarded-value-position fallbacks, a user ADT still emitting match), the singleton (value reference, the name-collision fallback for a user constructor and for None), slots=True on every class, plus e2e runs covering all of them and record update/compare/hash under slots. tests/project.rs covers cross-module singletons (load, collision fallback, and an e2e run through _pyfun_rt). docs/verify_lessons.py passes; the lesson and internals pages that show emitted Python are updated.

Decisions the issues left open: hidden active-pattern case classes are not given singletons (built once per recognizer call and matched on the spot); the ladder does not test with is against the singleton, so Python-side constructed values keep matching.

Closes #87
Closes #89

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

Copy link
Copy Markdown
Owner Author

Merge order: #90, #91, #93 first, then this one. The last commit here merges those three branches and resolves the overlap with #93 (both match-lowering hooks kept, lookup peephole first; the two new list helpers return the _None_ singleton; the interaction tests now expect the combined output). Until the others are merged the diff shows their changes too; once they are, this squashes to the representation change alone.

@simontreanor
simontreanor force-pushed the perf/option-nullary-representation branch from 70662a1 to bf4a8e8 Compare August 30, 2026 09:03
@simontreanor
simontreanor merged commit 7471523 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

1 participant