prometeu-runtime/docs/specs/pbs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md
bquarkz 565fc0e451 dev/pbs (#8)
Co-authored-by: Nilton Constantino <nilton.constantino@visma.com>
Reviewed-on: #8
2026-02-03 15:28:30 +00:00

98 KiB
Raw Blame History

Prometeu Base Script (PBS)

Status: v0 (frontend-freeze) Goal:

  • explicit semantics
  • predictable runtime
  • two worlds:
    • SAFE: stack/value-first, no aliasing
    • HIP storage/heap: gate-baked with aliasing
  • no GC tracing, but with explicit memory management by refcounts + reclaim by frame

0. Philosophy (anchor section)

0. Philosophy (anchor section)

PBS is a stack-only, value-first language designed for explicit semantics and a predictable runtime.

0.1 Non-negotiable constraints (v0)

In v0, the following are true:

  • No implicit references on SAFE model.
    • The HIP introduces handles (gates) and aliasing with an explicit sintax (alloc, borrow, mutate, peek, take).
  • No aliasing of mutable state between values on the stack. Gate-baked types may alias and mutate state.
  • No exceptions: failure is expressed only via explicit types (e.g. optional, result) and explicit syntax (else, ?, handle).
  • No top-level execution: modules/files contain declarations only; initialization occurs only through explicit function/service calls.

0.2 Design principles (how v0 is shaped)

PBS favors:

  • Value-first: values are conceptually independent; copying is always safe; identity does not exist for user-defined values.
  • Explicit mutability: mutability is a property of bindings, never a property of types.
  • Didactic rules: important rules must be visible in syntax, not hidden in conventions.
  • Runtime-cheap execution: PBS shifts complexity to compile time (frontend-heavy) to keep the runtime simple and predictable.

1. Project, Files & Modules

This section defines the physical and logical structure of a PBS project. All rules in this section are normative.


1.1 Project Root and Source Layout

A PBS project has a single project root directory ({root}).

Rules:

  • {root} is the directory that contains the file prometeu.json.
  • A project must contain the directory {root}/src/main/modules.
  • All PBS modules of the project live exclusively under {root}/src/main/modules.

Import resolution:

  • The import prefix @project: is resolved relative to {root}/src/main/modules.
  • Any path after @project: is interpreted as a module path, not a file path.
  • project is declared into prometeu.json as the project name. and int the case of missing it we should use {root} as project name.

If {root}/src/main/modules does not exist, compilation fails.


1.2 Module Model

  • One directory = one module.
  • The name of a module is the directory name as it appears on disk.
  • Module names are case-sensitive and are not normalized.

A module boundary is absolute:

  • Parent directories do not implicitly see child modules.
  • Child directories do not implicitly see parent modules.
  • Sibling directories are completely isolated.

Example:

{root}/src/main/modules/
├─ gfx/
│ └─ draw.pbs
└─ gfx/math/
└─ vec.pbs

In this structure:

  • @project:gfx and @project:gfx/math are two distinct modules.
  • @project:gfx does not see any declarations in @project:gfx/math.
  • @project:gfx/math does not see any declarations in @project:gfx.

Cross-module access is only possible via import, and only for pub symbols.


1.3 Automatic Module Index

PBS has no mandatory barrel or index file.

For each module directory M:

  • The compiler scans all .pbs files directly inside M.
  • Files in subdirectories of M are ignored.
  • The modules public index consists of:
    • all pub symbols
    • declared in those .pbs files
    • separated by namespace (type and value).

The public index is the only surface visible to other modules.


1.4 Visibility Modifiers

Visibility is explicit and exhaustive. There are exactly three visibility levels.

File-private (default)

  • No visibility modifier.
  • Visible only within the declaring .pbs file.
  • Never visible to other files, even inside the same module.

mod (module-visible)

  • Visible to all files in the same module (same directory).
  • Not visible outside the module.
  • No import is required within the module.

pub (public)

  • Exported as part of the modules public API.
  • Visible to other modules via import.
  • Within the same module, pub behaves exactly like mod.

Important rules:

  • A symbol has exactly one visibility level.
  • There is no implicit or inferred visibility.
  • Visibility is checked independently in the type namespace and the value namespace.
  • Directory structure, file names, and compilation order have no effect on visibility.

1.5 Symbols Covered by Visibility Rules

Visibility rules apply uniformly to:

  • Type-level declarations:
    • declare struct
    • declare error
    • declare contract
  • Value-level declarations:
    • service
    • fn
    • let

No other mechanism may expose a symbol.


2. Namespaces & Visibility

PBS uses two distinct and independent namespaces: Type and Value. This separation is strict, explicit, and fully enforced by the compiler.

There are no implicit namespaces and no name overloading across namespaces.


2.1 Namespace Model

PBS defines exactly two namespaces:

Type namespace

The type namespace contains only type-level declarations introduced by declare.

Valid type declarations are:

declare struct Name { ... }
declare error Name { ... }
declare contract Name { ... }

Rules:

  • Only declarations introduced by declare may appear in the type namespace.
  • No executable or runtime symbol may appear in this namespace.
  • Type declarations participate in visibility rules (file-private, mod, pub) but always remain in the type namespace.

Value namespace

The value namespace contains executable and runtime-visible symbols.

Symbols in the value namespace are introduced by:

  • service
  • top-level fnmod or file-private (default).
  • top-level let are not allowed.

Rules:

  • Only top-level declarations participate in the value namespace.
  • Local let bindings do not participate in the file, module, or project namespace.
  • Local bindings are scoped strictly to their enclosing block.

2.2 Strict Namespace Separation

A name MUST NOT exist in both namespaces.

Rules:

  • If a name appears in the type namespace, it cannot appear in the value namespace.
  • If a name appears in the value namespace, it cannot appear in the type namespace.
  • Any attempt to declare the same name in both namespaces is a compile-time error.

Example (INVALID):

declare struct Vector { ... }

service Vector { ... } // ERROR: `Vector` already exists in the type namespace

2.3 Scope of Name Uniqueness

Name uniqueness is enforced based on visibility scope.


File-private symbols (default visibility)

  • File-private symbols are visible only within the declaring file.
  • File-private symbols may reuse names that appear in other files or modules.

Example (VALID):

// file A.pbs
fn helper() { ... }

// file B.pbs (same module)
fn helper() { ... } // OK: file-private symbols are isolated per file

mod and pub symbols

  • Symbols declared with mod or pub visibility must be unique inside the module.
  • Declaring two mod or pub symbols with the same name in the same module is a compile-time error, even if they appear in different files.

Example (INVALID):

// file A.pbs
mod service Gfx { ... }

// file B.pbs (same module)
mod service Gfx { ... } // ERROR: duplicate symbol in module

// file C.pbs (same module)
mod declare error Gfx { ... } // ERROR: duplicate symbol in module

2.4 Visibility Does Not Create Namespaces

Visibility controls who can see a symbol, but does not create separate namespaces.

Rules:

  • file-private, mod, and pub symbols all live in the same namespace.
  • Visibility only restricts access; it never permits name reuse at the same scope.

Example (INVALID):

mod service Audio { ... }
pub service Audio { ... } // ERROR: duplicate symbol in module

2.5 Summary of Namespace Rules

  • PBS has exactly two namespaces: type and value.
  • A name cannot exist in both namespaces.
  • File-private symbols are unique only within a file.
  • mod and pub symbols are unique at the module level.
  • Visibility never creates new namespaces or exceptions to uniqueness rules.

3. Top-level Declarations

This section defines which declarations are allowed at the top level of a .pbs file and their semantic role in the language. All rules in this section are normative.


3.1 Allowed Top-level Declarations

A .pbs file may contain the following declarations, in any order:

  • import

  • type declarations via declare:

    • declare struct
    • declare error
    • declare contract
  • service

  • fn

No other constructs are allowed at the top level. In particular:

  • let is not allowed at the top level.
  • No executable statements may appear at the top level.
  • There is no top-level initialization or execution.

3.2 Top-level Execution Model

PBS has no top-level execution.

Rules:

  • A .pbs file is a collection of declarations only.

  • Code is executed only when:

    • a service method is invoked, or
    • a fn is called from within another function or service method.
  • Module loading does not execute any user-defined code.

This rule ensures that:

  • module import order has no semantic effect
  • import cycles can be resolved purely at the symbol level
  • the runtime remains predictable and side-effect free during loading

3.3 Functions (fn)

Top-level fn declarations define reusable executable logic.

Rules:

  • A top-level fn is always mod or file-private.
  • A top-level fn cannot be declared as pub.
  • fn defaults to file-private visibility.

Example (VALID):

// math.pbs
fn clamp(x: int, min: int, max: int): int { ... }

Example (INVALID):

pub fn clamp(x: int): int { ... } // ERROR: top-level fn cannot be pub

Rationale:

  • fn exists for implementation and helper logic.
  • Any logic intended to cross file or module boundaries must be exposed via a service.

3.4 Services (service)

A service is a top-level declaration that represents an explicit API boundary.

Rules:

  • A service must always be declared with an explicit visibility modifier:

    • pub service — public API of the module
    • mod service — internal API of the module
  • There is no private service.

  • A service may optionally implement a contract.

Example:

pub service Audio
{
  fn play(sound: Sound): void { ... }
}

Services are the only mechanism for exposing executable behavior across files or modules.


3.5 Type Declarations (declare)

Type declarations introduce symbols into the type namespace.

Rules:

  • All type declarations must use the declare keyword.
  • Type declarations may be file-private, mod, or pub.
  • Visibility controls where the type can be referenced.

Example:

pub declare struct Vector { ... }
mod declare error IOError { ... }

3.6 Imports at the Top Level

Rules:

  • import declarations are allowed only at the top level of a file.
  • Imports must appear before any usage of imported symbols.
  • Imports have no runtime effect and exist solely for name resolution.

Example:

import { Vector } from "@project:math";

pub service Physics { ... }

3.7 Summary of Top-level Rules

  • A .pbs file contains declarations only.
  • No let or executable statements are allowed at the top level.
  • Top-level fn are always file-private.
  • service is the only way to expose executable logic beyond a single file.
  • Module loading is side effect free.

4. Resolver Rules

This section defines how the compiler resolves modules, imports, and symbol names into concrete declarations. All rules in this section are normative and fully deterministic.

The resolver operates purely at the symbol level. There is no top-level execution, and resolution never depends on runtime behavior.


4.1 Units of Compilation

PBS is compiled as a project.

Rules:

  • The compilation unit is a project, consisting of:

    • the project root,
    • all .pbs files under {root}/src/main/modules, and
    • all resolved dependency projects copied into prometeu-cache.
  • A module is exactly one directory.

  • A file unit is exactly one .pbs file.

There is no partial compilation of individual files or modules.


4.2 Resolver Phases

Name resolution is performed in two explicit phases.

Phase 1 — Symbol Collection

The compiler performs a project-wide scan to collect symbols.

Rules:

  • For each module:

    • All .pbs files directly inside the module directory are scanned.
    • All mod and pub symbols are collected into the module symbol table.
  • Symbols are collected separately for the type namespace and the value namespace.

  • Function bodies and service method bodies are not inspected in this phase.

If any of the following occur during Phase 1, compilation fails:

  • Duplicate mod or pub symbols in the same namespace within a module.
  • A symbol declared in both the type and value namespaces.

Phase 2 — File Resolution

Each file is resolved independently using the symbol tables built in Phase 1.

Rules:

  • All names referenced in a file must resolve to a symbol that is:

    • visible under visibility rules, and
    • uniquely identifiable.
  • Resolution of one file does not depend on the resolution of another file.


4.3 Visibility Gates

For any candidate symbol S, visibility is checked explicitly.

Rules:

  • file-private (default) — visible only within the declaring file.
  • mod — visible to all files in the same module.
  • pub — visible to other modules via import.

No implicit visibility exists.


4.4 Module Public Index

For each module directory M, the compiler builds a public index.

Rules:

  • The public index contains only pub symbols.
  • Only symbols declared in .pbs files directly inside M are considered.
  • Subdirectories never contribute to the parent modules index.
  • The public index is immutable once constructed.

The public index is the only source for resolving imports from other modules.


4.5 Import Resolution

Imports are the only mechanism for cross-module name resolution.

Syntax:

import { X, Y as Z } from "@project:module";

Rules:

  • Imports may only reference modules, never individual files.
  • Only pub symbols may be imported.
  • Each imported name must exist in the target modules public index.
  • Aliased imports (as) introduce a new local name bound to the imported symbol.

Invalid imports are compile-time errors.


4.6 Local Name Resolution Order

Within a file, name lookup follows this order within each namespace:

  1. Local bindings (let, parameters), innermost scope first.
  2. File-level declarations in the same file (file-private, mod, pub).
  3. Imported symbols.

Rules:

  • Local let shadowing is allowed, with warnings.
  • Shadowing of service names is not allowed.
  • Shadowing of top-level fn names is not allowed.
  • If an imported symbol conflicts with an existing mod or pub symbol in the module, compilation fails.

4.7 Cross-file and Cross-module Access

Within the same module

  • mod and pub symbols are visible across files without import.
  • File-private symbols are never visible outside their declaring file.

Across modules

  • Only pub symbols are visible.
  • Access always requires an explicit import.

4.8 Import Cycles

Import cycles are permitted only if symbol resolution can be completed.

Rules:

  • PBS has no top-level execution.
  • Cycles are evaluated purely at the symbol level.
  • Any cycle that prevents completion of Phase 1 (symbol collection) is a compile-time error.

4.9 Contracts and Services

Rules:

  • declare contract C introduces C in the type namespace.

  • service S: C resolves C as a type during Phase 2.

  • The compiler validates that S implements all signatures declared in C.

  • Signature matching is exact:

    • name
    • parameter types
    • parameter mutability
    • return type

Contracts themselves contain no executable logic and have no runtime behavior.


4.10 Resolver Guarantees

The resolver guarantees that:

  • Name resolution is deterministic.
  • Resolution does not depend on file order or import order.
  • All symbol conflicts are reported at compile time.
  • No runtime name lookup is required.

5. Services

This section defines the service construct. A service represents an explicit API boundary and the only mechanism for exposing executable behavior beyond a single file. All rules in this section are normative.


5.1 Role of a Service

A service has the following properties:

  • an explicit API boundary between PBS modules
  • conceptually a singleton.
  • it has no identity beyond its name.
  • there is never an implementation detail.

Any behavior intended to be visible outside a file must be exposed via a service.


5.2 Declaration and Visibility

A service must always be declared at the top level of a .pbs file.

Rules:

  • A service must declare its visibility explicitly.

  • Valid visibility modifiers are:

    • pub service — public API of the module
    • mod service — internal API, visible only inside the module
  • There is no such thing as a private service.

Example:

pub service Audio
{
  fn play(sound: Sound): void { ... }
}

5.3 Services and Contracts

A service may optionally implement a contract.

Syntax:

service S: ContractName { ... }

Rules:

  • ContractName must resolve to a declare contract type.

  • If a service declares a contract, it must implement all declared signatures.

  • Signature matching is exact and includes:

    • method name
    • parameter count and order
    • parameter types
    • parameter mutability
    • return type

Failure to satisfy a contract is a compile-time error.


5.4 Service Methods

Methods declared inside a service define its executable interface.

Rules:

  • All service methods are public within the service.
  • There are no private, protected, or helper methods inside a service.
  • Service methods are not top-level symbols.
  • Service methods are accessible only via the service name.

Example:

Audio.play(sound);

5.5 Interaction with Other Code

A service method may call:

  • top-level fn declared in the same file
  • other services that are visible under normal visibility rules

Rules:

  • A service method may not access file-private symbols from other files.
  • A service may not directly access implementation details of another service.

5.6 Implementation Rule

If logic is not conceptually part of the service API, it must not live inside the service.

Rules:

  • Implementation logic must be written as file-private fn.
  • Services act as thin API layers that delegate to internal functions.

Example:

pub service Math
{
  fn add(a: int, b: int): int
  {
    return add_impl(a, b);
  }
}

fn add_impl(a: int, b: int): int { ... }

5.7 Summary of Service Rules

  • service is the only construct for exposing executable behavior across files or modules.
  • A service is always declared with explicit visibility.
  • Services may implement contracts, which are checked statically.
  • All service methods are public within the service.
  • Services contain no hidden implementation logic.

6. Types

This section defines all value types available in PBS v0 and their semantic properties. All rules in this section are normative.

PBS is divided into:

  • Value types (stack): usually primitive types, including string, struct, optional, result and tuples.
  • Gate-backed types (storage/heap): text (string builders and concats), array<T>[N] (fixed-size arrays), list<T> (dynamic storage), map<K,V> (hash maps) and declare storage struct (custom gate-backed).
    • Gate-backed types are not value types, they are handles. Copy is inexpensive, not a deep copy.
    • On HIP a deep copy should be made using copy(x).

6.1 Type Categories

PBS defines three categories of types:

  1. Primitive types
  2. User-defined struct types
  3. Composite container types (defined elsewhere, e.g. fixed arrays)

All types in PBS are value types.


6.2 Primitive Types

PBS provides a small, fixed set of primitive types:

  • void
  • int — 32-bit signed integer
  • long — 64-bit signed integer
  • float — 32-bit IEEE-754
  • double — 64-bit IEEE-754
  • bool
  • char — Unicode scalar value (32-bit)
  • string
  • bounded — unsigned 16-bit integer for indices and sizes

Rules:

  • Primitive types have no identity.
  • Primitive values are copied on assignment (except for string when on constant pool).
  • Primitive values cannot be partially mutated.

6.3 string

The string type represents an immutable sequence of Unicode characters.

Rules:

  • A string value is immutable.
  • String literals produce pool-backed string values.
  • Some operations may produce stack-owned (for example, concat) string snapshots.
    • when pool-backed, copy is cheap, since it is just a "handle" (pool-backed, compile time, constant pool).
    • when stack-owned, copy is expensive, since it is a deep copy (stack-owned, runtime).
  • Doesn't exist such a thing like alloc string, it never goes to HIP.
  • There is no heap/storage allocation for string at runtime.

Example:

let s1: string = "hello";          // pool-backed (cheap copy)
let s2: mut string = mut "hello";  // ERROR: string cannot be mutated

s1 += " world";                    // ERROR: no mutation, no +=
let s3: string = s1 + " world";    // OK: produces a runtime-owned snapshot (stack-owned)

6.4 User-defined Struct Types

User-defined value types are declared using declare struct.

Syntax:

declare struct Name(field1: T1, field2: T2) { ... }

Rules:

  • A struct is a pure value type.
  • A struct has no identity.
  • A struct has no subtyping, inheritance, or polymorphism.
  • A struct value conceptually exists only as data.

Assignment and passing:

  • Assigning a struct value copies the value.
  • Passing a struct as a function argument passes a value.
  • Returning a struct returns a value.

The compiler may optimize copies internally, but such optimizations must not be observable.


6.5 Mutability and Structs

Mutability is a property of bindings, not of struct types.

Rules:

  • A struct bound to an immutable binding cannot be mutated.
  • A struct bound to a mutable binding may be mutated via mutating methods.
  • Mutating a struct affects only that binding.

Example:

let v = Vector.ZERO;
v.scale(); // ERROR: immutable binding

let w = mut Vector.ZERO;
w.scale(); // OK

6.6 The this Type

Inside a declare struct body, the special type this refers to the enclosing struct type.

Rules:

  • this may appear only inside a struct declaration.
  • this always refers to the concrete struct type, not to an abstract or dynamic type.
  • Outside a struct body, use of this is illegal.

Example:

declare struct Vector(x: float, y: float)
{
  pub fn len(self: this): float { ... }
  pub fn scale(self: mut this): void { ... }
}

6.7 Type Visibility

Type declarations follow the same visibility rules as other symbols.

Rules:

  • A type declaration may be file-private, mod, or pub.
  • Visibility controls where the type name can be referenced.
  • Visibility does not affect the runtime representation of a type.

6.8 Summary of Type Rules

  • All types in PBS are value types.
  • Types have no identity, ownership, or lifetime.
  • Mutability applies only to bindings.
  • Structs are copied by value; optimizations are not observable.
  • this provides explicit self-reference inside struct bodies.

7. Contracts

This section defines contracts in PBS. Contracts specify pure interface-level guarantees and may represent either:

  • an interface that a service must implement, or
  • a host-bound API implemented directly by the Prometeu runtime / host environment.

All rules in this section are normative.

Contracts introduce no user-defined runtime behavior and contain no executable code bodies.


7.1 Role of a Contract

A contract represents a static promise about an available API surface.

A contract:

  • defines a set of method signatures,
  • is validated entirely at compile time,
  • may be bound to a PBS service or directly to the runtime,
  • has no identity and no state.

Contracts are never instantiated and never called directly (unless it is a host-bound contract).


7.2 Declaring a Contract

Contracts are declared using declare contract.

Syntax:

declare contract ContractName
{
  fn methodName(param1: T1, param2: T2): R;
}

Rules:

  • A contract declaration introduces a symbol into the type namespace.
  • Contract declarations may be mod, or pub (same as services).
  • A contract body should contain only method signatures (no {}).
  • Contract methods have no bodies and no default implementations.

7.3 Contract Method Signatures

A contract method signature specifies:

  • method name
  • parameter list
  • parameter types
  • parameter mutability
  • return type

Rules:

  • Contract methods must not declare bodies.
  • Contract methods must not declare else fallbacks (it is implementation specifics).
  • Contract methods must not declare visibility modifiers.
  • Contract methods exist only for static validation.

Example:

declare contract AudioAPI
{
  fn play(sound: Sound): void;
  fn stop(id: int): void;
}

7.4 Host-bound Contracts

A contract may be declared as host-bound, meaning its implementation is provided directly by the runtime or host environment.

Syntax:

pub declare contract Gfx host
{
  fn drawText(x: int, y: int, message: string, color: Color): void;
}

Rules:

  • A host-bound contract should be called directly and should never be implemented by a service (Gfx.drawText(...)).
  • A host-bound contract is always bound to the target runtime or host environment.
  • A call of the form ContractName.method(...) resolves to a host call (syscall).
  • The runtime must provide an implementation for every method declared in a host-bound contract.
  • If a host-bound contract is unavailable in the target runtime, compilation or linking fails.

Host-bound contracts define the official API boundary between PBS and the runtime.


7.5 Calling Contracts

Rules:

  • Contract methods are invoked using the syntax:
ContractName.method(...);
  • This syntax is valid only for host-bound contracts.
  • Calling a non-host-bound contract directly is a compile-time error.

Example (INVALID):

declare contract Foo
{
  fn bar(): void;
}

Foo.bar(); // ERROR: Foo is not host-bound

7.6 Implementing a Contract in a Service

A service may implement a non-host-bound contract.

Syntax:

pub service Audio: AudioAPI
{
  fn play(sound: Sound): void { ... }
  fn stop(id: int): void { ... }
}

Rules:

  • A service that declares a contract must implement all contract methods.
  • Method matching is exact.
  • Missing or mismatched methods are compile-time errors.

7.7 Signature Matching Rules

For a service method to satisfy a contract method, all of the following must match exactly:

  • method name
  • number of parameters
  • parameter order
  • parameter types
  • parameter mutability
  • return type

Rules:

  • Parameter names do not need to match.
  • Overloading is not permitted.
  • A contract method may be implemented by exactly one service method.

7.8 Contracts and Visibility

Rules:

  • A contract may be implemented only by a service that can legally see it.
  • A host-bound contract must be visible at the call site.
  • Contract visibility affects name resolution only, never runtime behavior.

7.9 Runtime Semantics of Contracts

Contracts:

  • do not generate PBS code bodies,
  • do not introduce dynamic dispatch,
  • do not allocate memory,
  • do not exist as runtime values.

All contract validation is performed at compile time.


7.10 Summary of Contract Rules

  • Contracts define static API surfaces only.
  • Contracts live in the type namespace.
  • Contracts may be service-bound or host-bound.
  • Host-bound contracts define the PBS ↔ runtime API.
  • Only host-bound contracts may be called directly.
  • Contract satisfaction is checked statically and exactly.

8. Structs & Constructors

This section defines declare struct, its fields, constructor aliases, static constants, and struct methods. All rules in this section are normative.

Structs are pure value types (§6). They have no identity and no inheritance.


8.1 Declaring a Struct

A struct is declared using declare struct.

Syntax:

declare struct Name(field1: T1, field2: T2)
[
  // constructor aliases
]
[[
  // static constants
]]
{
  // methods
}

Rules:

  • The field list in parentheses defines the structs stored data.
  • The constructor alias block [...] and static constant block [[...]] are optional.
  • The method body block {...} is optional.

8.2 Fields

Rules:

  • Struct fields are private by default.
  • Fields cannot be accessed directly outside the struct declaration.
  • All field reads/writes must occur through struct methods.

Example (INVALID):

declare struct Vector(x: float, y: float)
let v = Vector(1, 2);
let x = v.x; // ERROR: field `x` is private

Example (VALID):

declare struct Vector(x: float, y: float)
{
  pub fn getX(self: this): float { ... }
}

8.3 Constructor Aliases

Constructor aliases are named constructors defined inside the alias block [...].

Example:

declare struct Vector(x: float, y: float)
[
  (): (0.0, 0.0) as default { }
  (a: float): (a, a) as square { }
]
{
}

Rules:

  • each struct has a default constructor that produces a full-initialized value.
  • Each alias defines a parameter list uses a default constructor or alias to create a new struct value.
  • Alias bodies { ... } are compile-time only and may contain only compile-time constructs (see §8.5).
  • Constructor aliases live in the type namespace.
  • Constructor aliases are not importable. Only the struct type name is importable.

Calling a constructor and constructor alias:

declare struct Vector(x: float, y: float)
[
  (s: float): (s, s) as square { }
  (s: float): square(s * s) as doubleSquare { }
  (x, float, y, float): (x, y) as normalized
  {
     let l = sqrt(x * x + y * y);
     this.x /= l;
     this.y /= l;
  }
  (): square(0) as zero { }
]

That is the default constructor, it takes all parameters always; there is no alias:

let v0 = Vector(1, 3);

That is the zero alias, it will call the default constructor with x = 0 and y = 0

let v1 = Vector.zero();

That is the square alias, it will call the default constructor with x = 2.0 and y = 2.0

let v2 = Vector.square(2.0);

That is the doubleSquare alias, it will call the square alias, and load x = 4.0 and y = 4.0

let v3 = Vector.doubleSquare(2.0);

That is the normalized alias, it will call the default constructor with x = 3.0 and y = 4.0 and normalize it (x = 0.6, y = 0.8)

let v4 = Vector.normalized(3.0, 4.0);

Name rules:

  • Alias names must be unique within the struct.
  • Alias names must not conflict with method names (to keep it didactic).

8.4 Methods

Struct methods are declared inside the struct body { ... }.

Rules:

  • Struct methods may be mod, or pub. file-private methods are not allowed and should rely on fn.
  • Methods are not top-level symbols.
  • Methods are invoked on a struct value using dot syntax.

Example:

declare struct Vector(x: float, y: float)
[
  (): (0.0, 0.0) as default { }
]
{
  pub fn len(self: this): float { ... }
}

let v = Vector.default();
let l = v.len();

Receiver rules:

  • Methods must declare the receiver as the first parameter, named self.
  • The receiver type must be either this or mut this.

Example:

declare struct Vector(x: float, y: float)
{
  pub fn len(self: this): float { ... }
  pub fn scale(self: mut this, s: float): void { ... }
}

Mutability of self follows the binding rules in §9.


8.5 Static Constants Block

The static constants block [[ ... ]] declares named constants associated with the struct.

Example:

declare struct Vector(x: float, y: float)
[
  (): (0.0, 0.0) as default { }
  (a: float): (a, a) as square { }
]
[[
  ZERO: default()
  ONE: square(1.0)
]]
{
}

Rules:

  • Static constants are compile-time constants.

  • Static constants are stored in the constant pool.

  • Static constants are immutable and can never be mutated.

  • The initializer of a static constant may use only:

    • struct constructor aliases of the same struct, and
    • other static constants of the same struct declared earlier in the same static block.

Static constant access:

let z = Vector.ZERO;

8.6 No Static Methods

PBS has no static methods on structs.

Rules:

  • All executable behavior must be expressed as instance methods.
  • Constructor aliases are the only “static-like” callable members of a struct.
  • Static constants are values only.

This avoids overloading the meaning of TypeName.member.


8.7 Summary of Struct Rules

Full example of struct:

declare struct Vector(x: float, y: float)
[
  (): (0.0, 0.0) as default { }
  (a: float): (a, a) as square { }
]
[[ 
  ZERO: default()
]]
{
  pub fn len(self: this): float { ... }
  pub fn scale(self: mut this): void { ... }
}
  • Structs are declared with declare struct.
  • Fields are private and cannot be accessed directly.
  • Constructor aliases exist only inside the type and are called as Type.alias(...).
  • Constructor aliases are not importable.
  • Static constants are compile-time values and are immutable.
  • Struct methods use an explicit self: this / self: mut this receiver.
  • There are no static methods.

9. Mutability & Borrow Model

PBS defines two distinct and explicit mutability worlds:

  • SAFE world (Stack / Value-first)
  • HIP world (Storage / Gate-backed)

These worlds have different guarantees, different costs, and different risks. Crossing between them is always explicit in the language.


9.1 The SAFE World (Stack / Value-first)

The SAFE world includes:

  • Primitive types
  • declare struct value types
  • optional<T> and result<T, E>
  • All values that are not allocated via alloc

Rules (SAFE World):

  • All values are value-first.
  • Assignment copies values conceptually.
  • Mutation is always local to the binding.
  • There is no observable aliasing between stack values.
  • No pointers, references, or handles exist in this world.

Example:

let a: Vector = Vector(1, 2);
let b = a;        // conceptual copy
b.x = 10;

// a.x is still 1

Mutation in the SAFE world never affects other bindings.


9.2 Method Receivers

Methods declare mutability explicitly on the receiver.

Rules:

  • A receiver declared as self: this is read-only.
  • A receiver declared as self: mut this allows mutation.
  • A mutating method requires a mutable lvalue at the call site. Whatever it is passed in will be a copy from there on.

Example:

let v = Vector.ZERO;
v.scale(); // ERROR

let w = mut Vector.ZERO;
w.scale(); // OK

A mutating method may not be called on:

  • static constants
  • immutable bindings

9.3 The HIP World (Storage / Gate-Backed)

The HIP world is entered explicitly via allocation:

let a = alloc array<int>[4b];

Types allocated with alloc are gate-backed and live in Storage (heap).

  • Properties of the HIP World
  • Values are accessed via gates (handles).
  • Assignment copies the handle, not the underlying data.
  • Aliasing is intentional and observable.
  • Mutation affects all aliases.
  • Lifetime is managed via reference counting (RC).

Example:

let a = alloc array<int>[2b];
let b = a;   // alias (shared storage)

mutate a as aa
{
  aa[0b] = 10;
}

let x = peek b[0b]; // x == 10

9.4 Gate Semantics (Strong and Weak)

All gate-backed types are strong gates by default.

  • T (gate-backed type) → strong gate
  • weak → weak gate

Strong Gates

  • Increment reference count (RC)
  • Keep the storage object alive
  • Assignment (let b = a) increases RC

Weak Gates

  • Do not increment RC
  • Do not keep objects alive
  • Observe storage without ownership

Conversions:

let s: Node = alloc Node;
let w: weak<Node> = s as weak;       // always succeeds

let maybeS: optional<Node> = w as strong; // may fail
  • strong as weak is always valid
  • weak as strong returns optional<T>

9.5 Reference Counting and Collection

PBS does not use tracing garbage collection.

Instead:

  • Storage objects are managed via reference counting
  • RC tracks:
    • references from the stack
    • references stored inside other storage objects
  • When RC reaches zero, objects become eligible for reclamation

Collection Model

  • Reclamation occurs at safe runtime points, typically:
    • end of frame
    • explicit GC step (implementation-defined)
  • Cycles of strong references are not collected automatically

This behavior is intentional and part of the learning model.


9.6 Weak References and Cycles

Cycles of strong references prevent RC from reaching zero:

a -> b
b -> a

To break cycles, PBS provides weak gates:

declare storage struct Node
{
  next: optional<weak<Node>>;
}

Weak gates allow cyclic graphs without leaks, at the cost of requiring explicit validation when accessing.


9.7 Controlled Access: peek, borrow, and mutate

All access to gate-backed data is explicit and controlled.

peek — Copy to SAFE World (it is a borrow sugar)

let v = peek a[i];
  • Returns a value copy
  • Never exposes references
  • Always SAFE

borrow — Read-only Access (HIP)

let len = borrow a as aa
{
  aa.len()
};
  • aa is a read-only reference
  • The reference cannot escape
  • The block returns a value

mutate — Mutable Access (HIP)

mutate a as aa
{
  aa[1b] = 42;
}
  • aa is a mutable reference
  • Mutation is shared across aliases
  • The reference cannot escape

9.8 take — Mutation Sugar

For single mutation operations, PBS provides take as syntactic sugar:

take out.push(value);

Equivalent to:

mutate out as o
{
  o.push(value);
}

take:

  • Is only valid on gate-backed types
  • Signals intent clearly
  • Keeps HIP usage concise

9.9 Summary of Guarantees

| Context | Aliasing | Mutation | Risk | | SAFE (stack) | None | Local only | None | | HIP (storage) | Explicit | Shared | Intentional |

PBS makes power explicit and risk visible. If you do not allocate, you are safe. If you allocate, you are responsible.


10. Expressions & Control Flow

This section defines the expression model and control-flow constructs of PBS. All rules in this section are normative.

PBS favors explicit, expression-oriented control flow with no hidden execution paths.


10.1 Expression Model

In PBS, most constructs are expressions.

Rules:

  • An expression always evaluates to a value.
  • The value of an expression is immutable unless bound to a mutable binding.
  • Expressions have no side effects except through explicit mutation of mutable bindings.

Statements exist only as a syntactic convenience; semantically, they are expressions whose value is ignored.


10.2 Blocks

A block is a sequence of expressions enclosed in { ... }.

Rules:

  • A block introduces a new lexical scope.
  • The value of a block is the value of its last expression.
  • A block with no final expression evaluates to void.

Example:

let x = {
  let a = 10;
  let b = 20;
  a + b
}; // x == 30

10.3 if / else

The if / else construct is an expression. The blocks on if doesn't return a value, it is purely a control flow construct.

Syntax:

if (condition)
{
  // do something
}
else
{
  // do something else
}

Rules:

  • condition must be of type bool.
  • Both branches will not produce any value.

10.4 when Expression

when is a conditional expression equivalent to a ternary operator.

Syntax:

when condition then expr1 else expr2

Rules:

  • when always requires an else branch.
  • Both branches must produce values of the same type.
  • when is an expression and cannot be used without binding or return.

Example:

let sign: int = when x >= 0 then 1 else -1;

let complex: int = when x < 0 then
{
    // do something really complex here
    return 0;
}
else
{
    // do something else even more complex here
    return 1;
}

10.5 Loops

PBS supports explicit looping constructs.


10.5.1 for Loop

The for loop iterates over a bounded range.

Syntax:

for i in [start .. end] {
  // body
}

Rules:

  • start and end must be of type bounded unless explicitly annotated otherwise.
  • The loop variable i is immutable by default.
  • The range is half-open: [start .. end).
  • The body executes zero or more times.

Example:

for i in [0b..10b] {
  sum += i;
}
for i in [..10b] {
  sum += i;
}
for i in [0b..] {
  sum += i;
}

10.6 return

The return expression exits the current function or service method.

Rules:

  • return must return a value compatible with the enclosing functions return type.
  • return immediately terminates execution of the current body.
  • return is permitted anywhere inside a function or method body.
  • fn and service allow a falback return value.

Examples:

fn abs(x: int): int // no fallback
{
  return when x >= 0 then x else -x;
}

fn abs(x: int): int // ERROR: compilation error, there is no fallback
{
}

fn abs(x: int): int else 1000 // fallback value, no error
{
  if (x>0) return x;
  
  // missing a return...
}


10.7 Control Flow Restrictions

Rules:

  • There is break and continue.
    • break terminates the innermost enclosing loop.
    • continue continues to the next iteration of the innermost enclosing loop.
  • There is no implicit fallthrough.
  • All control flows are explicit and structured.

10.8 Summary of Expression Rules

  • when, and blocks are expressions.
  • All branching is explicit and typed.
  • if is flow control.
  • Loops operate over bounded ranges.
  • Control flow is predictable and structured.

10. Return Fallback (else)

A function can have an else clause, it is sugar for the sake of a fallback value.

  • else can be explicit for optional but it is not required, since none is always a return value if nothing else is returned.
    • fn f(): optional<int> else none {} == fn f(): optional<int> { return none; }
  • result can have an else clause. however, it should be explicit error or provided by function body (no fallback).
fn v(): Vector else Vector.ZERO
{
  if cond return Vector.ONE;
}

Used to guarantee total functions without boilerplate.


11. Numeric Rules & bounded

This section defines numeric types, numeric conversions, the special bounded type, and the built-in range value type. All rules in this section are normative.

PBS numeric rules are designed to be explicit, predictable, and safe by default.


11.1 Numeric Type Hierarchy

PBS defines the following numeric types:

  • int — 32-bit signed integer
  • long — 64-bit signed integer
  • float — 32-bit IEEE-754 floating point
  • double — 64-bit IEEE-754 floating point
  • bounded — unsigned 16-bit integer (0 .. 65535)

Rules:

  • All numeric types are value types.
  • Numeric types have no identity and no implicit mutability.

11.2 Implicit Widening Conversions

PBS allows implicit widening conversions only.

Allowed implicit conversions:

int      → long
int      → float
int      → double
long     → float
long     → double
float    → double
bounded  → int
bounded  → long

Rules:

  • Implicit widening never loses information.
  • Any conversion not listed above requires an explicit cast.

11.3 Explicit Casts

Explicit casts use the syntax:

expr as Type

Rules:

  • Explicit casts are required for any narrowing or potentially lossy conversion.
  • Explicit casts are checked at compile time and/or runtime as specified below.

11.4 The bounded Type

bounded is a dedicated scalar type for indices, sizes, and counters.

Purpose:

  • Make indexing and counting explicit in the type system.
  • Prevent accidental misuse of general-purpose integers.
  • Enable predictable and safe looping semantics.

Representation:

  • Internally represented as an unsigned 16-bit integer (u16).
  • Valid range: 0 .. 65535.

Literal syntax:

let a: bounded = 10b;
let b = 0b;

11.5 Conversions Involving bounded

Rules:

  • bounded → int and bounded → long are implicit.
  • int → bounded and long → bounded require an explicit cast.
  • float and double cannot be cast to bounded.

Casting to bounded:

let b: bounded = x as bounded;

Semantics:

  • Casting to bounded performs a clamping operation:

    • values < 0 become 0b
    • values > 65535 become 65535b
  • A compile-time or runtime warning must be issued when clamping occurs.


11.6 Arithmetic on bounded

Operations on bounded are intentionally limited.

Allowed operations:

  • comparison (==, !=, <, <=, >, >=)
  • addition (+)
  • subtraction (-)

Disallowed operations:

  • multiplication
  • division
  • modulo
  • bitwise operations

Rules:

  • Addition and subtraction on bounded are checked.

  • If an operation would overflow or underflow:

    • the result is clamped to the valid range, and
    • a warning is emitted.

11.7 The range Type

PBS provides a built-in value type range for representing bounded intervals.

A range represents a half-open interval:

[min .. max) - min is inclusive and max is exclusive.

That is, the sequence:

min, min + 1, ..., max - 1


Construction

A range is constructed using the built-in range constructor.

range(max: bounded)
range(min: bounded, max: bounded)

Examples:

let r1: range = range(10b);          // [0b .. 10b)
let r2: range = range(5b, 15b);      // [5b .. 15b)
let r3: range = range(5b, a.len());  // [5b .. a.len())

Rules:

  • The single-argument form sets min to 0b.
  • The two-argument form sets both bounds explicitly.
  • max must always be provided.
  • If min >= max, the range is empty.
  • range values are immutable.

API

A range exposes the following read-only fields:

  • min: bounded
  • max: bounded

Optional helper operations:

  • count(): bounded
  • isEmpty(): bool

11.8 Numeric Safety Guarantees

PBS guarantees that:

  • No implicit narrowing conversions occur.
  • All potentially lossy conversions are explicit.
  • bounded operations cannot produce undefined behavior.
  • range values have well-defined, deterministic semantics.
  • Numeric behavior is deterministic across platforms.

11.9 Summary of Numeric Rules

  • Implicit conversions are widening only.
  • Narrowing conversions require explicit casts.
  • bounded is used for indices and sizes.
  • bounded arithmetic is limited and checked.
  • range represents half-open bounded intervals.
  • Numeric behavior is explicit and predictable.

12. optional<T>

This section defines the optional<T> type. All rules in this section are normative.

optional<T> represents the explicit presence or absence of a value, without exceptions, traps, or implicit unwrapping.


12.1 Purpose and Design Goals

optional<T> exists to model situations where a value:

  • may or may not be present,
  • is not an error condition by itself,
  • must be handled explicitly by the programmer.

Design goals:

  • No implicit unwrapping
  • No runtime traps
  • No heap allocation
  • Stack-only representation

12.2 Type Definition

optional<T> is a built-in generic value type.

Rules:

  • T must be a valid value type.
  • optional<T> itself is a value type.
  • optional<T> has no identity and no aliasing semantics.

12.3 Construction

An optional<T> value is constructed using one of the following forms:

some(value)
none

Rules:

  • some(value) produces an optional<T> containing value. T follow the same type as value.
  • none produces an empty optional<T>.
  • none represents the absence of a value.
  • none must be typed by context.

Examples:

let a: optional<int> = some(10);
let b: optional<int> = none;

The following is invalid:

let x = none; // ERROR: type of `none` cannot be inferred

12.4 Extraction with else

The only way to extract a value from an optional<T> is using the else operator.

Syntax:

value = opt else fallback

Rules:

  • If opt is some(v), the expression evaluates to v.
  • If opt is none, the expression evaluates to fallback.
  • The type of fallback must be compatible with T.

Example:

let v: int = maybeInt else 0;

No other form of extraction is permitted.


12.5 Control Flow and Functions

Functions returning optional<T> may omit an explicit return none if no value is returned.

Example:

fn find(id: int): optional<int>
{
  if (id == 0) return some(42);
  // implicit return none
}

Rules:

  • Falling off the end of a function returning optional<T> is equivalent to return none.
  • This behavior applies only to optional<T>.

12.6 API Surface

optional<T> exposes a minimal, explicit API:

opt.hasSome(): bool
opt.hasNone(): bool

Rules:

  • These methods do not extract the contained value.
  • They exist only for explicit branching logic.

Example:

if opt.hasSome() then {
  // handle presence
} else {
  // handle absence
}

12.7 Mutability and Copying

Rules:

  • optional<T> follows normal value semantics.
  • Assigning an optional<T> copies the container and its contents.
  • Mutability of T inside optional<T> follows normal binding rules.

12.8 Summary of optional<T> Rules

  • Absence is explicit and normal.
  • No implicit unwrapping or traps exist.
  • Extraction is only possible via else.
  • optional<T> is stack-only and allocation-free.

13. result<T, E>

This section defines the result<T, E> type. All rules in this section are normative.

result<T, E> represents an explicit success-or-error outcome. It is the only mechanism for error signaling in PBS.


13.1 Purpose and Design Goals

result<T, E> exists to model operations that:

  • may succeed and produce a value of type T, or
  • may fail with a typed error E.

Design goals:

  • No exceptions
  • No implicit error propagation
  • Explicit, typed error handling
  • Stack-only representation

13.2 Type Definition

result<T, E> is a built-in generic value type.

Rules:

  • T must be a valid value type.
  • E must be an error label type declared via declare error.
  • result<T, E> itself is a value type.
  • result<T, E> has no identity and no aliasing semantics.

13.3 Construction

A result<T, E> value is constructed using one of the following forms:

ok(value)
err(errorLabel)

Rules:

  • ok(value) produces a successful result containing value.
  • err(label) produces a failed result containing an error label of type E.
  • The type of label must match E exactly.

Examples:

let r1: result<int, IOError> = ok(42);
let r2: result<int, IOError> = err(IOError.not_found);

13.4 The ? Propagation Operator

The ? operator propagates errors only when error types match.

Syntax:

value = expr?

Rules:

  • expr must have type result<T, E>.
  • The enclosing function must return result<_, E> with the same error type.
  • If expr is ok(v), the expression evaluates to v.
  • If expr is err(e), the enclosing function returns err(e) immediately.

Example:

fn g(): result<bool, ErrorA>
{
  return ok(true);
}

fn f(): result<int, ErrorA>
{
  let x: int = when g()? then 1 else 0;
  return ok(x + 1);
}

13.5 Error Handling with handle

handle is the only construct that allows extracting a value from a result while mapping errors.

Syntax:

value = handle expr
{
  ErrorA.case1 => ErrorB.mapped1,
  ErrorA.case2 => ErrorB.mapped2,
  _            => ErrorB.default,
};

Rules:

  • expr must have type result<T, E1>.
  • The enclosing function must return result<_, E2>.
  • Each arm maps an error label of E1 to a label of E2.
  • Arms must be exhaustive, either explicitly or via _.

Semantics:

  • If expr is ok(v), the expression evaluates to v.
  • If expr is err(e), the first matching arm is selected and the function returns err(mapped) immediately.

13.6 Restrictions

Rules:

  • There is no implicit extraction of result<T, E>.
  • There is no pattern matching beyond handle.
  • result<T, E> cannot be implicitly converted to optional<T> or vice versa.
  • Falling off the end of a function returning result<T, E> is a compile-time error.

13.7 Mutability and Copying

Rules:

  • result<T, E> follows normal value semantics.
  • Assigning a result<T, E> copies the container and its contents.
  • Mutability of T follows normal binding rules.

13.8 Summary of result<T, E> Rules

  • result<T, E> is the only error signaling mechanism.
  • Errors are typed and explicit.
  • ? propagates errors only when types match.
  • handle is the only construct for error mapping and extraction.
  • No exceptions or traps exist.

14. Tuples

PBS provides tuples as a lightweight, stack-only aggregation type.

Tuples exist to support small, local groupings of values without introducing heap allocation, gates, aliasing, or reference counting. They intentionally fill the ergonomic gap left by removing arrays from the stack.

Tuples belong entirely to the SAFE world.


14.1 Tuple Type

The tuple type is written as:

Tuple(T1, T2, ..., Tn)

Each element has a fixed position and type. Tuples are heterogeneous and their size is known at compile time.

Example:

let t: Tuple(int, float, bool) = tuple(1, 2.0, true);

14.2 Construction

Tuples are constructed using the tuple expression:

tuple(expr1, expr2, ..., exprN)

Each expression is evaluated and stored inline on the stack.

Example:

let position = tuple(10, 20);
let state = tuple("idle", true, 3);

14.3 Properties

Tuples have the following properties:

  • Tuples are value types
  • Tuples live exclusively on the stack
  • Assignment performs a conceptual copy of all elements
  • Tuples have no identity
  • Tuples have no observable aliasing
  • Tuples never allocate storage

Because of these properties, tuples are always safe to use and cannot leak memory.


14.4 Element Access

Tuple elements are accessed by zero-based positional index using dot syntax:

let t = tuple(1, 2.0, true);

let a: int   = t.0;
let b: float = t.1;
let c: bool  = t.2;

Index access is statically checked and always safe.


14.5 Destructuring

Tuples may be destructured into individual bindings:

let t = tuple(1, 2.0, true);
let (a, b, c) = t;

Types may be explicitly annotated if desired:

let (x: int, y: float, z: bool) = t;

Destructuring copies each element into a new stack binding.


14.6 Mutability

Tuple bindings may be mutable:

let t = mut tuple(1, 2, 3);
t.0 = 10;

Mutation is always local to the binding and never affects other values.

Example:

let a = tuple(1, 2, 3);
let b = a;      // copy

b.0 = 99;
// a.0 is still 1

14.7 Tuples vs Structs

Tuples and structs are closely related but serve different purposes:

Struct Tuple
Named type Anonymous type
Declared once, reused Created inline
Named fields Positional fields
Stack value Stack value

Tuples are intended for local composition, while structs are intended for named data models.


14.8 Tuples vs Arrays

Tuples are not arrays.

Tuple Array
Stack-only Gate-backed (storage)
Heterogeneous Homogeneous
No aliasing Aliasing possible
No allocation Requires alloc
SAFE world HIP world

Tuples are suitable for small, fixed groupings of values. Arrays are suitable for collections and data structures.


14.9 Didactic Intent

Tuples exist to reinforce the core PBS design principle:

If you do not allocate, you are safe.

They allow expressive local grouping without weakening the guarantees of the SAFE world or introducing hidden costs.


14.10 Summary

  • Tuples are stack-only value types
  • Tuples are heterogeneous and fixed-size
  • Tuples never allocate or alias
  • Tuples provide ergonomic local aggregation

Tuples completes the SAFE world without compromising the explicit nature of the PBS memory model.


15. Memory Model

PBS defines a segmented and explicit memory model designed to be:

  • Didactic: each memory region teaches a distinct mental model
  • Predictable: no implicit allocation or tracing garbage collection
  • Game-oriented: execution happens inside loops (frames), with controlled reclamation points
  • Explicit about cost and risk: moving between regions is always visible in code

The memory model is divided into four regions:

  1. Constant Pool
  2. Stack
  3. Gate Pool
  4. Storage (Heap)

Each region has a distinct role, lifetime, and set of guarantees.


15.1 Constant Pool

The Constant Pool stores immutable data known at compile time.

Examples include:

  • Numeric literals
  • String literals
  • Compile-time constant structs (e.g. Vector.ZERO)

Properties

  • Immutable
  • Read-only
  • Shared globally
  • Never reclaimed during execution

Example

let a: int = 10;
let s: string = "Hello World";
let v: Vector = Vector.ZERO;

In the example above:

  • 10, "Hello World", and Vector.ZERO live in the Constant Pool
  • Assignments copy references to constant data, not mutable memory
  • Constant Pool values can never be mutated

The Constant Pool is not a general-purpose memory region and cannot store dynamically created data.


15.2 Stack (SAFE World)

The Stack is the default execution memory for PBS.

It stores:

  • Local variables
  • Function parameters
  • Temporary values
  • Return values

Properties

  • Value-first semantics
  • Conceptual copy on assignment
  • No observable aliasing
  • No pointers or handles
  • Deterministic lifetime (scope-bound)

Example

fn example(): void
{
  let a: Vector = Vector(1, 2);
  let b = a;      // conceptual copy

  b.x = 10;
  // a.x is still 1
}

All values in the Stack are isolated. Mutating one value never affects another binding.

The Stack is the SAFE world of PBS. If a program only uses stack values and the Constant Pool, it cannot create memory leaks or aliasing bugs.


15.3 Gate Pool

The Gate Pool is an internal runtime structure that manages handles (gates) to objects stored in the Storage region.

A gate is a small, copyable value that represents access to a storage object.

Conceptual Structure

Each Gate Pool entry contains:

  • A pointer to a storage object
  • A strong reference count (RC)
  • A weak reference count
  • Optional runtime metadata (type id, flags, debug info)

The Gate Pool is not directly addressable by user code, but all gate-backed types are implemented on top of it.


15.4 Storage (Heap / HIP World)

The Storage region (heap) stores dynamically allocated, mutable data.

Objects in Storage are created explicitly using alloc.

Example

let a = alloc array<int>[4b];

This allocation:

  • Creates a new storage object
  • Creates a Gate Pool entry
  • Returns a strong gate to the caller

Properties

  • Mutable
  • Aliasing is possible and intentional
  • Lifetime managed via reference counting
  • Not reclaimed immediately when references disappear

The Storage region is the HIP world of PBS. Power and responsibility are explicit.


15.5 Strong and Weak Gates

All gate-backed types use gates to access storage objects.

Strong Gates

  • Represented directly by the type T
  • Increment strong reference count
  • Keep the storage object alive
let a: Node = alloc Node;
let b = a; // RC++

Weak Gates

  • Represented by weak<T>
  • Do not increment strong RC
  • Do not keep storage alive
let w: weak<Node> = a as weak;

Conversion Rules

let w: weak<Node> = strongNode as weak;     // always valid
let o: optional<Node> = w as strong;        // may fail

Weak gates must be promoted to strong gates before use.


15.6 Reference Counting (RC)

PBS uses reference counting to manage storage lifetimes.

RC Tracks

  • Strong gates stored on the stack
  • Strong gates stored inside other storage objects

RC is adjusted automatically when:

  • A strong gate is copied or destroyed
  • A strong gate is assigned into or removed from storage

Important Notes

  • Cycles of strong references are not reclaimed automatically
  • Weak gates do not participate in RC

This behavior is intentional and mirrors real-world systems such as C++ shared_ptr / weak_ptr.


15.7 Reclamation and Frame Boundaries

PBS does not use tracing garbage collection.

Instead, storage reclamation happens at safe runtime points, typically:

  • End of a frame
  • Explicit runtime GC step (implementation-defined)

At reclamation time:

  • Storage objects with strong RC == 0 are destroyed
  • Associated Gate Pool entries are cleaned up when no weak references remain

This model is predictable and suitable for real-time systems.


15.8 Crossing Memory Regions

Transitions between memory regions are always explicit:

Operation Effect
alloc Stack → Storage
peek Storage → Stack (copy)
borrow Temporary read-only access
mutate Temporary mutable access
take Single mutation sugar

Example

let a = alloc array<int>[2b];

mutate a as aa
{
  aa[0b] = 10;
}

let v = peek a[0b]; // copy back to stack

15.9 Didactic Intent

The memory model is intentionally structured to teach:

  • Stack vs heap
  • Value vs identity
  • Ownership and aliasing
  • Cost of allocation
  • Why cycles leak under RC

Programs that remain in the Stack and Constant Pool are safe by construction. Programs that enter Storage gain power, flexibility, and responsibility.

PBS does not hide these trade-offs.


15.10 Summary

  • Constant Pool: immutable, global
  • Stack: safe, value-first
  • Gate Pool: handle management and RC
  • Storage: mutable, shared, explicit

PBS makes memory visible, intentional, and teachable.


16. Gates (Strong & Weak)

PBS introduces gates as the fundamental mechanism for accessing dynamically allocated storage.

Gates make identity, aliasing, and lifetime explicit, while keeping the SAFE world free of references and pointers.

This chapter defines what gates are, how strong and weak gates behave, and how they interact with the memory model.


16.1 What Is a Gate

A gate is a small, copyable value that represents access to an object stored in Storage (heap).

Gates are not pointers in the traditional sense, but they serve a similar role:

  • They refer to a storage object
  • They can be copied cheaply
  • They may alias the same underlying data

All gate-backed types are created explicitly using alloc.

let a = alloc array<int>[4b];

The value returned by alloc is a gate.


16.2 Gate-Backed Types

The following kinds of types are gate-backed:

  • Built-in collections (array, list, map, text)
  • User-defined storage types (declare storage struct)

Gate-backed types live in Storage and are accessed exclusively through gates.


16.3 Strong Gates

A strong gate is the default form of a gate-backed value.

Properties:

  • Represented directly by the type T
  • Increment the strong reference count (RC)
  • Keep the storage object alive
  • Assignment creates an alias

Example:

let a: Node = alloc Node;
let b = a;        // alias, RC++

Both a and b refer to the same storage object.


16.4 Weak Gates

A weak gate is a non-owning reference to a storage object.

Properties:

  • Represented by the type weak<T>
  • Do not increment strong RC
  • Do not keep the storage object alive
  • May become invalid when the object is reclaimed

Weak gates are used to break reference cycles and express non-owning relationships.

Example:

let s: Node = alloc Node;
let w: weak<Node> = s as weak;

16.5 Conversions Between Strong and Weak

PBS provides explicit conversions between strong and weak gates.

Strong to Weak

let w: weak<Node> = s as weak;
  • Always succeeds
  • Does not affect strong RC

Weak to Strong

let o: optional<Node> = w as strong;
  • May fail
  • Returns none if the object has already been reclaimed
  • Returns some(T) if promotion succeeds

These conversions make ownership and lifetime explicit.


16.6 Reference Counting and Lifetime

Each storage object has:

  • A strong reference count (RC)
  • A weak reference count

Rules:

  • Strong gates increment and decrement RC automatically
  • Weak gates do not affect strong RC
  • When strong RC reaches zero, the storage object becomes eligible for reclamation

Reclamation occurs at safe runtime points (e.g. end of frame).

Weak gates may outlive the storage object they reference.


16.7 Gate Validity

A strong gate is always valid.

A weak gate may be:

  • Valid: the storage object still exists
  • Expired: the storage object has been reclaimed

Expired weak gates cannot be used directly and must be promoted first.

let maybeNode: optional<Node> = w as strong;

16.8 Interaction with Access Control

Only strong gates may be used with:

  • peek
  • borrow
  • mutate
  • take

Weak gates must be promoted before any access is allowed.

This rule prevents accidental use of expired references.


16.9 Didactic Intent

Gates are designed to teach:

  • The difference between value and identity
  • Ownership versus observation
  • Why aliasing exists and how it propagates
  • Why reference cycles leak under RC
  • How weak references break cycles safely

PBS does not hide these concepts.

If a program does not allocate, gates do not exist. If it allocates, gates make responsibility explicit.


16.10 Summary

  • Gates are explicit handles to storage objects
  • Strong gates own and keep objects alive
  • Weak gates observe without ownership
  • Conversions between strong and weak are explicit
  • Access to storage always requires a strong gate

Gates are the foundation of the HIP world in PBS.


17. Gate-backed Array

PBS provides gate-backed arrays as fixed-size, homogeneous collections stored in Storage (heap).

Gate-backed arrays are the primary indexed collection type in the HIP world. They deliberately trade safety for power and explicit control.


17.1 Array Type

The array type is written as:

array<T>[N]

Where:

  • T is the element type
  • N is a compile-time constant size (bounded)

Arrays are homogeneous and fixed-size.


17.2 Allocation

Arrays are created explicitly using alloc:

let a: array<int>[4b] = alloc array<int>[4b];

Allocation:

  • Creates a storage object
  • Registers it in the Gate Pool
  • Returns a strong gate

Arrays cannot exist on the stack.


17.3 Properties

Gate-backed arrays have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Elements are laid out contiguously
  • Size is fixed for the lifetime of the array

Arrays are not value types.


17.4 Aliasing Semantics

Assigning an array copies the gate, not the data:

let a = alloc array<int>[2b];
let b = a;   // alias

Mutations through one alias are visible through all aliases.


17.5 Element Access

Direct element access using a[i] is not allowed.

All element access is explicit and controlled.

Reading Elements (peek)

let v: int = peek a[1b];
  • Returns a value copy
  • SAFE operation
  • Never exposes references

Writing Elements (mutate)

mutate a as aa
{
  aa[1b] = 42;
}
  • Requires mutable access
  • Mutation is shared across aliases

17.6 Iteration

Arrays may be iterated by value:

for v in a
{
  // v is a value copy
}

Iteration copies each element from storage into the stack.

To iterate indices explicitly:

for i in a.indices()
{
  let v = peek a[i];
}

17.7 Passing Arrays to Functions

Arrays are passed by gate (handle), not by value:

fn setFirst(a: array<int>[2b], v: int): void
{
  mutate a as aa
  {
    aa[0b] = v;
  }
}

Calling this function mutates the caller's array.


17.8 Copying Arrays

To duplicate an array's contents, use copy:

let b = copy(a);

copy:

  • Allocates a new array
  • Copies all elements
  • Returns a new strong gate

17.9 Bounds and Safety

Array indices are statically bounded by N.

  • Out-of-bounds access is a runtime error
  • Index values are of type bounded

These checks preserve safety without hiding cost.


17.10 Didactic Intent

Gate-backed arrays exist to teach:

  • Heap allocation
  • Aliasing through handles
  • Explicit mutation
  • Copy versus alias
  • Cost of indexed collections

Arrays are powerful but intentionally not implicit or convenient.


17.11 Summary

  • Arrays are gate-backed, fixed-size collections
  • Arrays live in Storage and require alloc
  • Assignment aliases; copy duplicates
  • Element access is explicit (peek, mutate)
  • Arrays belong to the HIP world

Arrays are the foundation for structured data in PBS.


18. Gate-backed List

PBS provides gate-backed lists as dynamic, growable collections stored in Storage (heap).

Lists complement gate-backed arrays by supporting variable size and incremental construction, at the cost of additional runtime overhead and indirection.


18.1 List Type

The list type is written as:

list<T>

Where T is the element type.

Lists are homogeneous and dynamically sized.


18.2 Allocation

Lists are created explicitly using alloc:

let xs: list<int> = alloc list<int>;

Allocation:

  • Creates an empty list in Storage
  • Registers it in the Gate Pool
  • Returns a strong gate

Lists cannot exist on the stack.


18.3 Properties

Gate-backed lists have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Dynamic length
  • Elements may be reallocated internally

Lists are not value types.


18.4 Aliasing Semantics

Assigning a list copies the gate, not the data:

let a = alloc list<int>;
let b = a;   // alias

Mutations through one alias are visible through all aliases.


18.5 Core Operations

Length

let n: bounded = borrow xs as xsr { xsr.len() };

Appending Elements

Elements are appended using mutation:

mutate xs as xsr
{
  xsr.push(10);
  xsr.push(20);
}

For single operations, take may be used as syntactic sugar:

take xs.push(30);

18.6 Element Access

Direct element access using xs[i] is not allowed.

Reading Elements (peek)

let v: int = peek xs[i];
  • Returns a value copy
  • SAFE operation

Writing Elements (mutate)

mutate xs as xsr
{
  xsr[i] = 42;
}

18.7 Iteration

Lists may be iterated by value:

for v in xs
{
  // v is a value copy
}

To iterate indices explicitly:

for i in xs.indices()
{
  let v = peek xs[i];
}

18.8 Passing Lists to Functions

Lists are passed by gate (handle), not by value:

fn append(xs: list<int>, v: int): void
{
  take xs.push(v);
}

Calling this function mutates the caller's list.


18.9 Copying Lists

To duplicate a list and its contents, use copy:

let ys = copy(xs);

copy:

  • Allocates a new list
  • Copies all elements
  • Returns a new strong gate

18.10 Performance Notes

  • Appending may trigger internal reallocation
  • Element access is O(1)
  • Iteration copies elements to the stack

Lists trade predictability for flexibility.


18.11 Didactic Intent

Gate-backed lists exist to teach:

  • Dynamic allocation
  • Growth and reallocation cost
  • Aliasing through handles
  • Explicit mutation patterns

Lists are powerful but require discipline.


18.12 Summary

  • Lists are gate-backed, dynamic collections
  • Lists live in Storage and require alloc
  • Assignment aliases; copy duplicates
  • Mutation is explicit (mutate, take)
  • Lists belong to the HIP world

Lists provide flexibility while preserving explicit control.


19. Gate-backed Set

PBS provides gate-backed sets as unordered collections of unique values stored in Storage (heap).

Sets are designed for membership tests, uniqueness constraints, and dynamic collections where ordering is irrelevant.

Like all gate-backed collections, sets belong to the HIP world and make allocation, aliasing, and mutation explicit.


19.1 Set Type

The set type is written as:

set<T>

Where T is the element type.

Elements of a set must be hashable and comparable.


19.2 Allocation

Sets are created explicitly using alloc:

let s: set<int> = alloc set<int>;

Allocation:

  • Creates an empty set in Storage
  • Registers it in the Gate Pool
  • Returns a strong gate

Sets cannot exist on the stack.


19.3 Properties

Gate-backed sets have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Dynamic size
  • No duplicate elements

Sets are not value types.


19.4 Aliasing Semantics

Assigning a set copies the gate, not the data:

let a = alloc set<int>;
let b = a;   // alias

Mutations through one alias are visible through all aliases.


19.5 Core Operations

Insertion

Elements are inserted explicitly:

mutate s as ss
{
  ss.add(10);
  ss.add(20);
}

For single operations, take may be used:

take s.add(30);

Adding an element already present has no effect.


Membership Test

Membership checks do not mutate the set:

let exists: bool = borrow s as ss
{
  ss.contains(10);
};

Removal

Elements are removed explicitly:

mutate s as ss
{
  ss.remove(20);
}

19.6 Iteration

Sets may be iterated by value:

for v in s
{
  // v is a value copy
}

Iteration order is implementation-defined.


19.7 Passing Sets to Functions

Sets are passed by gate (handle), not by value:

fn addIfMissing(s: set<int>, v: int): void
{
  let exists = borrow s as ss { ss.contains(v) };
  if (!exists)
  {
    take s.add(v);
  }
}

Calling this function mutates the caller's set.


19.8 Copying Sets

To duplicate a set and its contents, use copy:

let s2 = copy(s);

copy:

  • Allocates a new set
  • Copies all elements
  • Returns a new strong gate

19.9 Performance Notes

  • Membership tests are expected to be O(1) average
  • Insertions and removals may rehash internally
  • Iteration copies elements into the stack

Sets trade ordering guarantees for fast membership checks.


19.10 Didactic Intent

Gate-backed sets exist to teach:

  • Unordered collections
  • Uniqueness constraints
  • Membership-oriented logic
  • Explicit mutation and aliasing

Sets are ideal for flags, visited markers, and dynamic uniqueness tracking.


19.11 Summary

  • Sets are gate-backed, unordered collections
  • Sets live in Storage and require alloc
  • Assignment aliases; copy duplicates
  • Mutation is explicit (mutate, take)
  • Iteration order is undefined

Sets complete the family of associative gate-backed collections.


20. Gate-backed Text

PBS provides gate-backed text as the primary dynamic string type.

text exists to support runtime string construction, mutation, and concatenation without weakening the guarantees of the SAFE world. String literals and compile-time strings remain immutable and live in the Constant Pool.


20.1 Text Type

The text type is written as:

text

text is a gate-backed, dynamically sized sequence of characters stored in Storage (heap).


20.2 Text vs String

PBS distinguishes clearly between string and text:

string text
Immutable Mutable
Constant Pool Storage (heap)
Compile-time Runtime
SAFE world HIP world

string values can never be mutated or concatenated. All dynamic string operations require text.


20.3 Allocation

Text objects are created explicitly using alloc:

let t: text = alloc text;

Allocation:

  • Creates an empty text object in Storage
  • Registers it in the Gate Pool
  • Returns a strong gate

20.4 Properties

Gate-backed text has the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Dynamic length
  • Internally growable

text is not a value type.


20.5 Appending and Mutation

Text is mutated explicitly using mutate or take.

Appending String Literals

let t = alloc text;

take t.append("Hello");
take t.append(", world");

Appending Other Text

let a = alloc text;
let b = alloc text;

take a.append("Hello");
take b.append("World");

take a.appendText(b);

All mutations affect all aliases.


20.6 Reading Text

Text contents are read by copying into the SAFE world.

Convert to String

let s: string = borrow t as tt { tt.toString() };

The returned string is immutable snapshot copied from text into a SAFE string value runtime-owned.


20.7 Aliasing Semantics

Assigning a text value copies the gate:

let a = alloc text;
let b = a; // alias

take a.append("!");

Both a and b observe the same content.


20.8 Passing Text to Functions

Text values are passed by gate (handle):

fn appendExclamation(t: text): void
{
  take t.append("!");
}

Calling this function mutates the caller's text.


20.9 Copying Text

To duplicate a text value, use copy:

let t2 = copy(t);

copy:

  • Allocates a new text object
  • Copies all characters
  • Returns a new strong gate

20.10 Performance Notes

  • Appending may trigger internal reallocation
  • Converting to string copies data
  • Text mutation is O(n) in the worst case

Text trades immutability for flexibility.


20.11 Didactic Intent

Gate-backed text exists to teach:

  • The cost of string mutation
  • The difference between immutable and mutable strings
  • Explicit heap allocation for dynamic text
  • Aliasing through handles

PBS does not allow silent string mutation.


20.12 Summary

  • text is the dynamic string type in PBS
  • string remains immutable and SAFE
  • Text lives in Storage and requires alloc
  • Mutation is explicit (mutate, take)
  • Assignment aliases; copy duplicates

Text completes the set of fundamental gate-backed collections.


21. Gate-backed Map

PBS provides gate-backed maps as associative collections stored in Storage (heap).

Maps associate keys to values and are designed for lookup-oriented data structures where indexed access is not sufficient.

Like all gate-backed collections, maps belong to the HIP world and make aliasing, mutation, and cost explicit.


21.1 Map Type

The map type is written as:

map<K, V>

Where:

  • K is the key type
  • V is the value type

Both K and V must be value-safe types or gate-backed types.


21.2 Allocation

Maps are created explicitly using alloc:

let m: map<string, int> = alloc map<string, int>;

Allocation:

  • Creates an empty map in Storage
  • Registers it in the Gate Pool
  • Returns a strong gate

Maps cannot exist on the stack.


21.3 Properties

Gate-backed maps have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Dynamic size
  • Key-based lookup

Maps are not value types.


21.4 Aliasing Semantics

Assigning a map copies the gate, not the data:

let a = alloc map<string, int>;
let b = a;   // alias

Mutations through one alias are visible through all aliases.


21.5 Core Operations

Insertion and Update

Entries are inserted or updated via mutation:

mutate m as mm
{
  mm.put("health", 100);
  mm.put("mana", 50);
}

For single operations, take may be used:

take m.put("score", 1000);

Lookup

Lookup operations do not mutate the map and return copied values:

let hp: optional<int> = borrow m as mm
{
  mm.get("health");
};
  • Returns some(value) if the key exists
  • Returns none otherwise

Removal

Entries are removed explicitly:

mutate m as mm
{
  mm.remove("mana");
}

21.6 Iteration

Maps may be iterated by key-value pairs copied into the SAFE world:

for pair in m
{
  let (k, v) = pair; // Tuple(K, V)
}

Iteration order is implementation-defined.

To iterate keys or values explicitly:

for k in m.keys() { }
for v in m.values() { }

21.7 Passing Maps to Functions

Maps are passed by gate (handle):

fn incrementScore(m: map<string, int>): void
{
  take m.put("score", (peek m.get("score") else 0) + 1);
}

Calling this function mutates the caller's map.


21.8 Copying Maps

To duplicate a map and its contents, use copy:

let m2 = copy(m);

copy:

  • Allocates a new map
  • Copies all entries
  • Returns a new strong gate

21.9 Performance Notes

  • Lookup is expected to be O(1) average
  • Insertion and removal may rehash internally
  • Iteration copies key-value pairs into the stack

Maps trade predictability for flexibility.


21.10 Didactic Intent

Gate-backed maps exist to teach:

  • Associative data structures
  • Explicit mutation and lookup
  • Cost of dynamic containers
  • Aliasing through handles

Maps are powerful but must be used deliberately.


21.11 Summary

  • Maps are gate-backed, associative collections
  • Maps live in Storage and require alloc
  • Assignment aliases; copy duplicates
  • Mutation is explicit (mutate, take)
  • Lookup returns optional values

Maps complete the core set of gate-backed collection types in PBS.


22. Gate-backed Deque

PBS provides gate-backed deques (double-ended queues) as flexible, dynamic collections stored in Storage (heap).

A deque<T> supports efficient insertion and removal at both ends and serves as the foundational structure for queues and stacks in PBS.

Like all gate-backed collections, deques belong to the HIP world and make allocation, aliasing, and mutation explicit.


22.1 Deque Type

The deque type is written as:

deque<T>

Where T is the element type.

Deques are homogeneous and dynamically sized.


22.2 Allocation

Deques are created explicitly using alloc:

let d: deque<int> = alloc deque<int>;

Allocation:

  • Creates an empty deque in Storage
  • Registers it in the Gate Pool
  • Returns a strong gate

Deques cannot exist on the stack.


22.3 Properties

Gate-backed deques have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Dynamic size
  • Efficient operations at both ends

Deques are not value types.


22.4 Aliasing Semantics

Assigning a deque copies the gate, not the data:

let a = alloc deque<int>;
let b = a;   // alias

Mutations through one alias are visible through all aliases.


22.5 Core Operations

Length

let n: bounded = borrow d as dd { dd.len() };

Push Operations

take d.pushFront(10);
take d.pushBack(20);

Pop Operations

let a: optional<int> = borrow d as dd { dd.popFront() };
let b: optional<int> = borrow d as dd { dd.popBack() };

Pop operations return optional<T> and do not mutate when the deque is empty.


22.6 Element Access

Direct indexed access is supported for convenience:

let v: int = peek d[i];

Mutation by index requires mutate:

mutate d as dd
{
  dd[i] = 42;
}

22.7 Iteration

Deques may be iterated by value:

for v in d
{
  // v is a value copy
}

Iteration order is front to back.


22.8 Passing Deques to Functions

Deques are passed by gate (handle), not by value:

fn pushPair(d: deque<int>, a: int, b: int): void
{
  take d.pushBack(a);
  take d.pushBack(b);
}

Calling this function mutates the caller's deque.


22.9 Copying Deques

To duplicate a deque and its contents, use copy:

let d2 = copy(d);

copy:

  • Allocates a new deque
  • Copies all elements
  • Returns a new strong gate

22.10 Queue and Stack Views

In PBS, queues and stacks are not separate data structures. Instead, they are usage conventions built on top of deque<T>.

Queue (FIFO)

A queue uses the following operations:

  • Enqueue: pushBack
  • Dequeue: popFront

Example:

let q: deque<Event> = alloc deque<Event>;

take q.pushBack(evt1);
take q.pushBack(evt2);

let e1 = borrow q as qq { qq.popFront() };

Stack (LIFO)

A stack uses the following operations:

  • Push: pushBack
  • Pop: popBack

Example:

let s: deque<int> = alloc deque<int>;

take s.pushBack(1);
take s.pushBack(2);

let top = borrow s as ss { ss.popBack() };

This approach reduces the number of built-in types while preserving expressiveness.


22.11 Performance Notes

  • Push and pop operations are expected to be O(1) amortized
  • Indexed access may be O(1) or O(n), implementation-defined
  • Iteration copies elements into the stack

22.12 Didactic Intent

Gate-backed deques exist to teach:

  • Flexible data structures
  • FIFO vs LIFO semantics
  • Explicit mutation and aliasing
  • How abstractions emerge from disciplined usage

Queues and stacks are concepts, not primitives.


22.13 Summary

  • deque<T> is a gate-backed, double-ended queue
  • Deques live in Storage and require alloc
  • Assignment aliases; copy duplicates
  • Queue and stack are usage patterns

Deques complete the set of core dynamic collections in PBS.


23. Gate-backed Blob

PBS provides gate-backed blobs as raw, byte-addressable buffers stored in Storage (heap).

A blob is the fundamental building block for:

  • Save data and serialization
  • Asset loading (sprites, maps, audio chunks)
  • Networking payloads
  • Bridging to host APIs

Blobs are intentionally low-level, but they remain explicit and teachable in the HIP world.


23.1 Blob Type

The blob type is written as:

blob

A blob is a dynamic buffer of bytes.


23.2 Allocation

Blobs are created explicitly using alloc with a size:

let b: blob = alloc blob(256b);

Allocation:

  • Creates a storage object containing size bytes
  • Registers it in the Gate Pool
  • Returns a strong gate

23.3 Properties

Gate-backed blobs have the following properties:

  • Stored in Storage (heap)
  • Accessed via gates
  • Assignment copies the gate (aliasing)
  • Byte-addressable
  • Size is fixed after allocation (v0)

Blobs are not value types.


23.4 Aliasing Semantics

Assigning a blob copies the gate, not the data:

let a = alloc blob(16b);
let c = a; // alias

take a.writeU8(0b, 255);
let x = borrow c as cc { cc.readU8(0b) }; // x == 255

Mutations through one alias are visible through all aliases.


23.5 Core Operations

The following operations are required for blob.

Size

let n: bounded = borrow b as bb { bb.size() };

Read/Write Bytes

take b.writeU8(0b, 42);
let v: bounded = borrow b as bb { bb.readU8(0b) };

Indices are bounded and refer to byte offsets.


23.6 Typed Reads and Writes

For convenience and portability, blobs may support typed operations.

The endianness of typed operations is explicit.

Examples (LE shown):

take b.writeU16LE(0b, 0xBEEF);
let x: bounded = borrow b as bb { bb.readU16LE(0b) };

take b.writeI32LE(2b, -10);
let y: int = borrow b as bb { bb.readI32LE(2b) };

Implementations may provide a minimal subset initially (e.g. only U8/U16/U32).


23.7 Slicing and Views (Deliberately Deferred)

blob does not expose raw pointers or escaping views in v0.

If slicing is introduced later, it must preserve PBS rules:

  • No escaping references
  • Explicit cost
  • Explicit lifetime

23.8 Bounds and Safety

  • Out-of-bounds reads and writes are runtime errors
  • Typed reads/writes must also bounds-check their byte width

These checks keep the model teachable without hiding risk.


23.9 Passing Blobs to Functions

Blobs are passed by gate (handle), not by value:

fn fill(b: blob, value: bounded): void
{
  let n = borrow b as bb { bb.size() };
  let i = 0b;
  while (i < n)
  {
    take b.writeU8(i, value);
    i = i + 1b;
  }
}

Calling this function mutates the caller's blob.


23.10 Copying Blobs

To duplicate a blob and its contents, use copy:

let b2 = copy(b);

copy:

  • Allocates a new blob with the same size
  • Copies all bytes
  • Returns a new strong gate

23.11 Didactic Intent

Gate-backed blobs exist to teach:

  • Raw memory as bytes
  • Bounds checking
  • Serialization patterns
  • Explicit mutation and aliasing

Blobs provide low-level power without introducing implicit pointers.


23.12 Summary

  • blob is a gate-backed byte buffer
  • Blobs live in Storage and require alloc blob(size)
  • Assignment aliases; copy duplicates
  • Mutation is explicit (mutate, take)
  • Bounds are checked at runtime

Blobs are the foundation for binary data handling in PBS.


24. Custom Storage API

PBS allows users to define custom gate-backed storage types using the declare storage struct construct.

A storage struct follows the same declaration model as a regular struct, but lives in Storage (heap) and obeys HIP-world semantics. Value-only features are intentionally not available.

This chapter defines the rules, capabilities, and limitations of custom storage types.


24.1 Storage Struct Declaration

A storage struct is declared using:

declare storage struct S(x: int, b: bool)
{
    fn f(self: mut this): void
    {
        // ...
    }
}

The declaration consists of:

  • A name (S)
  • A field list declared in the header
  • A method body block

Fields declared in the header are the only fields of the storage struct.


24.2 Allocation

Storage structs are created explicitly using alloc:

let s: S = alloc S;

Allocation:

  • Creates a storage object
  • Registers it in the Gate Pool
  • Returns a strong gate

Storage structs cannot exist on the stack and cannot be created by value construction.


24.3 Properties

Storage structs have the following properties:

  • Stored in Storage (heap)

  • Accessed via gates

  • Assignment copies the gate (aliasing)

  • Lifetime managed via reference counting

  • Fields may contain:

    • value types
    • gate-backed types
    • weak<T> and optional<T>

Storage structs are not value types.


24.4 Field Access Rules

Fields of a storage struct may only be accessed inside borrow or mutate blocks.

Reading Fields

let hp: int = borrow s as ss
{
    ss.x;
};

Writing Fields

mutate s as ss
{
    ss.b = true;
}

Direct field access outside these blocks is not allowed.


24.5 Methods on Storage Structs

Storage structs may define methods using the same syntax as regular structs.

fn f(self: mut this): void
{
    self.x = self.x + 1;
}

Rules:

  • self: this provides read-only access
  • self: mut this provides mutable access
  • Mutating methods require a mutable context to be called

24.6 Mutation Sugar (take)

For single mutating method calls, take may be used:

take s.f();

Equivalent to:

mutate s as ss
{
    ss.f();
}

24.7 Strong and Weak Fields

Fields of a storage struct may be declared as strong or weak gates.

declare storage struct Node(value: int, parent: optional<weak<Node>>)
{
    fn attach(self: mut this, p: Node): void
    {
        self.parent = some(p as weak);
    }
}

Rules:

  • Strong fields increment reference counts
  • Weak fields do not keep objects alive
  • Weak fields must be promoted before use

This enables cyclic graphs without leaks.


24.8 Copying Storage Structs

To duplicate a storage struct and its contents, use copy:

let s2 = copy(s);

copy:

  • Allocates a new storage object
  • Performs a deep copy of all fields
  • Returns a new strong gate

24.9 Differences from struct

While storage struct shares syntax with struct, several features are intentionally missing.

Not Available in storage struct

  • Constructors ([])
  • Static blocks ([[]])
  • Value initialization syntax (S(1, true))
  • Stack allocation

Initialization of storage structs must be performed explicitly after allocation.

let s = alloc S;
mutate s as ss
{
    ss.x = 10;
    ss.b = false;
}

These restrictions preserve the explicit nature of heap allocation.


24.10 Interaction with Built-in Collections

Storage structs may freely contain and manipulate gate-backed collections.

declare storage struct Inventory(items: list<Item>)
{
    fn add(self: mut this, it: Item): void
    {
        take self.items.push(it);
    }
}

All collection semantics (aliasing, mutation, copying) apply recursively.


24.11 Didactic Intent

The Custom Storage API exists to teach:

  • Heap allocation without implicit safety nets
  • Ownership and aliasing in user-defined types
  • Explicit initialization and mutation
  • How complex data structures are built from primitives

Storage structs intentionally expose power without hiding cost.


24.12 Summary

  • declare storage struct defines custom gate-backed types
  • Storage structs live in Storage and require alloc
  • Fields are declared in the header
  • No constructors, static blocks, or value initialization
  • Access is controlled via borrow, mutate, and take
  • Strong and weak fields control lifetime

Custom storage structs complete the PBS HIP world.


25. Box and Unbox (Built-in Heap Values)

PBS is a value-first language: primitive types and struct values live on the stack and follow SAFE-world semantics.

In some situations, however, a value must acquire identity:

  • It must be shared between multiple owners
  • It must be mutated through aliases
  • It must outlive a stack frame

For these cases, PBS provides the built-in Box abstraction.

Box<T> is a gate-backed built-in type, and box / unbox are built-in operations.


25.1 Box Type

Box<T>

A Box<T>:

  • Stores exactly one value of type T in Storage (heap)
  • Is gate-backed (strong gate)
  • Participates in aliasing and reference counting

Box<T> is not a value type.


25.2 Boxing a Value (box)

The box builtin allocates a Box<T> in Storage and initializes it with a copy of a SAFE value.

let v: Vector = Vector.ZERO;   // SAFE (stack)
let b: Box<Vector> = box(v);   // HIP (heap)

Rules:

  • box(x) performs a copy of x
  • The original value remains unchanged on the stack
  • The result is a strong gate

25.3 Unboxing a Value (unbox)

The unbox builtin copies the value stored in a box back into the SAFE world.

let vv: Vector = unbox(b);

Rules:

  • unbox(b) always returns a copy
  • The returned value has no identity
  • Mutating the returned value does not affect the box

25.4 Generic Form

The box and unbox builtins are fully generic:

let b: Box<T> = box(t);
let t2: T = unbox(b);

This works for any SAFE type T.


25.5 Aliasing and Mutation

Boxes follow standard gate semantics.

let a = box(10);
let b = a;        // alias

take a.set(20);
let x = unbox(b); // x == 20

Assignment copies the gate, not the boxed value.


25.6 Mutating a Box

A boxed value may be mutated via methods on Box<T>.

take b.set(newValue);

Reading without mutation uses unbox.

Direct access to internal storage is not exposed.


25.7 Relationship to SAFE and HIP Worlds

  • SAFE world: values without identity (stack)
  • HIP world: values with identity (heap)

box is the explicit transition from SAFE to HIP. unbox is the explicit transition from HIP to SAFE.


25.8 Informative: Possible Lowering Using storage struct

This section is informative, not normative.

A Box<T> can be implemented internally using a storage struct:

declare storage struct _BoxImpl<T>(value: T)
{
    fn get(self: this): T { self.value }
    fn set(self: mut this, v: T): void { self.value = v }
}

With lowering rules:

  • box(x)alloc _BoxImpl<T> + store copy of x
  • unbox(b)borrow b as bb { bb.get() }

This implementation detail is hidden from the user.


25.9 Didactic Intent

The Box abstraction exists to teach:

  • How values acquire identity
  • Why heap allocation must be explicit
  • The boundary between SAFE and HIP worlds
  • Controlled sharing and mutation of values

Boxing is explicit by design.


25.10 Summary

  • Box<T> is a built-in gate-backed type
  • box and unbox are built-in operations
  • Boxing copies a SAFE value into the heap
  • Unboxing copies the value back to the stack
  • Box<T> participates fully in aliasing and RC

Box and unbox complete the bridge between value semantics and identity semantics in PBS.