feat(gen): generic calls with explicit args and pointer-parameter inference

Three gaps kept `mod.f[T](x)` from compiling:

- get_func_name now unwraps a Subscript callee to its base name, so an
  expression statement like `f[int](x)` / `mod.f[int](x)` reaches the
  normal call path instead of failing name resolution. Generic class
  constructors and type subscripts still return None, keeping their
  dedicated paths.
- the generic-call dispatcher resolves `Attribute` bases inside the
  subscript (`temp.array2list[int]`) to the defining module's SHA1, and
  treats it as a module-qualified template lookup.
- inference understands pointer/array annotations around the type
  parameter: `_annotation_refers_type_param` walks PtrT/ArrayT wrappers
  and tuple slices, and inference peels as many pointer/array layers off
  the argument as the parameter annotation wraps around T, so
  `array2list(l, ...)` with `array: T*` and `l: []int` infers int.

Self-host: 110/110 with the new compiler building itself; GenericTest,
GenericTest3, GenericTest4, VectorGenericTest and ListTest pass.
This commit is contained in:
TermiNexus committed 2026-11-02 09:07:32 +08:00
1 parent 40e3388dc3
commit f57a7d962c
4 files changed
+121 -5

No files matched your search

+12
View File
@@ -1016,6 +1016,18 @@ novirtual class CallGenCtor(CallGenBase.CallGenBase):
nm_a: ast.Name* = ast.Name*(node)
if nm_a.id is not None and string.strcmp(nm_a.id, tp_name) == 0: return 1
return 0
if k == ast.ASTKind.Subscript:
# `T*` / `[]T` parse to PtrT[T] / ArrayT[T, N]; the type parameter sits in the slice.
sub_a: ast.Subscript* = ast.Subscript*(node)
return self._annotation_refers_type_param(sub_a.slice, tp_name)
if k == ast.ASTKind.Tuple:
# ArrayT[T, N]: the element type is the first element, the length the second.
tup_a: ast.Tuple* = ast.Tuple*(node)
if tup_a.elts is None: return 0
tn_a: sizet = len(tup_a.elts)
for ti_a in range(int(tn_a)):
if self._annotation_refers_type_param(tup_a.elts.get(ti_a), tp_name) != 0: return 1
return 0
if k == ast.ASTKind.BinOp:
bop_a: ast.BinOp* = ast.BinOp*(node)
if self._annotation_refers_type_param(bop_a.left, tp_name) != 0: return 1
+14 -3
View File
@@ -39,10 +39,20 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
fk: int = cl.func.kind()
if fk == ast.ASTKind.Subscript:
sub: ast.Subscript* = ast.Subscript*(cl.func)
explicit_slice = sub.slice
if sub.value is not None and sub.value.kind() == ast.ASTKind.Name:
sn: ast.Name* = ast.Name*(sub.value)
tpl_name = sn.id
explicit_slice = sub.slice
elif sub.value is not None and sub.value.kind() == ast.ASTKind.Attribute:
# `mod.f[T](...)`: the template lives in the imported module.
aq: ast.Attribute* = ast.Attribute*(sub.value)
if aq.attr is not None and aq.value is not None and aq.value.kind() == ast.ASTKind.Name:
mq: ast.Name* = ast.Name*(aq.value)
if mq.id is not None and trans._imported_modules is not None:
if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mq.id) != 0: tpl_mod_sha1 = self._lookup_module_sha1(mq.id)
if tpl_mod_sha1 is not None:
is_attr_call = 1
tpl_name = aq.attr
elif fk == ast.ASTKind.Name: tpl_name = func_name
elif fk == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
@@ -97,13 +107,14 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
tp_name: char* = tp_names.get(tpi)
ta: char* = None
slot: int = self.Gen.ClassGenGeneric._find_generic_func_param_slot(template_fd, tp_name)
if slot >= 0 and sizet(slot) < can: ta = self.Gen.ClassGenGeneric._infer_generic_func_type_arg(pool, cargs.get(sizet(slot)), tpl_name, trans)
pann: ast.AST* = self.Gen.ClassGenGeneric._generic_func_param_annot(template_fd, slot)
if slot >= 0 and sizet(slot) < can: ta = self.Gen.ClassGenGeneric._infer_generic_func_type_arg(pool, cargs.get(sizet(slot)), tpl_name, trans, tp_name, pann)
if ta is None and slot >= 0:
# Keyword arg: locate by template param name (positional wins; -1 if slot taken)
kwi: int = self._generic_func_kw_arg_index(template_fd, slot, cl.keywords, int(can))
if kwi >= 0:
kwn: ast.Keyword* = ast.Keyword*(cl.keywords.get(sizet(kwi)))
if kwn is not None and kwn.value is not None: ta = self.Gen.ClassGenGeneric._infer_generic_func_type_arg(pool, kwn.value, tpl_name, trans)
if kwn is not None and kwn.value is not None: ta = self.Gen.ClassGenGeneric._infer_generic_func_type_arg(pool, kwn.value, tpl_name, trans, tp_name, pann)
if ta is None and ctx_args is not None and sizet(tpi) < len(ctx_args): ta = ctx_args.get(tpi)
if ta is None:
self.Gen.TypeGenErr.fatal_error(cl, f"Cannot infer type argument {tpi + 1} of generic function '{tpl_name}' from the arguments (type parameter {tp_name});" f"silent default to int is forbidden. Annotate explicitly, e.g. {tpl_name}[Type](...), or annotate the corresponding argument", "TypeError")
+85 -2
View File
@@ -157,6 +157,19 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
if knm is not None and string.strcmp(knm, key) == 0: return nodes_local.get(i)
return None
def _has_generic_func_named(self, name: str) -> int:
"""1 when any module registered a generic function template with this bare name."""
if name is None or self.GenericFuncKeys is None: return 0
keys_local: list[str]* = self.GenericFuncKeys
kn: sizet = len(keys_local)
for i in range(int(kn)):
knm: str = keys_local.get(i)
if knm is None: continue
dot: str = string.strrchr(knm, '.')
if dot is None: continue
if string.strcmp(dot + 1, name) == 0: return 1
return 0
def _find_generic_func_template_sha1(self, key: str) -> str:
"""Find a generic function template's source module SHA1 by key; None when absent."""
if self.GenericFuncKeys is None or key is None: return None
@@ -227,9 +240,79 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
if self.Gen.CallGenCtor._annotation_refers_type_param(ag.annotation, tp_name) != 0: return int(ai)
return -1
def _infer_generic_func_type_arg(self, pool: memhub.MemBuddy*, arg_node: ast.AST*, func_name: str, trans: HT.Translator*) -> str:
def _generic_func_param_annot(self, fd: ast.FunctionDef*, slot: int) -> ast.AST*:
"""Annotation of 0-based parameter slot; None when absent."""
if fd is None or slot < 0 or fd.args is None: return None
if fd.args.kind() != ast.ASTKind.Arguments: return None
ags: ast.Arguments* = ast.Arguments*(fd.args)
if ags.args is None or sizet(slot) >= len(ags.args): return None
pa: ast.AST* = ags.args.get(sizet(slot))
if pa is None or pa.kind() != ast.ASTKind.Arg: return None
return ast.Arg*(pa).annotation
def _ptr_wrap_depth(self, ann: ast.AST*, tp_name: str) -> int:
"""Pointer/array layers wrapping bare tp_name (`T*` -> 1, `[]T` -> 1); -1 when not a wrapper."""
if ann is None or tp_name is None: return -1
k: int = ann.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(ann)
if nm.id is not None and string.strcmp(nm.id, tp_name) == 0: return 0
return -1
if k != ast.ASTKind.Subscript: return -1
sub: ast.Subscript* = ast.Subscript*(ann)
if sub.value is None or sub.slice is None: return -1
wn: str = None
if sub.value.kind() == ast.ASTKind.Name: wn = ast.Name*(sub.value).id
elif sub.value.kind() == ast.ASTKind.Attribute: wn = ast.Attribute*(sub.value).attr
if wn is None: return -1
if string.strcmp(wn, "ArrayT") == 0:
# []T / [N]T: element type first, length second.
if sub.slice.kind() != ast.ASTKind.Tuple: return -1
tup: ast.Tuple* = ast.Tuple*(sub.slice)
if tup.elts is None or len(tup.elts) == 0: return -1
inner_a: int = self._ptr_wrap_depth(tup.elts.get(0), tp_name)
if inner_a < 0: return -1
return inner_a + 1
inner: int = self._ptr_wrap_depth(sub.slice, tp_name)
if inner < 0: return -1
if string.strcmp(wn, "PtrT") == 0: return inner + 1
# Descriptor / value-reference wrappers (ConstT / RefT / TempRefT) add no pointer layer.
return inner
def _infer_arg_pointee_name(self, pool: memhub.MemBuddy*, arg_node: ast.AST*, strip: int, trans: HT.Translator*) -> str:
"""Type name reached by peeling `strip` pointer/array layers off a Name argument."""
if arg_node is None or strip <= 0 or trans is None: return None
if arg_node.kind() != ast.ASTKind.Name: return None
nm: ast.Name* = ast.Name*(arg_node)
if nm.id is None: return None
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if ve is None or ve.Spec is None: return None
sp: TSpec.TypeSpec* = ve.Spec
d: int = strip
while d > 0 and sp is not None:
if sp.Kind == TSpec.TS_ARRAY or sp.Kind == TSpec.TS_PTR: sp = sp.Elem
else: return None
d -= 1
if sp is None: return None
txt: str = self._infer_scalar_type_name_from_spec(pool, sp)
if txt is not None: return txt
cn: str = sp.SourceName
if cn is None: cn = sp.BaseName
if cn is None: return None
fi: str = None
if trans is not None: fi = trans._from_imports
return self.Gen.CallGenCtor._ptr_layer_type_arg(pool, cn, sp.PtrLevel, fi)
def _infer_generic_func_type_arg(self, pool: memhub.MemBuddy*, arg_node: ast.AST*, func_name: str, trans: HT.Translator*, tp_name: str = None, param_annot: ast.AST* = None) -> str:
"""Infer a generic function's type argument name from a call argument AST; None when unknown."""
if arg_node is None: return None
# `T*` / `[]T` parameters: the argument's pointer/array layer is the decayed parameter,
# not part of T. Peel exactly as many layers as the annotation wraps around T.
if tp_name is not None and param_annot is not None:
strip: int = self._ptr_wrap_depth(param_annot, tp_name)
if strip > 0:
pointee: str = self._infer_arg_pointee_name(pool, arg_node, strip, trans)
if pointee is not None: return pointee
k: int = arg_node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(arg_node)
@@ -240,7 +323,7 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
# Unary +/- constant (clamp(-3, ...)): the type is determined by the operand
if k == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(arg_node)
return self._infer_generic_func_type_arg(pool, uo.operand, func_name, trans)
return self._infer_generic_func_type_arg(pool, uo.operand, func_name, trans, tp_name, param_annot)
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(arg_node)
if nm.id is not None and trans is not None:
+10
View File
@@ -51,6 +51,16 @@ novirtual class ExprGenBase:
elif k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(func_node)
return at.attr
elif k == ast.ASTKind.Subscript:
# `f[T](...)` / `mod.f[T](...)`: the callable name lives in the subscript base.
# Generic class constructors and type subscripts (`list[int](pool)`, `rawptr[T](x)`)
# must keep returning None so their dedicated paths still run.
sub: ast.Subscript* = ast.Subscript*(func_node)
base: str = self.get_func_name(sub.value)
if base is None: return None
if self.Gen.ClassGenGeneric._find_generic_template(base) is not None: return None
if self.Gen.ClassGenGeneric._has_generic_func_named(base) != 0: return base
return None
return None
# Escape an LLVM IR string literal.
def escape_llvm_string(self, pool: memhub.MemBuddy*, src: str) -> str: