Skip to content

Volume 04: Declarations and Bindings

1. Introduction

This volume defines how names, declarations, bindings, scope, visibility, mutability, imports, and lifetime operate in the Rethon programming language.

This document is part of the official Rethon Language Specification. It is normative rather than descriptive. Implementations, tooling, documentation systems, and other source-processing tools shall treat the rules in this volume as authoritative.

This volume does not define package management, build tooling, or runtime module loading strategy. It defines the language-level rules by which names are introduced, resolved, scoped, and exposed.

Rethon is Python-inspired, statically typed, value-oriented, and designed for long-term maintainability. The declaration model shall therefore prioritize readability, explicit mutability, predictable scope behavior, and a clear separation between ordinary mutable bindings and explicitly immutable bindings.

2. Declaration Model

A declaration is a source-level construct that introduces a named entity into a scope.

A declaration may introduce:

  • a binding to a value
  • a type name
  • a protocol name
  • a module-level name
  • an imported name
  • another named language entity defined by later volumes

Declarations shall be the primary mechanism by which names enter the symbolic environment of a module.

The ordinary declaration model shall introduce a mutable binding by default.

let shall introduce an immutable binding.

The language shall not initially provide separate alternate binding keywords beyond let.

Bare assignment used in a declaration context shall introduce a mutable binding. Bare assignment used after a declaration shall operate on an already declared binding.

2.1 Declaration and Binding

A declaration introduces a name.

A binding is the association between that name and a value.

The two concepts are related but not identical. A declaration may introduce a binding, but the declaration model shall remain conceptually separate from mutability, scope, and visibility.

Binding is the normative semantic term used throughout this specification. The word variable may appear in explanatory prose only when no additional semantic distinction is intended.

2.2 Declaration Kinds

Later volumes may define additional declaration kinds such as function declarations, type declarations, protocol declarations, member declarations, and module declarations. Those declaration kinds shall obey the same name-introduction principles defined here.

2.3 Declaration Point

A declaration shall become visible at its declaration point unless a later volume explicitly defines a special forward-reference rule for a particular construct.

A name shall not be usable before it is declared in ordinary source code.

2.4 Example

name: str = "Brian"
count: int = 0
count = count + 1
let app_name: str = "Rethon"

These declarations show the ordinary mutable binding model and the explicit immutable binding form.

3. Bindings

A binding is the association between a declared name and a value.

Bindings shall be either mutable or immutable.

Binding mutability shall describe whether the name may later be associated with a different value. It shall not by itself describe whether the bound value contains internal mutable state.

3.1 Binding Identity

A binding shall be identified by the declaration that introduced it.

Two bindings with the same spelling in different scopes are distinct bindings.

3.2 Binding and Value

Binding mutability and value mutability are separate concerns.

An immutable binding may refer to a value that internally contains mutable state if the value's type permits such state.

A mutable binding may refer to an immutable value type.

3.3 Binding Initialization

A binding declaration shall provide an initializer unless a later volume explicitly defines a declaration form that permits deferred initialization in that context.

3.4 Example

message: str = "Hello"
retries: int = 1

Both bindings are mutable by default and may be reassigned later if the declared type permits the new value.

4. Mutable Bindings

A mutable binding shall be the default binding model for ordinary source code.

A mutable binding shall be introduced by an ordinary declaration without let.

Example:

count: int = 0
count = count + 1

Mutable bindings shall be used when reassignment is semantically necessary or when a name is expected to refer to changing state over time.

4.1 Semantics

A mutable binding shall permit the name to be assigned a new compatible value.

The language shall distinguish reassignment of the binding from internal mutation of the bound value.

4.2 Use Cases

Mutable bindings are appropriate for:

  • counters
  • accumulators
  • loop-local state
  • temporary replacement of a value
  • state that is intentionally updated over time

4.3 Type Safety

A reassigned value shall satisfy the binding's declared type or inferred type.

A mutable binding shall not weaken static typing.

4.4 Default Direction

Mutable bindings shall be the default direction.

The default should remain close to Python where practical, while still preserving static typing and explicit immutability support.

5. Immutable Bindings

An immutable binding shall be a binding that may be assigned exactly once by its declaration.

let shall introduce an immutable binding.

Immutable bindings shall be used when a name is intended to remain fixed for the lifetime of its scope.

5.1 Semantics

Once an immutable binding has been introduced and initialized, the binding shall not be reassigned.

The language shall reject attempts to assign a new value to an immutable binding.

5.2 Rationale

Immutable bindings support readability, maintainability, and simpler reasoning about code.

