keep(x) copies a string, a slice (header and elements) or a record (shallow) onto the heap; intern(s) hands back one heap string per distinct text from a fixed table in the runtime (FNV-1a, 65536 slots, copied the first time; past 49152 only copied). Both are how frame code keeps what it made on purpose: the escape analysis takes the copy as the heap's and leaves the argument LOCAL. The analysis now records why a class escapes (the store, the event, the global it reached) and ludic deps lists every allocation frame code makes and keeps - fkeep lines, 'ludic deps --keeps', the frame_keeps number --check ratchets - leaving out what is under @alloc_ok and a push's growth (25.5's capacities). --arena-strict (or 'arena strict') makes each an error naming the store, before anything is emitted. A test: a template stored into a state is the one error; keep and intern of the next two, an @alloc_ok push and a scratch temporary are not; 195 frames of arena resets under R3D_ARENA_CHECK=1 later the kept and interned texts read as made, and intern gives the same string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
276 lines
9.1 KiB
Text
276 lines
9.1 KiB
Text
# check_call.ludic — L4: a call gives each parameter the type it asks for, and as many arguments
|
|
# as there are parameters. A function, an extern, a function value (a local, a global or a
|
|
# record's field) and the builtins that convert are checked; a namespace call (Input.pad_axis)
|
|
# gives the result type of the Ludic function behind it, and its arguments are the emitter's.
|
|
function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
|
|
var i = 0
|
|
while i < len(f.kids) {
|
|
if f.kids[i].kind == N_PARAM {
|
|
push(labels, f.kids[i].s)
|
|
push(tys, f.kids[i].ty)
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
|
|
state_inject_generated(e, f)
|
|
state_inject_runtime(e, f)
|
|
if is_action_builtin(f.s) { state_inject(e, f) } # 0.R: the queue is supplied
|
|
mg_call(e, f)
|
|
call_fill_defaults(e, f)
|
|
ck_state_args(e, f)
|
|
let labels = new []pointer
|
|
let tys = new []pointer
|
|
ck_params(f, labels, tys)
|
|
ck_args(e, name, labels, tys)
|
|
if f.ty == null { return "void" }
|
|
return f.ty
|
|
}
|
|
function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer {
|
|
state_inject(e, f)
|
|
if g_al_labels[al] == null { call_fill_defaults(e, f) } else { call_mixed_to_named(e, ns_alias_labels(al)) }
|
|
let labels = new []pointer
|
|
let tys = new []pointer
|
|
ck_params(f, labels, tys)
|
|
let alabels = ns_alias_labels(al)
|
|
if len(alabels) == len(tys) { ck_args(e, name, alabels, tys) } else { ck_args(e, name, labels, tys) }
|
|
if f.ty == null { return "void" }
|
|
return f.ty
|
|
}
|
|
function ck_call_sig(e: Node, name: pointer, t: pointer) -> pointer {
|
|
ck_args(e, name, new []pointer, fn_ty_params(t))
|
|
return fn_ty_ret(t)
|
|
}
|
|
function ck_label_at(labels: []pointer, s: pointer) -> int {
|
|
var i = 0
|
|
while i < len(labels) {
|
|
if labels[i] == s { return i }
|
|
i += 1
|
|
}
|
|
return -1
|
|
}
|
|
function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> void {
|
|
let n = len(e.kids)
|
|
if n > 0 and e.kids[0].kind == E_FINIT {
|
|
var j = 0
|
|
while j < n {
|
|
let fi = e.kids[j]
|
|
let vt = ck_expr(fi.a)
|
|
let at = ck_label_at(labels, fi.s)
|
|
if at >= 0 { ck_give(tys[at], vt, fi.a, `argument {fi.s} of {name}`) }
|
|
if at < 0 and len(labels) > 0 { ck_err("arity", fi.a, `{name} has no parameter {fi.s}`) }
|
|
j += 1
|
|
}
|
|
var m = 0
|
|
while m < len(labels) {
|
|
if not ck_named_has(e, labels[m]) { ck_err("arity", e, `this call to {name} leaves out {labels[m]}, which has no default`) }
|
|
m += 1
|
|
}
|
|
return
|
|
}
|
|
if n < len(tys) and len(labels) == len(tys) { ck_err("arity", e, `this call to {name} leaves out {labels[n]}, which has no default`) }
|
|
else { if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) } }
|
|
var i = 0
|
|
while i < n {
|
|
if i < len(tys) { ck_expect = tys[i] }
|
|
let at = ck_expr(e.kids[i])
|
|
if i < len(tys) { ck_give(tys[i], at, e.kids[i], `argument {itoa(i + 1)} of {name}`) }
|
|
i += 1
|
|
}
|
|
}
|
|
function ck_named_has(e: Node, label: pointer) -> bool {
|
|
var i = 0
|
|
while i < len(e.kids) {
|
|
if (e.kids[i].s == label) { return true }
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
function ck_walk_args(e: Node) -> void {
|
|
var i = 0
|
|
while i < len(e.kids) {
|
|
ck_any(e.kids[i])
|
|
i += 1
|
|
}
|
|
}
|
|
# the builtins whose type is their name, and push, whose element must fit the slice
|
|
function ck_builtin(e: Node, name: pointer) -> pointer {
|
|
if (name == "keep") and len(e.kids) == 1 { return ck_expr(e.kids[0]) } # 25.3: a copy on the heap, the same type
|
|
if (name == "intern") and len(e.kids) == 1 { # 25.3: one heap string per distinct text
|
|
ck_walk_args(e)
|
|
return "string"
|
|
}
|
|
if (name == "string") or (name == "int") or (name == "float") or (name == "fixed") or (name == "long") or (name == "double") {
|
|
ck_walk_args(e)
|
|
return name
|
|
}
|
|
if (name == "as_int") or (name == "as_fixed") {
|
|
ck_walk_args(e)
|
|
return name[3..len(name)]
|
|
}
|
|
if (name == "len") or (name == "float_bits") {
|
|
ck_walk_args(e)
|
|
return "int"
|
|
}
|
|
if (name == "view") and len(e.kids) == 3 {
|
|
let vt = ck_expr(e.kids[0])
|
|
ck_give("int", ck_expr(e.kids[1]), e.kids[1], "the start of a view")
|
|
ck_give("int", ck_expr(e.kids[2]), e.kids[2], "the length of a view")
|
|
if not ck_unknown(vt) and not is_slice_ty(vt) { ck_err("kind", e, `view takes a slice, and this is {ck_a(vt)}`) }
|
|
return vt
|
|
}
|
|
if (name == "bytes") or (name == "offset") {
|
|
ck_walk_args(e)
|
|
return "pointer"
|
|
}
|
|
if (name == "data_of") {
|
|
ck_walk_args(e)
|
|
return "pointer"
|
|
}
|
|
if (name == "words") {
|
|
ck_walk_args(e)
|
|
return "[]int"
|
|
}
|
|
if (name == "buffer") {
|
|
ck_walk_args(e)
|
|
return "[]byte"
|
|
}
|
|
if (name == "fixeds") or (name == "pointers") {
|
|
ck_walk_args(e)
|
|
return "[]" + name[0 .. len(name) - 1]
|
|
}
|
|
if (name == "floats") or (name == "doubles") {
|
|
ck_walk_args(e)
|
|
return "[]" + name[0 .. len(name) - 1]
|
|
}
|
|
if (name == "float_from_bits") {
|
|
ck_walk_args(e)
|
|
return "float"
|
|
}
|
|
if (name == "print") {
|
|
ck_walk_args(e)
|
|
return "void"
|
|
}
|
|
if (name == "push") and len(e.kids) == 2 {
|
|
ck_write_check(e.kids[0], "push")
|
|
let st = ck_expr(e.kids[0])
|
|
let vt = ck_expr(e.kids[1])
|
|
if not ck_unknown(st) and is_slice_ty(st) { ck_give(slice_elem(st), vt, e.kids[1], `push onto {ck_a(st)}`) }
|
|
return "void"
|
|
}
|
|
return null
|
|
}
|
|
# a program's function named like the target of one of the engine's own namespace methods takes
|
|
# that method's calls - a package's rng_range(a, b, c) made every Random.range a call to it, and
|
|
# the checker then asked for its third argument. Refused where the method is called.
|
|
var ck_shadow_seen: []pointer = new []pointer
|
|
function ck_alias_shadow(e: Node, al: int, tf: Node) -> void {
|
|
if not has_sub(g_al_file[al], "runtime/native/") { return }
|
|
if tf.file == null or has_sub(tf.file, "runtime/native/") { return }
|
|
var i = 0
|
|
while i < len(ck_shadow_seen) {
|
|
if (ck_shadow_seen[i] == tf.s) { return }
|
|
i += 1
|
|
}
|
|
push(ck_shadow_seen, tf.s)
|
|
ck_err("shadow", tf, `{tf.s} is the engine's {g_al_ns[al]}.{g_al_meth[al]}, which this program calls ({e.file}:{itoa(e.line)}), and every such call would reach this function instead; choose another name`)
|
|
}
|
|
function ck_call(e: Node) -> pointer {
|
|
let c = e.a
|
|
if c.kind == E_ID { return ck_call_named(e, c.s) }
|
|
if c.kind == E_MEMBER {
|
|
let b = c.a
|
|
if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null {
|
|
# an alias (L6) is its target, labels and all, so its arguments are checked in full
|
|
if (b.s == "Memory") { ck_raw(e, `Memory.{c.s}`) }
|
|
let al = ns_alias_find(b.s, c.s)
|
|
if al >= 0 {
|
|
var tf = ck_fn(g_al_target[al])
|
|
if tf != null { ck_alias_shadow(e, al, tf) }
|
|
if tf == null {
|
|
tf = ck_extern(g_al_target[al])
|
|
if tf != null { ck_raw(e, `{b.s}.{c.s}, a C function`) }
|
|
}
|
|
if tf != null {
|
|
if ck_fn(g_al_target[al]) != null { ck_vis(tf, g_al_target[al], e) }
|
|
ck_extern_arg = ck_extern(g_al_target[al]) != null
|
|
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
|
|
ck_extern_arg = false
|
|
return at
|
|
}
|
|
ck_walk_args(e)
|
|
return "?"
|
|
}
|
|
# any other Ns.fn is the emitter's own table, which supplies defaults and reorders,
|
|
# so the Ludic function behind it gives the result's type and nothing about the arguments
|
|
ck_walk_args(e)
|
|
let nf = ck_fn(ns_lower(b.s) + "_" + c.s)
|
|
if nf != null { ck_vis(nf, nf.s, e) }
|
|
if nf != null and nf.ty != null { return nf.ty }
|
|
return "?"
|
|
}
|
|
let r = ck_record(ck_expr(b))
|
|
if r != null {
|
|
let ft = ck_field(r, c.s)
|
|
if ft != null and is_fn_type(ft) { return ck_call_sig(e, c.s, ft) }
|
|
}
|
|
ck_walk_args(e)
|
|
return "?"
|
|
}
|
|
ck_expr(c)
|
|
ck_walk_args(e)
|
|
return "?"
|
|
}
|
|
function ck_call_named(e: Node, name: pointer) -> pointer {
|
|
let li = ck_local(name)
|
|
if li >= 0 {
|
|
if is_fn_type(ck_tys[li]) { return ck_call_sig(e, name, ck_tys[li]) }
|
|
ck_walk_args(e)
|
|
return "?"
|
|
}
|
|
if ck_raw_builtin(name) { ck_raw(e, `{name}()`) }
|
|
let bt = ck_builtin(e, name)
|
|
if bt != null { return bt }
|
|
let f = ck_fn(name)
|
|
if f != null {
|
|
ck_vis(f, name, e)
|
|
return ck_call_fn(e, name, f)
|
|
}
|
|
let gt = gen_template(g_gen_fns, name)
|
|
if gt != null {
|
|
ck_vis(gt, name, e)
|
|
return gen_call(e, name, gt)
|
|
}
|
|
let x = ck_extern(name)
|
|
if x != null {
|
|
ck_raw(e, `the C function {name}`)
|
|
ck_extern_writes(e)
|
|
ck_extern_arg = true
|
|
let xt = ck_call_fn(e, name, x)
|
|
ck_extern_arg = false
|
|
return xt
|
|
}
|
|
let g = ck_global(name)
|
|
if g != null and is_fn_type(g.ty) {
|
|
mg_ref(e.a, g) # 0.S2: a call through a var of a function type reads it
|
|
return ck_call_sig(e, name, g.ty)
|
|
}
|
|
ck_walk_args(e)
|
|
return "?"
|
|
}
|
|
# emit E(field: v): each field gets the type the event declares for it
|
|
function ck_emit(s: Node) -> void {
|
|
let ev = find_event(s.s)
|
|
ck_vis(ev, s.s, s)
|
|
if s.a == null { return }
|
|
var i = 0
|
|
while i < len(s.a.kids) {
|
|
let fi = s.a.kids[i]
|
|
let vt = ck_expr(fi.a)
|
|
if ev != null {
|
|
let ft = ck_field(ev, fi.s)
|
|
if ft != null { ck_give(ft, vt, fi.a, `field {fi.s} of {s.s}`) }
|
|
}
|
|
i += 1
|
|
}
|
|
}
|