Visibility and Scopes
Sec uses names, scopes and visibility rules designed to make identifier resolution predictable from source code.
Visibility is encoded directly in an identifier's prefix:
Name
_name
__name
These spellings mean public, module-internal and private visibility respectively when they are declared in scopes that export names.
Visibility is only one part of name resolution. Sec also defines one declaration namespace per scope, forbids shadowing, combines type-member declarations into one member namespace, and registers selected declaration surfaces before body analysis so that ordinary forward references work predictably.
Identifier Model
An identifier names a declaration or binding.
Identifiers may name types, functions, variables, parameters, generic parameters, fields, properties, methods, events, nested declarations, imports, aliases, pattern bindings and loop bindings.
Character-level identifier syntax belongs to the lexical rules. This chapter defines the semantic use of valid identifier spellings.
One Declaration Namespace per Scope
Sec does not maintain separate type and value namespaces inside one scope.
This is invalid:
type User struct {
Id: uint64
}
fn User() int {
return 1
}
This is also invalid:
type User struct {
Id: uint64
}
let User := 10
A programmer should not need to know whether a syntactic position expects a type or value before knowing which declaration the spelling denotes.
Qualified names remain distinct:
storage.Reader
network.Reader
The Module Namespace Spans All Source Files
Every source file belonging to one module contributes to one shared module declaration namespace.
storage/
reader.sec
factory.sec
reader.sec:
module storage
type Reader struct {
...
}
factory.sec:
module storage
fn CreateReader() Reader {
...
}
No sibling-file import is needed. Conflicting module-scope declarations also conflict across source files. A filename does not become part of declaration identity.
Function Overload Groups Occupy One Name
fn Format(value: int) string {
return value.ToString()
}
fn Format(
value: int,
pattern: string,
) Result[string, FormatError] {
return value.ToString(pattern)
}
These declarations form one overload group named Format. A non-function
declaration cannot reuse that name in the same scope.
Two overloads need distinct parameter signatures. Return type does not participate in overload identity or overload resolution.
fn Read() int {
return 1
}
fn Read() string { // invalid
return "one"
}
Named Types Have One Member Namespace
Fields, properties, methods, method overload groups, associated/static functions, instance/static bindings, events and nested declarations share one member namespace.
type Person struct {
Name: string
}
impl Person {
fn Name() string { // invalid
return self.Name
}
}
Member conflicts are checked across the type declaration, primary ordinary
impl and valid same-module impl extends contributions.
Moving an impl fragment to another file does not create another member namespace.
Nested Declarations Belong to Their Owning Type
Outside the containing type and implementation context, use the qualified name:
Vehicle.FuelType
Inside the owning implementation, the short name may be used when unambiguous:
impl Vehicle {
fn DefaultFuel() FuelType {
return FuelType.Electric
}
}
A nested declaration never leaks into the surrounding module namespace.
Lexical Scopes
Relevant scopes include modules, types, impl blocks, functions, methods, property accessors, ordinary blocks, if branches, switch cases, match arms, loops, closures, pattern-binding regions and generic parameter lists.
Local Names Begin at Their Declaration Point
Ordinary local bindings become visible only after their declaration has been analyzed.
let value := value + 1 // invalid
The local being declared is not visible inside its own initializer.
Sec Forbids Shadowing
let count := 10
if enabled {
let count := 20 // invalid
}
Parameters cannot be shadowed by loop or pattern bindings:
fn Process(value: int) void {
for value in values { // invalid
...
}
}
Because types and values share the namespace, a local cannot hide a visible type:
type Packet struct {
}
fn Decode() void {
let Packet := 10 // invalid
}
The rule applies to locals, parameters, loop bindings, pattern bindings, generic parameters, nested declarations, imports and aliases.
Disjoint Sibling Scopes May Reuse a Name
if condition {
let result := First()
} else {
let result := Second()
}
The branch-local declarations do not shadow one another because neither is visible from the other branch.
Reserved Language Names Cannot Be Declarations
Keywords and modifiers cannot be used as declaration names:
let mut := 3 // invalid
fn unsafe() void { // invalid
}
type match int // invalid
Compiler-known fundamental type names and built-in type constructors are also reserved where the lexical rules define them as reserved.
let list := 3 // invalid
fn map() void { // invalid
}
type int uint64 // invalid
Contextual spellings are different: one spelling may have a language role in one grammar position while remaining an ordinary identifier elsewhere.
set Is Contextual
In type position:
let values: set[int]
Inside a property:
property Value: int {
set value {
_value = value
}
}
and:
property Value: int {
try set value {
_value = value
}
}
Outside those contexts, set may remain an ordinary identifier when the
grammar resolves it unambiguously:
let set: int := 3
fn set(value: int) int {
return value
}
x Can Be an Identifier or an Operator
As an identifier:
let x := 10
fn Scale(x: float32) float32 {
return x * 2.0
}
As matrix multiplication in the appropriate shaped-expression position:
let result := left x right
x is not globally reserved merely because it also has an operator role.
Visibility Is Encoded in the Identifier Prefix
| Spelling | Visibility |
|---|---|
Name |
Public |
_name |
Module-internal |
__name |
Private to its declaring owner |
The prefix is part of the identifier spelling. It is not a separate visibility modifier.
Public Names
A declaration without a leading underscore is public when declared in a scope that can export names.
type Reader struct {
}
fn Open() Result[Reader, IOError] {
...
}
Public visibility is still subject to project/module access restrictions. A public
declaration behind an inaccessible internal module boundary remains inaccessible.
Module-Internal Names
A name beginning with one underscore is visible throughout the declaring module but is not exported from that module.
type _BufferState struct {
}
fn _ValidateHeader() bool {
return true
}
All source files in the same module may use these declarations. External modules may not.
Private Names
A name beginning with two underscores is private to its declaring owner.
At module scope, the owner is the source file:
fn __ReadHeader() void {
...
}
A sibling source file in the same module cannot access it.
| Declaration | Same file | Sibling file, same module | External module |
|---|---|---|---|
Name |
Yes | Yes | Yes, subject to module access |
_name |
Yes | Yes | No |
__name at module scope |
Yes | No | No |
Private Members Are Private to Their Owning Type
type Counter struct {
__value: int
}
impl Counter {
fn Increment() void {
__value += 1
}
}
The private field is accessible from the owning type's primary impl and valid
same-module extensions. Another type in the same file or module cannot access
Counter.__value.
Private nested declarations follow the same owner-based rule.
Leading Underscores Do Not Change Local Lexical Scope
let _temporary := Calculate()
let __state := ReadState()
These are ordinary local variables. Their prefixes do not make them module-internal or file-private, suppress unused diagnostics, or turn them into discard bindings.
Bare _ Is Not a General Identifier
Bare underscore is a context-specific grammar form.
Defined uses include match ignore patterns, reserved unnamed register bits and other grammar positions that explicitly assign underscore semantics.
It is not an ordinary variable named _.
User-Defined Nominal Types Begin with an Uppercase Letter
After any visibility prefix, every user-defined nominal type begins with an uppercase letter.
Valid:
type Percent int
type Person struct {
Name: string
}
type Response union {
}
enum Color int {
Red = 1
}
interface Reader {
fn Read() Result[string, IOError]
}
type _ModuleState struct {
}
type __PrivateState struct {
}
Invalid:
type percent int
type person struct {
}
interface reader {
}
This is a language rule, not formatter style. It applies to named scalar/distinct types, structs, unions, enums, registers, interfaces, error types, generic nominal types and nested nominal types.
Compiler-known lowercase first-class types such as int,
string, list, map, set,
vector, matrix, tensor and
tensor_view are language-defined exceptions. Unit symbols follow unit naming rules.
Other Naming Styles
Sec 0.1 does not impose one universal capitalization convention on functions, methods, fields, properties, immutable values, variables, parameters, modules or enum values.
They still obey uniqueness, no-shadowing, reserved-name, visibility-prefix and category-specific rules.
The formatter never renames declarations to impose a style convention.
Generic Parameters
Generic type parameters are type names and begin with an uppercase letter:
type Pair[T, U] struct {
First: T
Second: U
}
Generic parameter names must be unique and must not shadow another generic parameter, a visible type, a visible value or a member made directly visible by the declaration grammar.
Compile-time value parameters follow ordinary value naming unless a more specific generic rule applies.
Imports and Aliases Participate in Name Resolution
import "storage"
import customerOrder "customer/order"
They create source-file-local qualifier bindings:
storage.Reader
storage.Open()
customerOrder.CreateCustomerOrder()
Import bindings do not become bindings in sibling source files. They participate in normal namespace conflicts and no-shadowing checks.
Unqualified Name Lookup
Unqualified lookup proceeds from the innermost valid scope outward.
- active pattern, loop, closure or block scope;
- function or property scope;
- generic parameter scope;
- owning type and impl member scope;
- module scope;
- the current file's imported module bindings and aliases;
- always-available core declarations.
Because shadowing is forbidden, an ambiguous outer/inner reuse is normally rejected at declaration time instead of becoming a lookup tie-breaker.
Qualified Lookup Uses the Resolved Owner
value.Member
Type.AssociatedFunction()
Module.Declaration
Semantic analysis resolves the member as a field, property, method, associated function, enum value, nested type, event, compiler-known member or core member.
Visibility is then checked against that resolved declaration and its owner.
Selected Declaration Surfaces Support Forward References
The compiler registers declaration surfaces before body analysis where Sec permits forward references.
At module scope this includes types, interfaces, functions/overload groups, supported module-scope bindings and the current file's imports/aliases.
For a type and its impl surface this includes fields, nested declarations, properties, methods/overload groups, events and supported associated/static bindings.
This permits methods to call methods declared later, properties to refer to later properties, and module declarations to refer across source files without file-order semantics.
Local Variables Do Not Have Forward References
Use(value) // invalid
let value := 10
Forward-reference support belongs to registered declaration surfaces, not ordinary local bindings.
Semantic Name Identity Includes the Owner Path
storage.Reader
network.Reader
Vehicle.FuelType
Result.Ok
Function overload identity additionally includes the parameter signature. Return type is excluded from overload identity.
Visibility prefixes remain part of source name identity unless an ABI rule defines an external symbol transformation.
Compiler-generated hidden names use compiler-private identities; the compiler does
not rely on a user-accessible __ prefix alone.
Visibility and Module Access Are Separate
A declaration may be public and still be unreachable because its module is inaccessible.
services/
internal/
transport/
transport.sec
A public declaration in that module remains inaccessible to importers outside the
subtree allowed by the internal rule.
Access therefore requires both an accessible module/import path and a declaration visible from the caller.
Diagnostics
Name-resolution diagnostics should distinguish conflicts, shadowing, inaccessibility and unknown names instead of collapsing them into one generic error.
Namespace conflict
function `User` conflicts with type `User`
the type is declared here:
type User struct { ... }
the function would occupy the same declaration namespace
Shadowing
local declaration `count` shadows a visible declaration
the existing `count` is declared here:
let count := 10
help: choose a distinct local name
Module-internal visibility
`_Open` is module-internal to `storage`
this declaration is not visible from the current module
Source-file-private visibility
`__ReadHeader` is private to its declaring source file
the current source file belongs to the same module but is not the private owner
Private member
`Counter.__value` is private to `Counter`
private members may be accessed only from the owning type's valid implementation context
Nominal type capitalization
user-defined type `percent` must begin with an uppercase letter
help: rename it to `Percent`
Diagnostics preserve original source spelling, identify both declarations in a conflict, distinguish duplicate declarations from overload conflicts, and distinguish inaccessible declarations from unknown names.
Formatter and Language-Server Behavior
The formatter preserves identifier spelling and capitalization. It does not rename public, module-internal, private or user-defined nominal declarations.
Syntax highlighting and semantic tokens classify contextual spellings such as
set and x from grammar context rather than spelling alone.
A rename operation must reject a new name when the rename would:
- collide in the unified declaration namespace;
- create an invalid overload;
- shadow a visible declaration;
- use a reserved language name;
- violate the uppercase nominal-type rule;
- change intended visibility by adding or removing
_or__.
Debugger and diagnostic displays should use fully qualified names whenever the short name would otherwise be ambiguous.
Best Practices
- Use unprefixed names for declarations intentionally exposed as public API.
- Use one leading underscore for implementation details shared across files in one module.
- Use two leading underscores for declarations private to one source file or owning type.
- Do not use underscore-prefixed locals merely to suppress diagnostics; they remain ordinary bindings.
- Prefer distinct descriptive names instead of relying on shadowing; Sec rejects shadowing intentionally.
- Keep nested declarations qualified outside their owning type.
- Use import aliases when dependency qualifiers would collide.
- Remember that public visibility never bypasses an
internalmodule boundary. - Begin every user-defined nominal type and generic type parameter with an uppercase letter.
- Do not rely on source-file or impl-fragment order for declaration visibility.
- Let contextual spellings be interpreted by grammar context rather than treating them as globally reserved.
Summary
- Sec uses one declaration namespace per scope.
- Types and values do not occupy separate namespaces.
- All source files in one module contribute to one shared module namespace.
- Function overloads form one namespace entry under their shared name.
- Parameter signature distinguishes overloads; return type does not.
- A named type has one combined member namespace.
- Primary impl and valid same-module impl extensions contribute to one member surface.
- Nested declarations belong to their owning type.
- Ordinary local bindings become visible at their declaration point.
- A local declaration is not visible inside its own initializer.
- Sec forbids shadowing of visible declarations.
- Disjoint sibling scopes may reuse a spelling when neither declaration is visible from the other.
- Keywords, modifiers and reserved language names cannot be used as declarations.
- Contextual spellings are resolved by grammar position rather than globally reserved.
setis contextual between collection type syntax, property setter syntax and ordinary identifier use.xis contextual between ordinary identifier use and shaped matrix multiplication.- Visibility is encoded in the identifier spelling, not in separate visibility modifiers.
- An unprefixed exportable name is public.
- A single leading underscore denotes module-internal visibility.
- A double leading underscore denotes privacy to the declaring owner.
- At module scope, a double-underscore declaration is private to its source file.
- A private type member is private to the owning type and its valid implementation context.
- Leading underscores on local variables do not alter lexical scope or suppress diagnostics.
- Bare
_is a context-specific grammar form rather than a general identifier. - User-defined nominal types begin with an uppercase letter after any visibility prefix.
- Compiler-known lowercase first-class types are language-defined exceptions.
- Sec 0.1 does not impose one universal capitalization style on functions, fields, values or modules.
- Generic type parameters begin with an uppercase letter and must not shadow visible declarations.
- Import names and aliases live in the current source file's import-binding scope.
- Import aliases participate in normal conflict and no-shadowing rules.
- Unqualified lookup proceeds from innermost lexical scopes outward through member, module, import and core scopes.
- Qualified lookup uses the resolved owner type or module.
- Module, type and member declaration surfaces are registered before body analysis where forward references are permitted.
- Forward references may cross source files within one module.
- Ordinary local variables do not receive forward-reference behavior.
- Semantic declaration identity includes the qualified owner path.
- Function overload identity additionally includes the parameter signature.
- Visibility prefixes remain part of source name identity.
- Compiler-generated hidden symbols use compiler-private identities instead of relying on user-accessible
__names. - Declaration visibility and module/import accessibility are separate checks.
- Public declarations inside inaccessible
internalmodules remain inaccessible to prohibited importers. - Diagnostics distinguish unknown names, inaccessible names, duplicate declarations, overload conflicts and shadowing.
- Formatter and LSP tooling preserve visibility spelling and contextual-name meaning.
- Rename operations reject changes that introduce conflicts, shadowing, reserved names, invalid capitalization or unintended visibility changes.