i dunno lol

This commit is contained in:
Joseph Ferano 2026-09-10 17:49:24 +07:00
parent 2f38738f84
commit c476d05f7d
3 changed files with 81 additions and 4 deletions

View File

@ -102,8 +102,12 @@ test/programs/bounds.flan:25:29: slice [2 1) is out of bounds for length 5 (ex
Three check sites, and the third is the one with the trap in it:
- **`at` on `[n T]`** — the bound is static, so LLVM folds the check away for a
literal index. A literal that is *out* of bounds still only traps at runtime;
rejecting it in `check.ml` is a separate job.
literal index. A literal that is *out* of bounds never reaches emit at all:
`check.ml` rejects it, along with a negative literal index (wrong whatever
the target) and a literal `slice` range that runs backwards. Only literals —
a `defconst` is a global in the typed IR, not a folded constant, so
`(at a k)` stays a runtime trap. A slice bound may sit one past the end and
an index may not, which is the one place the two rules differ.
- **`at` on a slice or string** — the bound is the runtime len.
- **`slice`** — *two* comparisons, `lo <= hi` and `hi <= len`, both non-strict
because a slice ending at len (or an empty one at `lo = len`) is legal and

View File

@ -529,6 +529,25 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
| other ->
fail loc "deref takes a (Ptr T), found %s" (Types.to_string other))
(* An index or a slice bound that is a literal is known now, so it is an error
now rather than a trap later. Only literals: a [defconst] is a global in the
typed IR, not a folded constant, so [(at arr size)] still traps at runtime
which is what the emitted bounds check is for. A negative literal is wrong
whatever the target, but a length is static only for [n T]. *)
and static_index loc (ty : Types.t) ~past_end what k =
if k < 0L then
fail loc "%s %Ld is negative — indices count from 0" what k;
match ty with
(* [past_end] is the difference between an index and a slice bound: the last
valid index is len - 1, but a slice may end at len. *)
| Types.Array (n, _) when if past_end then k > n else k >= n ->
fail loc "%s %Ld is out of bounds for length %Ld" what k n
| _ -> ()
(* The literal value of a checked expression, if it has one. *)
and literal (e : Tast.expr) =
match e.Tast.e with Tast.Int (k, _) -> Some k | _ -> None
(* [(at a i)] and [(at grid row col)]: one index per dimension. *)
and indexed ctx (target : Tast.expr) (idx : Ast.expr list) =
let rec go ty = function
@ -540,7 +559,11 @@ and indexed ctx (target : Tast.expr) (idx : Ast.expr list) =
| other ->
fail i.Ast.loc "%s cannot be indexed" (Types.to_string other)
in
let loc = i.Ast.loc in
let i = check ctx ~want:index_ty i in
(match literal i with
| Some k -> static_index loc ty ~past_end:false "index" k
| None -> ());
let rest, ty = go elem rest in
i :: rest, ty
in
@ -616,8 +639,23 @@ and named_call ctx ~want loc name args =
(Types.to_string other)
in
prim Tast.Slice (Types.Slice elem)
(let lo = check ctx ~want:index_ty lo in
[ target; lo; check ctx ~want:index_ty hi ])
(let lo_loc = lo.Ast.loc and hi_loc = hi.Ast.loc in
let lo = check ctx ~want:index_ty lo in
let hi = check ctx ~want:index_ty hi in
let ty = target.Tast.ty in
(* A bound may sit one past the end, so the length is checked against
lo and hi both, not against the last valid index. *)
(match literal lo with
| Some k -> static_index lo_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal hi with
| Some k -> static_index hi_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal lo, literal hi with
| Some a, Some b when a > b ->
fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b
| _ -> ());
[ target; lo; hi ])
| _ -> assert false)
(* ── pointers ──────────────────────────────────────────────────── *)

View File

@ -403,6 +403,41 @@ let () =
rejects_check "if branches disagree"
"(defn f [] i32 (if true 1 true))" ~needle:"expected i32";
(* ── Static bounds ─────────────────────────────────────────────── *)
(* A literal index into a fixed array is known now, so it is an error now
rather than a trap later; everything else is the emitted bounds check's
job. A [defconst] is a global in the typed IR, not a folded constant, so
it deliberately stays a runtime trap. *)
let arr = "(defvar a [3 i32]) " in
accepts "last valid index" (arr ^ "(defn f [] i32 (at a 2))");
rejects_check "index past the end" (arr ^ "(defn f [] i32 (at a 3))")
~needle:"out of bounds for length 3";
rejects_check "negative index" (arr ^ "(defn f [] i32 (at a -1))")
~needle:"is negative";
rejects_check "index past the end of an inner dimension"
"(defvar g [2 [4 i32]]) (defn f [] i32 (at g 1 4))"
~needle:"out of bounds for length 4";
accepts "a variable index is checked at runtime, not here"
(arr ^ "(defn f [i i32] i32 (at a i))");
accepts "a defconst index is not folded"
("(defconst k 9) " ^ arr ^ "(defn f [] i32 (at a k))");
(* A slice bound may sit one past the end; an index may not. *)
accepts "slice ending at len" (arr ^ "(defn f [] [i32] (slice a 1 3))");
accepts "empty slice at len" (arr ^ "(defn f [] [i32] (slice a 3 3))");
rejects_check "slice bound past len" (arr ^ "(defn f [] [i32] (slice a 1 4))")
~needle:"out of bounds for length 3";
rejects_check "negative slice bound" (arr ^ "(defn f [] [i32] (slice a -1 2))")
~needle:"is negative";
rejects_check "reversed slice" (arr ^ "(defn f [] [i32] (slice a 2 1))")
~needle:"runs backwards";
(* A slice has no static length, so only the two target-independent rules
apply to one. *)
rejects_check "reversed slice of a slice"
"(defn f [s [u8]] [u8] (slice s 2 1))" ~needle:"runs backwards";
accepts "a slice's length is not known here"
"(defn f [s [u8]] [u8] (slice s 0 99))";
(* ── Structs, fields and auto-deref ────────────────────────────── *)
let cursor = "(defstruct Cursor [src [u8] pos i32]) " in
accepts "struct literal, omitted field zeroed"