They reduce accidental state changes and make source code easier to understand in nested scopes, shared APIs, and long-lived modules.

5.3 Value Orientation

Immutable bindings align naturally with Rethon's value-oriented design.

A value may be transformed by creating a new value and binding it to a new name rather than by mutating an existing binding.

5.4 Example

let user_name: str = "Brian"
let app_name: str = "Rethon"

The names user_name and app_name shall remain bound to the same values for the lifetime of their scopes.

5.5 Rebinding Error

let count: int = 1
count = 2

The second line shall be rejected because count is immutable.

6. Type-Annotated Declarations

A type-annotated declaration is a declaration that states the type of the declared name explicitly.

Example:

name: str = "Brian"
count: int = 0

6.1 When Annotations Are Required

An explicit type annotation shall be required when:

  • the declaration has no initializer
  • the declaration is public
  • type inference would be ambiguous
  • the declaration is part of a public API surface where explicitness is desirable
  • later volumes require explicit typing for the declaration form in question

6.2 When Annotations Are Allowed

A type annotation shall be allowed whenever a declaration introduces a typed name.

The language shall not prohibit explicit annotations merely because a type could be inferred.

6.3 Public Declarations

Public declarations shall normally use explicit type annotations so that the contract is visible in source form.

6.4 Example

public name: str = "Rethon"
public let library_name: str = "Rethon Core"

These declarations make the intended type and visibility explicit.

7. Type-Inferred Declarations

A type-inferred declaration is a declaration whose type is determined from its initializer or from a permitted surrounding context.

Rethon shall support type inference in declarations where the resulting type remains obvious and safe.

7.1 Local Inference

Local declarations may use type inference when the initializer determines the type unambiguously.

Example:

name = "Brian"
age = 42
active = True

7.2 Inference Limits

Type inference shall not be used when it would obscure meaning or weaken API clarity.

Inference shall preserve nullability and other relevant type distinctions established by the type system.

7.3 Public Surfaces

Type inference shall be limited on public declarations.

Public declarations shall generally use explicit annotations so that their meaning is visible to readers and tools.

7.4 Example

queue_size = 0

The binding is mutable by default and the inferred type remains visible to the compiler.

7.5 Ambiguity

When inference cannot determine a unique and safe type, the declaration shall require an explicit annotation.

8. Declaration Scope

Declaration scope is the region of source in which a declaration may be referenced by name.

A declaration shall be visible only within its declared scope, subject to the visibility and import rules in this volume.

8.1 Scope and Visibility

Scope and visibility are related but distinct.

Scope determines where a name exists in the source structure. Visibility determines where that name may be accessed across module boundaries or within lexical regions defined by the language.

8.2 Scope Boundaries

A new scope may be introduced by:

  • a module
  • a block
  • a function body
  • a method body
  • a type body
  • a protocol body
  • any later construct that establishes a nested lexical region

8.3 Enclosing Names

Names declared in an outer scope shall be visible in an inner scope unless shadowed by a declaration in the inner scope.

8.4 Example

let outer_name = "Rethon"

if ready:
    inner_name = outer_name

The declaration inner_name may refer to outer_name because the outer binding is in scope.

9. Lexical Scope

Rethon shall use lexical scope.

A name shall be resolved according to the source-level nesting structure in which it appears.

Lexical scope shall be the primary mechanism for local name visibility.

9.1 Resolution From Inner to Outer

When a name is referenced, the language shall search the nearest enclosing scope first and then outward through enclosing scopes until it finds a visible declaration.

9.2 Predictability

Lexical scope shall be predictable and local.

Source code shall not depend on runtime state or dynamic name injection for ordinary name resolution.

9.3 Example

let name = "outer"

if True:
    let name = "inner"
    value = name

The inner name shall refer to the declaration inside the nested scope.

10. Nested Scope

A nested scope is a scope contained within another scope.

Nested scopes shall be used for local structure, temporary names, and controlled visibility.

10.1 Access to Outer Names

A nested scope may read names from outer scopes unless a local declaration shadows them.

10.2 Local Isolation

A name declared in a nested scope shall not be visible outside that scope.

10.3 Example

let message = "start"

if ready:
    let message = "ready"
    status = message

final_message = message

The inner message shall not replace the outer message; it shall coexist only within the nested scope.

10.4 Nested Declarations

Nested declarations shall be allowed where later volumes define them.

A nested declaration may use names from its enclosing scope, subject to shadowing and visibility rules.

11. Shadowing

Shadowing occurs when a declaration in an inner scope uses the same name as a declaration in an outer scope.

Shadowing shall be allowed in nested scopes.

