feat: widen == between T? and T, and widen generic-fn optional arguments - #807
Merged
Conversation
Two widening holes, closed together because they share the machinery and the safety argument. `==` / `!=` between an optional and a plain value of its inner type used to report E217. The checker now accepts the mixed comparison (both orders) when the inner type matches structurally, and the emitter lowers it as `is_some and value == v` — none never equals a plain value, neq negates, and a String? compares by content through __str_eq, the same as the existing T? == T? compare. The mixed arm is deliberately the LAST arm of the optional-compare dispatch: every both-sides-optional recovery (including the generic-specialization tid recovery) gets first refusal, and a side whose check-time tid is TID_ERR or TID_NONE is excluded by name since both are primitives with tids >= 0. A fresh string on the plain side is released after the compare, the same rule the value operator applies. Ordering operators stay rejected on optionals, and bare none keeps its own rules (#680). A generic function's optional parameter now widens a plain argument — `pick(x, 3)` against `fn pick[T](a: T, b: Int?)` — in both the inferred and explicit-type-args forms, by routing the two generic validation loops through the same argument_widens_to_optional record every non-generic argument position already uses; the emitter wrap comes for free through ir_emit_call_arg. #683 wired this up and backed it out because of the #148 specialization miscompile; #148 is fixed (declaration-driven optional tids), and its evidence was re-run first: none and a value through one shared specialization, read back via == none, unwrap_or, and match, all correct. docs/limitations.md: the `==` entry and the generic-function bullet now describe the widened behavior; the "three argument positions" list drops to two (container store/query, enum variant payload). ## what was tested - new tests/cases/test_optional_widen_eq.pith golden: Int?/String? mixed compares both orders, none vs value, neq, and the generic widening through one specialization read back three ways (the #148 evidence shapes) - new tests/leaks/leak_optional_plain_compare case, added to the leak gate: flat at 200k vs 800k rounds (2800 vs 2756 KB peak); a 900k-round probe with a fresh-string operand also flat at 2.3 MB - rejections that must survive, verified rejected: `x < 5` on Int? (E217), `x == "hi"` on Int? (E217), `f(none)` for a non-optional parameter (E219) - make bootstrap-verify: bootstrap ir fixed point verified (includes self regressions + std tests) - make check-invalid-only: 51 passed, 0 failed - fmt + lint clean on all changed files; bootstrap seed regenerated
kacy
added a commit
that referenced
this pull request
Aug 19, 2026
…s checksum (#808) The gate contract is that a case prints peak_kb() and nothing else; the case from #807 printed its hit counter instead, so the "growth" the gate compared was 800k*3 - 200k*3 = 1,800,000 — the difference of two checksums, not two peaks — and main went red on a case that does not leak. ## what was tested - the gate's own measure sequence against the rebuilt case: PITH_LEAK_ROUNDS=200000 -> 2616 kb, 800000 -> 2656 kb, growth 40 kb against the 2048 kb limit
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.
Two widening holes from the ergonomics sprint (task #239), closed together because they share the machinery and the safety argument.
== / != between T? and T
o == 5whereo: Int?used to report E217. The checker now accepts the mixed comparison (both orders) when exactly one side is an optional whose inner type matches the other side structurally. The emitter lowers it asis_some and value == v—nonenever equals a plain value,!=negates, and aString?compares by content through__str_eq, the same as the existingT? == T?compare.The mixed arm is deliberately the last arm of the optional-compare dispatch: every both-sides-optional recovery (including the generic-specialization tid recovery) gets first refusal, and a side whose check-time tid is
TID_ERRorTID_NONEis excluded by name — both are primitives with tids >= 0, sotid >= 0alone is not an "is typed" test. A fresh string on the plain side is released after the compare, the same rule the value operator applies to its operands.Deliberately unchanged: ordering operators (
<etc.) stay rejected on optionals; barenonekeeps its #680 rules (only an optional accepts it, and nothing here lets a plain value satisfynone).generic-function optional arguments
pick(x, 3)againstfn pick[T](a: T, b: Int?)now widens, in both the inferred and the explicitpick[String](x, 3)forms, by routing the two generic validation loops through the sameargument_widens_to_optionalrecord every non-generic argument position already uses — the emitter wrap comes for free throughir_emit_call_arg, so the emitter needed no change for this half.History: #683 wired exactly this up, tested it, and backed it out because of the #148 specialization miscompile (a specialization read a parameter's optional-ness off the call site, so one
nonecall site corrupted its siblings). #148 is fixed — declaration-driven optional tids — and its evidence was re-run on main before re-landing:noneand a value through one shared specialization, read back via== none,unwrap_or, andmatch, all correct.docs/limitations.md updated: the
==entry and the generic-function bullet now describe the widened behavior, and the "argument positions that still need aT?local" list drops from three to two (builtin container store/query, enum variant payload).what was tested
tests/cases/test_optional_widen_eq.pithgolden:Int?/String?mixed compares both orders,nonevs value,!=, and the generic widening through one shared specialization read back three ways (the feat: add websocket session surface #148 evidence shapes)tests/leaks/leak_optional_plain_comparecase, added to the leak gate: flat at 200k vs 800k rounds (2800 vs 2756 KB peak); a separate 900k-round probe with a fresh-string operand also flat at 2.3 MBx < 5onInt?(E217),x == "hi"onInt?(E217),f(none)for a non-optional parameter (E219)make bootstrap-verify: bootstrap ir fixed point verified (includes self regressions + std tests)make check-invalid-only: 51 passed, 0 failed