Skip to content

perf(python): emit union field reads via narrow helper instead of typing.cast - #4731

Merged
MangelMaxime merged 1 commit into
perf/python-read-union-casesfrom
perf/python-narrow-union-cases
Jul 8, 2026
Merged

MangelMaxime merged 1 commit into
perf/python-read-union-casesfrom
perf/python-narrow-union-cases

Conversation

@dbrattli

@dbrattli dbrattli commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #4725, targeting its branch.

#4725 speeds up union case field reads by emitting cast(Case, x).field instead of the .fields-rebuild property. To make Pyright accept those casts it disables reportUnnecessaryCast globally, because some sites (single-case unions, already-narrowed values) make the cast redundant from Pyright's point of view.

This PR moves that "cast" into a tiny library helper so generated code stays cast-free and the global rule can stay on.

Changes

  • fable_library/union.py — add narrow[T](case: type[T], value: object) -> T, a typed identity function (documented, exported in __all__). It plays the same role as typing.cast but is scoped to unions.
  • Fable2Python.Transforms.fs — union field reads now emit narrow(Case, x).field instead of cast(Case, x).field.
  • pyrightconfig.json — revert reportUnnecessaryCast back to true; narrow is not typing.cast, so the rule no longer fires on these reads (and still guards the rest of the generated code).

Generated output before → after:

# before (#4725)
return cast(Tree_Node, t).left_
# after (this PR)
return narrow(Tree_Node, t).left_

Why not cast(T, value) inside narrow?

narrow's body is return value # pyright: ignore[reportReturnType] rather than return cast(T, value). typing.cast is a plain Python function (return val), so routing through it would make two calls per field read instead of one — a needless regression on the exact hot path this optimization targets (~8M reads in the #4725 benchmark). The ignore is targeted, and reportUnnecessaryTypeIgnoreComment (enabled) confirms it's genuinely necessary.

Verification

  • Pyright on union.py: 0 errors/warnings; the ignore is not flagged as unnecessary.
  • Call-site check: narrow(Tree_Leaf, x).value_ resolves to the field's real type (int).
  • Quicktest compiles and emits narrow(...) for both user unions and library unions (Result/Choice); the only remaining cast in output is a legitimately-necessary one that reportUnnecessaryCast: true accepts.

🤖 Generated with Claude Code

…typing.cast`

Union case field reads are compiled as `cast(Case, x).field`, which required
disabling `reportUnnecessaryCast` globally (single-case unions and already-narrowed
values make some of those casts redundant from Pyright's view).

Move the "cast" into a small library helper `narrow(Case, x)` in union.py: a typed
identity function (single runtime call, same cost as the previous inline cast). This
keeps generated code free of `typing.cast`, so `reportUnnecessaryCast` can stay
enabled for the rest of the generated Python.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MangelMaxime

Copy link
Copy Markdown
Member

I tried to find a way to not disable reportUnnecessaryCast and could not come up with a trick for that. Great finding

@MangelMaxime
MangelMaxime merged commit a840d4e into perf/python-read-union-cases Jul 8, 2026
16 checks passed
@MangelMaxime
MangelMaxime deleted the perf/python-narrow-union-cases branch July 8, 2026 08:34
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.

2 participants