All checks were successful
docs / build-and-deploy (push) Successful in 3s
The security-sensitive counterpart to the fast, non-cryptographic Hash.* library: standard, test-vector-backed hashing for signed saves and message integrity, kept in its own namespace so nobody reaches for the wrong tool. Crypto.sha256(s) SHA-256 -> 64-char lowercase hex Crypto.hmac_sha256(key, msg) HMAC-SHA256 -> 64-char hex Crypto.verify_hmac(key, msg, mac) recompute + constant-time compare -> bool Crypto.hex(s) lowercase hex of a string's bytes Crypto.ct_equal(a, b) constant-time string equality The primitives are implemented from scratch in plain integer LLVM IR (FIPS 180-4 / RFC 2104): no libc crypto, no data-dependent branches in the compression rounds, so a given input hashes to the same 32 bytes on every platform and run. Digests are returned as hex strings, not raw bytes, because a `str` is null-terminated and a raw digest can contain a NUL. MAC checks use a non-short-circuiting compare so timing does not leak how much of a forged tag was correct. Emitted on demand via g_uses_cryptort, mirroring the emit_hash prelude gate. Scoped to the deterministic, known-answer-testable core; OS-backed random_bytes (the one piece that can't be validated by test vectors) is left for a follow-up. Tested against published SHA-256 vectors (empty/"abc"/fox + 55/56/64-byte multi-block padding) and HMAC-SHA256 vectors; wired into the self-host suite as `crypto`. Docs: a new Crypto section with honest "what this protects / does not" guidance, one page per method, all fences checked and in the inventory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
327 lines
15 KiB
Text
327 lines
15 KiB
Text
# emit_core.ludic — emitter state, type mapping, struct/slice helpers, and the
|
|
# module header. Mirrors the pieces of compiler/back/ that this subset needs.
|
|
# structs and slices are references, so every non-scalar type lowers to `ptr`.
|
|
|
|
property Val { code: pointer = null, ty: pointer = null }
|
|
function val(code: pointer, ty: pointer) -> Val { let v = new Val; v.code = code; v.ty = ty; return v }
|
|
|
|
var head: Buf # module-level: types, globals, string constants
|
|
var code: Buf # function bodies
|
|
var falloc: Buf # entry-block allocas for the current function
|
|
var ll_t: int = 0 # temp register counter (reset per function)
|
|
var ll_lbl: int = 0 # label counter
|
|
var ll_str: int = 0 # string-constant counter
|
|
|
|
# local environment (parallel slices), reset per function
|
|
var loc_name: []pointer
|
|
var loc_reg: []pointer
|
|
var loc_ty: []pointer
|
|
var loc_mut: []int # 1 = mutable (var / param / loop-var), 0 = immutable (let)
|
|
var nloc: int = 0
|
|
var g_uses_str: bool = false # a `str + str` / `str == str` was emitted -> emit the prelude
|
|
var g_uses_intstr: bool = false # `string(int)` was emitted -> emit the int->string prelude
|
|
var g_uses_strslice: bool = false # `s[a..b]` was emitted -> emit the substring prelude
|
|
var g_uses_mathrt: bool = false # Math.sqrt/sin/cos/tan was emitted -> emit the math runtime prelude
|
|
var g_uses_textrt: bool = false # Text.upper/lower/trim/repeat/pad was emitted -> emit the text builders
|
|
var g_uses_textrt2: bool = false # Text.split/join/replace was emitted -> emit the string/slice builders
|
|
var g_uses_hashrt: bool = false # Hash.of/fnv1a/crc32 was emitted -> emit the byte-stream hashers
|
|
var g_uses_cryptort: bool = false # Crypto.* was emitted -> emit the SHA-256 / HMAC runtime
|
|
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
|
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
|
|
|
# loop targets for break/continue (innermost last)
|
|
var brk_lbl: []pointer
|
|
var cnt_lbl: []pointer
|
|
var nloop: int = 0
|
|
|
|
var ret_ty: pointer # current function's return type
|
|
var g_term: bool = false # did the current block end in a terminator?
|
|
var self_stk: []pointer # entity-index slot (ip) per enclosing query, for self()
|
|
var nself: int = 0
|
|
var mach_stk: []Node # enclosing `machine` nodes, so `become` finds its register
|
|
var nmach: int = 0
|
|
|
|
# scenes: one implicit active-scene register (@L_scene). A scene lowers to a
|
|
# machine the compiler writes for you — `become <Scene>` runs the source scene's
|
|
# on-exit, stores the target id, and runs its on-enter.
|
|
var g_scenes: []Node # every `scene` declaration, in source order (ival = id)
|
|
var g_scene_count: int = 0 # parse-time id counter
|
|
var g_start_scene: int = 0 # id of the scene marked `start` (else the first)
|
|
var g_cur_scene: Node = null # scene owning the handler being emitted, for `become`
|
|
|
|
function find_scene(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_scenes) { if (g_scenes[i].s == name) { return g_scenes[i] }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
function emit(s: pointer) -> void { buf_puts(code, s) }
|
|
function emith(s: pointer) -> void { buf_puts(head, s) }
|
|
|
|
# stack slots MUST live in the entry block (an alloca in a loop walks the stack
|
|
# off its end), so they go into a per-function buffer spliced in at entry.
|
|
function emit_alloca(llt: pointer) -> pointer {
|
|
let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1
|
|
buf_puts(falloc, " "); buf_puts(falloc, r); buf_puts(falloc, " = alloca "); buf_puts(falloc, llt); buf_puts(falloc, "\n")
|
|
return r
|
|
}
|
|
|
|
# "%t<n>" fresh register
|
|
function nreg() -> pointer { let r = `%t{itoa(ll_t)}`; ll_t = ll_t + 1; return r }
|
|
function lbl(pfx: pointer) -> pointer { let r = (pfx + itoa(ll_lbl)); ll_lbl = ll_lbl + 1; return r }
|
|
|
|
# Ludic type -> LLVM type. int/bool are i32; everything else (ptr/str/struct/
|
|
# slice) is a pointer; void is void.
|
|
function llty(t: pointer) -> pointer {
|
|
if (t == "int") or (t == "bool") or (t == "fixed") or (t == "entity") { return "i32" } # entity = an i32 handle (self())
|
|
if (t == "long") { return "i64" } # a 64-bit signed integer
|
|
if (t == "Vector") { return "i64" } # a 2D vector: (x, y) fixeds packed into one i64
|
|
if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr)
|
|
if (t == "words") or (t == "fixeds") or (t == "pointers") { return "ptr" } # typed buffers
|
|
if (t == "void") { return "void" }
|
|
return "ptr"
|
|
}
|
|
|
|
function is_slice_ty(t: pointer) -> bool { return t[0] == 91 and t[1] == 93 } # "[]"
|
|
function slice_elem(t: pointer) -> pointer { return t[2..len(t)] }
|
|
|
|
function find_arch(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_ARCH and (d.s == name) { return d }; i = i + 1 }
|
|
return null
|
|
}
|
|
function find_comp(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_COMP and (d.s == name) { return d }; i = i + 1 }
|
|
return null
|
|
}
|
|
# every record is a `property` with a %Cmp_ layout of named fields — whether it
|
|
# is stored per-entity by the ECS or heap-allocated by `new` is a matter of use.
|
|
function layout_node(name: pointer) -> Node { return find_comp(name) }
|
|
function layout_ty(name: pointer) -> pointer { return (("%Cmp_") + name) }
|
|
|
|
function field_index(s: Node, fname: pointer) -> int {
|
|
var i = 0
|
|
while i < len(s.kids) { if (s.kids[i].s == fname) { return i }; i = i + 1 }
|
|
return 0 - 1
|
|
}
|
|
function field_type(s: Node, fname: pointer) -> pointer {
|
|
var i = 0
|
|
while i < len(s.kids) { if (s.kids[i].s == fname) { return s.kids[i].ty }; i = i + 1 }
|
|
return "int"
|
|
}
|
|
|
|
# find a global var/const by name
|
|
function find_global(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_VAR and (d.s == name) { return d }
|
|
if d.kind == N_CONST and (d.s == name) { return d }
|
|
i = i + 1
|
|
}
|
|
return null
|
|
}
|
|
|
|
# `Enum.Variant` -> the variant's ordinal (its index), or -1 if `ename` names no
|
|
# enum with that variant. Enum names live in `prog` like any other declaration.
|
|
function enum_ordinal(ename: pointer, vname: pointer) -> int {
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_ENUM and (d.s == ename) {
|
|
var j = 0
|
|
while j < len(d.kids) { if (d.kids[j].s == vname) { return j }; j = j + 1 }
|
|
}
|
|
i = i + 1
|
|
}
|
|
# LC1: the compiler owns `EndReason` — the reason bound by a reason-carrying
|
|
# teardown (`@OnDespawn(M, reason: r)`). Each despawn site passes one of these.
|
|
if (ename == "EndReason") {
|
|
if (vname == "Despawned") { return 0 } # explicit `despawn e`
|
|
if (vname == "SceneExit") { return 1 } # a scene tearing down its owned entities
|
|
if (vname == "Quit") { return 2 } # program shutdown
|
|
}
|
|
return 0 - 1
|
|
}
|
|
function find_fn(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_FN and (d.s == name) { return d }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# `extern function name(params) -> T = "sym"` binds a Ludic name to a link symbol. A
|
|
# call to `name` lowers to a direct `@<sym>` call (no @fn_ prefix — the string is
|
|
# the exact linked symbol), and emit_extern_decls emits a matching `declare`. This
|
|
# is the transport seam (net_send/net_poll), the windowing/socket FFI, and any
|
|
# C/Rust/Zig library binding — the same seam NETWORKING-DESIGN §5 names.
|
|
function find_extern(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(prog) { let d = prog[i]; if d.kind == N_EXTERN and (d.s == name) { return d }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# @Computed derived fields: a per-property (Prop.field -> expression) registry.
|
|
# These are NOT stored in the component layout; `x.field` expands inline to the
|
|
# expression with its bare names read as fields of `x`. Populated at parse time.
|
|
var g_computed: []Node # each: s = "Prop.field", ty = result type, a = expr
|
|
|
|
function register_computed(prop: pointer, field: pointer, ty: pointer, e: Node) -> void {
|
|
let cf = node(N_FIELD); cf.s = `{prop}.{field}`; cf.ty = ty; cf.a = e
|
|
push(g_computed, cf)
|
|
}
|
|
function computed_expr(prop: pointer, field: pointer) -> Node {
|
|
if (prop == null) { return null }
|
|
let key = `{prop}.{field}`
|
|
var i = 0
|
|
while i < len(g_computed) { if (g_computed[i].s == key) { return g_computed[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
# best-effort static type of an expression (for computed-field lookup; emits nothing)
|
|
function static_type(e: Node) -> pointer {
|
|
if e.kind == E_ID { let li = loc_find(e.s); if li >= 0 { return loc_ty[li] } }
|
|
return null
|
|
}
|
|
|
|
# @OnSpawn(Model) hooks: a Model -> hook-body registry. Populated at parse time;
|
|
# `spawn Model { … }` runs the body with the model's properties bound (like a
|
|
# constructor). Spawn statically knows the model, so no runtime dispatch is needed.
|
|
var g_onspawn: []Node # each: s = Model name, a = hook body block
|
|
|
|
function register_onspawn(model: pointer, body: Node) -> void {
|
|
let n = node(N_BLOCK); n.s = model; n.a = body; push(g_onspawn, n)
|
|
}
|
|
function onspawn_body(model: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_onspawn) { if (g_onspawn[i].s == model) { return g_onspawn[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# @OnDespawn(Model): a Model -> hook-body registry. Despawn does not statically
|
|
# know an entity's model, so these are emitted as functions and dispatched on the
|
|
# entity's kind at each `despawn`. @OnAttach(Property) fires per property-attach.
|
|
var g_ondespawn: []Node # each: s = Model name, a = hook body block
|
|
var g_onattach: []Node # each: s = Property name, a = hook body block
|
|
|
|
# LC1: `.ty` carries the optional `reason:` binding name (null if the hook took
|
|
# no reason). The despawn hook function gains an `i32 %reason` parameter and each
|
|
# teardown site passes a constant EndReason (see emit_despawn_hooks / emit_despawn).
|
|
function register_ondespawn(model: pointer, body: Node, reason: pointer) -> void {
|
|
let n = node(N_BLOCK); n.s = model; n.a = body; n.ty = reason; push(g_ondespawn, n)
|
|
}
|
|
function ondespawn_body(model: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_ondespawn) { if (g_ondespawn[i].s == model) { return g_ondespawn[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
function register_onattach(prop: pointer, body: Node) -> void {
|
|
let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onattach, n)
|
|
}
|
|
function onattach_body(prop: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_onattach) { if (g_onattach[i].s == prop) { return g_onattach[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# @OnDetach(Property): the teardown paired with @OnAttach — fires when a property
|
|
# is removed from a live entity (`detach P on e`), with the property bound by name
|
|
# so the body can read its outgoing value before it is cleared.
|
|
var g_ondetach: []Node # each: s = Property name, a = hook body block
|
|
function register_ondetach(prop: pointer, body: Node) -> void {
|
|
let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondetach, n)
|
|
}
|
|
function ondetach_body(prop: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_ondetach) { if (g_ondetach[i].s == prop) { return g_ondetach[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# @OnEnable(Property) / @OnDisable(Property): run when a property is toggled on an
|
|
# entity, with the property bound by name.
|
|
var g_onenable: []Node
|
|
var g_ondisable: []Node
|
|
function register_onenable(prop: pointer, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onenable, n) }
|
|
function register_ondisable(prop: pointer, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondisable, n) }
|
|
function onenable_body(prop: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_onenable) { if (g_onenable[i].s == prop) { return g_onenable[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
function ondisable_body(prop: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_ondisable) { if (g_ondisable[i].s == prop) { return g_ondisable[i].a }; i = i + 1 }
|
|
return null
|
|
}
|
|
|
|
# EV0 — the event bus core. `event E { fields }` declares a POD payload; `@On(E)
|
|
# handler …` registers a compile-time listener; `emit E(…)` fires it. An event
|
|
# lowers to a `@ev_<E>(payload)` function whose body is the concatenation of its
|
|
# listeners in declaration order — a direct call at each `emit` site, no runtime.
|
|
# Gated on `len(g_events) > 0`, so a program with no events is byte-identical.
|
|
var g_events: []Node # each: an N_EVENT node (s = name, kids = payload fields, ival=1 if cancellable)
|
|
var g_onlisten: []Node # each: N_BLOCK, s = event name, a = listener body block
|
|
var g_cancel_addr: pointer = null # EV3: address of the current cancellable dispatch's flag (null outside one)
|
|
|
|
function register_event(n: Node) -> void { push(g_events, n) }
|
|
function find_event(name: pointer) -> Node {
|
|
var i = 0
|
|
while i < len(g_events) { if (g_events[i].s == name) { return g_events[i] }; i = i + 1 }
|
|
return null
|
|
}
|
|
function register_onlisten(evt: pointer, body: Node) -> void {
|
|
let n = node(N_BLOCK); n.s = evt; n.a = body; push(g_onlisten, n)
|
|
}
|
|
|
|
# EV1 — `@Public` promotes a lifecycle hook to a public event. A promoted event
|
|
# is synthesized here (payload = the entity, plus a reason for despawn); its
|
|
# presence in g_events is what makes each lifecycle fire site also `emit` it, so
|
|
# `find_event(name) != null` doubles as the "is this hook public?" test. Names are
|
|
# the stable ABI contract: `model_<M>_spawn`, `model_<M>_despawn`, etc.
|
|
function ensure_event(name: pointer, with_reason: bool) -> void {
|
|
if (find_event(name) != null) { return }
|
|
let n = node(N_EVENT); n.s = name
|
|
let ent = node(N_FIELD); ent.s = "entity"; ent.ty = "int"; push(n.kids, ent)
|
|
if with_reason { let r = node(N_FIELD); r.s = "reason"; r.ty = "int"; push(n.kids, r) }
|
|
register_event(n)
|
|
}
|
|
# a promoted scene/program event has no per-entity payload
|
|
function ensure_event_empty(name: pointer) -> void {
|
|
if (find_event(name) != null) { return }
|
|
let n = node(N_EVENT); n.s = name; register_event(n)
|
|
}
|
|
|
|
# EV1/SCENES-E2 — layer toggle. A layer named in an `enable layer L`/`disable
|
|
# layer L` statement becomes "managed": it gets an @LE_<L> enabled flag and its
|
|
# handlers gate on it. Only managed layers pay for this, so a scene program that
|
|
# never toggles a layer is byte-identical.
|
|
var g_toggled_layers: []pointer
|
|
function note_toggled_layer(name: pointer) -> void {
|
|
var i = 0
|
|
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return }; i = i + 1 }
|
|
push(g_toggled_layers, name)
|
|
}
|
|
function is_toggled_layer(name: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(g_toggled_layers) { if (g_toggled_layers[i] == name) { return true }; i = i + 1 }
|
|
return false
|
|
}
|
|
|
|
# is `name` a model (archetype)? — chooses model-vs-handler for a bare enable/disable
|
|
function is_model(name: pointer) -> bool { return find_arch_id(name) > 0 }
|
|
|
|
# local variable environment
|
|
function loc_reset() -> void { nloc = 0 }
|
|
# push a local. Defaults to mutable (params, loop and query bindings are all
|
|
# reassignable/rebindable); a `let` binding marks its slot immutable afterward
|
|
# via loc_set_mut, so a later `name = …` can be rejected.
|
|
function loc_push(name: pointer, r: pointer, ty: pointer) -> void {
|
|
if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 }
|
|
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) }
|
|
nloc = nloc + 1
|
|
}
|
|
function loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } }
|
|
function loc_find(name: pointer) -> int {
|
|
var i = nloc - 1
|
|
while i >= 0 { if (loc_name[i] == name) { return i }; i = i - 1 }
|
|
return 0 - 1
|
|
}
|