// Instruction mnemonics.
//
// A first slice: the instructions the corpus's simplest modules are made of.
// Anything not here reports rather than guessing a name -- the same discipline
// the lowering uses, so a gap is a gap and not a plausible wrong answer.
///|
pub suberror WatError {
/// An instruction with no spelling here yet, named so the gap is legible.
NotPrinted(String)
}
///|
pub impl Show for WatError with fn output(self, logger) {
let NotPrinted(what) = self
logger.write_string(what + " has no text form yet")
}
///|
/// The constructor name of an instruction, for the gap report: the debug
/// rendering up to its first immediate.
fn constructor_of(i : @wasm_bin.Instruction) -> String {
let s = @debug.to_string(i)
match s.find("(") {
Some(k) => s[:k].to_owned()
None => s
}
}
///|
/// `br_on_cast $l (ref null $a) (ref $b)`: the type it came from, then the type
/// it is going to.
fn cast_branch(
name : String,
l : Int,
a : @wasm_bin.RefType,
b : @wasm_bin.RefType,
names : @wasm_bin.Names,
scope : Array[String?],
) -> String {
name +
" " +
label(scope, l) +
" " +
reftype(a, names.types) +
" " +
reftype(b, names.types)
}
///|
/// A resume's handler table: `(on $tag $label)` for each tag it handles, or
/// `(on $tag switch)` for the one it switches to.
fn on_clauses(
on : Array[@wasm_bin.OnClause],
names : @wasm_bin.Names,
scope : Array[String?],
) -> String {
let out = StringBuilder::new()
for c in on {
match c {
OnLabel(tag, l) =>
out.write_string(
" (on " + name_or(names.tags, tag) + " " + label(scope, l) + ")",
)
OnSwitch(tag) =>
out.write_string(" (on " + name_or(names.tags, tag) + " switch)")
}
}
out.to_string()
}
///|
/// The keyword a spelled constant belongs to.
fn const_keyword(i : @wasm_bin.Instruction) -> String raise WatError {
match i {
I32Const(_) => "i32.const"
I64Const(_) => "i64.const"
F32Const(_) => "f32.const"
F64Const(_) => "f64.const"
V128Const(_) => "v128.const"
_ => raise NotPrinted("a spelled " + constructor_of(i))
}
}
///|
/// A struct field's name, under the type that declared it.
fn field_name(names : @wasm_bin.Names, t : Int, f : Int) -> String {
match names.fields.get(t) {
Some(fs) => name_or(fs, f)
None => f.to_string()
}
}
///|
/// The two indices a copy takes, written only when they are not both the
/// unnamed default -- there is no way to write the second without the first.
fn index_pair(names : Map[Int, Bytes], a : Int, b : Int) -> String {
if a == 0 && b == 0 && !names.contains(0) {
""
} else {
" " + name_or(names, a) + " " + name_or(names, b)
}
}
///|
/// The label a branch depth names: the enclosing blocks are counted outward
/// from the innermost, so a depth is a distance from the END of the scope.
fn label(scope : Array[String?], depth : Int) -> String {
let k = scope.length() - 1 - depth
if k >= 0 && k < scope.length() && scope[k] is Some(name) {
name
} else {
depth.to_string()
}
}
///|
/// The mnemonic and immediates of one instruction, without its operands.
pub fn mnemonic(
i : @wasm_bin.Instruction,
names : @wasm_bin.Names,
locals : Map[Int, Bytes],
scope? : Array[String?] = [],
) -> Sexp raise WatError {
// The immediates are separate atoms so that a long one can WRAP: sixteen
// v128 lanes do not fit a line, and one atom cannot break.
let parts = top_level_words(mnemonic_text(i, names, locals, scope~))
if parts.length() > 1 {
return Block(parts.map(w => Atom(w)), KBox, false)
}
Atom(parts[0])
}
///|
/// Split on the spaces that separate IMMEDIATES, and not on the ones inside
/// one: `(ref null $t)` is a single thing to write and has nowhere to break.
fn top_level_words(text : String) -> Array[String] {
let out : Array[String] = []
let cur = StringBuilder::new()
let mut depth = 0
for c in text {
if c == '(' {
depth = depth + 1
} else if c == ')' {
depth = depth - 1
}
if c == ' ' && depth == 0 {
out.push(cur.to_string())
cur.reset()
continue
}
cur.write_char(c)
}
out.push(cur.to_string())
out
}
///|
/// The same, as one string. The immediates are not separate atoms yet, so an
/// instruction breaks as a unit -- which is where the layout still differs from
/// the reference for the few whose immediate lists are long.
fn mnemonic_text(
i : @wasm_bin.Instruction,
names : @wasm_bin.Names,
locals : Map[Int, Bytes],
scope? : Array[String?] = [],
) -> String raise WatError {
// Everything with no immediate follows one rule and lives in its own table;
// what is left here is the instructions whose immediates have to be written.
// The spelling the source used stands in for the value's own rendering: the
// literal is what was written, not a reading of what it denotes.
if i is Spelled(text, inner) {
return const_keyword(inner) + " " + text
}
if nullary(i) is Some(name) {
return name
}
if memarg_instr(i, names) is Some(name) {
return name
}
match i {
I32Const(v) => "i32.const " + v.to_string()
I64Const(v) => "i64.const " + v.to_string()
// A local is named where the function named it, which is per FUNCTION --
// unlike every other index space, whose names are the module's.
LocalGet(k) => "local.get " + name_or(locals, k)
LocalSet(k) => "local.set " + name_or(locals, k)
LocalTee(k) => "local.tee " + name_or(locals, k)
GlobalGet(k) => "global.get " + name_or(names.globals, k)
GlobalSet(k) => "global.set " + name_or(names.globals, k)
Call(k) => "call " + name_or(names.functions, k)
ReturnCall(k) => "return_call " + name_or(names.functions, k)
RefFunc(k) => "ref.func " + name_or(names.functions, k)
Br(d) => "br " + label(scope, d)
BrIf(d) => "br_if " + label(scope, d)
// The table is left out when it is the unnamed default, the same rule the
// memory accesses follow; the signature is always named, because an
// indirect call is checked against it at run time.
CallIndirect(t, tab) =>
"call_indirect" +
opt_index(names.tables, tab) +
" (type " +
name_or(names.types, t) +
")"
ReturnCallIndirect(t, tab) =>
"return_call_indirect" +
opt_index(names.tables, tab) +
" (type " +
name_or(names.types, t) +
")"
CallRef(t) => "call_ref " + name_or(names.types, t)
ReturnCallRef(t) => "return_call_ref " + name_or(names.types, t)
RefNull(h) => "ref.null " + heaptype(h, names.types)
F32Const(v) => "f32.const " + v.to_double().to_string()
F64Const(v) => "f64.const " + v.to_string()
StructNew(t) => "struct.new " + name_or(names.types, t)
StructNewDefault(t) => "struct.new_default " + name_or(names.types, t)
ArrayNew(t) => "array.new " + name_or(names.types, t)
ArrayNewDefault(t) => "array.new_default " + name_or(names.types, t)
ArrayNewFixed(t, n) =>
"array.new_fixed " + name_or(names.types, t) + " " + n.to_string()
RefTest(r) => "ref.test " + reftype(r, names.types)
RefCast(r) => "ref.cast " + reftype(r, names.types)
Throw(t) => "throw " + name_or(names.tags, t)
// Tables and memories: the index is left out when it is the unnamed
// default, which is what makes `(i32.load (local.get $i))` the usual
// spelling rather than `(i32.load 0 ...)`.
TableGet(t) => "table.get" + opt_index(names.tables, t)
TableSet(t) => "table.set" + opt_index(names.tables, t)
TableSize(t) => "table.size" + opt_index(names.tables, t)
TableGrow(t) => "table.grow" + opt_index(names.tables, t)
TableFill(t) => "table.fill" + opt_index(names.tables, t)
// Two indices, and the pair is left out only when BOTH are the default --
// `table.copy 0 2` needs the source, and there is no way to write the
// second without the first.
TableCopy(d, s) => "table.copy" + index_pair(names.tables, d, s)
TableInit(t, e) =>
"table.init" + opt_index(names.tables, t) + " " + name_or(names.elem, e)
ElemDrop(e) => "elem.drop " + name_or(names.elem, e)
MemorySize(m) => "memory.size" + memidx(names, m)
MemoryGrow(m) => "memory.grow" + memidx(names, m)
MemoryFill(m) => "memory.fill" + memidx(names, m)
MemoryCopy(d, s) => "memory.copy" + index_pair(names.memories, d, s)
MemoryInit(m, d) =>
"memory.init" + memidx(names, m) + " " + name_or(names.data, d)
DataDrop(d) => "data.drop " + name_or(names.data, d)
// A struct field is named where its TYPE named it, so the field name is
// looked up under the type the access goes through -- not under the
// instruction, which has no namespace of its own.
StructGet(t, f) =>
"struct.get " + name_or(names.types, t) + " " + field_name(names, t, f)
StructGetS(t, f) =>
"struct.get_s " + name_or(names.types, t) + " " + field_name(names, t, f)
StructGetU(t, f) =>
"struct.get_u " + name_or(names.types, t) + " " + field_name(names, t, f)
StructSet(t, f) =>
"struct.set " + name_or(names.types, t) + " " + field_name(names, t, f)
StructNewDesc(t) => "struct.new_desc " + name_or(names.types, t)
StructNewDefaultDesc(t) =>
"struct.new_default_desc " + name_or(names.types, t)
RefGetDesc(t) => "ref.get_desc " + name_or(names.types, t)
ArrayGet(t) => "array.get " + name_or(names.types, t)
ArrayGetS(t) => "array.get_s " + name_or(names.types, t)
ArrayGetU(t) => "array.get_u " + name_or(names.types, t)
ArraySet(t) => "array.set " + name_or(names.types, t)
ArrayFill(t) => "array.fill " + name_or(names.types, t)
ArrayCopy(d, s) =>
"array.copy " + name_or(names.types, d) + " " + name_or(names.types, s)
ArrayNewData(t, d) =>
"array.new_data " + name_or(names.types, t) + " " + name_or(names.data, d)
ArrayNewElem(t, e) =>
"array.new_elem " + name_or(names.types, t) + " " + name_or(names.elem, e)
ArrayInitData(t, d) =>
"array.init_data " +
name_or(names.types, t) +
" " +
name_or(names.data, d)
ArrayInitElem(t, e) =>
"array.init_elem " +
name_or(names.types, t) +
" " +
name_or(names.elem, e)
// A branch table names every arm and then the default, which is written
// last and no differently -- the position is what makes it the default.
BrTable(ls, d) => {
let out = StringBuilder::new()
out.write_string("br_table")
for l in ls {
out.write_string(" " + label(scope, l))
}
out.write_string(" " + label(scope, d))
out.to_string()
}
BrOnNull(l) => "br_on_null " + label(scope, l)
BrOnNonNull(l) => "br_on_non_null " + label(scope, l)
BrOnCast(l, a, b) => cast_branch("br_on_cast", l, a, b, names, scope)
BrOnCastFail(l, a, b) =>
cast_branch("br_on_cast_fail", l, a, b, names, scope)
BrOnCastDescEq(l, a, b) =>
cast_branch("br_on_cast_desc_eq", l, a, b, names, scope)
BrOnCastDescEqFail(l, a, b) =>
cast_branch("br_on_cast_desc_eq_fail", l, a, b, names, scope)
RefCastDescEq(r) => "ref.cast_desc_eq " + reftype(r, names.types)
// Stack switching. A continuation names the type it runs at, and a resume
// carries the handler table as immediates rather than as nested code.
ContNew(t) => "cont.new " + name_or(names.types, t)
ContBind(a, b) =>
"cont.bind " + name_or(names.types, a) + " " + name_or(names.types, b)
Suspend(t) => "suspend " + name_or(names.tags, t)
Resume(t, on) =>
"resume " + name_or(names.types, t) + on_clauses(on, names, scope)
ResumeThrow(t, tag, on) =>
"resume_throw " +
name_or(names.types, t) +
" " +
name_or(names.tags, tag) +
on_clauses(on, names, scope)
ResumeThrowRef(t, on) =>
"resume_throw_ref " +
name_or(names.types, t) +
on_clauses(on, names, scope)
Switch(t, tag) =>
"switch " + name_or(names.types, t) + " " + name_or(names.tags, tag)
// `select` with an explicit result type. Written with the type when the
// operands are references, because then it cannot be inferred.
SelectTyped(ts) => {
let out = StringBuilder::new()
out.write_string("select (result")
for t in ts {
out.write_string(" " + valtype(t, names.types))
}
out.write_string(")")
out.to_string()
}
// The lane accessors: the shape says which lane width, the immediate says
// which lane.
I8x16ExtractLaneS(l) => "i8x16.extract_lane_s " + l.to_string()
I8x16ExtractLaneU(l) => "i8x16.extract_lane_u " + l.to_string()
I16x8ExtractLaneS(l) => "i16x8.extract_lane_s " + l.to_string()
I16x8ExtractLaneU(l) => "i16x8.extract_lane_u " + l.to_string()
I32x4ExtractLane(l) => "i32x4.extract_lane " + l.to_string()
I64x2ExtractLane(l) => "i64x2.extract_lane " + l.to_string()
F32x4ExtractLane(l) => "f32x4.extract_lane " + l.to_string()
F64x2ExtractLane(l) => "f64x2.extract_lane " + l.to_string()
I8x16ReplaceLane(l) => "i8x16.replace_lane " + l.to_string()
I16x8ReplaceLane(l) => "i16x8.replace_lane " + l.to_string()
I32x4ReplaceLane(l) => "i32x4.replace_lane " + l.to_string()
I64x2ReplaceLane(l) => "i64x2.replace_lane " + l.to_string()
F32x4ReplaceLane(l) => "f32x4.replace_lane " + l.to_string()
F64x2ReplaceLane(l) => "f64x2.replace_lane " + l.to_string()
I8x16Shuffle(lanes) => {
let out = StringBuilder::new()
out.write_string("i8x16.shuffle")
for k in 0..<16 {
out.write_string(" " + lanes[k].to_string())
}
out.to_string()
}
// An atomic access. The opcode is the operation, and the registry that
// assigned it is also the one that knows its mnemonic and the alignment it
// naturally has -- an atomic's alignment is not a hint, so writing the
// wrong default would change what the instruction means.
Atomic(code, mem, align, offset) => {
guard @atomics.of_opcode(code) is Some(op) else {
raise NotPrinted("an atomic with opcode " + code.to_string())
}
memarg(
@atomics.name(op),
mem,
align,
offset,
@atomics.natural_align_log2(op),
names,
)
}
// Everything else is a gap, not a guess: reporting it keeps the burn-down
// honest, exactly as the lowering's own gaps do -- and NAMING it is what
// makes the remainder countable rather than one undifferentiated pile.
_ => raise NotPrinted(constructor_of(i))
}
}