Shadowing shall not be allowed within the same scope unless a later volume explicitly defines a special form that permits it.

11.1 Shadowing Policy

Shadowing shall be permitted but discouraged.

The language should not rely on shadowing as an ordinary design technique because it can reduce readability and complicate review.

11.2 Same-Scope Redeclaration

A name shall not normally be declared more than once in the same scope.

Such redeclaration shall be treated as an error unless a later volume defines an exception for a specific construct.

11.3 Shadowing and Imports

Imported names may be shadowed by local declarations in the current scope.

This rule allows local code to remain explicit even when imported names use common identifiers.

11.4 Example

let name = "outer"

if ready:
    let name = "inner"

The inner declaration shadows the outer declaration only within the nested scope.

12. Name Resolution

Name resolution is the process of determining which declaration a name reference refers to.

Rethon shall resolve names deterministically according to lexical scope and visibility rules.

12.1 Resolution Order

When a name is used in an unqualified position, the language shall resolve it by searching:

  1. the current local scope
  2. enclosing lexical scopes
  3. names imported into the current module scope
  4. other names visible through the module's declared visibility rules

12.2 Ambiguity

If a name reference cannot be resolved uniquely and safely, the language shall diagnose an error.

12.3 Qualified Names

Later specification volumes may define qualified-name syntax for referring to names through modules or other namespaces.

Qualified names shall not depend on accidental local shadowing when a more explicit reference form is available.

12.4 Example

count = 10
text = count

The reference to count shall resolve to the nearest visible declaration named count.

13. Visibility

Visibility determines which source regions may access a declaration.

Rethon shall support the following visibility levels:

  • public
  • internal
  • private

13.1 Visibility and Scope

Scope and visibility are related but distinct.

A declaration may be in scope but not visible across module boundaries, or it may be visible through an exported form.

13.2 Visibility Goals

Visibility rules shall support:

  • clear module boundaries
  • stable public APIs
  • encapsulation of implementation details
  • tooling-friendly navigation and diagnostics
  • long-term maintainability

13.3 Default Direction

Module-level declarations shall default to internal visibility unless explicitly marked public.

Local declarations inside nested lexical blocks shall ordinarily be governed by lexical scope rather than by visibility modifiers.

13.4 Example

internal name: str = "core"
public let api_name: str = "Rethon"

The first declaration shall be available within the module only. The second shall be available to importing code.

14. Public Declarations

A public declaration shall be visible to code outside the defining module when that module is imported or otherwise referenced according to later module rules.

Public declarations shall represent the module's intended external contract.

14.1 Public API Meaning

A public declaration is part of the language-level surface that downstream code may rely on.

Public declarations shall therefore be stable, explicit, and clear.

14.2 Annotation Expectation

Public declarations shall normally use explicit type annotations so that their contract is obvious in source form.

14.3 Example

public let application_name: str = "Rethon"
public version: str = "1.0"

These declarations form part of the module's external interface.

14.4 Public and Imports

A public declaration may be imported by other modules according to the import rules in this volume.

15. Private Declarations

A private declaration shall be visible only within the lexical region that encloses it and any nested scopes within that region.

Private declarations shall be used for local helpers, implementation details, and names that should not be exposed outside the immediate source region.

15.1 Private Scope

A private declaration shall not be accessible outside the body, block, or other lexical region in which it is declared.

15.2 Intended Use

Private declarations are appropriate for:

  • helper names inside functions or methods
  • intermediate values that should not leak outward
  • implementation details inside nested declarations

15.3 Example

if ready:
    private diagnostic_message = "ready"

The name diagnostic_message shall be usable only within the containing lexical region.

15.4 Private and Encapsulation

Private declarations shall support encapsulation without requiring additional runtime mechanisms.

16. Internal Declarations

An internal declaration shall be visible anywhere within the defining module, but not outside that module.

Internal visibility shall be the default for module-level declarations when no explicit visibility modifier is given.

16.1 Module Boundary

Internal declarations shall be hidden from importing modules.

They remain available to the module's own source files and nested declarations within the module.

16.2 Intended Use

Internal declarations are appropriate for:

  • shared module-local helpers
  • non-public support APIs
  • implementation details that should remain stable only within the module boundary
  • names that support internal organization but are not part of the public contract

16.3 Example

internal cache_size: int = 64

This declaration may be used within the module but not imported as public API.

16.4 Relation to Public Declarations

Internal declarations shall be preferred over public declarations when a name is not intended for downstream use.

17. Module-Level Declarations

A module-level declaration is a declaration that appears in the outermost scope of a module.

Module-level declarations shall form the primary boundary between a module's public API and its internal implementation details.

