blob: cfe7f310f28e8754579485e29a407e1909fe18e5 [file] [log] [blame] [edit]
open Ast
open Source
open Types
(* Errors *)
module Invalid = Error.Make ()
exception Invalid = Invalid.Error
let error = Invalid.error
let require b at s = if not b then error at s
(* Context *)
type context =
{
types : func_type list;
funcs : func_type list;
tables : table_type list;
memories : memory_type list;
globals : global_type list;
elems : ref_type list;
datas : unit list;
locals : value_type list;
results : value_type list;
labels : result_type list;
refs : Free.t;
}
let empty_context =
{ types = []; funcs = []; tables = []; memories = [];
globals = []; elems = []; datas = [];
locals = []; results = []; labels = [];
refs = Free.empty
}
let lookup category list x =
try Lib.List32.nth list x.it with Failure _ ->
error x.at ("unknown " ^ category ^ " " ^ I32.to_string_u x.it)
let type_ (c : context) x = lookup "type" c.types x
let func (c : context) x = lookup "function" c.funcs x
let table (c : context) x = lookup "table" c.tables x
let memory (c : context) x = lookup "memory" c.memories x
let global (c : context) x = lookup "global" c.globals x
let elem (c : context) x = lookup "elem segment" c.elems x
let data (c : context) x = lookup "data segment" c.datas x
let local (c : context) x = lookup "local" c.locals x
let label (c : context) x = lookup "label" c.labels x
let refer category (s : Free.Set.t) x =
if not (Free.Set.mem x.it s) then
error x.at
("undeclared " ^ category ^ " reference " ^ Int32.to_string x.it)
let refer_func (c : context) x = refer "function" c.refs.Free.funcs x
(* Stack typing *)
(*
* Note: The declarative typing rules are non-deterministic, that is, they
* have the liberty to locally "guess" the right types implied by the context.
* In the algorithmic formulation required here, stack types are hence modelled
* as lists of _options_ of types, where `None` represents a locally
* unknown type. Furthermore, an ellipses flag represents arbitrary sequences
* of unknown types, in order to handle stack polymorphism algorithmically.
*)
type ellipses = NoEllipses | Ellipses
type infer_result_type = ellipses * value_type option list
type op_type = {ins : infer_result_type; outs : infer_result_type}
let known = List.map (fun t -> Some t)
let stack ts = (NoEllipses, known ts)
let (-~>) ts1 ts2 = {ins = NoEllipses, ts1; outs = NoEllipses, ts2}
let (-->) ts1 ts2 = {ins = NoEllipses, known ts1; outs = NoEllipses, known ts2}
let (-~>...) ts1 ts2 = {ins = Ellipses, ts1; outs = Ellipses, ts2}
let (-->...) ts1 ts2 = {ins = Ellipses, known ts1; outs = Ellipses, known ts2}
let string_of_infer_type t =
Lib.Option.get (Lib.Option.map string_of_value_type t) "_"
let string_of_infer_types ts =
"[" ^ String.concat " " (List.map string_of_infer_type ts) ^ "]"
let eq_ty t1 t2 = (t1 = t2 || t1 = None || t2 = None)
let check_stack ts1 ts2 at =
require (List.length ts1 = List.length ts2 && List.for_all2 eq_ty ts1 ts2) at
("type mismatch: instruction requires " ^ string_of_infer_types ts1 ^
" but stack has " ^ string_of_infer_types ts2)
let pop (ell1, ts1) (ell2, ts2) at =
let n1 = List.length ts1 in
let n2 = List.length ts2 in
let n = min n1 n2 in
let n3 = if ell2 = Ellipses then (n1 - n) else 0 in
check_stack ts1 (Lib.List.make n3 None @ Lib.List.drop (n2 - n) ts2) at;
(ell2, if ell1 = Ellipses then [] else Lib.List.take (n2 - n) ts2)
let push (ell1, ts1) (ell2, ts2) =
assert (ell1 = NoEllipses || ts2 = []);
(if ell1 = Ellipses || ell2 = Ellipses then Ellipses else NoEllipses),
ts2 @ ts1
let peek i (ell, ts) =
try List.nth (List.rev ts) i with Failure _ -> None
(* Type Synthesis *)
let type_num = Values.type_of_num
let type_vec = Values.type_of_vec
let type_vec_lane = function
| Values.V128 laneop -> V128.type_of_lane laneop
let type_cvtop at = function
| Values.I32 cvtop ->
let open I32Op in
(match cvtop with
| ExtendSI32 | ExtendUI32 -> error at "invalid conversion"
| WrapI64 -> I64Type
| TruncSF32 | TruncUF32 | TruncSatSF32 | TruncSatUF32
| ReinterpretFloat -> F32Type
| TruncSF64 | TruncUF64 | TruncSatSF64 | TruncSatUF64 -> F64Type
), I32Type
| Values.I64 cvtop ->
let open I64Op in
(match cvtop with
| ExtendSI32 | ExtendUI32 -> I32Type
| WrapI64 -> error at "invalid conversion"
| TruncSF32 | TruncUF32 | TruncSatSF32 | TruncSatUF32 -> F32Type
| TruncSF64 | TruncUF64 | TruncSatSF64 | TruncSatUF64
| ReinterpretFloat -> F64Type
), I64Type
| Values.F32 cvtop ->
let open F32Op in
(match cvtop with
| ConvertSI32 | ConvertUI32 | ReinterpretInt -> I32Type
| ConvertSI64 | ConvertUI64 -> I64Type
| PromoteF32 -> error at "invalid conversion"
| DemoteF64 -> F64Type
), F32Type
| Values.F64 cvtop ->
let open F64Op in
(match cvtop with
| ConvertSI32 | ConvertUI32 -> I32Type
| ConvertSI64 | ConvertUI64 | ReinterpretInt -> I64Type
| PromoteF32 -> F32Type
| DemoteF64 -> error at "invalid conversion"
), F64Type
let num_lanes = function
| Values.V128 laneop -> V128.num_lanes laneop
let lane_extractop = function
| Values.V128 extractop ->
let open V128 in let open V128Op in
match extractop with
| I8x16 (Extract (i, _)) | I16x8 (Extract (i, _))
| I32x4 (Extract (i, _)) | I64x2 (Extract (i, _))
| F32x4 (Extract (i, _)) | F64x2 (Extract (i, _)) -> i
let lane_replaceop = function
| Values.V128 replaceop ->
let open V128 in let open V128Op in
match replaceop with
| I8x16 (Replace i) | I16x8 (Replace i)
| I32x4 (Replace i) | I64x2 (Replace i)
| F32x4 (Replace i) | F64x2 (Replace i) -> i
(* Expressions *)
let check_pack sz t_sz at =
require (packed_size sz < t_sz) at "invalid sign extension"
let check_unop unop at =
match unop with
| Values.I32 (IntOp.ExtendS sz) | Values.I64 (IntOp.ExtendS sz) ->
check_pack sz (num_size (Values.type_of_num unop)) at
| _ -> ()
let check_vec_binop binop at =
match binop with
| Values.(V128 (V128.I8x16 (V128Op.Shuffle is))) ->
if List.exists ((<=) 32) is then
error at "invalid lane index"
| _ -> ()
let check_memop (c : context) (memop : ('t, 's) memop) ty_size get_sz at =
let _mt = memory c (0l @@ at) in
let size =
match get_sz memop.pack with
| None -> ty_size memop.ty
| Some sz ->
check_pack sz (ty_size memop.ty) at;
packed_size sz
in
require (1 lsl memop.align <= size) at
"alignment must not be larger than natural"
(*
* Conventions:
* c : context
* e : instr
* es : instr list
* v : value
* t : value_type var
* ts : result_type
* x : variable
*
* Note: To deal with the non-determinism in some of the declarative rules,
* the function takes the current stack `s` as an additional argument, allowing
* it to "peek" when it would otherwise have to guess an input type.
*
* Furthermore, stack-polymorphic types are given with the `-->...` operator:
* a type `ts1 -->... ts2` expresses any type `(ts1' @ ts1) -> (ts2' @ ts2)`
* where `ts1'` and `ts2'` would be chosen non-deterministically in the
* declarative typing rules.
*)
let check_block_type (c : context) (bt : block_type) : func_type =
match bt with
| VarBlockType x -> type_ c x
| ValBlockType None -> FuncType ([], [])
| ValBlockType (Some t) -> FuncType ([], [t])
let rec check_instr (c : context) (e : instr) (s : infer_result_type) : op_type =
match e.it with
| Unreachable ->
[] -->... []
| Nop ->
[] --> []
| Drop ->
[peek 0 s] -~> []
| Select None ->
let t = peek 1 s in
require (match t with None -> true | Some t -> is_num_type t || is_vec_type t) e.at
("type mismatch: instruction requires numeric or vector type" ^
" but stack has " ^ string_of_infer_type t);
[t; t; Some (NumType I32Type)] -~> [t]
| Select (Some ts) ->
require (List.length ts = 1) e.at
"invalid result arity other than 1 is not (yet) allowed";
(ts @ ts @ [NumType I32Type]) --> ts
| Block (bt, es) ->
let FuncType (ts1, ts2) as ft = check_block_type c bt in
check_block {c with labels = ts2 :: c.labels} es ft e.at;
ts1 --> ts2
| Loop (bt, es) ->
let FuncType (ts1, ts2) as ft = check_block_type c bt in
check_block {c with labels = ts1 :: c.labels} es ft e.at;
ts1 --> ts2
| If (bt, es1, es2) ->
let FuncType (ts1, ts2) as ft = check_block_type c bt in
check_block {c with labels = ts2 :: c.labels} es1 ft e.at;
check_block {c with labels = ts2 :: c.labels} es2 ft e.at;
(ts1 @ [NumType I32Type]) --> ts2
| Br x ->
label c x -->... []
| BrIf x ->
(label c x @ [NumType I32Type]) --> label c x
| BrTable (xs, x) ->
let n = List.length (label c x) in
let ts = Lib.List.table n (fun i -> peek (n - i) s) in
check_stack ts (known (label c x)) x.at;
List.iter (fun x' -> check_stack ts (known (label c x')) x'.at) xs;
(ts @ [Some (NumType I32Type)]) -~>... []
| Return ->
c.results -->... []
| Call x ->
let FuncType (ts1, ts2) = func c x in
ts1 --> ts2
| CallIndirect (x, y) ->
let TableType (lim, t) = table c x in
let FuncType (ts1, ts2) = type_ c y in
require (t = FuncRefType) x.at
("type mismatch: instruction requires table of functions" ^
" but table has " ^ string_of_ref_type t);
(ts1 @ [NumType I32Type]) --> ts2
| LocalGet x ->
[] --> [local c x]
| LocalSet x ->
[local c x] --> []
| LocalTee x ->
[local c x] --> [local c x]
| GlobalGet x ->
let GlobalType (t, _mut) = global c x in
[] --> [t]
| GlobalSet x ->
let GlobalType (t, mut) = global c x in
require (mut = Mutable) x.at "global is immutable";
[t] --> []
| TableGet x ->
let TableType (_lim, t) = table c x in
[NumType I32Type] --> [RefType t]
| TableSet x ->
let TableType (_lim, t) = table c x in
[NumType I32Type; RefType t] --> []
| TableSize x ->
let _tt = table c x in
[] --> [NumType I32Type]
| TableGrow x ->
let TableType (_lim, t) = table c x in
[RefType t; NumType I32Type] --> [NumType I32Type]
| TableFill x ->
let TableType (_lim, t) = table c x in
[NumType I32Type; RefType t; NumType I32Type] --> []
| TableCopy (x, y) ->
let TableType (_lim1, t1) = table c x in
let TableType (_lim2, t2) = table c y in
require (t1 = t2) x.at
("type mismatch: source element type " ^ string_of_ref_type t1 ^
" does not match destination element type " ^ string_of_ref_type t2);
[NumType I32Type; NumType I32Type; NumType I32Type] --> []
| TableInit (x, y) ->
let TableType (_lim1, t1) = table c x in
let t2 = elem c y in
require (t1 = t2) x.at
("type mismatch: element segment's type " ^ string_of_ref_type t1 ^
" does not match table's element type " ^ string_of_ref_type t2);
[NumType I32Type; NumType I32Type; NumType I32Type] --> []
| ElemDrop x ->
ignore (elem c x);
[] --> []
| Load memop ->
check_memop c memop num_size (Lib.Option.map fst) e.at;
[NumType I32Type] --> [NumType memop.ty]
| Store memop ->
check_memop c memop num_size (fun sz -> sz) e.at;
[NumType I32Type; NumType memop.ty] --> []
| VecLoad memop ->
check_memop c memop vec_size (Lib.Option.map fst) e.at;
[NumType I32Type] --> [VecType memop.ty]
| VecStore memop ->
check_memop c memop vec_size (fun _ -> None) e.at;
[NumType I32Type; VecType memop.ty] --> []
| VecLoadLane (memop, i) ->
check_memop c memop vec_size (fun sz -> Some sz) e.at;
require (i < vec_size memop.ty / packed_size memop.pack) e.at
"invalid lane index";
[NumType I32Type; VecType memop.ty] --> [VecType memop.ty]
| VecStoreLane (memop, i) ->
check_memop c memop vec_size (fun sz -> Some sz) e.at;
require (i < vec_size memop.ty / packed_size memop.pack) e.at
"invalid lane index";
[NumType I32Type; VecType memop.ty] --> []
| MemorySize ->
let _mt = memory c (0l @@ e.at) in
[] --> [NumType I32Type]
| MemoryGrow ->
let _mt = memory c (0l @@ e.at) in
[NumType I32Type] --> [NumType I32Type]
| MemoryFill ->
ignore (memory c (0l @@ e.at));
[NumType I32Type; NumType I32Type; NumType I32Type] --> []
| MemoryCopy ->
ignore (memory c (0l @@ e.at));
[NumType I32Type; NumType I32Type; NumType I32Type] --> []
| MemoryInit x ->
ignore (memory c (0l @@ e.at));
ignore (data c x);
[NumType I32Type; NumType I32Type; NumType I32Type] --> []
| DataDrop x ->
ignore (data c x);
[] --> []
| RefNull t ->
[] --> [RefType t]
| RefIsNull ->
let t = peek 0 s in
require (match t with None -> true | Some t -> is_ref_type t) e.at
("type mismatch: instruction requires reference type" ^
" but stack has " ^ string_of_infer_type t);
[t] -~> [Some (NumType I32Type)]
| RefFunc x ->
let _ft = func c x in
refer_func c x;
[] --> [RefType FuncRefType]
| Const v ->
let t = NumType (type_num v.it) in
[] --> [t]
| Test testop ->
let t = NumType (type_num testop) in
[t] --> [NumType I32Type]
| Compare relop ->
let t = NumType (type_num relop) in
[t; t] --> [NumType I32Type]
| Unary unop ->
check_unop unop e.at;
let t = NumType (type_num unop) in
[t] --> [t]
| Binary binop ->
let t = NumType (type_num binop) in
[t; t] --> [t]
| Convert cvtop ->
let t1, t2 = type_cvtop e.at cvtop in
[NumType t1] --> [NumType t2]
| VecConst v ->
let t = VecType (type_vec v.it) in
[] --> [t]
| VecTest testop ->
let t = VecType (type_vec testop) in
[t] --> [NumType I32Type]
| VecUnary unop ->
let t = VecType (type_vec unop) in
[t] --> [t]
| VecBinary binop ->
check_vec_binop binop e.at;
let t = VecType (type_vec binop) in
[t; t] --> [t]
| VecCompare relop ->
let t = VecType (type_vec relop) in
[t; t] --> [t]
| VecConvert cvtop ->
let t = VecType (type_vec cvtop) in
[t] --> [t]
| VecShift shiftop ->
let t = VecType (type_vec shiftop) in
[t; NumType I32Type] --> [VecType V128Type]
| VecBitmask bitmaskop ->
let t = VecType (type_vec bitmaskop) in
[t] --> [NumType I32Type]
| VecTestBits vtestop ->
let t = VecType (type_vec vtestop) in
[t] --> [NumType I32Type]
| VecUnaryBits vunop ->
let t = VecType (type_vec vunop) in
[t] --> [t]
| VecBinaryBits vbinop ->
let t = VecType (type_vec vbinop) in
[t; t] --> [t]
| VecTernaryBits vternop ->
let t = VecType (type_vec vternop) in
[t; t; t] --> [t]
| VecSplat splatop ->
let t1 = type_vec_lane splatop in
let t2 = VecType (type_vec splatop) in
[NumType t1] --> [t2]
| VecExtract extractop ->
let t = VecType (type_vec extractop) in
let t2 = type_vec_lane extractop in
require (lane_extractop extractop < num_lanes extractop) e.at
"invalid lane index";
[t] --> [NumType t2]
| VecReplace replaceop ->
let t = VecType (type_vec replaceop) in
let t2 = type_vec_lane replaceop in
require (lane_replaceop replaceop < num_lanes replaceop) e.at
"invalid lane index";
[t; NumType t2] --> [t]
and check_seq (c : context) (s : infer_result_type) (es : instr list)
: infer_result_type =
match es with
| [] ->
s
| _ ->
let es', e = Lib.List.split_last es in
let s' = check_seq c s es' in
let {ins; outs} = check_instr c e s' in
push outs (pop ins s' e.at)
and check_block (c : context) (es : instr list) (ft : func_type) at =
let FuncType (ts1, ts2) = ft in
let s = check_seq c (stack ts1) es in
let s' = pop (stack ts2) s at in
require (snd s' = []) at
("type mismatch: block requires " ^ string_of_result_type ts2 ^
" but stack has " ^ string_of_infer_types (snd s))
(* Types *)
let check_limits {min; max} range at msg =
require (I32.le_u min range) at msg;
match max with
| None -> ()
| Some max ->
require (I32.le_u max range) at msg;
require (I32.le_u min max) at
"size minimum must not be greater than maximum"
let check_num_type (t : num_type) at =
()
let check_vec_type (t : vec_type) at =
()
let check_ref_type (t : ref_type) at =
()
let check_value_type (t : value_type) at =
match t with
| NumType t' -> check_num_type t' at
| VecType t' -> check_vec_type t' at
| RefType t' -> check_ref_type t' at
let check_func_type (ft : func_type) at =
let FuncType (ts1, ts2) = ft in
List.iter (fun t -> check_value_type t at) ts1;
List.iter (fun t -> check_value_type t at) ts2
let check_table_type (tt : table_type) at =
let TableType (lim, t) = tt in
check_limits lim 0xffff_ffffl at "table size must be at most 2^32-1";
check_ref_type t at
let check_memory_type (mt : memory_type) at =
let MemoryType lim = mt in
check_limits lim 0x1_0000l at
"memory size must be at most 65536 pages (4GiB)"
let check_global_type (gt : global_type) at =
let GlobalType (t, mut) = gt in
check_value_type t at
let check_type (t : type_) =
check_func_type t.it t.at
(* Functions & Constants *)
(*
* Conventions:
* c : context
* m : module_
* f : func
* e : instr
* v : value
* t : value_type
* s : func_type
* x : variable
*)
let check_func (c : context) (f : func) =
let {ftype; locals; body} = f.it in
let FuncType (ts1, ts2) = type_ c ftype in
let c' = {c with locals = ts1 @ locals; results = ts2; labels = [ts2]} in
check_block c' body (FuncType ([], ts2)) f.at
let is_const (c : context) (e : instr) =
match e.it with
| RefNull _
| RefFunc _
| Const _
| VecConst _ -> true
| GlobalGet x -> let GlobalType (_, mut) = global c x in mut = Immutable
| _ -> false
let check_const (c : context) (const : const) (t : value_type) =
require (List.for_all (is_const c) const.it) const.at
"constant expression required";
check_block c const.it (FuncType ([], [t])) const.at
(* Tables, Memories, & Globals *)
let check_table (c : context) (tab : table) =
let {ttype} = tab.it in
check_table_type ttype tab.at
let check_memory (c : context) (mem : memory) =
let {mtype} = mem.it in
check_memory_type mtype mem.at
let check_elem_mode (c : context) (t : ref_type) (mode : segment_mode) =
match mode.it with
| Passive -> ()
| Active {index; offset} ->
let TableType (_, et) = table c index in
require (t = et) mode.at
("type mismatch: element segment's type " ^ string_of_ref_type t ^
" does not match table's element type " ^ string_of_ref_type et);
check_const c offset (NumType I32Type)
| Declarative -> ()
let check_elem (c : context) (seg : elem_segment) =
let {etype; einit; emode} = seg.it in
List.iter (fun const -> check_const c const (RefType etype)) einit;
check_elem_mode c etype emode
let check_data_mode (c : context) (mode : segment_mode) =
match mode.it with
| Passive -> ()
| Active {index; offset} ->
ignore (memory c index);
check_const c offset (NumType I32Type)
| Declarative -> assert false
let check_data (c : context) (seg : data_segment) =
let {dinit; dmode} = seg.it in
check_data_mode c dmode
let check_global (c : context) (glob : global) =
let {gtype; ginit} = glob.it in
let GlobalType (t, mut) = gtype in
check_const c ginit t
(* Modules *)
let check_start (c : context) (start : start) =
let {sfunc} = start.it in
require (func c sfunc = FuncType ([], [])) start.at
"start function must not have parameters or results"
let check_import (im : import) (c : context) : context =
let {module_name = _; item_name = _; idesc} = im.it in
match idesc.it with
| FuncImport x ->
{c with funcs = type_ c x :: c.funcs}
| TableImport tt ->
check_table_type tt idesc.at;
{c with tables = tt :: c.tables}
| MemoryImport mt ->
check_memory_type mt idesc.at;
{c with memories = mt :: c.memories}
| GlobalImport gt ->
check_global_type gt idesc.at;
{c with globals = gt :: c.globals}
module NameSet = Set.Make(struct type t = Ast.name let compare = compare end)
let check_export (c : context) (set : NameSet.t) (ex : export) : NameSet.t =
let {name; edesc} = ex.it in
(match edesc.it with
| FuncExport x -> ignore (func c x)
| TableExport x -> ignore (table c x)
| MemoryExport x -> ignore (memory c x)
| GlobalExport x -> ignore (global c x)
);
require (not (NameSet.mem name set)) ex.at "duplicate export name";
NameSet.add name set
let check_module (m : module_) =
let
{ types; imports; tables; memories; globals; funcs; start; elems; datas;
exports } = m.it
in
let c0 =
List.fold_right check_import imports
{ empty_context with
refs = Free.module_ ({m.it with funcs = []; start = None} @@ m.at);
types = List.map (fun ty -> ty.it) types;
}
in
let c1 =
{ c0 with
funcs = c0.funcs @ List.map (fun f -> type_ c0 f.it.ftype) funcs;
tables = c0.tables @ List.map (fun tab -> tab.it.ttype) tables;
memories = c0.memories @ List.map (fun mem -> mem.it.mtype) memories;
elems = List.map (fun elem -> elem.it.etype) elems;
datas = List.map (fun _data -> ()) datas;
}
in
let c =
{ c1 with globals = c1.globals @ List.map (fun g -> g.it.gtype) globals }
in
List.iter check_type types;
List.iter (check_global c1) globals;
List.iter (check_table c1) tables;
List.iter (check_memory c1) memories;
List.iter (check_elem c1) elems;
List.iter (check_data c1) datas;
List.iter (check_func c) funcs;
Lib.Option.app (check_start c) start;
ignore (List.fold_left (check_export c) NameSet.empty exports);
require (List.length c.memories <= 1) m.at
"multiple memories are not allowed (yet)"