feat(compiler): a slice index is checked against its length

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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-22 20:54:12 +03:00
parent f7f47152bd
commit 0998cb5a18
77 changed files with 803 additions and 696 deletions

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,28 @@ function emit_index_addr(e: Node) -> pointer {
let data = nreg() let data = nreg()
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n") emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
let ix = emit_expr(e.b) # (evaluate index BEFORE setting 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 let r = nreg() # g_addr_ty — a member-access index
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el)) # overwrites it) emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el)) # overwrites it)
emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n") emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n")

View file

@ -41,6 +41,9 @@ var g_uses_pak: bool = false # file_open was emitted -> emit the asset-pack ru
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
var g_uses_expect: bool = false # expect/expect_eq/expect_near was emitted -> emit the test-assert globals var g_uses_expect: bool = false # expect/expect_eq/expect_near was emitted -> emit the test-assert globals
var g_uses_panic: bool = false # panic/assert was emitted -> declare @fprintf + the panic format var g_uses_panic: bool = false # panic/assert was emitted -> declare @fprintf + the panic format
var g_uses_bounds: bool = false # a checked slice index was emitted -> declare the bounds format
var g_fprintf_declared: bool = false # @fprintf is declared once, by whoever needs it first
var g_bounds: bool = true # check every slice index (--no-bounds turns it off)
var g_uses_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46) var g_uses_result: bool = false # ok()/err()/try was emitted -> define the %Result value type (issue #46)
var g_uses_option: bool = false # some()/none() was emitted -> define the %Option value type (issue #53) var g_uses_option: bool = false # some()/none() was emitted -> define the %Option value type (issue #53)
var g_tests: []Node # test "name" { ... } blocks collected by the parser var g_tests: []Node # test "name" { ... } blocks collected by the parser

View file

@ -217,6 +217,7 @@ function emit_program() -> void {
g_uses_loopback = false g_uses_loopback = false
g_uses_expect = false g_uses_expect = false
g_uses_panic = false g_uses_panic = false
g_fprintf_declared = false
g_uses_result = false g_uses_result = false
g_uses_option = false g_uses_option = false
g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below) g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below)
@ -281,8 +282,13 @@ function emit_program() -> void {
if g_uses_datert { emit_datetime_prelude() } # @lp_days_from_civil / @lp_civil_from_days conversions if g_uses_datert { emit_datetime_prelude() } # @lp_days_from_civil / @lp_civil_from_days conversions
if g_uses_panic { # panic/assert: located abort to stderr if g_uses_panic { # panic/assert: located abort to stderr
emith("declare i32 @fprintf(ptr, ptr, ...)\n") emith("declare i32 @fprintf(ptr, ptr, ...)\n")
g_fprintf_declared = true
emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n") emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n")
} }
if g_uses_bounds { # a slice index out of range: located abort
if not g_fprintf_declared { emith("declare i32 @fprintf(ptr, ptr, ...)\n"); g_fprintf_declared = true }
emith("@.fmt_bounds = private unnamed_addr constant [14 x i8] c\"%s%d, len %d\\0A\\00\"\n")
}
if g_uses_result { emith("%Result = type { i32, i32, ptr }\n") } # issue #46: ok/err/try value if g_uses_result { emith("%Result = type { i32, i32, ptr }\n") } # issue #46: ok/err/try value
if g_uses_option { emith("%Option = type { i32, i32 }\n") } # issue #53: some/none value if g_uses_option { emith("%Option = type { i32, i32 }\n") } # issue #53: some/none value
emit_cov_runtime() # issue #45: --coverage tables + exit dump emit_cov_runtime() # issue #45: --coverage tables + exit dump
@ -318,7 +324,7 @@ function emit_cov_runtime() -> void {
let modec = emit_str_const("w") let modec = emit_str_const("w")
emith("@.cov_filefmt = private unnamed_addr constant [9 x i8] c\"FILE %s\\0A\\00\"\n") emith("@.cov_filefmt = private unnamed_addr constant [9 x i8] c\"FILE %s\\0A\\00\"\n")
emith("@.cov_rowfmt = private unnamed_addr constant [7 x i8] c\"%d %d\\0A\\00\"\n") emith("@.cov_rowfmt = private unnamed_addr constant [7 x i8] c\"%d %d\\0A\\00\"\n")
if not g_uses_panic { emith("declare i32 @fprintf(ptr, ptr, ...)\n") } if not g_fprintf_declared { emith("declare i32 @fprintf(ptr, ptr, ...)\n"); g_fprintf_declared = true }
emith("declare i32 @atexit(ptr)\n") emith("declare i32 @atexit(ptr)\n")
# @cov_dump: open $LUDIC_COVERAGE (or "ludic.cov"), write a FILE header then one # @cov_dump: open $LUDIC_COVERAGE (or "ludic.cov"), write a FILE header then one