Background
PR-in-flight for #5508 / #5453 adds a _jac.poly.* runtime-polymorphic dispatch lane in the ECMAScript codegen. When the type evaluator cannot prove the receiver of a Python-style method call (x.lower(), x.strip(), x.append(...), etc.) is a JS-native primitive, the codegen falls back to _jac.poly.<method>(target, ...) instead of leaking the Python identifier verbatim into the compiled JavaScript. This stops the runtime crashes described in #5508 / #5453.
The fallback is necessary on its own merits — genuinely dynamic code (def f(x: Any) -> str { return x.lower(); }) needs runtime dispatch regardless of how good the type system is. But in several common patterns the receiver really is a string at runtime, and the type system could know that — it just doesn't today. Every _jac.poly.* call that survives in compiled output for one of these patterns is a marker for an upstream type-inference improvement worth doing.
This issue tracks the four gaps that the PR for #5508 surfaced. Closing each one removes another _jac.poly.* call from compiled output and shifts the work back to the zero-cost typed-dispatch path.
Gap 1 — bare dict parameter, subscript yields Any
cl {
def:pub Inner(props: dict) -> JsxElement {
name = props["name"] or "";
initial = name[:1].upper();
return <div>{initial}</div>;
}
}
props: dict parameterizes to something like dict[Unknown, Unknown], so props[\"name\"] is Any, and the chain … or \"\" widens but does not narrow. The codegen has no way to know name is a string. Today this falls through to _jac.poly.upper(name.slice(0, 1)).
What we'd want: either bare dict defaults to a more useful element type, or the JSX/component layer declares prop types so subscript narrows to str.
Gap 2 — value or default_str doesn't narrow to str
name = props[\"name\"] or \"\"; # name should narrow to `str` here
Any | LiteralString should narrow to str in the or \"\" arm, since the right-hand side is statically str and the result is the union of the truthy LHS and the RHS. The current evaluator widens to Any and the narrowing opportunity is lost.
Gap 3 — useState(\"...\") generic inference
[input, setInput] = useState(\"\");
# `input` should be inferred as `str` from the literal arg
… input.strip() …
useState is a React import; the type evaluator does not propagate the generic type parameter from the literal argument to the destructuring target, so input is Any and .strip() falls through to _jac.poly.strip(input). This is the case that broke tests/compiler/passes/ecmascript/test_js_generation.jac::separated files in the #5508 PR — the test had to be updated to assert the polyfill output instead of the typed input.trim() it would ideally produce.
Gap 4 — comprehension over bare list
def list_compr_method(items: list) -> list {
return [x.strip() for x in items];
}
items: list has no element type, so x is Unknown inside the comprehension and x.strip() falls through. Even when later usage of the result statically constrains the element type to str, the inference doesn't propagate backwards into the comprehension. Today this compiles to items.map(x => _jac.poly.strip(x)) instead of items.map(x => x.trim()).
Why this matters
- Performance. Each
_jac.poly.* call is a function call + typeof check where typed dispatch would be zero-cost native.
- Discoverability. Users have no signal that their type annotations left optimization on the table — the codegen silently falls back. (Possible follow-up: have the type checker emit a warning when polyfill dispatch is selected.)
- Python fidelity. The polyfill is close to but not bit-exact with Python semantics in edge cases (e.g.
list.append return value, str.strip(chars) set semantics). The typed path is exact.
- Code reading. Compiled output is more idiomatic when typed dispatch fires — reviewers reading the JS see
name.trim() instead of _jac.poly.strip(name).
Suggested ordering
- Gap 2 (
or narrowing) is probably the smallest type-system change and unblocks Gap 1 partially.
- Gap 4 (list element-type inference from usage) is well-bounded.
- Gap 3 (generic inference through destructuring of external functions) is the largest — likely needs work in how
.d.ts-equivalent declarations are consumed.
- Gap 1 (
dict defaults) is partly a language-design call — what should bare dict mean.
Acceptance
For each gap, add a fixture under tests/compiler/passes/ecmascript/fixtures/ demonstrating the pattern, and add an assertion in test_js_generation.jac that the output uses the typed JS (x.trim(), x.toLowerCase(), etc.) rather than _jac.poly.*. As gaps are closed, the corresponding _jac.poly.* assertions in the existing tests (compr list method call, separated files, issue 5508 case2 ...) should be flipped to assert the typed path.
References
Background
PR-in-flight for #5508 / #5453 adds a
_jac.poly.*runtime-polymorphic dispatch lane in the ECMAScript codegen. When the type evaluator cannot prove the receiver of a Python-style method call (x.lower(),x.strip(),x.append(...), etc.) is a JS-native primitive, the codegen falls back to_jac.poly.<method>(target, ...)instead of leaking the Python identifier verbatim into the compiled JavaScript. This stops the runtime crashes described in #5508 / #5453.The fallback is necessary on its own merits — genuinely dynamic code (
def f(x: Any) -> str { return x.lower(); }) needs runtime dispatch regardless of how good the type system is. But in several common patterns the receiver really is a string at runtime, and the type system could know that — it just doesn't today. Every_jac.poly.*call that survives in compiled output for one of these patterns is a marker for an upstream type-inference improvement worth doing.This issue tracks the four gaps that the PR for #5508 surfaced. Closing each one removes another
_jac.poly.*call from compiled output and shifts the work back to the zero-cost typed-dispatch path.Gap 1 — bare
dictparameter, subscript yieldsAnyprops: dictparameterizes to something likedict[Unknown, Unknown], soprops[\"name\"]isAny, and the chain… or \"\"widens but does not narrow. The codegen has no way to knownameis a string. Today this falls through to_jac.poly.upper(name.slice(0, 1)).What we'd want: either bare
dictdefaults to a more useful element type, or the JSX/component layer declares prop types so subscript narrows tostr.Gap 2 —
value or default_strdoesn't narrow tostrAny | LiteralStringshould narrow tostrin theor \"\"arm, since the right-hand side is staticallystrand the result is the union of the truthy LHS and the RHS. The current evaluator widens toAnyand the narrowing opportunity is lost.Gap 3 —
useState(\"...\")generic inferenceuseStateis a React import; the type evaluator does not propagate the generic type parameter from the literal argument to the destructuring target, soinputisAnyand.strip()falls through to_jac.poly.strip(input). This is the case that broketests/compiler/passes/ecmascript/test_js_generation.jac::separated filesin the #5508 PR — the test had to be updated to assert the polyfill output instead of the typedinput.trim()it would ideally produce.Gap 4 — comprehension over bare
listitems: listhas no element type, soxis Unknown inside the comprehension andx.strip()falls through. Even when later usage of the result statically constrains the element type tostr, the inference doesn't propagate backwards into the comprehension. Today this compiles toitems.map(x => _jac.poly.strip(x))instead ofitems.map(x => x.trim()).Why this matters
_jac.poly.*call is a function call +typeofcheck where typed dispatch would be zero-cost native.list.appendreturn value,str.strip(chars)set semantics). The typed path is exact.name.trim()instead of_jac.poly.strip(name).Suggested ordering
ornarrowing) is probably the smallest type-system change and unblocks Gap 1 partially..d.ts-equivalent declarations are consumed.dictdefaults) is partly a language-design call — what should baredictmean.Acceptance
For each gap, add a fixture under
tests/compiler/passes/ecmascript/fixtures/demonstrating the pattern, and add an assertion intest_js_generation.jacthat the output uses the typed JS (x.trim(),x.toLowerCase(), etc.) rather than_jac.poly.*. As gaps are closed, the corresponding_jac.poly.*assertions in the existing tests (compr list method call,separated files,issue 5508 case2 ...) should be flipped to assert the typed path.References
.cl.jac: valid Jac code produces crashing JavaScript at runtime while jac check passes #5508 — bug report for runtime crashes, motivating the polyfill.append,.strip,.lowerleaking verbatim)_jac.polydispatch lane and the Site-A/Site-B MRO-walk symmetry fix