Skip to content

Commit 3c30d61

Browse files
authored
fix: Generic constructor type inference from iterable arguments (jaseci-labs#5182)
## Summary - `set(list[Item])` now correctly infers `set[Item]` - `enumerate(list[Item])` now correctly infers `enumerate[Item]` - Propagates TypeVar bindings through `base_classes` in MRO walk Previously, generic constructors like `set()` and `enumerate()` returned unparameterized types because TypeVar bindings weren't propagated through the inheritance chain.
1 parent 93c74b4 commit 3c30d61

File tree

8 files changed

+78
-80
lines changed

8 files changed

+78
-80
lines changed

docs/docs/community/release_notes/jaclang.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This document provides a summary of new features, improvements, and bug fixes in
1111
- **Type Checker: `type[T]` Member Access**: Accessing class-level members (e.g., `ClassVar`) on `type[T]` parameters now works correctly. `cls: type[MyClass]``cls.my_class_var` resolves to `MyClass`'s members.
1212
- **Type Checker: Property Support**: `@property` and `@cached_property` now correctly type-check. Accessing `obj.my_property` returns the property's return type instead of `FunctionType`.
1313
- **Fix: Static Methods on Class-Based Enums**: Static methods on `IntEnum`/`StrEnum` classes now correctly return their declared type instead of `<Unknown>`.
14+
- **Fix: Generic Constructor Type Inference**: `set(my_list)`, `enumerate(items)`, and similar generic constructors now correctly infer type parameters from iterable arguments (e.g., `set(list[Item])``set[Item]`). Previously returned unparameterized types.
1415
- **Type Checker: Generic Inheritance & Bidirectional Inference**: Added MRO-aware type argument resolution (`resolve_type_args_for_base`, `build_type_var_solution`) so multi-level generic inheritance chains are properly tracked. `_assign_class` now validates type args through the MRO with covariant/contravariant/invariant variance. `type[X]` annotations are now covariant (`type[SubClass]` assignable to `type[BaseClass]`). `FunctionType.specialize` and `specialize_member_type` use transitive TypeVar resolution. `infer_type_args` walks inherited constructors. Container literals (`list`, `dict`, `set`, `tuple`) now support bidirectional type inference via `expected_type` propagation from return statements, assignments, and function arguments, with element-level validation before adopting the expected type.
1516
- **Type Checker: Parameterized Types in Error Messages**: `ClassType.__str__` now displays type arguments for parameterized types (e.g., `list[int]`, `dict[str, bool]`) instead of bare class names. Error messages like `Cannot return <class list>, expected <class list>` now read `Cannot return list[Dog | Cat], expected list[int]`, making type mismatches immediately actionable.
1617
- **Client-Side Error Reporting**: Unhandled JavaScript errors and promise rejections in Jac client apps are now automatically captured and forwarded to the server via `POST /cl/__error__`, where they are logged through both the `jaclang.client_errors` logger and the dev console. Global error handlers (`window.onerror`, `unhandledrejection`) are installed at app initialization, and the `ErrorBoundary` fallback component also reports caught errors. Works with both the stdlib HTTP server and jac-scale (FastAPI).

jac/jaclang/compiler/type_system/impl/type_utils.impl.jac

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,7 @@ impl resolve_type_args_for_base(
534534
return [];
535535
}
536536

537-
# Build cumulative TypeVar solution by walking the MRO.
538-
# Start with src's own type_params -> type_args mapping.
537+
# Build TypeVar solution starting with src's type_params -> type_args.
539538
solution: dict[str, types.TypeBase] = {};
540539
if src.shared.type_params and src.private and src.private.type_args {
541540
for (idx, tp) in enumerate(src.shared.type_params) {
@@ -545,33 +544,45 @@ impl resolve_type_args_for_base(
545544
}
546545
}
547546

548-
# Walk the MRO (which includes src itself as first entry).
549-
# For each class in the MRO, if it has type_params and type_args,
550-
# resolve the type_args using the current solution and add new bindings.
547+
# Walk MRO and propagate TypeVar bindings through base_classes.
551548
for mro_cls in src.shared.mro {
552549
if mro_cls.shared.type_params
553550
and mro_cls.private
554551
and mro_cls.private.type_args {
555552
for (idx, tp) in enumerate(mro_cls.shared.type_params) {
556553
if tp.name and idx < len(mro_cls.private.type_args) {
557554
arg = mro_cls.private.type_args[idx];
558-
# Resolve any TypeVars in this arg using accumulated solution
559-
resolved = apply_solved_type_vars(arg, solution);
560-
solution[tp.name] = resolved;
555+
solution[tp.name] = apply_solved_type_vars(arg, solution);
556+
}
557+
}
558+
}
559+
560+
# Propagate through base_classes (where type_args are stored).
561+
if mro_cls.shared.base_classes {
562+
for base_cls in mro_cls.shared.base_classes {
563+
if isinstance(base_cls, types.ClassType)
564+
and base_cls.shared.type_params
565+
and base_cls.private
566+
and base_cls.private.type_args {
567+
for (idx, base_tp) in enumerate(base_cls.shared.type_params) {
568+
if base_tp.name and idx < len(base_cls.private.type_args) {
569+
arg = base_cls.private.type_args[idx];
570+
solution[base_tp.name] = apply_solved_type_vars(
571+
arg, solution
572+
);
573+
}
574+
}
561575
}
562576
}
563577
}
564578

565-
# When we reach the target base, extract its type_args with full substitution
566579
if mro_cls.shared == target_base.shared {
567580
if mro_cls.private and mro_cls.private.type_args {
568581
return [
569582
apply_solved_type_vars(arg, solution)
570583
for arg in mro_cls.private.type_args
571584
];
572585
}
573-
# MRO entry has no type_args — try to resolve target's type_params
574-
# from accumulated solution (handles cases like class Foo(Bar[T]))
575586
if target_base.shared.type_params {
576587
result: list[types.TypeBase] = [];
577588
for tp in target_base.shared.type_params {
@@ -581,7 +592,6 @@ impl resolve_type_args_for_base(
581592
result.append(types.UnknownType());
582593
}
583594
}
584-
# Only return if we resolved at least one param
585595
if any(not isinstance(r, types.UnknownType) for r in result) {
586596
return result;
587597
}

jac/jaclang/compiler/type_system/operations.jac

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,8 @@ def get_type_of_binary_operation(
323323
);
324324
}
325325
# Check whether a type is a valid node operand for connections.
326-
# Accepts: node instances, UnionType where all members are node instances,
327-
# AnyType, and UnknownType (not enough info to diagnose).
326+
# Accepts: node instances, list of node instances, UnionType where all
327+
# members are valid, AnyType, and UnknownType (not enough info to diagnose).
328328
def is_valid_node_operand(t: jtypes.TypeBase) -> bool {
329329
if t.is_any_type() or isinstance(t, jtypes.UnknownType) {
330330
return True;
@@ -335,6 +335,16 @@ def get_type_of_binary_operation(
335335
if isinstance(t, jtypes.UnionType) {
336336
return all(is_valid_node_operand(m) for m in t.types);
337337
}
338+
# Also accept list of node instances (runtime accepts list[NodeArchetype])
339+
if isinstance(t, jtypes.ClassType)
340+
and t.is_builtin()
341+
and t.shared.class_name == "list" {
342+
if t.private.type_args and len(t.private.type_args) > 0 {
343+
return is_valid_node_operand(t.private.type_args[0]);
344+
}
345+
return True; # Unparameterized list - allow
346+
347+
}
338348
return False;
339349
}
340350
# TODO: In strict mode, UnknownType, AnyType, TypeVarType, and object should be errors
@@ -419,9 +429,21 @@ def get_type_of_binary_operation(
419429
}
420430
}
421431
}
432+
# Connect operator returns list of nodes (right operand)
433+
if evaluator.prefetch and evaluator.prefetch.list_class {
434+
list_type = evaluator.prefetch.list_class.clone_as_instance();
435+
if isinstance(list_type, jtypes.ClassType) {
436+
list_type.private.type_args = [right_type];
437+
return list_type;
438+
}
439+
}
422440
return right_type;
423441
}
424442
}
443+
# Spawn operator: node spawn Walker() returns the walker instance
444+
if isinstance(expr.op, uni.Token) and expr.op.name == Tok.KW_SPAWN {
445+
return right_type;
446+
}
425447
# walrus assignment
426448
if isinstance(expr.op, uni.Token) and expr.op.name == Tok.WALRUS_EQ {
427449
# Walrus operator: (x := expr) assigns right to left and returns right's type

jac/jaclang/compiler/type_system/type_evaluator.impl/type_evaluator.impl.jac

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,18 @@ impl TypeEvaluator._get_type_of_expression_core(
15421542
}
15431543
return types.UnknownType();
15441544

1545+
case uni.EdgeRefTrailer():
1546+
# Edge traversal: [node-->], [node<--], [node->:Edge:->], etc.
1547+
# Returns list[NodeArchetype] (default) or list[EdgeArchetype] (edges_only)
1548+
# TODO: Infer more specific types from filters like [?:Profile]
1549+
if self.prefetch and self.prefetch.list_class {
1550+
list_type = self.prefetch.list_class.clone_as_instance();
1551+
# Return list[Any] so indexing returns Any (allows attribute access)
1552+
list_type.private.type_args = [types.AnyType()];
1553+
return list_type;
1554+
}
1555+
return types.UnknownType();
1556+
15451557
}
15461558
# TODO: More expressions.
15471559
return types.UnknownType();
Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,4 @@
1-
"""Gap: enumerate() does not infer element types correctly.
2-
3-
When using `for (idx, item) in enumerate(items)` where items is a typed list,
4-
the type checker should infer:
5-
- idx: int
6-
- item: the element type of the list
7-
8-
Currently, both idx and item are Unknown because:
9-
1. enumerate(items) returns enumerate with empty type_args (should be enumerate[Item])
10-
2. __iter__ returns Self which is not resolved
11-
3. __next__ returns tuple[int, _T] but _T is not specialized
12-
13-
Expected: Type errors for invalid assignments to idx and item.
14-
Actual: No type errors because types are Unknown.
15-
"""
1+
"""Test enumerate() type inference from iterable argument."""
162

173
node Item {
184
has id: int;
@@ -22,13 +8,8 @@ with entry {
228
items: list[Item] = [Item(id=1), Item(id=2), Item(id=3)];
239

2410
for (idx, item) in enumerate(items) {
25-
# When fixed: idx is int, so assigning to str should error
26-
wrong_idx: str = idx; # <-- Should Error: Cannot assign int to str
27-
28-
# When fixed: item is Item, so assigning to int should error
29-
wrong_item: int = item; # <-- Should Error: Cannot assign Item to int
30-
31-
# When fixed: item.id should work since item is Item
32-
print(item.id); # <-- Should work, currently errors with Unknown
11+
wrong_idx: str = idx; # Error: int -> str
12+
wrong_item: int = item; # Error: Item -> int
13+
print(item.id);
3314
}
3415
}
Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
1-
"""Gap: Generic type parameter inference from function/constructor arguments.
2-
3-
When calling a generic function or constructor, the type parameter should be
4-
inferred from the argument types.
5-
6-
For example:
7-
- enumerate(items: list[Item]) should return enumerate[Item]
8-
- zip(list[int], list[str]) should return zip[tuple[int, str]]
9-
10-
Currently, generic calls return the generic type without type_args populated,
11-
so the type parameter remains unresolved.
12-
13-
This is the root cause of the enumerate issue - without proper inference,
14-
enumerate(items) returns enumerate (no type_args) instead of enumerate[Item].
15-
"""
1+
"""Test generic type parameter inference from constructor arguments."""
162

173
node Item {
184
has value: int;
195
}
206

21-
# Test: Generic class constructor should infer type parameter
227
obj MyGeneric[T] {
238
has data: T;
249

@@ -28,22 +13,17 @@ obj MyGeneric[T] {
2813
}
2914

3015
with entry {
31-
# Test 1: enumerate should infer T from iterable
3216
items: list[Item] = [Item(value=1), Item(value=2)];
3317

34-
# enumerate(items) should be enumerate[Item]
35-
# __next__ should return tuple[int, Item]
18+
# enumerate(items) infers enumerate[Item]
3619
for (i, x) in enumerate(items) {
37-
# When fixed: x is Item, assigning to str should error
38-
wrong_x: str = x; # <-- Should Error: Cannot assign Item to str
39-
print(x.value); # <-- Should work, currently fails
20+
wrong_x: str = x; # Error: Item -> str
21+
print(x.value);
4022
}
4123

42-
# Test 2: Custom generic class should infer type parameter
24+
# MyGeneric(data=Item()) infers MyGeneric[Item]
4325
wrapper = MyGeneric(data=Item(value=42));
44-
# wrapper should be MyGeneric[Item]
4526
result = wrapper.get_data();
46-
# When fixed: result is Item, assigning to int should error
47-
wrong_result: int = result; # <-- Should Error: Cannot assign Item to int
48-
print(result.value); # <-- Should work if inference works
27+
wrong_result: int = result; # Error: Item -> int
28+
print(result.value);
4929
}

jac/tests/compiler/passes/main/test_checker_pass.jac

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,38 +2087,30 @@ test "final_union_type_no_crash" {
20872087
# =============================================================================
20882088
# Generic Type Inference Tests (Pyright-style)
20892089
# =============================================================================
2090-
"""Test enumerate() correctly infers element types from the iterable."""
20912090
test "enumerate_type_inference" {
20922091
program = JacProgram();
20932092
path = os.path.join(CHECKER_FIXTURES, "checker_gap_enumerate_type.jac");
20942093
mod = program.compile(path, options=NO_TC);
20952094
TypeCheckPass(ir_in=mod, prog=program);
2096-
# idx inference works (int), but item type from enumerate's __next__ -> tuple[int, _T]
2097-
# is not yet fully resolved through stdlib stub overloads.
2098-
# Fully resolved: 2 errors (int->str, Item->int). Current: 3 (idx works, item still Unknown).
2099-
assert len(program.errors_had) == 3 , f"Expected 3 type errors, got {len(
2095+
assert len(program.errors_had) == 2 , f"Expected 2 type errors, got {len(
21002096
program.errors_had
21012097
)}";
2102-
21032098
errors_str = "\n".join(e.pretty_print() for e in program.errors_had);
2104-
assert "Cannot assign int to str" in errors_str , "Expected error for idx: int assigned to str";
2099+
assert "Cannot assign int to str" in errors_str;
2100+
assert "Cannot assign Item to int" in errors_str;
21052101
}
21062102

2107-
"""Test generic class constructor infers type parameters from arguments."""
21082103
test "generic_call_inference" {
21092104
program = JacProgram();
21102105
path = os.path.join(CHECKER_FIXTURES, "checker_gap_generic_call_inference.jac");
21112106
mod = program.compile(path, options=NO_TC);
21122107
TypeCheckPass(ir_in=mod, prog=program);
2113-
# Custom generic inference works (MyGeneric[Item] correctly inferred from constructor args).
2114-
# enumerate inference still partial — item type from stdlib stub overloads not fully resolved.
2115-
# Fully resolved: 2 errors (Item->str, Item->int). Current: 3 (MyGeneric works, enumerate partial).
2116-
assert len(program.errors_had) == 3 , f"Expected 3 type errors, got {len(
2108+
assert len(program.errors_had) == 2 , f"Expected 2 type errors, got {len(
21172109
program.errors_had
21182110
)}";
2119-
21202111
errors_str = "\n".join(e.pretty_print() for e in program.errors_had);
2121-
assert "Cannot assign Item to int" in errors_str , "Expected error for result: Item assigned to int";
2112+
assert "Cannot assign Item to str" in errors_str;
2113+
assert "Cannot assign Item to int" in errors_str;
21222114
}
21232115

21242116
"""Test bidirectional type inference for container literals and type[X] covariance."""

jac/tests/runtimelib/fixtures/serve_api.jac

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ walker ListCompletedTasks {
6262

6363
walker GetTask {
6464
can fetch with Task entry {
65-
return here;
65+
report here;
6666
}
6767
}
6868

@@ -94,7 +94,7 @@ def greet(name: str = "World") -> str {
9494

9595
cl glob WELCOME_TITLE: str = "Runtime Test";
9696

97-
cl def:pub client_page() {
97+
cl def:pub client_page() -> JsxElement {
9898
return <section class="welcome">
9999
<h1>
100100
{WELCOME_TITLE}

0 commit comments

Comments
 (0)