A slice has carried { ptr, len, cap } since it existed and nothing ever read
the len: every index emitted a bare getelementptr. Running off the end of one
wrote into whatever the allocator had put next, and the program died somewhere
else entirely - a maroon-lake crash took a day to find because the stack named
a texture upload and the write was in a telemetry buffer five frames earlier.
Now each index loads the length and compares unsigned, which rejects a negative
index in the same instruction, and a failure aborts with the location the other
located errors use:
oob.ludic:9: index out of range: 7, len 3
`words` and the other raw buffers are unchanged - they are a bare malloc with no
length to check, which is the argument for moving off them.
The SPIR-V variants are regenerated in the same commit: shaders --check was
failing against the edited GLSL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
4.2 KiB
Text
87 lines
4.2 KiB
Text
# emit_addr.ludic — addresses of lvalues (struct fields and slice elements),
|
|
# shared by expression loads and assignment stores. Each returns the address
|
|
# register; the element/field type is written into g_addr_ty.
|
|
|
|
var g_addr_ty: pointer # out-param: the type at the computed address
|
|
|
|
# address of `base.field`
|
|
function emit_member_addr(e: Node) -> pointer {
|
|
let base = emit_expr(e.a)
|
|
let s = layout_node(base.ty)
|
|
if (s == null) { perr(`member access on non-aggregate {base.ty}`) }
|
|
let fidx = field_index(s, e.s)
|
|
if fidx < 0 { perr(`no such field {e.s}`) }
|
|
g_addr_ty = field_type(s, e.s)
|
|
let r = nreg()
|
|
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(layout_ty(base.ty))
|
|
emit(", ptr "); emit(base.code); emit(", i32 0, i32 "); emit(itoa(fidx)); emit("\n")
|
|
return r
|
|
}
|
|
|
|
# address of `base[index]` (slices only in this subset)
|
|
function emit_index_addr(e: Node) -> pointer {
|
|
let base = emit_expr(e.a)
|
|
if not is_slice_ty(base.ty) { # a raw pointer: address of element i
|
|
let bi = emit_expr(e.b) # (evaluate index first — it may set g_addr_ty)
|
|
if (base.ty == "words") { # a `words` buffer: 32-bit int elements
|
|
let rw = emit_bind(`getelementptr inbounds i32, ptr {base.code}, i32 {bi.code}`)
|
|
g_addr_ty = "int"
|
|
return rw
|
|
}
|
|
if (base.ty == "fixeds") { # a `fixeds` buffer: 32-bit fixed elements
|
|
let rf = emit_bind(`getelementptr inbounds i32, ptr {base.code}, i32 {bi.code}`)
|
|
g_addr_ty = "fixed"
|
|
return rf
|
|
}
|
|
if (base.ty == "floats") or (base.ty == "doubles") { # IEEE float / double elements
|
|
var et = "float"
|
|
if (base.ty == "doubles") { et = "double" }
|
|
let rd = emit_bind(`getelementptr inbounds {et}, ptr {base.code}, i32 {bi.code}`)
|
|
g_addr_ty = et
|
|
return rd
|
|
}
|
|
if (base.ty == "pointers") { # a `pointers` buffer: pointer elements
|
|
let rp = emit_bind(`getelementptr inbounds ptr, ptr {base.code}, i32 {bi.code}`)
|
|
g_addr_ty = "ptr"
|
|
return rp
|
|
}
|
|
let r = emit_bind(`getelementptr inbounds i8, ptr {base.code}, i32 {bi.code}`) # a ptr/str: bytes
|
|
g_addr_ty = "byte" # set last so the caller sees it
|
|
return r
|
|
}
|
|
let el = slice_elem(base.ty)
|
|
# load the data pointer from the slice header (field 0)
|
|
let dp = nreg()
|
|
emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ")
|
|
emit(base.code); emit(", i32 0, i32 0\n")
|
|
let data = nreg()
|
|
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
|
|
let ix = emit_expr(e.b) # (evaluate index BEFORE setting
|
|
# THE INDEX IS CHECKED AGAINST THE LENGTH. A slice has carried { ptr, len, cap } since it
|
|
# existed, and nothing ever read the len: every index was a bare getelementptr, so running
|
|
# off the end of one wrote into whatever the allocator had put next and the program died
|
|
# somewhere else entirely, minutes later, with a stack that named an innocent function.
|
|
# The comparison is UNSIGNED, which rejects a negative index in the same instruction.
|
|
if g_bounds {
|
|
g_uses_bounds = true
|
|
let lnp = nreg()
|
|
emit(" "); emit(lnp); emit(" = getelementptr inbounds %LSlice, ptr ")
|
|
emit(base.code); emit(", i32 0, i32 1\n")
|
|
let lnv = emit_bind(`load i32, ptr {lnp}`)
|
|
let okc = emit_bind(`icmp ult i32 {ix.code}, {lnv}`)
|
|
let lok = lbl("ixok")
|
|
let lbad = lbl("ixbad")
|
|
emit(` br i1 {okc}, label %{lok}, label %{lbad}\n`)
|
|
emit(`{lbad}:\n`)
|
|
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: index out of range: `)
|
|
let bse = emit_bind(stdstream_rhs(2))
|
|
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {ix.code}, i32 {lnv})\n`)
|
|
emit(" call void @exit(i32 1)\n unreachable\n")
|
|
emit(`{lok}:\n`)
|
|
}
|
|
let r = nreg() # g_addr_ty — a member-access index
|
|
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el)) # overwrites it)
|
|
emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n")
|
|
g_addr_ty = el # set last so the caller sees the element type
|
|
return r
|
|
}
|