Method-Level Generics — Design

Issue: #1242, split out of #1186.

Why

Type parameters existed only on classes. A generic helper needed a throwaway generic class, instantiated at every call with explicit type arguments (Swapper->New()<String>->Swap(...)). Research code is full of small generic helpers: argmax over a collection, swap, pick-first, map over a vector.

function : Max<T : Compare>(values : Vector<T>) ~ T { ... }
function : Swap<T>(a : T[], i : Int, j : Int) ~ Nil { ... }
function : First<T>(a : T[]) ~ T { return a[0]; }

best := Max(scores);        # T inferred: scores is a Vector<IntRef>, so best is an IntRef
name := First(names);       # a String

Decisions

Syntax

The type parameters follow the method name, exactly as on a class: Name<T, U : Bound>(...). Bounds, compound bounds and F-bounds parse as they do for classes.

Inference, not explicit type arguments

Type arguments are inferred from the call's arguments, which is what makes the feature worth having. A parameter typed T, T[] or C<..., T, ...> (any generic class) binds T to the argument's class.

Code generation: erasure

The same model as class generics: one body, and the type parameters are erased.

Scope of phase 1

TargetPhase 1
functions and methods of the program's own classesyes
type parameters on constructorsno (a class's own type parameters cover that)
primitive arguments (Max(3, 5))no: a type parameter takes an object, as class generics do (IntRef)
library methods (Vector->Map<R>)phase 2 (shipped): the .obl keeps the type-parameter marks

Phase 2: library methods (shipped)

A library's signatures carry the bound a type parameter was erased to (Max<T : Compare> reads Max(o.Collection.Vector|o.System.Compare)), not the parameter, so a caller in another binary could not infer anything: a library generic returned its bound.

Found on the way: 34 doc examples in gen_collect.obs used \(x : T) ~ R { return e; }, which does not parse. They are now \(T) ~ R : (x) => e, and every lambda example in the file compiles.

Tests: method_generics_library.obs (Map<R> with typed, inferred and function-value arguments, chained, in a loop), bad_method_generic_library_bound.obs, and check_lib_generic_methods.py, which builds a library of generic functions and methods and links a program against it in three modes.

Implementation

  1. Parser:
    • ParseMethod parses <...> after the name with the existing ParseGenericClasses.
    • While the method is parsed, ParseType maps each of those names to its bound type marked with the parameter name (Type::SetMethodGeneric). That covers the signature, locals, casts and generic arguments (Vector<T>).
  2. Analyzer (ResolveMethodCall, program classes): for a candidate with marked types:
    • infer the bindings from the arguments (InferMethodGenerics);
    • substitute them into the parameter types the arguments are matched against;
    • check each binding against its bound.

The bindings stay on the call so that the call's value takes the substituted return type.

  1. Emitter: unchanged.

Tests

method_generics.obs covers:

The bad_method_generic_*.obs files cover: