Documentation
¶
Overview ¶
Package narrow provides type narrowing operations for control flow refinement.
Type narrowing reduces types based on runtime checks, enabling sound type refinement after conditionals. For example, "if x ~= nil" narrows x's type by removing nil, giving a more precise type in the then-branch.
Lua Truthiness Model ¶
Lua has a simple truthiness model: only nil and false are falsy; all other values (including 0, "", empty tables, and userdata) are truthy. This package implements Lua's truthiness semantics for type narrowing, which differs from many other languages where 0 and "" might be falsy.
Architecture ¶
The package is organized into several functional areas:
Core narrowing engine (narrow.go): The [narrowConfig] pattern provides a recursive traversal framework for implementing narrowing operations. Each operation defines handlers for optionals, unions, and leaf types.
Truthiness operations (narrow.go): ToTruthy, ToFalsy, RemoveNil, and RemoveFalse implement Lua's truthiness-based narrowing.
Kind-based narrowing (narrow.go): FilterByKind and ExcludeKind narrow types based on Lua's typeof results.
Type-based narrowing (narrow.go): ExcludeType and Intersect narrow using type relationships.
TypeKey narrowing (type_key.go, key_narrow.go): Compact type identifiers enable efficient storage and comparison of type constraints.
Filter-based narrowing (filter.go): The TypeMatcher pattern enables flexible predicate-based filtering.
Field-based narrowing (filter.go, match.go): ByFieldLiteral and ExcludeByFieldLiteral implement discriminated union narrowing.
Resolver interface (resolver.go): Abstracts field and index lookup to decouple narrowing from the full type checker.
Integration with Type System ¶
The narrow package is used by the constraint solver during control flow analysis. When the solver encounters conditionals, it generates narrowing constraints that are applied using these operations:
- if x then: Apply ToTruthy to x in the then-branch.
- if not x then: Apply ToFalsy to x in the then-branch.
- if type(x) == "number" then: Apply FilterByKind with kind.Number.
- if x.kind == "a" then: Apply ByFieldLiteral with the literal.
Soundness Guarantees ¶
All narrowing operations maintain type system soundness:
- Never over-narrow: If exclusion would produce Never, preserve original.
- Respect subtyping: TypesOverlap uses bidirectional subtype checks.
- Handle placeholders: Any/Unknown types are handled conservatively.
- Distinguish exact vs contains: Negative field checks use exact matching.
Example Usage ¶
// Narrow optional to non-nil after guard.
optStr := typ.NewOptional(typ.String)
nonNil := narrow.RemoveNil(optStr) // typ.String
// Narrow union by typeof check.
union := typ.NewUnion(typ.String, typ.Number, typ.Nil)
nums := narrow.FilterByKind(union, kind.Number) // typ.Number
// Narrow discriminated union by field check.
narrowed := narrow.ByFieldLiteral(eventUnion, "kind", typ.LiteralString("click"), resolver)
Performance Considerations ¶
Narrowing operations are designed for efficiency:
- TypeKey uses hashing for fast type identity comparison.
- Narrowing short-circuits when types are unchanged.
- Union filtering is linear in the number of members.
- No allocations when narrowing produces the same type.
Index ¶
- func ByFieldFalsy(t typ.Type, field string, resolver Resolver) typ.Type
- func ByFieldLiteral(t typ.Type, field string, lit *typ.Literal, resolver Resolver) typ.Type
- func ByFieldTruthy(t typ.Type, field string, resolver Resolver) typ.Type
- func ByTypeKey(t typ.Type, key TypeKey, resolve TypeResolver) typ.Type
- func ExcludeByFieldLiteral(t typ.Type, field string, lit *typ.Literal, resolver Resolver) typ.Type
- func ExcludeByTypeKey(t typ.Type, key TypeKey, resolve TypeResolver) typ.Type
- func ExcludeKind(t typ.Type, target kind.Kind) typ.Type
- func ExcludeType(t typ.Type, excluded typ.Type) typ.Type
- func FieldIsExactlyLiteral(t typ.Type, field string, lit *typ.Literal, resolver Resolver) bool
- func FieldMatchesLiteral(t typ.Type, field string, lit *typ.Literal, resolver Resolver) bool
- func FilterByKind(t typ.Type, target kind.Kind) typ.Type
- func FilterByMatch(t typ.Type, matches TypeMatcher, exclude bool) typ.Type
- func Intersect(a, b typ.Type) typ.Type
- func KindMatches(t typ.Type, target kind.Kind) bool
- func RemoveFalse(t typ.Type) typ.Type
- func RemoveNil(t typ.Type) typ.Type
- func ToFalsy(t typ.Type) typ.Type
- func ToTruthy(t typ.Type) typ.Type
- func TypeForKind(k kind.Kind) typ.Type
- func TypeIsExactlyLiteral(t typ.Type, lit *typ.Literal) bool
- func TypesOverlap(a, b typ.Type) bool
- type Resolver
- type TypeKey
- type TypeKeyKind
- type TypeMatcher
- type TypeResolver
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ByFieldFalsy ¶ added in v1.5.2
ByFieldFalsy narrows a type to members where a field can be falsy.
func ByFieldLiteral ¶
ByFieldLiteral narrows a type to members where a field equals a literal value.
This function is the primary mechanism for discriminated union narrowing in Lua. When a guard checks a specific field value (e.g., "if x.kind == 'a'"), this function narrows the type to only those union variants where the field matches the literal.
Discriminated Unions ¶
A discriminated union uses a common field (discriminant) to distinguish between variants. For example:
type Event = {kind: "click", x: number, y: number}
| {kind: "key", code: string}
After checking "event.kind == 'click'", the type narrows to just the click variant, enabling type-safe access to x and y.
Parameters ¶
- t: The type to narrow (typically a union of records).
- field: The discriminant field name.
- lit: The literal value to match against.
- resolver: Provides field type lookup for the type.
Returns ¶
Returns the narrowed type containing only variants where the field can match the literal. Returns the original type unchanged if any parameter is nil/empty or if the resolver is not provided.
Example ¶
recA := typ.NewRecord().Field("kind", typ.LiteralString("a")).Build()
recB := typ.NewRecord().Field("kind", typ.LiteralString("b")).Build()
union := typ.NewUnion(recA, recB)
narrowed := ByFieldLiteral(union, "kind", typ.LiteralString("a"), resolver)
// narrowed is recA
func ByFieldTruthy ¶ added in v1.5.2
ByFieldTruthy narrows a type to members where a field can be truthy.
func ByTypeKey ¶
ByTypeKey narrows a type to match a specific TypeKey identity.
This function is the primary entry point for type-based narrowing after typeof checks in Lua. It handles both built-in type checks (type(x) == "number") and user-defined type checks (via hash-based keys).
Builtin Keys ¶
For built-in type keys (TypeKeyBuiltin), the function:
- Converts the key name to a kind via kind.FromString.
- Delegates to FilterByKind to narrow the type.
This handles Lua's typeof semantics where "table" matches multiple kinds (Record, Map, Array, etc.) and "number" matches both Number and Integer.
Hash Keys ¶
For hash-based type keys (TypeKeyHash), the function:
- Uses the resolver to recover the full type.
- Computes the intersection of the base type and resolved type.
This enables narrowing to specific record or interface types.
Edge Cases ¶
- Nil input: Returns nil.
- Zero key: Returns original type unchanged.
- Unknown builtin kind: Returns original type unchanged.
- Nil resolver for hash key: Returns original type unchanged.
- Nil resolution: Returns original type unchanged.
Examples ¶
// Narrow union to only number members.
key := BuiltinTypeKey("number")
narrowed := ByTypeKey(typ.NewUnion(typ.String, typ.Number), key, nil)
// narrowed is typ.Number
// Narrow to a specific record type.
key := HashTypeKey(myRecord.Hash())
narrowed := ByTypeKey(union, key, resolver)
func ExcludeByFieldLiteral ¶
ExcludeByFieldLiteral excludes union members where a field exactly equals a literal.
This function handles negative discriminant checks (field ~= value). Unlike ByFieldLiteral, it only excludes types where the field IS exactly the literal, not types where the field might contain it.
Exact vs Contains Semantics ¶
The distinction between exact and contains matching is crucial for soundness:
- Exact: field type is the singleton literal (e.g., kind: "a").
- Contains: field type includes the literal (e.g., kind: string).
For negative checks (x.kind ~= "a"), we can only exclude variants where kind is exactly "a". A variant where kind is string cannot be excluded because it might hold any string value, not just "a".
Parameters ¶
- t: The type to narrow.
- field: The discriminant field name.
- lit: The literal value to exclude.
- resolver: Provides field type lookup.
Returns ¶
Returns the type with variants removed where the field is exactly the literal. Variants with broader field types (like string) are preserved.
Example ¶
recA := typ.NewRecord().Field("kind", typ.LiteralString("a")).Build()
recB := typ.NewRecord().Field("kind", typ.String).Build()
union := typ.NewUnion(recA, recB)
narrowed := ExcludeByFieldLiteral(union, "kind", typ.LiteralString("a"), resolver)
// narrowed is recB (recA excluded, recB kept because kind: string is broader)
func ExcludeByTypeKey ¶
ExcludeByTypeKey removes types matching a TypeKey from the input type.
This function is the negative counterpart to ByTypeKey. It handles exclusion after negative typeof checks (type(x) ~= "number").
Builtin Keys ¶
For built-in type keys, delegates to ExcludeKind to remove matching members.
Hash Keys ¶
For hash-based keys, uses the resolver to recover the full type and excludes it via ExcludeType.
Never Preservation ¶
If exclusion would result in Never (all members excluded), returns the original type unchanged. This prevents over-narrowing when the type check cannot definitively exclude all possibilities.
This is important for soundness: if we have type "any" and check "type(x) ~= 'number'", we cannot narrow to anything useful because "any" could be anything, and excluding number from any still leaves everything except number (which we cannot represent precisely).
Edge Cases ¶
- Nil input: Returns nil.
- Zero key: Returns original type unchanged.
- Unknown builtin kind: Returns original type unchanged.
- Nil resolver for hash key: Returns original type unchanged.
- Nil resolution: Returns original type unchanged.
- Exclusion produces Never: Returns original type (preservation).
Examples ¶
// Exclude string from a union.
key := BuiltinTypeKey("string")
narrowed := ExcludeByTypeKey(typ.NewUnion(typ.String, typ.Number), key, nil)
// narrowed is typ.Number
// Excluding the only member preserves original.
narrowed := ExcludeByTypeKey(typ.String, key, nil)
// narrowed is typ.String (not Never)
func ExcludeKind ¶
ExcludeKind removes types matching the target kind from a type.
This operation narrows after a negative typeof check like "type(x) ~= 'string'". It removes all union members whose kind matches the target.
Lua Kind Mapping ¶
In Lua, typeof returns: "nil", "boolean", "number", "string", "function", "table", "thread", "userdata". The kind.Record matches all table-like types (Record, Map, Array, Tuple, Interface, Intersection).
Behavior by Type ¶
- Union: Removes members matching target kind.
- Optional: If target is Nil, returns inner; else recurses.
- Placeholder (Any, Unknown): Returns unchanged.
- Interface: Returns unchanged (interfaces need type-based exclusion).
- Other: Returns Never if kind matches; else unchanged.
Examples ¶
ExcludeKind(typ.NewUnion(typ.String, typ.Number), kind.String) // Number ExcludeKind(typ.NewOptional(typ.String), kind.Nil) // String
func ExcludeType ¶
ExcludeType removes union members that overlap with the excluded type.
For discriminated union narrowing, this operation removes variants that match a specific type after a negative type check.
Behavior by Type ¶
- Union: Removes members that overlap with excluded; returns remaining.
- Optional<T>: If T overlaps with excluded, returns nil; else unchanged.
- Placeholder (Any, Unknown): Returns unchanged (cannot narrow).
- Other: Returns Never if overlaps with excluded; else unchanged.
Examples ¶
ExcludeType(typ.NewUnion(typ.String, typ.Number), typ.String) // Number ExcludeType(typ.String, typ.String) // Never ExcludeType(typ.String, typ.Number) // String
func FieldIsExactlyLiteral ¶
FieldIsExactlyLiteral returns true only if the field type is exactly the literal.
This function checks whether a type's field has a singleton literal type that matches the given literal exactly. It returns false for broader types that contain the literal as a possible value.
Use Case ¶
This is used by ExcludeByFieldLiteral to implement sound negative narrowing. Only types where the field is provably the excluded literal can be removed; types where the field might be something else must be kept.
Parameters ¶
- t: The type to check (typically a record).
- field: The field name to look up.
- lit: The literal to compare against.
- resolver: Provides field type lookup.
Returns ¶
Returns true if:
- The field exists on the type.
- The field's type is exactly the literal (via TypeIsExactlyLiteral).
Returns false if any parameter is nil, the field doesn't exist, or the field type is broader than the literal.
func FieldMatchesLiteral ¶
FieldMatchesLiteral checks if a type's field can match a literal value.
This function is the core predicate for discriminated union narrowing. It determines whether a specific union variant should be kept when narrowing based on a field value check.
Matching Semantics ¶
A field "matches" a literal if the literal is a valid value for that field. This is determined by typ.TypeMatchesLiteral, which checks if the literal is a subtype of the field type or vice versa.
For example:
- Field type "a" matches literal "a" (exact match).
- Field type string matches literal "a" (literal is subtype).
- Field type "a" | "b" matches literal "a" (literal in union).
- Field type number does NOT match literal "a" (incompatible).
Use Case ¶
This function is used by ByFieldLiteral to filter union variants. For a check like "x.kind == 'a'", we want to keep variants where the kind field can possibly equal "a".
Parameters ¶
- t: The type to check (typically a record in a discriminated union).
- field: The name of the discriminant field.
- lit: The literal value being checked.
- resolver: Provides field type lookup for the type.
Returns ¶
Returns true if:
- All parameters are non-nil.
- The field exists on the type.
- The field's type is compatible with the literal.
Returns false if any parameter is nil, the field doesn't exist, or the field type is incompatible with the literal.
Example ¶
rec := typ.NewRecord().Field("kind", typ.LiteralString("a")).Build()
lit := typ.LiteralString("a")
matches := FieldMatchesLiteral(rec, "kind", lit, resolver) // true
rec := typ.NewRecord().Field("kind", typ.Number).Build()
matches := FieldMatchesLiteral(rec, "kind", lit, resolver) // false
func FilterByKind ¶
FilterByKind narrows a type to keep only parts matching the target kind.
This operation narrows after a positive typeof check like "type(x) == 'string'". It is the inverse of ExcludeKind.
Behavior by Type ¶
- Placeholder (Any, Unknown): Returns the canonical type for the kind.
- Union: Keeps members matching target kind.
- Optional: If target is Nil, returns nil; else recurses into inner.
- Other: Returns type if kind matches; else Never.
Examples ¶
FilterByKind(typ.NewUnion(typ.String, typ.Number), kind.String) // String FilterByKind(typ.Any, kind.Number) // Number FilterByKind(typ.NewOptional(typ.String), kind.Nil) // nil
func FilterByMatch ¶
FilterByMatch filters a type by applying a matcher predicate with configurable polarity (narrow vs exclude).
This is the core filtering function used by field-based and type-key-based narrowing. It provides a flexible way to narrow types based on arbitrary predicates.
Narrowing Mode (exclude=false) ¶
Keeps only type members where the matcher returns true:
- Union: keeps members where matcher(member) == true.
- Optional: returns inner type if matcher(inner) == true, else Never.
- Intersection: returns type if matcher(intersection) == true, else Never.
- Other: returns type if matcher(type) == true, else Never.
Exclusion Mode (exclude=true) ¶
Removes type members where the matcher returns true:
- Union: keeps members where matcher(member) == false.
- Optional: returns Nil if matcher(inner) == true, else unchanged.
- Intersection: returns Never if matcher(intersection) == true, else unchanged.
- Other: returns Never if matcher(type) == true, else unchanged.
Type Unwrapping ¶
Aliases and instantiated generics are unwrapped before matching. The matcher receives the underlying concrete type, not wrapper types. This ensures consistent behavior regardless of how types are aliased or instantiated.
Intersection Handling ¶
Intersections are treated atomically: the matcher is applied to the whole intersection, not distributed over members. This is because intersection members cannot be independently filtered without changing the type's meaning.
Examples ¶
// Keep only string members from a union.
result := FilterByMatch(union, func(t typ.Type) bool {
return t == typ.String
}, false)
// Exclude number members from a union.
result := FilterByMatch(union, func(t typ.Type) bool {
return t.Kind() == kind.Number
}, true)
func Intersect ¶
Intersect computes the intersection of two types.
Type intersection produces the type containing values that belong to both input types. This is used for narrowing when multiple constraints apply to the same variable.
Algorithm ¶
- If either type is a placeholder (Any, Unknown), return the other.
- Unwrap aliases and instantiated generics.
- If either is an intersection, merge members.
- If a <: b, return a (more specific); if b <: a, return b.
- For unions, filter to overlapping members.
- Otherwise, create a new intersection type.
Examples ¶
Intersect(typ.String, typ.Any) // String Intersect(typ.NewUnion(typ.String, typ.Number), typ.String) // String Intersect(typ.String, typ.Number) // String & Number
func KindMatches ¶
KindMatches checks if a type matches the target Lua typeof kind.
This function implements Lua's typeof semantics for type narrowing. It handles special cases where multiple type system kinds map to a single Lua typeof result.
Special Cases ¶
- kind.Record matches: Record, Map, Array, Tuple, Interface, Intersection (all are "table" in Lua typeof).
- kind.Number matches: Number and Integer (integer is a subtype of number).
- Instantiated types: Match based on the underlying generic body's kind.
Examples ¶
KindMatches(typ.String, kind.String) // true KindMatches(typ.Integer, kind.Number) // true KindMatches(typ.NewArray(typ.String), kind.Record) // true (arrays are tables)
func RemoveFalse ¶
RemoveFalse removes literal false from a type, keeping only truthy boolean values.
This operation narrows boolean types after checks that exclude false. Combined with RemoveNil, it implements full truthiness narrowing.
Behavior by Type ¶
- Literal false: Returns Never.
- Literal true: Returns true (unchanged).
- Boolean: Returns true (the truthy boolean literal).
- Optional<T>: Recurses into T; if T becomes Never, returns nil.
- Union: Filters out members that become Never after recursion.
- Other types: Returns unchanged (non-boolean types are unaffected).
Examples ¶
RemoveFalse(typ.LiteralBool(false)) // Never RemoveFalse(typ.Boolean) // true RemoveFalse(typ.String) // String (unchanged)
func RemoveNil ¶
RemoveNil removes nil from a type, producing the non-nullable subset.
This operation is used when narrowing after a "x ~= nil" or "x" truthiness check in Lua. It handles all type structures that can contain nil:
Behavior by Type ¶
- nil: Returns Never (removing nil from nil leaves nothing).
- Optional<T>: Returns T (the inner non-nil type).
- Union containing nil: Returns union without nil members.
- Union containing Optional<T>: Unwraps optional, keeps T.
- Other types: Returns unchanged (already non-nullable).
Examples ¶
RemoveNil(typ.Nil) // Never RemoveNil(typ.NewOptional(typ.String)) // String RemoveNil(typ.NewUnion(typ.String, typ.Nil)) // String RemoveNil(typ.Number) // Number (unchanged)
func ToFalsy ¶
ToFalsy narrows a type to its falsy subset, keeping only nil and false.
In Lua, only nil and false are falsy. This operation produces the type that remains after a truthiness check fails (the "else" branch of "if x then").
Behavior by Type ¶
- nil: Returns nil.
- Literal false: Returns false.
- Literal true: Returns Never (true is truthy).
- Boolean: Returns false (the falsy boolean literal).
- Optional<T>: Returns nil | ToFalsy(T).
- Union: Collects falsy parts from each member.
- Placeholder types (Any, Unknown): Returns nil | false (conservative).
- FieldAccess, IndexAccess: Returns nil | false (deferred types).
- Other types: Returns Never (all other types are truthy).
Examples ¶
ToFalsy(typ.NewOptional(typ.String)) // nil ToFalsy(typ.Boolean) // false ToFalsy(typ.String) // Never ToFalsy(typ.Any) // nil | false
func ToTruthy ¶
ToTruthy narrows a type to its truthy subset by removing nil and false.
In Lua, only nil and false are falsy. This operation produces the type that remains after a truthiness check succeeds (the "then" branch of "if x then").
ToTruthy is equivalent to RemoveFalse(RemoveNil(t)).
Examples ¶
ToTruthy(typ.NewOptional(typ.String)) // String ToTruthy(typ.Boolean) // true ToTruthy(typ.NewUnion(typ.String, typ.Nil)) // String ToTruthy(typ.NewUnion(typ.Nil, typ.False)) // Never
func TypeForKind ¶
TypeForKind returns the canonical type for a Lua typeof kind.
This mapping is used when narrowing placeholder types (Any, Unknown) by kind. It returns the broadest type for each Lua typeof result.
Mapping ¶
- Nil: typ.Nil
- Boolean: typ.Boolean
- Number: typ.Number
- Integer: typ.Integer
- String: typ.String
- Function: fun(...any) -> any
- Any: typ.Any
- Other: typ.Unknown (no canonical type available)
func TypeIsExactlyLiteral ¶
TypeIsExactlyLiteral returns true only if the type is exactly the given literal.
This function distinguishes between a singleton literal type and broader types that contain the literal as a member. It is fundamental to sound negative narrowing.
Behavior by Type ¶
- Literal: Returns true if the literal values are equal.
- Alias: Unwraps and checks the target type.
- Union: Returns true only if ALL members are exactly the literal.
- Optional: Returns true if the inner type is exactly the literal.
- Other: Returns false (primitives, records, etc. are broader).
Examples ¶
TypeIsExactlyLiteral(typ.LiteralString("a"), typ.LiteralString("a")) // true
TypeIsExactlyLiteral(typ.String, typ.LiteralString("a")) // false
TypeIsExactlyLiteral(typ.NewOptional(typ.LiteralString("a")), ...) // true
Union Semantics ¶
For unions, all members must be the exact literal. This handles the case of duplicate literal types in unions. A union like "a" | "b" would return false for either literal because not all members match.
func TypesOverlap ¶
TypesOverlap checks if two types have any potential runtime overlap.
Two types overlap if either is a subtype of the other. This is used by ExcludeType to determine which union members to remove.
Returns false if either argument is nil.
Examples ¶
TypesOverlap(typ.String, typ.String) // true TypesOverlap(typ.String, typ.NewUnion(typ.String, typ.Number)) // true TypesOverlap(typ.String, typ.Number) // false
Types ¶
type Resolver ¶
type Resolver interface {
// Field returns the type of field 'name' on type t.
//
// Returns (fieldType, true) if the field exists and is accessible.
// Returns (nil, false) if the field does not exist or t is not a
// structured type that supports field access.
//
// For records, this looks up named fields.
// For interfaces, this looks up methods.
Field(t typ.Type, name string) (typ.Type, bool)
// Index returns the element type when indexing t with key.
//
// Returns (elementType, true) if the index operation is valid.
// Returns (nil, false) if t does not support indexing with the given key.
//
// For arrays, key should be an integer type.
// For maps, key should be compatible with the map's key type.
// For tuples, key should be an integer literal within bounds.
Index(t typ.Type, key typ.Type) (typ.Type, bool)
}
Resolver provides context-free type queries for field and index access.
Narrowing operations need to resolve field types (for discriminant checks) and index types (for array/map access) without access to full semantic context. Resolver abstracts these operations to decouple narrowing logic from the full type checker.
Purpose ¶
The Resolver interface enables the narrow package to work with structured types (records, interfaces, arrays, maps) without depending on the full type checking infrastructure. This separation allows:
- Testing narrowing logic in isolation.
- Using narrowing from different contexts (checker, solver).
- Avoiding circular dependencies between packages.
Implementation ¶
Implementors should handle the following type structures:
For Field:
- Record: Return the field's type if it exists.
- Interface: Return the method's type if it exists.
- Intersection: Try each member, return first match.
- Union: Only if all members have the field with same type.
For Index:
- Array: Return element type for integer keys.
- Map: Return value type if key is compatible.
- Tuple: Return element type for integer literal keys.
- Record with map component: Return map value type.
Thread Safety ¶
Resolver implementations should be safe for concurrent use if the narrowing operations may be called from multiple goroutines.
Example Implementation ¶
type MyResolver struct {
// ... fields for type environment ...
}
func (r *MyResolver) Field(t typ.Type, name string) (typ.Type, bool) {
if rec, ok := t.(*typ.Record); ok {
if f := rec.GetField(name); f != nil {
return f.Type, true
}
}
return nil, false
}
func (r *MyResolver) Index(t typ.Type, key typ.Type) (typ.Type, bool) {
if arr, ok := t.(*typ.Array); ok {
return arr.Element, true
}
return nil, false
}
type TypeKey ¶
type TypeKey struct {
Kind TypeKeyKind // Discriminant for key type.
Name string // Type name for built-in types (e.g., "number", "string").
Hash uint64 // Content hash for user-defined types.
}
TypeKey identifies a type for narrowing operations using either a built-in type name or a content hash.
Type keys enable efficient type discrimination without carrying full type information. They are used by the constraint solver to represent type checks (HasType, NotHasType) in a compact form that can be stored in constraints and compared quickly.
Key Kinds ¶
There are two kinds of type keys:
Builtin (TypeKeyBuiltin): Identifies primitive types by name. Used for typeof checks like "type(x) == 'number'". The Name field contains the Lua typeof string.
Hash (TypeKeyHash): Identifies user-defined types by structural hash. Used for interface/record type checks. The Hash field contains the type's content hash.
Thread Safety ¶
TypeKey is a value type and safe to copy and use concurrently.
Examples ¶
BuiltinTypeKey("number") // identifies the number type
HashTypeKey(record.Hash()) // identifies a specific record type
func BuiltinTypeKey ¶
BuiltinTypeKey creates a TypeKey for a built-in Lua type name.
Built-in type keys are used for typeof-based narrowing. The name should be a valid Lua typeof result: "nil", "boolean", "number", "string", "function", "table", "thread", or "userdata".
Returns the zero TypeKey if name is empty.
func HashTypeKey ¶
HashTypeKey creates a TypeKey for a hash-based type identity.
Hash-based type keys are used for user-defined types (records, interfaces) where typeof would just return "table". The hash uniquely identifies the type's structure.
Returns the zero TypeKey if hash is 0.
func KnownBuiltinTypeKey ¶ added in v1.5.6
KnownBuiltinTypeKey creates a TypeKey for a recognized Lua type() result.
Returns false when name is not a supported built-in type string.
func (TypeKey) BuiltinKind ¶ added in v1.5.6
BuiltinKind resolves a built-in key name to a Kind.
Returns false when k is not a built-in key or when its name is unknown.
func (TypeKey) Equal ¶
Equal reports whether two type keys are identical.
Keys must match in kind, name (for builtins), and hash (for user types). Two zero keys are considered equal.
func (TypeKey) Hash64 ¶
Hash64 computes a 64-bit hash for the type key suitable for use in hash tables.
The hash combines the key kind with either the name hash (for builtins) or the type hash (for user types). This enables efficient storage in hash-based data structures.
Returns 0 for invalid/zero keys.
type TypeKeyKind ¶
type TypeKeyKind uint8
TypeKeyKind distinguishes between type key variants.
const ( // TypeKeyInvalid represents an uninitialized or invalid type key. // This is the zero value and indicates no type. TypeKeyInvalid TypeKeyKind = iota // TypeKeyBuiltin identifies a built-in Lua type by name. // The Name field contains the typeof string (e.g., "number", "string"). TypeKeyBuiltin // TypeKeyHash identifies a user-defined type by structural hash. // The Hash field contains the type's content hash for equality comparison. TypeKeyHash )
type TypeMatcher ¶
TypeMatcher is a predicate function that determines whether a type satisfies a specific criterion for narrowing purposes.
Type matchers enable flexible narrowing by abstracting the matching logic. They are used with FilterByMatch to selectively keep or exclude union members based on arbitrary type properties such as:
- Kind matching (type(x) == "number")
- Discriminant checks (x.kind == "a")
- Structural properties (has certain fields)
The matcher receives individual union members (not the union itself) and returns true if the member should be kept (for narrowing) or removed (for exclusion).
Example ¶
matcher := func(t typ.Type) bool {
return t.Kind() == kind.Number
}
result := FilterByMatch(union, matcher, false) // keep numbers
result := FilterByMatch(union, matcher, true) // exclude numbers
type TypeResolver ¶
TypeResolver resolves a concrete type from a type key.
Used during constraint application to recover full type information from the compact key representation. The resolver maps:
- Builtin keys to their corresponding primitive types.
- Hash keys to their registered user-defined types.
The resolver is typically provided by the type environment that tracks all defined types in the program.
Returns nil if the key cannot be resolved (unknown type).