prometeu-runtime/docs/specs/pbs/Prometeu Scripting - Prometeu Bytecode Script (PBS).md
bQUARKz f9120e740b
dev/pbs (#8)
Co-authored-by: Nilton Constantino <nilton.constantino@visma.com>
Reviewed-on: #8
2026-03-24 13:40:22 +00:00

4901 lines
98 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```pbs
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 `fn``mod` 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):
```pbs
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):
```pbs
// 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):
```pbs
// 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):
```pbs
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):
```pbs
// math.pbs
fn clamp(x: int, min: int, max: int): int { ... }
```
Example (INVALID):
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
pub service Audio
{
fn play(sound: Sound): void { ... }
}
```
---
### 5.3 Services and Contracts
A service may optionally implement a contract.
Syntax:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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):
```pbs
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:
```pbs
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:
```pbs
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):
```pbs
declare struct Vector(x: float, y: float)
let v = Vector(1, 2);
let x = v.x; // ERROR: field `x` is private
```
Example (VALID):
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
let v0 = Vector(1, 3);
```
That is the zero alias, it will call the default constructor with x = 0 and y = 0
```pbs
let v1 = Vector.zero();
```
That is the square alias, it will call the default constructor with x = 2.0 and y = 2.0
```pbs
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
```pbs
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)
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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<T> → 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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
for i in [0b..10b] {
sum += i;
}
```
```pbs
for i in [..10b] {
sum += i;
}
```
```pbs
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:
```pbs
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).
```pbs
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:
```pbs
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:
```pbs
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`:
```pbs
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.
```pbs
range(max: bounded)
range(min: bounded, max: bounded)
```
Examples:
```pbs
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:
```pbs
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:
```pbs
let a: optional<int> = some(10);
let b: optional<int> = none;
```
The following is invalid:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
opt.hasSome(): bool
opt.hasNone(): bool
```
Rules:
* These methods do not extract the contained value.
* They exist only for explicit branching logic.
Example:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
Tuple(T1, T2, ..., Tn)
```
Each element has a fixed position and type. Tuples are **heterogeneous** and their size is known at compile time.
Example:
```pbs
let t: Tuple(int, float, bool) = tuple(1, 2.0, true);
```
---
### 14.2 Construction
Tuples are constructed using the `tuple` expression:
```pbs
tuple(expr1, expr2, ..., exprN)
```
Each expression is evaluated and stored **inline on the stack**.
Example:
```pbs
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:
```pbs
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:
```pbs
let t = tuple(1, 2.0, true);
let (a, b, c) = t;
```
Types may be explicitly annotated if desired:
```pbs
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:
```pbs
let t = mut tuple(1, 2, 3);
t.0 = 10;
```
Mutation is always **local to the binding** and never affects other values.
Example:
```pbs
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
```pbs
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
```pbs
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
```pbs
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
```pbs
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
```pbs
let w: weak<Node> = a as weak;
```
### Conversion Rules
```pbs
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
```pbs
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`.
```pbs
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:
```pbs
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:
```pbs
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
```pbs
let w: weak<Node> = s as weak;
```
* Always succeeds
* Does not affect strong RC
### Weak to Strong
```pbs
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.
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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`)
```pbs
let v: int = peek a[1b];
```
* Returns a value copy
* SAFE operation
* Never exposes references
### Writing Elements (`mutate`)
```pbs
mutate a as aa
{
aa[1b] = 42;
}
```
* Requires mutable access
* Mutation is shared across aliases
---
## 17.6 Iteration
Arrays may be iterated by value:
```pbs
for v in a
{
// v is a value copy
}
```
Iteration copies each element from storage into the stack.
To iterate indices explicitly:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
list<T>
```
Where `T` is the element type.
Lists are **homogeneous** and **dynamically sized**.
---
### 18.2 Allocation
Lists are created explicitly using `alloc`:
```pbs
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:
```pbs
let a = alloc list<int>;
let b = a; // alias
```
Mutations through one alias are visible through all aliases.
---
### 18.5 Core Operations
### Length
```pbs
let n: bounded = borrow xs as xsr { xsr.len() };
```
### Appending Elements
Elements are appended using mutation:
```pbs
mutate xs as xsr
{
xsr.push(10);
xsr.push(20);
}
```
For single operations, `take` may be used as syntactic sugar:
```pbs
take xs.push(30);
```
---
### 18.6 Element Access
Direct element access using `xs[i]` is **not allowed**.
### Reading Elements (`peek`)
```pbs
let v: int = peek xs[i];
```
* Returns a value copy
* SAFE operation
### Writing Elements (`mutate`)
```pbs
mutate xs as xsr
{
xsr[i] = 42;
}
```
---
### 18.7 Iteration
Lists may be iterated by value:
```pbs
for v in xs
{
// v is a value copy
}
```
To iterate indices explicitly:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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:
```pbs
mutate s as ss
{
ss.add(10);
ss.add(20);
}
```
For single operations, `take` may be used:
```pbs
take s.add(30);
```
Adding an element already present has no effect.
---
### Membership Test
Membership checks do not mutate the set:
```pbs
let exists: bool = borrow s as ss
{
ss.contains(10);
};
```
---
### Removal
Elements are removed explicitly:
```pbs
mutate s as ss
{
ss.remove(20);
}
```
---
## 19.6 Iteration
Sets may be iterated by value:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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`:
```pbs
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
```pbs
let t = alloc text;
take t.append("Hello");
take t.append(", world");
```
### Appending Other Text
```pbs
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
```pbs
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:
```pbs
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):
```pbs
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`:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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:
```pbs
mutate m as mm
{
mm.put("health", 100);
mm.put("mana", 50);
}
```
For single operations, `take` may be used:
```pbs
take m.put("score", 1000);
```
---
### Lookup
Lookup operations do not mutate the map and return copied values:
```pbs
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:
```pbs
mutate m as mm
{
mm.remove("mana");
}
```
---
### 21.6 Iteration
Maps may be iterated by key-value pairs copied into the SAFE world:
```pbs
for pair in m
{
let (k, v) = pair; // Tuple(K, V)
}
```
Iteration order is implementation-defined.
To iterate keys or values explicitly:
```pbs
for k in m.keys() { }
for v in m.values() { }
```
---
### 21.7 Passing Maps to Functions
Maps are passed by gate (handle):
```pbs
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`:
```pbs
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:
```pbs
deque<T>
```
Where `T` is the element type.
Deques are **homogeneous** and **dynamically sized**.
---
## 22.2 Allocation
Deques are created explicitly using `alloc`:
```pbs
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:
```pbs
let a = alloc deque<int>;
let b = a; // alias
```
Mutations through one alias are visible through all aliases.
---
## 22.5 Core Operations
### Length
```pbs
let n: bounded = borrow d as dd { dd.len() };
```
### Push Operations
```pbs
take d.pushFront(10);
take d.pushBack(20);
```
### Pop Operations
```pbs
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:
```pbs
let v: int = peek d[i];
```
Mutation by index requires `mutate`:
```pbs
mutate d as dd
{
dd[i] = 42;
}
```
---
## 22.7 Iteration
Deques may be iterated by value:
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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:
```pbs
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:
```pbs
blob
```
A `blob` is a dynamic buffer of bytes.
---
### 23.2 Allocation
Blobs are created explicitly using `alloc` with a size:
```pbs
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:
```pbs
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
```pbs
let n: bounded = borrow b as bb { bb.size() };
```
### Read/Write Bytes
```pbs
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):
```pbs
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:
```pbs
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`:
```pbs
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:
```pbs
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`:
```pbs
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
```pbs
let hp: int = borrow s as ss
{
ss.x;
};
```
### Writing Fields
```pbs
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.
```pbs
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:
```pbs
take s.f();
```
Equivalent to:
```pbs
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.
```pbs
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`:
```pbs
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.
```pbs
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.
```pbs
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
```pbs
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.
```pbs
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.
```pbs
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:
```pbs
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.
```pbs
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>`.
```pbs
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:
```pbs
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.