Skip to content

Names and Scope

This page defines the target surface model for how names exist in Shape.

The governing rule is lexical scope:

  • all user-defined names are lexically scoped
  • the outermost lexical scope is the module
  • Shape does not have a user-defined global namespace

The target model has six categories.

Top-level declarations and imports live in module scope.

That includes:

  • user-defined fn, type, enum, trait, interface, annotation, and mod
  • imported names from from ... use { ... }
  • imported module namespaces from use some_module
  • public exports from ordinary modules
  • builtin surface API declared by stdlib modules such as Option, Result, DateTime, print, and format

Builtin surface names are not a separate “magic global” category in the design. They are still owned by modules, typically under std::core::*.

from std::core::intrinsics use { Option, Result, DateTime }
from std::core::remote use { @remote }
use std::core::state as state

Names introduced inside a function, block, lambda, or pattern are local lexical bindings:

  • parameters
  • let / const bindings
  • pattern bindings from match
  • lambda parameters and captures
fn render(user) {
let label = user.name
match user.role {
"admin" => label
other => other
}
}

Users can create local names freely, but they cannot create new names outside lexical scope.

Some names belong to a type rather than to module scope.

That includes:

  • enum variants such as Status::Ready
  • associated constructors such as Result::Ok, Result::Err, Option::Some, and Option::None
  • associated items attached to a type
  • methods resolved from a receiver or type
from std::core::intrinsics use { Result }
let value = Result::Ok(1)

The intended rule is that associated namespaces come with the type. If a type is in scope, its associated namespace is addressable through that type.

Some spellings belong to the language grammar itself, not to modules:

  • keywords such as if, match, fn, let, return
  • literal spellings such as true, false, numbers, and strings
  • primitive type spellings such as int, number, string, and bool

These are part of Shape syntax. They are not imported and they are not exports.

Shape should keep the implicit prelude intentionally tiny.

The intended rule is:

  • a very small number of module-owned surface names may be available everywhere without an explicit import
  • print is the canonical example
  • everything else should be imported explicitly or accessed through a module namespace
print("hello")

The prelude does not create a separate owner for a name. It only means the name was imported implicitly.

There is also a lower-level implementation layer used by the stdlib and the compiler:

  • __intrinsic_*
  • __native_*
  • __json_*

These are not part of the public language surface. They are implementation hooks, not user-facing API. User programs should not consume them directly.

If a bare name is not:

  • in local scope
  • in module scope through an explicit import
  • available through a tiny implicit prelude import
  • or syntax-reserved

then it should not resolve.

If a name belongs to a type, it should be reached through that type’s associated namespace rather than through a freestanding global binding.

The clean mental model is:

  • all user-defined names are lexically scoped
  • top-level names live in modules
  • builtin surface API is also module-owned
  • associated constructors and variants belong to type scope
  • only syntax-reserved names, the tiny implicit prelude, and internal intrinsics sit outside ordinary module/local scope

This keeps the surface explicit, avoids accidental collisions, and makes imports carry the meaning of where a capability comes from.