Co-authored-by: Nilton Constantino <nilton.constantino@visma.com> Reviewed-on: #8
98 KiB
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).
- The HIP introduces handles (gates) and aliasing with an explicit sintax (
- 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 fileprometeu.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. projectis declared intoprometeu.jsonas 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:gfxand@project:gfx/mathare two distinct modules.@project:gfxdoes not see any declarations in@project:gfx/math.@project:gfx/mathdoes 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
.pbsfiles directly insideM. - Files in subdirectories of
Mare ignored. - The module’s public index consists of:
- all
pubsymbols - declared in those
.pbsfiles - separated by namespace (type and value).
- all
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
.pbsfile. - 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
importis 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,
pubbehaves exactly likemod.
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 structdeclare errordeclare contract
- Value-level declarations:
servicefnlet
No other mechanism may expose a symbol.
2. Namespaces & Visibility
PBS uses two distinct and independent namespaces: Type and Value. This separation is strict, explicit, and fully enforced by the compiler.
There are no implicit namespaces and no name overloading across namespaces.
2.1 Namespace Model
PBS defines exactly two namespaces:
Type namespace
The type namespace contains only type-level declarations introduced by declare.
Valid type declarations are:
declare struct Name { ... }
declare error Name { ... }
declare contract Name { ... }
Rules:
- Only declarations introduced by
declaremay 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—modorfile-private(default). - top-level
letare not allowed.
Rules:
- Only top-level declarations participate in the value namespace.
- Local
letbindings do not participate in the file, module, or project namespace. - Local bindings are scoped strictly to their enclosing block.
2.2 Strict Namespace Separation
A name MUST NOT exist in both namespaces.
Rules:
- If a name appears in the type namespace, it cannot appear in the value namespace.
- If a name appears in the value namespace, it cannot appear in the type namespace.
- Any attempt to declare the same name in both namespaces is a compile-time error.
Example (INVALID):
declare struct Vector { ... }
service Vector { ... } // ERROR: `Vector` already exists in the type namespace
2.3 Scope of Name Uniqueness
Name uniqueness is enforced based on visibility scope.
File-private symbols (default visibility)
- File-private symbols are visible only within the declaring file.
- File-private symbols may reuse names that appear in other files or modules.
Example (VALID):
// file A.pbs
fn helper() { ... }
// file B.pbs (same module)
fn helper() { ... } // OK: file-private symbols are isolated per file
mod and pub symbols
- Symbols declared with
modorpubvisibility must be unique inside the module. - Declaring two
modorpubsymbols with the same name in the same module is a compile-time error, even if they appear in different files.
Example (INVALID):
// file A.pbs
mod service Gfx { ... }
// file B.pbs (same module)
mod service Gfx { ... } // ERROR: duplicate symbol in module
// file C.pbs (same module)
mod declare error Gfx { ... } // ERROR: duplicate symbol in module
2.4 Visibility Does Not Create Namespaces
Visibility controls who can see a symbol, but does not create separate namespaces.
Rules:
file-private,mod, andpubsymbols all live in the same namespace.- Visibility only restricts access; it never permits name reuse at the same scope.
Example (INVALID):
mod service Audio { ... }
pub service Audio { ... } // ERROR: duplicate symbol in module
2.5 Summary of Namespace Rules
- PBS has exactly two namespaces: type and value.
- A name cannot exist in both namespaces.
- File-private symbols are unique only within a file.
modandpubsymbols 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 structdeclare errordeclare contract
-
service -
fn
No other constructs are allowed at the top level. In particular:
letis 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
.pbsfile is a collection of declarations only. -
Code is executed only when:
- a
servicemethod is invoked, or - a
fnis called from within another function or service method.
- a
-
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
fnis always mod or file-private. - A top-level
fncannot be declared aspub. fndefaults to file-private visibility.
Example (VALID):
// math.pbs
fn clamp(x: int, min: int, max: int): int { ... }
Example (INVALID):
pub fn clamp(x: int): int { ... } // ERROR: top-level fn cannot be pub
Rationale:
fnexists 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
servicemust always be declared with an explicit visibility modifier:pub service— public API of the modulemod service— internal API of the module
-
There is no private service.
-
A service may optionally implement a
contract.
Example:
pub service Audio
{
fn play(sound: Sound): void { ... }
}
Services are the only mechanism for exposing executable behavior across files or modules.
3.5 Type Declarations (declare)
Type declarations introduce symbols into the type namespace.
Rules:
- All type declarations must use the
declarekeyword. - Type declarations may be
file-private,mod, orpub. - Visibility controls where the type can be referenced.
Example:
pub declare struct Vector { ... }
mod declare error IOError { ... }
3.6 Imports at the Top Level
Rules:
importdeclarations are allowed only at the top level of a file.- Imports must appear before any usage of imported symbols.
- Imports have no runtime effect and exist solely for name resolution.
Example:
import { Vector } from "@project:math";
pub service Physics { ... }
3.7 Summary of Top-level Rules
- A
.pbsfile contains declarations only. - No
letor executable statements are allowed at the top level. - Top-level
fnare always file-private. serviceis 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
.pbsfiles 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
.pbsfile.
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
.pbsfiles directly inside the module directory are scanned. - All
modandpubsymbols are collected into the module symbol table.
- All
-
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
modorpubsymbols 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 viaimport.
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
pubsymbols. - Only symbols declared in
.pbsfiles directly insideMare 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:
import { X, Y as Z } from "@project:module";
Rules:
- Imports may only reference modules, never individual files.
- Only
pubsymbols 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:
- Local bindings (
let, parameters), innermost scope first. - File-level declarations in the same file (file-private,
mod,pub). - Imported symbols.
Rules:
- Local
letshadowing is allowed, with warnings. - Shadowing of
servicenames is not allowed. - Shadowing of top-level
fnnames is not allowed. - If an imported symbol conflicts with an existing
modorpubsymbol in the module, compilation fails.
4.7 Cross-file and Cross-module Access
Within the same module
modandpubsymbols are visible across files withoutimport.- File-private symbols are never visible outside their declaring file.
Across modules
- Only
pubsymbols 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 CintroducesCin the type namespace. -
service S: CresolvesCas a type during Phase 2. -
The compiler validates that
Simplements all signatures declared inC. -
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 modulemod service— internal API, visible only inside the module
-
There is no such thing as a private service.
Example:
pub service Audio
{
fn play(sound: Sound): void { ... }
}
5.3 Services and Contracts
A service may optionally implement a contract.
Syntax:
service S: ContractName { ... }
Rules:
-
ContractNamemust resolve to adeclare contracttype. -
If a service declares a contract, it must implement all declared signatures.
-
Signature matching is exact and includes:
- method name
- parameter count and order
- parameter types
- parameter mutability
- return type
Failure to satisfy a contract is a compile-time error.
5.4 Service Methods
Methods declared inside a service define its executable interface.
Rules:
- All service methods are public within the service.
- There are no private, protected, or helper methods inside a service.
- Service methods are not top-level symbols.
- Service methods are accessible only via the service name.
Example:
Audio.play(sound);
5.5 Interaction with Other Code
A service method may call:
- top-level
fndeclared in the same file - other services that are visible under normal visibility rules
Rules:
- A service method may not access file-private symbols from other files.
- A service may not directly access implementation details of another service.
5.6 Implementation Rule
If logic is not conceptually part of the service API, it must not live inside the service.
Rules:
- Implementation logic must be written as file-private
fn. - Services act as thin API layers that delegate to internal functions.
Example:
pub service Math
{
fn add(a: int, b: int): int
{
return add_impl(a, b);
}
}
fn add_impl(a: int, b: int): int { ... }
5.7 Summary of Service Rules
serviceis 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,resultandtuples. - 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) anddeclare 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:
- Primitive types
- User-defined struct types
- 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:
voidint— 32-bit signed integerlong— 64-bit signed integerfloat— 32-bit IEEE-754double— 64-bit IEEE-754boolchar— Unicode scalar value (32-bit)stringbounded— unsigned 16-bit integer for indices and sizes
Rules:
- Primitive types have no identity.
- Primitive values are copied on assignment (except for
stringwhen on constant pool). - Primitive values cannot be partially mutated.
6.3 string
The string type represents an immutable sequence of Unicode characters.
Rules:
- A
stringvalue is immutable. - String literals produce pool-backed string values.
- Some operations may produce stack-owned (for example,
concat) string snapshots.- when pool-backed, copy is cheap, since it is just a "handle" (pool-backed, compile time, constant pool).
- when stack-owned, copy is expensive, since it is a deep copy (stack-owned, runtime).
- Doesn't exist such a thing like
alloc string, it never goes to HIP. - There is no heap/storage allocation for string at runtime.
Example:
let s1: string = "hello"; // pool-backed (cheap copy)
let s2: mut string = mut "hello"; // ERROR: string cannot be mutated
s1 += " world"; // ERROR: no mutation, no +=
let s3: string = s1 + " world"; // OK: produces a runtime-owned snapshot (stack-owned)
6.4 User-defined Struct Types
User-defined value types are declared using declare struct.
Syntax:
declare struct Name(field1: T1, field2: T2) { ... }
Rules:
- A struct is a pure value type.
- A struct has no identity.
- A struct has no subtyping, inheritance, or polymorphism.
- A struct value conceptually exists only as data.
Assignment and passing:
- Assigning a struct value copies the value.
- Passing a struct as a function argument passes a value.
- Returning a struct returns a value.
The compiler may optimize copies internally, but such optimizations must not be observable.
6.5 Mutability and Structs
Mutability is a property of bindings, not of struct types.
Rules:
- A struct bound to an immutable binding cannot be mutated.
- A struct bound to a mutable binding may be mutated via mutating methods.
- Mutating a struct affects only that binding.
Example:
let v = Vector.ZERO;
v.scale(); // ERROR: immutable binding
let w = mut Vector.ZERO;
w.scale(); // OK
6.6 The this Type
Inside a declare struct body, the special type this refers to the enclosing struct type.
Rules:
thismay appear only inside a struct declaration.thisalways refers to the concrete struct type, not to an abstract or dynamic type.- Outside a struct body, use of
thisis illegal.
Example:
declare struct Vector(x: float, y: float)
{
pub fn len(self: this): float { ... }
pub fn scale(self: mut this): void { ... }
}
6.7 Type Visibility
Type declarations follow the same visibility rules as other symbols.
Rules:
- A type declaration may be file-private,
mod, orpub. - 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.
thisprovides 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
servicemust 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
serviceor directly to the runtime, - has no identity and no state.
Contracts are never instantiated and never called directly (unless it is a host-bound contract).
7.2 Declaring a Contract
Contracts are declared using declare contract.
Syntax:
declare contract ContractName
{
fn methodName(param1: T1, param2: T2): R;
}
Rules:
- A contract declaration introduces a symbol into the type namespace.
- Contract declarations may be
mod, orpub(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
elsefallbacks (it is implementation specifics). - Contract methods must not declare visibility modifiers.
- Contract methods exist only for static validation.
Example:
declare contract AudioAPI
{
fn play(sound: Sound): void;
fn stop(id: int): void;
}
7.4 Host-bound Contracts
A contract may be declared as host-bound, meaning its implementation is provided directly by the runtime or host environment.
Syntax:
pub declare contract Gfx host
{
fn drawText(x: int, y: int, message: string, color: Color): void;
}
Rules:
- A host-bound contract should be called directly and should never be implemented by a
service(Gfx.drawText(...)). - A host-bound contract is always bound to the target runtime or host environment.
- A call of the form
ContractName.method(...)resolves to a host call (syscall). - The runtime must provide an implementation for every method declared in a host-bound contract.
- If a host-bound contract is unavailable in the target runtime, compilation or linking fails.
Host-bound contracts define the official API boundary between PBS and the runtime.
7.5 Calling Contracts
Rules:
- Contract methods are invoked using the syntax:
ContractName.method(...);
- This syntax is valid only for host-bound contracts.
- Calling a non-host-bound contract directly is a compile-time error.
Example (INVALID):
declare contract Foo
{
fn bar(): void;
}
Foo.bar(); // ERROR: Foo is not host-bound
7.6 Implementing a Contract in a Service
A service may implement a non-host-bound contract.
Syntax:
pub service Audio: AudioAPI
{
fn play(sound: Sound): void { ... }
fn stop(id: int): void { ... }
}
Rules:
- A service that declares a contract must implement all contract methods.
- Method matching is exact.
- Missing or mismatched methods are compile-time errors.
7.7 Signature Matching Rules
For a service method to satisfy a contract method, all of the following must match exactly:
- method name
- number of parameters
- parameter order
- parameter types
- parameter mutability
- return type
Rules:
- Parameter names do not need to match.
- Overloading is not permitted.
- A contract method may be implemented by exactly one service method.
7.8 Contracts and Visibility
Rules:
- A contract may be implemented only by a service that can legally see it.
- A host-bound contract must be visible at the call site.
- Contract visibility affects name resolution only, never runtime behavior.
7.9 Runtime Semantics of Contracts
Contracts:
- do not generate PBS code bodies,
- do not introduce dynamic dispatch,
- do not allocate memory,
- do not exist as runtime values.
All contract validation is performed at compile time.
7.10 Summary of Contract Rules
- Contracts define static API surfaces only.
- Contracts live in the type namespace.
- Contracts may be service-bound or host-bound.
- Host-bound contracts define the PBS ↔ runtime API.
- Only host-bound contracts may be called directly.
- Contract satisfaction is checked statically and exactly.
8. Structs & Constructors
This section defines declare struct, its fields, constructor aliases, static constants, and struct methods.
All rules in this section are normative.
Structs are pure value types (§6). They have no identity and no inheritance.
8.1 Declaring a Struct
A struct is declared using declare struct.
Syntax:
declare struct Name(field1: T1, field2: T2)
[
// constructor aliases
]
[[
// static constants
]]
{
// methods
}
Rules:
- The field list in parentheses defines the 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):
declare struct Vector(x: float, y: float)
let v = Vector(1, 2);
let x = v.x; // ERROR: field `x` is private
Example (VALID):
declare struct Vector(x: float, y: float)
{
pub fn getX(self: this): float { ... }
}
8.3 Constructor Aliases
Constructor aliases are named constructors defined inside the alias block [...].
Example:
declare struct Vector(x: float, y: float)
[
(): (0.0, 0.0) as default { }
(a: float): (a, a) as square { }
]
{
}
Rules:
- each struct has a default constructor that produces a full-initialized value.
- Each alias defines a parameter list uses a default constructor or alias to create a new struct value.
- Alias bodies
{ ... }are compile-time only and may contain only compile-time constructs (see §8.5). - Constructor aliases live in the type namespace.
- Constructor aliases are not importable. Only the struct type name is importable.
Calling a constructor and constructor alias:
declare struct Vector(x: float, y: float)
[
(s: float): (s, s) as square { }
(s: float): square(s * s) as doubleSquare { }
(x, float, y, float): (x, y) as normalized
{
let l = sqrt(x * x + y * y);
this.x /= l;
this.y /= l;
}
(): square(0) as zero { }
]
That is the default constructor, it takes all parameters always; there is no alias:
let v0 = Vector(1, 3);
That is the zero alias, it will call the default constructor with x = 0 and y = 0
let v1 = Vector.zero();
That is the square alias, it will call the default constructor with x = 2.0 and y = 2.0
let v2 = Vector.square(2.0);
That is the doubleSquare alias, it will call the square alias, and load x = 4.0 and y = 4.0
let v3 = Vector.doubleSquare(2.0);
That is the normalized alias, it will call the default constructor with x = 3.0 and y = 4.0 and normalize it (x = 0.6, y = 0.8)
let v4 = Vector.normalized(3.0, 4.0);
Name rules:
- Alias names must be unique within the struct.
- Alias names must not conflict with method names (to keep it didactic).
8.4 Methods
Struct methods are declared inside the struct body { ... }.
Rules:
- Struct methods may be
mod, orpub.file-privatemethods are not allowed and should rely onfn. - Methods are not top-level symbols.
- Methods are invoked on a struct value using dot syntax.
Example:
declare struct Vector(x: float, y: float)
[
(): (0.0, 0.0) as default { }
]
{
pub fn len(self: this): float { ... }
}
let v = Vector.default();
let l = v.len();
Receiver rules:
- Methods must declare the receiver as the first parameter, named
self. - The receiver type must be either
thisormut this.
Example:
declare struct Vector(x: float, y: float)
{
pub fn len(self: this): float { ... }
pub fn scale(self: mut this, s: float): void { ... }
}
Mutability of self follows the binding rules in §9.
8.5 Static Constants Block
The static constants block [[ ... ]] declares named constants associated with the struct.
Example:
declare struct Vector(x: float, y: float)
[
(): (0.0, 0.0) as default { }
(a: float): (a, a) as square { }
]
[[
ZERO: default()
ONE: square(1.0)
]]
{
}
Rules:
-
Static constants are compile-time constants.
-
Static constants are stored in the constant pool.
-
Static constants are immutable and can never be mutated.
-
The initializer of a static constant may use only:
- struct constructor aliases of the same struct, and
- other static constants of the same struct declared earlier in the same static block.
Static constant access:
let z = Vector.ZERO;
8.6 No Static Methods
PBS has no static methods on structs.
Rules:
- All executable behavior must be expressed as instance methods.
- Constructor aliases are the only “static-like” callable members of a struct.
- Static constants are values only.
This avoids overloading the meaning of TypeName.member.
8.7 Summary of Struct Rules
Full example of struct:
declare struct Vector(x: float, y: float)
[
(): (0.0, 0.0) as default { }
(a: float): (a, a) as square { }
]
[[
ZERO: default()
]]
{
pub fn len(self: this): float { ... }
pub fn scale(self: mut this): void { ... }
}
- Structs are declared with
declare struct. - Fields are private and cannot be accessed directly.
- Constructor aliases exist only inside the type and are called as
Type.alias(...). - Constructor aliases are not importable.
- Static constants are compile-time values and are immutable.
- Struct methods use an explicit
self: this/self: mut thisreceiver. - 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>andresult<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: thisis read-only. - A receiver declared as
self: mut thisallows mutation. - A mutating method requires a mutable lvalue at the call site. Whatever it is passed in will be a copy from there on.
Example:
let v = Vector.ZERO;
v.scale(); // ERROR
let w = mut Vector.ZERO;
w.scale(); // OK
A mutating method may not be called on:
- static constants
- immutable bindings
9.3 The HIP World (Storage / Gate-Backed)
The HIP world is entered explicitly via allocation:
let a = alloc array<int>[4b];
Types allocated with alloc are gate-backed and live in Storage (heap).
- Properties of the HIP World
- Values are accessed via gates (handles).
- Assignment copies the handle, not the underlying data.
- Aliasing is intentional and observable.
- Mutation affects all aliases.
- Lifetime is managed via reference counting (RC).
Example:
let a = alloc array<int>[2b];
let b = a; // alias (shared storage)
mutate a as aa
{
aa[0b] = 10;
}
let x = peek b[0b]; // x == 10
9.4 Gate Semantics (Strong and Weak)
All gate-backed types are strong gates by default.
- T (gate-backed type) → strong gate
- weak → weak gate
Strong Gates
- Increment reference count (RC)
- Keep the storage object alive
- Assignment (let b = a) increases RC
Weak Gates
- Do not increment RC
- Do not keep objects alive
- Observe storage without ownership
Conversions:
let s: Node = alloc Node;
let w: weak<Node> = s as weak; // always succeeds
let maybeS: optional<Node> = w as strong; // may fail
strong as weakis always validweak as strongreturnsoptional<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()
};
aais a read-only reference- The reference cannot escape
- The block returns a value
mutate — Mutable Access (HIP)
mutate a as aa
{
aa[1b] = 42;
}
aais a mutable reference- Mutation is shared across aliases
- The reference cannot escape
9.8 take — Mutation Sugar
For single mutation operations, PBS provides take as syntactic sugar:
take out.push(value);
Equivalent to:
mutate out as o
{
o.push(value);
}
take:
- Is only valid on gate-backed types
- Signals intent clearly
- Keeps HIP usage concise
9.9 Summary of Guarantees
| Context | Aliasing | Mutation | Risk | | SAFE (stack) | None | Local only | None | | HIP (storage) | Explicit | Shared | Intentional |
PBS makes power explicit and risk visible. If you do not allocate, you are safe. If you allocate, you are responsible.
10. Expressions & Control Flow
This section defines the expression model and control-flow constructs of PBS. All rules in this section are normative.
PBS favors explicit, expression-oriented control flow with no hidden execution paths.
10.1 Expression Model
In PBS, most constructs are expressions.
Rules:
- An expression always evaluates to a value.
- The value of an expression is immutable unless bound to a mutable binding.
- Expressions have no side effects except through explicit mutation of mutable bindings.
Statements exist only as a syntactic convenience; semantically, they are expressions whose value is ignored.
10.2 Blocks
A block is a sequence of expressions enclosed in { ... }.
Rules:
- A block introduces a new lexical scope.
- The value of a block is the value of its last expression.
- A block with no final expression evaluates to
void.
Example:
let x = {
let a = 10;
let b = 20;
a + b
}; // x == 30
10.3 if / else
The if / else construct is an expression. The blocks on if doesn't return a value, it is purely a control flow construct.
Syntax:
if (condition)
{
// do something
}
else
{
// do something else
}
Rules:
conditionmust be of typebool.- Both branches will not produce any value.
10.4 when Expression
when is a conditional expression equivalent to a ternary operator.
Syntax:
when condition then expr1 else expr2
Rules:
whenalways requires anelsebranch.- Both branches must produce values of the same type.
whenis an expression and cannot be used without binding or return.
Example:
let sign: int = when x >= 0 then 1 else -1;
let complex: int = when x < 0 then
{
// do something really complex here
return 0;
}
else
{
// do something else even more complex here
return 1;
}
10.5 Loops
PBS supports explicit looping constructs.
10.5.1 for Loop
The for loop iterates over a bounded range.
Syntax:
for i in [start .. end] {
// body
}
Rules:
startandendmust be of typeboundedunless explicitly annotated otherwise.- The loop variable
iis immutable by default. - The range is half-open:
[start .. end). - The body executes zero or more times.
Example:
for i in [0b..10b] {
sum += i;
}
for i in [..10b] {
sum += i;
}
for i in [0b..] {
sum += i;
}
10.6 return
The return expression exits the current function or service method.
Rules:
returnmust return a value compatible with the enclosing function’s return type.returnimmediately terminates execution of the current body.returnis permitted anywhere inside a function or method body.fnandserviceallow a falbackreturnvalue.
Examples:
fn abs(x: int): int // no fallback
{
return when x >= 0 then x else -x;
}
fn abs(x: int): int // ERROR: compilation error, there is no fallback
{
}
fn abs(x: int): int else 1000 // fallback value, no error
{
if (x>0) return x;
// missing a return...
}
10.7 Control Flow Restrictions
Rules:
- There is
breakandcontinue.breakterminates the innermost enclosing loop.continuecontinues 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.
ifis 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.
elsecan be explicit foroptionalbut it is not required, sincenoneis always a return value if nothing else is returned.fn f(): optional<int> else none {}==fn f(): optional<int> { return none; }
resultcan have anelseclause. however, it should be expliciterroror provided by function body (no fallback).
fn v(): Vector else Vector.ZERO
{
if cond return Vector.ONE;
}
Used to guarantee total functions without boilerplate.
11. Numeric Rules & bounded
This section defines numeric types, numeric conversions, the special bounded type, and the built-in range value type.
All rules in this section are normative.
PBS numeric rules are designed to be explicit, predictable, and safe by default.
11.1 Numeric Type Hierarchy
PBS defines the following numeric types:
int— 32-bit signed integerlong— 64-bit signed integerfloat— 32-bit IEEE-754 floating pointdouble— 64-bit IEEE-754 floating pointbounded— unsigned 16-bit integer (0 .. 65535)
Rules:
- All numeric types are value types.
- Numeric types have no identity and no implicit mutability.
11.2 Implicit Widening Conversions
PBS allows implicit widening conversions only.
Allowed implicit conversions:
int → long
int → float
int → double
long → float
long → double
float → double
bounded → int
bounded → long
Rules:
- Implicit widening never loses information.
- Any conversion not listed above requires an explicit cast.
11.3 Explicit Casts
Explicit casts use the syntax:
expr as Type
Rules:
- Explicit casts are required for any narrowing or potentially lossy conversion.
- Explicit casts are checked at compile time and/or runtime as specified below.
11.4 The bounded Type
bounded is a dedicated scalar type for indices, sizes, and counters.
Purpose:
- Make indexing and counting explicit in the type system.
- Prevent accidental misuse of general-purpose integers.
- Enable predictable and safe looping semantics.
Representation:
- Internally represented as an unsigned 16-bit integer (
u16). - Valid range:
0 .. 65535.
Literal syntax:
let a: bounded = 10b;
let b = 0b;
11.5 Conversions Involving bounded
Rules:
bounded → intandbounded → longare implicit.int → boundedandlong → boundedrequire an explicit cast.floatanddoublecannot be cast tobounded.
Casting to bounded:
let b: bounded = x as bounded;
Semantics:
-
Casting to
boundedperforms a clamping operation:- values
< 0become0b - values
> 65535become65535b
- values
-
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
boundedare checked. -
If an operation would overflow or underflow:
- the result is clamped to the valid range, and
- a warning is emitted.
11.7 The range Type
PBS provides a built-in value type range for representing bounded intervals.
A range represents a half-open interval:
[min .. max) - min is inclusive and max is exclusive.
That is, the sequence:
min, min + 1, ..., max - 1
Construction
A range is constructed using the built-in range constructor.
range(max: bounded)
range(min: bounded, max: bounded)
Examples:
let r1: range = range(10b); // [0b .. 10b)
let r2: range = range(5b, 15b); // [5b .. 15b)
let r3: range = range(5b, a.len()); // [5b .. a.len())
Rules:
- The single-argument form sets
minto0b. - The two-argument form sets both bounds explicitly.
maxmust always be provided.- If
min >= max, the range is empty. rangevalues are immutable.
API
A range exposes the following read-only fields:
min: boundedmax: bounded
Optional helper operations:
count(): boundedisEmpty(): bool
11.8 Numeric Safety Guarantees
PBS guarantees that:
- No implicit narrowing conversions occur.
- All potentially lossy conversions are explicit.
boundedoperations cannot produce undefined behavior.rangevalues 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.
boundedis used for indices and sizes.boundedarithmetic is limited and checked.rangerepresents 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:
Tmust be a valid value type.optional<T>itself is a value type.optional<T>has no identity and no aliasing semantics.
12.3 Construction
An optional<T> value is constructed using one of the following forms:
some(value)
none
Rules:
some(value)produces anoptional<T>containingvalue.Tfollow the same type asvalue.noneproduces an emptyoptional<T>.nonerepresents the absence of a value.nonemust be typed by context.
Examples:
let a: optional<int> = some(10);
let b: optional<int> = none;
The following is invalid:
let x = none; // ERROR: type of `none` cannot be inferred
12.4 Extraction with else
The only way to extract a value from an optional<T> is using the else operator.
Syntax:
value = opt else fallback
Rules:
- If
optissome(v), the expression evaluates tov. - If
optisnone, the expression evaluates tofallback. - The type of
fallbackmust be compatible withT.
Example:
let v: int = maybeInt else 0;
No other form of extraction is permitted.
12.5 Control Flow and Functions
Functions returning optional<T> may omit an explicit return none if no value is returned.
Example:
fn find(id: int): optional<int>
{
if (id == 0) return some(42);
// implicit return none
}
Rules:
- Falling off the end of a function returning
optional<T>is equivalent toreturn none. - This behavior applies only to
optional<T>.
12.6 API Surface
optional<T> exposes a minimal, explicit API:
opt.hasSome(): bool
opt.hasNone(): bool
Rules:
- These methods do not extract the contained value.
- They exist only for explicit branching logic.
Example:
if opt.hasSome() then {
// handle presence
} else {
// handle absence
}
12.7 Mutability and Copying
Rules:
optional<T>follows normal value semantics.- Assigning an
optional<T>copies the container and its contents. - Mutability of
Tinsideoptional<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:
Tmust be a valid value type.Emust be anerrorlabel type declared viadeclare error.result<T, E>itself is a value type.result<T, E>has no identity and no aliasing semantics.
13.3 Construction
A result<T, E> value is constructed using one of the following forms:
ok(value)
err(errorLabel)
Rules:
ok(value)produces a successful result containingvalue.err(label)produces a failed result containing anerrorlabel of typeE.- The type of
labelmust matchEexactly.
Examples:
let r1: result<int, IOError> = ok(42);
let r2: result<int, IOError> = err(IOError.not_found);
13.4 The ? Propagation Operator
The ? operator propagates errors only when error types match.
Syntax:
value = expr?
Rules:
exprmust have typeresult<T, E>.- The enclosing function must return
result<_, E>with the same error type. - If
exprisok(v), the expression evaluates tov. - If
expriserr(e), the enclosing function returnserr(e)immediately.
Example:
fn g(): result<bool, ErrorA>
{
return ok(true);
}
fn f(): result<int, ErrorA>
{
let x: int = when g()? then 1 else 0;
return ok(x + 1);
}
13.5 Error Handling with handle
handle is the only construct that allows extracting a value from a result while mapping errors.
Syntax:
value = handle expr
{
ErrorA.case1 => ErrorB.mapped1,
ErrorA.case2 => ErrorB.mapped2,
_ => ErrorB.default,
};
Rules:
exprmust have typeresult<T, E1>.- The enclosing function must return
result<_, E2>. - Each arm maps an error label of
E1to a label ofE2. - Arms must be exhaustive, either explicitly or via
_.
Semantics:
- If
exprisok(v), the expression evaluates tov. - If
expriserr(e), the first matching arm is selected and the function returnserr(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 tooptional<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
Tfollows 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.handleis the only construct for error mapping and extraction.- No exceptions or traps exist.
14. Tuples
PBS provides tuples as a lightweight, stack-only aggregation type.
Tuples exist to support small, local groupings of values without introducing heap allocation, gates, aliasing, or reference counting. They intentionally fill the ergonomic gap left by removing arrays from the stack.
Tuples belong entirely to the SAFE world.
14.1 Tuple Type
The tuple type is written as:
Tuple(T1, T2, ..., Tn)
Each element has a fixed position and type. Tuples are heterogeneous and their size is known at compile time.
Example:
let t: Tuple(int, float, bool) = tuple(1, 2.0, true);
14.2 Construction
Tuples are constructed using the tuple expression:
tuple(expr1, expr2, ..., exprN)
Each expression is evaluated and stored inline on the stack.
Example:
let position = tuple(10, 20);
let state = tuple("idle", true, 3);
14.3 Properties
Tuples have the following properties:
- Tuples are value types
- Tuples live exclusively on the stack
- Assignment performs a conceptual copy of all elements
- Tuples have no identity
- Tuples have no observable aliasing
- Tuples never allocate storage
Because of these properties, tuples are always safe to use and cannot leak memory.
14.4 Element Access
Tuple elements are accessed by zero-based positional index using dot syntax:
let t = tuple(1, 2.0, true);
let a: int = t.0;
let b: float = t.1;
let c: bool = t.2;
Index access is statically checked and always safe.
14.5 Destructuring
Tuples may be destructured into individual bindings:
let t = tuple(1, 2.0, true);
let (a, b, c) = t;
Types may be explicitly annotated if desired:
let (x: int, y: float, z: bool) = t;
Destructuring copies each element into a new stack binding.
14.6 Mutability
Tuple bindings may be mutable:
let t = mut tuple(1, 2, 3);
t.0 = 10;
Mutation is always local to the binding and never affects other values.
Example:
let a = tuple(1, 2, 3);
let b = a; // copy
b.0 = 99;
// a.0 is still 1
14.7 Tuples vs Structs
Tuples and structs are closely related but serve different purposes:
| Struct | Tuple |
|---|---|
| Named type | Anonymous type |
| Declared once, reused | Created inline |
| Named fields | Positional fields |
| Stack value | Stack value |
Tuples are intended for local composition, while structs are intended for named data models.
14.8 Tuples vs Arrays
Tuples are not arrays.
| Tuple | Array |
|---|---|
| Stack-only | Gate-backed (storage) |
| Heterogeneous | Homogeneous |
| No aliasing | Aliasing possible |
| No allocation | Requires alloc |
| SAFE world | HIP world |
Tuples are suitable for small, fixed groupings of values. Arrays are suitable for collections and data structures.
14.9 Didactic Intent
Tuples exist to reinforce the core PBS design principle:
If you do not allocate, you are safe.
They allow expressive local grouping without weakening the guarantees of the SAFE world or introducing hidden costs.
14.10 Summary
- Tuples are stack-only value types
- Tuples are heterogeneous and fixed-size
- Tuples never allocate or alias
- Tuples provide ergonomic local aggregation
Tuples completes the SAFE world without compromising the explicit nature of the PBS memory model.
15. Memory Model
PBS defines a segmented and explicit memory model designed to be:
- Didactic: each memory region teaches a distinct mental model
- Predictable: no implicit allocation or tracing garbage collection
- Game-oriented: execution happens inside loops (frames), with controlled reclamation points
- Explicit about cost and risk: moving between regions is always visible in code
The memory model is divided into four regions:
- Constant Pool
- Stack
- Gate Pool
- Storage (Heap)
Each region has a distinct role, lifetime, and set of guarantees.
15.1 Constant Pool
The Constant Pool stores immutable data known at compile time.
Examples include:
- Numeric literals
- String literals
- Compile-time constant structs (e.g.
Vector.ZERO)
Properties
- Immutable
- Read-only
- Shared globally
- Never reclaimed during execution
Example
let a: int = 10;
let s: string = "Hello World";
let v: Vector = Vector.ZERO;
In the example above:
10,"Hello World", andVector.ZEROlive in the Constant Pool- Assignments copy references to constant data, not mutable memory
- Constant Pool values can never be mutated
The Constant Pool is not a general-purpose memory region and cannot store dynamically created data.
15.2 Stack (SAFE World)
The Stack is the default execution memory for PBS.
It stores:
- Local variables
- Function parameters
- Temporary values
- Return values
Properties
- Value-first semantics
- Conceptual copy on assignment
- No observable aliasing
- No pointers or handles
- Deterministic lifetime (scope-bound)
Example
fn example(): void
{
let a: Vector = Vector(1, 2);
let b = a; // conceptual copy
b.x = 10;
// a.x is still 1
}
All values in the Stack are isolated. Mutating one value never affects another binding.
The Stack is the SAFE world of PBS. If a program only uses stack values and the Constant Pool, it cannot create memory leaks or aliasing bugs.
15.3 Gate Pool
The Gate Pool is an internal runtime structure that manages handles (gates) to objects stored in the Storage region.
A gate is a small, copyable value that represents access to a storage object.
Conceptual Structure
Each Gate Pool entry contains:
- A pointer to a storage object
- A strong reference count (RC)
- A weak reference count
- Optional runtime metadata (type id, flags, debug info)
The Gate Pool is not directly addressable by user code, but all gate-backed types are implemented on top of it.
15.4 Storage (Heap / HIP World)
The Storage region (heap) stores dynamically allocated, mutable data.
Objects in Storage are created explicitly using alloc.
Example
let a = alloc array<int>[4b];
This allocation:
- Creates a new storage object
- Creates a Gate Pool entry
- Returns a strong gate to the caller
Properties
- Mutable
- Aliasing is possible and intentional
- Lifetime managed via reference counting
- Not reclaimed immediately when references disappear
The Storage region is the HIP world of PBS. Power and responsibility are explicit.
15.5 Strong and Weak Gates
All gate-backed types use gates to access storage objects.
Strong Gates
- Represented directly by the type
T - Increment strong reference count
- Keep the storage object alive
let a: Node = alloc Node;
let b = a; // RC++
Weak Gates
- Represented by
weak<T> - Do not increment strong RC
- Do not keep storage alive
let w: weak<Node> = a as weak;
Conversion Rules
let w: weak<Node> = strongNode as weak; // always valid
let o: optional<Node> = w as strong; // may fail
Weak gates must be promoted to strong gates before use.
15.6 Reference Counting (RC)
PBS uses reference counting to manage storage lifetimes.
RC Tracks
- Strong gates stored on the stack
- Strong gates stored inside other storage objects
RC is adjusted automatically when:
- A strong gate is copied or destroyed
- A strong gate is assigned into or removed from storage
Important Notes
- Cycles of strong references are not reclaimed automatically
- Weak gates do not participate in RC
This behavior is intentional and mirrors real-world systems such as C++ shared_ptr / weak_ptr.
15.7 Reclamation and Frame Boundaries
PBS does not use tracing garbage collection.
Instead, storage reclamation happens at safe runtime points, typically:
- End of a frame
- Explicit runtime GC step (implementation-defined)
At reclamation time:
- Storage objects with strong RC == 0 are destroyed
- Associated Gate Pool entries are cleaned up when no weak references remain
This model is predictable and suitable for real-time systems.
15.8 Crossing Memory Regions
Transitions between memory regions are always explicit:
| Operation | Effect |
|---|---|
alloc |
Stack → Storage |
peek |
Storage → Stack (copy) |
borrow |
Temporary read-only access |
mutate |
Temporary mutable access |
take |
Single mutation sugar |
Example
let a = alloc array<int>[2b];
mutate a as aa
{
aa[0b] = 10;
}
let v = peek a[0b]; // copy back to stack
15.9 Didactic Intent
The memory model is intentionally structured to teach:
- Stack vs heap
- Value vs identity
- Ownership and aliasing
- Cost of allocation
- Why cycles leak under RC
Programs that remain in the Stack and Constant Pool are safe by construction. Programs that enter Storage gain power, flexibility, and responsibility.
PBS does not hide these trade-offs.
15.10 Summary
- Constant Pool: immutable, global
- Stack: safe, value-first
- Gate Pool: handle management and RC
- Storage: mutable, shared, explicit
PBS makes memory visible, intentional, and teachable.
16. Gates (Strong & Weak)
PBS introduces gates as the fundamental mechanism for accessing dynamically allocated storage.
Gates make identity, aliasing, and lifetime explicit, while keeping the SAFE world free of references and pointers.
This chapter defines what gates are, how strong and weak gates behave, and how they interact with the memory model.
16.1 What Is a Gate
A gate is a small, copyable value that represents access to an object stored in Storage (heap).
Gates are not pointers in the traditional sense, but they serve a similar role:
- They refer to a storage object
- They can be copied cheaply
- They may alias the same underlying data
All gate-backed types are created explicitly using alloc.
let a = alloc array<int>[4b];
The value returned by alloc is a gate.
16.2 Gate-Backed Types
The following kinds of types are gate-backed:
- Built-in collections (
array,list,map,text) - User-defined storage types (
declare storage struct)
Gate-backed types live in Storage and are accessed exclusively through gates.
16.3 Strong Gates
A strong gate is the default form of a gate-backed value.
Properties:
- Represented directly by the type
T - Increment the strong reference count (RC)
- Keep the storage object alive
- Assignment creates an alias
Example:
let a: Node = alloc Node;
let b = a; // alias, RC++
Both a and b refer to the same storage object.
16.4 Weak Gates
A weak gate is a non-owning reference to a storage object.
Properties:
- Represented by the type
weak<T> - Do not increment strong RC
- Do not keep the storage object alive
- May become invalid when the object is reclaimed
Weak gates are used to break reference cycles and express non-owning relationships.
Example:
let s: Node = alloc Node;
let w: weak<Node> = s as weak;
16.5 Conversions Between Strong and Weak
PBS provides explicit conversions between strong and weak gates.
Strong to Weak
let w: weak<Node> = s as weak;
- Always succeeds
- Does not affect strong RC
Weak to Strong
let o: optional<Node> = w as strong;
- May fail
- Returns
noneif the object has already been reclaimed - Returns
some(T)if promotion succeeds
These conversions make ownership and lifetime explicit.
16.6 Reference Counting and Lifetime
Each storage object has:
- A strong reference count (RC)
- A weak reference count
Rules:
- Strong gates increment and decrement RC automatically
- Weak gates do not affect strong RC
- When strong RC reaches zero, the storage object becomes eligible for reclamation
Reclamation occurs at safe runtime points (e.g. end of frame).
Weak gates may outlive the storage object they reference.
16.7 Gate Validity
A strong gate is always valid.
A weak gate may be:
- Valid: the storage object still exists
- Expired: the storage object has been reclaimed
Expired weak gates cannot be used directly and must be promoted first.
let maybeNode: optional<Node> = w as strong;
16.8 Interaction with Access Control
Only strong gates may be used with:
peekborrowmutatetake
Weak gates must be promoted before any access is allowed.
This rule prevents accidental use of expired references.
16.9 Didactic Intent
Gates are designed to teach:
- The difference between value and identity
- Ownership versus observation
- Why aliasing exists and how it propagates
- Why reference cycles leak under RC
- How weak references break cycles safely
PBS does not hide these concepts.
If a program does not allocate, gates do not exist. If it allocates, gates make responsibility explicit.
16.10 Summary
- Gates are explicit handles to storage objects
- Strong gates own and keep objects alive
- Weak gates observe without ownership
- Conversions between strong and weak are explicit
- Access to storage always requires a strong gate
Gates are the foundation of the HIP world in PBS.
17. Gate-backed Array
PBS provides gate-backed arrays as fixed-size, homogeneous collections stored in Storage (heap).
Gate-backed arrays are the primary indexed collection type in the HIP world. They deliberately trade safety for power and explicit control.
17.1 Array Type
The array type is written as:
array<T>[N]
Where:
Tis the element typeNis a compile-time constant size (bounded)
Arrays are homogeneous and fixed-size.
17.2 Allocation
Arrays are created explicitly using alloc:
let a: array<int>[4b] = alloc array<int>[4b];
Allocation:
- Creates a storage object
- Registers it in the Gate Pool
- Returns a strong gate
Arrays cannot exist on the stack.
17.3 Properties
Gate-backed arrays have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Elements are laid out contiguously
- Size is fixed for the lifetime of the array
Arrays are not value types.
17.4 Aliasing Semantics
Assigning an array copies the gate, not the data:
let a = alloc array<int>[2b];
let b = a; // alias
Mutations through one alias are visible through all aliases.
17.5 Element Access
Direct element access using a[i] is not allowed.
All element access is explicit and controlled.
Reading Elements (peek)
let v: int = peek a[1b];
- Returns a value copy
- SAFE operation
- Never exposes references
Writing Elements (mutate)
mutate a as aa
{
aa[1b] = 42;
}
- Requires mutable access
- Mutation is shared across aliases
17.6 Iteration
Arrays may be iterated by value:
for v in a
{
// v is a value copy
}
Iteration copies each element from storage into the stack.
To iterate indices explicitly:
for i in a.indices()
{
let v = peek a[i];
}
17.7 Passing Arrays to Functions
Arrays are passed by gate (handle), not by value:
fn setFirst(a: array<int>[2b], v: int): void
{
mutate a as aa
{
aa[0b] = v;
}
}
Calling this function mutates the caller's array.
17.8 Copying Arrays
To duplicate an array's contents, use copy:
let b = copy(a);
copy:
- Allocates a new array
- Copies all elements
- Returns a new strong gate
17.9 Bounds and Safety
Array indices are statically bounded by N.
- Out-of-bounds access is a runtime error
- Index values are of type
bounded
These checks preserve safety without hiding cost.
17.10 Didactic Intent
Gate-backed arrays exist to teach:
- Heap allocation
- Aliasing through handles
- Explicit mutation
- Copy versus alias
- Cost of indexed collections
Arrays are powerful but intentionally not implicit or convenient.
17.11 Summary
- Arrays are gate-backed, fixed-size collections
- Arrays live in Storage and require
alloc - Assignment aliases;
copyduplicates - Element access is explicit (
peek,mutate) - Arrays belong to the HIP world
Arrays are the foundation for structured data in PBS.
18. Gate-backed List
PBS provides gate-backed lists as dynamic, growable collections stored in Storage (heap).
Lists complement gate-backed arrays by supporting variable size and incremental construction, at the cost of additional runtime overhead and indirection.
18.1 List Type
The list type is written as:
list<T>
Where T is the element type.
Lists are homogeneous and dynamically sized.
18.2 Allocation
Lists are created explicitly using alloc:
let xs: list<int> = alloc list<int>;
Allocation:
- Creates an empty list in Storage
- Registers it in the Gate Pool
- Returns a strong gate
Lists cannot exist on the stack.
18.3 Properties
Gate-backed lists have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Dynamic length
- Elements may be reallocated internally
Lists are not value types.
18.4 Aliasing Semantics
Assigning a list copies the gate, not the data:
let a = alloc list<int>;
let b = a; // alias
Mutations through one alias are visible through all aliases.
18.5 Core Operations
Length
let n: bounded = borrow xs as xsr { xsr.len() };
Appending Elements
Elements are appended using mutation:
mutate xs as xsr
{
xsr.push(10);
xsr.push(20);
}
For single operations, take may be used as syntactic sugar:
take xs.push(30);
18.6 Element Access
Direct element access using xs[i] is not allowed.
Reading Elements (peek)
let v: int = peek xs[i];
- Returns a value copy
- SAFE operation
Writing Elements (mutate)
mutate xs as xsr
{
xsr[i] = 42;
}
18.7 Iteration
Lists may be iterated by value:
for v in xs
{
// v is a value copy
}
To iterate indices explicitly:
for i in xs.indices()
{
let v = peek xs[i];
}
18.8 Passing Lists to Functions
Lists are passed by gate (handle), not by value:
fn append(xs: list<int>, v: int): void
{
take xs.push(v);
}
Calling this function mutates the caller's list.
18.9 Copying Lists
To duplicate a list and its contents, use copy:
let ys = copy(xs);
copy:
- Allocates a new list
- Copies all elements
- Returns a new strong gate
18.10 Performance Notes
- Appending may trigger internal reallocation
- Element access is O(1)
- Iteration copies elements to the stack
Lists trade predictability for flexibility.
18.11 Didactic Intent
Gate-backed lists exist to teach:
- Dynamic allocation
- Growth and reallocation cost
- Aliasing through handles
- Explicit mutation patterns
Lists are powerful but require discipline.
18.12 Summary
- Lists are gate-backed, dynamic collections
- Lists live in Storage and require
alloc - Assignment aliases;
copyduplicates - Mutation is explicit (
mutate,take) - Lists belong to the HIP world
Lists provide flexibility while preserving explicit control.
19. Gate-backed Set
PBS provides gate-backed sets as unordered collections of unique values stored in Storage (heap).
Sets are designed for membership tests, uniqueness constraints, and dynamic collections where ordering is irrelevant.
Like all gate-backed collections, sets belong to the HIP world and make allocation, aliasing, and mutation explicit.
19.1 Set Type
The set type is written as:
set<T>
Where T is the element type.
Elements of a set must be hashable and comparable.
19.2 Allocation
Sets are created explicitly using alloc:
let s: set<int> = alloc set<int>;
Allocation:
- Creates an empty set in Storage
- Registers it in the Gate Pool
- Returns a strong gate
Sets cannot exist on the stack.
19.3 Properties
Gate-backed sets have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Dynamic size
- No duplicate elements
Sets are not value types.
19.4 Aliasing Semantics
Assigning a set copies the gate, not the data:
let a = alloc set<int>;
let b = a; // alias
Mutations through one alias are visible through all aliases.
19.5 Core Operations
Insertion
Elements are inserted explicitly:
mutate s as ss
{
ss.add(10);
ss.add(20);
}
For single operations, take may be used:
take s.add(30);
Adding an element already present has no effect.
Membership Test
Membership checks do not mutate the set:
let exists: bool = borrow s as ss
{
ss.contains(10);
};
Removal
Elements are removed explicitly:
mutate s as ss
{
ss.remove(20);
}
19.6 Iteration
Sets may be iterated by value:
for v in s
{
// v is a value copy
}
Iteration order is implementation-defined.
19.7 Passing Sets to Functions
Sets are passed by gate (handle), not by value:
fn addIfMissing(s: set<int>, v: int): void
{
let exists = borrow s as ss { ss.contains(v) };
if (!exists)
{
take s.add(v);
}
}
Calling this function mutates the caller's set.
19.8 Copying Sets
To duplicate a set and its contents, use copy:
let s2 = copy(s);
copy:
- Allocates a new set
- Copies all elements
- Returns a new strong gate
19.9 Performance Notes
- Membership tests are expected to be O(1) average
- Insertions and removals may rehash internally
- Iteration copies elements into the stack
Sets trade ordering guarantees for fast membership checks.
19.10 Didactic Intent
Gate-backed sets exist to teach:
- Unordered collections
- Uniqueness constraints
- Membership-oriented logic
- Explicit mutation and aliasing
Sets are ideal for flags, visited markers, and dynamic uniqueness tracking.
19.11 Summary
- Sets are gate-backed, unordered collections
- Sets live in Storage and require
alloc - Assignment aliases;
copyduplicates - Mutation is explicit (
mutate,take) - Iteration order is undefined
Sets complete the family of associative gate-backed collections.
20. Gate-backed Text
PBS provides gate-backed text as the primary dynamic string type.
text exists to support runtime string construction, mutation, and concatenation without weakening the guarantees of the SAFE world. String literals and compile-time strings remain immutable and live in the Constant Pool.
20.1 Text Type
The text type is written as:
text
text is a gate-backed, dynamically sized sequence of characters stored in Storage (heap).
20.2 Text vs String
PBS distinguishes clearly between string and text:
| string | text |
|---|---|
| Immutable | Mutable |
| Constant Pool | Storage (heap) |
| Compile-time | Runtime |
| SAFE world | HIP world |
string values can never be mutated or concatenated. All dynamic string operations require text.
20.3 Allocation
Text objects are created explicitly using alloc:
let t: text = alloc text;
Allocation:
- Creates an empty text object in Storage
- Registers it in the Gate Pool
- Returns a strong gate
20.4 Properties
Gate-backed text has the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Dynamic length
- Internally growable
text is not a value type.
20.5 Appending and Mutation
Text is mutated explicitly using mutate or take.
Appending String Literals
let t = alloc text;
take t.append("Hello");
take t.append(", world");
Appending Other Text
let a = alloc text;
let b = alloc text;
take a.append("Hello");
take b.append("World");
take a.appendText(b);
All mutations affect all aliases.
20.6 Reading Text
Text contents are read by copying into the SAFE world.
Convert to String
let s: string = borrow t as tt { tt.toString() };
The returned string is immutable snapshot copied from text into a SAFE string value runtime-owned.
20.7 Aliasing Semantics
Assigning a text value copies the gate:
let a = alloc text;
let b = a; // alias
take a.append("!");
Both a and b observe the same content.
20.8 Passing Text to Functions
Text values are passed by gate (handle):
fn appendExclamation(t: text): void
{
take t.append("!");
}
Calling this function mutates the caller's text.
20.9 Copying Text
To duplicate a text value, use copy:
let t2 = copy(t);
copy:
- Allocates a new text object
- Copies all characters
- Returns a new strong gate
20.10 Performance Notes
- Appending may trigger internal reallocation
- Converting to
stringcopies 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
textis the dynamic string type in PBSstringremains immutable and SAFE- Text lives in Storage and requires
alloc - Mutation is explicit (
mutate,take) - Assignment aliases;
copyduplicates
Text completes the set of fundamental gate-backed collections.
21. Gate-backed Map
PBS provides gate-backed maps as associative collections stored in Storage (heap).
Maps associate keys to values and are designed for lookup-oriented data structures where indexed access is not sufficient.
Like all gate-backed collections, maps belong to the HIP world and make aliasing, mutation, and cost explicit.
21.1 Map Type
The map type is written as:
map<K, V>
Where:
Kis the key typeVis the value type
Both K and V must be value-safe types or gate-backed types.
21.2 Allocation
Maps are created explicitly using alloc:
let m: map<string, int> = alloc map<string, int>;
Allocation:
- Creates an empty map in Storage
- Registers it in the Gate Pool
- Returns a strong gate
Maps cannot exist on the stack.
21.3 Properties
Gate-backed maps have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Dynamic size
- Key-based lookup
Maps are not value types.
21.4 Aliasing Semantics
Assigning a map copies the gate, not the data:
let a = alloc map<string, int>;
let b = a; // alias
Mutations through one alias are visible through all aliases.
21.5 Core Operations
Insertion and Update
Entries are inserted or updated via mutation:
mutate m as mm
{
mm.put("health", 100);
mm.put("mana", 50);
}
For single operations, take may be used:
take m.put("score", 1000);
Lookup
Lookup operations do not mutate the map and return copied values:
let hp: optional<int> = borrow m as mm
{
mm.get("health");
};
- Returns
some(value)if the key exists - Returns
noneotherwise
Removal
Entries are removed explicitly:
mutate m as mm
{
mm.remove("mana");
}
21.6 Iteration
Maps may be iterated by key-value pairs copied into the SAFE world:
for pair in m
{
let (k, v) = pair; // Tuple(K, V)
}
Iteration order is implementation-defined.
To iterate keys or values explicitly:
for k in m.keys() { }
for v in m.values() { }
21.7 Passing Maps to Functions
Maps are passed by gate (handle):
fn incrementScore(m: map<string, int>): void
{
take m.put("score", (peek m.get("score") else 0) + 1);
}
Calling this function mutates the caller's map.
21.8 Copying Maps
To duplicate a map and its contents, use copy:
let m2 = copy(m);
copy:
- Allocates a new map
- Copies all entries
- Returns a new strong gate
21.9 Performance Notes
- Lookup is expected to be O(1) average
- Insertion and removal may rehash internally
- Iteration copies key-value pairs into the stack
Maps trade predictability for flexibility.
21.10 Didactic Intent
Gate-backed maps exist to teach:
- Associative data structures
- Explicit mutation and lookup
- Cost of dynamic containers
- Aliasing through handles
Maps are powerful but must be used deliberately.
21.11 Summary
- Maps are gate-backed, associative collections
- Maps live in Storage and require
alloc - Assignment aliases;
copyduplicates - Mutation is explicit (
mutate,take) - Lookup returns optional values
Maps complete the core set of gate-backed collection types in PBS.
22. Gate-backed Deque
PBS provides gate-backed deques (double-ended queues) as flexible, dynamic collections stored in Storage (heap).
A deque<T> supports efficient insertion and removal at both ends and serves as the foundational structure for queues and stacks in PBS.
Like all gate-backed collections, deques belong to the HIP world and make allocation, aliasing, and mutation explicit.
22.1 Deque Type
The deque type is written as:
deque<T>
Where T is the element type.
Deques are homogeneous and dynamically sized.
22.2 Allocation
Deques are created explicitly using alloc:
let d: deque<int> = alloc deque<int>;
Allocation:
- Creates an empty deque in Storage
- Registers it in the Gate Pool
- Returns a strong gate
Deques cannot exist on the stack.
22.3 Properties
Gate-backed deques have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Dynamic size
- Efficient operations at both ends
Deques are not value types.
22.4 Aliasing Semantics
Assigning a deque copies the gate, not the data:
let a = alloc deque<int>;
let b = a; // alias
Mutations through one alias are visible through all aliases.
22.5 Core Operations
Length
let n: bounded = borrow d as dd { dd.len() };
Push Operations
take d.pushFront(10);
take d.pushBack(20);
Pop Operations
let a: optional<int> = borrow d as dd { dd.popFront() };
let b: optional<int> = borrow d as dd { dd.popBack() };
Pop operations return optional<T> and do not mutate when the deque is empty.
22.6 Element Access
Direct indexed access is supported for convenience:
let v: int = peek d[i];
Mutation by index requires mutate:
mutate d as dd
{
dd[i] = 42;
}
22.7 Iteration
Deques may be iterated by value:
for v in d
{
// v is a value copy
}
Iteration order is front to back.
22.8 Passing Deques to Functions
Deques are passed by gate (handle), not by value:
fn pushPair(d: deque<int>, a: int, b: int): void
{
take d.pushBack(a);
take d.pushBack(b);
}
Calling this function mutates the caller's deque.
22.9 Copying Deques
To duplicate a deque and its contents, use copy:
let d2 = copy(d);
copy:
- Allocates a new deque
- Copies all elements
- Returns a new strong gate
22.10 Queue and Stack Views
In PBS, queues and stacks are not separate data structures. Instead, they are usage conventions built on top of deque<T>.
Queue (FIFO)
A queue uses the following operations:
- Enqueue:
pushBack - Dequeue:
popFront
Example:
let q: deque<Event> = alloc deque<Event>;
take q.pushBack(evt1);
take q.pushBack(evt2);
let e1 = borrow q as qq { qq.popFront() };
Stack (LIFO)
A stack uses the following operations:
- Push:
pushBack - Pop:
popBack
Example:
let s: deque<int> = alloc deque<int>;
take s.pushBack(1);
take s.pushBack(2);
let top = borrow s as ss { ss.popBack() };
This approach reduces the number of built-in types while preserving expressiveness.
22.11 Performance Notes
- Push and pop operations are expected to be O(1) amortized
- Indexed access may be O(1) or O(n), implementation-defined
- Iteration copies elements into the stack
22.12 Didactic Intent
Gate-backed deques exist to teach:
- Flexible data structures
- FIFO vs LIFO semantics
- Explicit mutation and aliasing
- How abstractions emerge from disciplined usage
Queues and stacks are concepts, not primitives.
22.13 Summary
deque<T>is a gate-backed, double-ended queue- Deques live in Storage and require
alloc - Assignment aliases;
copyduplicates - Queue and stack are usage patterns
Deques complete the set of core dynamic collections in PBS.
23. Gate-backed Blob
PBS provides gate-backed blobs as raw, byte-addressable buffers stored in Storage (heap).
A blob is the fundamental building block for:
- Save data and serialization
- Asset loading (sprites, maps, audio chunks)
- Networking payloads
- Bridging to host APIs
Blobs are intentionally low-level, but they remain explicit and teachable in the HIP world.
23.1 Blob Type
The blob type is written as:
blob
A blob is a dynamic buffer of bytes.
23.2 Allocation
Blobs are created explicitly using alloc with a size:
let b: blob = alloc blob(256b);
Allocation:
- Creates a storage object containing
sizebytes - Registers it in the Gate Pool
- Returns a strong gate
23.3 Properties
Gate-backed blobs have the following properties:
- Stored in Storage (heap)
- Accessed via gates
- Assignment copies the gate (aliasing)
- Byte-addressable
- Size is fixed after allocation (v0)
Blobs are not value types.
23.4 Aliasing Semantics
Assigning a blob copies the gate, not the data:
let a = alloc blob(16b);
let c = a; // alias
take a.writeU8(0b, 255);
let x = borrow c as cc { cc.readU8(0b) }; // x == 255
Mutations through one alias are visible through all aliases.
23.5 Core Operations
The following operations are required for blob.
Size
let n: bounded = borrow b as bb { bb.size() };
Read/Write Bytes
take b.writeU8(0b, 42);
let v: bounded = borrow b as bb { bb.readU8(0b) };
Indices are bounded and refer to byte offsets.
23.6 Typed Reads and Writes
For convenience and portability, blobs may support typed operations.
The endianness of typed operations is explicit.
Examples (LE shown):
take b.writeU16LE(0b, 0xBEEF);
let x: bounded = borrow b as bb { bb.readU16LE(0b) };
take b.writeI32LE(2b, -10);
let y: int = borrow b as bb { bb.readI32LE(2b) };
Implementations may provide a minimal subset initially (e.g. only U8/U16/U32).
23.7 Slicing and Views (Deliberately Deferred)
blob does not expose raw pointers or escaping views in v0.
If slicing is introduced later, it must preserve PBS rules:
- No escaping references
- Explicit cost
- Explicit lifetime
23.8 Bounds and Safety
- Out-of-bounds reads and writes are runtime errors
- Typed reads/writes must also bounds-check their byte width
These checks keep the model teachable without hiding risk.
23.9 Passing Blobs to Functions
Blobs are passed by gate (handle), not by value:
fn fill(b: blob, value: bounded): void
{
let n = borrow b as bb { bb.size() };
let i = 0b;
while (i < n)
{
take b.writeU8(i, value);
i = i + 1b;
}
}
Calling this function mutates the caller's blob.
23.10 Copying Blobs
To duplicate a blob and its contents, use copy:
let b2 = copy(b);
copy:
- Allocates a new blob with the same size
- Copies all bytes
- Returns a new strong gate
23.11 Didactic Intent
Gate-backed blobs exist to teach:
- Raw memory as bytes
- Bounds checking
- Serialization patterns
- Explicit mutation and aliasing
Blobs provide low-level power without introducing implicit pointers.
23.12 Summary
blobis a gate-backed byte buffer- Blobs live in Storage and require
alloc blob(size) - Assignment aliases;
copyduplicates - Mutation is explicit (
mutate,take) - Bounds are checked at runtime
Blobs are the foundation for binary data handling in PBS.
24. Custom Storage API
PBS allows users to define custom gate-backed storage types using the declare storage struct construct.
A storage struct follows the same declaration model as a regular struct, but lives in Storage (heap) and obeys HIP-world semantics. Value-only features are intentionally not available.
This chapter defines the rules, capabilities, and limitations of custom storage types.
24.1 Storage Struct Declaration
A storage struct is declared using:
declare storage struct S(x: int, b: bool)
{
fn f(self: mut this): void
{
// ...
}
}
The declaration consists of:
- A name (
S) - A field list declared in the header
- A method body block
Fields declared in the header are the only fields of the storage struct.
24.2 Allocation
Storage structs are created explicitly using alloc:
let s: S = alloc S;
Allocation:
- Creates a storage object
- Registers it in the Gate Pool
- Returns a strong gate
Storage structs cannot exist on the stack and cannot be created by value construction.
24.3 Properties
Storage structs have the following properties:
-
Stored in Storage (heap)
-
Accessed via gates
-
Assignment copies the gate (aliasing)
-
Lifetime managed via reference counting
-
Fields may contain:
- value types
- gate-backed types
weak<T>andoptional<T>
Storage structs are not value types.
24.4 Field Access Rules
Fields of a storage struct may only be accessed inside borrow or mutate blocks.
Reading Fields
let hp: int = borrow s as ss
{
ss.x;
};
Writing Fields
mutate s as ss
{
ss.b = true;
}
Direct field access outside these blocks is not allowed.
24.5 Methods on Storage Structs
Storage structs may define methods using the same syntax as regular structs.
fn f(self: mut this): void
{
self.x = self.x + 1;
}
Rules:
self: thisprovides read-only accessself: mut thisprovides mutable access- Mutating methods require a mutable context to be called
24.6 Mutation Sugar (take)
For single mutating method calls, take may be used:
take s.f();
Equivalent to:
mutate s as ss
{
ss.f();
}
24.7 Strong and Weak Fields
Fields of a storage struct may be declared as strong or weak gates.
declare storage struct Node(value: int, parent: optional<weak<Node>>)
{
fn attach(self: mut this, p: Node): void
{
self.parent = some(p as weak);
}
}
Rules:
- Strong fields increment reference counts
- Weak fields do not keep objects alive
- Weak fields must be promoted before use
This enables cyclic graphs without leaks.
24.8 Copying Storage Structs
To duplicate a storage struct and its contents, use copy:
let s2 = copy(s);
copy:
- Allocates a new storage object
- Performs a deep copy of all fields
- Returns a new strong gate
24.9 Differences from struct
While storage struct shares syntax with struct, several features are intentionally missing.
Not Available in storage struct
- Constructors (
[]) - Static blocks (
[[]]) - Value initialization syntax (
S(1, true)) - Stack allocation
Initialization of storage structs must be performed explicitly after allocation.
let s = alloc S;
mutate s as ss
{
ss.x = 10;
ss.b = false;
}
These restrictions preserve the explicit nature of heap allocation.
24.10 Interaction with Built-in Collections
Storage structs may freely contain and manipulate gate-backed collections.
declare storage struct Inventory(items: list<Item>)
{
fn add(self: mut this, it: Item): void
{
take self.items.push(it);
}
}
All collection semantics (aliasing, mutation, copying) apply recursively.
24.11 Didactic Intent
The Custom Storage API exists to teach:
- Heap allocation without implicit safety nets
- Ownership and aliasing in user-defined types
- Explicit initialization and mutation
- How complex data structures are built from primitives
Storage structs intentionally expose power without hiding cost.
24.12 Summary
declare storage structdefines 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, andtake - Strong and weak fields control lifetime
Custom storage structs complete the PBS HIP world.
25. Box and Unbox (Built-in Heap Values)
PBS is a value-first language: primitive types and struct values live on the stack and follow SAFE-world semantics.
In some situations, however, a value must acquire identity:
- It must be shared between multiple owners
- It must be mutated through aliases
- It must outlive a stack frame
For these cases, PBS provides the built-in Box abstraction.
Box<T> is a gate-backed built-in type, and box / unbox are built-in operations.
25.1 Box Type
Box<T>
A Box<T>:
- Stores exactly one value of type
Tin Storage (heap) - Is gate-backed (strong gate)
- Participates in aliasing and reference counting
Box<T> is not a value type.
25.2 Boxing a Value (box)
The box builtin allocates a Box<T> in Storage and initializes it with a copy of a SAFE value.
let v: Vector = Vector.ZERO; // SAFE (stack)
let b: Box<Vector> = box(v); // HIP (heap)
Rules:
box(x)performs a copy ofx- The original value remains unchanged on the stack
- The result is a strong gate
25.3 Unboxing a Value (unbox)
The unbox builtin copies the value stored in a box back into the SAFE world.
let vv: Vector = unbox(b);
Rules:
unbox(b)always returns a copy- The returned value has no identity
- Mutating the returned value does not affect the box
25.4 Generic Form
The box and unbox builtins are fully generic:
let b: Box<T> = box(t);
let t2: T = unbox(b);
This works for any SAFE type T.
25.5 Aliasing and Mutation
Boxes follow standard gate semantics.
let a = box(10);
let b = a; // alias
take a.set(20);
let x = unbox(b); // x == 20
Assignment copies the gate, not the boxed value.
25.6 Mutating a Box
A boxed value may be mutated via methods on Box<T>.
take b.set(newValue);
Reading without mutation uses unbox.
Direct access to internal storage is not exposed.
25.7 Relationship to SAFE and HIP Worlds
- SAFE world: values without identity (stack)
- HIP world: values with identity (heap)
box is the explicit transition from SAFE to HIP.
unbox is the explicit transition from HIP to SAFE.
25.8 Informative: Possible Lowering Using storage struct
This section is informative, not normative.
A Box<T> can be implemented internally using a storage struct:
declare storage struct _BoxImpl<T>(value: T)
{
fn get(self: this): T { self.value }
fn set(self: mut this, v: T): void { self.value = v }
}
With lowering rules:
box(x)→alloc _BoxImpl<T>+ store copy ofxunbox(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 typeboxandunboxare 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.