Name Resolution
This chapter describes how Clef resolves names in various contexts during type inference. This is a component of the overall Inference Procedures.
The following sections describe how F# resolves names in various contexts.
Name Environments
Each point in the interpretation of an F# program is subject to an environment. The environment encompasses:
All referenced source packages and native libraries.
ModulesAndNamespaces : a table that maps
long-idents to a list of signatures. Each signature is either a namespace declaration group signature or a module signature.For example,
Collectionsmay map to one namespace declaration group signature for each referenced package that contributes to theCollectionsnamespace, and to a module signature, if a module calledCollectionsis declared or in a referenced package.If the program references multiple packages, the packages are added to the name resolution environment in the order in which the references appear in the project file. The order is important only if ambiguities occur in referencing the contents of packages; for example, if two packages define the type
MyNamespace.C.ExprItems : a table that maps names to the following items:
- A value
- A union case for use when constructing data
- An active pattern result tag for use when returning results from active patterns
- A type name for each class or struct type
FieldLabels : a table that maps names to sets of field references for record types
PatItems : a table that maps names to the following items:
- A union case, for use when pattern matching on data
- An active pattern case name, for use when specifying active patterns
- A literal definition
Types : a table that maps names to type definitions. Two queries are supported on this table:
Find a type by name alone. This query may return multiple types. For example, a type name like
Mapmay resolve to multiple generic types with different arities.Find a type by name and generic arity
n. This query returns at most one type. For example, the resolution ofMapwithn = 2returns a single typeMap<'Key, 'Value>.
ExtensionsInScope : a table that maps type names to one or more member definitions
The dot notation is resolved during type checking by consulting these tables.
Name Resolution in Module and Namespace Paths
Given an input long-ident and environment env, Name Resolution in Module and Namespace Paths
computes the result of interpreting long-ident as a module or namespace. The procedure returns a
list of modules and namespace declaration groups.
Name Resolution in Module and Namespace Paths proceeds through the following steps:
- Consult the ModulesAndNamespaces table to resolve the
long-identprefix to a list of modules and namespace declaration group signatures. - If any identifiers remain unresolved, recursively consult the declared modules and sub-modules of these namespace declaration groups.
- Concatenate all the results.
If the long-ident starts with the special pseudo-identifier keyword global, the identifier is resolved
by consulting the ModulesAndNamespaces table and ignoring all open directives, including those
implied by AutoOpen attributes.
For example, if the environment contains two referenced packages, and each package has namespace
declaration groups for the namespaces Fidelity, Fidelity.Collections, and
Fidelity.Collections.Concurrent, Name Resolution in Module and Namespace Paths for
Fidelity.Collections returns the two namespace declaration groups named Fidelity.Collections, one
from each package.
Opening Modules and Namespace Declaration Groups
When a module or namespace declaration group F is opened, the compiler adds items to the name
environment as follows:
Add each exception label for each exception type definition (§ Exception Definitions) in
Fto the ExprItems and PatItems tables in the original order of declaration inF.Add each type definition in the original order of declaration in
F. Adding a type definition involves the following procedure:- If the type is a class or struct type (or an abbreviation of such a type), add the type name to the ExprItems table.
- If the type definition is a record, add the record field labels to the FieldLabels table, unless
the type has the
RequireQualifiedAccessattribute. - If the type is a union, add the union cases to the ExprItems and PatItems tables, unless the
type has the
RequireQualifiedAccessattribute. - Add the type to the TypeNames table. For generic types such as
List<'T>, add an entry underListthat records the generic arity.
Add each value in the original order of declaration in
F, as follows:- Add the value to the ExprItems table.
- If any value is an active pattern, add the tags of that active pattern to the PatItems table according to the original order of declaration.
- If the value is a literal, add it to the PatItems table.
Add the member contents of each type extension in
Fito the ExtensionsInScope table according to the original order of declaration inFi.Add each sub-module or sub-namespace declaration group in
Fito the ModulesAndNamespaces table according to the original order of declaration inFi.Open any sub-modules that are marked with the
FSharp.Core.AutoOpenattribute.
Name Resolution in Expressions
Given an input long-ident , environment env , and an optional count n of the number of subsequent
type arguments <_, ..., _>, Name Resolution in Expressions computes a result that contains the
interpretation of the long-ident <_, ..., _> prefix as a value or other expression item, and a residue
path rest.
How Name Resolution in Expressions proceeds depends on whether long-ident is a single identifier
or is composed of more than one identifier.
If long-ident is a single identifier ident:
- Look up
identin the ExprItems table. Return the result and emptyrest. - If
identdoes not appear in the ExprItems table, look it up in the Types table, with generic arity that matchesnif available. Return this type and emptyrest. - If
identdoes not appear in either the ExprItems table or the Types table, fail.
If long-ident is composed of more than one identifier ident.rest, Name Resolution in Expressions
proceeds as follows:
If
identexists as a value in the ExprItems table, return the result, withrestas the residue.If
identdoes not exist as a value in the ExprItems table, perform a backtracking search as follows:- Consider each division of
long-identinto[namespace-or-module-path].ident[.rest], in which thenamespace-or-module-pathbecomes successively longer. - For each such division, consider each module signature or namespace declaration group
signature
Fin the list that is produced by resolvingnamespace-or-module-pathby using Name Resolution in Module and Namespace Paths. - For each such
F, attempt to resolveident[.rest]in the following order. If any resolution succeeds, then terminate the search:- A value in
F. Return this item andrest. - A union case in
F. Return this item andrest. - An exception constructor in
F. Return this item andrest. - A type in
F. Ifrestis empty, then return this type; if not, resolve using Name Resolution for Members. - A [sub-]module in
F. Recursively resolverestagainst the contents of this module.
- A value in
- Consider each division of
If steps 1 and 2 do not resolve
long-ident, look upidentin the Types table.- If the generic arity
nis available, then look for a type that matches bothidentandn. - If no generic arity
nis available, andrestis not empty:- If the Types table contains a type
identthat does not have generic arguments, resolve to this type. - If the Types table contains a unique type
identthat has generic arguments, resolve to this type. However, if the overall result of the Name Resolution in Expressions operation is a member, and the generic arguments do not appear in either the return or argument types of the item, warn that the generic arguments cannot be inferred from the type of the item. - If neither of the preceding steps resolves the type, give an error.
- If the Types table contains a type
- If rest is empty, return the type, otherwise resolve using Name Resolution for Members.
- If the generic arity
If steps 1-3 do not resolve
long-ident, look upidentin the ExprItems table and return the result and residuerest.Otherwise, if
identis a symbolic operator name, resolve to an item that indicates an implicitly resolved symbolic operator.Otherwise, fail.
If the expression contains ambiguities, Name Resolution in Expressions returns the first result that the process generates. For example, consider the following cases:
module M =
type C =
| C of string
| D of string
member x.Prop1 = 3
type Data =
| C of string
| E
member x.Prop1 = 3
member x.Prop2 = 3
let C = 5
open M
let C = 4
let D = 6
let test1 = C // resolves to the value C
let test2 = C.ToString() // resolves to the value C with residue ToString
let test3 = M.C // resolves to the value M.C
let test4 = M.Data.C // resolves to the union case M.Data.C
let test5 = M.C.C // error: first part resolves to the value M.C,
// and this contains no field or property "C"
let test6 = C.Prop1 // error: the value C does not have a property Prop
let test7 = M.E.Prop2 // resolves to M.E, and then a property lookupThe following example shows the resolution behavior for type lookups that are ambiguous by generic arity:
module M =
type C<'T>() =
static member P = 1
type C<'T,'U>() =
static member P = 1
let _ = new M.C() // gives an error
let _ = new M.C<int>() // no error, resolves to C<'T>
let _ = M.C() // gives an error
let _ = M.C<int>() // no error, resolves to C<'T>
let _ = M.C<int,int>() // no error, resolves to C<'T,'U>
let _ = M.C<_>() // no error, resolves to C<'T>
let _ = M.C<_,_>() // no error, resolves to C<'T,'U>
let _ = M.C.P // gives an error
let _ = M.C<_>.P // no error, resolves to C<'T>
let _ = M.C<_,_>.P // no error, resolves to C<'T,'U>The following example shows how the resolution behavior differs slightly if one of the types has no generic arguments.
module M =
type C() =
static member P = 1
type C<'T>() =
static member P = 1
let _ = new M.C() // no error, resolves to C
let _ = new M.C<int>() // no error, resolves to C<'T>
let _ = M.C() // no error, resolves to C
let _ = M.C< >() // no error, resolves to C
let _ = M.C<int>() // no error, resolves to C<'T>
let _ = M.C< >() // no error, resolves to C
let _ = M.C<_>() // no error, resolves to C<'T>
let _ = M.C.P // no error, resolves to C
let _ = M.C< >.P // no error, resolves to C
let _ = M.C<_>.P // no error, resolves to C<'T>In the following example, the procedure issues a warning for an incomplete type. In this case, the
type parameter 'T cannot be inferred from the use M.C.P, because 'T does not appear at all in the
type of the resolved element M.C<'T>.P.
module M =
type C<'T>() =
static member P = 1
let _ = M.C.P // no error, resolves to C<'T>.P, warning givenThe effect of these rules is to prefer value names over module names for single identifiers. For example, consider this case:
let Foo = 1
module Foo =
let ABC = 2
let x1 = Foo // evaluates to 1The rules, however, prefer type names over value names for single identifiers, because type names appear in the ExprItems table. For example, consider this case:
let Foo = 1
type Foo() =
static member ABC = 2
let x1 = Foo.ABC // evaluates to 2
let x2 = Foo() // evaluates to a new Foo()Name Resolution for Members
Name Resolution for Members is a sub-procedure used to resolve .member-ident[.rest] to a
member, in the context of a particular type type.
Name Resolution for Members proceeds through the following steps:
Search the hierarchy of the type from its root base type to
type.Clef Note: Clef classes do not inherit from
System.Object. The type hierarchy is determined by explicitinheritdeclarations.At each type, try to resolve
member-identto one of the following, in order:- A union case of
type. - A property group of
type. - A method group of
type. - A field of
type. - An event of
type. - A property group of extension members of
type, by consulting the ExtensionsInScope table. - A method group of extension members of
type, by consulting the ExtensionsInScope table. - A nested type
type-nestedoftype. Recursively resolve.restif it is present, otherwise returntype-nested.
- A union case of
At any type, the existence of a property, event, field, or union case named
member-identcauses any methods or other entities of that same name from base types to be hidden.Combine method groups with method groups from base types. For example:
type A() =
member this.Foo(i : int) = 0
type B() =
inherit A()
member this.Foo(s : string) = 1
let b = new B()
b.Foo(1) // resolves to method in A
b.Foo("abc") // resolves to method in BName Resolution in Patterns
Name Resolution for Patterns is used to resolve long-ident in the context of pattern expressions.
The long-ident must resolve to a union case, exception label, literal value, or active pattern case
name. If it does not, the long-ident may represent a new variable definition in the pattern.
Name Resolution for Patterns follows the same steps to resolve the member-ident as Name
Resolution in Expressions (§ Name Resolution in Expressions) except that it consults the PatItems table instead of the ExprItems
table. As a result, values are not present in the namespace that is used to resolve identifiers in
patterns. For example:
let C = 3
match 4 with
| C -> sprintf "matched, C = %d" C
| _ -> sprintf "no match, C = %d" Cresults in "matched, C = 4", because C is not present in the PatItems table, and hence becomes a
value pattern. In contrast,
[<Literal>]
let C = 3
match 4 with
| C -> sprintf "matched, C = %d" C
| _ -> sprintf "no match, C = %d" Cresults in "no match, C = 3", because C is a literal and therefore is present in the PatItems table.
Name Resolution for Types
Name Resolution for Types is used to resolve long-ident in the context of a syntactic type. A generic
arity that matches n is always available. The result is a type definition and a possible residue rest.
Name Resolution for Types proceeds through the following steps:
Given
ident[.rest], look upidentin the Types table, with generic arityn. Return the result and residuerest.If
identis not present in the Types table:- Divide
long-identinto[namespace-or-module-path].ident[.rest], in which thenamespace- or-module-pathbecomes successively longer. - For each such division, consider each module and namespace declaration group
Fin the list that results from resolvingnamespace-or-module-pathby using Name Resolution in Module and Namespace Paths (§ Name Resolution in Module and Namespace Paths). - For each such
F, attempt to resolveident[.rest]in the following order. Terminate the search when the expression is successfully resolved.- A type in
F. Return this type and residuerest. - A [sub-]module in
F. Recursively resolverestagainst the contents of this module.
- A type in
- Divide
In the following example, the name C on the last line resolves to the named type M.C<_,_> because C
is applied to two type arguments:
module M =
type C<'T, 'U> = 'T * 'T * 'U
module N =
type C<'T> = 'T * 'T
open M
open N
let x : C<int, string> = (1, 1, "abc")Name Resolution for Type Variables
Whenever the F# compiler processes syntactic types and expressions, it assumes a context that maps identifiers to inference type variables. This mapping ensures that multiple uses of the same type variable name map to the same type inference variable. For example, consider the following function:
let f x y = (x:'T), (y:'T)In this case, the compiler assigns the identifiers x and y the same static type - that is, the same type
inference variable is associated with the name 'T. The full inferred type of the function is:
val f<'T> : 'T -> 'T -> 'T * 'TThe map is used throughout the processing of expressions and types in a left-to-right order. It is initially empty for any member or any other top-level construct that contains expressions and types. Entries are eliminated from the map after they are generalized. As a result, the following code checks correctly:
let f () =
let g1 (x:'T) = x
let g2 (y:'T) = (y:string)
g1 3, g1 "3", g2 "4"The compiler generalizes g1, which is applied to both integer and string types. The type variable 'T in
(y:'T) on the third line refers to a different type inference variable, which is eventually constrained to
be type string.
Field Label Resolution
Field Label Resolution specifies how to resolve identifiers such as field1 in {field1 = expr; ... fieldN = expr}.
Clef Extension: Unlike standard F# where memory layout is delegated to the CLR runtime, Clef computes deterministic memory layouts at compile time. Field Label Resolution therefore encompasses both type resolution AND layout determination. See § 32 Records Named Products for memory layout principles.
Field Label Resolution proceeds through the following steps:
Step 1: Candidate Set Construction
For each field-label_i in the record expression:
If
field-label_iis a single identifierfldAND the initial type is known to be a record typeR<_, ..., _>that has fieldF_iwith namefld, thenfield-label_iresolves toF_idirectly.Otherwise, look up
field-label_iin the FieldLabels table. This yields a set of field referencesFSet_i, where each reference identifies a field in some record type. The corresponding set of record types isRSet_i.
Step 2: Type Resolution via Intersection
Compute the intersection of all candidate record type sets:
R_candidates = RSet_1 ∩ RSet_2 ∩ ... ∩ RSet_nThe resolution proceeds based on the cardinality of R_candidates:
| Cardinality | Result |
|---|---|
| 0 | Error CCS8704: “No single record type contains all specified fields” |
| 1 | Success: The unique record type R is identified |
| > 1 | Error CCS8702: “Ambiguous record type. Could be: {types}. Use type annotation to disambiguate.” |
Step 3: Completeness Verification
For the resolved record type R, verify that every field defined in R has exactly one corresponding field-label_i in the expression. Missing fields result in error CCS8705.
Step 4: Layout Computation (Clef Extension)
Core Principle: “Field order determines memory layout” (§ 32 Records Named Products). The compiler controls layout, not MLIR, not LLVM.
For the resolved record type R with fields f_1, f_2, ..., f_n in declaration order:
- Initialize
offset = 0,max_align = 1 - For each field
f_iwith typeT_i:- Compute
(size_i, align_i) = layoutOf(T_i) - Compute padding:
pad = (align_i - (offset mod align_i)) mod align_i - Set
offset_i = offset + pad - Update
offset = offset_i + size_i - Update
max_align = max(max_align, align_i)
- Compute
- Compute final padding for struct alignment:
final_pad = (max_align - (offset mod max_align)) mod max_align - Total layout:
TypeLayout.Inline(offset + final_pad, max_align)
The resolved record type carries this computed layout, ensuring deterministic memory representation throughout the compilation pipeline.
Step 5: Return Resolution
Return the resolved record type R with:
- Type constructor reference (including computed layout)
- Field types in declaration order
- Memory offsets for each field
Error Codes
| Code | Condition |
|---|---|
| CCS8701 | Field name not found in any record type in scope |
| CCS8702 | Multiple record types contain all specified fields (ambiguity) |
| CCS8703 | Record type lookup failed (internal error) |
| CCS8704 | No single record type contains all specified fields |
| CCS8705 | Record expression is incomplete (missing required fields) |