A $T parameter makes a proc a blank for a type the compiler fills in from the call. You write one definition; at every call the compiler reads the argument's type, fixes T to it, and stamps out a fresh, fully concrete copy of the proc for that type. Pick a call below and watch which copy gets pressed.
All three copies exist in the binary at once — one for int, one for f32, one for string. Each uses the right machine instructions for its type: integer compare, floating-point compare, the runtime's string compare. The call site jumps straight into its own copy — no type carried along at run time, nothing decided while the program runs.
Level 1 stamped procs. A $T on a struct stamps types — and they are genuinely different types with different memory layouts, not one shape with a label swapped. The proof is that they have different size_of. Pick a T and watch the bytes change.
Here is Slot :: struct($T: typeid) { value: T, live: u8 } — one T by value plus a one-byte live flag. The stamped size is whatever that T needs, plus the flag, plus alignment padding.
The consequence: because each stamp is its own type, the compiler keeps them apart the same way it keeps int and string apart. Try to assign one stamped struct into a variable of another and the build stops you — it even prints both stamped names:
a: Bag(int) b: Bag(string) b = a // two different stamped types Error: Cannot assign value 'a' of type 'Bag($T=int)' to 'Bag($T=string)' in assignment
So far $T stamped on a type. A $N stamps on a compile-time value — most often a length. filled :: proc($N: int, val: $T) -> [N]T bakes the number N straight into the return type, so different Ns produce arrays of different lengths — and different types.
N is part of the type, not a runtime length: filled(3, 7) returns a [3]int and filled(5, 7) returns a [5]int — two distinct fixed-array types, each with its length fixed before the program runs. The loop bound 0 ..< N is a constant the compiler can see.
The catch: a $N parameter demands a value the compiler already knows. Hand it a runtime variable and there is no length to bake into the type, so the build fails — and the message says exactly that:
n := 3 // an ordinary runtime variable arr := make_constant_array(n, 3) // $N wants a constant, not n Error: Expected a constant value for this polymorphic name parameter, got n
The fix is to pass a literal or a constant: make_constant_array(3, 3), or SIZE :: 3 then make_constant_array(SIZE, 3). The whole point of $N is that the value is settled at stamp time, so it can shape the type itself.
The last level is the gate on the stamp. A blank that accepts any type is too loose — most generic bodies only make sense for some types. A where clause is a compile-time predicate the compiler checks before it ever stamps the body, so an unsupported type is turned away early, with a message that names the type.
total :: proc(arr: []$T) -> T where ...type_is_numeric(T) adds up a slice. The where says "T must be numeric." Run it on three slices and watch the gate decide:
Hand total a slice of strings and the where check fails up front. The build never stamps the body — it stops at the gate, prints the constraint that failed, and reports the offending type as T :: string:
total :: proc(arr: []$T) -> T where intrinsics.type_is_numeric(T) { ... } words := []string{"a", "b", "c"} total(words) Error: 'where' clause evaluated to false: intrinsics.type_is_numeric(T) T :: string;
The other guard is inference itself. When one $T feeds two arguments, both must agree on the same stamped type — the compiler won't quietly bend one to fit. Call max(3, 2.5): the first argument fixes T to int, and rather than silently chop 2.5 down to match, the build refuses:
max(3, 2.5) // T fixed to int by the 3; 2.5 doesn't fit Error: '2.5' truncated to 'int', got 2.500000
The emergent payoff: a generic proc is not a hole you can drop anything into and hope. The where clause turns "this only works for numbers" from a comment into a checked precondition, and shared-$T inference turns "both arguments are the same type" into a checked fact. A wrong call is caught at the stamp — at compile time, with the failing type named — so a generic stays exactly as safe as a hand-written proc for one concrete type, while you only wrote it once.
That's the arc: L1 $T is a blank the compiler fills from the call, stamping one concrete copy per type → L2 stamped on a struct it makes real, distinctly-sized types, not one erased shape → L3 a $N stamps a compile-time value into the type itself, so it must be a constant → L4 a where clause gates the stamp, rejecting unsupported types before the body is ever pressed.