4901 lines
98 KiB
Markdown
4901 lines
98 KiB
Markdown
# 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 module’s **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 module’s 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 module’s 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 module’s 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 struct’s 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 function’s 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. |