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 C

create_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.