Split language manual into topic pages with cross-references
Break docs/manual.md (3074 lines) into 14 focused topic pages under docs/manual/, add inter-page cross-references, and remove duplicated sections (? operator, iterator protocols, unsafe blocks examples). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,7 +128,7 @@ async fn parallelSum(a: string, b: string): int64 {
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Language Manual](docs/manual.md)** -- complete reference for implemented features
|
||||
- **[Language Manual](docs/manual/index.md)** -- complete reference for implemented features
|
||||
- **[Compiler Architecture](docs/architecture.md)** -- end-to-end pipeline documentation
|
||||
- **[Grammar](docs/grammar.md)** -- formal grammar specification
|
||||
- **[Bootstrap TODO](docs/bootstrap-todo.md)** -- roadmap toward self-hosting
|
||||
|
||||
3083
docs/manual.md
3083
docs/manual.md
File diff suppressed because it is too large
Load Diff
250
docs/manual/basics.md
Normal file
250
docs/manual/basics.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Basics
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## Hello, world
|
||||
|
||||
```
|
||||
print("Hello, world!");
|
||||
```
|
||||
|
||||
All programs implicitly `import std` to load Peon's standard library.
|
||||
|
||||
|
||||
## Variable declarations
|
||||
|
||||
Peon has three declaration keywords with distinct semantics:
|
||||
|
||||
| Keyword | Mutable? | Initialization | Evaluation |
|
||||
|---------|----------|----------------|------------|
|
||||
| `var` | Yes | Optional | Runtime |
|
||||
| `let` | No | Optional | Runtime |
|
||||
| `const` | No | Required | Compile time |
|
||||
|
||||
- **`var`** declares a mutable variable. Its value can be reassigned after declaration.
|
||||
A `var` may be declared without an initializer if a type annotation is provided;
|
||||
the compiler tracks the first assignment and prevents use before initialization
|
||||
rather than zero-initializing the storage.
|
||||
- **`let`** declares an immutable binding. Once assigned, the value cannot be changed.
|
||||
Like `var`, a `let` may be declared without an initializer if a type annotation is
|
||||
provided. The compiler ensures the binding is assigned exactly once before use and
|
||||
prevents any subsequent reassignment.
|
||||
- **`const`** declares a compile-time constant. The initializer must be evaluable at
|
||||
compile time, and the resulting value is inlined at every use site. `const` values
|
||||
do not occupy runtime storage. See [Compile-Time Features](compile-time.md) for more
|
||||
on compile-time evaluation.
|
||||
|
||||
In function parameter position, `const` has a different meaning: `fn f(x: const int64)`
|
||||
declares a static compile-time parameter rather than a `const` variable declaration
|
||||
(see [Generics](generics.md#static-compile-time-parameters)).
|
||||
|
||||
```
|
||||
var x = 5; # Inferred type is int64
|
||||
var y = 3'u16; # Type suffix specifies uint16
|
||||
x = 6; # Works: type matches
|
||||
x = 3.0; # Error: cannot assign float64 to x
|
||||
var x = 3.14; # Error: cannot re-declare x
|
||||
const z = 6.28; # Constant: evaluated at compile time
|
||||
let a = "hi!"; # Immutable: cannot be reassigned
|
||||
var b: int32 = 5; # Explicit type annotation
|
||||
var c: int64; # No initializer: use before first assignment is an error
|
||||
let d: string; # No initializer: must be assigned exactly once before use
|
||||
d = "hello"; # First assignment is OK
|
||||
# d = "world"; # Error: cannot reassign a let binding
|
||||
```
|
||||
|
||||
All three forms support optional type annotations (`var x: int32 = 5`). When the
|
||||
annotation is omitted, the type is inferred from the initializer. For `var` and `let`
|
||||
declarations without an initializer, a type annotation is required.
|
||||
|
||||
__Note__: Peon supports [name stropping](https://en.wikipedia.org/wiki/Stropping_(syntax)), meaning
|
||||
that almost any ASCII sequence of characters can be used as an identifier, including language
|
||||
keywords, but stropped names need to be enclosed by matching pairs of backticks (`` ` ``).
|
||||
|
||||
## Comments
|
||||
|
||||
```
|
||||
# This is a single-line comment
|
||||
# Peon has no specific syntax for multi-line comments.
|
||||
|
||||
fn id[T](x: T): T {
|
||||
## Documentation comments start
|
||||
## with two hashes. They are currently
|
||||
## parsed but ignored by the compiler.
|
||||
return x;
|
||||
}
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
String literals are enclosed in double quotes. The following escape sequences are
|
||||
supported:
|
||||
|
||||
| Escape | Meaning |
|
||||
|--------|---------|
|
||||
| `\\` | Backslash |
|
||||
| `\"` | Double quote |
|
||||
| `\'` | Single quote |
|
||||
| `\n` | Newline |
|
||||
| `\t` | Tab |
|
||||
| `\r` | Carriage return |
|
||||
| `\a` | Bell |
|
||||
| `\b` | Backspace |
|
||||
| `\e` | Escape (0x1B) |
|
||||
| `\f` | Form feed |
|
||||
| `\v` | Vertical tab |
|
||||
| `\xHH` | Hex byte (value <= 255) |
|
||||
| `\NNN` | Octal byte (up to 3 digits, value <= 255) |
|
||||
|
||||
## Primitive types
|
||||
|
||||
The standard library (`std`) provides these built-in types:
|
||||
|
||||
| Type | Aliases | Description |
|
||||
|------|---------|-------------|
|
||||
| `int64` | `int`, `i64` | 64-bit signed integer |
|
||||
| `int32` | `i32` | 32-bit signed integer |
|
||||
| `int16` | `i16` | 16-bit signed integer |
|
||||
| `int8` | `i8` | 8-bit signed integer |
|
||||
| `uint64` | `u64` | 64-bit unsigned integer |
|
||||
| `uint32` | `u32` | 32-bit unsigned integer |
|
||||
| `uint16` | `u16` | 16-bit unsigned integer |
|
||||
| `uint8` | `u8` | 8-bit unsigned integer |
|
||||
| `float64` | `float`, `f64` | 64-bit floating point |
|
||||
| `float32` | `f32` | 32-bit floating point |
|
||||
| `bool` | | Boolean |
|
||||
| `string` | | String |
|
||||
|
||||
Type union aliases are also provided for use in [generic constraints](generics.md#type-union-constraints):
|
||||
|
||||
- `SignedInteger` = `int64 | int32 | int16 | int8`
|
||||
- `UnsignedInteger` = `uint64 | uint32 | uint16 | uint8`
|
||||
- `Integer` = `SignedInteger | UnsignedInteger`
|
||||
- `Number` = `Integer | float64 | float32`
|
||||
|
||||
Integer literals can use type suffixes: `42'i32`, `255'u8`, `1'i64`.
|
||||
|
||||
Number literals support several bases:
|
||||
|
||||
```
|
||||
42 # Decimal
|
||||
0xFF # Hexadecimal
|
||||
0b1010 # Binary
|
||||
0o755 # Octal
|
||||
3.14 # Float
|
||||
1.0e-10 # Float with exponent
|
||||
```
|
||||
|
||||
The standard library also exposes several built-in type constructors and handle forms:
|
||||
|
||||
| Form | Description |
|
||||
|------|-------------|
|
||||
| `array[N, T]` | Fixed-size value array with length `N` and element type `T` |
|
||||
| `ref T` | Managed owning reference to heap-allocated `T` |
|
||||
| `lent T` | Borrowed read-only view of `T` |
|
||||
| `mut lent T` | Borrowed mutable view of `T` |
|
||||
| `pointer` | Opaque raw pointer type with no pointee information |
|
||||
| `ptr T` | Typed raw pointer to plain storage `T` |
|
||||
| `UncheckedArray[T]` | Unsized contiguous storage type used behind raw pointers |
|
||||
|
||||
Use `pointer` when the pointee type is intentionally erased. Use `ptr T` when you want a
|
||||
typed pointer to one plain-storage value. Converting between `pointer` and `ptr T`
|
||||
always requires an explicit `cast[...]`. Built-in indexing is reserved for
|
||||
`ptr UncheckedArray[T]`; plain `ptr T` values are not indexable. Raw pointers cannot
|
||||
point to managed `ref` values or `lent` borrows.
|
||||
|
||||
For details on managed references and borrows, see [Memory Model](memory-model.md).
|
||||
For raw pointers and unmanaged memory, see [Unsafe](unsafe.md#raw-pointers).
|
||||
|
||||
### No implicit conversions
|
||||
|
||||
Peon never performs automatic type conversions. Assigning an `int32` to an
|
||||
`int64` variable, or passing a `float32` where `float64` is expected, is a
|
||||
type error. All conversions must be explicit:
|
||||
|
||||
```peon
|
||||
var x: int32 = 42;
|
||||
# var y: int64 = x; # Error: type mismatch
|
||||
var y: int64 = int64(x); # OK: explicit converter call
|
||||
```
|
||||
|
||||
The one exception is **compile-time literal inference**: an untyped numeric
|
||||
or string literal can be inferred to the expected type at the call site when
|
||||
the constant value fits:
|
||||
|
||||
```peon
|
||||
let x: int8 = 42; # OK: literal 42 fits in int8
|
||||
# let y: int8 = 200; # Error: literal 200 does not fit in int8
|
||||
```
|
||||
|
||||
This is not a conversion -- the literal is typed directly as the target type.
|
||||
It only applies to compile-time constants, never to variables or expressions.
|
||||
|
||||
Custom converter functions can be defined with the `converter` pragma
|
||||
(see [Operators -- Defining custom converters](operators.md#defining-custom-converters)).
|
||||
|
||||
## Built-in functions
|
||||
|
||||
The standard library provides these built-in functions:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `print(x)` | Print a value to stdout followed by a newline |
|
||||
| `panic()` | Abort the program with a runtime panic |
|
||||
| `panic(msg)` | Abort with a message |
|
||||
| `borrow(x)` or `&x` | Create a non-owning borrow (`lent`) of `x` |
|
||||
| `move(src, dst)` | Explicitly move `src` into `dst` |
|
||||
| `clone(x)` | Create an independent copy of `x` |
|
||||
| `cast[T](x)` | Reinterpret `x` as `T` for low-level raw-pointer or same-size scalar bitcasts. Requires `unsafe`. |
|
||||
| `new(T)` | Heap-allocate a new value of type `T` |
|
||||
| `low(T)` | Return the minimum value of a numeric type (`-inf` for floats) |
|
||||
| `high(T)` | Return the maximum value of a numeric type (`inf` for floats) |
|
||||
| `typeof(x)` | Return the compile-time type witness for the value `x` |
|
||||
| `sizeof(T)` | Return the compile-time size in bytes of a sized type |
|
||||
| `sizeof(x)` | Return the compile-time size in bytes of the type of `x` |
|
||||
| `needsDestroy(T)` | Return whether values of type `T` carry owned cleanup work |
|
||||
| `low(x)` | Return the first valid index of a string, array, or `seq` |
|
||||
| `high(x)` | Return the last valid index of a string, array, or `seq` |
|
||||
| `len(x)` | Return the length of a collection |
|
||||
| `alloc(T, n)` | Allocate raw storage for `n` values of type `T` (uninitialized). Requires `unsafe`. |
|
||||
| `create(T, n)` | Allocate zero-initialized raw storage for `n` values of type `T`. Requires `unsafe`. |
|
||||
| `realloc(ptr, n)` | Resize a raw allocation to `n` elements. Requires `unsafe`. |
|
||||
| `dealloc(ptr)` | Free a raw pointer allocation. Requires `unsafe`. |
|
||||
| `copyMem(dst, src, n)` | Copy `n` bytes from `src` to `dst` (well-defined even for overlapping regions). Requires `unsafe`. |
|
||||
| `addr(x)` | Take the raw address of a raw-pointer-backed value, returning `ptr T`. |
|
||||
| `append(s, x)` | Append element `x` to `mut lent seq[T]` |
|
||||
| `start(g)` | Start a generator, returning `GeneratorState[Y, R]` |
|
||||
| `resume(g, v)` | Resume a generator with send value `v`, returning `GeneratorState[Y, R]` |
|
||||
| `next(it)` | Advance an iterator or generator, returning `Option[Y]` |
|
||||
|
||||
For collections, valid indices are `low(x) .. high(x)` when the collection is not
|
||||
empty. For strings, arrays, and `seq`s, `low(x)` is always `0` and `high(x)` is the
|
||||
last valid index (`-1` for empty values).
|
||||
|
||||
`typeof(x)` returns a compile-time type witness, which can be passed to helpers like
|
||||
`sizeof`. Both `sizeof(T)` and `sizeof(x)` are compile-time constants. They are useful
|
||||
for raw allocation helpers and FFI-style layout checks:
|
||||
|
||||
```peon
|
||||
type Pair = object {
|
||||
left: int64;
|
||||
right: int64;
|
||||
}
|
||||
|
||||
print(sizeof(int64)); # 8
|
||||
print(sizeof(array[3, int64])); # 24
|
||||
print(sizeof(Pair)); # 16
|
||||
let pair = Pair(left = 1, right = 2);
|
||||
print(sizeof(pair)); # 16
|
||||
```
|
||||
|
||||
String indexing uses the same bounds as `low(text)` and `high(text)`, but because
|
||||
strings are immutable it returns an owned `char` value rather than a borrow:
|
||||
|
||||
```peon
|
||||
let text = "abc";
|
||||
let middle: char = text[1];
|
||||
print(middle); # b
|
||||
print(low(text)); # 0
|
||||
print(high(text)); # 2
|
||||
```
|
||||
224
docs/manual/c-interop.md
Normal file
224
docs/manual/c-interop.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# C Interop
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon provides [pragmas](compile-time.md#pragmas) and types for calling C functions from
|
||||
Peon and exposing Peon functions to C.
|
||||
|
||||
## C-compatible types
|
||||
|
||||
The `cinterop` module provides types that map directly to their
|
||||
C equivalents:
|
||||
|
||||
| Peon type | C type |
|
||||
|-----------|--------|
|
||||
| `cchar` | `char` |
|
||||
| `cschar` | `signed char` |
|
||||
| `cuchar` | `unsigned char` |
|
||||
| `cshort` | `short` |
|
||||
| `cushort` | `unsigned short` |
|
||||
| `cint` | `int` |
|
||||
| `cuint` | `unsigned int` |
|
||||
| `clong` | `long` |
|
||||
| `culong` | `unsigned long` |
|
||||
| `clonglong` | `long long` |
|
||||
| `culonglong` | `unsigned long long` |
|
||||
| `cfloat` | `float` |
|
||||
| `cdouble` | `double` |
|
||||
| `cbool` | `_Bool` |
|
||||
| `csize` | `size_t` |
|
||||
| `cptrdiff` | `ptrdiff_t` |
|
||||
| `cstring` | `char *` |
|
||||
| `cvoid` | `void` (pointer position only) |
|
||||
|
||||
These types support arithmetic, bitwise, comparison, and logical operators as appropriate
|
||||
for their C counterparts.
|
||||
|
||||
Type union aliases are provided for [generic constraints](generics.md#type-union-constraints):
|
||||
|
||||
- `CInteropInteger` -- all C integer types
|
||||
- `CInteropNumeric` -- all C integer and float types
|
||||
- `CInteropSignedInteger` / `CInteropUnsignedInteger` -- signed/unsigned subsets
|
||||
|
||||
## Type converters
|
||||
|
||||
The `cinterop` module provides converter functions for converting between Peon and C
|
||||
types. Peon has [no implicit conversions](basics.md#no-implicit-conversions) -- all
|
||||
conversions must be written explicitly. However, literals are inferred to the expected
|
||||
type, so a numeric or string literal passed where a C type is expected needs no
|
||||
conversion:
|
||||
|
||||
```peon
|
||||
import cinterop;
|
||||
|
||||
fn puts(msg: cstring): cint; #pragma[importc, header: "<stdio.h>"]
|
||||
|
||||
unsafe { puts("hello"); } # Literal inferred as cstring -- no conversion needed
|
||||
|
||||
var s = "hello"; # s is a Peon string
|
||||
# puts(s); # Error: string is not cstring
|
||||
unsafe { puts(cstring(s)); } # Explicit conversion required for non-literals
|
||||
|
||||
let n: cint = 42; # Literal inferred as cint
|
||||
var x: int64 = 42;
|
||||
let m = cint(x); # Explicit conversion from int64 to cint
|
||||
```
|
||||
|
||||
For defining your own converter functions, see
|
||||
[Operators -- Defining custom converters](operators.md#defining-custom-converters).
|
||||
|
||||
## Importing C functions (`importc`)
|
||||
|
||||
`importc` marks a top-level forward declaration as an external C function:
|
||||
|
||||
```peon
|
||||
|
||||
fn strlen(s: cstring): csize; #pragma[importc, header: "<string.h>"]
|
||||
|
||||
let len = unsafe { strlen("hello") };
|
||||
```
|
||||
|
||||
- The pragma takes an optional string argument for the C symbol name. When omitted, the
|
||||
Peon identifier spelling is used: `#pragma[importc: "my_strlen"]`.
|
||||
- `header` specifies the C header to `#include`. The string is emitted verbatim, so both
|
||||
`"<stdio.h>"` and `"mylib.h"` work.
|
||||
- `noDecl` suppresses the generated C prototype when the header already provides it:
|
||||
`#pragma[importc, header: "<math.h>", noDecl]`.
|
||||
- Imported C functions are treated as [`unsafe fn`](unsafe.md#unsafe-functions): they must
|
||||
be called from `unsafe { ... }`.
|
||||
|
||||
`importc` is valid on:
|
||||
|
||||
- top-level forward function declarations (no body)
|
||||
- top-level `const` declarations without initializers
|
||||
- top-level non-`ref`, non-interface type declarations
|
||||
|
||||
It cannot be combined with `exportc`.
|
||||
|
||||
## Importing C constants (`importc` on `const`)
|
||||
|
||||
Imported C constants use top-level `const` declarations with no initializer and an
|
||||
explicit `cinterop` type:
|
||||
|
||||
```peon
|
||||
import cinterop;
|
||||
|
||||
const CLOCKS_PER_SEC: clong; #pragma[importc, header: "<time.h>"]
|
||||
|
||||
let value = CLOCKS_PER_SEC;
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- the declaration must be a top-level `const`
|
||||
- it must not provide an initializer
|
||||
- it must use an explicit `cinterop` type such as `cint`, `clong`, `csize`, `cdouble`,
|
||||
and so on
|
||||
- `header` and `noDecl` follow the same meaning as for imported functions
|
||||
- despite the `const` spelling, imported C constants are runtime-only bindings, not Peon
|
||||
compile-time constants in the general [CTFE](compile-time.md#ctfe-limitations) sense
|
||||
|
||||
This means they are available in ordinary runtime expressions and native C lowering, but
|
||||
they are rejected by compile-time evaluation machinery such as `const` initializers,
|
||||
`when` conditions, and static generic arguments.
|
||||
|
||||
One exception exists for [value-enum](types.md#value-enums) discriminants: imported
|
||||
integral constants may be used there so stdlib and user code can map Peon enum variants
|
||||
onto C numeric constants.
|
||||
|
||||
## Importing C structs (`importc` on types)
|
||||
|
||||
Plain object types can be imported from C headers:
|
||||
|
||||
```peon
|
||||
type timespec = object {
|
||||
#pragma[importc: "struct timespec", header: "<time.h>"]
|
||||
tv_sec: clong;
|
||||
tv_nsec: clong;
|
||||
}
|
||||
```
|
||||
|
||||
For plain object and enum declarations, the `importc` pragma lives inside the
|
||||
declaration body as the first C interop slice. The imported type maps to the named C
|
||||
struct. Fields must match the C layout. Tuple type declarations are not supported by
|
||||
`importc`.
|
||||
|
||||
Imported and exported plain object fields may include fixed arrays
|
||||
(`array[N, T]`) when `T` itself is C-ABI-safe. This allows direct mirrors of
|
||||
C fields such as `char name[32]` or `unsigned char addr[128]`.
|
||||
|
||||
## Importing C typedef aliases
|
||||
|
||||
Type aliases can import named C typedefs, including scalar and pointer aliases:
|
||||
|
||||
```peon
|
||||
type clock_t = clong; #pragma[importc, header: "<time.h>"]
|
||||
|
||||
fn clock(): clock_t; #pragma[importc, header: "<time.h>", noDecl]
|
||||
```
|
||||
|
||||
This keeps the typedef name available to the C backend instead of lowering the alias to
|
||||
its raw Peon spelling everywhere.
|
||||
|
||||
## Importing C enums
|
||||
|
||||
Value enums can map to C enums:
|
||||
|
||||
```peon
|
||||
type ClockId = enum {
|
||||
#pragma[importc: "ClockId", header: "<time.h>"]
|
||||
CLOCK_REALTIME,
|
||||
CLOCK_MONOTONIC
|
||||
}
|
||||
|
||||
fn clock_gettime(id: ClockId, tp: ptr timespec): cint; #pragma[importc, header: "<time.h>"]
|
||||
|
||||
let id: ClockId = CLOCK_MONOTONIC;
|
||||
let other: ClockId = ClockId.CLOCK_REALTIME;
|
||||
```
|
||||
|
||||
As with imported object declarations, the `importc` pragma belongs inside the enum body.
|
||||
Imported enums must be value enums, which in practice means:
|
||||
|
||||
- no payload fields
|
||||
- optional explicit integral discriminants
|
||||
- variant values lower to the matching C enum constants
|
||||
|
||||
Their Peon variant names should therefore match the C identifiers.
|
||||
|
||||
## Exporting Peon functions (`exportc`)
|
||||
|
||||
`exportc` exposes a top-level Peon function with a plain C ABI:
|
||||
|
||||
```peon
|
||||
|
||||
fn add(a: cint, b: cint): cint {
|
||||
#pragma[exportc]
|
||||
return a + b;
|
||||
}
|
||||
|
||||
fn peonAdd(a: cint, b: cint): cint {
|
||||
#pragma[exportc: "peon_add"] # Custom C symbol name
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
The compiler generates a C wrapper that marshals arguments. `exportc` cannot have
|
||||
mutable reference parameters and cannot be combined with `importc`.
|
||||
|
||||
## Exporting Peon structs (`exportc` on types)
|
||||
|
||||
Plain object types can be exported so they are visible as C structs:
|
||||
|
||||
```peon
|
||||
type Point = object {
|
||||
#pragma[exportc]
|
||||
x: cint;
|
||||
y: cint;
|
||||
}
|
||||
```
|
||||
|
||||
`exportc` on types is only valid for top-level, non-generic, plain `object` declarations
|
||||
(not `ref object`, not enums, not interfaces, not tuples, and not type aliases).
|
||||
As with `importc`, exported plain object fields may use fixed arrays of C-ABI-safe
|
||||
element types.
|
||||
237
docs/manual/compile-time.md
Normal file
237
docs/manual/compile-time.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Compile-Time Features
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## Compile-time conditionals (`when`)
|
||||
|
||||
`when` is a compile-time `if`. The condition must be evaluable at compile time, and only
|
||||
the selected branch is compiled. Names declared in the chosen branch are visible in the
|
||||
enclosing scope (no new scope is introduced):
|
||||
|
||||
```
|
||||
when 1 < 2 {
|
||||
const chosen = 41;
|
||||
} else {
|
||||
let chosen: bool = 1; # Would be a type error, but never compiled
|
||||
}
|
||||
print(chosen + 1); # 42
|
||||
```
|
||||
|
||||
Inside a function with [static parameters](generics.md#static-compile-time-parameters),
|
||||
`when` conditions may depend on those parameters. The static arguments are treated like
|
||||
const-generic values, so each distinct compile-time value produces its own specialization:
|
||||
|
||||
```
|
||||
fn select(flag: const bool): int64 {
|
||||
when flag {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
print(select(true)); # specializes `select` for `true`
|
||||
print(select(false)); # specializes `select` for `false`
|
||||
```
|
||||
|
||||
The condition is not limited to a bare identifier. Any expression is allowed as long as
|
||||
the compiler's current compile-time evaluator can evaluate it under the static bindings.
|
||||
That includes nested control-flow expressions and compile-time-safe helper calls:
|
||||
|
||||
```
|
||||
fn isOdd(n: int64): bool;
|
||||
|
||||
fn isEven(n: int64): bool {
|
||||
if n == 0 {
|
||||
return true;
|
||||
}
|
||||
return isOdd(n - 1);
|
||||
}
|
||||
|
||||
fn isOdd(n: int64): bool {
|
||||
if n == 0 {
|
||||
return false;
|
||||
}
|
||||
return isEven(n - 1);
|
||||
}
|
||||
|
||||
fn classify(start: const int64): int64 {
|
||||
when isEven(start) {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If a `when` condition is not compile-time evaluable, it is still rejected. Static
|
||||
parameters do not make runtime-dependent expressions legal.
|
||||
|
||||
`when` can also be used as an expression -- see
|
||||
[Control Flow -- Control flow expressions](control-flow.md#control-flow-expressions).
|
||||
|
||||
## CTFE limitations
|
||||
|
||||
Compile-time evaluation runs on the bytecode VM, but only over the CTFE-supported
|
||||
subset of the language. In particular, compile-time evaluation cannot allocate runtime
|
||||
heap-backed structures such as `seq` storage and cannot mutate module or global state.
|
||||
Imported C constants declared with `#pragma[importc]` are also runtime-only bindings:
|
||||
they may be read in ordinary runtime code, but they are rejected in `const`
|
||||
initializers, `when` conditions, static [generic](generics.md) arguments, and other
|
||||
compile-time evaluation contexts.
|
||||
|
||||
## Config symbols
|
||||
|
||||
The built-in `defined("name")` can be used in `when` conditions to query compiler
|
||||
configuration symbols and user defines:
|
||||
|
||||
```
|
||||
when defined("checks") {
|
||||
print("safety checks enabled");
|
||||
}
|
||||
```
|
||||
|
||||
The compiler currently guarantees these canonical config symbols:
|
||||
|
||||
- `checks`
|
||||
- `boundChecks`
|
||||
- `overflowChecks`
|
||||
- `floatChecks`
|
||||
- `debug`
|
||||
- `release`
|
||||
- `danger`
|
||||
- `lineTrace`
|
||||
- `stackTrace`
|
||||
|
||||
Default behavior:
|
||||
|
||||
- `debug` is on by default.
|
||||
- `release` and `danger` are off by default.
|
||||
- `lineTrace` and `stackTrace` default to on in `debug` and off otherwise.
|
||||
- `checks` defaults to on, except in `danger`, where it defaults to off.
|
||||
- `boundChecks`, `overflowChecks`, and `floatChecks` default to the value of `checks`
|
||||
unless explicitly overridden.
|
||||
|
||||
## Pragmas
|
||||
|
||||
Peon supports both declaration pragmas and standalone config pragmas.
|
||||
|
||||
### Declaration pragmas
|
||||
|
||||
Declaration pragmas attach metadata or compiler directives to [functions](functions.md),
|
||||
[types](types.md), and [variables](basics.md#variable-declarations):
|
||||
|
||||
- `#pragma[magic: "..."]` marks a compiler intrinsic type or builtin function
|
||||
implementation.
|
||||
- `#pragma[pure]` marks a function or lambda as pure. Pure callables cannot call impure
|
||||
functions, cannot take mutable reference parameters, and cannot mutate non-local
|
||||
state. `print(...)` is the one allowed side effect so pure code can still emit debug
|
||||
output.
|
||||
- `#pragma[inline]` asks the C backend to emit `inline`. It is only valid on named
|
||||
functions, not on lambdas.
|
||||
- `#pragma[constructor]` marks a top-level named function as a constructor candidate for
|
||||
`T(...)` syntax. See [Operators -- Defining custom constructors](operators.md#defining-custom-constructors).
|
||||
- `#pragma[noreturn]` marks a named function as not returning.
|
||||
- `#pragma[noinit]` skips managed-construction initialization paths on a type declaration
|
||||
or variable declaration.
|
||||
- `#pragma[define]` binds a constant declaration to the compile-time define table, so a
|
||||
CLI or user define can override its initializer.
|
||||
- `#pragma[booldefine]` does the same for boolean constants.
|
||||
- `#pragma[warn: "message"]` and `#pragma[warning: "message"]` emit a `UserWarning`
|
||||
when the annotated declaration is used.
|
||||
- `#pragma[error: "message"]` raises a compile-time error when the annotated declaration
|
||||
is used.
|
||||
- `#pragma[deprecated]` is shorthand for a use-site warning saying the function or
|
||||
object is deprecated.
|
||||
- `#pragma[lentFrom: param]` annotates a named `lent`-returning function with the
|
||||
parameter that owns the returned borrow. See
|
||||
[Concurrency -- Borrow safety in generators](concurrency.md#borrow-safety-in-generators).
|
||||
- `#pragma[used]` marks a variable, function, or type as intentionally used. It suppresses
|
||||
unused-declaration warnings for that declaration.
|
||||
|
||||
Delayed use-site pragmas fire when the declaration is referenced, called, or constructed,
|
||||
not when it is declared. For example:
|
||||
|
||||
```peon
|
||||
fn oldApi() {
|
||||
return;
|
||||
} #pragma[deprecated]
|
||||
|
||||
fn caller() {
|
||||
oldApi(); # Emits a warning here
|
||||
}
|
||||
```
|
||||
|
||||
### Scoped config pragmas
|
||||
|
||||
Standalone pragmas override canonical compiler config in a scoped way:
|
||||
|
||||
```peon
|
||||
#pragma[push]
|
||||
#pragma[checks: off]
|
||||
|
||||
fn uncheckedRead(values: array[3, int64], i: int64): int64 {
|
||||
return values[i];
|
||||
}
|
||||
|
||||
#pragma[pop]
|
||||
```
|
||||
|
||||
`#pragma[push]` snapshots the current canonical configuration and `#pragma[pop]` restores
|
||||
it. These config scopes are intentionally not nestable: one `push` must be matched by one
|
||||
`pop` before another `push` is allowed.
|
||||
|
||||
Inside a pushed scope, canonical boolean options can be rebound with standalone pragmas
|
||||
such as:
|
||||
|
||||
- `#pragma[checks: off]`
|
||||
- `#pragma[boundChecks: on]`
|
||||
- `#pragma[overflowChecks: off]`
|
||||
- `#pragma[floatChecks: off]`
|
||||
- `#pragma[lineTrace: off]`
|
||||
- `#pragma[stackTrace: on]`
|
||||
|
||||
Canonical build mode can be switched for the same scoped context with:
|
||||
|
||||
- `#pragma[debug]`
|
||||
- `#pragma[release]`
|
||||
- `#pragma[danger]`
|
||||
|
||||
Warnings can also be disabled for the current config scope with:
|
||||
|
||||
- `#pragma[noWarn: UserWarning]`
|
||||
- `#pragma[noWarn: [UserWarning, RawPointerLeak]]`
|
||||
|
||||
The currently supported warning kinds are:
|
||||
|
||||
- `UserWarning`
|
||||
- `RawPointerLeak`
|
||||
- `UnusedLocalVariable`
|
||||
- `UnusedGlobalVariable`
|
||||
- `UnusedTypeDeclaration`
|
||||
- `UnusedImport`
|
||||
- `UnusedFunction`
|
||||
|
||||
### Function-body pragmas
|
||||
|
||||
Inside a function body, a standalone config pragma applies to the rest of that function
|
||||
body automatically, so `push`/`pop` is not needed for local tweaks:
|
||||
|
||||
```peon
|
||||
fn wrapping_add[T: Integer](a, b: T): T {
|
||||
#pragma[overflowChecks: off]
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
The same rule applies to `noWarn`:
|
||||
|
||||
```peon
|
||||
fn caller() {
|
||||
#pragma[noWarn: UserWarning]
|
||||
oldApi();
|
||||
}
|
||||
```
|
||||
|
||||
This function-body form is preferred when the change is meant to affect just one function,
|
||||
because the active config stays visible at the point where it matters.
|
||||
250
docs/manual/concurrency.md
Normal file
250
docs/manual/concurrency.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Structured Concurrency
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon implements **structured concurrency** end to end on the native C backend:
|
||||
|
||||
```peon
|
||||
async fn parallelSum(a: string, b: string): int64 {
|
||||
nursery tasks {
|
||||
let left = spawn fetchCount(a);
|
||||
let right = spawn fetchCount(b);
|
||||
return await left + await right;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Current status:
|
||||
|
||||
- synchronous code enters async execution with `runAsync(...)`, which returns `AsyncCompletion[T]`
|
||||
- `async fn` bodies may use `await`, and async roots run through the native C backend
|
||||
- `spawn` starts child work inside a `nursery`, and `await` accepts either direct async calls or `SpawnHandle[Return]`
|
||||
- async calls must be explicitly `await`ed or `spawn`ed
|
||||
- the nursery is currently the runtime unit of concurrent work: child tasks are created inside a nursery and drained when that nursery exits
|
||||
- nursery exit joins outstanding child tasks before the scope completes
|
||||
- nursery capabilities (`nursery`) and child task handles (`SpawnHandle[Return]`) are restricted compiler-known capability types
|
||||
- cancellation and timing helpers are available through `cancel`, `cancelAfter`, `cancelAt`, `clearDeadline`, `extendDeadline`, `deadline`, `checkpoint`, `sleep`, `sleepUntil`, `waitReadable`, and `waitWritable`
|
||||
- `shield { ... }` defers outer cancellation long enough to finish a small cleanup section, but direct cancellation of the current nursery still pierces the shield
|
||||
- compile-time evaluation rejects async with an error by design
|
||||
- the stdlib already uses this runtime for operations such as `net.getAddrInfo`
|
||||
|
||||
Peon also accepts `coroutine fn` as a compatibility spelling for `async fn`, but
|
||||
`async fn` is the preferred syntax in documentation.
|
||||
|
||||
For async function declaration syntax, see [Functions -- Async functions](functions.md#async-functions).
|
||||
|
||||
For design background and older planning notes around the coroutine/task model, see
|
||||
[structured-concurrency-plan.md](../structured-concurrency-plan.md).
|
||||
|
||||
For design notes covering typed generators, coroutine lowering, and non-escaping frame placement,
|
||||
see [generators-and-coroutines-plan.md](../generators-and-coroutines-plan.md).
|
||||
|
||||
## The problem with unstructured concurrency
|
||||
|
||||
Most languages today offer some form of "fire and forget" concurrency -- `go` in Go,
|
||||
`spawn` in Erlang, `threading.Thread` in Python, `Task.Run` in C#, or even raw
|
||||
`pthread_create`. These primitives all share the same fundamental flaw: they allow a
|
||||
function to launch background work that outlives the function itself. When a function
|
||||
returns, there is no guarantee that all the concurrent work it started has finished.
|
||||
|
||||
This breaks the basic abstraction boundary of a function call. If `foo()` spawns a
|
||||
background task, callers of `foo` cannot reason about when `foo` is truly done, what
|
||||
resources it still holds, or whether errors in its background work will ever be observed.
|
||||
The spawned task floats free in the program's control flow graph, invisible to the caller,
|
||||
unaccounted for by the type system, and disconnected from the surrounding control flow.
|
||||
|
||||
The blog post [Notes on structured concurrency, or: Go statement considered harmful](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/)
|
||||
by Nathaniel J. Smith draws an analogy to the historical transition from `goto` to
|
||||
structured programming. Unrestricted `goto` allowed control to jump anywhere, making it
|
||||
impossible to reason locally about what a piece of code does. Languages eliminated `goto`
|
||||
in favor of structured control flow (`if`, `while`, functions) that guarantees control
|
||||
always enters a block at the top and leaves at the bottom. The same argument applies to
|
||||
concurrency: an unrestricted spawn is a concurrent `goto`. It allows control flow to fork
|
||||
without any guarantee that the branches will rejoin.
|
||||
|
||||
## What structured concurrency provides
|
||||
|
||||
Structured concurrency imposes a simple discipline: **concurrent tasks form a tree that
|
||||
mirrors the lexical structure of the code**.
|
||||
|
||||
The key rules are:
|
||||
|
||||
1. **Child tasks are scoped to a block.** Concurrent work is started inside a dedicated
|
||||
scope (often called a *nursery* or *task group*). The scope cannot exit until all tasks
|
||||
started within it have completed.
|
||||
|
||||
2. **Tasks form a tree.** Every task has a parent. The root task is the program's entry
|
||||
point. Child tasks can start their own children, forming a tree. No task can outlive
|
||||
its parent.
|
||||
|
||||
3. **Failures propagate upward.** If a child task fails, that failure is delivered to the
|
||||
parent scope instead of being silently dropped.
|
||||
|
||||
4. **Cancellation propagates downward.** If a parent scope is cancelled (e.g. because one
|
||||
child failed and the parent is unwinding), all remaining children in that scope are
|
||||
cancelled before the parent finishes cleaning up.
|
||||
|
||||
The result is that a function call remains a black box: when `foo()` returns, all
|
||||
concurrent work it started has finished, all errors have been reported, and all resources
|
||||
have been released. Callers do not need to know or care whether `foo` used concurrency
|
||||
internally.
|
||||
|
||||
## Current rules
|
||||
|
||||
The currently implemented compiler and runtime enforce these rules:
|
||||
|
||||
- `await` is only valid inside `async fn`
|
||||
- `spawn` is only valid inside `async fn`
|
||||
- `shield` is only valid inside `async fn`
|
||||
- bare `spawn` must appear inside an active `nursery`
|
||||
- `spawn call(...) in tasks` may target an explicitly passed nursery capability
|
||||
- `await` accepts either a direct async call or a `SpawnHandle[Return]`
|
||||
- `runAsync(...)` is the sync-to-async entry point for a root task and is rejected inside `async fn`
|
||||
- async calls in expression position are rejected unless they are immediately awaited or spawned
|
||||
- nursery capabilities and `SpawnHandle[...]` values cannot be returned from a function
|
||||
|
||||
Nursery capabilities are intentionally restricted. They may be bound by `nursery name { ... }`,
|
||||
passed to helper functions as parameters of type `nursery`, and used as explicit `spawn ...
|
||||
in name` targets, but they are not ordinary first-class runtime objects.
|
||||
|
||||
Similarly, a `SpawnHandle[Return]` is only meant to be awaited inside the nursery subtree
|
||||
that created it. Today it is essentially a convenience wrapper for "something you may
|
||||
`await` later" rather than a user-facing general-purpose `Task[T]` object with an API of
|
||||
its own.
|
||||
|
||||
Peon also does not expose first-class coroutine objects for ordinary async calls. Calling
|
||||
an async function does not manufacture a heap object that can be stored, passed around,
|
||||
and maybe forgotten. The language only accepts async calls in compiler-known async
|
||||
positions such as `await f(...)`, `spawn f(...)`, or `runAsync(f(...))`.
|
||||
|
||||
This restriction is deliberate:
|
||||
|
||||
- in most cases, the useful information is just the function plus its arguments, not an
|
||||
inert coroutine object
|
||||
- allowing first-class coroutine objects would reintroduce the old "did I forget to
|
||||
await this?" failure mode when an unstarted coroutine is dropped
|
||||
- `await` exists to make suspension explicit in the control flow, not to require async
|
||||
calls to behave like ordinary value-producing expressions everywhere
|
||||
|
||||
## Why this matters for Peon
|
||||
|
||||
Peon's existing design already values lexical scoping and deterministic resource
|
||||
management. The [generational reference model](memory-model.md) gives each allocation a
|
||||
precise owner and a deterministic destruction point. Structured concurrency extends the
|
||||
same philosophy to concurrent work:
|
||||
|
||||
- **Resource safety.** Because child tasks cannot outlive their parent scope, scope-based
|
||||
cleanup remains correct even in the presence of concurrency. A resource opened before a
|
||||
concurrent scope is guaranteed to still be valid for all tasks within it, and cleanup
|
||||
after the scope is guaranteed to happen after all tasks finish.
|
||||
|
||||
- **Failure reporting.** Unstructured concurrency silently drops failures from background
|
||||
tasks. Structured concurrency ensures every failure has a place to go: up the task tree
|
||||
to the enclosing scope.
|
||||
|
||||
- **Cancellation.** Cancelling a scope means cancelling a well-defined subtree of work.
|
||||
There are no orphaned tasks left running after the thing that cared about their result
|
||||
has moved on.
|
||||
|
||||
- **Local reasoning.** A programmer reading a Peon function can understand its concurrent
|
||||
behavior from its lexical structure alone, without tracing transitive spawns through
|
||||
the entire program.
|
||||
|
||||
- **Composability.** A function that internally uses a concurrent scope looks identical to
|
||||
a sequential function from the outside. This means concurrent building blocks compose
|
||||
the same way sequential ones do.
|
||||
|
||||
## What Peon will not do
|
||||
|
||||
Peon will not provide unstructured primitives like a bare `spawn` or `go` that starts a
|
||||
task with an unbounded lifetime. All concurrent work will be lexically scoped and
|
||||
tree-structured. This is a deliberate restriction: the loss of expressiveness is minimal
|
||||
(any pattern expressible with unstructured concurrency can be restructured into a
|
||||
tree-shaped scope), while the gains in safety and composability are substantial.
|
||||
|
||||
## Generators
|
||||
|
||||
Generators are suspendable functions that produce a sequence of values lazily.
|
||||
A generator is declared with the `generator` keyword before `fn` and must return
|
||||
`Generator[Yield, Send, Return]`:
|
||||
|
||||
```peon
|
||||
generator fn range(start: int64, stop: int64): Generator[int64, void, void] {
|
||||
var i = start;
|
||||
while i < stop {
|
||||
yield i;
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Yield` is the type of values produced by `yield`. `Send` is the type that
|
||||
callers can send back into the generator on each resumption (use `void` when
|
||||
no value is sent). `Return` is the final return type when the generator
|
||||
completes (use `void` for generators that simply exhaust).
|
||||
|
||||
The generator itself is not executed when called. Calling a generator function
|
||||
returns a `Generator` value. Advancing it is done via the `start` and `resume`
|
||||
builtins, which return `GeneratorState[Yield, Return]`:
|
||||
|
||||
```peon
|
||||
type GeneratorState[Yield, Return] = enum {
|
||||
Yielded { value: Yield; },
|
||||
Complete { value: Return; }
|
||||
}
|
||||
```
|
||||
|
||||
For the common case, the `next` builtin wraps start/resume into a single call
|
||||
that returns `Option[Yield]`:
|
||||
|
||||
```peon
|
||||
var g = range(0, 3);
|
||||
match next(g) {
|
||||
case Some(v) { print(v); } # 0
|
||||
case None { }
|
||||
}
|
||||
```
|
||||
|
||||
Generators are the basis for [`for` loop](control-flow.md#for-loops) iteration.
|
||||
|
||||
### Yield from (delegation)
|
||||
|
||||
A generator can delegate to another generator with `yield from`:
|
||||
|
||||
```peon
|
||||
generator fn chain(a: Generator[int64, void, void],
|
||||
b: Generator[int64, void, void]): Generator[int64, void, void] {
|
||||
yield from a;
|
||||
yield from b;
|
||||
}
|
||||
```
|
||||
|
||||
`yield from` yields all values from the delegated generator, forwarding send
|
||||
values through, and evaluates to the delegated generator's return value when
|
||||
it completes.
|
||||
|
||||
### Borrow safety in generators
|
||||
|
||||
Generators that yield borrowed values (`lent T` or `mut lent T`) must yield
|
||||
from stable storage owners -- values behind a `Buffer[T]` or `seq[T]`, not
|
||||
from raw locals or arrays whose backing memory can move. This ensures that
|
||||
yielded borrows remain valid across suspension points.
|
||||
|
||||
The compiler can usually infer borrowed return provenance through common
|
||||
wrapper chains, including owner-cell projections such as `value.storage[].data[index]`.
|
||||
When inference still cannot prove which parameter owns the returned borrow,
|
||||
Peon provides `#pragma[lentFrom: param]` as an explicit fallback:
|
||||
|
||||
```peon
|
||||
fn first[T](value: lent Wrapper[T]): lent T {
|
||||
#pragma[lentFrom: value]
|
||||
return helper(value);
|
||||
}
|
||||
```
|
||||
|
||||
This is similar in spirit to a small lifetime annotation: it tells the
|
||||
[borrow checker](memory-model.md#escape-analysis) that the returned `lent T` is
|
||||
tied to `value`. It does not change runtime behavior or code generation; it only
|
||||
refines compile-time escape analysis. In normal code you should prefer inference
|
||||
and only use `lentFrom` when the compiler reports unknown borrowed provenance for
|
||||
a wrapper API.
|
||||
270
docs/manual/control-flow.md
Normal file
270
docs/manual/control-flow.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Control Flow
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## `if`/`else`
|
||||
|
||||
```
|
||||
if x > 0 {
|
||||
print("positive");
|
||||
} else {
|
||||
print("non-positive");
|
||||
}
|
||||
```
|
||||
|
||||
## `while` loops
|
||||
|
||||
```
|
||||
var i = 0;
|
||||
while i < 10 {
|
||||
print(i);
|
||||
i = i + 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Named blocks
|
||||
|
||||
Named blocks provide labeled control flow for breaking out of nested constructs:
|
||||
|
||||
```
|
||||
block outer {
|
||||
block inner {
|
||||
print("Yup");
|
||||
break outer; # Jumps past the outer block
|
||||
}
|
||||
print("Never printed.");
|
||||
}
|
||||
print("And we land here");
|
||||
```
|
||||
|
||||
`continue` on a named block jumps back to its beginning, which can be used to create
|
||||
loops:
|
||||
|
||||
```
|
||||
var i = 5;
|
||||
block loop {
|
||||
if i > 0 {
|
||||
i = i - 1;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
print(i == 0); # true
|
||||
```
|
||||
|
||||
In loops, `break` and `continue` without a label apply to the innermost `while` loop.
|
||||
Inside named blocks, a label is required -- bare `break` inside a named block is an error.
|
||||
|
||||
## Pattern matching
|
||||
|
||||
`match` expressions destructure values, especially useful with
|
||||
[enums](types.md#enumeration-types):
|
||||
|
||||
```
|
||||
|
||||
var value = some(42);
|
||||
|
||||
match value {
|
||||
case Some(v) {
|
||||
print(v); # 42
|
||||
}
|
||||
case None {
|
||||
print("nope");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Match can destructure named fields:
|
||||
|
||||
```
|
||||
type Result = enum {
|
||||
Ok { value: int64; },
|
||||
Err { code: int64; }
|
||||
}
|
||||
|
||||
fn unwrap(result: Result): int64 {
|
||||
match result {
|
||||
case Ok(value = value) {
|
||||
return value;
|
||||
}
|
||||
case Err(code = code) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Value enums match by variant name without payload bindings:
|
||||
|
||||
```peon
|
||||
type SocketFamily = enum {
|
||||
AnyFamily = 0,
|
||||
Unix = 1,
|
||||
Inet = 2
|
||||
}
|
||||
|
||||
match family {
|
||||
case Unix { print("unix"); }
|
||||
case Inet { print("inet"); }
|
||||
else { print("other"); }
|
||||
}
|
||||
```
|
||||
|
||||
Match also works on plain values with an `else` fallback:
|
||||
|
||||
```
|
||||
match x {
|
||||
case 1 { print("one"); }
|
||||
else { print("other"); }
|
||||
}
|
||||
```
|
||||
|
||||
Multiple patterns can share one arm with `|`:
|
||||
|
||||
```
|
||||
type Result = enum {
|
||||
Ok { value: int64; },
|
||||
Err { code: int64; }
|
||||
}
|
||||
|
||||
fn unwrap(result: Result): int64 {
|
||||
match result {
|
||||
case Ok(value) | Err(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As in Rust, every alternative in the same arm must bind the same names with the same
|
||||
types. This is valid:
|
||||
|
||||
```
|
||||
case Ok(value) | Err(value) { ... }
|
||||
```
|
||||
|
||||
This is rejected because the bindings differ:
|
||||
|
||||
```
|
||||
case Ok(value) | Err(code) { ... }
|
||||
```
|
||||
|
||||
Bare `|` in a `case` label is parsed as a pattern separator, not as a bitwise-or
|
||||
expression. If you want to match against the value of an ordinary `|` expression, wrap it
|
||||
in parentheses:
|
||||
|
||||
```
|
||||
match x {
|
||||
case 1 | 2 { print("small"); } # Two alternative patterns
|
||||
case (4 | 8) { print("twelve"); } # One expression pattern
|
||||
else { print("other"); }
|
||||
}
|
||||
```
|
||||
|
||||
Non-void functions must have exhaustive matches: the compiler rejects enum matches that
|
||||
don't cover all variants, and plain-value matches without an `else` branch are flagged as
|
||||
not covering all control-flow paths.
|
||||
|
||||
### Borrow matching
|
||||
|
||||
Use `match &x` when you want to destructure through a borrow without moving payload fields
|
||||
out of `x`. In that form, payload bindings are borrowed views rather than owned values:
|
||||
|
||||
```
|
||||
type Result = enum {
|
||||
Ok { value: int64; },
|
||||
Err { code: int64; }
|
||||
}
|
||||
|
||||
fn read(result: Result): int64 {
|
||||
match &result {
|
||||
case Ok(value) {
|
||||
return value[];
|
||||
}
|
||||
case Err(code) {
|
||||
return code[];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `for` loops
|
||||
|
||||
`for`/`in` loops iterate over generators (see [Concurrency -- Generators](concurrency.md#generators)),
|
||||
built-in collections such as arrays and sequences, direct iterator values, or
|
||||
types implementing the [`Iterable` interface](interfaces.md#iterator-and-iterable-protocols):
|
||||
|
||||
```peon
|
||||
for x in range(0, 5) {
|
||||
print(x);
|
||||
}
|
||||
```
|
||||
|
||||
Arrays and sequences provide built-in `items` generators:
|
||||
|
||||
```peon
|
||||
let arr = [10, 20, 30];
|
||||
for x in arr {
|
||||
print(x[]); # 10, 20, 30 (x is lent int64, deref to get value)
|
||||
}
|
||||
|
||||
var s = @[1, 2, 3];
|
||||
for x in s {
|
||||
print(x[]); # 1, 2, 3
|
||||
}
|
||||
```
|
||||
|
||||
Mutable iteration uses `mitems`:
|
||||
|
||||
```peon
|
||||
var s = @[1, 2, 3];
|
||||
for x in mitems(s) {
|
||||
x[] = x[] + 10;
|
||||
}
|
||||
# s is now [11, 12, 13]
|
||||
```
|
||||
|
||||
For collections, the `for` loop desugars into:
|
||||
|
||||
```
|
||||
var __iter = items(collection);
|
||||
while true {
|
||||
match next(__iter) {
|
||||
case Some(x) { <body> }
|
||||
case None { break; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the loop expression is already an `Iterator` (including `BorrowIterator`,
|
||||
`MutBorrowIterator`, or `Generator`), the loop consumes it directly instead of
|
||||
calling `iter()`. All iterators advance through `next`.
|
||||
|
||||
Types can participate in `for` loops by implementing the iterator or iterable protocols.
|
||||
See [Interfaces -- Iterator and Iterable protocols](interfaces.md#iterator-and-iterable-protocols)
|
||||
for the full protocol definitions.
|
||||
|
||||
## Control flow expressions
|
||||
|
||||
`if`, `when`, `match`, and bare blocks can all be used as expressions. The value of the
|
||||
expression is the last expression in the taken branch:
|
||||
|
||||
```
|
||||
fn test(flag: bool): int64 {
|
||||
let a = if flag { 1 } else { 2 };
|
||||
|
||||
let b = when true { 3 } else { 4 };
|
||||
|
||||
let c = { let x = 7; x }; # Block expression
|
||||
|
||||
let d = match a {
|
||||
case 1 { c }
|
||||
else { b }
|
||||
};
|
||||
|
||||
return d;
|
||||
}
|
||||
```
|
||||
|
||||
`while` is statement-only. It cannot be used in expression position.
|
||||
|
||||
For compile-time `when` conditionals, see [Compile-Time Features](compile-time.md#compile-time-conditionals-when).
|
||||
282
docs/manual/functions.md
Normal file
282
docs/manual/functions.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Functions
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## Basic functions
|
||||
|
||||
```
|
||||
fn fib(n: int): int {
|
||||
if n < 2 {
|
||||
return n;
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2);
|
||||
}
|
||||
|
||||
fib(30);
|
||||
```
|
||||
|
||||
Functions with no return type default to `void`, and `void` is also a real unit
|
||||
type you can use in annotations and [generics](generics.md):
|
||||
|
||||
```peon
|
||||
fn greet() {
|
||||
print("hello");
|
||||
}
|
||||
|
||||
let done: Result[void, string] = Ok();
|
||||
```
|
||||
|
||||
Bare `return;` is valid in void functions.
|
||||
|
||||
## Async functions
|
||||
|
||||
Async functions are declared with `async fn`:
|
||||
|
||||
```peon
|
||||
async fn child(): int64 {
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
Calling an async function is not a normal fire-and-forget operation. The call must be
|
||||
used in a compiler-known async position, typically `await child()` or `spawn child()`.
|
||||
The compatibility spelling `coroutine fn` is accepted, but `async fn` is preferred.
|
||||
|
||||
Peon intentionally does not treat an async call as an ordinary first-class coroutine
|
||||
value. You cannot instantiate a coroutine object, store it for later, and rely on some
|
||||
later code to remember to start it. If you need to defer work, the useful payload is
|
||||
usually just the original function plus its arguments. This avoids the familiar bug class
|
||||
where a coroutine object is created, never awaited, and eventually discarded without
|
||||
ever running. In Peon, `await` is primarily a marker that suspension may happen at this
|
||||
point in control flow; it does not imply that async calls must behave like ordinary
|
||||
expression values in every context.
|
||||
|
||||
Native execution enters an async root with `runAsync(...)`, which returns an
|
||||
`AsyncCompletion[T]`:
|
||||
|
||||
```peon
|
||||
match runAsync(child()) {
|
||||
case Completed(value) {
|
||||
print(value);
|
||||
}
|
||||
case Cancelled {
|
||||
print("cancelled");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Function types may also be async:
|
||||
|
||||
```peon
|
||||
async fn run(job: async fn(): int64): int64 {
|
||||
return await job();
|
||||
}
|
||||
```
|
||||
|
||||
Async code runs on the native C backend today. Compile-time evaluation still rejects
|
||||
async code by design.
|
||||
|
||||
For the full structured concurrency model (nurseries, spawn, cancellation), see
|
||||
[Structured Concurrency](concurrency.md).
|
||||
|
||||
## Nested functions
|
||||
|
||||
Functions can be defined inside other functions:
|
||||
|
||||
```
|
||||
fn outer(): int64 {
|
||||
fn inner(): int64 {
|
||||
return 1;
|
||||
}
|
||||
return inner();
|
||||
}
|
||||
```
|
||||
|
||||
## Forward declarations
|
||||
|
||||
A function can be forward-declared with a semicolon and no body, then defined later in
|
||||
the same module:
|
||||
|
||||
```
|
||||
fn someF(): int64; # Forward declaration
|
||||
|
||||
print(someF()); # Prints 42
|
||||
|
||||
fn someF(): int64 {
|
||||
return 42;
|
||||
}
|
||||
```
|
||||
|
||||
## Named arguments and default parameters
|
||||
|
||||
Function calls support positional and named (keyword) arguments. All positional arguments
|
||||
must come before any keyword arguments:
|
||||
|
||||
```
|
||||
fn greet(name: string, loud: bool) {
|
||||
if loud {
|
||||
print(name);
|
||||
}
|
||||
}
|
||||
|
||||
greet("Alice", true); # Positional
|
||||
greet("Bob", loud = false); # Mixed
|
||||
greet(loud = true, name = "Eve"); # All keyword (any order)
|
||||
```
|
||||
|
||||
Parameters can have default values. A parameter with a default may be omitted at the call
|
||||
site:
|
||||
|
||||
```
|
||||
fn connect(host: string, port: int64 = 8080) {
|
||||
print(host);
|
||||
print(port);
|
||||
}
|
||||
|
||||
connect("localhost"); # port defaults to 8080
|
||||
connect("localhost", 3000); # explicit port
|
||||
```
|
||||
|
||||
Positional parameters without defaults cannot follow parameters that have defaults.
|
||||
|
||||
Parameters are immutable bindings by default, even though ordinary parameters are still
|
||||
passed by value. Reassigning a parameter is an error; if you want a mutable local copy,
|
||||
re-declare it explicitly:
|
||||
|
||||
```
|
||||
fn bump(x: int64): int64 {
|
||||
var copy = x;
|
||||
copy += 1;
|
||||
return copy;
|
||||
}
|
||||
```
|
||||
|
||||
## Mutable borrow parameters
|
||||
|
||||
Peon does not support `var` parameter syntax. If a callee needs writable access to
|
||||
caller-owned storage, spell the parameter as `mut lent T` and pass an explicit mutable
|
||||
borrow in an ordinary call:
|
||||
|
||||
```
|
||||
fn bump(x: mut lent int64) {
|
||||
x = x + 1;
|
||||
}
|
||||
|
||||
var value = 3;
|
||||
bump(mut borrow(value));
|
||||
print(value); # 4
|
||||
```
|
||||
|
||||
The borrow keeps referring to the caller's storage, so writes in the callee are visible
|
||||
after the call returns. See [Memory Model -- Borrows](memory-model.md#borrows-and-lent)
|
||||
for the full borrowing rules.
|
||||
|
||||
Immutable borrows are auto-inserted whenever a stable value is used where `lent T` is
|
||||
required. Mutable auto-borrows are narrower: regular `f(x)` calls still require
|
||||
`mut borrow(x)` (or `mut &x`) when they introduce a mutable borrow from plain storage,
|
||||
but an argument that is already `mut lent T` can be passed through regular calls
|
||||
without another reborrow. [Assignment operators](operators.md#assignment-operators)
|
||||
and UFCS method-style calls such as `x += 1` or `values.append(item)` still auto-borrow
|
||||
mutably because the mutation is already syntactically obvious.
|
||||
|
||||
Mutable borrow parameters can also appear in function types:
|
||||
|
||||
```
|
||||
fn applyTwice(f: fn(x: mut lent int64), x: mut lent int64) {
|
||||
f(x);
|
||||
f(x);
|
||||
}
|
||||
```
|
||||
|
||||
## Function calls and UFCS
|
||||
|
||||
```
|
||||
foo(1, 2 + 3, 3.14, bar(baz));
|
||||
```
|
||||
|
||||
Code like `a.b()` is desugared to `b(a)` if there exists a function `b` whose signature
|
||||
is compatible with the value of `a` and `a` does not already have a field named `b`. This
|
||||
is known as Uniform Function Call Syntax (UFCS).
|
||||
|
||||
Borrow insertion follows the same rule. Stable values are always auto-borrowed when a
|
||||
call expects `lent T`. For `mut lent T`, the UFCS form `a.b()` auto-borrows mutably, and
|
||||
regular call syntax accepts arguments that are already `mut lent T`; an explicit mutable
|
||||
borrow is still required only when the call needs to create that borrow from plain storage.
|
||||
|
||||
## Lambdas and closures
|
||||
|
||||
Lambdas are anonymous functions:
|
||||
|
||||
```
|
||||
let inc = lambda(x: int64): int64 {
|
||||
return x + 1;
|
||||
};
|
||||
|
||||
print(inc(4)); # 5
|
||||
```
|
||||
|
||||
Lambdas can be immediately invoked:
|
||||
|
||||
```
|
||||
fn run(): int64 {
|
||||
return lambda(x: int64): int64 {
|
||||
return x + 1;
|
||||
}(4);
|
||||
}
|
||||
```
|
||||
|
||||
### Higher-order functions
|
||||
|
||||
Lambdas are first-class values. Function types are written `fn(params): returnType`.
|
||||
If the callable itself is unsafe to call, write `unsafe fn(params): returnType`
|
||||
(see [Unsafe -- Unsafe function types](unsafe.md#unsafe-function-types)):
|
||||
|
||||
```
|
||||
fn makeInc(): fn(x: int64): int64 {
|
||||
return lambda(x: int64): int64 {
|
||||
return x + 1;
|
||||
};
|
||||
}
|
||||
|
||||
fn apply(f: fn(x: int64): int64, value: int64): int64 {
|
||||
return f(value);
|
||||
}
|
||||
|
||||
let inc = makeInc();
|
||||
print(apply(inc, 4)); # 5
|
||||
```
|
||||
|
||||
### Closures
|
||||
|
||||
Lambdas capture variables from their enclosing scope. Primitives are captured by value:
|
||||
|
||||
```
|
||||
fn makeAdder(base: int64): fn(x: int64): int64 {
|
||||
return lambda(x: int64): int64 {
|
||||
return x + base;
|
||||
};
|
||||
}
|
||||
|
||||
let add2 = makeAdder(2);
|
||||
print(add2(4)); # 6
|
||||
```
|
||||
|
||||
Borrowed values can be captured in non-escaping closures (immediately invoked):
|
||||
|
||||
```
|
||||
type Box = ref object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
fn read(view: lent Box): int64 {
|
||||
return lambda(): int64 {
|
||||
return view.value;
|
||||
}();
|
||||
}
|
||||
|
||||
var b = Box(value = 1);
|
||||
print(read(b)); # 1
|
||||
```
|
||||
|
||||
Capturing a borrowed value in an escaping closure (one that is returned or stored) is
|
||||
rejected. Capturing mutable locals is not yet implemented.
|
||||
140
docs/manual/generics.md
Normal file
140
docs/manual/generics.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Generics
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon generics use [parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism).
|
||||
Generic functions are typechecked at declaration time against their constraints, not at
|
||||
each call site like C++ templates. The compiler then monomorphizes them -- generating
|
||||
specialized code for each concrete type used.
|
||||
|
||||
```
|
||||
fn genericSum[T: Number](a, b: T): T {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
genericSum(1, 2);
|
||||
genericSum(3.14, 0.1);
|
||||
genericSum(1'u8, 250'u8);
|
||||
```
|
||||
|
||||
An unconstrained generic cannot use operations that are not defined for all types:
|
||||
|
||||
```
|
||||
fn add[T](a, b: T): T {
|
||||
return a + b; # Error: cannot prove + is defined on all types
|
||||
}
|
||||
```
|
||||
|
||||
This is intentional. To make it compile, constrain `T` to types that support `+`:
|
||||
|
||||
```
|
||||
fn add[T: Number](a, b: T): T {
|
||||
return a + b; # OK: + is defined for all Number types
|
||||
}
|
||||
```
|
||||
|
||||
## Type union constraints
|
||||
|
||||
A constraint like `T: int32 | int` means `T` can be any one of the listed types.
|
||||
When a generic function with a union constraint calls another function, the compiler
|
||||
uses deferred dispatch: it collects all matching candidates during type checking, then
|
||||
re-resolves the call during monomorphization once the concrete type is known. This means
|
||||
callees can be individual non-generic overloads:
|
||||
|
||||
```
|
||||
fn foo(x: int32): int32 {
|
||||
return x;
|
||||
}
|
||||
|
||||
fn foo(x: int): int {
|
||||
return x;
|
||||
}
|
||||
|
||||
fn identity[T: int | int32](x: T): T {
|
||||
return foo(x); # OK: dispatches to the matching foo overload
|
||||
}
|
||||
```
|
||||
|
||||
The callee does not need to be generic over the same union -- any set of overloads that
|
||||
covers all members of the constraint is sufficient.
|
||||
|
||||
## Interface constraints
|
||||
|
||||
[Interfaces](interfaces.md) can also be used in generic constraints. Use `&` to require
|
||||
multiple interfaces at once:
|
||||
|
||||
```peon
|
||||
fn debugCopy[T: Copy & Printable](value: T) {
|
||||
print(clone(value));
|
||||
}
|
||||
```
|
||||
|
||||
This is an intersection: `T` must satisfy all listed interfaces. The same syntax works
|
||||
for generic type declarations:
|
||||
|
||||
```peon
|
||||
type Buffer[T: Copy & Printable] = object {
|
||||
value: T;
|
||||
}
|
||||
```
|
||||
|
||||
## Generic types
|
||||
|
||||
Types can be generic:
|
||||
|
||||
```
|
||||
type Box*[T] = object {
|
||||
value: T;
|
||||
}
|
||||
|
||||
var boxFloat = Box[float64](value = 1.0);
|
||||
var boxInt = Box[int64](value = 1);
|
||||
```
|
||||
|
||||
Generic types support const parameters for compile-time values:
|
||||
|
||||
```
|
||||
type array[N: const int64, T] = object {
|
||||
#pragma[magic: "array"]
|
||||
}
|
||||
```
|
||||
|
||||
__Note__: The `*` [visibility modifier](modules.md#visibility) must be placed __before__
|
||||
the generic constraints: `fn foo*[T](a: T) {}` and `type Box*[T] = object { ... }`.
|
||||
|
||||
## Static compile-time parameters
|
||||
|
||||
Generic functions also support static compile-time parameters directly in their
|
||||
parameter list:
|
||||
|
||||
```
|
||||
fn repeat(count: const int64, value: string): string {
|
||||
when count == 0 {
|
||||
return "";
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Current behavior:
|
||||
|
||||
- Static function parameters are supported for integer and `bool` types.
|
||||
- Call arguments for static parameters must be compile-time evaluable.
|
||||
- Static parameters participate in specialization like hidden const-generic parameters.
|
||||
- Specialized runtime functions do not keep static parameters in their runtime ABI.
|
||||
|
||||
For more on compile-time evaluation and `when`, see [Compile-Time Features](compile-time.md).
|
||||
|
||||
## `typevar` parameters
|
||||
|
||||
A `typevar[T]` parameter accepts a type itself as a value, allowing generic functions to
|
||||
be called with a type argument rather than a value. Several builtins use this pattern:
|
||||
|
||||
```
|
||||
var cell = new(int64); # typevar[int64] parameter
|
||||
let buf = create(int64, 4); # typevar[int64] + count
|
||||
print(low(int32)); # minimum value of int32
|
||||
print(high(float64)); # inf
|
||||
print(sizeof(int64)); # compile-time size in bytes
|
||||
```
|
||||
75
docs/manual/index.md
Normal file
75
docs/manual/index.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Peon - Manual
|
||||
|
||||
Peon is a statically typed programming language with a native C backend and a
|
||||
restricted bytecode VM used for compile-time evaluation. This manual documents the
|
||||
language features that are currently implemented.
|
||||
|
||||
Peon ~~steals~~ borrows many ideas from Python, Nim (the language peon itself is written
|
||||
in), Rust, C and many others.
|
||||
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Basics](basics.md) -- variables, comments, strings, primitive types
|
||||
- [Type Declarations](types.md) -- objects, tuples, ref objects, enums, standard library types
|
||||
- [Functions](functions.md) -- functions, async, lambdas, closures, UFCS
|
||||
- [Control Flow](control-flow.md) -- if/else, while, for, pattern matching, blocks
|
||||
- [Generics](generics.md) -- parametric polymorphism, constraints, static parameters
|
||||
- [Interfaces](interfaces.md) -- iface declarations, built-in interfaces, iterator protocols
|
||||
- [Operators](operators.md) -- overloading, postfix, assignment, `?`/try, `is`
|
||||
- [Memory Model](memory-model.md) -- ownership, generational references, borrows, lifecycle hooks
|
||||
- [Unsafe](unsafe.md) -- unsafe blocks, unsafe fn, raw pointers, unmanaged memory
|
||||
- [Structured Concurrency](concurrency.md) -- nurseries, spawn/await, generators
|
||||
- [Compile-Time Features](compile-time.md) -- `when`/`defined()`, CTFE, pragmas
|
||||
- [Modules](modules.md) -- import/export, visibility, module execution
|
||||
- [C Interop](c-interop.md) -- importc, exportc, C-compatible types, converters
|
||||
- [Grammar](../grammar.md)
|
||||
- [Bytecode](../bytecode.md)
|
||||
|
||||
|
||||
## Current Scope
|
||||
|
||||
The current implementation includes:
|
||||
|
||||
- Variables with `var`, `let`, and `const`
|
||||
- Plain objects, structural `tuple`s, `ref` forms, enums, and constructors
|
||||
- Functions, lambdas, closures, and forward declarations
|
||||
- Named/keyword arguments and default parameter values
|
||||
- Generics with constrained parametric polymorphism
|
||||
- Static function parameters written as `name: const T`
|
||||
- Interfaces with `iface`, `object with ...`, and generic interface constraints
|
||||
- Operator overloading and UFCS-style call rewriting
|
||||
- A memory model based on generational references
|
||||
- Borrows (`lent`) and mutable borrows (`mut lent`)
|
||||
- Pattern matching with `match`/`case`
|
||||
- Compile-time conditionals with `when`
|
||||
- Canonical compile-time config symbols via `defined(...)`
|
||||
- Named blocks with labeled `break`/`continue`
|
||||
- Control flow expressions (`if`, `when`, `match`, and blocks as expressions)
|
||||
- Static arrays (`array[N, T]`) with bounds checking
|
||||
- Standard-library sequences (`seq[T]`) with borrowed indexing
|
||||
- Generators (`generator fn`) with `yield`, `yield from`, and the `next()` iterator protocol
|
||||
- Async functions and structured concurrency on the native C backend (`async fn`, `await`, `nursery`, `spawn`, `shield`, `runAsync`)
|
||||
- Native async execution through the C backend, including stdlib networking helpers
|
||||
- `for`/`in` iteration over generators, arrays, sequences, iterables, and direct iterators
|
||||
- Explicit `unsafe { ... }` blocks for low-level unchecked operations
|
||||
- Raw pointers via `pointer`, `ptr T`, and `ptr UncheckedArray[T]`
|
||||
- Unmanaged memory via `alloc`, `create`, `realloc`, `dealloc`, `copyMem`, and `addr`
|
||||
- Modules with `import`, `from`/`import`/`as`, and `export`
|
||||
- Pragmas for compiler directives (`#pragma[...]`)
|
||||
- C interop via `importc`, `exportc`, `header`, and `noDecl` pragmas
|
||||
- C-compatible types (`cint`, `cstring`, `cdouble`, etc.) and explicit type converters
|
||||
- A native C backend
|
||||
|
||||
## Future Plans
|
||||
|
||||
Planned or desired future work includes:
|
||||
|
||||
- A richer import and package system
|
||||
- Richer interface features such as dynamic interface values or dispatch if they become necessary
|
||||
- Multithreading support
|
||||
- More built-in collection types
|
||||
- `assert` statements (currently parsed but not yet compiled)
|
||||
- A working LSP server
|
||||
- A fully self-hosted Peon compiler
|
||||
- A Peon-written toolchain
|
||||
119
docs/manual/interfaces.md
Normal file
119
docs/manual/interfaces.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Interfaces
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon has a static **interface system** broadly similar in spirit to Rust traits.
|
||||
Interfaces declare required function signatures:
|
||||
|
||||
```peon
|
||||
iface Foo {
|
||||
fn bar(lent self): int64;
|
||||
fn baz(self, name: string): bool;
|
||||
}
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
Interfaces can inherit other interfaces with `from`:
|
||||
|
||||
```peon
|
||||
iface Tagged from Named, Printable {
|
||||
fn tag(self): string;
|
||||
}
|
||||
```
|
||||
|
||||
## Conformance
|
||||
|
||||
Object types declare implemented interfaces with `with`:
|
||||
|
||||
```peon
|
||||
type Stuff = object with Foo {
|
||||
}
|
||||
|
||||
fn bar(self: lent Stuff): int64 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn baz(self: Stuff, name: string): bool {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
An object can list multiple interfaces: `type X = object with A, B { ... }`.
|
||||
Both value `object` and `ref object` declarations may list interfaces with `with`.
|
||||
|
||||
If implementing or inheriting an interface carries extra unchecked obligations, declare
|
||||
it as `unsafe iface`. See [Unsafe -- Unsafe interfaces](unsafe.md#unsafe-interfaces) for
|
||||
the full rules and examples.
|
||||
|
||||
## Current semantics
|
||||
|
||||
- Interfaces are checked statically. There is no runtime vtable or trait-object
|
||||
representation.
|
||||
- Requirements are satisfied by free functions in the same module as the implementing
|
||||
object declaration. If a requirement writes `self`, `lent self`, or `mut self`, that
|
||||
receiver is matched against the implementing concrete type. If no receiver is written,
|
||||
the checker still assumes an implicit by-value `self` parameter.
|
||||
- Interface declarations are signature-only. Requirements cannot define bodies or default
|
||||
arguments. Receiver shorthand is only valid inside interface requirements. Interface
|
||||
requirements may themselves be `unsafe fn`.
|
||||
- Interfaces participate in [generic constraints](generics.md#interface-constraints),
|
||||
but they are currently a conformance/constraint feature rather than a general-purpose
|
||||
runtime type.
|
||||
|
||||
## Built-in interfaces
|
||||
|
||||
The stdlib exposes a few built-in interfaces with compiler-aware semantics:
|
||||
|
||||
- `Copy` means a value can be duplicated with `clone(x)`. User-defined value types may
|
||||
satisfy it structurally, or explicitly via a `clone=`
|
||||
[lifecycle hook](memory-model.md#lifecycle-hooks). `ref object` types may opt in with
|
||||
`with Copy`; cloning such a value duplicates the referent into a fresh managed
|
||||
allocation.
|
||||
- `ImplicitCopy` extends `Copy`. Values with `ImplicitCopy` may be duplicated
|
||||
automatically in by-value assignments, argument passing, and returns instead of being
|
||||
moved. Built-in scalars and `string` implement `ImplicitCopy`; arrays and `seq` do not.
|
||||
- `Printable` requires `toString(lent self): string` and is what the generic `print(x)`
|
||||
builtin relies on.
|
||||
|
||||
## Iterator and Iterable protocols
|
||||
|
||||
The standard library exposes static iterator and iterable protocols with borrow-aware
|
||||
variants. These protocols are used by [`for` loops](control-flow.md#for-loops) to
|
||||
iterate over collections and custom types.
|
||||
|
||||
```peon
|
||||
iface Iterator[T] {
|
||||
fn next(mut self): Option[T];
|
||||
}
|
||||
|
||||
iface BorrowIterator[lent T] from Iterator[T] {
|
||||
# Yields borrowed elements
|
||||
}
|
||||
|
||||
iface MutBorrowIterator[mut lent T] from Iterator[T] {
|
||||
# Yields mutable borrowed elements
|
||||
}
|
||||
|
||||
iface Iterable[T] {
|
||||
fn iter(lent self): Iterator[T];
|
||||
}
|
||||
|
||||
iface BorrowIterable[lent T] {
|
||||
fn items(lent self): BorrowIterator[lent T];
|
||||
}
|
||||
|
||||
iface MutBorrowIterable[mut lent T] {
|
||||
fn mitems(mut self): MutBorrowIterator[mut lent T];
|
||||
}
|
||||
```
|
||||
|
||||
`Iterator` is the base cursor protocol. `BorrowIterator` and
|
||||
`MutBorrowIterator` are subtypes for iterators that yield borrowed or
|
||||
mutable-borrowed elements respectively. `Iterable`, `BorrowIterable`, and
|
||||
`MutBorrowIterable` are collection-style adapters that produce the
|
||||
corresponding iterator type via `iter()`, `items()`, and `mitems()`.
|
||||
|
||||
Types can participate in `for` loops by implementing the direct iterator
|
||||
protocol (`Iterator[T]`), or by implementing one of the collection adapter
|
||||
protocols.
|
||||
262
docs/manual/memory-model.md
Normal file
262
docs/manual/memory-model.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Memory Model
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon manages heap-allocated values using **generational references** -- a scheme that
|
||||
provides memory safety without a garbage collector and without the full complexity of
|
||||
Rust's borrow checker.
|
||||
|
||||
## Ownership and references
|
||||
|
||||
There are four kinds of handles to data in Peon:
|
||||
|
||||
- **Plain values** (`T`) -- inline value storage. Integers, floats, booleans, arrays,
|
||||
and plain `object` types are plain values. Assignment and argument passing duplicate
|
||||
[`ImplicitCopy`](interfaces.md#built-in-interfaces) values and otherwise move by default.
|
||||
- **Managed references** (`ref T`) -- heap-allocated, owning handles. Created with
|
||||
[`ref object`](types.md#ref-objects) types or with `new(T)`. The runtime tracks their liveness.
|
||||
- **Borrows** (`lent T`) -- non-owning, read-only views into an existing value.
|
||||
Created with `borrow(x)` or the `&` operator. Cannot outlive the value they borrow from.
|
||||
- **Mutable borrows** (`mut lent T`) -- non-owning, writable views.
|
||||
|
||||
```
|
||||
type Box = ref object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
var b = Box(value = 42); # Managed reference, heap-allocated
|
||||
let view = borrow(b); # Borrow: non-owning read-only view
|
||||
```
|
||||
|
||||
## Lifecycle hooks
|
||||
|
||||
User-defined value types can customize their behavior with hooks:
|
||||
|
||||
- `clone=` for duplication
|
||||
- `move=` for destructive transfer
|
||||
- `destroy=` for cleanup
|
||||
|
||||
`ref object` types can also define a custom `destroy=` hook. That hook runs during the
|
||||
automatic destruction path for the managed reference; explicit `destroy(x)` calls are only
|
||||
for value types and are rejected for managed refs.
|
||||
|
||||
If a user-defined value type or `ref object` type does not define the relevant hooks, the
|
||||
compiler uses the default behavior. The important distinction is:
|
||||
|
||||
- `clone(x)` only works for `T: Copy` and always preserves the source.
|
||||
- `move(src, dst)` is always destructive and leaves `src` moved-from.
|
||||
- Ordinary by-value assignment, initialization, argument passing, and returns clone only
|
||||
for `ImplicitCopy` types; everything else is transferred with move semantics.
|
||||
|
||||
[`Copy` and `ImplicitCopy`](interfaces.md#built-in-interfaces) are built-in interfaces.
|
||||
|
||||
`clone=` always takes one by-value source parameter and returns the hooked type:
|
||||
|
||||
```peon
|
||||
fn `clone=`(src: Box): Box {
|
||||
return defaultClone();
|
||||
}
|
||||
```
|
||||
|
||||
That same shape is used for both plain `object` types and `ref object` types, so refs may
|
||||
override cloning when they need custom duplication logic for payload-managed resources.
|
||||
`defaultClone()` is only valid inside a `clone=` hook and performs the compiler's
|
||||
default cloning behavior for the hooked type.
|
||||
|
||||
`move=` uses source-first ordering and both parameters must be `mut lent`:
|
||||
|
||||
```peon
|
||||
fn `move=`(src: mut lent Box, dst: mut lent Box) {
|
||||
defaultMove();
|
||||
}
|
||||
```
|
||||
|
||||
The source is a mutable borrow because moving is destructive: the hook is allowed to
|
||||
leave the source in a moved-from state. `defaultMove()` is only valid inside a `move=`
|
||||
hook and performs the compiler's default move for that type. Only plain user-defined
|
||||
value types may define `move=`; `ref object` handle moves are fixed language behavior
|
||||
and cannot be hooked.
|
||||
|
||||
`destroy=` takes a single `mut lent` parameter:
|
||||
|
||||
```peon
|
||||
fn `destroy=`(value: mut lent Box) {
|
||||
defaultDestroy();
|
||||
}
|
||||
```
|
||||
|
||||
`defaultDestroy()` is only valid inside a `destroy=` hook and delegates to the
|
||||
compiler's default cleanup logic.
|
||||
|
||||
For `ref object` hooks, the `mut lent Box` parameter borrows the owning `Box` handle
|
||||
slot itself. Hooks still run in place; they do not take ownership as a by-value `Box`
|
||||
argument.
|
||||
|
||||
Explicit `destroy(x)` is only valid for storage locations that may actually need owned
|
||||
cleanup work. It is rejected for managed refs, bare borrowed handles (`lent T`,
|
||||
`mut lent T`, and `const T` bindings), and plain no-op types such as integers.
|
||||
Destroying projected substorage such as a field or indexed slot is still valid inside
|
||||
owner cleanup code. Generic cleanup code should guard explicit destruction with
|
||||
`needsDestroy(T)`:
|
||||
|
||||
```peon
|
||||
fn `destroy=`[T](values: mut lent seq[T]) {
|
||||
var i = 0;
|
||||
while i < values.len() {
|
||||
when needsDestroy(T) {
|
||||
destroy(values[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
defaultDestroy();
|
||||
}
|
||||
```
|
||||
|
||||
This matters for containers of borrows such as `seq[lent T]`: destroying the container
|
||||
still frees the container's own storage, but it must not recursively destroy the
|
||||
borrowed referents.
|
||||
|
||||
## How generational references work
|
||||
|
||||
Every managed allocation is assigned a random nonzero 64-bit **generation ID** when it
|
||||
is created. A reference stores three things:
|
||||
|
||||
1. A pointer to the payload (the actual data).
|
||||
2. A pointer to the **owner cell** -- a piece of side metadata that holds the current
|
||||
generation of the allocation.
|
||||
3. The **remembered generation** -- the generation that was live when the reference was
|
||||
formed.
|
||||
|
||||
A reference is valid if and only if:
|
||||
|
||||
```
|
||||
owner.current_gen != 0 && owner.current_gen == remembered_gen
|
||||
```
|
||||
|
||||
When an allocation is freed, its owner cell's generation is set to zero. If any stale
|
||||
reference is later used, the runtime detects the mismatch and panics. If the same memory
|
||||
slot is reused for a new allocation, it gets a fresh random generation, so stale
|
||||
references from the old allocation will not accidentally match.
|
||||
|
||||
This scheme is similar to Rust's ownership model in that it prevents use-after-free at
|
||||
runtime, but differs in important ways:
|
||||
|
||||
- There is no compile-time borrow checker that forbids aliasing. Multiple references to
|
||||
the same allocation can coexist freely.
|
||||
- Safety is enforced at runtime through generation checks, not at compile time through
|
||||
lifetime analysis.
|
||||
- The compiler does perform **escape analysis** on `lent` values to prevent returning a
|
||||
borrow that refers to local storage, but it does not enforce Rust-style exclusivity
|
||||
rules between mutable and immutable borrows.
|
||||
|
||||
## Interior references
|
||||
|
||||
References to fields, array elements, and slices all carry the same `(ptr, owner, gen)`
|
||||
triple. The `ptr` points at the interior location, while the owner cell still belongs to
|
||||
the containing object. This means a field reference becomes invalid the moment its
|
||||
containing object is freed, just like a whole-object reference would.
|
||||
|
||||
```
|
||||
type Pair = ref object {
|
||||
left: int64;
|
||||
right: int64;
|
||||
}
|
||||
|
||||
var p = Pair(left = 1, right = 2);
|
||||
# A reference to p.left is valid as long as p is alive.
|
||||
# If p is freed, any reference to p.left will fail at runtime.
|
||||
```
|
||||
|
||||
## Mutability
|
||||
|
||||
Mutability is a **compile-time** property. The runtime only tracks liveness, not whether
|
||||
a handle is mutable or immutable. The surface types are:
|
||||
|
||||
| Handle | Meaning |
|
||||
|--------|---------|
|
||||
| `ref T` | Managed owning reference, read-only through this handle |
|
||||
| `mut ref T` | Managed owning reference, mutable through this handle |
|
||||
| `lent T` | Non-owning borrow, read-only |
|
||||
| `mut lent T` | Non-owning borrow, mutable |
|
||||
|
||||
The compiler rejects writes through immutable handles at type-check time.
|
||||
|
||||
## Deterministic destruction
|
||||
|
||||
The compiler inserts destroy/free calls at ownership boundaries:
|
||||
|
||||
- End of local scope for owning values.
|
||||
- Reassignment of owning locals (the old value is destroyed before overwrite).
|
||||
- `return` statements destroy locals that are not being returned.
|
||||
|
||||
There is no garbage collector and no reference counting. Deallocation is deterministic
|
||||
and happens at precisely the points the compiler identifies.
|
||||
|
||||
## Borrows and `lent`
|
||||
|
||||
A borrow creates a non-owning view of an existing value. The `borrow` builtin (or the
|
||||
`&` operator) creates borrows. Borrowed values have type `lent T`:
|
||||
|
||||
```
|
||||
fn read(view: lent Box): int64 {
|
||||
return view.value;
|
||||
}
|
||||
|
||||
var b = Box(value = 42);
|
||||
print(read(b)); # 42
|
||||
```
|
||||
|
||||
Stable values are automatically borrowed whenever a `lent T` is required, so both
|
||||
`read(b)` and `let view: lent Box = b` work without an explicit `borrow(...)`.
|
||||
|
||||
### Escape analysis
|
||||
|
||||
The compiler performs escape analysis to prevent returning a borrow that refers to local
|
||||
storage. When inference cannot determine provenance through wrapper functions,
|
||||
`#pragma[lentFrom: param]` can be used as an explicit hint
|
||||
(see [Concurrency -- Borrow safety in generators](concurrency.md#borrow-safety-in-generators)).
|
||||
|
||||
```
|
||||
fn bad(): lent int64 {
|
||||
var x = 55;
|
||||
return borrow(x); # Error: cannot return borrowed value that refers to local storage
|
||||
}
|
||||
```
|
||||
|
||||
This analysis tracks provenance through function calls. Even when a borrow is passed
|
||||
through a helper function, the compiler infers that the returned borrow may refer to
|
||||
local storage and rejects it:
|
||||
|
||||
```
|
||||
fn passthrough(x: lent int64): lent int64 {
|
||||
return x;
|
||||
}
|
||||
|
||||
fn bad(): lent int64 {
|
||||
var x = 55;
|
||||
return passthrough(x); # Error: still refers to local storage
|
||||
}
|
||||
```
|
||||
|
||||
Forwarding a borrow from a parameter (not local storage) is allowed:
|
||||
|
||||
```
|
||||
fn passthrough(x: lent int64): lent int64 {
|
||||
return x; # OK: x comes from the caller
|
||||
}
|
||||
```
|
||||
|
||||
The same rule applies to borrows derived from mutable borrow parameters:
|
||||
|
||||
```
|
||||
type Box = object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
fn view(box: mut lent Box): lent int64 {
|
||||
return box.value; # OK: the borrow is derived from caller-owned storage
|
||||
}
|
||||
```
|
||||
|
||||
For mutable borrow parameters in function signatures, see
|
||||
[Functions -- Mutable borrow parameters](functions.md#mutable-borrow-parameters).
|
||||
94
docs/manual/modules.md
Normal file
94
docs/manual/modules.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Modules
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon code is organized into modules (one file per module). The standard library is
|
||||
available by default, and the `import` statement brings other modules' exported names
|
||||
into scope:
|
||||
|
||||
```
|
||||
import helper; # Import another module
|
||||
```
|
||||
|
||||
Selective imports with aliasing:
|
||||
|
||||
```
|
||||
from helper import used as alias;
|
||||
print(alias());
|
||||
```
|
||||
|
||||
Qualified access to module members:
|
||||
|
||||
```
|
||||
import helper;
|
||||
print(helper.used());
|
||||
```
|
||||
|
||||
## Visibility
|
||||
|
||||
The `*` modifier makes a name visible outside its module:
|
||||
|
||||
```
|
||||
type Point* = object {
|
||||
x: int64;
|
||||
y: int64;
|
||||
}
|
||||
|
||||
fn distance*(a, b: Point): float64 {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
Without `*`, names are private to the module they are defined in. Unused private
|
||||
declarations are not compiled into the final output.
|
||||
|
||||
The `*` modifier must be placed before [generic](generics.md) constraints:
|
||||
`fn foo*[T](a: T) {}` and `type Box*[T] = object { ... }`.
|
||||
|
||||
## Re-exports
|
||||
|
||||
Modules re-export their imports with `export`:
|
||||
|
||||
```
|
||||
import builtins/values;
|
||||
export values;
|
||||
```
|
||||
|
||||
## Module execution
|
||||
|
||||
When a module is imported for the first time, all of its top-level statements
|
||||
are executed in order. This means that any expressions, variable
|
||||
initializations, or side-effecting code at the top level of an imported module
|
||||
will run once, at the point where the import is first encountered. Subsequent
|
||||
imports of the same module (from other files) do not re-execute its top-level
|
||||
code -- each module is loaded and executed at most once per compilation.
|
||||
|
||||
This is relevant for modules that perform initialization work (e.g., setting up
|
||||
global state or printing diagnostics) at the top level: that work happens
|
||||
exactly once, during the first import.
|
||||
|
||||
## `isMainModule`
|
||||
|
||||
The compiler automatically manages the `isMainModule` compile-time define. It
|
||||
is `true` only for the module that is the compilation entry point (the file
|
||||
passed to the compiler on the command line) and `false` for every imported
|
||||
module. This lets a module include code that only runs when it is compiled
|
||||
directly, not when it is imported as a library:
|
||||
|
||||
```
|
||||
fn helper*(): int64 {
|
||||
return 42;
|
||||
}
|
||||
|
||||
when defined("isMainModule") {
|
||||
# This block only executes when this file is the entry point,
|
||||
# not when another module imports it.
|
||||
print(helper());
|
||||
}
|
||||
```
|
||||
|
||||
`isMainModule` is a compiler-managed define and cannot be overridden by
|
||||
`-d:isMainModule` or `#pragma[define]`.
|
||||
|
||||
For more on `when` and `defined()`, see
|
||||
[Compile-Time Features](compile-time.md#compile-time-conditionals-when).
|
||||
283
docs/manual/operators.md
Normal file
283
docs/manual/operators.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# Operators
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## Operator overloading
|
||||
|
||||
```
|
||||
type Vec2 = object {
|
||||
x: int64;
|
||||
y: int64;
|
||||
}
|
||||
|
||||
operator `+`(a, b: Vec2): Vec2 {
|
||||
return Vec2(x = a.x + b.x, y = a.y + b.y);
|
||||
}
|
||||
|
||||
Vec2(x = 1, y = 3) + Vec2(x = 2, y = 3); # Vec2(x = 3, y = 6)
|
||||
```
|
||||
|
||||
Custom operators (e.g. `` `dot` ``) can also be defined. The backticks mark the name as an
|
||||
identifier rather than a symbol. Even the built-in arithmetic operators are defined as peon
|
||||
stubs with `#pragma[magic: ...]` and are then specialized by the compiler.
|
||||
|
||||
Operators can be called as regular functions:
|
||||
|
||||
```
|
||||
`+`(1, 2) # Identical to 1 + 2
|
||||
```
|
||||
|
||||
## Postfix operators
|
||||
|
||||
Operators can be declared as postfix using `#pragma[postfix]`. A postfix operator
|
||||
takes exactly one parameter and is written after its operand:
|
||||
|
||||
```
|
||||
operator `++`*(x: int32): int32 {
|
||||
#pragma[postfix]
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
var y = x++; # postfix: applies ++ after x
|
||||
```
|
||||
|
||||
Postfix operators bind tighter than prefix: `-x++` means `-(x++)`.
|
||||
|
||||
An operator is either prefix or postfix -- declaring the same operator as both is
|
||||
not currently supported.
|
||||
|
||||
## Assignment operators
|
||||
|
||||
Operators whose name ends with `=` (excluding comparison operators `==`, `!=`, `>=`,
|
||||
`<=`) are *assignment operators*. They must return `void` -- they mutate their first
|
||||
argument in place and are statements, not expressions. This matches Rust's `AddAssign`
|
||||
semantics.
|
||||
|
||||
```
|
||||
operator `+=`*(a: mut lent int64, b: int64) {
|
||||
a = a + b;
|
||||
}
|
||||
|
||||
var x = 1;
|
||||
x += 2; # x is now 3
|
||||
```
|
||||
|
||||
The index-assignment operator `[]=` follows the same rule:
|
||||
|
||||
```
|
||||
operator `[]=`*[T](self: mut lent seq[T], index: int64, value: T) {
|
||||
# mutates self in place, returns nothing
|
||||
}
|
||||
```
|
||||
|
||||
Attempting to declare an assignment operator with a return type is a compile-time error.
|
||||
|
||||
## The `?` operator and `#pragma[try]`
|
||||
|
||||
The `?` postfix operator provides Rust-style early-return unwrapping for enum types
|
||||
annotated with `#pragma[try]`. When applied to a value:
|
||||
|
||||
- If the value matches the **success variant**, `?` extracts the specified field.
|
||||
- If the success variant has no payload, `?` succeeds with a `void` value.
|
||||
- If the value matches the **error variant**, `?` early-returns from the enclosing
|
||||
function with an error value of the function's return type.
|
||||
|
||||
Any enum type can opt into `?` by adding `#pragma[try]` to its declaration:
|
||||
|
||||
```
|
||||
type MyResult[T, E] = enum {
|
||||
#pragma[try(ok: "Success", value: "result", err: "Failure", error: "reason")]
|
||||
Success { result: T; },
|
||||
Failure { reason: E; }
|
||||
}
|
||||
```
|
||||
|
||||
### Named arguments
|
||||
|
||||
| Argument | Required | Meaning |
|
||||
|----------|----------|---------|
|
||||
| `ok` | yes | Name of the success variant (as a string) |
|
||||
| `value` | no | Name of the field to extract from the success variant; omit it when the specialized success variant has no payload |
|
||||
| `err` | yes | Name of the error variant (as a string) |
|
||||
| `error` | no | Name of the field to copy from the error variant on early return |
|
||||
|
||||
When `error` is omitted, the error variant is assumed to carry no payload
|
||||
(like `Option`'s `None`). When it is present, the error variant's payload is
|
||||
forwarded into the enclosing function's return type on early return.
|
||||
|
||||
The enclosing function must return a type that also has `#pragma[try]`. When the error
|
||||
variant has a payload, the error field types must match between the expression's type
|
||||
and the function's return type. The enclosing function cannot be `void` -- `?` needs a
|
||||
return type to propagate the error into.
|
||||
|
||||
### Usage with standard library types
|
||||
|
||||
Both [`Option[T]` and `Result[T, E]`](types.md#standard-library-types) in the standard
|
||||
library carry the pragma and support `?` out of the box:
|
||||
|
||||
```peon
|
||||
fn parse(input: string): Option[int64] {
|
||||
# ...
|
||||
}
|
||||
|
||||
fn process(input: string): Option[int64] {
|
||||
let n = parse(input)?; # Unwraps Some, or returns None
|
||||
return some(n + 1);
|
||||
}
|
||||
```
|
||||
|
||||
```peon
|
||||
fn readFile(path: string): Result[string, string] {
|
||||
# ...
|
||||
}
|
||||
|
||||
fn processFile(path: string): Result[int64, string] {
|
||||
let contents = readFile(path)?; # Unwraps Ok, or returns Err
|
||||
return Ok(value = len(contents));
|
||||
}
|
||||
```
|
||||
|
||||
```peon
|
||||
fn divide(a: int64, b: int64): Result[int64, string] {
|
||||
if b == 0 {
|
||||
return Err(error = "division by zero");
|
||||
}
|
||||
return Ok(value = a / b);
|
||||
}
|
||||
|
||||
fn compute(): Result[int64, string] {
|
||||
var x = divide(10, 2)?; # unwraps Ok(5), or returns Err
|
||||
var y = divide(x, 0)?; # returns Err("division by zero")
|
||||
return Ok(value = y);
|
||||
}
|
||||
```
|
||||
|
||||
Generic try-compatible enums can still name a success field even when a concrete
|
||||
specialization erases it:
|
||||
|
||||
```peon
|
||||
fn ensureDir(path: string): Result[void, string] {
|
||||
# ...
|
||||
return Ok();
|
||||
}
|
||||
|
||||
fn build(path: string): Result[int64, string] {
|
||||
var pending = ensureDir(path);
|
||||
var ready = pending?;
|
||||
return Ok(value = 1);
|
||||
}
|
||||
```
|
||||
|
||||
### The `?` operator declaration
|
||||
|
||||
The `?` operator itself is declared as a postfix builtin in the standard
|
||||
library:
|
||||
|
||||
```peon
|
||||
operator `?`*[T, E](value: Result[T, E]): T {
|
||||
#pragma[postfix, magic: "try"]
|
||||
}
|
||||
|
||||
operator `?`*[T](value: Option[T]): T {
|
||||
#pragma[postfix, magic: "try"]
|
||||
}
|
||||
```
|
||||
|
||||
User-defined types do not need to redeclare the operator -- the standard library
|
||||
declarations are generic and work with any `#pragma[try]`-annotated enum as
|
||||
long as the variant and field names match.
|
||||
|
||||
## The `is` operator
|
||||
|
||||
The `is` operator performs identity comparison. For value types it checks bitwise
|
||||
identity; for managed references it checks whether two handles refer to the same
|
||||
allocation:
|
||||
|
||||
```
|
||||
var a = 1;
|
||||
var b = 1;
|
||||
print(a is b); # true (same value)
|
||||
|
||||
var x = Box(value = 1);
|
||||
var y = x;
|
||||
print(x is y); # true (same object)
|
||||
```
|
||||
|
||||
## Defining custom converters
|
||||
|
||||
The `converter` [pragma](compile-time.md#declaration-pragmas) marks a single-parameter
|
||||
function as a type converter. Defining a converter from `T` to `A` enables the syntax
|
||||
`A(expr)` as a shorthand for calling the converter -- this looks like a constructor call
|
||||
but dispatches to the converter function:
|
||||
|
||||
```peon
|
||||
fn myConvert(x: MyTypeA): MyTypeB {
|
||||
#pragma[converter]
|
||||
# ...
|
||||
}
|
||||
|
||||
let a: MyTypeA = ...;
|
||||
let b: MyTypeB = myConvert(a); # Direct call
|
||||
let c: MyTypeB = MyTypeB(a); # Equivalent: constructor-style syntax via converter
|
||||
```
|
||||
|
||||
The [C interop module](c-interop.md) uses this mechanism, so C types can be constructed
|
||||
from Peon values with constructor syntax:
|
||||
|
||||
```peon
|
||||
var x: int64 = 42;
|
||||
let n = cint(x); # Calls the int64 -> cint converter
|
||||
let y = int64(n); # Calls the cint -> int64 converter
|
||||
```
|
||||
|
||||
Converter requirements:
|
||||
- Exactly one parameter (no mutable reference parameters)
|
||||
- Non-void return type
|
||||
|
||||
## Defining custom constructors
|
||||
|
||||
The `constructor` [pragma](compile-time.md#declaration-pragmas) marks a named top-level
|
||||
function as a constructor candidate for call syntax on its return type. This lets you
|
||||
expose `T(...)` without forcing users to spell a separate helper such as `newT()`:
|
||||
|
||||
```peon
|
||||
type Event = object {
|
||||
signaled: bool;
|
||||
}
|
||||
|
||||
fn Event(): Event {
|
||||
#pragma[constructor]
|
||||
return Event(signaled = false);
|
||||
}
|
||||
|
||||
let evt = Event();
|
||||
```
|
||||
|
||||
Constructor requirements:
|
||||
- Top-level named function only
|
||||
- Non-generic
|
||||
- Non-void return type
|
||||
- Return type must be a named type
|
||||
- Function name must exactly match the return type name
|
||||
|
||||
Resolution rules:
|
||||
- Structural object construction and enum-variant construction still work as before
|
||||
- Pragma constructors join the same `T(...)` candidate set
|
||||
- If multiple constructor candidates match, including a mix of structural and pragma
|
||||
constructors, the call is rejected as ambiguous
|
||||
|
||||
This is distinct from `#pragma[converter]`: converters reinterpret `T(expr)` as a type
|
||||
conversion, while pragma constructors let a user-defined function act as the constructor
|
||||
for that type itself.
|
||||
|
||||
### Named pragma arguments
|
||||
|
||||
[Pragmas](compile-time.md#pragmas) support named arguments inside parenthesized argument
|
||||
lists:
|
||||
|
||||
```
|
||||
#pragma[try(ok: "Ok", value: "value", err: "Err", error: "error")]
|
||||
```
|
||||
|
||||
Named arguments use `key: value` syntax. They can be mixed with positional arguments
|
||||
in the same pragma.
|
||||
366
docs/manual/types.md
Normal file
366
docs/manual/types.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Type Declarations
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
## Plain objects
|
||||
|
||||
Plain objects are value types with inline storage and normal copy/move semantics:
|
||||
|
||||
```
|
||||
type Point = object {
|
||||
x: int64;
|
||||
y: int64;
|
||||
}
|
||||
|
||||
let point = Point(x = 3, y = 4);
|
||||
print(point.x); # 3
|
||||
```
|
||||
|
||||
Object types are nominal. If two modules both declare `type Foo = object { ... }`, those
|
||||
types stay distinct even when their field sets match exactly.
|
||||
|
||||
Fields can be read with dot syntax and assigned on mutable values:
|
||||
|
||||
```
|
||||
var pair = Point(x = 1, y = 2);
|
||||
pair.x = 7;
|
||||
print(pair.x); # 7
|
||||
```
|
||||
|
||||
## Tuple types
|
||||
|
||||
Tuple types use the same field syntax as objects, but they are structural instead of
|
||||
nominal. A tuple value matches a tuple type when the field names and field types match,
|
||||
regardless of where the type was declared.
|
||||
|
||||
```peon
|
||||
type Point = tuple {
|
||||
x: int64;
|
||||
y: int64;
|
||||
}
|
||||
|
||||
let point: Point = (y: 4, x: 3);
|
||||
print(point.x); # 3
|
||||
```
|
||||
|
||||
Tuple literals use parentheses plus `name: value` entries. Field order does not matter:
|
||||
|
||||
```peon
|
||||
let a = (left: 1, right: 2);
|
||||
let b = (right: 2, left: 1);
|
||||
```
|
||||
|
||||
The same parenthesized field syntax also works in type position for anonymous tuple
|
||||
types:
|
||||
|
||||
```peon
|
||||
fn project(point: (x: int64, y: int64)): int64 {
|
||||
return point.x;
|
||||
}
|
||||
|
||||
let point: (x: int64, y: int64) = (y: 4, x: 3);
|
||||
```
|
||||
|
||||
This means tuple construction is structural: the compiler checks field names and field
|
||||
types, not declaration identity. In contrast, ordinary object construction stays nominal
|
||||
and uses the usual call-style constructor syntax:
|
||||
|
||||
```peon
|
||||
type Pair = object {
|
||||
left: int64;
|
||||
right: int64;
|
||||
}
|
||||
|
||||
type PairView = tuple {
|
||||
left: int64;
|
||||
right: int64;
|
||||
}
|
||||
|
||||
let view: PairView = (right: 2, left: 1); # OK
|
||||
let pair = Pair(left = 1, right = 2); # OK
|
||||
# let bad: Pair = (left: 1, right: 2); # Error: objects are nominal
|
||||
```
|
||||
|
||||
Peon also supports custom constructor functions through `#pragma[constructor]`
|
||||
(see [Operators -- Defining custom constructors](operators.md#defining-custom-constructors)).
|
||||
|
||||
Tuple values must still spell out field names at construction time. Peon does not treat
|
||||
`(x, y)` as shorthand for `(x: x, y: y)`, because tuple field names are part of the
|
||||
structural type itself rather than just positional slots.
|
||||
|
||||
Like objects, tuples support dot-field access and can be declared in managed form with
|
||||
`ref tuple` when you want a heap-allocated structural payload.
|
||||
|
||||
## Ref objects
|
||||
|
||||
Ref objects are heap-allocated [managed references](memory-model.md):
|
||||
|
||||
```
|
||||
type Box = ref object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
var b = Box(value = 42);
|
||||
print(b.value); # 42
|
||||
```
|
||||
|
||||
They can also be defined as a `ref` alias to an existing plain type:
|
||||
|
||||
```
|
||||
type BoxData = object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
type Box = ref BoxData;
|
||||
|
||||
let b = Box(value = 7);
|
||||
```
|
||||
|
||||
Ref objects are dereferenced with `[]`:
|
||||
|
||||
```
|
||||
var b = Box(value = 1);
|
||||
b[] = Box(value = 7)[]; # Whole-payload dereference and assignment
|
||||
```
|
||||
|
||||
Heap-allocated values of any type can be created with `new`:
|
||||
|
||||
```
|
||||
var cell = new(int64);
|
||||
cell[] = 42;
|
||||
print(cell[]); # 42
|
||||
|
||||
var boxed = new(Pair);
|
||||
boxed.left = 7;
|
||||
```
|
||||
|
||||
## Type aliases
|
||||
|
||||
Types can be aliased:
|
||||
|
||||
```
|
||||
type IntBox = Box[int64];
|
||||
```
|
||||
|
||||
## Enumeration types
|
||||
|
||||
Peon supports two enum forms:
|
||||
|
||||
- Value enums: payloadless variants, optionally with explicit integral values
|
||||
- Payload enums: tagged unions, where variants may carry named fields
|
||||
|
||||
These two forms are intentionally disjoint. A single enum declaration cannot mix
|
||||
payload-bearing variants with explicit discriminants.
|
||||
|
||||
### Payload enums
|
||||
|
||||
Payload enums are tagged unions. Each variant can optionally carry fields:
|
||||
|
||||
```
|
||||
type Result = enum {
|
||||
Ok { value: int64; },
|
||||
Err { message: string; }
|
||||
}
|
||||
|
||||
let ok = Ok(value = 1);
|
||||
let err = Err(message = "failed");
|
||||
```
|
||||
|
||||
### Value enums
|
||||
|
||||
Value enums are closed sets of integer-backed values:
|
||||
|
||||
```peon
|
||||
type SocketKind = enum {
|
||||
Stream = 1,
|
||||
Datagram,
|
||||
Raw = 7
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- every variant must be payloadless
|
||||
- explicit discriminants must be integral expressions
|
||||
- omitted discriminants auto-increment from the previous value
|
||||
- the first omitted discriminant defaults to `0`
|
||||
- matching uses bare variant names such as `case Stream`
|
||||
|
||||
Imported integral C constants may also be used as discriminants
|
||||
(see [C Interop](c-interop.md#importing-c-constants-importc-on-const)):
|
||||
|
||||
```peon
|
||||
const TEST_STREAM: cint; #pragma[importc: "TEST_STREAM", header: "<value_enum.h>"]
|
||||
|
||||
type SocketKind = enum {
|
||||
Stream = TEST_STREAM,
|
||||
Datagram = 9
|
||||
}
|
||||
```
|
||||
|
||||
This is rejected because it mixes the two models:
|
||||
|
||||
```peon
|
||||
type Bad = enum {
|
||||
Good = 1,
|
||||
Broken { value: int64; }
|
||||
}
|
||||
```
|
||||
|
||||
## Standard library types
|
||||
|
||||
The standard library provides `Option[T]` and `Result[T, E]`:
|
||||
|
||||
```peon
|
||||
|
||||
var some42 = some(42);
|
||||
var noneInt = none(int);
|
||||
|
||||
let done: Result[void, string] = Ok();
|
||||
|
||||
match done {
|
||||
case Ok { print("done"); }
|
||||
case Err(error) { print(error); }
|
||||
}
|
||||
```
|
||||
|
||||
Both `Option[T]` and `Result[T, E]` support the [`?` operator](operators.md#the--operator-and-pragmatry)
|
||||
for concise error propagation.
|
||||
|
||||
For pattern matching on enum types, see [Control Flow -- Pattern matching](control-flow.md#pattern-matching).
|
||||
|
||||
### Static arrays
|
||||
|
||||
The built-in `array[N, T]` type constructor creates fixed-size arrays. Arrays can be
|
||||
spelled directly, named with a type alias, or inferred from an array literal:
|
||||
|
||||
```
|
||||
|
||||
type IntVec = array[3, int64];
|
||||
|
||||
var a = IntVec(1, 2, 3);
|
||||
var b = [1, 2, 3, 4]; # Inferred as array[4, int64]
|
||||
|
||||
print(a[0]); # 1
|
||||
a[0] = 9;
|
||||
print(len(a)); # 3
|
||||
print(a[2]); # 3
|
||||
```
|
||||
|
||||
Array access is bounds-checked at runtime when `boundChecks` is enabled
|
||||
(see [Compile-Time Features -- Config symbols](compile-time.md#config-symbols)). Out-of-bounds
|
||||
access panics.
|
||||
|
||||
Plain arrays (non-ref) are value types. Assignment moves the array -- to avoid implicit
|
||||
copies, the source becomes inaccessible after the move:
|
||||
|
||||
```
|
||||
var a = [1, 2, 3];
|
||||
var b = a; # a is moved into b
|
||||
b[0] = 9;
|
||||
# print(a[0]); # Error: a has been moved
|
||||
print(b[0]); # 9
|
||||
```
|
||||
|
||||
To create an independent copy, use `clone` explicitly:
|
||||
|
||||
```
|
||||
var a = [1, 2, 3];
|
||||
var b = clone(a); # Explicit copy
|
||||
b[0] = 9;
|
||||
print(a[0]); # 1 -- a is unchanged
|
||||
```
|
||||
|
||||
Ref arrays are heap-allocated and can be mutably borrowed:
|
||||
|
||||
```
|
||||
type RefVec = ref array[2, int64];
|
||||
|
||||
var a = RefVec(4, 5);
|
||||
var b = mut borrow(a);
|
||||
b[1] = 7;
|
||||
print(a[1]); # 7 -- a and b share the same data
|
||||
```
|
||||
|
||||
### Sequences (`seq[T]`)
|
||||
|
||||
The stdlib provides `seq[T]` as a growable collection:
|
||||
|
||||
```peon
|
||||
|
||||
type Box = object {
|
||||
value: int64;
|
||||
}
|
||||
|
||||
var boxes = @[Box(value = 1), Box(value = 2)];
|
||||
let first: mut lent Box = boxes[0];
|
||||
first.value = 9;
|
||||
|
||||
var ints = @[1, 2, 3];
|
||||
ints[1] = 7;
|
||||
print(ints[1][]); # Scalar reads use whole-deref
|
||||
```
|
||||
|
||||
Important `seq` indexing semantics:
|
||||
|
||||
- `value[index]` returns a borrow, not an owned `T`.
|
||||
- Immutable indexing yields `lent T`.
|
||||
- Indexing through a mutable borrow of `seq[T]` yields `mut lent T`.
|
||||
- `value[index] = newValue` returns `mut lent T`.
|
||||
- `unsafe { value.get_unchecked(index) }` returns the same borrow shapes as
|
||||
`value[index]`, but skips the bounds check for that one access.
|
||||
- When the element is a scalar or you need an owned value, use whole-deref:
|
||||
`value[index][]`.
|
||||
|
||||
`seq[T]` is a runtime collection. It is available in native code, but compile-time
|
||||
evaluation rejects it because CTFE does not permit heap-backed sequence storage.
|
||||
Field access works directly through borrows, so `boxes[0].value` is valid.
|
||||
|
||||
Implementation note: `seq[T]` stores its payload through an internal `buffer:
|
||||
Buffer[T]`
|
||||
owner cell instead of keeping only a raw pointer in the sequence header. This is
|
||||
what allows element borrows and iterator yields to stay tied to the backing
|
||||
storage owner. If growth moves the raw payload, the buffer can swap to a new
|
||||
owner so old element borrows go stale cleanly instead of silently dangling into
|
||||
moved memory.
|
||||
|
||||
Sequences support `append` and standard queries:
|
||||
|
||||
```peon
|
||||
|
||||
var s = @[1, 2, 3];
|
||||
s.append(4);
|
||||
print(len(s)); # 4
|
||||
print(s[3][]); # 4
|
||||
```
|
||||
|
||||
The `@` prefix operator converts a static array to a `seq`:
|
||||
|
||||
```peon
|
||||
var arr = [1, 2, 3];
|
||||
var s = @arr; # seq[int64] with the same elements
|
||||
```
|
||||
|
||||
### Networking (`net`)
|
||||
|
||||
The standard library also provides networking in `net`:
|
||||
|
||||
```peon
|
||||
import net;
|
||||
|
||||
let pair = socketPair(Unix, Stream, 0).unwrap();
|
||||
let addrs = runAsync(getAddrInfo("localhost", 80, Inet, Stream, 0)).unwrap();
|
||||
```
|
||||
|
||||
The public socket-related value types are real enums:
|
||||
|
||||
- `SocketFamily`: `AnyFamily`, `Unix`, `Inet`, `Inet6`
|
||||
- `SocketKind`: `AnyKind`, `Stream`, `Datagram`
|
||||
|
||||
The public address-resolution surface is:
|
||||
|
||||
- `getAddrInfo(host, port)`
|
||||
- `getAddrInfo(host, port, family, kind, protocol)`
|
||||
|
||||
`host` is a `string`, `port` and `protocol` are `int64`, and `family` / `kind`
|
||||
use those enums instead of raw C constants.
|
||||
174
docs/manual/unsafe.md
Normal file
174
docs/manual/unsafe.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Unsafe
|
||||
|
||||
[Back to Manual](index.md)
|
||||
|
||||
Peon's type system and borrow checker enforce safety by default. The `unsafe` keyword
|
||||
is the explicit opt-out: it marks code that the compiler cannot fully verify and that
|
||||
the programmer takes responsibility for. `unsafe` is intentionally granular -- it appears
|
||||
in four distinct positions, each with different meaning.
|
||||
|
||||
## Unsafe blocks
|
||||
|
||||
`unsafe { ... }` is a block expression that permits specific unchecked operations inside
|
||||
its body. It does **not** disable ordinary typing, purity, or borrow rules -- only the
|
||||
operations listed below are unlocked:
|
||||
|
||||
- Calling an `unsafe fn`, including [imported C functions](c-interop.md#importing-c-functions-importc)
|
||||
- Raw pointer dereference and indexing (`ptr[]`, `ptr[i]`)
|
||||
- Raw allocation and deallocation via `alloc`, `create`, `realloc`, `dealloc`, and `copyMem`
|
||||
- Reinterpret casts via `cast[T](x)`
|
||||
|
||||
Safe wrappers can use tiny internal `unsafe` blocks and expose ordinary safe APIs.
|
||||
That is the intended style for abstractions such as `Buffer[T]` and `seq[T]`.
|
||||
|
||||
```peon
|
||||
let values = @[1, 2, 3];
|
||||
let second = unsafe { values.get_unchecked(1)[] };
|
||||
```
|
||||
|
||||
## Unsafe functions
|
||||
|
||||
`unsafe fn` declares a function that is unsafe **to call**. The caller must wrap every
|
||||
call site in `unsafe { ... }` because the function exposes preconditions the type system
|
||||
does not prove.
|
||||
|
||||
This is separate from implementation unsafety. Even inside an `unsafe fn`, unchecked
|
||||
operations still belong in explicit `unsafe { ... }` blocks:
|
||||
|
||||
```peon
|
||||
unsafe fn get_unchecked(self: lent seq[int64], index: int64): lent int64 {
|
||||
# Still need unsafe here for the raw pointer access
|
||||
return unsafe { self.data[index] };
|
||||
}
|
||||
```
|
||||
|
||||
## Unsafe function types
|
||||
|
||||
When a first-class function value is unsafe to call, its type is written
|
||||
`unsafe fn(params): returnType`:
|
||||
|
||||
```peon
|
||||
fn apply_unsafe(f: unsafe fn(x: int64): int64, value: int64): int64 {
|
||||
return unsafe { f(value) };
|
||||
}
|
||||
```
|
||||
|
||||
See also [Functions -- Higher-order functions](functions.md#higher-order-functions).
|
||||
|
||||
## Unsafe interfaces
|
||||
|
||||
If implementing or inheriting an [interface](interfaces.md) carries extra unchecked
|
||||
obligations -- for example, semantic contracts the compiler cannot verify -- declare it
|
||||
as `unsafe iface`. Types and child interfaces must then acknowledge the unsafety
|
||||
explicitly:
|
||||
|
||||
```peon
|
||||
unsafe iface Sendable {
|
||||
unsafe fn transfer(self);
|
||||
}
|
||||
|
||||
type Channel = object with unsafe Sendable {
|
||||
}
|
||||
|
||||
unsafe fn transfer(self: Channel) {
|
||||
}
|
||||
```
|
||||
|
||||
`unsafe iface` propagates through inheritance and conformance:
|
||||
|
||||
- `unsafe iface Child from unsafe Parent { ... }` -- the `unsafe` marker is required
|
||||
when inheriting from an unsafe parent.
|
||||
- `type X = object with unsafe Parent { ... }` -- the `unsafe` marker is required
|
||||
when conforming to an unsafe interface.
|
||||
- Interface requirements may themselves be `unsafe fn`.
|
||||
|
||||
## Imported C functions
|
||||
|
||||
All functions imported with `#pragma[importc]` are treated as `unsafe fn`. Calls to
|
||||
them must appear inside `unsafe { ... }`. See [C Interop](c-interop.md) for details.
|
||||
|
||||
```peon
|
||||
fn strlen(s: cstring): csize; #pragma[importc, header: "<string.h>"]
|
||||
|
||||
let len = unsafe { strlen("hello") };
|
||||
```
|
||||
|
||||
## Raw pointers
|
||||
|
||||
Peon supports low-level raw pointers. `pointer` is the erased raw-pointer type,
|
||||
while `ptr T` is a typed raw pointer to plain storage `T`:
|
||||
|
||||
```
|
||||
var opaque: pointer;
|
||||
let values: ptr int64 = unsafe { alloc(int64, 1) };
|
||||
unsafe {
|
||||
values[] = 4;
|
||||
dealloc(values);
|
||||
}
|
||||
```
|
||||
|
||||
If you intentionally erase the pointee type, use explicit casts in both directions:
|
||||
|
||||
```
|
||||
unsafe {
|
||||
let values = create(int64, 2);
|
||||
let opaque = cast[pointer](values);
|
||||
let base = cast[ptr int64](opaque);
|
||||
let view = cast[ptr UncheckedArray[int64]](base);
|
||||
print(view[0]);
|
||||
dealloc(values);
|
||||
}
|
||||
```
|
||||
|
||||
`alloc(T, n)` returns a `ptr T` without zero-initializing the storage. `create(T, n)`
|
||||
returns a zero-initialized `ptr T`. These operations require `unsafe`:
|
||||
|
||||
```
|
||||
unsafe {
|
||||
let values = create(int64, 3); # ptr int64
|
||||
let view = cast[ptr UncheckedArray[int64]](values);
|
||||
print(view[0]); # 0
|
||||
dealloc(values);
|
||||
}
|
||||
```
|
||||
|
||||
`ptr UncheckedArray[T]` is useful for functions that consume a contiguous buffer with no
|
||||
statically known length, or when you need unchecked-style pointer indexing:
|
||||
|
||||
```
|
||||
fn sum(values: ptr UncheckedArray[int64]): int64 {
|
||||
return unsafe { values[0] + values[1] };
|
||||
}
|
||||
```
|
||||
|
||||
Unlike `ref T`, raw pointers are not managed by the
|
||||
[generational-reference runtime](memory-model.md#how-generational-references-work). They
|
||||
do not carry liveness checks and are intended for manual allocation and low-level
|
||||
interop-style code.
|
||||
|
||||
## Unmanaged memory operations
|
||||
|
||||
The full set of raw memory operations:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `alloc(T, n)` | Allocate raw storage for `n` values of type `T` (uninitialized). Returns `ptr T`. Requires `unsafe`. |
|
||||
| `create(T, n)` | Allocate zero-initialized raw storage for `n` values of type `T`. Returns `ptr T`. Requires `unsafe`. |
|
||||
| `realloc(p, n)` | Resize the allocation at `p` to hold `n` elements. Returns `ptr T`. Requires `unsafe`. |
|
||||
| `dealloc(p)` | Free a raw pointer allocation. Requires `unsafe`. |
|
||||
| `copyMem(dst, src, n)` | Copy `n` bytes from `src` to `dst`. Well-defined even when the source and destination regions overlap (it behaves like C's `memmove`, not `memcpy`). Requires `unsafe`. |
|
||||
| `addr(x)` | Take the raw address of a raw-pointer-backed value, returning `ptr T`. |
|
||||
| `cast[T](x)` | Reinterpret the bits of `x` as type `T`. Source and target must have the same size. Requires `unsafe`. |
|
||||
|
||||
Raw pointers cannot point to managed `ref` types or `lent` types. This is enforced
|
||||
at compile time.
|
||||
|
||||
```peon
|
||||
unsafe {
|
||||
let buf = alloc(int64, 4);
|
||||
let src = create(int64, 4);
|
||||
copyMem(buf, src, 4 * sizeof(int64));
|
||||
dealloc(src);
|
||||
dealloc(buf);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user