Two rules the checker was missing
A shift by the operand's own width or more is poison in LLVM, not a wrong number: (<< 1 32) at -O2 compiled to a bare retq. A literal count out of range is now rejected in check.ml, and emit.ml masks a computed one to width - 1, which is what the hardware does and which LLVM folds away for a constant. There is one top-level namespace, but the environment's tables are per-kind, so only a function was ever checked for a duplicate. (defn item ...) beside (defvar item ...) type checked and then died in LLVM as a redefinition of '@flan.item'; two colliding type declarations were not caught anywhere. One pass over Ast.declared_name now runs before every other collection pass. That function lives in ast.ml because Load needs the same set - the names an import renames - and two copies would drift.
This commit is contained in:
parent
a9d93c7b64
commit
f83ca7de6f
27
NEXT.md
27
NEXT.md
@ -237,13 +237,38 @@ Non-local exit is lowered explicitly: `return`, `some` and a failed bounds
|
|||||||
check are branches, never platform unwinding, so wasm32 needs no exception
|
check are branches, never platform unwinding, so wasm32 needs no exception
|
||||||
proposal.
|
proposal.
|
||||||
|
|
||||||
## Sharp edges found and left visible
|
## Sharp edges
|
||||||
|
|
||||||
|
Most of these are edges the language keeps and you should know about. Two —
|
||||||
|
the top-level namespace and the shift count, both found by review after
|
||||||
|
milestone 4 — were bugs that reached LLVM or ran wrong, and are **fixed**; each
|
||||||
|
says so. They stay written down because each one is now a rule the checker
|
||||||
|
enforces, and a later change could quietly drop it.
|
||||||
|
|
||||||
- **An index converts from a narrower integer and never from a wider one.**
|
- **An index converts from a narrower integer and never from a wider one.**
|
||||||
`(nth colors current-color)` with a `u32` index works — anything above 2³¹
|
`(nth colors current-color)` with a `u32` index works — anything above 2³¹
|
||||||
truncates to a negative `i32` and the unsigned bounds check rejects it. An
|
truncates to a negative `i32` and the unsigned bounds check rejects it. An
|
||||||
`i64` index is refused with the reason: 2³²+5 truncates to 5 and would read
|
`i64` index is refused with the reason: 2³²+5 truncates to 5 and would read
|
||||||
the wrong element with no trap at all.
|
the wrong element with no trap at all.
|
||||||
|
- **There is one top-level namespace, and `check.ml` now enforces it.** The
|
||||||
|
environment's tables are per-kind — structs, unions, aliases, enums,
|
||||||
|
functions, externs and globals each have their own — so only a function was
|
||||||
|
ever checked for a duplicate. `(defn item …)` beside `(defvar item …)` type
|
||||||
|
checked and then died in LLVM as `redefinition of function '@flan.item'`, a
|
||||||
|
message about an emitted symbol with no source location left, and two
|
||||||
|
colliding *type* declarations were not caught anywhere. One pass over
|
||||||
|
`Ast.declared_name` now runs before every other collection pass and rejects
|
||||||
|
the second declaration of a name whatever kind either one is. `declared_name`
|
||||||
|
lives in `ast.ml` because `Load` needs exactly the same set — the names an
|
||||||
|
import renames — and two copies of that list would drift.
|
||||||
|
- **A shift count is bounded, two different ways.** A shift by the operand's
|
||||||
|
own width or more is *poison* in LLVM, not a wrong number: `(defn main [] i32
|
||||||
|
(<< 1 32))` compiled at -O2 to a bare `retq`, returning an undefined value. A
|
||||||
|
literal count out of range is now rejected in `check.ml` — that is the typo
|
||||||
|
case — and `emit.ml` masks a computed count to `width - 1`, which is what the
|
||||||
|
hardware does anyway and which LLVM folds away whenever the count is
|
||||||
|
constant. The prelude's rotate masks its own count; that is now redundant but
|
||||||
|
harmless.
|
||||||
- **A `u64` literal is its 64-bit pattern**, so `0xcbf29ce484222325` is a real
|
- **A `u64` literal is its 64-bit pattern**, so `0xcbf29ce484222325` is a real
|
||||||
`u64` and not an error. The cost is that a negative *decimal* literal is
|
`u64` and not an error. The cost is that a negative *decimal* literal is
|
||||||
accepted as a `u64` too, because the reader records the value and not how it
|
accepted as a `u64` too, because the reader records the value and not how it
|
||||||
|
|||||||
11
lib/ast.ml
11
lib/ast.ml
@ -108,3 +108,14 @@ and decl_kind =
|
|||||||
and variant = { vname : string; vfields : field list; vloc : Loc.t }
|
and variant = { vname : string; vfields : field list; vloc : Loc.t }
|
||||||
|
|
||||||
and init = Zeroed | Uninit | Init of expr
|
and init = Zeroed | Uninit | Init of expr
|
||||||
|
|
||||||
|
(* Every top-level name a declaration introduces, whatever kind it is. There is
|
||||||
|
one top-level namespace, so this is both the set [Load] renames on an import
|
||||||
|
and the set [Check] refuses to see twice — one definition, so the two cannot
|
||||||
|
drift apart. *)
|
||||||
|
let declared_name (d : decl) =
|
||||||
|
match d.d with
|
||||||
|
| Defenum (n, _) | Defalias (n, _) | Defstruct (n, _) | Defunion (n, _)
|
||||||
|
| Defvar (n, _, _) | Defconst (n, _, _) -> Some n
|
||||||
|
| Declare (fn, _) | Defn fn -> Some fn.name
|
||||||
|
| Package _ | Import _ -> None
|
||||||
|
|||||||
31
lib/check.ml
31
lib/check.ml
@ -767,6 +767,19 @@ and named_call ctx ~want loc name args =
|
|||||||
| Types.Int _ -> ()
|
| Types.Int _ -> ()
|
||||||
| other -> fail loc "%s takes integers, found %s" name
|
| other -> fail loc "%s takes integers, found %s" name
|
||||||
(Types.to_string other));
|
(Types.to_string other));
|
||||||
|
(* A shift by the operand's own width or more is poison in LLVM, which at
|
||||||
|
-O2 turns the whole function into an undefined value rather than into a
|
||||||
|
wrong number. A literal count is rejected here — that is the typo — and
|
||||||
|
[emit] masks a computed one, so no shift can reach the hardware out of
|
||||||
|
range. *)
|
||||||
|
(match a.Tast.ty, b.Tast.e with
|
||||||
|
| Types.Int k, Tast.Int (n, _) when p = Tast.Shl || p = Tast.Shr ->
|
||||||
|
let w = Int64.of_int (Types.bits k) in
|
||||||
|
if Int64.unsigned_compare n w >= 0 then
|
||||||
|
fail loc
|
||||||
|
"%s by %Ld is out of range for %s, which is %d bits wide" name n
|
||||||
|
(Types.to_string a.Tast.ty) (Types.bits k)
|
||||||
|
| _ -> ());
|
||||||
prim p a.Tast.ty [ a; b ]
|
prim p a.Tast.ty [ a; b ]
|
||||||
(* (min a b) and (max a b) evaluate each operand once — hence the slots —
|
(* (min a b) and (max a b) evaluate each operand once — hence the slots —
|
||||||
because a min over two calls must not call either of them twice. *)
|
because a min over two calls must not call either of them twice. *)
|
||||||
@ -983,6 +996,22 @@ let rec const_int env (e : Ast.expr) : int64 option =
|
|||||||
| _ -> None
|
| _ -> None
|
||||||
|
|
||||||
let collect env (decls : Ast.decl list) =
|
let collect env (decls : Ast.decl list) =
|
||||||
|
(* One pass over every declaration kind before any of the others, because
|
||||||
|
the tables below are per-kind — structs, unions, aliases, enums, functions
|
||||||
|
and globals each have their own — and a collision between two of them
|
||||||
|
would otherwise be found by LLVM, as [redefinition of function
|
||||||
|
'@flan.item'], or not at all. A [defn item] and a [defvar item] are two
|
||||||
|
declarations of one name and are rejected here. *)
|
||||||
|
let claimed = Hashtbl.create 64 in
|
||||||
|
List.iter
|
||||||
|
(fun (d : Ast.decl) ->
|
||||||
|
match Ast.declared_name d with
|
||||||
|
| None -> ()
|
||||||
|
| Some n ->
|
||||||
|
if Hashtbl.mem claimed n then
|
||||||
|
fail d.Ast.dloc "%s is defined twice" n;
|
||||||
|
Hashtbl.add claimed n ())
|
||||||
|
decls;
|
||||||
(* Names first, so a struct may mention one declared below it. *)
|
(* Names first, so a struct may mention one declared below it. *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun (d : Ast.decl) ->
|
(fun (d : Ast.decl) ->
|
||||||
@ -1086,8 +1115,6 @@ let collect env (decls : Ast.decl list) =
|
|||||||
{ Tast.vname = v.Ast.vname;
|
{ Tast.vname = v.Ast.vname;
|
||||||
vfields = List.map field v.Ast.vfields }) vs }
|
vfields = List.map field v.Ast.vfields }) vs }
|
||||||
| Ast.Defn fn ->
|
| Ast.Defn fn ->
|
||||||
if Hashtbl.mem env.fns fn.Ast.name then
|
|
||||||
fail loc "%s is defined twice" fn.Ast.name;
|
|
||||||
let params =
|
let params =
|
||||||
List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) fn.Ast.params
|
List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) fn.Ast.params
|
||||||
in
|
in
|
||||||
|
|||||||
14
lib/emit.ml
14
lib/emit.ml
@ -555,6 +555,20 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
|||||||
| Types.Int k, _ -> if Types.signed k then "ashr" else "lshr"
|
| Types.Int k, _ -> if Types.signed k then "ashr" else "lshr"
|
||||||
| t, _ -> failwith ("bitwise on " ^ Types.to_string t)
|
| t, _ -> failwith ("bitwise on " ^ Types.to_string t)
|
||||||
in
|
in
|
||||||
|
(* The count is masked to the operand's width. LLVM makes an over-wide
|
||||||
|
shift poison, and a poison return at -O2 is a function that returns
|
||||||
|
nothing at all; masking is what the hardware does anyway, and LLVM folds
|
||||||
|
the [and] away whenever the count is a constant. [check] has already
|
||||||
|
rejected a literal that is out of range, so this only ever fires on a
|
||||||
|
computed count. *)
|
||||||
|
let b =
|
||||||
|
match x.Tast.ty, p with
|
||||||
|
| Types.Int k, (Tast.Shl | Tast.Shr) ->
|
||||||
|
let m = fresh f in
|
||||||
|
ins f "%s = and %s %s, %d" m (ll x.Tast.ty) b (Types.bits k - 1);
|
||||||
|
m
|
||||||
|
| _ -> b
|
||||||
|
in
|
||||||
let t = fresh f in
|
let t = fresh f in
|
||||||
ins f "%s = %s %s %s, %s" t op (ll x.Tast.ty) a b;
|
ins f "%s = %s %s %s, %s" t op (ll x.Tast.ty) a b;
|
||||||
t
|
t
|
||||||
|
|||||||
10
lib/load.ml
10
lib/load.ml
@ -228,15 +228,7 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
|
|||||||
(* Every top-level name the package declares — types and values alike, since a
|
(* Every top-level name the package declares — types and values alike, since a
|
||||||
use site is rewritten by name and the two never collide in one namespace. *)
|
use site is rewritten by name and the two never collide in one namespace. *)
|
||||||
let owned_names (ds : Ast.decl list) =
|
let owned_names (ds : Ast.decl list) =
|
||||||
List.filter_map
|
List.filter_map Ast.declared_name ds
|
||||||
(fun (d : Ast.decl) ->
|
|
||||||
match d.Ast.d with
|
|
||||||
| Ast.Defenum (n, _) | Ast.Defalias (n, _) | Ast.Defstruct (n, _)
|
|
||||||
| Ast.Defunion (n, _) | Ast.Defvar (n, _, _) | Ast.Defconst (n, _, _) ->
|
|
||||||
Some n
|
|
||||||
| Ast.Declare (fn, _) | Ast.Defn fn -> Some fn.Ast.name
|
|
||||||
| Ast.Package _ | Ast.Import _ -> None)
|
|
||||||
ds
|
|
||||||
|
|
||||||
let import ~loc alias dir =
|
let import ~loc alias dir =
|
||||||
let files = entries dir ".flan" in
|
let files = entries dir ".flan" in
|
||||||
|
|||||||
@ -552,6 +552,30 @@ let () =
|
|||||||
(* A folded constant skips [check], so its range check has to be its own. *)
|
(* A folded constant skips [check], so its range check has to be its own. *)
|
||||||
rejects_check "a folded constant is still range-checked"
|
rejects_check "a folded constant is still range-checked"
|
||||||
"(defconst c u8 300) (defn f [] u8 c)" ~needle:"does not fit in u8";
|
"(defconst c u8 300) (defn f [] u8 c)" ~needle:"does not fit in u8";
|
||||||
|
(* One top-level namespace, enforced across declaration kinds. Each of these
|
||||||
|
used to pass the checker — the tables are per-kind — and be caught by LLVM
|
||||||
|
as a redefinition of an emitted symbol, or not caught at all. *)
|
||||||
|
rejects_check "a global defined twice"
|
||||||
|
"(defvar x i32 1) (defvar x i32 2)" ~needle:"defined twice";
|
||||||
|
rejects_check "a constant shadowing a variable"
|
||||||
|
"(defconst c 1) (defvar c i32 2)" ~needle:"defined twice";
|
||||||
|
rejects_check "a function and a global"
|
||||||
|
"(defn item [] i32 1) (defvar item i32 2)" ~needle:"defined twice";
|
||||||
|
rejects_check "a struct and an alias"
|
||||||
|
"(defstruct P [x i32]) (defalias P i32)" ~needle:"defined twice";
|
||||||
|
rejects_check "an enum and a struct"
|
||||||
|
"(defenum E [a 1]) (defstruct E [x i32])" ~needle:"defined twice";
|
||||||
|
rejects_check "an extern and a constant"
|
||||||
|
"(declare cw [] \"flan_cw\") (defconst cw 1)" ~needle:"defined twice";
|
||||||
|
(* A shift by the operand's own width or more is poison in LLVM, and at -O2
|
||||||
|
a poison return is a function that returns nothing at all. A literal count
|
||||||
|
is rejected; a computed one is masked in [emit]. *)
|
||||||
|
rejects_check "a shift past the operand's width"
|
||||||
|
"(defn f [] i32 (<< 1 32))" ~needle:"out of range";
|
||||||
|
rejects_check "a right shift past the operand's width"
|
||||||
|
"(defn f [] u8 (>> (u8 1) 8))" ~needle:"out of range";
|
||||||
|
accepts "a shift by the widest count in range"
|
||||||
|
"(defn f [] i32 (<< 1 31))";
|
||||||
(* An index converts from a narrower integer and never from a wider one. *)
|
(* An index converts from a narrower integer and never from a wider one. *)
|
||||||
accepts "a u32 index" "(defvar a [4 u32]) (defn f [] u32 (let [i 2] (at a (u32 i))))";
|
accepts "a u32 index" "(defvar a [4 u32]) (defn f [] u32 (let [i 2] (at a (u32 i))))";
|
||||||
rejects_check "an i64 index"
|
rejects_check "an i64 index"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user