Low-level PekoScript
Low-level PekoScript
Raw scalars versus value types
number, bool, char, and string are classes wrapping a raw scalar. The raw
types are i1, i8, i16, i32, i64, f16, f32, f64.
Boxing is one-directional: a raw scalar is promoted to its wrapper automatically, but a wrapper is never silently treated as a raw scalar. Cross the boundary explicitly:
let raw: f64 = value.to_raw()
let count: i32 = danger_cast<i32>(raw)
let boxed: number = new number(danger_cast<f64>(count))A comparison between two raw scalars produces a raw i1, not a bool.
constant
A bare literal is a boxed object. constant<T>(...) produces a raw one:
let zero: i64 = constant<i64>(0)
let newline: i8 = constant<i8>(10)Needed for any literal in raw-typed code: FFI arguments, buffer sizes, loop
bounds over i64, NUL terminators.
Applied to a literal it produces a real constant of that type, so
constant<f64>(7) is 7.0. Applied to a non-literal it is a pure relabel
with no conversion, which is a trap: use danger_cast to convert a value.
danger_cast
An unchecked conversion. Between numeric types it emits a real conversion: truncation, extension, or an integer and float exchange. Between anything else, including all pointer types, it emits no instruction at all and simply relabels the value.
The name is the warning. Use it where the range is known and the relabel is intended, and nowhere else.
Pointers and references
| Type | Traced by the collector |
|---|---|
pointer<T> |
yes, and may move |
&T |
no |
opaque |
no |
cstr |
no |
pointer<T> is a managed pointer, the type of a managed buffer. Indexing it
yields an interior pointer the collector understands. Dereferencing loads the
pointee.
Pointer arithmetic does not exist. Index instead of adding.
&T is a reference to a slot, which is what makes a[i] = v work through
index_ref. Together with opaque and cstr, it is unmanaged: an object
reachable only through one is not kept alive.
cstr is a raw C string and opaque is an unmanaged handle. Both are plain
addresses.
Built-ins
| Built-in | Result |
|---|---|
sizeof<T>() |
the size of T as i64 |
__rt_peko_alloc<T>(count) |
a managed buffer of T |
Error(message) |
an optional in the error state |
cstring("literal") |
a cstr from a literal |
runtime::allocate<T>(count) wraps the allocator and is the normal way to reach
it. It picks the element descriptor from T, so a buffer of managed elements is
traced and a buffer of scalars is not.
Modifiers that matter here
| Modifier | Effect |
|---|---|
[external] |
no name mangling; the symbol is a foreign one |
[gcsafe] |
the call is a safepoint; live managed pointers survive it |
[constant] |
the binding cannot be reassigned or taken mutably |
[variadic] |
the function takes trailing arguments |
Two clarifications worth having, because the names mislead.
[notrack] has nothing to do with GC tracking. It suppresses the source
position bookkeeping the compiler emits around a call for error reporting. It is
a size and speed optimization for hot paths, not a memory annotation.
[opaque] is not a modifier. opaque is a type keyword. Writing it in a
modifier list does nothing.
Getting at raw text
let raw: pointer<i8> = text.to_raw() // the managed buffer
let managed: string = runtime::create_managed(c_ptr) // copies from Ccreate_managed copies raw C bytes into a fresh managed string, which is what
you want for a char* returned by C. Its counterpart managed_string wraps a
buffer that is already managed and takes ownership. Confusing them leaks or
produces a use-after-move.