17.1 Module Scope

The module scope shall contain names declared at the outermost level of the module.

Module-level declarations shall be accessible to later declarations in the same module, subject to ordering and visibility rules.

17.2 Default Visibility

Module-level declarations shall default to internal visibility unless explicitly marked public.

17.3 Example

version: str = "1.0"
public let library_name: str = "Rethon Core"

The first declaration shall remain internal by default. The second shall be exported as public.

17.4 Module-Level Nesting

Module-level declarations may contain nested declarations when later volumes define such constructs.

Nested declarations shall follow the scope and visibility rules defined in this volume.

18. Future Extension Note

Rethon does not initially provide a separate constant-declaration keyword or a dedicated compile-time constant model in this volume.

If later specification work determines that explicit constant declarations or compile-time constant forms are needed, they shall be introduced through a future extension that preserves the current mutable-default declaration model and the let-based immutable binding form.

Until such a future extension is accepted, ordinary mutable declarations and explicit immutable bindings shall remain the only binding forms defined here.

19. Declaration Lifetime

A declaration lifetime is the span of source-level validity for the binding or named entity introduced by a declaration.

Declaration lifetime shall be governed by lexical scope and visibility.

19.1 Binding Lifetime

A binding shall become usable at the point where its declaration becomes visible within the scope.

A binding shall cease to be usable when its scope ends.

19.2 No Use Before Declaration

A name shall not be used before its declaration within ordinary scope rules.

A later volume may define special forms that allow forward references in carefully controlled contexts, but such behavior shall not be assumed by default.

19.3 Lifetime and Values

The lifetime of a binding is not the same as the lifetime of the value associated with it.

The memory model may manage object lifetime independently of the lexical lifetime of the binding, but the binding itself shall follow the lexical rules of this volume.

19.4 Example

let message = "hello"
if ready:
    let local_message = message

local_message shall exist only within the nested scope, while message shall remain available for the outer scope.

20. Imports and Name Introduction

An import shall introduce names into the current module scope according to explicit source-level rules.

Imports shall be a name-introduction mechanism, not a package-management mechanism.

This volume does not define how modules are discovered, installed, versioned, or distributed.

20.1 Import Purpose

Imports allow a module to refer to names defined in another module without rewriting those names locally.

20.2 Import Forms

Later syntax volumes may define one or more import forms such as:

  • importing a module namespace
  • importing selected names from a module
  • importing a name under an alias

20.3 Name Introduction

Imported names shall behave as names introduced into the current module scope, subject to visibility rules of the imported module.

20.4 Aliasing

An imported name may be given a local alias when that improves readability or avoids collisions.

20.5 Example

import math
from collections import deque as queue

These forms illustrate module namespace import and selected-name import with aliasing.

20.6 Imports and Visibility

A declaration that is not visible outside its source module shall not be importable as part of that module's public surface.

20.7 Imports and Shadowing

Imported names may be shadowed by local declarations according to the shadowing rules in this volume.

21. Future Extensions

This volume defines the current direction for declarations and bindings in Rethon, but future specification work may refine the model in carefully controlled ways.

Possible future extensions include:

  • additional declaration forms for pattern binding or destructuring if later volumes justify them
  • more detailed export syntax
  • refined module and namespace controls
  • explicit re-export rules
  • specialized declaration forms for type parameters or member declarations
  • a dedicated constant model if the language later requires one
  • additional constant-expression contexts if the language later needs them

Any future extension shall preserve the current direction:

  • Python-inspired readability
  • static typing
  • value-oriented design
  • mutable bindings by default
  • explicit immutability through let
  • lexical scope and deterministic resolution
  • maintainable module boundaries

22. Summary

Rethon shall use explicit declarations to introduce names, with mutable bindings as the default and immutable bindings made explicit through let.

Type annotations shall be required for public declarations and whenever inference would be ambiguous. Type inference shall be allowed in local contexts where the initializer makes the meaning clear.

Scope shall be lexical and deterministic. Nested scopes may access outer names unless shadowed, and shadowing shall be allowed in nested scopes but discouraged as a regular design technique.

Visibility shall distinguish public, private, and internal declarations. Public declarations form the external API. Internal declarations remain within the defining module. Private declarations remain within the containing lexical region.

Imports shall introduce names into module scope without defining package management. The language shall not initially provide separate alternate binding keywords beyond let. Ordinary declarations shall be mutable by default, and let shall provide the language's explicit immutable binding form.

The declaration model shall remain readable, predictable, explicit where useful, and consistent with Rethon's static, value-oriented, Python-inspired language design.