ProScript is a statically typed, garbage-collected programming language for highly concurrent application software. It is designed primarily for web services and secondarily for command-line applications.
ProScript combines:
- Go-like operational simplicity, deployment, and lightweight concurrency;
- algebraic data types, exhaustive pattern matching, and static modeling associated with Rust-like languages;
- an interpreted edit-run experience backed by a language-owned virtual machine; and
- automatic memory and resource management without ordinary ownership, borrowing, or lifetime annotations.
This document defines ProScript language version 0.1 and its canonical syntax.
It is intended to be read as a language manual. All examples use the .pr
source-file extension.
- Language goals
- Conformance and execution
- Source text and lexical structure
- Literals
- Packages, modules, and imports
- Declarations and names
- Types
- Bindings, constants, and value semantics
- Functions, methods, and generics
- Protocols
- Expressions and evaluation
- Statements and control flow
- Patterns
- Errors, panics, and recovery
- Managed memory and arenas
- Deterministic resources
- Concurrency
- Structural debugging, JSON, and template metadata
- Native extensions
- Diagnostics and tooling
- Compatibility and evolution
- Language 0.1 boundary
ProScript is optimized for application servers that:
- handle many concurrent network connections;
- spend substantial time waiting on databases, files, queues, and remote services;
- require predictable failure and cleanup behavior;
- benefit from expressive request, response, event, and error types; and
- must remain easy to prototype, inspect, and deploy.
Command-line applications are a secondary first-class workload.
CPU-intensive operations may use optimized standard-library implementations or native extensions. ProScript does not promise that arbitrary interpreted application code matches optimized native code.
When language goals conflict, ProScript prioritizes:
- safety;
- clarity and local reasoning;
- simplicity in ordinary programs;
- rapid iteration;
- practical server performance; and
- maximum low-level flexibility.
The language automates memory placement, garbage collection, arena promotion,
resource claims, and task scheduling. Consequential programmer intent remains
visible through constructs such as spawn, explicit error types, channel
operations, arena, native declarations, and explicit discards.
ProScript 0.1 is not intended for:
- kernels, device drivers, or bare-metal software;
- hard real-time systems;
- software requiring stable managed addresses or direct pointer arithmetic;
- unrestricted foreign-memory manipulation; or
- programs whose primary purpose is manual memory-layout control.
A conforming implementation must accept every valid program defined by this specification and must preserve its observable behavior. Optimizations may change representation, allocation placement, scheduling details, or execution tier only when the change is unobservable under the language rules.
Source code is the compatibility contract. Bytecode is an internal, implementation-versioned format.
Before executing a target, the implementation parses, resolves, and statically checks every package reachable from that target. An unrelated file outside the reachable package graph does not prevent execution.
Validated package-level caches may be reused when their source, compiler mode, dependencies, and relevant configuration are unchanged.
ProScript source runs directly through a ProScript-owned runtime. An implementation may:
- interpret an abstract syntax tree;
- lower source to internal bytecode;
- cache checked bytecode;
- transparently compile bytecode ahead of time; or
- package source-derived code with a matching runtime.
These choices do not create distinct source-language modes.
The permanent execution model consists of a bytecode virtual-machine tier and an optional ahead-of-time native tier. A JIT is not required. Every tier must agree on types, arithmetic, panics, cleanup, scheduling-visible behavior, and all other source semantics.
Source-only execution is the normal development workflow. Tooling may also produce target-specific self-contained artifacts that require no separate ProScript installation.
Pure ProScript code has platform-independent semantics except where a type is
explicitly target-dependent, such as int and uint. Native packages declare
their supported operating systems and architectures. An unsupported target is
reported before an artifact is produced.
The sole ProScript source-file extension is lowercase .pr on every platform.
The compiler, formatter, language server, package loader, test discovery,
editor integrations, and every other ProScript-aware tool recognize exactly
that extension. .pros, .proscript, .ps, .pro, uppercase variants, and
all other spellings are not source-file aliases.
Filename stems otherwise have no language-level meaning: they do not determine
package identity, type names, visibility, or entry points. A filename ending
in _test.pr is selected only by the test tool.
Source files are Unicode text encoded as UTF-8. Source text is not Unicode-normalized.
The source newline is U+000A LINE FEED. The lexical whitespace characters are exactly:
U+0020 space
U+0009 horizontal tab
U+000D carriage return
U+000A line feed
Other Unicode whitespace outside comments and literals is a lexical error.
A single UTF-8 byte-order mark is accepted only at the beginning of an ordinary source file. NUL is rejected.
Carriage return is whitespace rather than a newline. Within a raw string, carriage returns are discarded, making equivalent LF and CRLF sources produce the same raw-string value.
A source file may begin with a Unix-style shebang at byte offset zero:
#!/usr/bin/env proscript
package mainEverything from #! through the first line feed, or through end of file, is
ignored by the ProScript parser. No whitespace or byte-order mark may precede
the shebang. #! elsewhere is invalid.
Line comments begin with // and continue through the end of the line:
// A line comment.
let retries = 3; // A trailing comment.Documentation comments begin with /// and document the following declaration:
/// Creates a user after validating the request.
pub fn create_user(request: CreateUserRequest) Result<User, CreateUserError> {
// ...
}ProScript has no block-comment syntax.
Identifiers contain ASCII letters, ASCII digits, and underscores. The first character must be a letter or underscore.
identifier = (letter | "_") (letter | digit | "_")*
Identifiers are case-sensitive and are not normalized or case-folded.
The single token _ is the discard pattern or discard assignment target. A
longer underscore-prefixed name is an ordinary identifier.
Canonical casing is:
PascalCasefor types and enum variants;snake_casefor packages, functions, methods, fields, parameters, and local bindings;SCREAMING_SNAKE_CASEfor constants; and- lowercase for language keywords.
Acronyms use word casing: HttpServer, JsonPayload, UserId,
http_server, and user_id.
The globally reserved keywords are:
package import
pub
const let mut
fn self
struct enum type opaque
resource cleanup
protocol conform
if else match
for in break continue
return
spawn select case default
arena
recover with
native offload
true false
A keyword cannot be used in any identifier position. ProScript has no raw identifier escape.
map and set are contextual literal introducers rather than reserved
keywords. In expression position, map { and set { always begin collection
literals. Elsewhere, both words are ordinary identifiers.
When an identifier named map or set occurs immediately before a
block-opening brace, it must be parenthesized:
if (set) {
apply();
}
match (map) {
// ...
}Primitive type names, predeclared variants, and compiler operations are predeclared names rather than keywords. They cannot be redeclared or shadowed.
Outside comments and literals, a newline is ordinary whitespace. ProScript does not insert semicolons automatically.
Multi-character tokens such as ::, =>, +=, &&, and ..= cannot be
split by whitespace.
There is no backslash line continuation.
An ordinary source file canonically ends with one newline. The parser accepts a file whose final token is immediately followed by end of file.
Executable blocks always use braces:
if ready {
process();
}Indentation, a colon, then, do, begin, and end never delimit a block.
There is no standalone bare-block statement. A newline may precede an opening
brace because newlines are ordinary whitespace, although canonical formatting
places the brace on the header line.
Every comma-delimited list requires exactly one comma between adjacent entries. One trailing comma is accepted in any nonempty list independently of line breaks. Leading commas, repeated commas, and missing separators are invalid.
The canonical formatter removes a trailing comma from a single-line list and emits one in a multiline list.
The Boolean literals are true and false. They have type bool.
ProScript has no truthiness. Numeric and string values do not act as Boolean values.
Integer literals support decimal, binary, octal, and hexadecimal forms:
42
0b1101_0010
0o755
0xFF_A2_00The base prefixes are lowercase. Hexadecimal digits may use either case. Legacy leading-zero octal notation is invalid.
Underscores may appear only between digits. Integer literals have no type suffix. A leading sign is a unary operator, not part of the literal.
An integer literal takes the integer type required by its immediate context. An
unconstrained integer literal defaults to int. The selected type must
represent the value.
In expression position, unary - applied to an unsuffixed integer literal
through zero or more grouping parentheses is one signed contextual-literal
form. The compiler checks the final negated mathematical value, so -128,
-(128), and -((128)) are all valid in an int8 context. Recognition does
not cross another prefix operator, arithmetic, a conversion, or another general
expression. Rune and byte literals retain their fixed types and are excluded.
An additional outer prefix is an ordinary operation on the typed inner value.
Consequently, let value: int8 = - -128; type-checks and the outer negation
panics at runtime. The equivalent operation in a package constant is rejected
during compile-time evaluation. This grouping allowance is expression-only;
parenthesized range-pattern bounds remain invalid.
Floating-point literals use decimal or scientific notation:
1.5
1e-3
2.5E6
6.022_140_76e23A decimal point requires digits on both sides. Exponents require at least one digit. Underscores may appear between digits. Floating literals have no type suffix and do not support hexadecimal notation.
A floating literal takes the floating type required by context. An
unconstrained floating literal defaults to float64.
A rune literal uses single quotes and has the fixed type rune:
'A'
'世'
'\n'
'\u2764'It contains exactly one Unicode scalar value, literally or through one escape. Recognized escapes are:
\a,\b,\f,\n,\r,\t,\v,\\, and\';- exactly three octal digits;
\xplus exactly two hexadecimal digits;\uplus exactly four hexadecimal digits; and\Uplus exactly eight hexadecimal digits.
Surrogates and values above U+10FFFF are invalid. A multi-scalar grapheme is not one rune literal.
A byte literal uses b'…' and has the fixed type byte:
b'A'
b'\n'
b'\xff'
b'\377'It accepts one ASCII source character, one single-byte special escape, one two-digit hexadecimal escape, or one three-digit octal escape in the byte range. Unicode escapes and non-ASCII source characters are invalid.
A byte literal denotes a numeric byte. It is not a UTF-8 encoding operation.
An interpreted string uses double quotes and has type string:
"hello"
"Hello, 世界"
"first\nsecond"
"created user ${user.id}"Recognized escapes are \a, \b, \f, \n, \r, \t, \v, \\,
\", \u with four hexadecimal digits, and \U with eight hexadecimal
digits.
Octal and \x byte escapes are invalid in strings. A physical newline cannot
appear inside an interpreted string.
A raw string uses backticks:
let query = `SELECT id, email
FROM users
WHERE active = true`;Backslashes have no escape meaning, physical newlines are allowed, and interpolation is disabled. A raw string cannot contain a backtick.
Both string forms produce immutable, valid UTF-8 string values.
Interpolation uses ${name}, ${name.field}, or ${self.field} inside an
interpreted string:
"user ${user.id}"
"owner ${request.user.display_name}"
"label ${self.display_name}"An interpolation slot accepts only:
- a value-binding or constant name;
- the receiver keyword
self, inside a type-owned method body; or - a chain of field accesses beginning at either of the above.
self in a slot remains a field-access base only, exactly as it is elsewhere.
A slot consisting of ${self} alone is invalid, including when the receiver is
a primitive-backed nominal; bind a temporary to interpolate the receiver itself.
Calls, conversions, indexing, arithmetic, conditionals, blocks, and other expressions are not permitted in a slot. Bind a temporary first:
let next_attempt = attempts + 1;
let message = "starting attempt ${next_attempt}";The final value must be a primitive or a primitive-backed nominal type. Canonical rendering is:
- strings as their contents;
- integers in decimal;
- finite floats in the canonical ES6-style shortest round-trip text form,
preserving
-0; - non-finite floats as exactly
NaN,+Inf, and-Inf; - Boolean values as
trueorfalse; - bytes, runes, and every other integer alias in decimal; and
- open or opaque primitive-backed nominals through their underlying primitive.
Aggregates, resources, capabilities, closures, protocol values, and unit
cannot be interpolated.
Slots evaluate from left to right. Rendering cannot fail. \${ emits the
literal characters ${.
The three non-finite float spellings apply identically to float32, float64,
and float-backed nominals. Every NaN sign and payload renders as NaN. These
strings are interpolation output rather than source literals or JSON numbers;
JSON encoding continues to reject non-finite floats with JsonError.
Structural Debug formatting remains a separate contract.
byte and rune are exact aliases of uint8 and int32; alias spelling
cannot change runtime behavior. Consequently 'A' interpolates as the decimal
integer 65. Character text requires an explicit standard-library conversion
or formatting operation whose resulting string is bound before interpolation.
Adjacent string literals do not concatenate. Use + or interpolation.
One directory is one package compilation and namespace unit. All selected
.pr files in that directory contribute declarations to the package.
Subdirectories are separate packages.
All selected ordinary source files in one directory package must declare the same package name. Different written names do not create co-located packages; they are a compile-time package-header mismatch.
A module is the distribution unit identified by proscript.toml. It may
contain multiple packages.
Every source file begins, after an optional shebang, with:
package users_apiFor an ordinary package, the package name is an ASCII snake_case identifier
and must exactly equal the final component of the file's canonical package
path.
An executable command package instead declares:
package mainpackage main is exempt from directory-name agreement. Its directory may have
a different name or one that is not a valid ordinary package identifier. The
command package retains the distinct canonical logical identity supplied by
target and module resolution; command packages from different locations do not
merge merely because they share the written name main.
Every selected ordinary source file in that command directory must declare
package main. A file with another package header is a compile-time error.
Whether another package may import a command package is not defined by this
exception.
The package declaration has no semicolon.
Imports follow the package declaration and are file-local. Imports do not execute code.
Whole-package import:
import std.httpSingle-item import:
import std.http.RequestGrouped item import:
import std.http {
Request,
Response,
}Whole-package alias:
import cloud_provider.http as cloud_httpas is contextual to whole-package imports. Individual item aliases, wildcard
imports, public imports, and re-exports do not exist.
An import path is a canonical dot-separated logical path rooted at:
std;- the current module identity; or
- a declared dependency identity.
Filesystem-relative imports do not exist. Source files in the same package do not import one another.
Self-imports and package cycles are compile-time errors. A cycle diagnostic must report a concrete cycle path.
Packages have no runtime initialization phase:
- imports execute no code;
- package-level
letis invalid; - package-level constants are evaluated during checking;
- no
initfunction is discovered; and - file and declaration order have no runtime meaning.
Execution begins at the selected entry point.
A module with external dependencies, native dependencies, multiple packages,
publishing, or self-contained packaging has a proscript.toml manifest:
[package]
name = "billing_api"
version = "0.3.1"
language = "0.1"
[dependencies]
zstd = "1.2"The generated proscript.lock records the exact dependency graph, source
identities, versions, integrity information, and native artifacts.
A standalone standard-library-only package main command may run without
either file.
One manifest describes one module. Workspace manifests and runtime package initializers do not exist in ProScript 0.1.
Declarations are package-private by default. pub makes a declaration visible
to importing packages:
pub struct User {
pub id: UserId,
pub name: string,
password_hash: string,
}Visibility does not depend on capitalization.
A pub struct exports the type name, but every field is independently
package-private by default. Mark a field pub to expose it to importing
packages. A field is externally accessible only when both it and its containing
struct are public. A public enum still exposes every variant and payload; a
private enum exposes none of them.
All files in the declaring directory package may access every field. Importers
may access, mutate, interpolate, and pattern-match only public fields. When a
public struct has any private field, importers cannot directly construct it,
because construction must initialize every field and private fields cannot be
named there. Such types expose public associated constructors or factories.
External patterns must use final .. whenever private fields exist.
Privacy does not remove a field from equality, copying, or compiler-generated structural providers. Debug, JSON, and template exposure remain controlled by their own eligibility and directives; private secrets still require redaction, skipping, or provider denial. Private fields may use package-private types, while every public field type must be externally nameable.
Methods and associated functions have independent visibility:
pub fn User::display_name(self) string {
return self.name;
}A public type does not make all its methods public.
Every type appearing in a public signature must be externally nameable,
including nested generic arguments, callable types, protocol bounds, enum
payloads, public fields, constants, aliases, and both positions of
Result<T, E>.
Public structs with private fields, public opaque nominal types, and public resources provide representation sealing appropriate to their distinct value, nominal, and lifecycle semantics.
Restricted visibility, friend access, file-private declarations, wildcard exports, and re-exports do not exist.
Only a package declared package main is an executable command. It contains
exactly one non-public main function with either signature:
fn main()
fn main() Result<unit, E>In the second form, E must support Debug. Ok(unit) exits successfully.
Err(error) prints a diagnostic representation and exits unsuccessfully.
main accepts no parameters and cannot be pub.
The entry function is selected across the complete directory package. Its
source file need not be named main.pr, and a source-file target need not name
the file containing it. Multiple source files may belong to package main, but
a second package-level declaration named main is a duplicate declaration.
A function named main in an ordinary package is an ordinary package function
and does not make that package executable.
A package has one namespace for package-level constants, functions, structs, enums, nominal types, aliases, resources, and protocols. Two declarations cannot use the same name even when they belong to different declaration categories.
Each type has a separate member namespace for its fields, methods, and associated functions. Overloading is not supported: a member name denotes at most one callable or field in that namespace.
For an enum, every variant and every type-owned callable also belong to one
collision-free exact-name set. An associated function, read-only method, or
mut self method cannot use the exact name of a variant. This is checked across
the whole directory package independently of file and declaration order.
Resolution never uses receiver category, call shape, argument or result types,
visibility, or variant/callable precedence to escape the collision.
Imports are file-local and cannot collide with one another or with visible package declarations.
Name resolution is independent of source order. It never uses capitalization, expected types, generic arity, or callable shape to resolve ambiguity.
Compiler-predeclared names cannot be shadowed or redeclared. These include:
- primitive types and
unit; - core type families such as
Option,Result,Array,Map,Set,Range, andTask; - structural protocols and providers;
Some,None,Ok, andErr; and- compiler operations such as
panicandtimeout.
A function has one active lexical value namespace spanning parameters, local bindings, patterns, loops, and closures.
A declaration cannot shadow or redeclare an active local name. A nested block may reuse a name only after the earlier binding is no longer in scope.
Generic parameters similarly cannot duplicate or shadow an active generic parameter.
An unused named local binding is a compile-time error. Prefixing a binding with an underscore explicitly permits it to remain unused:
let _request_id = request.id;The name still denotes a real value and participates in uniqueness rules.
Use _ when the value itself is deliberately discarded.
The primitive type names are:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
float32 float64
byte
rune
unit
byte is an exact alias of uint8. rune is an exact alias of int32.
int is a signed target-word integer and uint is its unsigned counterpart.
Their width is either 32 or 64 bits for a target. ProScript has no uintptr,
128-bit primitive integers, or arbitrary-precision runtime integer.
unit is both the unit type and its sole value:
fn record() {
return;
}
let result: Result<unit, StorageError> = Ok(unit);unit is not permitted as a struct or resource field type. Use an empty struct
for a named zero-data record and an empty enum variant for a state.
ProScript has no primitive complex-number types.
Every string is immutable valid UTF-8 text. Malformed UTF-8 cannot inhabit
string.
Binary data uses Array<byte>. There is no separate built-in Bytes type and
no implicit conversion between string and Array<byte>.
String equality and hashing compare the exact Unicode scalar sequence, which is equivalent to the exact UTF-8 sequence. No normalization, case folding, or locale processing occurs implicitly.
String ordering is exact lexicographic UTF-8 ordering.
string has no integer indexing, numeric slicing, or unqualified length
operation. Named text APIs must make byte, rune, or grapheme units explicit.
Because rune is an alias of int32, an arbitrary runtime rune need not be a
valid Unicode scalar. Any operation that inserts a rune into a string validates
it and cannot create malformed UTF-8.
Array<T> is an ordered, variable-length structural value collection.
Array literals use brackets:
let users = [
first_user,
second_user,
];
let empty: Array<User> = [];Array indexing uses array[index]. The index must be an integer in range.
Out-of-range reads or writes panic. A checked get operation returns an
Option.
Index assignment replaces an existing element and never grows the array. ProScript has no built-in slice expression.
Map<K, V> is a structural value collection from eligible keys to values.
Map literals use contextual map and ::
let headers = map {
"content-type": "application/json",
"accept": "application/json",
};
let empty: Map<string, string> = map {};Map order is unspecified. Maps use named lookup and mutation methods rather than bracket indexing because key absence is ordinary data.
The core operation signatures are:
Map<K, V>.get(key: K) Option<V>
Map<K, V>.set(key: K, value: V) unit
Map<K, V>.remove(key: K) unit
get is a read-only observation. Its non-unit result must be used or explicitly
discarded. set and remove require a writable map place and always return
unit. Setting an existing key replaces its value; removing an absent key is a
successful no-op. A caller that needs the previous value observes it explicitly
with get; no mutator overload returns it.
A map literal behaves as sequential insertion into an initially empty map. Entries evaluate in source order, with each key followed by its value, exactly once. A later key equal to an earlier key replaces the earlier associated value. Duplicate source entries do not produce a compile-time error or runtime panic, even when the duplicate is statically apparent.
Eligible map keys are:
bool;string;- the integer family, including
byteandrune; and - nominal types whose ultimate underlying type is one of those primitives.
Floats, structs, enums, collections, resources, handles, protocol values, and other types are not map keys. User-defined hashing and equality do not extend the key set.
Set<T> is a structural value collection of unique eligible values:
let permissions = set {
"read",
"write",
};
let empty: Set<string> = set {};Set element eligibility is the same scalar whitelist as map-key eligibility. A set literal behaves as sequential insertion into an initially empty set. Its elements evaluate in source order exactly once. Repeated equal elements collapse to one member without an error or panic, even when the duplicate is statically apparent. Set order is unspecified.
The core operation signatures are:
Set<T>.contains(value: T) bool
Set<T>.add(value: T) unit
Set<T>.remove(value: T) unit
contains is a read-only observation whose result must be used or explicitly
discarded. add and remove require a writable set place and return unit
whether they change membership or encounter an already-present or absent
element. No mutator overload reports the prior membership state.
Collection mutator arguments evaluate exactly once from left to right. The mutation occurs only after argument evaluation completes; a failed or panicking argument leaves the collection unchanged.
These source-literal rules do not alter the separately defined JSON duplicate handling rules.
Absence uses Option<T>:
enum Option<T> {
Some(T),
None,
}Recoverable failure uses Result<T, E>:
enum Result<T, E> {
Ok(T),
Err(E),
}Some, None, Ok, and Err are predeclared unqualified variants. There is
no null, nullable reference, or implicit absence.
Result places no universal protocol constraint on E. APIs that report an
error generically impose Debug or another relevant protocol locally.
A struct declares named fields:
struct User {
id: UserId,
name: string,
email: string,
}Fields use optional pub followed by name: Type and comma separation. Fields
are package-private by default independently of the struct declaration. A
multiline declaration is formatted with a trailing comma and has no semicolon
after its closing brace.
Struct construction names every field exactly once:
let user = User {
id: user_id,
name: payload.name,
email: payload.email,
};Every field must be initialized. Struct declarations have no default field initializers, and construction has no spread or update syntax.
Inside the declaring package, construction may name every field. An importing
package may directly construct a public struct only when every field is public.
If any field is private, importers use a public associated constructor or
factory; they cannot omit hidden fields or supply a construction ...
A generic struct construction writes its complete owner type-argument vector:
let box = Box<int> {
value: 42,
};Fields, destinations, expected results, and later uses do not infer that vector. Every owner argument appears once in declaration order. Empty, partial, and placeholder vectors are invalid.
Structs have structural value semantics and never support whole-struct == or
!=, regardless of their fields.
An enum declares a closed set of variants:
enum CreateUserError {
InvalidEmail,
Storage(DatabaseError),
Conflict(UserId, string),
}Variants are constructed and named through :::
CreateUserError::InvalidEmail
CreateUserError::Storage(error)A generic enum writes its complete owner type-argument vector before ::,
including for payload-free values and payload-bearing constructor values:
Outcome<int, string>::Success(value)
Outcome<int, string>::Pending
let wrap: fn(int) Outcome<int, string> =
Outcome<int, string>::Success;Payloads, destinations, expected results, and later uses do not infer that vector. Every owner argument appears once in declaration order. Empty, partial, and placeholder vectors are invalid.
The parser commits to a generic aggregate head when a syntactically valid
nonempty type-argument list is followed by { for struct construction or ::
for enum selection. This decision uses tokens only. Before commitment, bounded
speculation restores the input for ordinary expression parsing; after
commitment, errors recover within the construction and do not reinterpret the
angle brackets as comparisons.
Enum equality is derived structurally when every payload type is comparable. An enum containing a non-comparable payload is not comparable.
A payload-bearing variant constructor is a callable value:
let wrap_error: fn(DatabaseError) CreateUserError =
CreateUserError::Storage;Anonymous functions and payload-bearing variant constructors are the only sources of callable values. A payload-free variant is an ordinary value, not a zero-parameter callable.
An open nominal type creates a distinct type with public explicit construction:
type RequestId string;An opaque nominal type restricts construction from its underlying type to its declaring package:
pub type opaque UserId string;An exact alias creates another name for the same type:
type UserTable = Map<UserId, User>;Aliases may be generic:
type Table<Key, Value> = Map<Key, Value>;Aliases have no independent identity, methods, visibility authority, or conformances. Every generic alias parameter must be used in its target. Alias cycles are invalid.
A fully instantiated transparent alias may be used as a struct or enum construction head when normalization produces the corresponding concrete aggregate:
type IntBox = Box<int>;
let box = IntBox { value: 42 };
type BoxAlias<Value> = Box<Value>;
let other_box = BoxAlias<string> { value: "ready" };
type IntOutcome<Failure> = Outcome<int, Failure>;
let pending = IntOutcome<string>::Pending;Open and opaque nominal declarations are not generic.
An explicit conversion between a nominal type and its immediately declared underlying type uses call notation:
let request_id = RequestId(raw);
let raw_again = string(request_id);Construction of an opaque nominal is legal only in its declaring package. Observation through conversion to the immediate underlying type is legal wherever the opaque value is usable.
Conversions traverse one declared nominal edge at a time. They do not skip nominal layers or convert directly between unrelated nominal types with equal representations.
Primitive-backed nominals inherit only observation-producing comparisons:
==, !=, and, when ordered, <, <=, >, and >=. Operands must have
the same nominal type. Arithmetic, bitwise operations, shifts, unary operators,
and concatenation do not inherit.
Structs, enums, and resources may refer recursively to themselves without source-level pointer or box syntax. The compiler supplies unobservable managed indirection where required.
A recursive type must have a finite construction path. Generic recursion must recur with the same parameter vector rather than expanding indefinitely.
Nominal underlyings cannot be recursive.
A callable signature type uses fn:
fn(Request) Response
fn(int, int) Result<int, ArithmeticError>Parameter names are omitted in callable types. An omitted result type means
unit.
Named functions and methods are not first-class values. Anonymous functions and enum variant constructors are the callable-value sources.
A protocol name may appear as a static type. A conforming concrete value widens implicitly at a protocol-typed assignment, argument, field, element, or return.
This permits fully typed heterogeneous collections without variadics or Any:
let items: Array<Displayable> = [
user,
order,
invoice,
];
log.info("request observed", [
request_id,
method,
path,
]);Protocol values:
- expose only protocol methods;
- are not downcastable;
- are not comparable;
- are not map keys or set elements;
- are not structurally JSON-eligible; and
- cannot cross task boundaries.
Ordinary values widen with ordinary value semantics. A resource widened to a protocol value retains its resource identity and lifetime claim.
There is no implicit widening or conversion from one protocol-value type to
another. A bare protocol name is the protocol-value syntax; there is no dyn
or any marker.
Primitive equality follows each primitive type's rules. Equality requires compatible static types and cannot be overloaded.
Enums compare by variant and then by corresponding payloads. An enum is comparable only when every possible payload is comparable.
Collections have structural equality when their element domains are comparable:
- arrays compare corresponding elements in order;
- maps compare key/value associations independently of storage or iteration order; and
- sets compare membership independently of order.
Different collection types are not comparable. A non-comparable element, key, or value makes the containing collection non-comparable. In particular, a collection containing a struct is not comparable because structs never support whole-value equality.
Resources, synchronized handles, channels, tasks, closures, protocol values, and native handles do not receive automatic identity equality. An abstraction that needs a meaningful identity comparison exposes a named operation or an ordinary comparable identifier.
Structural equality invokes no user code, cannot suspend, and cannot observe allocation identity, capacity, hash-table layout, or shared backing storage.
let introduces an immutable local binding:
let user = load_user()?;
let retries: int = 3;The binding cannot be reassigned and cannot serve as the root of ordinary structural mutation.
let mut permits reassignment and transitive mutation through ordinary value
fields:
let mut retries = 0;
for retries < maximum_retries {
retries += 1;
}mut controls the receiving binding or parameter. It does not change the
static type.
Capability handles may expose safe identity operations through immutable bindings. Calling such an operation does not reassign the binding.
A local may be declared before initialization:
let user;
if cached {
user = cached_user;
} else {
user = load_user()?;
}
return user;The compiler must prove that every read occurs after initialization.
An immutable delayed binding may be initialized exactly once on each reachable path. A mutable delayed binding may be assigned again after initialization.
The type may be written explicitly:
let user: User;When omitted, all local constraints must determine one unambiguous type.
There is no implicit zero value or runtime initialized flag.
Parameters are immutable bindings by default:
fn process(request: Request) Response {
// ...
}mut permits local reassignment and ordinary nested value mutation:
fn increment(mut counter: Counter) Counter {
counter.value += 1;
return counter;
}Parameter mutation does not structurally mutate the caller's ordinary value. Capability-bearing fields retain their own identity semantics.
const declares an immutable compile-time value:
package server
const DEFAULT_PORT = 8080;
pub const MAX_REQUEST_SIZE: uint64 = 1_048_576;
fn configured_port() int {
let port = DEFAULT_PORT;
return port;
}Constants may appear only at package scope. A constant written at the top level of any source file belongs to the entire directory package, not to that individual file. Every source file in the package may name it without an import, independently of file or declaration order.
A constant is package-private by default. pub const makes it visible to
importing packages.
Constants participate in the package declaration namespace. They cannot duplicate another package declaration or use a protected compiler-predeclared name.
Declaring const inside a function, anonymous function, loop, conditional,
match arm, arena, recovery block, or any other executable or nested lexical
scope is a compile-time error. Function-local immutable values use let.
Conversely, let cannot appear at package scope.
A constant expression may use:
- primitive scalar, string, and
unitliterals; - payload-free enum variants;
- other constants;
- grouping;
- supported primitive unary operators; and
- supported primitive arithmetic, comparison, Boolean, bitwise, shift, and string-concatenation operators.
It cannot call a user function or method, allocate a collection or aggregate, construct a payload-bearing enum or resource, use control flow, access a field or index, spawn, suspend, panic, perform I/O, or use runtime data.
The final compile-time result is stored as the constant value. Constant arithmetic must be valid and representable; invalid operations are compile-time errors.
The constant's type is inferred unless an explicit : Type annotation selects
or documents it. Every constant has one static type. Constants cannot contain
structs, arrays, maps, sets, resources, or other composed object graphs.
Constants are evaluated during checking. The language does not define
const fn, runtime global initialization, a stable address, native static
storage, or another observable storage strategy for a constant.
Ordinary structs, enums, arrays, maps, sets, and closures are structural values. Assignment, parameter passing, return, and capture produce logically independent ordinary structures.
let original = Counter { value: 0 };
let mut second = original;
second.value += 1;
// original.value is still 0.The implementation may use copy-on-write storage, persistent structures, shared immutable backing, copy elision, or in-place optimization when later mutation cannot reveal unwanted aliasing.
Capability-bearing elements preserve their own semantics transitively. Copying a
struct that contains a channel endpoint, Shared<T>, native handle, or resource
does not duplicate the underlying capability.
A resource is not a structural value. Assignment, parameter passing, return, storage, capture, and protocol erasure create handles to the same lifecycle identity.
Mutation through a resource identity is visible through its aliases. Lifetime claims prevent a valid alias from becoming dangling.
Closures capture:
- immutable ordinary values by snapshot;
- safely shareable handles by protected identity; and
- mutable locals through a same-task binding cell.
A closure that captures a mutable local is not shareable across tasks.
mut self is not an ordinary mutable local for this rule. It carries
temporary mutation authority over the caller's writable receiver place, and
that authority ends when the method call performs transactional copy-back.
Consequently, an anonymous function cannot directly capture self from a
mut self method. This is rejected whether the closure reads or mutates
self, escapes, is passed onward, or is immediately invoked. There is no
escape-analysis exception.
An explicit ordinary value copy makes detachment visible:
fn Counter::incrementer(mut self) fn() {
let mut detached = self;
return fn() {
detached.value += 1;
};
}The closure retains detached through a same-task binding cell. Calls to it do
not later mutate the caller's receiver place. let snapshot = self; similarly
creates an immutable ordinary value for snapshot capture. Capability-bearing
fields retain their existing identity semantics through these copies.
Concurrent-callback requirements are inferred and propagated through function and method types. Framework authors do not write a manual “thread-safe” annotation to make a callback safe.
A named function declares every parameter and every non-unit result type:
pub fn create_user(
request: CreateUserRequest,
database: DatabasePool,
) Result<User, CreateUserError> {
// ...
}The result type follows the parameter list directly. ProScript does not use
-> or a result colon. Omitting the result type declares unit.
Parameters use name: Type; a mutable parameter uses mut name: Type.
Multiline parameter lists are formatted with a trailing comma.
Named functions require explicit return for returned values:
fn load() Result<Config, LoadError> {
return Ok(config);
}A final expression in a named function is not an implicit return.
An anonymous function omits only the function name:
fn(request: Request) Response {
return handle(request);
}When an immediate expected callable type uniquely determines parameter types, anonymous parameter annotations may be omitted:
router.post("/users", fn(request) Response {
return handle(request);
});Without such an expected type, each parameter type is required. The result type
is never inferred from context; omission declares unit.
Anonymous functions use explicit return just like named functions.
Type-owned callables are declared at package scope:
pub fn UserId::parse(raw: string) Result<UserId, InvalidUserId> {
// ...
}
pub fn User::display_name(self) string {
return self.name;
}
fn Counter::increment(mut self) {
self.value += 1;
return;
}An associated function has no receiver and is called with Type::name(...).
A method's first parameter is exactly self or mut self.
self permits observation and identity-safe capability operations.
mut self permits ordinary nested mutation and complete replacement of the
receiver:
fn Counter::reset(mut self) {
self = Counter { value: 0 };
return;
}Replacement follows ordinary assignment and cleanup rules and is visible through the caller's writable place.
A read-only self method may be called on an immutable binding or temporary.
A mut self method requires an existing writable place; the compiler does not
create a hidden mutable temporary.
Receiver fields and methods are always selected explicitly through self.
There is no implicit unqualified receiver-member lookup.
Structs, enums, open and opaque nominal types, and resources may own methods and associated functions. An alias cannot.
Only the package that declares a type may declare its methods, associated functions, or conformances.
Named functions, associated functions, and methods cannot be used as values. Wrap one in an anonymous function when a callable value is required:
let parser: fn(string) Result<UserId, InvalidUserId> =
fn(raw: string) Result<UserId, InvalidUserId> {
return UserId::parse(raw);
};The name new has no compiler-designated constructor meaning. It is an ordinary
associated-function name and follows the same rules as every other name.
An enum variant and any callable owned by that enum cannot share an exact name:
enum State {
Ready,
}
fn State::Ready() State { // compile-time error
return State::Ready;
}The restriction applies equally to associated functions, read-only methods,
and mut self methods. It is a declaration-time package-wide rule, not
call-site overload resolution. No alternate syntax or precedence chooses one
declaration over the other.
Generic parameters use angle brackets:
struct Cache<Key, Value> {
entries: Map<Key, Value>,
}
fn identity<Value>(value: Value) Value {
return value;
}A bound follows a parameter name with :. Multiple bounds use +:
fn render<Value: Displayable + Debug>(value: Value) string {
return value.display();
}Bound order is insignificant. Duplicate bounds and method-name collisions between composed protocols are compile-time errors.
ProScript has no where clauses, specialization, conditional conformance, or
bounded-method extension syntax.
Explicit generic arguments use angle brackets:
decode<User>(body)
json.decode<Array<User>>(body)
UserRepository::load<User>(database)
request.json<CreateUserPayload>()After a direct callable head, <...> is parsed as generic arguments when it
contains a valid nonempty type-argument list and the closing > is followed by
(. Otherwise < and > are comparisons.
Parsing does not consult name resolution or capitalization. Explicit generic application is not available on arbitrary runtime callable values.
Nested generic closers may be adjacent:
Map<string, Array<Option<User>>>When the complete type-argument list is omitted, a direct generic package function, associated function, or method call infers callable-local generic parameters only by structurally matching declared parameter types against the static types of explicit arguments:
fn first<Value>(values: Array<Value>) Value {
return values[0];
}
let user = first(users); // Value = UserConstraints from all arguments must agree. Matching descends through generic type constructors and callable signatures, expands aliases, preserves nominal identity, and inserts no conversion. Ordinary numeric-literal defaulting and contextual representability participate. An anonymous function parameter may be contextually typed after other arguments determine its callable parameter type.
Inference never uses the expected result type, assignment or return destination, implementation or anonymous-function body, later uses, runtime values, or dynamic dispatch. Every generic parameter must have one complete bound-satisfying solution; otherwise the call must provide the complete explicit list:
fn unavailable<Value>() Value {
panic("value unavailable");
}
let user = unavailable<User>(); // explicit: no argument determines ValuePartial explicit type-argument lists are not supported.
This call-site inference applies only to callable-local generic parameters. It does not infer owner arguments for generic struct construction or enum variant construction and value selection.
A method or associated function owned by a generic type repeats the owner's declared parameter names:
fn Cache<Key, Value>::get(self, key: Key) Option<Value> {
// ...
}Owner bounds are inherited. Callable-local generic parameters follow the callable name:
fn Cache<Key, Value>::map<Output: Debug>(
self,
transform: fn(Value) Output,
) Array<Output> {
// ...
}A generic conformance uses the same owner head:
conform PageIterator<Element>: Iterator<Element>;Type-owned generic-call inference occurs in this order:
- Determine and normalize the complete owner instantiation from the written owner vector for an associated-function call or the receiver's exact type for a method call.
- Substitute those owner arguments through the callable signature and inherited owner bounds.
- Infer only callable-local parameters from explicit ordinary arguments under the bounded call-inference rule, or accept one complete explicit callable-local vector.
- Validate every substituted owner and callable-local bound.
The receiver provides no additional callable-local inference evidence. Owner parameters are never re-inferred from ordinary arguments. Result expectations, destinations, bodies, later uses, runtime values, and partial explicit vectors provide no inference evidence. Transparent aliases normalize before this process and cannot create a second owner instantiation.
Conformance must apply uniformly for every permitted owner instantiation.
A protocol declares required instance-method signatures:
pub protocol Displayable {
fn display(self) string,
}
pub protocol Iterator<Element> {
fn next(mut self) Option<Element>,
}Requirements:
- have no body;
- begin with
selformut self; - declare every additional parameter type;
- use the ordinary direct result notation; and
- are comma-separated.
Protocols cannot declare fields, constants, associated functions, or default method bodies.
Conformance is declared separately in the conforming type's package:
conform User: Displayable;
fn User::display(self) string {
return self.name;
}The compiler checks exact method names, receiver mutability, parameters, and
results. Matching methods without conform do not create structural
conformance.
A type may conform at most once to a protocol family. For a generic protocol, each instantiation is distinct, but a type may select only one instantiation of that family:
conform Pager: Iterator<Page>;Retroactive conformance for imported types is not permitted.
A conformance declaration has no visibility modifier. It is externally usable when both the conforming type and protocol are externally nameable. A method that satisfies a public protocol may remain package-private for direct lookup; dispatch through the public protocol value remains available.
A generic parameter constrained by a protocol exposes exactly that protocol's methods. Conformance is checked at each instantiation.
Protocol values provide dynamic dispatch. Generic bounds provide statically specializable dispatch and preserve the concrete type.
The standard iteration protocols are:
protocol Iterator<Element> {
fn next(mut self) Option<Element>,
}
protocol Iterable<Element> {
fn iterate(self) Iterator<Element>,
}A for ... in source may conform to either. An Iterable first produces an
iterator. An Iterator is used directly through a compiler-managed mutable
local.
Value-semantic iterable sources naturally iterate a snapshot copy. A capability-backed iterable defines its own documented live behavior.
Expressions evaluate from left to right in written source order unless a specific construct states otherwise.
This includes:
- callable targets and receivers;
- call arguments;
- struct fields in construction order;
- enum payloads;
- array, map, and set elements;
- select operands before arbitration; and
- interpolation slots.
An implementation may reorder only when errors, panics, suspension, allocation, resource acquisition, cleanup, and every other observable effect remain unchanged.
Parentheses group one expression:
(a + b) * cProScript has no tuple expression. (value,) and comma-separated parenthesized
values are invalid.
Calls use parentheses:
load_user(id)
database.users.insert(user)
UserId::parse(raw)Postfix operations bind more tightly than unary and binary operators. They include:
- calls;
- field and method selection with
.; - type and package qualification with
::or package.; - array indexing; and
- error propagation with
?.
Named call arguments do not exist. Arguments are positional.
From tightest to loosest:
- postfix operations;
- unary
-,!, and^; *,/,%,<<,>>,&, and&^;+,-,|, and^;==,!=,<,<=,>, and>=;&&;||; and- finite range delimiters
..and..=.
Ordinary binary operators are left-associative. A range delimiter is non-associative, so one range expression cannot be an endpoint of another even when parenthesized. Each endpoint is otherwise a complete non-range expression:
start + 1..limit * 2 // (start + 1)..(limit * 2)
0..3..5 // syntax errorAssignment is a statement and has no expression precedence.
&& and || require bool operands and short-circuit from left to right.
There is no truthiness.
Comparison chaining parses left-associatively and is normally rejected by typing:
a < b < c // parses as (a < b) < cInteger operators require operands of the same integer type unless an explicit conversion is written.
Runtime +, -, *, unary negation, division overflow, and their compound
forms panic when the exact result is not representable. Behavior does not vary
between debug and release modes.
The expression-position negative-literal rule checks the final mathematical
value of -literal, including grouping around only the literal, before
materializing its signed integer value. A further outer negation is an ordinary
runtime operation and follows this overflow rule. Package constants instead
evaluate that operation before execution and reject overflow statically.
Integer division truncates toward zero. Remainder has the dividend's sign. Division or remainder by zero panics.
Shift counts must be nonnegative and representable. A shift producing bits outside the fixed-width destination never introduces arbitrary precision. The right operand may have any integer type. A negative constant count is a compile-time error and a negative runtime count panics. Nonnegative counts have no upper bound and a count at least as large as the left type's width does not itself panic. Left shift truncates to the fixed width; right shift is arithmetic for signed left operands and logical for unsigned left operands.
Bitwise operators are &, |, ^, &^, <<, and >>. Prefix ^ is
bitwise complement.
For a signed minimum value, division by -1 panics because the quotient is not
representable. Remainder by -1 is zero.
Named standard-library operations provide deliberate wrapping, checked, saturating, and other arithmetic policies.
float32 and float64 follow IEEE 754.
Runtime operations preserve NaN, infinities, and signed zero. Runtime floating-point division by zero produces IEEE results rather than a panic.
NaN compares unequal to every value, including itself. Ordered comparisons with NaN are false. Positive and negative zero compare equal.
Primitive floating operations round each source-level result independently. An optimizer cannot silently fuse multiplication and addition. A named fused operation may request single-rounding behavior.
% is not defined for floats.
Constant division by zero is a compile-time error for both integer and floating constants.
Primitive conversion uses call notation:
let wider = int64(value);
let count = int(float_value);There are no implicit mixed numeric operations.
Integer-to-integer conversion succeeds only when the value is representable in the destination; otherwise it panics. It never silently truncates or wraps.
Integer-to-float and float-to-integer conversions follow their target representability and rounding rules. Converting NaN, infinity, or an out-of-range float to an integer panics.
Narrowing a finite float64 beyond the float32 finite range produces signed
infinity according to IEEE 754.
Validation-oriented code uses named checked operations returning typed results.
string + string produces a new logical string:
let path = "/users/" + user_id_text;No operand is implicitly stringified. All other uses of + with strings are
compile-time errors.
An unparenthesized top-level struct construction is not permitted directly in
an if, if let, match, condition-form for, or iterator-for header.
Parenthesize it or place it inside another delimiter:
if (AccessCheck {
user: user,
route: route,
}).allowed() {
authorize();
}
if authorize(AccessCheck {
user: user,
route: route,
}) {
process();
}The rule is syntactic and does not use type lookup.
An expression-owned block obtains its value from its final expression without a semicolon:
let response = match result {
Ok(user) => {
audit(user);
Response::ok(user)
},
Err(error) => handle_error(error),
};Ordinary statements inside the block retain semicolons.
Named and anonymous function bodies do not implicitly return their final expression.
An ordinary if may produce a value when it has a final else:
let status = if healthy {
"up"
} else {
"down"
};Every normally completing branch must produce the same exact static type after
ordinary contextual typing. A branch that terminates with return, break,
continue, or panic does not participate in the type join.
Statement-form if may omit else and produces unit.
ProScript has no ternary operator.
match evaluates one scrutinee once and selects the first matching arm:
let response = match result {
Ok(user) => Response::json(Status::Created, user),
Err(CreateUserError::InvalidEmail) => {
Response::unprocessable_entity()
},
Err(error) => handle_error(error),
};Arms use pattern => expression and are comma-separated. A multiline match is
formatted with a trailing comma.
Every match must be exhaustive. Fully unreachable arms and alternatives produce warnings. Partial overlap is permitted, and source order resolves it.
Match guards do not exist. Use an if expression inside an arm.
Postfix ? unwraps Ok(value) or propagates Err(error):
let user = load_user(id)?;Propagation requires the enclosing function to return Result<_, E> with the
exact same error type E. ProScript performs no implicit error conversion,
variant lifting, or From-style adaptation.
Use an explicit map_error or match to adapt an error.
panic(message) produces a structured panic:
panic("invariant violated")The argument is a string or an existing Panic value. Arbitrary panic payloads
do not exist.
Passing an existing Panic re-propagates it while preserving its original
origin and stack information and recording the new propagation point.
Panic is terminating control flow for type checking, but ProScript has no source-visible never type.
Ordinary statements end with ;:
let user = load_user()?;
audit(user);
retries += 1;
return Ok(user);A standalone braced control-flow statement has no trailing semicolon. A braced expression embedded in an ordinary statement is terminated by the containing statement's semicolon.
Empty statements are invalid; repeated semicolons are errors.
An expression statement must have type unit. Discarding a non-unit value
requires:
_ = expression;The expression evaluates exactly once. Its resulting value follows ordinary cleanup rules.
There is no discard expression and no compound discard assignment.
A writable place consists of a writable root followed by zero or more struct field and array-index steps:
counter
user.profile.name
users[index].statusWritable roots include:
let mutbindings;- mutable parameters and receivers;
- mutable pattern bindings;
- compiler-provided mutable protected views; and
- a delayed binding during whole initialization.
Immutable bindings, constants, temporaries, call results, map keys, set elements, optional chains, and protocol values are not writable places.
Maps and sets mutate through methods.
Plain assignment is:
place = expression;Evaluation occurs in three phases:
- evaluate and validate the writable root and selectors left to right;
- evaluate the right-hand side; and
- commit once through the saved logical path.
The implementation never retains a physical managed address across right-hand side evaluation. At commit, it follows the saved selectors through the root's then-current value and revalidates array bounds. Unrelated same-root changes made by right-hand-side evaluation are preserved. If evaluation fails or panics, no assignment commit occurs.
The discard target _ is valid only with plain assignment.
Multiple assignment and assignment expressions do not exist.
ProScript supports +=, -=, *=, /=, %=, <<=, >>=, &=, |=,
^=, and &^=.
A compound assignment:
- evaluates and reads its place once;
- evaluates the right operand;
- applies the corresponding typed binary operator; and
- commits once.
String += is valid. A delayed binding and _ cannot be compound targets.
When right-operand evaluation changes the same writable root, the compound
assignment's final write updates the originally selected logical leaf and
preserves unrelated changes.
ProScript has no ++ or --.
if condition {
statements
} else if other_condition {
statements
} else {
statements
}Conditions have type bool. Parentheses are optional grouping, not required
syntax. Braces are always required.
if let tests one refutable pattern:
if let Some(user) = cache.get(key) {
greet(user);
} else if let Some(guest) = trial_session(key) {
tour(guest);
} else {
onboard();
}The scrutinee evaluates once. Pattern bindings exist only in the success block. The full match-pattern grammar is accepted.
An irrefutable pattern makes an else path unreachable and produces the
ordinary unreachable-code warning.
if let is statement-only. It does not combine with &&. ProScript has no
while let or let else.
ProScript uses one for keyword.
Condition loop:
for retries < maximum_retries {
retries += 1;
}Unconditional loop:
for {
process_next();
}Iterator loop:
for user in users {
process(user);
}The iterator pattern may be:
- one identifier;
mutplus one identifier; or- one flat irrefutable struct pattern.
The source expression evaluates once.
Built-in iterable sources are:
Array<T>, in index order;Map<K, V>, asMapEntry<K, V>values in unspecified order;Set<T>, in unspecified order;Receiver<T>, as a live competing stream ending when drained and closed; andRange<T>, as a finite ascending integer sequence.
User-defined sources implement Iterable<Element> or Iterator<Element>.
Each iteration binds an independent ordinary value. Value-semantic collections iterate a snapshot of the source. Capability-backed iterables may define live behavior.
Finite first-class integer ranges use:
let exclusive = 0..3; // 0, 1, 2
let inclusive = 0..=3; // 0, 1, 2, 3
for attempt in 0..maximum_retries {
retry(attempt);
}Both endpoints are required and must have one exact primitive integer type. An
immediately expected Range<T> contextually types both endpoints. Without that
context, an untyped literal may take the opposite endpoint's type when
representable; two untyped literals default normally to int. Differently
typed endpoints receive no implicit conversion. The exact aliases byte and
rune work through uint8 and int32; integer-backed nominals do not qualify.
Range<T> is a compiler-predeclared value-semantic type, nameable in types and
automatically iterable, with unobservable representation and no source
construction except a range expression. It has no v1 structural pattern,
automatic equality, hashing, map-key eligibility, JSON representation, or
range-specific method surface.
The start and end expressions evaluate once from left to right. Iteration
advances by one. start..end excludes the end and start..=end includes it.
Equal endpoints produce zero and one elements respectively; a start greater
than the end produces an empty range. An inclusive range ending at the maximum
value of its type yields that value and stops without overflow.
The range delimiter is non-associative and has lower precedence than every
ordinary binary operator. Open-ended ranges, descending or stepped iteration,
floating and nominal ranges, for i in integer, and bracket slicing are not
supported. Range patterns retain their separate forms and rules; in
particular, an empty range value is valid while a statically empty range
pattern is rejected. Range values are not compile-time constant-compatible.
break; exits the nearest loop. continue; begins its next iteration.
They are statements and must appear in a block when used as a match arm:
match receiver.receive() {
Some(value) => process(value),
None => {
break;
},
}Labels and value-carrying breaks do not exist. Loops do not produce values.
return; exits a unit-returning function. return expression; exits a
value-returning function.
return always exits the innermost named or anonymous function, not merely an
expression block.
Full refutable patterns appear in:
- match arms; and
if let.
Irrefutable patterns appear in:
- initialized
letdestructuring; and - iterator-loop bindings.
An identifier binds the matched value. _ discards it. A binding is immutable
unless prefixed by mut:
Ok(mut user) => {
user.login_count += 1;
persist(user)
},An underscore-prefixed identifier is a real binding whose unused status is explicitly permitted.
name @ pattern binds the entire matched subvalue and also applies the nested
pattern:
event @ BackgroundEvent::UserCreated(
User { id, .. },
) => record(event, id),mut name @ pattern makes the whole binding mutable.
Patterns may contain:
- Boolean, integer, floating-point, rune, byte, and string literals, including negative numeric literal forms;
- eligible compile-time constants;
- payload-free enum variants;
_;- bindings; and
- parenthesized grouping.
An unqualified identifier resolves as a constant or payload-free variant when such a value exists; otherwise it binds. A qualified path never introduces a binding.
Literal and constant patterns require an exact expected type and use that type's compiler-defined equality. A floating literal follows IEEE equality; there is no NaN literal pattern.
When the scrutinee is an exact nominal whose layered underlying chain ends in the corresponding primitive, a bare literal leaf may be contextually typed as that exact nominal. The eligible literal families are:
- integer-family literals, including
byteandrune; - string literals; and
- Boolean literals.
This contextual typing exists only in pattern position. It does not implicitly construct a nominal in an expression or broaden the constant-value domain. The literal's terminal primitive must match the nominal's terminal primitive; floating-point and non-primitive terminals are ineligible.
For an opaque nominal, a bare literal pattern is available only in the
declaring package. Other packages use exported constants of the exact nominal
type, bindings, or _. A Boolean-backed nominal has the closed coverage domain
true and false; a string-backed nominal remains open and requires a
catch-all or binding for exhaustiveness.
type UserStatus string;
type FeatureEnabled bool;
match status {
"active" => handle_active(),
_ => handle_other(),
}
match enabled {
true => enable(),
false => disable(),
}Enum payload patterns mirror construction:
DatabaseError::UniqueViolation("email")
DatabaseError::UniqueViolation(field)
DatabaseError::UniqueViolation(_)Payload patterns are positional and may be nested.
For a generic enum, the pattern omits owner type arguments:
Outcome::Success(value)
Outcome::PendingThe scrutinee's exact static type determines the instantiation.
Struct patterns use named fields:
User {
id,
name: display_name,
..
}A bare field binds the same local name. field: pattern applies another
pattern. One final .. ignores all omitted fields. Without .., every field
must appear exactly once.
Outside the struct's declaring package, a pattern may mention only public
fields. If any private field exists, final .. is mandatory. These rules also
apply to flat let and iterator struct patterns.
Resource types do not support structural patterns.
For a generic struct, the pattern writes Box { ... }, never
Box<Type> { ... }. The scrutinee's exact static type determines the
instantiation.
Array patterns use brackets:
[]
["users", user_id]
[first, .., last]Without .., the length must match exactly. At most one .. may appear and
matches zero or more ignored elements. It cannot bind the rest.
Array patterns are full refutable patterns and are not accepted in let or
iterator bindings.
An initialized let may use one flat irrefutable struct pattern:
let RequestObservation {
request_id,
path,
..
} = observation;
let User { id, mut name, .. } = load_user()?;It permits same-name bindings, field: name renaming, per-binding mut, and
one final ...
Nested patterns, literals, enum patterns, alternation, ranges, and @ are
invalid in this position. let mut does not prefix a destructuring pattern;
mark each mutable binding.
Alternatives use |:
CreateUserError::EmptyName |
CreateUserError::InvalidEmail => reject(),A leading | is permitted; a trailing | is not. Every alternative must bind
the same names with the same types and mutability.
Alternation may appear at any nested pattern position.
@ binds more tightly than |. Group alternatives when one whole-value
binding covers them.
Integer-family, byte, and rune range patterns are:
start..end
start..=end
start..
..end
..=end.. excludes the upper bound; ..= includes it. A standalone .. is invalid
as a complete arm pattern.
Bounds may be:
- integer literals;
- rune literals;
- a negative integer literal;
- an eligible constant; or
- a contextually typed literal for an eligible nominal scrutinee.
Calls and general expressions are invalid as bounds.
The expression-position allowance for grouping inside a negative integer
literal does not apply here. A negative range bound remains directly - plus
an integer literal; a parenthesized bound is invalid.
Empty ranges are compile-time errors. Coverage and overlap are computed over the exact finite or ordered domain of the scrutinee type.
Opaque nominal range literals are available only in the declaring package.
Nominal range patterns remain limited to integer-family terminals, including
byte and rune. The string and Boolean nominal literal-leaf rules do not
create string or Boolean ranges.
Expected failure is ordinary typed data, conventionally represented by
Result<T, E>. Error types are usually enums:
enum CreateUserError {
InvalidEmail,
DuplicateEmail,
Storage(DatabaseError),
}No universal error interface is imposed on E. Generic reporting boundaries
require Debug explicitly.
Every non-unit expression result must be consumed, returned, propagated, matched, assigned, or explicitly discarded. This prevents accidental loss of typed failures.
A panic represents a violated program, runtime, or native invariant rather than an expected domain outcome.
The immutable Panic value contains:
- a message;
- a broad
PanicKind; - a source location;
- a stack trace; and
- related panic and error context.
The runtime-owned diagnostic model is:
enum PanicKind {
Explicit,
Assertion,
Runtime,
Native,
}
struct SourceLocation {
file: string,
line: uint32,
column: uint32,
}
struct StackFrame {
function: string,
location: Option<SourceLocation>,
}
type StackTrace = Array<StackFrame>;
struct ErrorContext {
type_name: string,
propagation_location: SourceLocation,
debug_report: Option<string>,
}Application code inspects a panic through:
failure.message() // string
failure.kind() // PanicKind
failure.location() // Option<SourceLocation>
failure.stack_trace() // StackTrace
failure.related_panics() // Array<Panic>
failure.interrupted_error() // Option<ErrorContext>These return the corresponding immutable diagnostic values. Application code cannot fabricate source locations, stack traces, native panics, or related panic groups.
Runtime faults such as bounds errors, integer overflow, invalid direct conversions, and detected native contract violations become structured panics.
Runtime exhaustion that cannot preserve ordinary unwinding and cleanup, such as catastrophic allocation failure in the panic path, terminates the process immediately. Expected operating-system exhaustion remains a typed error or backpressure outcome.
Recovery uses:
let response = recover {
create_user_endpoint(request, state)
} with failure {
log.error("request handler panicked", [failure]);
Response::internal_server_error()
};The body executes in the current task. If it completes normally, the recovery expression produces its value. If it panics:
- the task unwinds to the boundary;
- every exited lexical cleanup runs;
- the immutable
Panicis bound to the handler name; and - the handler produces the recovery expression's value.
Every completing normal and recovery path must have the same exact result type
after alias normalization and ordinary contextual literal typing. A surrounding
expected type may check both paths, including by widening both to an explicitly
expected protocol type. Without that context, the expression does not infer a
common protocol supertype or insert a conversion. A return or panic path
does not constrain the join; if neither path completes, the expression is
internally noncompleting without exposing a source-level bottom type. Recovery
adds no implicit Option, Result, union, or numeric widening.
Recovery resumes after the boundary, not at the panic site. A handler panic
continues unwinding outward. A task cannot recover another task's panic.
Recovery catches only panic; it does not intercept an ordinary Err, a channel
outcome, or a losing selection branch.
An unhandled panic in any green task is process-fatal. Before termination, the panicking task performs its required unwinding and deterministic cleanup when the runtime remains capable of doing so.
Task<T> retains only normal completion values. Task failure is not silently
converted into a task-result variant. Work whose failure must be observed
returns Result explicitly.
Automatic cleanup returns unit. Expected finalization failure must be exposed
by an explicit typed operation such as commit, finish, or close.
If automatic cleanup cannot uphold its invariant, it panics.
When cleanup panics during another panic:
- the initiating panic remains primary;
- cleanup panics are attached in occurrence order;
- all remaining cleanups are attempted; and
- nested panic groups are flattened.
When cleanup panics while an Err(E) is leaving a scope, the cleanup panic
takes control. It records the interrupted error's static type and propagation
location and records an immutable Debug report when E supports Debug.
Ordinary dynamic allocations live in a precise tracing garbage-collected heap. Cycles are supported and collectible.
Programs do not manually free managed memory and do not use ordinary ownership, borrowing, or lifetime annotations.
The ProScript 0.1 production collector is precise, concurrent, and non-moving. The language nevertheless treats managed addresses as unstable and unobservable, allowing future collectors to move objects without changing source semantics.
The collector prioritizes low and reasonably predictable pause times for concurrent servers over maximum collection throughput. This is a performance objective, not a hard real-time pause bound.
Ordinary source cannot obtain or retain a raw managed address. Native code uses opaque handles, exact stable-payload leases, runtime-owned output reservations, or transparent copied fallback storage under P-357.
Garbage-collection timing never performs correctness-critical non-memory resource cleanup.
An arena is a scoped allocation context:
arena {
let response = build_large_response();
send(response);
}An optional name may identify an arena for diagnostics or advanced APIs when such an API requires it:
arena scratch {
build_index();
}Eligible allocations made directly or by ordinary called and transparently suspending functions use the innermost active arena automatically. Allocator parameters do not propagate through ordinary APIs.
Nested arenas are permitted.
An arena-backed value may safely escape:
fn create_user() User {
let out;
arena scratch {
let user = build_user();
out = user;
}
return out;
}The runtime promotes the reachable escaped graph to the nearest longer-lived parent arena or to the managed heap. It may allocate a predictably escaping value directly in its eventual region.
Promotion preserves language-level identity and updates aliases. Placement and movement are unobservable except through documented performance diagnostics.
There is one spawn behavior. Spawned work may outlive the current arena.
Captured arena-backed graphs are selectively promoted before the task becomes
live. The arena is not retained and does not wait merely to preserve captured
memory.
The arena allocation capability itself cannot escape into spawned work.
resource applies to domain-level lifecycle responsibilities as well as files,
sockets, and database handles. It represents identity-bearing temporary
authority, reservation, or responsibility whose unfinished lifecycle requires
deterministic fallback work. All aliases observe the same lifecycle identity.
Examples include inventory reservations, payment authorization holds, distributed job leases, idempotency claims, quota or concurrency permits, and provisioned external accounts that require compensation if activation never completes.
Ordinary durable business data remains value-semantic. Types such as User,
Order, Invoice, and Money remain structs or enums merely because they are
mutable or persisted. A resource is appropriate when identity, automatic
lifetime registration, exactly-once cleanup invocation, and mandatory fallback
work are all part of the abstraction's correctness contract.
The usual design exposes an explicit typed success operation and reserves cleanup for the unfinished path:
resource InventoryReservation {
inventory: InventoryService,
reservation_id: ReservationId,
state: ReservationState,
cleanup(mut self) {
if self.state == ReservationState::Active {
self.inventory.release_for_cleanup(self.reservation_id);
}
}
}
pub fn InventoryReservation::commit(
mut self,
) Result<OrderId, InventoryError> {
let order_id = self.inventory.commit_reservation(
self.reservation_id,
)?;
self.state = ReservationState::Committed;
return Ok(order_id);
}commit reports expected business failure as typed data. Cleanup observes the
protocol state and releases only an unfinished reservation. It is not a hidden
success path, general-purpose defer, or distributed transaction system.
Cleanup returns unit and cannot propagate expected failure with ?. Resource
implementations must handle cleanup failure explicitly, delegate to a
unit-returning cleanup-safe protocol, or panic when they cannot uphold the
automatic invariant. External compensation still requires ordinary idempotency,
timeouts, retry, and reconciliation. Cleanup is guaranteed during orderly exit,
error propagation, and panic unwinding in a running process, not after abrupt
process or machine termination.
An identity-bearing resource is declared explicitly:
pub resource DatabaseTransaction {
connection: DatabaseConnection,
state: TransactionState,
cleanup(mut self) {
if self.state == TransactionState::Active {
self.rollback_for_cleanup();
}
}
}General form:
resource Name<Parameters> {
field: Type,
cleanup(mut self) {
statements
}
}A resource may have zero or more fields and may be generic or recursively shaped under the ordinary recursive-type rules.
Every resource has exactly one cleanup(mut self) clause. It:
- follows every field and is the final body item;
- has no
fn, visibility, additional parameter, generics, or result type; - returns
unit; - may suspend;
- is invoked only by the runtime;
- cannot be called, referenced, selected, overridden, or used as protocol conformance; and
- is not part of the ordinary member namespace.
Ordinary methods and associated functions remain package-scope declarations:
pub fn DatabaseTransaction::commit(
mut self,
) Result<unit, DatabaseError> {
// ...
}pub resource exports the type but not its representation. Fields and direct
named-field construction remain restricted to the declaring package.
Importing packages obtain resources through public associated functions and use them through public methods.
Resource declarations and fields do not accept structural-provider directives. Resources do not support struct patterns.
The cleanup receiver is a non-escaping finalizing view. Inside cleanup, whole
self may be used only as a field-access base or method receiver.
Cleanup cannot:
- replace
self; - pass whole
selfas an argument; - return or store it;
- capture it in a closure or spawned task;
- send it through a channel; or
- establish another lifetime-domain claim.
The runtime rejects indirect claim creation after cleanup begins.
Resources use ordinary let and let mut. There is no resource-specific
binding:
let file = File::open(path)?;Every newly created resource, including an unnamed temporary, registers automatically with the nearest lexical lifetime domain.
Passing, returning, storing, capturing, or widening a resource automatically establishes the destination domain's claim before the source domain can release its own. This is identity-preserving sharing, not ownership transfer, and does not invalidate the source binding.
Cleanup runs:
- on normal block completion;
- before an early return;
- during typed error propagation;
- during panic unwinding; and
- before arena memory reclamation.
Cleanups execute in reverse registration order. A loop iteration is a lexical cleanup domain, so iteration-local resources do not accumulate until the surrounding function returns.
Cleanup may transparently suspend the current green task. Remaining cleanups wait and run sequentially in order. A task is not complete until its cleanup finishes.
Multi-part expressions evaluate left to right. Successfully initialized resource-bearing components register immediately in a hidden construction domain until the complete value exists.
If a later field, argument, or collection element fails or panics, earlier components clean in reverse order. On success, their claims transfer atomically to the completed destination.
Partially initialized outer values are never observable.
An ordinary aggregate containing a resource automatically retains the required claims. This behavior is recursive through structs, enums, collections, closures, channels, tasks, and protocol values.
The aggregate remains a structural value, while contained resource handles retain their identities.
Removing or replacing a resource-bearing value does not perform hidden suspending cleanup during the mutation. The nearest lexical cleanup domain adopts the displaced value and releases it at that scope's ordered cleanup phase.
Resource claims belong to external lifetime domains rather than every internal reference edge. Domains include lexical scopes, tasks, aggregates, channels, and runtime roots.
Internal graph edges determine reachability but do not independently keep a resource alive. A rootless cyclic graph cannot defer deterministic cleanup until an unrelated garbage-collection cycle.
Each resource has the runtime cleanup state:
CleanupPending
-> Cleaning
-> Cleaned
-> CleanupFailed(Panic)
Final applicable claim release atomically begins cleanup. Cleanup is invoked
exactly once and is never retried automatically. No new claim can be created
after Cleaning begins.
This runtime lifecycle is distinct from resource-defined protocol states such
as Open, Committed, RolledBack, or Closed.
Explicit methods update ordinary private protocol state and return typed outcomes. The eventual cleanup invocation observes that state and performs only remaining fallback work:
commit succeeds
-> protocol state becomes Committed
-> final claim release invokes cleanup
-> cleanup observes Committed and performs no rollback
The language guarantees safe lifetime and once-only cleanup invocation. A resource implementation remains responsible for correctly coordinating its external protocol.
Resources do not automatically support:
- equality or ordering;
- hashing or map-key eligibility;
- structural
Debug; - structural JSON encoding or decoding; or
- cross-task sharing.
A user resource is task-confined unless the compiler derives safe shareability from recognized concurrency primitives and the complete exposed operation set. Safe source has no annotation that overrides the derivation.
Resources may explicitly conform to ordinary user protocols. Widening to a protocol value preserves resource identity and the receiving domain's claim.
ProScript has no general defer, user-visible garbage-collector finalizer,
manual drop, use binding, or public weak reference.
ProScript functions are ordinary functions. A suspending operation suspends only the current green task and does not block an operating-system worker.
There is no async fn, standalone await, public future, or promise type.
spawn starts one independent green task and returns Task<T>:
let task = spawn serve(router);The operand must be call-shaped, with its final postfix operation being a function, method, associated-function, or callable-value invocation.
The spawning task evaluates the callable target, receiver, and arguments once from left to right. If evaluation fails or panics, no task is created.
Before the task becomes runnable, the runtime:
- snapshots ordinary captured data;
- promotes escaping arena graphs;
- establishes claims on safely shareable resources; and
- rejects non-shareable identities.
Only the selected callable body runs in the new task.
A spawned call cannot require writable authority over a caller-owned receiver
place. Calls to mut self methods and compiler-defined writable-place
collection operations are therefore rejected, including through nested
receiver places:
spawn users.set(user.id, user); // rejected
spawn values.push(value); // rejected
spawn values.pop(); // rejected
spawn counter.increment(); // rejected when increment uses mut self
spawn groups[index].push(value); // rejectedThe child neither mutates and discards a receiver snapshot nor copies a value
back when it completes or is awaited. Read-only calls may operate on eligible
ordinary snapshots. An ordinary function with a mut parameter may also be
spawned because that parameter receives an independent value; returning and
awaiting an updated value is the explicit ordinary-value path.
The rejection is about caller-place authority, not state change generally.
Operations on safely shareable capabilities and protected handles remain legal
when their receiver does not require a writable caller place. This includes
Shared<T>.lock, channel operations, and concurrency-safe resource or native
handle methods:
let task = spawn registry.lock(fn(mut users) {
users.set(user.id, user);
});Here the callback receives an exclusive protected view inside the child; it
does not acquire writable authority over the parent's registry binding.
A task handle is a must-use value. Explicitly discard it to detach observation:
_ = spawn emit_metrics(snapshot);Discarding the handle does not cancel the task or change its resource lifetime. The spawned task is not lexically joined to its parent and may outlive the parent scope.
Wait for task completion with:
let result = task.await();await is a method name, not a standalone keyword. It transparently suspends
the current green task.
Task<T> retains one normal completion value. Multiple concurrent and repeated
awaits observe logically independent ordinary copies of that same value while
capabilities retain their semantics.
The result becomes observable only after the task's lexical cleanup finishes. The task domain retains resource claims reachable from its completion value until observation transfers the necessary claims or the retained result is released.
Tasks have no cancel, close, or general supervisor operation. Programs stop
work through channels, typed messages, deadlines, normal return, or process
termination.
A task boundary accepts:
- ordinary value-semantic data, copied logically; and
- safely shareable handles, preserving protected identity.
Every other identity-bearing value is rejected statically. There is no affine ownership transfer that invalidates the source binding.
Safe ProScript prevents:
- managed-memory data races;
- dangling or post-cleanup resource access;
- simultaneous exclusive protected views; and
- escape of protected state.
It does not guarantee freedom from deadlock, starvation caused by program protocols, business-logic races, legal-operation ordering mistakes, or external distributed races.
Shareability is derived from compiler-recognized safe primitives, including:
- channel endpoints and owner-task client handles;
Shared<T>;- atomic values with defined operations;
- audited runtime and native handles; and
- ordinary value data.
Wrapping a non-shareable identity does not conceal it. Safe source cannot assert shareability through an unchecked annotation or marker protocol.
Typed channels expose shareable Sender<T> and Receiver<T> endpoints.
Ordinary channels are multi-producer, multi-consumer: each accepted value is
received exactly once by one competing receiver. Broadcast is a distinct
library abstraction.
The default channel is unbuffered. A bounded channel requires an explicit nonnegative capacity. An unbounded channel uses an explicitly named constructor.
Normal send:
sender.send(value) // Result<unit, SendError<T>>It suspends while an unbuffered receiver or bounded capacity is unavailable. It never silently drops due to pressure.
Immediate send:
sender.try_send(value) // Result<unit, TrySendError<T>>The outcomes are:
enum SendError<T> {
Closed(T),
}
enum TrySendError<T> {
Full(T),
Closed(T),
}Every failed send returns the unsent value.
Suspending receive:
receiver.receive() // Option<T>None means the channel is closed and drained. An open empty channel suspends.
Immediate receive:
enum TryReceive<T> {
Message(T),
Empty,
Closed,
}Dropping the final sender-domain claim closes the channel automatically. New sends fail; already accepted values drain before receivers observe permanent closure.
Dropping the final receiver-domain claim permanently disconnects the receiving side. Pending and future sends fail with their unsent values. Buffered values that can no longer be received are discarded and their resource claims are released through ordinary cleanup.
Ordinary endpoints cannot globally close a shared channel. A separate shareable administrative control capability performs idempotent graceful close without retaining endpoint liveness.
Channels have no abort operation.
Timeouts for channel operations compose through select; channel endpoints do
not define a separate timeout or deadline call family.
Sequential sends by one producer preserve FIFO acceptance order. Concurrent producers have no predetermined relative order.
Accepted values leave the channel in acceptance order, although competing consumer tasks may complete processing out of order.
Selection among waiting senders or receivers resists systematic starvation but does not promise FIFO waiter service or a bounded waiting time.
When a channel accepts a value, it atomically retains claims for reachable safely shareable resources. Receiving transfers those claims without a protection gap. A failed or losing send creates no channel claim. Non-shareable resources cannot be sent.
select waits on several typed operations and commits exactly one:
let outcome = select {
case message = receiver.receive() => handle_message(message),
case result = worker.await() => handle_result(result),
case _ = sender.send(value) => sent(),
case timeout(deadline) => timed_out(),
default => not_ready(),
};Selectable operations are:
- channel send;
- channel receive;
- task await;
timeout(duration); and- privileged runtime or native wait operations with the same atomic contract.
Every branch operand evaluates exactly once from top to bottom before readiness
arbitration, including when default eventually wins.
A case result target is one immutable identifier or _. A non-unit result must
be bound or explicitly discarded.
If no operation is ready and there is no default, the task suspends. At most
one default is permitted, and it must be final.
When several operations are ready, one wins nondeterministically. Source order provides no priority. Repeated arbitration must resist systematic starvation, but the language promises no bounded wait or specific queue algorithm.
A losing branch withdraws its registration without committing:
- no message is consumed;
- no send is accepted;
- no losing timer remains; and
- a losing task await does not cancel the task.
Branches form an expression and must produce compatible types, like match.
Cases and default are comma-separated, with the ordinary optional
trailing-comma rule.
std.time.Duration is nonnegative. A zero timeout is immediately eligible.
Negative duration construction fails according to the duration API.
Shared<T> is a safely shareable handle providing scoped access:
let metrics = Shared<Metrics>::new(initial_metrics);
metrics.lock(fn(mut state) {
state.requests += 1;
return;
});T must be recursively ordinary value-semantic state and cannot contain a
resource. Shared<T> does not make a task-confined resource shareable.
The callback parameter selects the acquisition mode:
fn(state)acquires shared read access;fn(mut state)acquires exclusive access; and- a zero-parameter
Shared::gate()callback acquires exclusively.
The protected view cannot escape. Access remains held across transparent suspension and releases on every normal, error, or panic exit.
Shared<T> is non-reentrant for the same identity. It supports no mode upgrade,
public guard, manual unlock, poisoning, or try_lock.
Re-entry is rejected statically when provable and otherwise produces a structured runtime panic. Waiter selection resists systematic starvation but promises neither FIFO service nor a bounded waiting time.
A coordination-only gate is:
let migration_gate = Shared::gate();
migration_gate.lock(fn() Result<unit, MigrationError> {
return migrate(database, cache);
});Shared<T> protects ordinary in-memory state. It is not a wrapper that makes a
task-confined resource shareable.
A task-confined resource cannot cross a task boundary, be sent through a channel, or be captured by a concurrently invoked callback.
A resource is safely shareable only when the compiler can derive that property from recognized protected primitives across its complete exposed operation set. Such a resource commonly uses internally synchronized state or delegates operations to an owner task. Concurrent calls preserve the resource's typed protocol and cannot create a managed-memory data race.
Safe source cannot declare an unchecked shareability annotation. Binding mutability does not grant or remove concurrent authority.
The compiler recognizes a closed set of structural providers and configuration families:
Debug;json::Encode;json::Decode;- the shared JSON configuration family
json; - the shared
templateconfiguration family.
Eligible user-defined types obtain provider capabilities through static
structural analysis. Configuration families contribute only statically
validated metadata; they do not create runtime protocols or conformances. A
declaration does not contain a derives list. The compiler may synthesize
specialized code, but it does not use Any, runtime reflection, runtime
protocol discovery, or runtime field enumeration.
Structural eligibility is recursive. A participating field must support the provider unless a valid provider policy causes the generated operation not to visit that field. Generic type eligibility is conditional on the required capabilities of its type arguments.
External packages cannot define new structural-provider paths, compiler generators, or field-directive vocabularies. They may define and explicitly implement ordinary protocols.
A field directive group follows the field type and precedes the field comma:
struct User {
id: Uuid #[json::Encode(skip)],
name: string #[json(name: "displayName")],
password_hash: string #[
Debug(redact),
json::Encode(skip),
],
}Field-level template metadata is also permitted through the same directive shape:
struct UserExtra {
id: Uuid #[template(skip)],
name: string #[template(name: "displayName")],
password_hash: string #[
Debug(redact),
template(skip),
],
}An enum-variant directive group follows the variant name and any payload:
enum UserStatus {
Pending #[json(name: "pending")],
Active,
Suspended #[json(name: "suspended")],
}A whole-type group follows the type name and generic parameters and precedes the body:
struct Credentials #[
Debug(deny),
json::Encode(deny),
] {
username: string,
password: string,
}There is at most one group at each attachment point. Directives are comma-separated. A trailing comma is accepted in any nonempty group and is canonical in a multiline group.
Directive arguments belong to a closed, statically validated vocabulary. They are not arbitrary runtime expressions.
An ordinary struct or enum has structural Debug when all values visited by
its generated representation are debug-eligible. Debug(redact) replaces a
field's normal structural representation and therefore does not require the
field type itself to implement Debug.
Debug(deny) on a whole type prevents automatic synthesis even if every field
is otherwise eligible. Only the package defining the type may then provide a
deliberate explicit implementation. Denial applies only to Debug; it does not
hide fields or alter JSON behavior.
Structural directives configure generated representations; they are not
access-control or information-flow rules. Debug(redact) does not skip JSON,
JSON skipping does not redact Debug, and code that can access a sensitive
field can still display or transmit that field deliberately.
Resources do not receive structural Debug automatically.
Debug is distinct from ordinary interpolation. Interpolation accepts only its
closed primitive rendering set and does not fall back to Debug.
The shared template configuration family is a closed, standard-library-only
metadata channel for template-oriented projection. It does not define a
runtime protocol, enable runtime reflection, or permit user-defined template
directive vocabularies.
The permitted field-level options are:
template(name: "field_name")to use an explicit template binding name.template(skip)to omit a field from template projection.
struct UserExtra {
id: Uuid #[template(skip)],
name: string #[template(name: "displayName")],
password_hash: string #[Debug(redact), template(skip)],
}template(name: "field_name") is the template equivalent of the existing
json(name: "..."): it replaces the source field name for template-facing
rendering contracts only.
template(skip) is a template-only skip flag and does not participate in JSON
behavior.
The compiler validates the directive path and every option at compile time. Template metadata may be emitted through the ordinary structured-metadata mechanism used by the standard library. It does not change interpolation or formatter behavior and introduces no format specifiers or runtime-dispatched template hooks.
Structural JSON encoding and decoding are independent:
- an eligible struct may satisfy root
json::Encode; - an eligible struct may independently satisfy root
json::Decode; - a user enum can participate recursively only after traversal has crossed a participating struct field; and
json::Valueis a standard-library-owned root JSON value with explicit encoding and decoding.
Encoding capability does not imply decoding capability, or conversely.
json::Encode(deny) and json::Decode(deny) prevent automatic synthesis for
their respective directions. Only the defining package may replace a denial
with a deliberate manual implementation.
Structural JSON is intended for data-transfer shapes. A domain type whose construction enforces invariants may deny decoding and use an explicit input DTO or a manual decoder.
The following types have no automatic structural JSON representation:
unit;Result<T, E>;- directly nested
Option<Option<T>>; - resources;
- functions, tasks, channels, and synchronized handles except where a standard-library type explicitly owns a representation; and
- opaque nominal types without a deliberate implementation by their defining package.
A field skipped by a provider does not need to qualify for that provider.
The default JSON member name is the field's exact source name:
struct User {
created_at: Timestamp,
}The corresponding member is "created_at". ProScript performs no automatic
case conversion, case-insensitive matching, or Unicode normalization.
json(name: "wireName") replaces the name in both encoding and decoding:
created_at: Timestamp #[json(name: "createdAt")],The source name is not retained as an alias. Direction-specific names and multiple aliases do not exist. Different input and output contracts use different types.
json::Encode(skip) omits a field during structural encoding. It does not
alter decoding.
An ordinary non-optional participating field is required during decoding. A
missing member produces a typed JsonError; the decoder does not invent zero,
false, empty, or other implicit values.
A missing Option<T> member decodes as None. A present JSON null also
decodes as None. A non-null value decodes as Some(value).
Encoding an Option<T> emits the field by default:
Some(value)emits the encoded value; andNoneemits JSONnull.
json::Encode(omit_none) may appear only on an Option<T> field. It omits the
member for None and emits every Some(value), including zero-like or empty
values. There is no general omitempty or omit_zero policy.
An ordinary field may declare a missing-member default:
struct CreateUserRequest {
send_welcome_email: bool #[
json::Decode(default: true),
],
}The default is used only when the member is absent. A present null, false
value, or wrong-typed value is decoded normally and is not mistaken for
absence.
The default must be a compile-time constant-compatible flat value of the field
type. It cannot call a function or method, read runtime state, use control
flow, access a field or index, or construct a composed value. A decode default
is invalid on Option<T> and json::Field<T>.
Unknown members are rejected by default. A type can deliberately allow them:
struct ForwardCompatibleEvent #[
json::Decode(allow_unknown),
] {
event_id: string,
}This policy affects only unmapped members at that type. It does not weaken required-field, type, malformed-input, or duplicate-name validation.
Duplicate member names in one JSON object are always a typed JsonError,
including duplicate unknown names and names expressed with different escape
spellings. Duplicate detection occurs on decoded member names before field
mapping. There is no permissive override.
json::Field<T> models the three states required by partial-update contracts:
// Declared by the standard json package.
pub enum Field<T> {
Missing,
Null,
Value(T),
}For a direct participating struct field:
| JSON state | ProScript state |
|---|---|
| member absent | json::Field::Missing |
member present as null |
json::Field::Null |
member present as a valid T |
json::Field::Value(value) |
Encoding performs the inverse mapping. Missing omits the containing member,
Null emits JSON null, and Value(value) emits the value.
json::Field<T> is structurally meaningful only as the direct type of a struct
field. It is not a structural JSON root and cannot appear beneath an array,
map, set, option, enum payload, or other container. Its immediate payload type
cannot be Option<U>, because that would make Null and Value(None)
indistinguishable.
A payload-free enum variant beneath a participating struct field is a JSON string containing its exact source name:
enum UserStatus {
Pending,
Active,
}UserStatus::Active encodes as "Active". A variant-level
json(name: "...") replaces that exact name in both directions.
A payload-bearing variant uses an externally tagged one-member object:
enum BackgroundEvent {
UserCreated(User),
Range(int, int),
}{
"UserCreated": {
"id": "019c...",
"name": "Ada"
}
}A single payload is emitted directly. Multiple positional payloads are emitted as a JSON array in declaration order:
{"Range":[1,10]}Decoding requires exactly one member, an exact known variant name, and the correct payload shape. The ProScript 0.1 structural representation provides no internal tagging, adjacent tagging, untagged matching, flattening, numeric discriminants, or representation switch.
The only enum-variant JSON directive is json(name: "..."). An empty
json(), field policy, skip policy, or whole-type policy on a variant is
invalid.
A user enum cannot be the direct root of structural JSON encoding or decoding. A root container also does not establish eligibility:
Response::json(Status::Ok, UserStatus::Active); // invalid
request.json<Array<UserStatus>>(); // invalidThe contract must cross a struct field:
struct UserStatusResponse {
status: UserStatus,
}A root collection of structs remains valid when enum values occur beneath fields of those structs.
The standard library provides the sealed recursive type:
// Declared by the standard json package.
pub enum Value {
Null,
Bool(bool),
Number(Number),
String(string),
Array(Array<Value>),
Object(Map<string, Value>),
}json::Value can be encoded and decoded at the root. It contains JSON data
only; it cannot hold arbitrary ProScript values, resources, capabilities,
functions, channels, or runtime objects. Pattern matching over it is
exhaustive.
json::Number is an immutable validated JSON-number token. Decoding a dynamic
tree preserves the token's exact spelling and precision rather than converting
through float64. Encoding writes it as a JSON number, not a string.
Construction from arbitrary text is fallible. Construction from a typed
integer or finite float uses that type's canonical JSON spelling. Checked
conversions to numeric primitives return Result; no implicit numeric
conversion exists.
json::Number equality compares exact token spelling, so 1, 1.0, and
1e0 are distinct. It is not a map key or set element.
An eligible Map<K, V> encodes as a JSON object. Every allowed map-key type has
one canonical member-name representation:
stringuses its exact value;- integers use canonical base-10 text;
booluses"true"or"false";byteandrunefollow their underlying integer types; and- an open primitive-backed nominal uses the canonical representation of its ultimate primitive.
Integer key text has no leading +, no leading zeros except "0", and no
"-0". Decoding rejects noncanonical or out-of-range names.
An opaque nominal key does not automatically gain JSON map-key behavior. Floats, structs, enums, and all other non-whitelisted map keys remain invalid.
Default map-member output order is unspecified. Struct fields retain declaration order. The JSON encoder provides an explicit operation-wide deterministic mode that recursively sorts maps and sets, at additional time and memory cost. Deterministic mode is not a cryptographic canonical-JSON guarantee.
An eligible Set<T> encodes as a JSON array. Default element order is
unspecified. Decoding rejects duplicate elements rather than silently
collapsing them. Deterministic mode sorts elements using their compiler-defined
scalar ordering.
Every integer type encodes as an unquoted JSON number, including values outside JavaScript's exact integer range. Decoding parses directly into the exact target integer domain. Numeric strings, fractional forms, exponent forms, negative values for unsigned targets, and out-of-range values are rejected.
Finite float32 and float64 values encode using Go-style shortest
round-tripping formatting at the original width. Fixed notation is used for
zero and magnitudes from 1e-6 inclusive to 1e21 exclusive; lowercase
scientific notation is used outside that interval. Exponents are not
zero-padded, and negative zero is emitted as -0.
NaN and positive or negative infinity cannot be represented in JSON. Encoding
them, or decoding a number that overflows the target float, returns a typed
JsonError.
JSON strings are valid UTF-8. Encoding escapes quotation marks, backslashes,
control characters, and characters required by JSON. It does not additionally
escape /, <, >, &, U+2028, U+2029, or ordinary non-ASCII text.
Malformed UTF-8 and invalid Unicode escape sequences are decoding errors.
Array<byte>—and therefore Array<uint8>—encodes as a standard padded RFC
4648 base64 JSON string. A numeric JSON array is not accepted as an alternative
binary representation.
An open nominal type delegates structural JSON behavior to its immediate underlying type while retaining its static identity.
An opaque nominal type receives neither encoding nor decoding from its underlying type. Its defining package may provide a deliberate explicit implementation. External packages cannot do so, and ordinary generated code does not insert an underlying conversion automatically.
An exact alias has exactly the JSON behavior of its target.
Public native packages target the versioned, language-owned ps_ffi_v1 ABI.
Arbitrary C, C++, Rust, or platform libraries are adapted to that ABI by native
package code.
The safe source language provides no:
- direct C-header import;
- arbitrary foreign-symbol declaration;
- raw pointer or foreign-layout type;
- variadic native call;
- public native-to-ProScript callback; or
- unsafe application-source block.
Native adapter code is trusted. It can corrupt the process if it violates the ABI, but its complexity does not enter the safe ProScript type system.
Native declarations appear only at package scope in a manifest-authorized package and have no ProScript body:
native fn hash_block(
input: Array<byte>,
) Result<Array<byte>, HashError>;
native offload fn compress(
input: Array<byte>,
level: int32,
) Result<Array<byte>, CompressionError>;
pub native fn public_hash(
input: Array<byte>,
) Result<Array<byte>, HashError>;
pub native offload fn public_compress(
input: Array<byte>,
level: int32,
) Result<Array<byte>, CompressionError>;
pub native type CipherHandle;native fn is inline. Its adapter promises short, bounded, non-blocking
execution on the current scheduler worker.
native offload fn runs potentially blocking or long-running work through the
runtime's bounded native worker facility. The calling green task transparently
suspends. Call sites use the function normally; no public future or additional
await exists.
A native function declaration is package-private when unmarked. A leading
pub directly exports either the inline or offloaded form as a safe typed
ProScript function. Its raw symbol, ABI representation, artifact identity, and
transport remain private. A package instead uses a private native declaration
and ordinary public wrapper when it needs validation, richer application types,
or a public signature outside the closed native set.
A native type is an opaque runtime-registered resource handle. It may be
public. Safe source cannot construct it, inspect its token, cast it, or override
its lifecycle or shareability metadata.
The source-level native binding type set is closed to:
- fixed-width signed and unsigned integers, including
byteandrune; float32,float64,bool, andunit;- valid UTF-8
string; Array<byte>;- a locally declared
native type; and - an outer
Result<Success, Error>whose ABI mapping is statically known.
Target-width int and uint, arbitrary arrays, maps, sets, structs, user
enums, options, protocol values, tasks, channels, closures, callbacks,
variadics, managed references, pointers, and shared layouts cannot cross the
ABI directly. Ordinary wrapper code validates and converts richer types.
Marshaling preserves semantic copy isolation without requiring physical copy-in/copy-out:
- fixed-width scalar arguments use their canonical ABI values without payload allocation;
- strings and
Array<byte>may use read-only call-scoped views over stable contiguous payload storage; - the runtime holds an exact payload lease until the native entry returns, and an admitted offloaded job holds its leases until every completion or failure path has finished;
- native code cannot retain an input pointer beyond the entry and receives no managed wrapper address, collector reference, or arena address;
- an implementation that cannot provide stable storage uses independent copied call storage instead;
- physically shared byte storage remains unobservable under value semantics,
and a mutable
Array<byte>detaches through copy-on-write or an equivalent strategy before a write could affect another logical array; - strings cross as UTF-8 bytes with explicit length, and embedded NUL remains data;
- variable output uses a runtime reservation that the adapter writes, after which the runtime validates, seals, and adopts the same payload without a required post-return copy;
- string output is UTF-8-validated before sealing, while byte output validates its reservation identity, length, and capacity;
- output cannot alias input, and lengths and capacities use validated fixed-width ABI integers; and
- every output reservation and input lease is completed or released exactly once on success, expected error, panic, malformed output, allocation failure, and result-construction failure.
Zero length may use a null data pointer only with zero length and capacity. Copied transport remains a transparent fallback and may be selected for small values when measured faster. Public ProScript pinning, borrowing, pointers, lifetimes, and ownership-transfer operations do not exist. Stable payload, lease, unfinished-output, pooled-capacity, and copy-on-write memory all count toward runtime pressure and backpressure.
Every native handle contains a runtime-registered opaque token with type, generation, and lifecycle validation. An operation acquires a lease before entering native code and releases it afterward, preventing cleanup from racing a live operation.
Native metadata classifies a handle as task-confined by default or as shareable-by-construction. It also classifies cleanup as inline or offloaded. Only audited runtime metadata can claim shareability.
The offload facility has bounded workers and bounded admission. A green task acquires admission before large inputs are copied or retained for a queued job. After admission the job acquires exact payload leases, and releases them on every completion or failure path. Saturation suspends the task until capacity becomes available; it does not create unbounded threads, unbounded queued payload retention, or a new application error branch.
Admission is starvation-resistant but has no FIFO or bounded-wait promise. ProScript has no task cancellation, so queued or running native work is not implicitly cancelled.
Expected failures map to the declared typed Result. Detected ABI contract
faults—including invalid status values, invalid UTF-8 promises, malformed
buffers, stale handles, wrong handle types, and foreign unwinding—produce
PanicKind::Native. Undetectable in-process memory corruption is outside the
safe-source guarantee.
The package manifest pins:
- the
ps_ffi_v1ABI major; - target-specific artifacts or target-build recipes;
- exported binding metadata; and
- artifact integrity identity.
The lockfile pins the exact selected artifacts. Source cannot load an arbitrary filesystem path or symbol. A target without a matching artifact or declared build recipe is unsupported and is rejected before execution or packaging.
Incompatible ABI majors use distinct ABI names.
For a Rust-backed native package, its checked ProScript native declarations are
the single source of truth for binding signatures, execution classes, expected
errors, and native-handle identities. ProScript tooling generates both a
binding-specific safe Rust implementation interface and the private raw
ps_ffi_v1 glue. Normal extension authors implement the safe interface rather
than handwritten C-compatible entry points or duplicated numeric status maps.
The generated layer owns raw ABI entry points and metadata, validation, scalar conversion, call-bounded read-only string and byte views, runtime output writers, opaque typed handle adapters, expected-error mapping, Rust-panic containment, and exactly-once cleanup. Its safe Rust input-view lifetime cannot outlive the invocation. Its output abstraction cannot safely fabricate a reservation, exceed its bounds, alias an input, or finish twice.
The generated Rust API is reproducible derived build material associated with
the selected toolchain and declarations. It is checked for staleness before an
artifact is accepted. It is not the public binary ABI: exact Rust identifiers,
module organization, generated paths, and code-generation strategy are not
ProScript language compatibility promises. The stable cross-artifact contract
remains the C-compatible ps_ffi_v1 ABI.
Ordinary adapter implementation requires no handwritten raw pointer, extern
symbol, status table, or ABI-level unsafe code. Adapter-internal audited unsafe
Rust remains possible when required by the wrapped library and remains outside
the safe-source guarantee. Tooling supplies scaffolding, regeneration and
staleness checking, native building, and integration with check, run, and
package; exact convenience-command and manifest-table spellings are tooling
and packaging details.
Applications call the package's ordinary public ProScript API, whether that is
a direct public native declaration or an ordinary wrapper. The call site
contains no native marker, callback, future, explicit await, pointer, borrow,
pin, output buffer, or manual release operation.
pub native fn and pub native offload fn are direct safe exports. Their
public compatibility shape is the exact ProScript function signature, which
must satisfy both ordinary exported-signature nameability and the closed native
binding set. Native backing and inline/offload execution class are not part of
that compatibility fingerprint; an exact public signature may move between an
ordinary ProScript body and either native class without changing callers.
The normal Rust implementation consists of one function per binding marked:
#[proscript_native::implements]
fn compress(
input: InputBytes<'_>,
level: i32,
output: &mut ByteOutput<'_>,
) -> Result<(), CompressionError> {
// Rust implementation
}The marker takes no duplicate binding name, execution class, signature, symbol, or status arguments. Generated context and exact function name identify the binding. Generated private glue owns registration and export, so ordinary code contains no adapter struct, generated-trait implementation, export macro, raw symbol, pointer, or numeric status table. Missing, extra, duplicate, and mismatched implementations produce a joined diagnostic over the ProScript and Rust locations.
String and byte results admit two generated safe Rust profiles. A convenience
implementation returns owned String or Vec<u8> storage and incurs one
reported copy into the runtime reservation. A direct-output implementation
receives the generated text/byte writer and returns unit on success, permitting
zero-copy production into that reservation. An outer expected-error Result
uses the generated exact error type in both profiles. Tooling reports the
selected profile and expected payload-copy count.
proscript check automatically regenerates stale hidden bindings and checks
local Rust implementations. run and package reuse matching cached native
artifacts or build them before continuing. Generated glue and build products
are derived tool-managed material rather than ordinary source. Optional native
scaffold, regeneration, build, and report commands may exist, but are not
required in the normal edit-run path. Consumers of a matching pinned prebuilt
artifact require no installed Rust, Cargo, C compiler, or native headers.
For a manifest-authorized local Rust build, these commands may invoke trusted
Cargo procedural macros and build scripts. That is native build-time code, not
ProScript application execution, and is outside the safe-source guarantee.
Matching pinned prebuilt artifacts never invoke it. Native cache identity binds
the declaration schema, Rust sources/dependency lock, target, SDK/ABI versions,
and relevant toolchain identity.
One proscript command owns the standard workflow:
proscript check [target]
proscript run [target] [-- program arguments]
proscript test [target]
proscript fmt [paths]
proscript fmt --check [paths]
proscript package [target]
check parses, resolves, and statically validates the reachable package graph
without executing application code.
run performs the same validation and executes the selected target.
package creates a target-specific self-contained artifact.
The ordinary edit-run workflow has no required user-facing compilation command.
For check and run, a .pr file target selects its containing directory
package rather than an independent source compilation unit. The tool normalizes
an existing target before deriving its parent, so bare, ./, parent-relative,
and absolute spellings select consistently; lexical . and .. are not
package names. File targeting selects the same deterministic set of ordinary
direct sibling .pr files as targeting the containing directory. _test.pr
remains excluded except during proscript test. A named file does not acquire
entry-point significance, and fmt retains exact path selection rather than
expanding a file target to its package.
Every compiler diagnostic has:
- a stable categorical code within the selected language version;
- one primary source span and a concise message;
- related source spans where relevant; and
- actionable notes that do not assume a speculative rewrite is safe.
--message-format=json produces a machine-readable representation whose schema
is versioned independently of human prose.
Rendered wording, color, and layout are not source-compatibility contracts. Warnings do not make a program invalid unless a tool invocation explicitly promotes them to errors. A target with a reachable static error is never executed.
Generic runtime reporting boundaries use Debug; ProScript does not define a
second universal diagnostic protocol.
proscript fmt is idempotent and semantics-preserving. fmt --check performs
no writes and exits unsuccessfully when formatting would change a file.
Canonical formatting:
- uses four spaces per structural indentation level and no indentation tabs;
- places opening braces on the declaration or control-flow header line;
- aligns closing delimiters with their opener;
- emits one ordinary statement per line;
- removes trailing whitespace;
- collapses excessive blank-line runs;
- ends an ordinary file with one line feed;
- removes an optional trailing comma from a single-line list;
- emits a trailing comma in a multiline list;
- normalizes spacing around tokens;
- preserves literal contents and comment prose; and
- never reorders declarations, fields, imports, cases, or other program elements.
The formatter has no hard line-width limit and preserves the author's choice between single-line and multiline list or chain layout where both are valid.
A source filename ending in _test.pr belongs to its directory package only
during proscript test.
The test tool discovers package-private package-scope functions named
test_* with either signature:
fn test_name()
fn test_name() Result<unit, E>For the second form, E must support Debug. Test functions take no
parameters. There is no test keyword or test annotation.
Each test runs inside a same-task recovery boundary. A panic fails that test after ordinary unwind and cleanup. Tests run sequentially within one package by default; a test that needs concurrency creates tasks explicitly. Different packages may run in separate worker processes.
Assertions and fixtures are ordinary library APIs.
The formatter, language server, editor integrations, documentation tools, and linters reuse compiler parsing, name resolution, and type information. They do not define alternative language semantics.
A project manifest declares its source-language contract:
[package]
name = "billing_api"
version = "0.3.1"
language = "0.1"The compiler checks the project under that declared mode and never silently
reinterprets it using a newer incompatible grammar. A manifest-free standalone
package main command uses the installed toolchain's current language mode.
Before language 1.0, a new minor language version may make documented breaking changes. Patch releases do not intentionally change the behavior of accepted programs.
Starting with language 1.0, a breaking source or semantic change requires a new major language version. ProScript uses language versions rather than a separate edition mechanism.
Package releases use semantic versioning. A dependency requirement "1.2"
means >=1.2.0, <2.0.0; an exact requirement uses "=1.2.3".
Resolution selects one version for each canonical package identity in the dependency graph. Conflicting requirements are errors rather than loading multiple type-incompatible copies under one identity.
The lockfile records the exact selected version, source identity, integrity digest, native artifacts, and complete graph.
The standard library is selected with the toolchain and imported through
std. It is not resolved as an independent manifest dependency. A toolchain
supporting an older language mode provides that mode's predeclared names and
standard contracts.
Public compatibility follows exported static shape. The following changes are breaking:
- adding or removing a public enum variant;
- adding, removing, or changing a public struct field;
- changing a public callable parameter or result type;
- strengthening a public generic bound;
- adding a requirement to a public protocol;
- removing a visible conformance; or
- exposing a type whose identity or visibility no longer matches the public signature.
Tooling may compare exported package metadata and report these changes.
Source behavior and documented package APIs are compatibility contracts.
Cached bytecode is disposable and implementation-internal. A self-contained
artifact carries its matching runtime. Native package compatibility is tied to
an explicit ps_ffi_vN major.
The following constructs are not part of ProScript:
Any, downcasting, general reflection, and a source-visible never type;- null values outside JSON, tuples, multiple returns, and variadic functions;
- macros, pipeline operators, operator overloading, and user-defined precedence;
- labeled loops and value-producing loops;
- public futures or promises,
async fn, a secondawaitsyntax, task cancellation, task groups, nurseries, supervisors, and channel abort; - general
defer, garbage-collector finalizers, public weak references, pointers, pinning, stable managed addresses, and ownership or borrowing syntax; - protocol default bodies, protocol inheritance, associated types,
higher-kinded types, conditional or overlapping conformance,
specialization, and
whereclauses; - restricted visibility, re-exports, wildcard imports, package runtime initialization, workspace manifests, source build tags, and platform-selecting filename conventions;
- direct foreign-header binding, public native callbacks, foreign-layout sharing, borrowed managed buffers, and unsafe application-source blocks;
- interpolation format specifiers or protocol fallback; and
- user-defined structural providers or an open attribute system.
The absence of these constructs is part of the language's simplicity and safety model. A library cannot simulate them by weakening the static or runtime guarantees defined here.
HTTP, database, filesystem, networking, cryptography, compression, process, and other service APIs belong to the standard library and package ecosystem. They use the language mechanisms defined by this specification but are not compiler intrinsics.