Emit wasm
This commit is contained in:
parent
6d86d09a84
commit
2f38738f84
61
NEXT.md
61
NEXT.md
@ -24,7 +24,7 @@ reader ✅ → parse ✅ → check ✅ → emit ✅ → clang ✅
|
|||||||
| `runtime/flan_rt.c` | the whole host ABI: argv, stdout, exit, 4 conversions |
|
| `runtime/flan_rt.c` | the whole host ABI: argv, stdout, exit, 4 conversions |
|
||||||
| `bin/main.ml` | `flan read \| parse \| check \| emit \| build \| run` |
|
| `bin/main.ml` | `flan read \| parse \| check \| emit \| build \| run` |
|
||||||
| `test/test_flan.ml` | reader, parser and checker |
|
| `test/test_flan.ml` | reader, parser and checker |
|
||||||
| `test/test_acceptance.ml` | 20 expression/result pairs + 3 whole programs |
|
| `test/test_acceptance.ml` | 20 expression/result pairs + 3 whole programs + the traps |
|
||||||
| `test/programs/*.flan` | the milestone-2 surface calc-me does not reach |
|
| `test/programs/*.flan` | the milestone-2 surface calc-me does not reach |
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -86,16 +86,65 @@ nothing marshals. Two consequences carry the semantics:
|
|||||||
Non-local exit is lowered explicitly: `return` and `some` are branches to a
|
Non-local exit is lowered explicitly: `return` and `some` are branches to a
|
||||||
`ret`, never platform unwinding, so wasm32 needs no exception proposal.
|
`ret`, never platform unwinding, so wasm32 needs no exception proposal.
|
||||||
|
|
||||||
|
## Bounds checks — done
|
||||||
|
|
||||||
|
`at` and `slice` no longer emit a bare `getelementptr`. A failure is a branch
|
||||||
|
to a `noreturn cold` call and then `unreachable` — the same explicit shape as
|
||||||
|
`return` and `some`, so wasm32 needs nothing extra for it either. The message
|
||||||
|
carries the source location, because `Tast.expr` keeps a `Loc.t` and a language
|
||||||
|
that threads locations through the whole frontend should not trap anonymously:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ flan run test/programs/bounds.flan 2
|
||||||
|
test/programs/bounds.flan:25:29: slice [2 1) is out of bounds for length 5 (exit 134)
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
- **`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
|
||||||
|
its one-past-the-end gep is defined. `lo <= hi` is not redundant: without it
|
||||||
|
a reversed range yields `hi - lo` as a huge unsigned length, which is a worse
|
||||||
|
hole than the missing check was.
|
||||||
|
|
||||||
|
All comparisons are unsigned. Indices are i32 sign-extended to i64 for the gep,
|
||||||
|
so a negative one arrives as a huge unsigned value and one test catches both
|
||||||
|
directions; the runtime still prints the signed value in the message.
|
||||||
|
|
||||||
|
`Build.opts.checks` is on by default and **is not tied to `opts.opt`** — dev
|
||||||
|
traps, release does not, and that is a release decision rather than an
|
||||||
|
optimisation one. Keeping them separate is what lets the acceptance table go on
|
||||||
|
running the same programs at `-O0` and `-O2` with identical checks. The CLI
|
||||||
|
flag is `--no-bounds-checks`, on `build` and `emit`.
|
||||||
|
|
||||||
|
The write path is its own case. `(set (at arr n) …)` lowers through
|
||||||
|
`place`/`Pindex`, not through `At`, so a refactor that split them would break
|
||||||
|
the write check silently — the test covers both.
|
||||||
|
|
||||||
|
`test/programs/bounds.flan` is one program with one case per argument, because
|
||||||
|
a trap ends the process. The acceptance test asserts the exit code, that the
|
||||||
|
message names the file, and the reason — but not line and column, so editing
|
||||||
|
the program does not break the test that reads it. It runs at both `-O0` and
|
||||||
|
`-O2`, and one more case checks the IR directly: `--no-bounds-checks` emits no
|
||||||
|
`call` to either failure function. (The two `declare`s stay in the header
|
||||||
|
unconditionally; LLVM drops the unused ones.)
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
1. **wasm32.** The backend is there (`llc` lists `wasm32`) and
|
1. **wasm32.** The backend is there (`llc` lists `wasm32`) and
|
||||||
`Build.opts.target` already plumbs `--target`, but there is **no wasi
|
`Build.opts.target` already plumbs `--target`, but there is **no wasi
|
||||||
sysroot on this machine** — `clang --target=wasm32-wasi` cannot find
|
sysroot on this machine** — `clang --target=wasm32-wasi` cannot find
|
||||||
`stdio.h`. Install `wasi-sdk`/`wasi-libc`, then run the same acceptance
|
`stdio.h`. Install `wasi-sdk`/`wasi-libc` (`dnf search wasi` for the Fedora
|
||||||
table on both targets in CI. That is milestone 3's real remaining work.
|
package name), then run the same acceptance table on both targets in CI.
|
||||||
2. **Bounds checks.** `at` and `slice` emit a bare `getelementptr`. Dev builds
|
That is milestone 3's real remaining work. The bounds work above was written
|
||||||
should trap; release should not.
|
to survive the port — no unwinding, and `exit(134)` rather than `abort()`,
|
||||||
3. **Then milestone 4** — sand.flan: fixed 2-D arrays (done), `dotimes`,
|
so the same trap assertion should hold on wasm32 — but that is intent, not a
|
||||||
|
tested result: nothing here has ever been built for wasm32.
|
||||||
|
2. **Then milestone 4** — sand.flan: fixed 2-D arrays (done), `dotimes`,
|
||||||
`defer`, and typed raylib FFI with keyword→enum coercion.
|
`defer`, and typed raylib FFI with keyword→enum coercion.
|
||||||
|
|
||||||
## Watch for
|
## Watch for
|
||||||
|
|||||||
23
bin/main.ml
23
bin/main.ml
@ -23,6 +23,10 @@ let summarise (d : Flan.Ast.decl) =
|
|||||||
(match fn.ret with None -> "Unit" | Some _ -> "explicit")
|
(match fn.ret with None -> "Unit" | Some _ -> "explicit")
|
||||||
(List.length fn.fbody)
|
(List.length fn.fbody)
|
||||||
|
|
||||||
|
(* Bounds checks are on unless a build asks for them off — the release
|
||||||
|
decision, not the optimisation level (NEXT.md, Bounds checks). *)
|
||||||
|
let no_checks_flag = "--no-bounds-checks"
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
match Array.to_list Sys.argv with
|
match Array.to_list Sys.argv with
|
||||||
| _ :: "read" :: files when files <> [] ->
|
| _ :: "read" :: files when files <> [] ->
|
||||||
@ -63,28 +67,35 @@ let () =
|
|||||||
(Flan.Types.to_string f.ret) (Array.length f.slots))
|
(Flan.Types.to_string f.ret) (Array.length f.slots))
|
||||||
p.fns))
|
p.fns))
|
||||||
files
|
files
|
||||||
| _ :: "emit" :: files when files <> [] ->
|
| _ :: "emit" :: args when List.exists (fun a -> a <> no_checks_flag) args ->
|
||||||
|
let checks = not (List.mem no_checks_flag args) in
|
||||||
|
let files = List.filter (fun a -> a <> no_checks_flag) args in
|
||||||
List.iter
|
List.iter
|
||||||
(fun path ->
|
(fun path ->
|
||||||
with_errors path (fun () ->
|
with_errors path (fun () ->
|
||||||
Flan.Reader.read_file path
|
Flan.Reader.read_file path
|
||||||
|> Flan.Parse.program
|
|> Flan.Parse.program
|
||||||
|> Flan.Check.program
|
|> Flan.Check.program
|
||||||
|> Flan.Emit.program
|
|> Flan.Emit.program ~checks
|
||||||
|> print_string))
|
|> print_string))
|
||||||
files
|
files
|
||||||
| _ :: "build" :: path :: rest ->
|
| _ :: "build" :: path :: rest ->
|
||||||
|
let checks = not (List.mem no_checks_flag rest) in
|
||||||
let out =
|
let out =
|
||||||
match rest with
|
match List.filter (fun a -> a <> no_checks_flag) rest with
|
||||||
| [ "-o"; o ] -> o
|
| [ "-o"; o ] -> o
|
||||||
| [] -> Filename.remove_extension (Filename.basename path)
|
| [] -> Filename.remove_extension (Filename.basename path)
|
||||||
| _ -> prerr_endline "usage: flan build <file.flan> [-o out]"; exit 2
|
| _ ->
|
||||||
|
prerr_endline
|
||||||
|
"usage: flan build <file.flan> [-o out] [--no-bounds-checks]";
|
||||||
|
exit 2
|
||||||
in
|
in
|
||||||
with_errors path (fun () ->
|
with_errors path (fun () ->
|
||||||
Flan.Reader.read_file path
|
Flan.Reader.read_file path
|
||||||
|> Flan.Parse.program
|
|> Flan.Parse.program
|
||||||
|> Flan.Check.program
|
|> Flan.Check.program
|
||||||
|> fun p -> ignore (Flan.Build.executable p ~out))
|
|> fun p ->
|
||||||
|
ignore (Flan.Build.executable ~opts:{ Flan.Build.default with checks } p ~out))
|
||||||
| _ :: "run" :: path :: args ->
|
| _ :: "run" :: path :: args ->
|
||||||
with_errors path (fun () ->
|
with_errors path (fun () ->
|
||||||
let exe =
|
let exe =
|
||||||
@ -104,6 +115,6 @@ let () =
|
|||||||
| _ ->
|
| _ ->
|
||||||
prerr_endline
|
prerr_endline
|
||||||
"usage: flan (read|parse|check|emit) <file.flan>...\n\
|
"usage: flan (read|parse|check|emit) <file.flan>...\n\
|
||||||
\ flan build <file.flan> [-o out]\n\
|
\ flan build <file.flan> [-o out] [--no-bounds-checks]\n\
|
||||||
\ flan run <file.flan> [args...]";
|
\ flan run <file.flan> [args...]";
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
@ -29,15 +29,20 @@ type opts = {
|
|||||||
target : string option; (* None is the host; "wasm32-wasi" is the other *)
|
target : string option; (* None is the host; "wasm32-wasi" is the other *)
|
||||||
opt : string;
|
opt : string;
|
||||||
keep : bool; (* leave the .ll behind *)
|
keep : bool; (* leave the .ll behind *)
|
||||||
|
checks : bool; (* bounds-check [at] and [slice] *)
|
||||||
}
|
}
|
||||||
|
|
||||||
let default = { target = None; opt = "-O2"; keep = false }
|
(* Checks are deliberately independent of [opt]: the acceptance table runs the
|
||||||
|
same programs at -O0 and -O2 to compare the emitted IR against what mem2reg
|
||||||
|
makes of it, and that comparison is only meaningful if both emit the same
|
||||||
|
checks. Dropping them is a release decision, not an optimisation one. *)
|
||||||
|
let default = { target = None; opt = "-O2"; keep = false; checks = true }
|
||||||
|
|
||||||
let executable ?(opts = default) (p : Tast.program) ~out =
|
let executable ?(opts = default) (p : Tast.program) ~out =
|
||||||
let dir = workdir () in
|
let dir = workdir () in
|
||||||
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
||||||
let rt = Filename.concat dir "flan_rt.c" in
|
let rt = Filename.concat dir "flan_rt.c" in
|
||||||
write ll (Emit.program p);
|
write ll (Emit.program ~checks:opts.checks p);
|
||||||
write rt Runtime_src.source;
|
write rt Runtime_src.source;
|
||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
|
|||||||
94
lib/emit.ml
94
lib/emit.ml
@ -80,6 +80,7 @@ type m = {
|
|||||||
strs : Buffer.t; (* string literal constants *)
|
strs : Buffer.t; (* string literal constants *)
|
||||||
structs : (string, Tast.structure) Hashtbl.t;
|
structs : (string, Tast.structure) Hashtbl.t;
|
||||||
globals : (string, Types.t) Hashtbl.t;
|
globals : (string, Types.t) Hashtbl.t;
|
||||||
|
checks : bool; (* emit bounds checks *)
|
||||||
mutable nstr : int;
|
mutable nstr : int;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,14 +142,67 @@ let escape s =
|
|||||||
s;
|
s;
|
||||||
Buffer.contents b
|
Buffer.contents b
|
||||||
|
|
||||||
let string_const m s =
|
(* The constant itself, as the pointer and length a caller needs separately —
|
||||||
|
a bounds message crosses to C as ptr+len like any other slice. *)
|
||||||
|
let string_bytes m s =
|
||||||
let id = Printf.sprintf "@\".str.%d\"" m.nstr in
|
let id = Printf.sprintf "@\".str.%d\"" m.nstr in
|
||||||
m.nstr <- m.nstr + 1;
|
m.nstr <- m.nstr + 1;
|
||||||
Buffer.add_string m.strs
|
Buffer.add_string m.strs
|
||||||
(Printf.sprintf "%s = private unnamed_addr constant [%d x i8] c\"%s\"\n"
|
(Printf.sprintf "%s = private unnamed_addr constant [%d x i8] c\"%s\"\n"
|
||||||
id (String.length s) (escape s));
|
id (String.length s) (escape s));
|
||||||
|
id, String.length s
|
||||||
|
|
||||||
|
let string_const m s =
|
||||||
|
let id, n = string_bytes m s in
|
||||||
(* The value alone: LLVM takes the type from the operand's context. *)
|
(* The value alone: LLVM takes the type from the operand's context. *)
|
||||||
Printf.sprintf "{ ptr %s, i64 %d }" id (String.length s)
|
Printf.sprintf "{ ptr %s, i64 %d }" id n
|
||||||
|
|
||||||
|
(* ── Bounds checks ───────────────────────────────────────────────────── *)
|
||||||
|
|
||||||
|
(* A failure is a branch to a [noreturn] call and then [unreachable] — the same
|
||||||
|
explicit shape as [return] and [some], so wasm32 needs no unwinding for it
|
||||||
|
either. Whether to check is its own flag, not the optimisation level: dev
|
||||||
|
builds trap, release builds do not (NEXT.md), and the acceptance table runs
|
||||||
|
at both -O0 and -O2 with the checks on either way.
|
||||||
|
|
||||||
|
Indices are i32 in Flan and sign-extended to i64 for the gep, so a negative
|
||||||
|
one arrives here as a huge unsigned value: an unsigned comparison catches
|
||||||
|
the negative and the too-large case in a single test. *)
|
||||||
|
let fail_block f (loc : Loc.t) ok emit_call =
|
||||||
|
let good = fresh_label f "inb" and bad = fresh_label f "oob" in
|
||||||
|
term f "br i1 %s, label %%%s, label %%%s" ok good bad;
|
||||||
|
label f bad;
|
||||||
|
let id, n = string_bytes f.md (Loc.to_string loc) in
|
||||||
|
emit_call id n;
|
||||||
|
term f "unreachable";
|
||||||
|
label f good
|
||||||
|
|
||||||
|
(* [at] is strict: the last valid index is len - 1. *)
|
||||||
|
let check_at f loc idx len =
|
||||||
|
if f.md.checks then begin
|
||||||
|
let ok = fresh f in
|
||||||
|
ins f "%s = icmp ult i64 %s, %s" ok idx len;
|
||||||
|
fail_block f loc ok (fun id n ->
|
||||||
|
ins f "call void @flan_bounds_fail(ptr %s, i64 %d, i64 %s, i64 %s)"
|
||||||
|
id n idx len)
|
||||||
|
end
|
||||||
|
|
||||||
|
(* [slice] is not: a slice ending at len — or an empty one at lo = len — is
|
||||||
|
legal, and its one-past-the-end gep is defined. [lo <= hi] is not redundant
|
||||||
|
with it, because a reversed range would otherwise yield hi - lo as a huge
|
||||||
|
unsigned length, which is a worse hole than the missing check. *)
|
||||||
|
let check_slice f loc lo hi len =
|
||||||
|
if f.md.checks then begin
|
||||||
|
let a = fresh f in
|
||||||
|
ins f "%s = icmp ule i64 %s, %s" a lo hi;
|
||||||
|
let b = fresh f in
|
||||||
|
ins f "%s = icmp ule i64 %s, %s" b hi len;
|
||||||
|
let ok = fresh f in
|
||||||
|
ins f "%s = and i1 %s, %s" ok a b;
|
||||||
|
fail_block f loc ok (fun id n ->
|
||||||
|
ins f "call void @flan_slice_fail(ptr %s, i64 %d, i64 %s, i64 %s, i64 %s)"
|
||||||
|
id n lo hi len)
|
||||||
|
end
|
||||||
|
|
||||||
(* ── Expressions ───────────────────────────────────────────────────── *)
|
(* ── Expressions ───────────────────────────────────────────────────── *)
|
||||||
|
|
||||||
@ -254,7 +308,9 @@ and element_addr f (target : Tast.expr) idx =
|
|||||||
let i64 = fresh f in
|
let i64 = fresh f in
|
||||||
ins f "%s = sext %s %s to i64" i64 (ll i.Tast.ty) iv;
|
ins f "%s = sext %s %s to i64" i64 (ll i.Tast.ty) iv;
|
||||||
(match ty with
|
(match ty with
|
||||||
| Types.Array (_, elem) ->
|
| Types.Array (n, elem) ->
|
||||||
|
(* The bound is static; LLVM folds the check away for a literal index. *)
|
||||||
|
check_at f i.Tast.loc i64 (Int64.to_string n);
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
|
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
|
||||||
p (ll ty) ptr i64;
|
p (ll ty) ptr i64;
|
||||||
@ -264,6 +320,9 @@ and element_addr f (target : Tast.expr) idx =
|
|||||||
let s = load f ptr ty in
|
let s = load f ptr ty in
|
||||||
let base = fresh f in
|
let base = fresh f in
|
||||||
ins f "%s = extractvalue %%slice %s, 0" base s;
|
ins f "%s = extractvalue %%slice %s, 0" base s;
|
||||||
|
let len = fresh f in
|
||||||
|
ins f "%s = extractvalue %%slice %s, 1" len s;
|
||||||
|
check_at f i.Tast.loc i64 len;
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) base i64;
|
ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) base i64;
|
||||||
go p elem rest
|
go p elem rest
|
||||||
@ -424,7 +483,6 @@ and emit_unwrap f ty v =
|
|||||||
(* ── Primitives ────────────────────────────────────────────────────── *)
|
(* ── Primitives ────────────────────────────────────────────────────── *)
|
||||||
|
|
||||||
and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||||
ignore e;
|
|
||||||
match p, args with
|
match p, args with
|
||||||
| (Tast.Add | Tast.Sub | Tast.Mul | Tast.Div | Tast.Rem), [ x; y ] ->
|
| (Tast.Add | Tast.Sub | Tast.Mul | Tast.Div | Tast.Rem), [ x; y ] ->
|
||||||
let a = value f x in
|
let a = value f x in
|
||||||
@ -480,10 +538,15 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
|||||||
let hiv = value f hi in
|
let hiv = value f hi in
|
||||||
let lo64 = fresh f in
|
let lo64 = fresh f in
|
||||||
ins f "%s = sext i32 %s to i64" lo64 lov;
|
ins f "%s = sext i32 %s to i64" lo64 lov;
|
||||||
|
let hi64 = fresh f in
|
||||||
|
ins f "%s = sext i32 %s to i64" hi64 hiv;
|
||||||
|
(* The source is read once, and the check goes between reading it and the
|
||||||
|
gep: the length it is checked against must be the one the gep uses. *)
|
||||||
let base =
|
let base =
|
||||||
match target.Tast.ty with
|
match target.Tast.ty with
|
||||||
| Types.Array (_, _) ->
|
| Types.Array (n, _) ->
|
||||||
let a = addr f target in
|
let a = addr f target in
|
||||||
|
check_slice f e.Tast.loc lo64 hi64 (Int64.to_string n);
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
|
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
|
||||||
p (ll target.Tast.ty) a lo64;
|
p (ll target.Tast.ty) a lo64;
|
||||||
@ -492,6 +555,9 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
|||||||
let v = value f target in
|
let v = value f target in
|
||||||
let q = fresh f in
|
let q = fresh f in
|
||||||
ins f "%s = extractvalue %%slice %s, 0" q v;
|
ins f "%s = extractvalue %%slice %s, 0" q v;
|
||||||
|
let n = fresh f in
|
||||||
|
ins f "%s = extractvalue %%slice %s, 1" n v;
|
||||||
|
check_slice f e.Tast.loc lo64 hi64 n;
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) q lo64;
|
ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) q lo64;
|
||||||
p
|
p
|
||||||
@ -499,19 +565,20 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
|||||||
let v = value f target in
|
let v = value f target in
|
||||||
let q = fresh f in
|
let q = fresh f in
|
||||||
ins f "%s = extractvalue %%slice %s, 0" q v;
|
ins f "%s = extractvalue %%slice %s, 0" q v;
|
||||||
|
let n = fresh f in
|
||||||
|
ins f "%s = extractvalue %%slice %s, 1" n v;
|
||||||
|
check_slice f e.Tast.loc lo64 hi64 n;
|
||||||
let p = fresh f in
|
let p = fresh f in
|
||||||
ins f "%s = getelementptr inbounds i8, ptr %s, i64 %s" p q lo64;
|
ins f "%s = getelementptr inbounds i8, ptr %s, i64 %s" p q lo64;
|
||||||
p
|
p
|
||||||
| t -> failwith ("slice of " ^ Types.to_string t)
|
| t -> failwith ("slice of " ^ Types.to_string t)
|
||||||
in
|
in
|
||||||
let d = fresh f in
|
let d = fresh f in
|
||||||
ins f "%s = sub i32 %s, %s" d hiv lov;
|
ins f "%s = sub i64 %s, %s" d hi64 lo64;
|
||||||
let n = fresh f in
|
|
||||||
ins f "%s = sext i32 %s to i64" n d;
|
|
||||||
let a = fresh f in
|
let a = fresh f in
|
||||||
ins f "%s = insertvalue %%slice zeroinitializer, ptr %s, 0" a base;
|
ins f "%s = insertvalue %%slice zeroinitializer, ptr %s, 0" a base;
|
||||||
let b = fresh f in
|
let b = fresh f in
|
||||||
ins f "%s = insertvalue %%slice %s, i64 %s, 1" b a n;
|
ins f "%s = insertvalue %%slice %s, i64 %s, 1" b a d;
|
||||||
b
|
b
|
||||||
(* string and [u8] have the same layout, so bytes is the identity — a view,
|
(* string and [u8] have the same layout, so bytes is the identity — a view,
|
||||||
no copy (plan.org, Milestone-2 primitives). *)
|
no copy (plan.org, Milestone-2 primitives). *)
|
||||||
@ -668,6 +735,8 @@ declare double @flan_bytes_to_f64(ptr, i64)
|
|||||||
declare i64 @flan_bytes_to_i64(ptr, i64)
|
declare i64 @flan_bytes_to_i64(ptr, i64)
|
||||||
declare void @flan_f64_to_bytes(double, ptr)
|
declare void @flan_f64_to_bytes(double, ptr)
|
||||||
declare void @flan_i64_to_bytes(i64, ptr)
|
declare void @flan_i64_to_bytes(i64, ptr)
|
||||||
|
declare void @flan_bounds_fail(ptr, i64, i64, i64) noreturn cold
|
||||||
|
declare void @flan_slice_fail(ptr, i64, i64, i64, i64) noreturn cold
|
||||||
|}
|
|}
|
||||||
|
|
||||||
(* C's main, adapting to whichever of the four shapes Flan's main has: argv and
|
(* C's main, adapting to whichever of the four shapes Flan's main has: argv and
|
||||||
@ -695,10 +764,13 @@ let emit_main m (fn : Tast.fn) =
|
|||||||
Buffer.add_string b ")\n unreachable\n}\n";
|
Buffer.add_string b ")\n unreachable\n}\n";
|
||||||
Buffer.add_string m.out (Buffer.contents b)
|
Buffer.add_string m.out (Buffer.contents b)
|
||||||
|
|
||||||
let program (p : Tast.program) : string =
|
(* [checks] is on by default: a dev build traps on an out-of-bounds [at] or
|
||||||
|
[slice], a release build is told to drop them. *)
|
||||||
|
let program ?(checks = true) (p : Tast.program) : string =
|
||||||
let m = {
|
let m = {
|
||||||
out = Buffer.create 8192; strs = Buffer.create 512;
|
out = Buffer.create 8192; strs = Buffer.create 512;
|
||||||
structs = Hashtbl.create 16; globals = Hashtbl.create 16; nstr = 0;
|
structs = Hashtbl.create 16; globals = Hashtbl.create 16;
|
||||||
|
checks; nstr = 0;
|
||||||
} in
|
} in
|
||||||
List.iter (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s)
|
List.iter (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s)
|
||||||
p.Tast.structs;
|
p.Tast.structs;
|
||||||
|
|||||||
@ -84,3 +84,38 @@ void flan_i64_to_bytes(int64_t x, flan_slice *out) {
|
|||||||
out->ptr = (const uint8_t *)scratch;
|
out->ptr = (const uint8_t *)scratch;
|
||||||
out->len = n < 0 ? 0 : (int64_t)n;
|
out->len = n < 0 ? 0 : (int64_t)n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bounds failures. The emitted code branches here and then falls off the end
|
||||||
|
* with `unreachable`, so these must not return — the same explicit shape as
|
||||||
|
* every other non-local exit, which is what keeps wasm32 free of unwinding.
|
||||||
|
*
|
||||||
|
* The location is passed as ptr+len because that is what a Flan string already
|
||||||
|
* is; nothing here allocates. Exit 134 is abort()'s status without abort()'s
|
||||||
|
* signal, so the same assertion should hold once wasm32 builds.
|
||||||
|
*
|
||||||
|
* stdout is flushed *before* the message: stderr is unbuffered and a
|
||||||
|
* redirected stdout is not, so without this the error appears above the output
|
||||||
|
* that led to it. */
|
||||||
|
|
||||||
|
static _Noreturn void rt_die(void) {
|
||||||
|
fflush(stdout);
|
||||||
|
fflush(stderr);
|
||||||
|
exit(134);
|
||||||
|
}
|
||||||
|
|
||||||
|
_Noreturn void flan_bounds_fail(const uint8_t *loc, int64_t loclen,
|
||||||
|
int64_t idx, int64_t len) {
|
||||||
|
fflush(stdout);
|
||||||
|
fprintf(stderr, "%.*s: index %lld is out of bounds for length %lld\n",
|
||||||
|
(int)loclen, (const char *)loc, (long long)idx, (long long)len);
|
||||||
|
rt_die();
|
||||||
|
}
|
||||||
|
|
||||||
|
_Noreturn void flan_slice_fail(const uint8_t *loc, int64_t loclen,
|
||||||
|
int64_t lo, int64_t hi, int64_t len) {
|
||||||
|
fflush(stdout);
|
||||||
|
fprintf(stderr, "%.*s: slice [%lld %lld) is out of bounds for length %lld\n",
|
||||||
|
(int)loclen, (const char *)loc, (long long)lo, (long long)hi,
|
||||||
|
(long long)len);
|
||||||
|
rt_die();
|
||||||
|
}
|
||||||
|
|||||||
31
test/programs/bounds.flan
Normal file
31
test/programs/bounds.flan
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
;;;; Bounds checks, NEXT.md item 2. One program, one case per argument, so a
|
||||||
|
;;;; trap is observable: the checked build exits 134 with the source location
|
||||||
|
;;;; on stderr, the unchecked one runs off the end and is not asserted on.
|
||||||
|
;;;;
|
||||||
|
;;;; The selector is also the index wherever it can be, which is what keeps the
|
||||||
|
;;;; index dynamic — a literal would let the checker reject it outright one day
|
||||||
|
;;;; (that is a separate job) and lets LLVM fold the branch away here.
|
||||||
|
(defvar arr [3 i32])
|
||||||
|
|
||||||
|
(defn main [args [string]] i32
|
||||||
|
(let [n (i32 (bytes->i64 (bytes (at args 1))))
|
||||||
|
s (bytes "hello")] ; len 5
|
||||||
|
(cond
|
||||||
|
;; In bounds, including both edges: the last index, and a slice that
|
||||||
|
;; ends exactly at len. Neither may trap.
|
||||||
|
(= n 0) (do (print-i64 (i64 (at arr 2)))
|
||||||
|
(print-bytes (slice s 1 5))
|
||||||
|
(print-bytes (slice s 5 5)) ; empty at len is legal
|
||||||
|
(newline))
|
||||||
|
|
||||||
|
(= n 3) (print-i64 (i64 (at arr n))) ; past the end of a fixed array
|
||||||
|
(= n -1) (print-i64 (i64 (at arr n))) ; negative index
|
||||||
|
(= n 9) (print-i64 (i64 (at s n))) ; past the end of a slice
|
||||||
|
;; The write path lowers through place/Pindex rather than through At, so
|
||||||
|
;; it is checked separately even though the message is the same.
|
||||||
|
(= n 7) (set (at arr n) 1) ; write past the end
|
||||||
|
(= n 4) (print-bytes (slice s n 9)) ; hi past the end
|
||||||
|
(= n 2) (print-bytes (slice s n 1)) ; reversed range
|
||||||
|
|
||||||
|
:else (print-line "?"))
|
||||||
|
0))
|
||||||
@ -23,15 +23,21 @@ let run exe arg =
|
|||||||
Sys.remove out;
|
Sys.remove out;
|
||||||
(code, text)
|
(code, text)
|
||||||
|
|
||||||
let compile ?(opt = "-O2") path =
|
let compile ?(opt = "-O2") ?(checks = true) path =
|
||||||
let exe =
|
let exe =
|
||||||
Filename.concat scratch
|
Filename.concat scratch
|
||||||
("flan-t-" ^ Filename.remove_extension (Filename.basename path))
|
("flan-t-" ^ Filename.remove_extension (Filename.basename path))
|
||||||
in
|
in
|
||||||
let p = Reader.read_file path |> Parse.program |> Check.program in
|
let p = Reader.read_file path |> Parse.program |> Check.program in
|
||||||
ignore (Build.executable ~opts:{ Build.default with opt } p ~out:exe);
|
ignore (Build.executable ~opts:{ Build.default with opt; checks } p ~out:exe);
|
||||||
exe
|
exe
|
||||||
|
|
||||||
|
(* No Str, and the reader is hand-written for the same reason. *)
|
||||||
|
let contains hay needle =
|
||||||
|
let n = String.length needle and h = String.length hay in
|
||||||
|
let rec go i = i + n <= h && (String.sub hay i n = needle || go (i + 1)) in
|
||||||
|
go 0
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
match Sys.command "command -v clang > /dev/null 2>&1" with
|
match Sys.command "command -v clang > /dev/null 2>&1" with
|
||||||
| 0 ->
|
| 0 ->
|
||||||
@ -106,6 +112,71 @@ let () =
|
|||||||
outputs ~opt:"-O0" "value semantics, -O0" "programs/values.flan" values_out;
|
outputs ~opt:"-O0" "value semantics, -O0" "programs/values.flan" values_out;
|
||||||
outputs ~opt:"-O0" "machine surface, -O0" "programs/machine.flan" machine_out;
|
outputs ~opt:"-O0" "machine surface, -O0" "programs/machine.flan" machine_out;
|
||||||
|
|
||||||
|
(* Bounds checks, NEXT.md item 2. A trap has no result — it has a nonzero
|
||||||
|
exit and a message on stderr — so it needs a case shape the table above
|
||||||
|
does not have. What is asserted is the *reason*: the location, and which
|
||||||
|
index against which length. The line and column are not pinned, because
|
||||||
|
editing the program should not break the test that reads it. *)
|
||||||
|
let bounds ?opt () =
|
||||||
|
let exe = compile ?opt "programs/bounds.flan" in
|
||||||
|
let traps name arg reason =
|
||||||
|
let code, text = run exe (Some arg) in
|
||||||
|
if code <> 134
|
||||||
|
|| not (contains text "programs/bounds.flan:")
|
||||||
|
|| not (contains text reason)
|
||||||
|
then begin
|
||||||
|
incr failures;
|
||||||
|
Printf.printf
|
||||||
|
"FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 134)\n"
|
||||||
|
name text code reason
|
||||||
|
end
|
||||||
|
in
|
||||||
|
(* Both edges are in bounds and must not trap: the last index of a fixed
|
||||||
|
array, a slice ending exactly at len, and an empty slice at len. *)
|
||||||
|
let code, text = run exe (Some "0") in
|
||||||
|
if text <> "0ello\n" || code <> 0 then begin
|
||||||
|
incr failures;
|
||||||
|
Printf.printf "FAIL in-bounds edges\n got: %S (exit %d)\n" text code
|
||||||
|
end;
|
||||||
|
traps "at past a fixed array" "3"
|
||||||
|
"index 3 is out of bounds for length 3";
|
||||||
|
(* Negative indices sext to a huge unsigned, so the one unsigned
|
||||||
|
comparison catches them; the message still reports the signed value. *)
|
||||||
|
traps "at with a negative index" "-1"
|
||||||
|
"index -1 is out of bounds for length 3";
|
||||||
|
traps "at past a slice" "9"
|
||||||
|
"index 9 is out of bounds for length 5";
|
||||||
|
(* A different lowering — place/Pindex, not At — so it is its own case. *)
|
||||||
|
traps "set past a fixed array" "7"
|
||||||
|
"index 7 is out of bounds for length 3";
|
||||||
|
traps "slice with hi past len" "4"
|
||||||
|
"slice [4 9) is out of bounds for length 5";
|
||||||
|
(* Without the lo <= hi test this one would not trap: it would build a
|
||||||
|
slice of length hi - lo as a huge unsigned, which is worse. *)
|
||||||
|
traps "slice with a reversed range" "2"
|
||||||
|
"slice [2 1) is out of bounds for length 5";
|
||||||
|
(try Sys.remove exe with Sys_error _ -> ())
|
||||||
|
in
|
||||||
|
bounds ();
|
||||||
|
bounds ~opt:"-O0" ();
|
||||||
|
|
||||||
|
(* The release build drops them — the calls, that is; the two declarations
|
||||||
|
stay in the header and LLVM discards the unused ones. Asserted on the IR
|
||||||
|
rather than by running an unchecked out-of-bounds program, which has no
|
||||||
|
defined behaviour to assert on. *)
|
||||||
|
let p =
|
||||||
|
Reader.read_file "programs/bounds.flan" |> Parse.program |> Check.program
|
||||||
|
in
|
||||||
|
if not (contains (Emit.program p) "call void @flan_bounds_fail(") then begin
|
||||||
|
incr failures;
|
||||||
|
print_endline "FAIL checks on: no bounds call emitted"
|
||||||
|
end;
|
||||||
|
let off = Emit.program ~checks:false p in
|
||||||
|
if contains off "call void @flan_bounds_fail(" || contains off "call void @flan_slice_fail(" then begin
|
||||||
|
incr failures;
|
||||||
|
print_endline "FAIL --no-bounds-checks: a check survived"
|
||||||
|
end;
|
||||||
|
|
||||||
if !failures = 0 then print_endline "acceptance: all tests passed"
|
if !failures = 0 then print_endline "acceptance: all tests passed"
|
||||||
else begin
|
else begin
|
||||||
Printf.printf "\n%d failure(s)\n" !failures;
|
Printf.printf "\n%d failure(s)\n" !failures;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user