hd-lang

#Names and Scopes

Status: language specification draft.

This chapter defines the scopes introduced by hd-lang programs and how names resolve. It does not define type compatibility or which places are writable.

#Name Categories

  1. names.category.fourhd-lang has four lookup categories, which this table lists:
Rule Names Identify
names.category.moduleModule Module names top-level types, traits, functions, and names introduced by use declarations
names.category.valueValue Value names top-level executable bindings, parameters, local bindings, local named functions, loop bindings, pattern bindings, and captured values
names.category.local-typeLocal type Local type names structs, enums, traits, aliases, and newtypes declared inside an executable suite
names.category.memberMember Member names struct fields, embedded fields, methods, enum variants, and tuple fields within the namespace of their owning type
  1. names.category.syntaxA use is resolved in the category required by its syntax.
  2. names.category.syntax.typeFor example, the name before { in User { ... } is resolved as a type.
  3. names.category.syntax.valueIn user.email, user is resolved as a value and email as a member of its type.
  4. names.type.qualifiedA type may be named through a module in scope, as types.User after use pkg.user.types, by the rules of expr.name.qualified. A missing declaration is an error. Error: unknown-import. A private one is an error. Error: private-import.
  5. names.type-as-valueA name resolved as a value that names a type, a trait, or a type alias and no value is an error. Examples are let x = User and field.metadata(MaxLen). Error: type-used-as-value.
  6. names.module.uniqueA module cannot contain two declarations with the same module name, even if they are different kinds of declaration. Error: duplicate-module-name.
  7. names.module.no-overloadingFunction overloading is therefore not permitted.
Error example
fn value(input: i32) -> i32: input
fn value(input: string) -> string: input  # error: duplicate-module-name
Error example
struct User:
    name: string

fn make() -> void:
    x := User  # error: type-used-as-value

Note. By names.category.syntax, a form whose syntax names a type resolves the name as a type. So User { name: "Ada" }, a newtype constructor call such as Mile(1), and a cast such as i16(wide) are not value uses.

#Prelude Names

  1. names.prelude.sourceEvery built-in core type is supplied by the prelude described in Modules and Packages.
  2. names.prelude.not-reservedPrelude names remain ordinary identifier tokens rather than reserved words.
  3. names.prelude.one-namespaceThere is no separate predeclared-name namespace or shadowing rule.
Error example
struct i32:  # error: prelude-name-shadow
    value: string

See also: module.prelude.no-shadow, which makes shadowing a prelude name an error.

#Type Parameters

  1. names.type-param.localType parameters are type names local to their declaration.
  2. names.type-param.shadowA type parameter may shadow a module name within that declaration.
  3. names.type-param.uniqueA type parameter must not duplicate another type parameter in the same parameter list.
  4. names.type-param.no-redeclareNo declaration within a type parameter's scope may reuse its name: no nested type parameter, local declaration, local value, or parameter. Error: duplicate-binding.
  5. names.type-param.no-redeclare.methodSo a method's own type parameter must not reuse a type parameter of its impl or trait, as fn echo[T] inside impl[T] Box[T] would.
  6. names.type-param.no-redeclare.bodyInside fn work[T], a local struct T, a local value T, and a local fn convert[T] are each an error.
Error example
struct Box[+T]:
    value: T

impl[T] Box[T]:
    pub fn echo[T](self, value: T) -> T:  # error: duplicate-binding
        value

fn work[T < Display](value: T) -> string:
    struct T:  # error: duplicate-binding
        count: i32
    T := value  # error: duplicate-binding
    "${value}"

Why. One name then means one thing inside a declaration. A reader never has to work out which T a signature or a T:: call names.

#The Self Name

  1. names.self-type.reservedSelf is a reserved word that names the implementation target inside a trait or impl body.
  2. names.self-type.traitIn a trait, Self denotes the eventual implementing type.
  3. names.self-type.implIn a trait or inherent implementation, Self denotes that implementation's target type.
  4. names.self-type.outsideSelf has no valid type meaning outside those bodies.
struct Counter:
    value: i32

impl Counter:
    fn zero() -> Self:
        Counter { value: 0 }   # Self is Counter here

#Lexical Scopes

  1. names.scope.lexicalScopes are lexical.
  2. names.scope.constructsThe following constructs introduce a local scope:
  • a function or closure body;
  • each indented or same-line suite of if, else if, and else;
  • the body and else suite of a for or while loop;
  • each match arm;
  • the else block of a let-else statement;
  • a comprehension;
  • a trailing callback block.
  1. names.scope.shadowA name declared in an inner scope may shadow a name from an outer scope.
  2. names.scope.duplicateTwo bindings with the same name in one scope are a compile-time error.
  3. names.scope.reassignReassignment uses = and does not introduce another binding.
fn demo(flag: bool) -> i32:
    x := +1
    if flag:
        x := +2   # a new scope: this x shadows the outer one
        _ := x
    x   # the outer one: +1

#Binding Start

A local binding is not visible in its own initializer:

name := normalize(name)  # the right-hand name, if valid, resolves outward
  1. names.scope.startThe scope of a local binding begins immediately after its initializer has been evaluated.
  2. names.scope.not-in-initializerThe binding is not visible in its own initializer.
  3. names.scope.recursive-closureThe sole exception is a direct local closure initializer with an explicit result type: its body may refer to the name being bound for recursion.
  4. names.scope.recursive-closure.evalThe initializer expression itself is still evaluated before the binding becomes available; only the closure body receives this forward name.
  5. names.scope.recursive-closure.no-early-runThe closure body cannot run before initialization completes.

Why. This permits ordinary inner-scope shadowing without making a new binding self-referential.

#Module Scope

  1. names.module.fileEvery source file is a module. Its path-derived identity is specified in Modules.
  2. names.module.declarationsTop-level declarations belong to the module scope.
  3. names.module.order-freeTheir names are visible throughout the module, independent of textual order, which permits direct and mutual recursion between functions in one module.
  4. names.module.well-typedA declaration must still be well typed as a whole.
  5. names.module.no-init-orderForward visibility does not imply initialization order for executable top-level statements.
  6. names.module.scopeA module's scope holds its own top-level declarations, the prelude names, and the names that its own use declarations introduce.
  7. names.module.other-moduleOutside the prelude, a declaration of another module is in scope only through this module's own use of it. That holds for a module in the package, in a dependency, or in std.
  8. names.module.use-own-moduleA use declaration introduces its names into its own module only, never into another module of the same package.
  9. names.module.other-module.valueA bare value name that is not in scope is an error, even when another module declares it. Error: unknown-name.
  10. names.module.other-module.typeA bare type name that is not in scope is an error, even when another module declares it. Error: unknown-type.
  11. names.module.other-module.traitA bare trait name that is not in scope is an error, even when another module declares it. Error: unknown-trait.
  12. names.module.other-module.hintThe message of such an error may name the module that declares the name and the use declaration that would import it.
Error example
# src/cart.hd
pub struct Cart:
    pub items: i32

pub fn total(cart: Cart) -> i32: cart.items * 100

fn fee() -> i32: +5

# src/checkout.hd
use pkg.cart.{Cart}

fn charge(cart: Cart) -> i32:
    total(cart) + fee()  # error: unknown-name

Why. No action at a distance. Every bare name in a file traces to that file's declarations, the prelude, or one of its own use lines.

See also: Use Declarations, Public Uses And Visibility.

#Module Execution Scope

  1. names.exec.scopeTop-level executable statements run in a separate module execution scope.
  2. names.exec.visibleBindings created by those statements become visible to later top-level statements and to the bodies of functions declared after their binding point.
  3. names.exec.accessThose functions may read a top-level := or let binding and may reassign a top-level let binding.
  4. names.exec.not-usableSuch bindings are not nameable by a use declaration.
  5. names.exec.not-beforeSuch bindings are not visible before their binding point, including from the body of a function declared earlier. Error: binding-not-yet-visible.
  6. names.exec.declarationsA top-level executable statement may refer to a named module declaration regardless of that declaration's textual position.
  7. names.exec.methodsIn the rules of this subsection, a function also means an inherent or trait method, and a method's position is that of its impl block.
Error example
fn read_count() -> i32:
    count  # error: binding-not-yet-visible

let count = +0

#Initialization Order

  1. names.init.no-bypassReferring to a named module declaration from a top-level executable statement does not bypass initialization order.
  2. names.init.transitiveAt every top-level executable statement, the compiler computes the transitive set of top-level bindings read by every module function or closure referenced by that statement.
  3. names.init.referencesA function or closure counts as referenced whether it is called directly or passed as a value. It also counts when reached through a trait method, interpolation, iteration, or another implicit call.
  4. names.init.requiredEvery binding in that set must have been initialized by an earlier top-level statement.
  5. names.init.whole-moduleThis is a whole-module value-flow and call-graph check; an indirect read through a later function value is an error like a direct forward binding reference. Error: top-level-read-before-initialization.
start := +1

fn total() -> i32:
    start + 1

demo := total()   # reads start through total, after both are initialized

#Declarations In Blocks

  1. names.block-decl.allowedNamed fn, struct, enum, trait, and type declarations may also occur inside executable block suites.
  2. names.block-decl.implimpl declarations may occur at module scope or inside an executable block suite; they do not introduce an independently referencable name.
fn demo() -> i32:
    struct Pair:
        a: i32
        b: i32
    p := Pair { a: +1, b: +2 }
    p.a + p.b   # 3; Pair is local to demo

See also: Function And Closure Scopes.

#Tests Blocks

The items of a tests: block are module items that only the block sees:

Error example
use pkg.pricing.{rates}

fn late_fee(days: i32) -> i32:
    if days > 30: rates().late else: 0

tests:
    use std.testing.assert_equal
    use dep.fx.{fixed_rates as rates}  # valid: the block's import shadows the module's

    fn overdue() -> i32: 31
    pub fn shared_overdue() -> i32: 45  # error: public-test-item

    it("uses a private function"):
        assert_equal(late_fee(overdue()) > 0, true, reason="the block sees late_fee")

    it("reads the fixed rates"):
        assert_equal(rates().late, 5, reason="inside the block, rates is the block's import")

fn report() -> i32:
    overdue()  # error: unknown-name

fn check_flag(ok: bool) -> void:
    assert_equal(ok, true, reason="no import here")  # error: unknown-name
  1. names.tests.module-itemsThe items of a tests: block are module items of the file's module.
  2. names.tests.sees-moduleCode inside the block sees every module name, including private declarations and uses outside the block.
  3. names.tests.inside-onlyA name that an item of the block declares or uses is visible only inside the block. Naming it outside the block is an error. Error: unknown-name.
  4. names.tests.uniqueBecause they are module items, a name declared in the block must not repeat a module name declared outside it. Error: duplicate-module-name.
  5. names.tests.no-pubAn item inside a tests: block must not be marked pub. Error: public-test-item.
  6. names.tests.shadowA use inside the block may introduce a name the module declares or uses outside the block; inside the block (its cases and helpers) the block's import wins, as a nested scope. Outside the block nothing changes.

Why. Module items, rather than local declarations, let the block hold implementations and derivation blocks, as Rust's mod tests can. Nothing outside the block sees its items, so pub would promise what it cannot give; shared test helpers belong in a test module.

See also: Test Cases.

#Module Privacy

  1. names.pub.private-defaultDeclarations are module-private unless marked pub.
  2. names.pub.eligiblepub makes a declaration eligible to be used from another module.
  3. names.pub.no-exportpub does not register a Wasm export or make a declaration host-callable.
pub fn visible() -> i32:
    internal()   # same module: fine

fn internal() -> i32:
    +2

#Use Declarations

A use declaration introduces either one local module name or one or more local declaration names into the using module's module scope:

use pkg.user.types
use pkg.user.types as user_types
use pkg.user.types.{User, UserId}
use dep.billing.types.{UserId as BillingUserId}
  1. names.use.introducesA use declaration introduces either one local module name or one or more local declaration names into the using module's module scope.
  2. names.use.path-onlyA path-only use may name either a module namespace or one public declaration.
  3. names.use.final-componentWithout as, a path-only use binds the final path component; the first example therefore binds types.
  4. names.use.groupedA grouped use binds each selected name after applying any item alias.
  5. names.use.no-collisionA used name must not collide with another module-scope declaration or use.
  6. names.use.no-overloadUse declarations do not create overload sets and are not implicitly renamed.
  7. names.use.resolution-orderUse declarations are resolved before declarations are type checked.
  8. names.use.positionThe textual position of a use declaration does not limit its visibility.
  9. names.use.styleStyle tools should place use declarations before other top-level items.
  10. names.use.cycles-in-folderThe files of one folder may use each other in a loop.
  11. names.use.folder-cycleThe folders of a package must not depend on each other in a loop, as Dependency Cycles defines. Error: folder-cycle.

#Public Use Declarations

  1. names.use.pubPrefixing a grouped use declaration with pub makes every name it introduces available to other modules.
  2. names.use.pub.sourceThe source declaration must already be public.
  3. names.use.pub.bindingA pub use introduces the same local binding as an ordinary use; it additionally exposes that binding without creating a new declaration identity.
pub use std.text.{trim, split}

#Literal Suffix Names

The suffix of a suffixed literal names a function in module scope, usually one brought in by use:

use std.ops.num_suffix

@num_suffix
fn secs(count: i64) -> i64: count * 1000

fn retry_after(secs: i32) -> i32:
    limit := 5secs  # the module'secs suffix function secs, not the parameter
    secs
  1. names.literal-fn.user-scopeA user literal suffix or string prefix resolves in module scope, including declarations, imports and prelude names.
  2. names.literal-fn.no-localParameters, local bindings, and local type declarations never participate, so a local named s or r does not change what 5s or r"..." calls.
  3. names.literal-fn.unknown-userA user suffix or string prefix naming nothing in module scope is an error. Error: unknown-name.
Error example
fn margin() -> i32:
    width := 12px  # error: unknown-name
    0

Why. Resolving through use tells a reader where ms comes from, and clashes such as two libraries' m use the existing as renaming. Module scope alone keeps a local name from changing what a literal means.

See also: Literal Suffixes.

#String Prefix Names

The prefix of a prefixed string names a function in module scope, as a literal suffix does:

use std.ops.{Template, str_prefix}

@str_prefix
fn r(t: Template[string]) -> string:
    t.raw_parts[0]

fn escape(r: i32) -> string:
    r"\d+"  # the module's prefix function r, not the parameter

The rules of Literal Suffix Names cover a prefix too, and so an unknown prefix is an error:

Error example
fn query(id: i32) -> string:
    sql"select $id"  # error: unknown-name

Note. A prefix is never written with a module path. To call the stdlib prefix function r of Text, write use std.text.r, then r"\d+".

See also: Prefixed Strings, grammar.primary.prefix-after-dot.

#Local Bindings

:= and let introduce local names:

name := "Ada"
let count = +0
count = count + 1
  1. names.local.short:= introduces inferred, non-reassignable local names. Reassigning one is an error. Error: non-reassignable-binding.
  2. names.local.letlet introduces local names that may be reassigned.
  3. names.local.same-scopeThe two forms differ in rebinding permission, not in lexical scope.
  4. names.local.in-placeWhether a value may change in place is decided by Writable Places, apart from whether the local name can be reassigned.
Error example
fn invalid() -> void:
    name := "Ada"
    name = "Grace"  # error: non-reassignable-binding

#Tuple Bindings

A tuple binding, like any let pattern, introduces several names at once:

let (x, y) = point
let (name, score) = entry
  1. names.pattern.simultaneousA let pattern introduces every name it binds at once, after the initializer is evaluated and matched.
  2. names.pattern.distinctAll names in one binding pattern must be distinct. A name bound twice is an error. Error: duplicate-binding.
  3. names.tuple.annotationWhen a let tuple binding has one type annotation, that annotation describes the complete right-hand tuple, not each individual name.
  4. names.tuple.arityThe annotation's arity must match the binding pattern, and each local receives the corresponding element type.

#Let-Else Scope

The names that a let-else pattern binds are visible after the statement, and not in its else block:

fn find(id: i32) -> i32?:
    if id == 0: .Some(7) else: .None

fn read(id: i32) -> i32:
    let .Some(value) = find(id) else:
        return 0  # value is not visible here
    value + 1
  1. names.let-else.afterThe names that a let pattern binds are visible to the end of the enclosing scope. Their scope starts at the end of the statement, after any else block.
  2. names.let-else.not-in-elseThey are not visible in the else block. A use there resolves to an outer binding of the same name, if one exists.

Why. The else block runs only when the pattern did not match, so no name of the pattern has a value there.

#Binding Expressions

A := binding expression binds in its enclosing scope:

if (size := input.len()) > 0:
    println(size)
  1. names.bind.expression:= may appear as the lowest-precedence expression.
  2. names.bind.scopeThe binding of a := expression belongs to the nearest enclosing executable scope, not to a synthetic scope around the subexpression.
  3. names.bind.visibleIn the example, size is visible after its initializer completes, including in the selected if suite and in later statements of the enclosing scope.
  4. names.bind.initializedsize is initialized regardless of which if branch executes because the condition is evaluated before branch selection.
  5. names.bind.no-redeclareA binding expression must not redeclare a name already bound in that same scope. Error: duplicate-binding.
Error example
fn main() -> i32:
    value := 1
    (value := 2)  # error: duplicate-binding
Open in playground

Note. Use assignment to update a let binding, or introduce an inner suite to shadow an outer name.

#Definite Initialization

  1. names.definite.requiredLexical scope does not waive definite initialization.
  2. names.definite.pathA := name is usable on a control-flow path only after that path evaluates its initializer.
  3. names.definite.mergeAt a merge, the name is definitely initialized only if every incoming reachable path has evaluated the same binding.
  4. names.definite.rejectA use that may observe an uninitialized binding is an error. Error: possibly-uninitialized-binding.
  5. names.definite.conditionA direct binding in an if or while condition is evaluated whenever that condition is evaluated.
  6. names.definite.skippedSome paths skip a binding inside the conditionally evaluated operand of && or ||, an unselected branch or match arm, or a loop body. On those paths the binding is not thereby initialized.
  7. names.definite.proofFlow analysis may still prove it initialized inside a branch whose selection implies that the binding ran.
  8. names.definite.divergingA branch that diverges, as flow.let.else.diverge.forms defines, is not an incoming path at the merge after it.
Error example
fn invalid(flag: bool) -> string:
    if flag && (name := "Ada") != "":
        pass
    name  # error: possibly-uninitialized-binding

A guard whose branch returns, or enters an infinite loop, leaves only the paths that ran the binding:

fn greeting(args: List[string]) -> string:
    if args.len() < 2 || (name := args[1]) == "":
        return "usage: greet NAME"
    "hello $name"

#Function And Closure Scopes

  1. names.fn.value-params.visibleA function's value parameters are visible throughout its signature after their declaration point and throughout its body.
  2. names.generic.params.visibleThe generic parameters of a function, method, struct, enum, trait, or type declaration are visible throughout that declaration, including its body.
  3. names.generic.bounds.whole-listA bound sees the whole list: it may name any parameter of its own list, earlier or later, as in fn read[S < Source[U], U = string](s: S) -> U.
  4. names.generic.defaults.earlierA type-argument default sees only the earlier parameters of its list, as types.generic.default.later states. Error: binding-not-yet-visible.
  5. names.fn.params.scopeAll value parameters belong to the function body's outermost local scope and must have distinct names.
fn first[A, B](pair: (A, B)) -> A:
    pair._0

#Local Functions

  1. names.local-fn.nameA named function declared in a block suite introduces a local value name at its declaration point.
  2. names.local-fn.visibleThe name is visible in the rest of that suite and in the function's own body, permitting recursion.
  3. names.local-fn.not-visibleThe name is not visible before its declaration, outside the suite, or from another module.
  4. names.local-fn.rulesA local named function follows the same shadowing and duplicate-name rules as other local values.
fn demo() -> i32:
    fn down(n: i32) -> i32:
        if n == 0: 0
        else: down(n - 1)   # the name is visible in its own body
    down(+3)

#Local Type Declarations

  1. names.local-type.nameA local struct, enum, trait, or type declaration introduces a type name at its declaration point. The name is visible in its own definition and in the rest of its enclosing suite.
  2. names.local-type.not-visibleThe name is not visible before that point or outside the suite.
  3. names.local-type.staticLocal type declarations do not execute, capture runtime values, or become module members nameable by a use declaration.
  4. names.local-type.refsLocal type declarations may refer to type names and type parameters visible at their declaration point.
  5. names.local-type.identityA local nominal type has one declaration identity, not a fresh identity per call to the enclosing function.
  6. names.local-type.duplicateLocal type and value declarations cannot duplicate a name in the same scope.
  7. names.local-type.preludeLocal type declarations are also subject to the prelude shadowing rule.
fn demo() -> i32:
    enum Choice:
        Yes
        No
    match Choice.Yes:
        Choice.Yes => +1
        Choice.No => +0

See also: Prelude Names.

#Local Declaration Limits

  1. names.local.no-pubpub is not permitted on local declarations. Error: syntax-error.
  2. names.local.no-metadataDecorators and derivation blocks with no trait are not permitted in a local scope.
fn demo() -> i32:
    fn helper(n: i32) -> i32: n * 2   # a plain local fn: no pub, no decorators
    helper(+21)

#Local Implementations

  1. names.local-impl.extentA local impl contributes methods or trait conformance from its declaration point to the end of its enclosing suite and its child scopes.
  2. names.local-impl.staticA local impl has no runtime execution step.
  3. names.local-impl.no-captureNeither its methods nor methods on a local trait capture enclosing runtime values.
  4. names.local-impl.involveA local implementation must involve a visible local nominal type or local trait, as specified in Traits.
fn describe(name: string) -> string:
    type Label = string
    struct Entry:
        label: Label
    enum Format:
        Plain
        Loud
    trait Named:
        fn name(self) -> string
    impl Named for Entry:
        fn name(self) -> string: self.label
    impl Entry:
        fn shout(self) -> string: self.label + "!"

    entry := Entry { label: name }
    match Format.Plain:
        Format.Plain => entry.name()
        Format.Loud => entry.shout()

#Receivers And Captures

  1. names.self.reservedWithin a method, self is a reserved word that names the receiver parameter supplied by the receiver syntax.
  2. names.self.receiver-formsmut self, &self, and &mut self also name the receiver self; they do not create a different lookup category.
  3. names.capture.resolveA closure or local named function resolves otherwise-unbound local names in lexically enclosing scopes.
  4. names.capture.definitionThose resolved names are its captures.
  5. names.capture.mutation-accessWhether a capture permits mutation is determined by the captured binding and its type alone, as specified in Functions.
  6. names.return.closurereturn in a closure or trailing block targets that closure, not the enclosing named function.
fn demo() -> i32:
    base := +10
    add := fn(n: i32) -> i32: n + base   # captures base
    add(+5)

#Control-Flow Binding Scopes

This section defines the scopes of bindings made inside conditionals, loops, matches, and comprehensions.

#Conditional Suites

  1. names.cond.childEach if, else if, and else suite has its own child scope.
  2. names.cond.no-leakA binding made inside one branch is not visible in another branch or after the conditional. A use there is an error. Error: unknown-name.
  3. names.cond.conditionA binding expression in a condition follows the enclosing-scope rule of Binding Expressions.
Error example
fn main() -> i32:
    if true:
        hidden := 1
    hidden  # error: unknown-name
Open in playground

#Loops

A for binding is local to the loop body:

for item in items:
    println(item)
  1. names.loop.for-localA for binding is local to the loop body.
  2. names.loop.for-elseA for binding is not visible in the loop's else suite or after the loop. A use there is an error. Error: unknown-name.
  3. names.loop.separateThe loop body and else suite are separate child scopes.
  4. names.loop.per-iterationEach iteration creates the body bindings for that iteration; a closure that escapes an iteration captures that iteration's binding rather than one shared loop variable.
  5. names.loop.whileA while body and its else suite likewise have separate child scopes.
Error example
fn last(values: List[i32]) -> i32:
    for value in values:
        if value > 10:
            break value
    else:
        value  # error: unknown-name

#Matches

Names bound by an arm's pattern are visible in that arm only:

match error:
    ToolError.NotFound(resource) => resource
    ToolError.Internal(message) => message
  1. names.match.arm-scopeEvery match arm has an independent child scope.
  2. names.match.visibleNames bound by an arm's pattern are visible in that arm's optional guard and body only.
  3. names.match.reusePattern bindings in different arms may reuse the same spelling.
  4. names.match.duplicateDuplicate bindings within one pattern are a compile-time error. Error: duplicate-binding.
Error example
fn sum(pair: (i32, i32)) -> i32:
    match pair:
        (n, n) => n  # error: duplicate-binding

#Comprehensions

A comprehension has one scope of its own:

pairs := [for x in xs for y in ys if x.id == y.owner_id => (x, y)]
  1. names.comp.scopeA comprehension introduces one scope that does not leak into the enclosing scope.
  2. names.comp.orderClauses are processed left to right.
  3. names.comp.forEach for binding is visible to later clauses and the result expression, but not to earlier clauses.
  4. names.comp.bindA := expression in a comprehension binds in the comprehension scope and is visible after the point where it is evaluated.
sizes := [for user in users
          if (size := user.name.len()) > 0
          => size]
  1. names.comp.no-leakThe names user and size are not visible after the comprehension. A use there is an error. Error: unknown-name.
Error example
fn doubled(values: List[i32]) -> i32:
    results := [for value in values => value * 2]
    value  # error: unknown-name

#Member Resolution

This section defines which field x.name and which method x.name(args) select.

#Member Namespaces

  1. names.member.namespacesEach nominal type has two member namespaces, and the form of a use chooses between them.
  2. names.member.fieldsIts fields, including embedded fields named by their embedded type name, are found by field lookup.
  3. names.member.methodsIts methods are found by method lookup.
  4. names.member.trait-methodsA type's trait methods are the methods of every trait that a known implementation implements for the type.
  5. names.member.known-implA known implementation is any implementation in the program's dependency graph whose target matches the type. The exception is a local implementation, which counts only where its methods are available for lookup.
  6. names.member.trait-availableA trait method is a lookup candidate in method lookup only where its trait is available.
  7. names.member.associatedAssociated functions are not dot-call members; they are reached through Type::function or Trait::function.
  8. names.member.shared-nameA field and a method may share a name, whether the method is inherent or a trait method.
  9. names.member.no-hidingNeither hides the other, because no use looks in both namespaces.
struct Timer:
    count: i32

impl Timer:
    fn count(self) -> i32: self.count   # a field and a method may share a name

Note. Trait availability works as in Rust, where a trait method is a candidate only while its trait is in scope.

See also: Implementation Declarations.

#Lookup Forms

  1. names.lookup.formsMember lookup resolves x.name and x.name(args) where the receiver x has nominal type S, or is a reference &S or &mut S by ref.read.auto.
  2. names.lookup.onlyMember lookup is the only lookup algorithm for these forms; the rest of the specification refers to it.
  3. names.lookup.field-formx.name without an argument clause uses field lookup only. It never selects a method, because a bare method is not a value.
  4. names.lookup.method-formx.name(args) uses method lookup only. It never selects a field.
  5. names.lookup.stored-fnA function stored in a field is called as (x.name)(args).
Error example
struct Base:
    pub run: fn() -> i32

struct Job:
    Base

fn invalid(job: Job) -> i32:
    job.run()  # error: unknown-method

See also: Member Access.

#Visibility

  1. names.visible.field-methodA field or inherent method is visible from a module when it is declared in that module or marked pub.
  2. names.visible.traitA trait method is available when its trait is available to dot-call lookup there.
struct User:
    pub name: string
    id: i32   # private: usable inside this module only

See also: Struct Declarations, Inherent Implementations, Method Resolution.

#Depths And Promoted Members

  1. names.part.definitionA part of S is a value reached from S through one or more embedded fields.
  2. names.part.depthA part's depth is the number of embedded fields on its path; the own fields and inherent methods of S are at depth 0.
  3. names.promote.memberEach pub field and pub inherent method of a part's type, at any depth, is a promoted member of S. It sits at the part's depth, reached through the part's path. A private member or a trait method of a part's type is never promoted, even in the module that declares it. It has no effect on lookup through S.
  4. names.promote.embeddedA part's embedded field is also a promoted member of S, at its own depth. An embedded field takes no pub marker but is always public (data.vis.embedded-public).
Error example
struct Base:
    secret: string

struct Record:
    Base

fn invalid(record: Record) -> string:
    record.secret  # error: unknown-field
struct Base:
    pub id: string

struct Middle:
    Base

struct Outer:
    Base
    Middle  # the embedded field Base is promoted twice; the shallower copy hides the deeper

#Members Lookup Considers

  1. names.members.consideredThe members of S that lookup considers are its own fields and inherent methods, whatever their visibility, and its promoted members.
  2. names.members.uniformThey are the same for every use, in every module. A type has a single view of its members, and each name resolves to the same member for every caller. Visibility decides only whether a caller may use the member that lookup finds.
struct Point:
    x: i32
    y: i32

fn demo(p: Point) -> i32:
    p.x + p.y

Note. This differs from Rust's and Go's privacy-aware lookup, where a private name does not match outside its module.

#Hiding And Conflicts

  1. names.hide.depthIn each namespace, a member hides every member with the same name at a greater depth.
  2. names.conflict.definitionA conflict is two or more members with one name at the smallest depth where that name occurs. That includes one member reached through two different paths. An example is the embedded field name of a type embedded twice at one depth.
  3. names.conflict.private-shadowAn own field or inherent method of S may have the name of a promoted member in its namespace. When that field or method is not pub, it is also a conflict.

Only a pub own member hides a promoted one. A private own member with that name is an error, and its author picks another name:

Error example
struct CreatedBySystem:
    pub id: string

struct AuditDraft:
    CreatedBySystem
    id: string  # error: ambiguous-promoted-member

struct ReviewDraft:
    CreatedBySystem
    pub id: string  # valid: hides CreatedBySystem's id

#Conflicts Are Declaration Errors

  1. names.conflict.errorEvery conflict is an error at the declaration of S, never at a use, so lookup never meets one. Error: ambiguous-promoted-member.
Error example
struct Left:
    pub id: string

struct Right:
    pub id: string

struct LeftBox:
    Left

struct RightBox:
    Right

struct Record:
    LeftBox
    RightBox  # error: ambiguous-promoted-member

Note. For the error revamp: a conflict is reported on the later of the two embedded fields of S that reach the conflicting members. When both are reached through one embedded field E, it is reported at S only when it is not also a conflict of E's type. The message names both paths, as in Record.LeftBox.Left.id and Record.RightBox.Right.id. A private own member's conflict is reported on that member, and its message names the promoted member's path, as in AuditDraft.CreatedBySystem.id.

Note. A package that adds a pub member to a type used as a part can therefore break the declarations of types that embed it. They break in their own packages, and a use never breaks.

Note. An implementation may compute one table of resolved members for each struct. It holds the type's own members at depth 0 and the pub members of every part at the part's depth. A shallower member replaces a deeper one. A part's private member is not in the table, so it hides nothing there. The rules above define only the result.

#Field Lookup

Field lookup of x.name from a module M proceeds as follows:

  1. names.field-lookup.selectSelection. Among the fields of S that lookup considers, the field named name at the smallest depth is selected. There is at most one, because a conflict is a declaration error.
  2. names.field-lookup.privateVisibility. When the selected field is an own field of S that is not visible from M, the use is an error. Error: private-member.
  3. names.field-lookup.unknownNot found. If lookup considers no field named name, the use is an error. Error: unknown-field.
struct User:
    name: string

fn demo(user: User) -> string:
    user.name

#Method Lookup

Method lookup of x.name(args) from a module M tries an own inherent method, then the candidates, and reports an error when neither applies.

#Own Inherent Methods

  1. names.method-lookup.inherentIf S has a visible inherent method named name, it is selected; it wins over every trait method.
  2. names.method-lookup.inherent.by-nameSelection is by name alone, whatever the method's arity or parameter types. A visible inherent method with the wrong signature is still selected, and the call is then checked against it.
struct Counter:
    value: i32

impl Counter:
    fn read(self) -> i32: self.value

fn demo(c: Counter) -> i32:
    c.read()

#Method Candidates

  1. names.method-lookup.candidatesOtherwise the candidates are the promoted candidate and the trait candidates.
  2. names.method-lookup.promoted-candidateThe promoted candidate is, among the promoted inherent methods that lookup considers, the one named name at the smallest depth, if any.
  3. names.method-lookup.trait-candidatesThe trait candidates are the trait methods of S named name whose trait is available at the call, wherever their implementations are declared.
  4. names.method-lookup.unavailableA trait method whose trait is not available is not a candidate and has no effect on the lookup.
  5. names.method-lookup.oneExactly one candidate is selected; a single candidate with the wrong signature is still selected, and the call is then checked against it.
  6. names.method-lookup.generic-traitWhen the only candidates come from several instantiations of one generic trait, the call chooses among them as in Method Resolution.
  7. names.method-lookup.ambiguousA promoted candidate beside a trait candidate is an error, and so are trait candidates of two or more traits. This holds whatever the signatures, and wherever the implementation is declared, a delegating one included. Neither silently wins. Error: ambiguous-method.

Note. For the error revamp: the ambiguous-method message suggests the trait-qualified form Trait::name(x, ...) or the explicit path x.E1...Ek.name(args).

Error example
trait Describe:
    fn describe(self) -> string

struct Base:
    name: string

impl Base:
    pub fn describe(self) -> string:
        "base " + self.name

struct Page:
    Base

impl Describe for Page:
    fn describe(self) -> string:
        "page " + self.Base.name

fn invalid(page: Page) -> string:
    page.describe()  # error: ambiguous-method

#Methods Not Found

  1. names.method-lookup.privateWith no candidate, the use is an error when S itself has an inherent method named name, which is then not visible from M. Error: private-member.
  2. names.method-lookup.unknownWith no candidate and no such inherent method of S, the use is an error. Error: unknown-method.
  3. names.method-lookup.hint.useThe message should suggest a use declaration when S has a trait method named name whose trait is not available at the call.
  4. names.method-lookup.hint.fieldThe message should suggest (x.name)(args) when the receiver has a field named name.
Error example
struct Button:
    on_click: fn(i32) -> i32

fn invalid(button: Button) -> i32:
    button.on_click(41)   # error: unknown-method

Note. For the error revamp: the message should suggest the explicit path x.E1...Ek.name(args) when a part's type has a trait method named name.

#Method Lookup Example

Here Page embeds Label, Label implements Display and embeds Base, and Base has a pub inherent to_string:

Error example
struct Base:
    pub name: string

impl Base:
    pub fn to_string(self) -> string:
        "base " + self.name

struct Label:
    Base

impl Display for Label:
    fn to_string(self) -> string:
        "label"

struct Page:
    Label

fn show(page: Page) -> string:
    page.to_string()                # Base's, promoted at depth 2; Label's method is not promoted
    page.Label.to_string()          # error: ambiguous-method
    Display::to_string(page.Label)  # Label's Display method
    page.Label.Base.to_string()     # Base's to_string

If Page also implemented Display, page.to_string() would be ambiguous-method too, and Page's method is called as Display::to_string(page). Without Base, page.to_string() is unknown-method, and its message should suggest page.Label.to_string().

  1. names.promoted.pathWhen the selected member is promoted, x.name means the explicit path x.E1.E2...Ek.name through the embedded fields E1 to Ek. It has the same type, writability, and evaluation. Each embedded step follows its container's access, so through a writable receiver a promoted field may be assigned and a promoted &mut self method called. A promoted method runs as the embedded type's own method with the part as its receiver, so there is no overriding.
  2. names.promoted.readonlyThrough a receiver that is not writable, a promoted field cannot be assigned, and a promoted &mut self method is selected and then rejected. Lookup never skips it to try another member. Error: not-writable.
  3. names.promoted.explicitExplicit qualification through an embedded field, as in x.E1.name, starts a new lookup with E1's type as the receiver's type. There the member's own visibility applies. It reaches a private member of a part, as in x.Part.secret, where that member is visible.
Error example
struct Resetter:
    count: i32

impl Resetter:
    pub fn reset(self) -> i32:
        0

struct Wrapper:
    Resetter

struct Counter:
    value: i32

impl Counter:
    pub fn reset(&mut self) -> void:
        self.value = 0

struct Page:
    Counter
    Wrapper

fn invalid(page: Page) -> void:
    page.reset()  # error: not-writable

See also: Writable Places.

#Dependency Changes

Note. The shallower member wins even across packages. A part's type in a dependency may gain a public member at a shallower depth than the one a use selects. The use then silently selects the new member.

Note. No other change outside the package that owns S switches a use silently. A new conflict is an error at the declaration of S, and a new candidate can make a call ambiguous but never takes its place.

Why. This consequence is intended. Rust accepts the same kind of switch when a trait import changes which Deref step answers a method call.

#No Overriding

A promoted method is its explicit path, as names.promoted.path states, so inside it self has the embedded type. A call self.m() inside Base's methods resolves against Base and never reaches a method of a type that embeds Base.

See also: Embedding And Trait Satisfaction.

#Enum Variants And Tuple Members

Enum variants may be named with the qualified or the contextual spelling:

status := JobStatus.Queued

match status:
    .Queued => "waiting"
  1. names.variant.memberEnum variants are members of their enum.
  2. names.variant.spellingConstruction and patterns may use the qualified spelling or .Variant with an unambiguous contextual enum type.
  3. names.tuple-member.underscoreTuple members are named _0, _1, and so on: _ followed by the zero-based element index.
  4. names.tuple-member.identifierA tuple member name is an ordinary identifier.

#Unsupported Scope Extensions

  1. names.unsupported.shadow-warninghd-lang permits shadowing of outer local names; a style tool may warn about it, but that warning is not part of language semantics.
  2. names.unsupported.variant-usehd-lang does not support direct uses of enum variants.
  3. names.unsupported.stored-valueTop-level stored values use ordinary := and let bindings; there is no separate stored-value declaration form.
enum Choice:
    Yes
    No

fn demo() -> Choice:
    Choice.Yes   # the qualified spelling