// The module fields, as the text format writes them.
//
// Ported from `modulefield` in wax/src/lib-wasm/output.ml.
//
// Exports are written INLINE on the thing exported -- `(func $f (export "f")
// ...)` -- not as fields of their own. The binary has an export section
// instead, so the inline form is recovered by asking which exports point at
// this item: the two spellings hold the same information.
//
// Except a GUARDED one. `#[export = "n", if(c)]` is a field of its own under
// its condition, because a clause on the thing exported has nowhere to put a
// condition.
///|
/// The export clauses attached to one item, in section order.
fn export_clauses(
m : @wasm_bin.Module,
of_ : @wasm_bin.ExportDesc,
) -> Array[Sexp] {
let out : Array[Sexp] = []
for k, e in m.exports {
if e.desc == of_ && !m.text.standalone_exports.contains(k) {
out.push(List([Atom("export"), Atom(quoted(e.name))]))
}
}
out
}
///|
/// A guarded export, as a field: `(export "n" (func $f))`.
fn export_field(m : @wasm_bin.Module, k : Int) -> Sexp {
let e = m.exports[k]
let (kind, i) = match e.desc {
Func(i) => ("func", i)
Table(i) => ("table", i)
Memory(i) => ("memory", i)
Global(i) => ("global", i)
Tag(i) => ("tag", i)
}
let names = match e.desc {
Func(_) => m.names.functions
Table(_) => m.names.tables
Memory(_) => m.names.memories
Global(_) => m.names.globals
Tag(_) => m.names.tags
}
List([
Atom("export"),
Atom(quoted(e.name)),
List([Atom(kind), Atom(name_or(names, i))]),
])
}
///|
/// A string literal, escaped the way the format escapes one.
///
/// The choice is ALL OR NOTHING. Text that decodes as UTF-8 and holds no
/// character a readable rendering would have to hex-escape is written
/// readably; anything else is written byte by byte as `\HH`. Mixing the two
/// -- printable characters as themselves, the rest escaped -- reads as text
/// that is not text, and is not what the format does.
fn quoted(b : Bytes) -> String {
let out = StringBuilder::new()
out.write_char('"')
if readable(b) {
let text = text_of(b)
for c in text {
let n = c.to_int()
if n >= 32 && n != 127 && n != 34 && n != 92 {
out.write_char(c)
} else {
match n {
0x09 => out.write_string("\\t")
0x0A => out.write_string("\\n")
0x0D => out.write_string("\\r")
0x22 => out.write_string("\\\"")
0x5C => out.write_string("\\\\")
_ => out.write_string("\\" + hex2(n))
}
}
}
} else {
// Binary: every byte, rather than decoded text interleaved with escapes.
for k in 0.. Bool {
let mut k = 0
while k < b.length() {
let c = b[k].to_int()
let n = if c < 0x80 {
// The controls that have no short escape force the byte-by-byte form.
if (c < 0x20 && c != 9 && c != 10 && c != 13) || c == 127 {
return false
}
1
} else if c >= 0xC2 && c <= 0xDF {
2
} else if c >= 0xE0 && c <= 0xEF {
3
} else if c >= 0xF0 && c <= 0xF4 {
4
} else {
return false
}
if k + n > b.length() {
return false
}
for j in 1.. 0xBF {
return false
}
}
k = k + n
}
true
}
///|
/// Two lowercase hex digits.
fn hex2(c : Int) -> String {
let digits = [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f",
]
digits[c / 16] + digits[c % 16]
}
///|
/// A field's identifier: what the source called it, or nothing.
///
/// Unlike an operand, where an index is a valid spelling, a FIELD written
/// `(elem 3 ...)` would name the index in the wrong space -- so an unnamed
/// field carries no identifier at all and takes its index from its position.
fn opt_id(names : Map[Int, Bytes], k : Int) -> Array[Sexp] {
match names.get(k) {
Some(b) => [Atom(ident(b))]
None => []
}
}
///|
/// A defined type: `(type $n (struct ...))`, or the `sub` form when it is not
/// final or has a supertype.
fn subtype_field(m : @wasm_bin.Module, k : Int) -> Sexp {
let t = m.types[k]
let head = block([Atom("type"), ..opt_id(m.names.types, k)])
let body = comptype(m, k, t.composite)
let clauses = describes_clauses(m, t)
if t.final_ && t.supertypes.is_empty() {
let l : Array[Sexp] = [head]
for c in clauses {
l.push(c)
}
l.push(block([body]))
List(l)
} else {
let sub : Array[Sexp] = [Atom("sub")]
if t.final_ {
sub.push(Atom("final"))
}
for s in t.supertypes {
sub.push(Atom(name_or(m.names.types, s)))
}
let inner : Array[Sexp] = [block(sub)]
for c in clauses {
inner.push(c)
}
inner.push(body)
List([head, List([block(inner)])])
}
}
///|
/// The custom-descriptors clauses, which precede the composite type.
fn describes_clauses(
m : @wasm_bin.Module,
t : @wasm_bin.SubType,
) -> Array[Sexp] {
let out : Array[Sexp] = []
if t.describes is Some(i) {
out.push(List([Atom("describes"), Atom(name_or(m.names.types, i))]))
}
if t.descriptor is Some(i) {
out.push(List([Atom("descriptor"), Atom(name_or(m.names.types, i))]))
}
out
}
///|
/// What a defined type defines. `of_` is the type's own index, which is where
/// the field names live.
fn comptype(
m : @wasm_bin.Module,
of_ : Int,
c : @wasm_bin.CompositeType,
) -> Sexp {
match c {
Func(ft) => {
let l : Array[Sexp] = [Atom("func")]
let names = m.text.decl_param_names.get(OwnerType(of_)).unwrap_or(Map([]))
for x in functype(m, ft, names~) {
l.push(x)
}
List(l)
}
Array(at) => List([Atom("array"), Atom(fieldtype(m, at.element))])
Cont(i) => List([Atom("cont"), Atom(name_or(m.names.types, i))])
Struct(st) => {
let l : Array[Sexp] = [Atom("struct")]
let field_names = m.names.fields.get(of_).unwrap_or(Map([]))
for j, f in st.fields {
let name = match field_names.get(j) {
Some(n) => ident(n)
None => ""
}
l.push(List(named_type(name, "field", fieldtype(m, f))))
}
List(l)
}
}
}
///|
/// A function type's parameters and results, each group written once.
fn functype(
m : @wasm_bin.Module,
ft : @wasm_bin.FuncType,
names? : Map[Int, Bytes] = Map([]),
) -> Array[Sexp] {
let out : Array[Sexp] = []
// Named parameters take a group each; unnamed ones share one. Same rule a
// defined function follows, and for the same reason: the shared form has
// nowhere to put a name.
if !names.is_empty() {
for k, t in ft.params {
let n = match names.get(k) {
Some(b) => ident(b)
None => ""
}
out.push(List(named_type(n, "param", valtype(t, m.names.types))))
}
} else if !ft.params.is_empty() {
let l : Array[Sexp] = [Atom("param")]
for t in ft.params {
l.push(Atom(valtype(t, m.names.types)))
}
out.push(List(l))
}
if !ft.results.is_empty() {
let l : Array[Sexp] = [Atom("result")]
for t in ft.results {
l.push(Atom(valtype(t, m.names.types)))
}
out.push(List(l))
}
out
}
///|
fn fieldtype(m : @wasm_bin.Module, f : @wasm_bin.FieldType) -> String {
let t = match f.typ {
Value(v) => valtype(v, m.names.types)
Packed(I8) => "i8"
Packed(I16) => "i16"
}
if f.mut_ {
"(mut " + t + ")"
} else {
t
}
}
///|
fn globaltype(m : @wasm_bin.Module, g : @wasm_bin.GlobalType) -> String {
let t = valtype(g.typ, m.names.types)
if g.mut_ {
"(mut " + t + ")"
} else {
t
}
}
///|
/// A memory's or table's size bounds, and what else the limits carry.
fn limits(l : @wasm_types.Limits) -> Array[Sexp] {
let out : Array[Sexp] = []
if l.address_type is I64 {
out.push(Atom("i64"))
}
out.push(Atom(l.mi.to_string()))
if l.ma is Some(x) {
out.push(Atom(x.to_string()))
}
if l.shared {
out.push(Atom("shared"))
}
if l.page_size_log2 is Some(p) {
out.push(List([Atom("pagesize"), Atom((1UL << p).to_string())]))
}
out
}
///|
fn tabletype(m : @wasm_bin.Module, t : @wasm_bin.TableType) -> Array[Sexp] {
let out = limits(t.limits)
out.push(Atom(reftype(t.elem_type, m.names.types)))
out
}
///|
/// The type of an imported function or tag, written inline.
///
/// The signature is a group of its own, the same way a defined function's is:
/// it moves to the next line whole rather than splitting across the name.
fn typeuse(
m : @wasm_bin.Module,
k : Int,
owner? : @wasm_bin.ParamOwner? = None,
) -> Array[Sexp] {
// The two clauses are independent, and a declaration may write either or
// both: `fn f: ft(i32)` names a type AND spells the signature out.
let wrote = match owner {
Some(o) => m.text.decl_typeuse.get(o)
None => None
}
let out : Array[Sexp] = []
if wrote is Some({ named: true, .. }) {
out.push(List([Atom("type"), Atom(name_or(m.names.types, k))]))
if wrote is Some({ spelled: false, .. }) {
return out
}
}
// Whether this is written `(type $n)` or spelled out depends on which the
// SOURCE wrote, and the binary does not record that. Preferring `(type $n)`
// whenever the type has a name was measured and is worse -- 916 exact to
// 911 -- so the inline form stays until the choice is recorded.
match m.types[k].composite {
Func(ft) => {
let names = match owner {
Some(o) => m.text.decl_param_names.get(o).unwrap_or(Map([]))
None => Map([])
}
let sign = functype(m, ft, names~)
if !sign.is_empty() {
out.push(block(sign))
}
out
}
// Anything else at a function's type index is not a function type, and
// there is no inline spelling for it -- so name it.
_ => [List([Atom("type"), Atom(name_or(m.names.types, k))])]
}
}
///|
/// An import. With no exports it is written the way the binary reads --
/// `(import "m" "n" (func $f ...))`; with exports the inline form is the only
/// one that has room for them, so the two clauses swap places.
///
/// A COMPACT group is one entry standing for a run of imports from the same
/// module, and it is written as one field with an `item` per name.
fn import_field(m : @wasm_bin.Module, k : Int) -> Sexp {
let i = m.imports[k]
let at = import_slots(m, k)
match i.group {
Some(g) => compact_import(m, i, g, at)
None => {
let (kind, id, typ, desc) = import_parts(m, i.desc, at)
let e = export_clauses(m, desc)
let names : Array[Sexp] = [
Atom("import"),
Atom(quoted(i.mod_name)),
Atom(quoted(i.name)),
]
if e.is_empty() {
let inner : Array[Sexp] = [Atom(kind)]
for x in id {
inner.push(x)
}
for x in typ {
inner.push(x)
}
List([block(names), List([block(inner)])])
} else {
let head : Array[Sexp] = [Atom(kind)]
for x in id {
head.push(x)
}
for x in e {
head.push(x)
}
head.push(List(names))
let l : Array[Sexp] = [block(head)]
for x in typ {
l.push(x)
}
List(l)
}
}
}
}
///|
/// `(import "m" (item "n" (func ...)) ...)`, or -- when every item shares one
/// descriptor -- `(import "m" (item $id "n") ... (func ...))`.
fn compact_import(
m : @wasm_bin.Module,
i : @wasm_bin.Import,
g : @wasm_bin.ImportGroup,
at : Map[String, Int],
) -> Sexp {
let out : Array[Sexp] = [block([Atom("import"), Atom(quoted(i.mod_name))])]
match g {
Heterogeneous(items) =>
for it in items {
let (kind, id, typ, _) = import_parts(m, it.1, at)
let inner : Array[Sexp] = [Atom(kind)]
for x in id {
inner.push(x)
}
for x in typ {
inner.push(x)
}
out.push(List([Atom("item"), Atom(quoted(it.0)), List([block(inner)])]))
bump(at, kind)
}
Homogeneous(names) => {
let (kind, _, typ, _) = import_parts(m, i.desc, at)
for n in names {
let (_, id, _, _) = import_parts(m, i.desc, at)
let item : Array[Sexp] = [Atom("item")]
for x in id {
item.push(x)
}
item.push(Atom(quoted(n)))
out.push(List(item))
bump(at, kind)
}
let tail : Array[Sexp] = [Atom(kind)]
for x in typ {
tail.push(x)
}
out.push(List(tail))
}
}
List(out)
}
///|
/// Which index space an import takes a slot in.
fn kind_of(d : @wasm_bin.ImportDesc) -> String {
match d {
Func(_, _) => "func"
Table(_) => "table"
Memory(_) => "memory"
Global(_) => "global"
Tag(_) => "tag"
}
}
///|
fn bump(at : Map[String, Int], kind : String) -> Unit {
at[kind] = at.get(kind).unwrap_or(0) + 1
}
///|
/// The index each space stands at when the import at `k` is reached.
///
/// A compact group holds several imports in one entry, so the count advances
/// by what the entry stands for rather than by one.
fn import_slots(m : @wasm_bin.Module, k : Int) -> Map[String, Int] {
let at : Map[String, Int] = Map([])
for j in 0.. bump(at, kind_of(m.imports[j].desc))
Some(Heterogeneous(items)) =>
for it in items {
bump(at, kind_of(it.1))
}
Some(Homogeneous(names)) =>
for _ in names {
bump(at, kind_of(m.imports[j].desc))
}
}
}
at
}
///|
/// What an import brings in: its keyword, its name, its type, and where in the
/// export section it would be pointed at.
fn import_parts(
m : @wasm_bin.Module,
d : @wasm_bin.ImportDesc,
at : Map[String, Int],
) -> (String, Array[Sexp], Array[Sexp], @wasm_bin.ExportDesc) {
let k = kind_of(d)
let n = at.get(k).unwrap_or(0)
match d {
Func(t, exact) => {
let inline = typeuse(m, t, owner=Some(OwnerFunc(n)))
let typ : Array[Sexp] = if exact {
let l : Array[Sexp] = [Atom("exact")]
for x in inline {
l.push(x)
}
[List(l)]
} else {
inline
}
("func", opt_id(m.names.functions, n), typ, Func(n))
}
Global(g) =>
(
"global",
opt_id(m.names.globals, n),
[Atom(globaltype(m, g))],
Global(n),
)
Tag(t) =>
(
"tag",
opt_id(m.names.tags, n),
typeuse(m, t, owner=Some(OwnerTag(n))),
Tag(n),
)
Memory(mt) =>
("memory", opt_id(m.names.memories, n), limits(mt.limits), Memory(n))
Table(tt) =>
("table", opt_id(m.names.tables, n), tabletype(m, tt), Table(n))
}
}
///|
/// A global: its type, then the constant expression that initialises it.
fn global_field(m : @wasm_bin.Module, k : Int) -> Sexp raise WatError {
let g = m.globals[k]
let at = k + imported_count(m, "global")
let head : Array[Sexp] = [Atom("global")]
for x in opt_id(m.names.globals, at) {
head.push(x)
}
for e in export_clauses(m, Global(at)) {
head.push(e)
}
head.push(Atom(globaltype(m, g.type_)))
let l : Array[Sexp] = [block(head)]
for x in const_expr(m, g.init, spans=g.init_spans) {
l.push(x)
}
List(l)
}
///|
fn tag_field(m : @wasm_bin.Module, k : Int) -> Sexp {
let at = k + imported_count(m, "tag")
let head : Array[Sexp] = [Atom("tag")]
for x in opt_id(m.names.tags, at) {
head.push(x)
}
for e in export_clauses(m, Tag(at)) {
head.push(e)
}
for x in typeuse(m, m.tags[k].type_idx, owner=Some(OwnerTag(at))) {
head.push(x)
}
List([block(head)])
}
///|
fn memory_field(m : @wasm_bin.Module, k : Int) -> Sexp {
let at = k + imported_count(m, "memory")
let head : Array[Sexp] = [Atom("memory")]
for x in opt_id(m.names.memories, at) {
head.push(x)
}
for e in export_clauses(m, Memory(at)) {
head.push(e)
}
for x in limits(m.memories[k].limits) {
head.push(x)
}
List([block(head)])
}
///|
fn table_field(m : @wasm_bin.Module, k : Int) -> Sexp raise WatError {
let t = m.tables[k]
let at = k + imported_count(m, "table")
let head : Array[Sexp] = [Atom("table")]
for x in opt_id(m.names.tables, at) {
head.push(x)
}
for e in export_clauses(m, Table(at)) {
head.push(e)
}
for x in tabletype(m, t.type_) {
head.push(x)
}
let l : Array[Sexp] = [block(head)]
if t.init is Some(e) {
for x in const_expr(m, e, spans=t.init_spans) {
l.push(x)
}
}
List(l)
}
///|
fn elem_field(m : @wasm_bin.Module, k : Int) -> Sexp raise WatError {
let e = m.elems[k]
let head : Array[Sexp] = [Atom("elem")]
for x in opt_id(m.names.elem, k) {
head.push(x)
}
match e.mode {
Passive => ()
Declarative => head.push(Atom("declare"))
Active(tab, offset) => {
if m.names.tables.get(tab) is Some(b) {
head.push(List([Atom("table"), Atom(ident(b))]))
} else if tab != 0 {
head.push(List([Atom("table"), Atom(tab.to_string())]))
}
head.push(expr("offset", const_expr(m, offset, spans=e.offset_spans)))
}
}
// The `func` SHORTHAND: a segment of plain `ref.func`s at the non-nullable
// `(ref func)` type writes its function names bare. It may only be used at
// that type -- abbreviating a `funcref` segment would silently drop the
// nullability, which is a different segment.
let plain = if e.type_.nullable || !(e.type_.typ is Func) {
None
} else {
func_indices(e.init)
}
let items : Array[Sexp] = []
match plain {
Some(fs) => {
head.push(Atom("func"))
for f in fs {
items.push(Atom(name_or(m.names.functions, f)))
}
}
None => {
head.push(Atom(reftype(e.type_, m.names.types)))
for j, item in e.init {
let sp = if j < e.init_spans.length() { e.init_spans[j] } else { [] }
items.push(expr("item", const_expr(m, item, spans=sp)))
}
}
}
List([block(head), block(items)])
}
///|
/// The functions a segment names, when every element is a bare `ref.func` and
/// nothing else.
fn func_indices(init : Array[Array[@wasm_bin.Instruction]]) -> Array[Int]? {
let out : Array[Int] = []
for e in init {
guard e.length() == 1 && e[0] is RefFunc(f) else { return None }
out.push(f)
}
Some(out)
}
///|
fn data_field(m : @wasm_bin.Module, k : Int) -> Sexp raise WatError {
let d = m.datas[k]
let head : Array[Sexp] = [Atom("data")]
for x in opt_id(m.names.data, k) {
head.push(x)
}
if d.mode is Active(mem, offset) {
if m.names.memories.get(mem) is Some(b) {
head.push(List([Atom("memory"), Atom(ident(b))]))
} else if mem != 0 {
head.push(List([Atom("memory"), Atom(mem.to_string())]))
}
head.push(expr("offset", const_expr(m, offset, spans=d.offset_spans)))
}
// The pieces the source wrote, when they were recorded; otherwise the bytes
// as one string, which is always a correct spelling of them.
let body : Array[Sexp] = [block(head)]
if d.spelling.is_empty() {
body.push(Atom(quoted(d.init)))
} else {
for piece in d.spelling {
body.push(data_piece(piece))
}
}
List(body)
}
///|
fn data_piece(p : @wasm_bin.DataPiece) -> Sexp {
match p {
PieceStr(b) => Atom(quoted(b))
PieceRun(kw, items) => {
let l : Array[Sexp] = [Atom(kw)]
for i in items {
l.push(Atom(i))
}
List(l)
}
}
}
///|
/// `(offset ...)` and the like -- except that a single FOLDED instruction
/// already reads as one expression, so the keyword is left out and the
/// instruction stands for itself. That is the abbreviation the format allows
/// and the one the reference takes.
fn expr(name : String, body : Array[Sexp]) -> Sexp {
if body.length() == 1 {
return body[0]
}
let l : Array[Sexp] = [Atom(name)]
for x in body {
l.push(x)
}
List(l)
}
///|
/// How many items of a kind the imports took before the defined ones start.
fn imported_count(m : @wasm_bin.Module, of_ : String) -> Int {
import_slots(m, m.imports.length()).get(of_).unwrap_or(0)
}
///|
/// A constant expression, folded like any other body, from the spans the
/// lowering kept for it. Without them a multi-instruction initialiser folds by
/// the fallback, which reads `(i32.add (i32.const 0) (i32.const 42))` back as
/// `(i32.add (i32.const 42 (i32.const 0)))`.
fn const_expr(
m : @wasm_bin.Module,
body : Array[@wasm_bin.Instruction],
spans? : Array[@wasm_bin.Span] = [],
) -> Array[Sexp] raise WatError {
let ctx = {
m,
locals: Map([]),
labels: Map([]),
opened: 0,
scope: [],
conditionals: [],
}
let out : Array[Sexp] = []
for node in fold(body, spans) {
out.push(folded(node, ctx))
}
out
}