From e5d37cefa8644ab3dec2919259c0bc0158f235cf Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:40:58 +0000 Subject: [PATCH 1/5] implement a basic asm.js code generator The code generator / pass takes a subset of the L0 language as input and translates it to asm.js. This is a very early implementation, based on `pass0`, with lots of issues and missing features. --- passes/asmjs.nim | 894 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 894 insertions(+) create mode 100644 passes/asmjs.nim diff --git a/passes/asmjs.nim b/passes/asmjs.nim new file mode 100644 index 00000000..793b76a1 --- /dev/null +++ b/passes/asmjs.nim @@ -0,0 +1,894 @@ +## Implements the translation/lowering of L? code into the asm.js JavaScript +## subset. + +import + std/tables, + passes/[ + trees, + spec + ], + vm/[ + utils + ] + +type + TypeId = distinct int + + ProcBucket = object + ## Procedures are sorted into buckets, based on their type. + start: int + elems: seq[int32] + + Tables = object + procMap: seq[tuple[bucket, rel: int]] + bucketMap: Table[TypeId, int] ## proc type ID -> bucket index + buckets: seq[ProcBucket] + + GraphNode = object + ## A node in a control-flow graph. Corresponds to a continuation. + isLoop: bool + ## whether the node has a loop exit + exits: seq[int32] + ## all relevant outgoing edges + firstExit: int + ## the node with the lowest index jumping to this node + + isTarget: bool + ## whether the node is targeted by a jump + labels: seq[tuple[isLoop: bool, val: int]] + + PassCtx = object + # inputs: + types: NodeIndex + + # per-procedure state: + locals: NodeIndex + returnType: JsType + tables: ptr Tables + usesSp: bool + current: int + ## index of the current continuation + nodes: seq[GraphNode] + + Formatter = object + buf: string + indent: int + first: bool + + JsType = enum + ## Corresponds to the asm.js types. + jsVoid + jsSigned + jsUnsigned + jsFloat + jsDouble + jsInt + jsFixnum + jsIntish + jsFloatish + + jsUnknown + + TypeInfo = object + kind: range[jsSigned..jsDouble] + size: int + +# shorten some common parameter definitions: +using + c: PassCtx + tree: PackedTree[NodeKind] + f: var Formatter + +proc `==`(a, b: TypeId): bool {.borrow.} + +proc write(f: var Formatter, str: string) = + if f.first: + for i in 0..>>0" + of jsFloat: + f.write "fround(" & val & ")" + of jsDouble: + f.write "+" & val + else: + unreachable() + +proc genExpr(c; f; tree; val: NodeIndex): JsType + +proc isSubtype(a, b: JsType): bool = + ## Whether `b` is a sub-type of `a`. + case a + of jsFloatish: + b in {jsFloat, jsFloatish} + of jsFloat: + b == jsFloat + of jsDouble: + b == jsDouble + of jsInt: + b in {jsInt, jsSigned, jsUnsigned, jsFixnum} + of jsIntish: + b in {jsIntish, jsInt, jsSigned, jsUnsigned, jsFixnum} + of jsUnsigned: + b in {jsUnsigned, jsFixnum} + of jsSigned: + b in {jsSigned, jsFixnum} + else: + false + +proc genExpr(c; f; tree; val: NodeIndex, expect: JsType; force = false) = + var start = f.buf.len + let typ = c.genExpr(f, tree, val) + # calls always require a coercion + if not isSubtype(expect, typ) or tree[val].kind == Call or force: + case expect + of jsInt, jsSigned: + f.buf.insert("(", start) + f.write "|0)" + of jsUnsigned: + f.buf.insert("(", start) + f.write ">>>0)" + of jsFloat, jsFloatish: + f.buf.insert("fround(", start) + f.write ")" + of jsDouble: + f.buf.insert("+", start) + else: + unreachable() + +proc jump(f; target: int) = + f.write "break L" & $target & ";" + +proc toJs(t: TypeInfo): range[jsSigned..jsDouble] = + t.kind + +proc toJs(tree; types: NodeIndex, t: TypeId): JsType = + toJs parseType(tree, types, t) + +proc generic(x: JsType): JsType {.inline.} = + if x in {jsSigned, jsUnsigned}: + jsInt + else: + x + +proc returnType(tree; types: NodeIndex, t: TypeId): JsType = + let n = tree.child(types, int(t)) + case tree[n, 0].kind + of Void: jsVoid + of Type: generic toJs(tree, types, tree[n, 0].typ) + else: unreachable() + +proc genCall(c; f; tree; call: NodeIndex, + start: int, fin: BackwardsIndex): JsType = + ## Generates the code a call. `start` and `fin` are where the relevant parts + ## of the list in `call` start and end, respectively. + if tree[call, start].kind == Proc: + # it's a static call + let id = tree[call, start].id + let typ = tree.child(c.types, tree[tree.child(tree.child(2), id), 0].val.int) + + f.write "f" & $id & "(" + var i = 1 + for it in tree.items(call, start + 1, fin): + if i > 1: + f.write ", " + c.genExpr(f, tree, it, generic toJs(tree, c.types, tree[typ, i].typ)) + inc i + f.write ")" + result = returnType(tree, c.types, tree[tree.child(tree.child(2), id), 0].typ) + else: + # it's an indirect call + let bucket = c.tables.bucketMap[tree[call, start].typ] + f.write "indirect" & $bucket & "[(" + c.genExpr(f, tree, tree.child(call, start + 1), jsIntish) + f.write " - " & $(c.tables.buckets[bucket].start + 1) & ") & " + f.write $(c.tables.buckets[bucket].elems.len-1) & "](" + + let typ = tree.child(c.types, tree[call, start].val.int) + + var i = 1 + for it in tree.items(call, start + 2, fin): + if i > 1: + f.write ", " + c.genExpr(f, tree, it, generic toJs(tree, c.types, tree[typ, i].typ)) + inc i + f.write ")" + result = returnType(tree, c.types, tree[call, start].typ) + +proc signExtend(c; f; typ: TypeInfo, inTyp: JsType): JsType = + if typ.size < 4: + let shift = 32 - (typ.size * 8) + f.write " << " & $shift & " >> " & $shift + jsSigned + else: + inTyp + +proc mask(c; f; typ: TypeInfo, inTyp: JsType): JsType = + if typ.size < 4: + f.write " & " & $((1 shl (typ.size * 8)) - 1) + jsSigned + else: + inTyp + +proc genBinaryOp(c; f; tree; a, b: NodeIndex, binop: string, typ: JsType) = + f.write "(" + c.genExpr(f, tree, a, typ) + f.write " " & binop & " " + c.genExpr(f, tree, b, typ) + f.write ")" + +proc genBinaryOp(c; f; tree; op: NodeIndex, + binop: string, s, u, fl, d: JsType): TypeInfo {.discardable.} = + ## Generates the code for a two-operand operation, with the opcode picked + ## based on the type. + let (typ, a, b) = triplet(tree, op) + result = parseType(tree, c.types, tree[typ].typ) + genBinaryOp(c, f, tree, a, b, binop, s) + +proc genBinaryArithOp(c; f; tree; op: NodeIndex, binop: string, s,u,fl,d: JsType): JsType = + ## Similar to ``genBinaryOp``, but also handles uint overflow. + let typ = c.genBinaryOp(f, tree, op, binop, s,u,fl,d) + if typ.kind == jsUnsigned: + # unsigned integers need to "wrap around" on overflow + c.mask(f, typ, jsIntish) + else: + jsIntish + +proc heapAccess(c; f; tree; typ: TypeInfo, e: NodeIndex): JsType = + f.write: + case typ.kind + of jsSigned: + case typ.size + of 1: "HEAP_I8[" + of 2: "HEAP_I16[" + of 4: "HEAP_I32[" + else: unreachable() + of jsUnsigned: + case typ.size + of 1: "HEAP_U8[" + of 2: "HEAP_U16[" + of 4: "HEAP_U32[" + else: unreachable() + of jsFloat: + "HEAP_F32[" + of jsDouble: + "HEAP_F64[" + + c.genExpr(f, tree, e, jsIntish) + case typ.size + of 1: f.write "]" + of 2: f.write " >> 1]" + of 4: f.write " >> 2]" + of 8: f.write " >> 3]" + else: unreachable() + + # per the asm.js specification: + case typ.kind + of jsSigned, jsUnsigned: jsIntish + of jsFloat: jsFloat # TODO: wrong, it's 'float?' + of jsDouble: jsDouble # TODO: wrong, it's 'dobule?' + +proc typeof(c; tree; local: int): JsType = + generic toJs(tree, c.types, tree[c.locals, local].typ) + +proc genExpr(c; f; tree; val: NodeIndex): JsType = + ## Generates the code for an expression (`val`), which is ``value`` in the + ## grammar. + ## + ## The returned JS type is a subtype of the JS operation as documented in the + ## asm.js spec. + case tree[val].kind + of IntVal: + let val = tree.getInt(val) + f.write $val + if val >= 0 and val < high(int32): + jsFixnum + else: + jsSigned + of FloatVal: + f.write $tree.getFloat(val) + jsDouble + of ProcVal: + # always append 1 so that the zero value can represent "no procedure" + # TODO: ^^ this should be handled by a higher-level language instead + let (bucket, rel) = c.tables.procMap[tree[val].id] + f.write $(c.tables.buckets[bucket].start + rel + 1) + jsFixnum + of Copy: + case tree[val, 0].kind + of Local: + f.write "lo" & $tree[val, 0].id + c.typeof(tree, tree[val, 0].id) + of Global: + f.write "g" & $tree[val, 0].id + jsUnknown # TODO: we *do* know the type + else: + unreachable() + of Load: + let typ = parseType(tree, c.types, tree[val, 0].typ) + c.heapAccess(f, tree, typ, tree.child(val, 1)) + of Addr: + f.write "(bp + " & $tree.getInt(tree.child(val, 0)) & ")" + jsIntish + of Neg: + let (typ, operand) = pair(tree, val) + let t = toJs(tree, c.types, tree[typ].typ) + f.write "-" + c.genExpr(f, tree, operand, t) + case t + of jsDouble: jsDouble + of jsFloat: jsFloatish + else: jsIntish + of Add: + let (typ, a, b) = triplet(tree, val) + let t = parseType(tree, c.types, tree[typ].typ) + case toJs(t) + of jsSigned: c.genBinaryOp(f, tree, a, b, "+", jsInt); jsIntish + of jsUnsigned: c.genBinaryOp(f, tree, a, b, "+", jsInt); c.mask(f, t, jsIntish) + of jsFloat: c.genBinaryOp(f, tree, a, b, "+", jsFloat); jsFloatish + of jsDouble: c.genBinaryOp(f, tree, a, b, "+", jsDouble); jsDouble + of Sub: + let (typ, a, b) = triplet(tree, val) + let t = parseType(tree, c.types, tree[typ].typ) + case toJs(t) + of jsSigned: c.genBinaryOp(f, tree, a, b, "-", jsInt); jsIntish + of jsUnsigned: c.genBinaryOp(f, tree, a, b, "-", jsInt); c.mask(f, t, jsIntish) + of jsFloat: c.genBinaryOp(f, tree, a, b, "-", jsFloat); jsFloatish + of jsDouble: c.genBinaryOp(f, tree, a, b, "-", jsDouble); jsDouble + of Mul: + let (typ, a, b) = triplet(tree, val) + let t = parseType(tree, c.types, tree[typ].typ) + case t.kind + of jsSigned, jsUnsigned: + f.write "imul(" + c.genExpr(f, tree, a, jsInt) + f.write ", " + c.genExpr(f, tree, b, jsInt) + f.write ")" + if t.kind == jsUnsigned: + c.mask(f, t, jsSigned) + else: + jsSigned + of jsFloat: + c.genBinaryOp(f, tree, a, b, "*", jsFloat) # TODO: needs to be 'float?' + jsFloatish + of jsDouble: + c.genBinaryOp(f, tree, a, b, "*", jsDouble) # TODO: needs to be 'double?' + jsDouble + of Div: + let (typ, a, b) = triplet(tree, val) + let t = parseType(tree, c.types, tree[typ].typ) + case toJs(t) + of jsSigned: c.genBinaryOp(f, tree, a, b, "/", jsSigned); jsIntish + of jsUnsigned: c.genBinaryOp(f, tree, a, b, "/", jsUnsigned); jsIntish + of jsFloat: c.genBinaryOp(f, tree, a, b, "/", jsFloat); jsFloatish + of jsDouble: c.genBinaryOp(f, tree, a, b, "/", jsDouble); jsDouble + of Mod: + toJs c.genBinaryOp(f, tree, val, "%", jsIntish, jsIntish, jsDouble, jsDouble) + of BitNot: + let (typ, a) = pair(tree, val) + let t = parseType(tree, c.types, tree[typ].typ) + f.write "~" + c.genExpr(f, tree, a, jsIntish) + if t.kind == jsUnsigned: + # discard the unused higher bits: + c.mask(f, t, jsSigned) + else: + c.signExtend(f, t, jsSigned) + of BitAnd: + let (a, b) = pair(tree, val) + c.genBinaryOp(f, tree, a, b, "&", jsIntish) + jsSigned + of BitOr: + let (a, b) = pair(tree, val) + c.genBinaryOp(f, tree, a, b, "|", jsIntish) + jsSigned + of BitXor: + c.genBinaryArithOp(f, tree, val, "^", jsIntish, jsIntish, jsUnknown, jsUnknown) + of Shl: + let (typ, a, b) = triplet(tree, val) + c.genExpr(f, tree, a, jsIntish) + f.write " << " + c.genExpr(f, tree, b, jsIntish) + let t = parseType(tree, c.types, tree[typ].typ) + if t.kind == jsSigned: + c.signExtend(f, t, jsSigned) + else: + c.mask(f, t, jsSigned) + of Shr: + let (typ, a, b) = triplet(tree, val) + c.genExpr(f, tree, a, jsIntish) + case parseType(tree, c.types, tree[typ].typ).kind + of jsSigned: f.write " >> " # arithmetic right-shift + of jsUnsigned: f.write " >>> " # logical right-shift + else: unreachable() + c.genExpr(f, tree, b, jsIntish) + jsIntish + of Not: + f.write "!" + c.genExpr(f, tree, tree.child(val, 0), jsInt) + jsInt + of Eq: + c.genBinaryOp(f, tree, val, "==", jsSigned, jsUnsigned, jsFloat, jsDouble) + jsInt + of Lt: + c.genBinaryOp(f, tree, val, "<", jsSigned, jsUnsigned, jsFloat, jsDouble) + jsInt + of Le: + c.genBinaryOp(f, tree, val, "<=", jsSigned, jsUnsigned, jsFloat, jsDouble) + jsInt + of Reinterp: + # reinterpret the bit pattern as another type + let + dtyp = parseType(tree, c.types, tree[val, 0].typ) + styp = parseType(tree, c.types, tree[val, 1].typ) + # sanity checks: + assert dtyp.kind != styp.kind + assert dtyp.size == styp.size + + case dtyp.kind + of jsSigned, jsUnsigned: + case styp.kind + of jsSigned, jsUnsigned: + c.genExpr(f, tree, tree.child(val, 2), jsInt) + jsInt + of jsFloat: + # TODO: make sure the stack-pointer always stays 8-byte aligned + f.write "(HEAP_F32[((sp + 7) & ~7) >> 2] = " + c.genExpr(f, tree, tree.child(val, 2), jsFloatish) + f.write ", HEAP_I32[((sp + 7) & ~7) >> 2])" + jsIntish + of jsDouble: + unreachable() + of jsFloat, jsDouble: + unreachable() + of Conv: + # numeric conversion + let + dtyp = parseType(tree, c.types, tree[val, 0].typ) + styp = parseType(tree, c.types, tree[val, 1].typ) + + case dtyp.kind + of jsSigned: + case styp.kind + of jsSigned, jsUnsigned: + c.genExpr(f, tree, tree.child(val, 2), jsInt) + c.signExtend(f, dtyp, jsInt) + # leave coercing to the caller + of jsDouble, jsFloat: + f.write "(~~" + c.genExpr(f, tree, tree.child(val, 2), styp.kind) + f.write ")" + jsSigned + of jsUnsigned: + case styp.kind + of jsSigned, jsUnsigned: + c.genExpr(f, tree, tree.child(val, 2), jsInt) + # the upper bits can only be set if the source type has larger bit- + # width than the destination + if dtyp.size < styp.size: + c.mask(f, dtyp, jsInt) + else: + jsInt # leave coercing to unsigned to the caller + of jsDouble, jsFloat: + f.write "(~~" + c.genExpr(f, tree, tree.child(val, 2), styp.kind) + f.write ")" + jsSigned # leave coercing to unsigned to the caller + of jsFloat: + c.genExpr(f, tree, tree.child(val, 2), jsFloat) + jsFloat + of jsDouble: + c.genExpr(f, tree, tree.child(val, 2), jsDouble) + jsDouble + + of Call: + c.genCall(f, tree, val, 0, ^1) + else: + unreachable() + +proc genChoice(c; f; tree; choice: NodeIndex) = + ## Generates the code for ``Choice`` tree, where `val` is the selector and + ## `typ` the selector's type. + f.write "case " + c.genExpr(f, tree, tree.child(choice, 0), jsSigned) + f.write ": " + f.jump(tree[tree.child(choice, 1), 0].imm) + f.writeLine "" + +proc genExit(f; c; tree; exit: NodeIndex) = + ## Generates the code for a continuation exit. + case tree[exit].kind + of Continue: + case tree.len(exit) + of 1: + let i = tree[exit, 0].imm + if i.int != c.current + 1: + f.writeLine "break L" & $i & ";" + of 2: + # continue with argument can only mean return + # TODO: this is wrong. The stack pointer must be reset *after* the + # return value is computed, since the expression might depend on + # the stack-pointer (directly or indirectly) + if c.usesSp: + f.writeLine "sp = bp;" + f.write "return " + c.genExpr(f, tree, tree.child(exit, 1), c.returnType, force=true) + f.writeLine ";" + else: + unreachable() + of Loop: + dec f.indent + f.writeLine "}" + of SelectBool: + let (sel, a, b) = triplet(tree, exit) + f.write "if (" + c.genExpr(f, tree, sel, jsInt) + f.write ") { " + f.jump(tree[a, 0].imm) + f.write " } else { " + f.jump(tree[b, 0].imm) + f.writeLine " }" + of Select: + let + val = tree.child(exit, 1) # the value to select the target with + f.write "switch (" + # unsigned integers are coerced into signed ones + c.genExpr(f, tree, val, jsSigned) + f.writeLine ") {" + for it in tree.items(exit, 2, ^2): + c.genChoice(f, tree, it) + f.write "default: " + f.jump(tree[tree.last(tree.last(exit)), 0].imm) + f.writeLine "" + f.writeLine "}" + of Unreachable: + discard "a no-op" + else: + unreachable() + +proc genStmt(f; c; tree; n: NodeIndex) = + ## Generates the bytecode for a statement (`n`). + case tree[n].kind + of Asgn: + f.write "lo" & $tree[n, 0].id & " = " + c.genExpr(f, tree, tree.child(n, 1), c.typeof(tree, tree[n, 0].id)) + f.writeLine ";" + of Store: + let + (tn, a, b) = triplet(tree, n) + typ = parseType(tree, c.types, tree[tn].typ) + + let jt = c.heapAccess(f, tree, typ, a) + f.write " = " + c.genExpr(f, tree, b, jt) # TODO: using `jt` is not really correct here + f.writeLine ";" + of Copy: + let (a, b, size) = triplet(tree, n) + f.write "memcopy(" + c.genExpr(f, tree, a, jsInt) + f.write ", " + c.genExpr(f, tree, b, jsInt) + f.write ", " + c.genExpr(f, tree, size, jsInt) + f.writeLine ");" + of Clear: + let (a, size) = pair(tree, n) + f.write "memclear(" + c.genExpr(f, tree, a, jsInt) + f.write ", " + c.genExpr(f, tree, size, jsInt) + f.writeLine ");" + of Call: + discard c.genCall(f, tree, n, 0, ^1) + f.writeLine ";" + of Drop: + discard c.genExpr(f, tree, tree.child(n, 0)) + f.writeLine ";" + else: + unreachable() + +proc gen(f: var Formatter, c; tree; n: NodeIndex) = + ## Generates bytecode for the given continuation. + if c.nodes[c.current].isTarget: + dec f.indent + f.writeLine "}" + + for it in c.nodes[c.current].labels.items: + if it.isLoop: + f.writeLine "while (1) {" + inc f.indent + else: + f.writeLine "L" & $it.val & ": {" + inc f.indent + + if c.usesSp: + f.writeLine "sp = (bp + " & $tree[n, 1].imm & ")|0;" + + for it in tree.items(n, 2, ^2): + genStmt(f, c, tree, it) + + genExit(f, c, tree, tree.last(n)) + +proc default(f: var Formatter, typ: JsType) = + case typ + of jsSigned, jsUnsigned: + f.write "0" + of jsFloat: + f.write "fround(0.0)" + of jsDouble: + f.write "0.0" + else: + unreachable() + +proc scan(tree; conts: NodeIndex): seq[GraphNode] = + result.newSeq(tree.len(conts)) + + proc scanAux(tree; cont: NodeIndex, i: int32): GraphNode = + proc addNoDup[T](s: var seq[T], val: T) = + if val notin s: + s.add val + + let exit = tree.last(cont) + case tree[exit].kind + of SelectBool: + result.exits.addNoDup tree[tree.child(exit, 1), 0].imm + result.exits.addNoDup tree[tree.child(exit, 2), 0].imm + of Select: + for it in tree.items(exit, 2): + result.exits.addNoDup tree[tree.last(it), 0].imm + of Loop: + result.isLoop = true + result.exits = @[tree[exit, 0].imm] + of Continue: + if tree.len(exit) == 1: + let e = tree[exit, 0].imm + if e != i + 1: + result.exits = @[tree[exit, 0].imm] + else: + echo tree[exit].kind + unreachable() + + result.firstExit = high(int) + + for i, it in tree.pairs(conts): + if tree.len(it) > 1: + result[i] = scanAux(tree, it, i.int32) + else: + result[i].firstExit = high(int) + + for i, it in result.mpairs: + if it.isLoop: + result[it.exits[0]].labels.add (true, i) + else: + for e in it.exits.items: + result[e].isTarget = true + result[e].firstExit = min(result[e].firstExit, i) + + for i, it in result.mpairs: + if it.firstExit != high(int): + var x = 0 + while x < result[it.firstExit].labels.len: + if result[it.firstExit].labels[x].val < i: + break + inc x + result[it.firstExit].labels.insert((false, i), x) + +proc translate(f: var Formatter, tables: Tables, tree; types, def: NodeIndex) = + ## Translates the single procedure body `body` to VM bytecode. `types` + ## provides the type environment. + let (typ, locals, conts) = tree.triplet(def) + + var c = PassCtx(types: types, locals: locals, tables: addr tables) + + let procTy = tree.child(types, tree[typ].val.int) + let numParams = tree.len(procTy) - 1 + + if tree[procTy, 0].kind != Void: + c.returnType = generic toJs(tree, types, tree[procTy, 0].typ) + + f.write "(" + # put all parameters into locals: + for i in countdown(numParams, 1): + if i < numParams: + f.write ", " + f.write "lo" & $(numParams - i) + f.writeLine ") {" + inc f.indent + + for i in 0.. 1: # ignore the return continuation + if tree[it, 1].imm > 0: + c.usesSp = true + break + + if c.usesSp or tree.len(locals) > numParams: + f.write "var " + + var first = true + + # setup the stack pointer, if one is required: + if c.usesSp: + f.write "bp = 0" + first = false + + for i in numParams.. 0: + code.write ", " + code.write "f" & $def + code.writeLine "]" + + code.writeLine "return f" & $(module.len(procs) - 1) & ";" + dec code.indent + code.writeLine "}" + + code.writeLine "var stdlib = {Math: { fround: Math.fround, imul: Math.imul }, Int8Array: Int8Array, Int16Array: Int16Array, Int32Array: Int32Array, Uint8Array: Uint8Array, Uint16Array: Uint16Array, Uint32Array: Uint32Array, Float32Array: Float32Array, Float64Array: Float64Array };" + code.writeLine "var heap = new ArrayBuffer(0x10000);" + code.writeLine "var mod = module(stdlib, null, heap);" + code.writeLine "console.log(mod());" + + result = code.buf From 9c92ee9498b67712034b579197bcd287eca84776 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:40:59 +0000 Subject: [PATCH 2/5] tests: add a test runner for the asm.js code generator It just writes the asm.js code to a file and invokes Node.js on it. --- tests/asmjs/runner.nim | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/asmjs/runner.nim diff --git a/tests/asmjs/runner.nim b/tests/asmjs/runner.nim new file mode 100644 index 00000000..86ff1edf --- /dev/null +++ b/tests/asmjs/runner.nim @@ -0,0 +1,32 @@ +## The test runner for asm.js tests. + +import + std/[ + os, + streams, + strutils + ], + experimental/[ + sexp + ], + passes/[ + asmjs, + spec, + trees + ] + +let args = getExecArgs() +let s = openFileStream(args[^1], fmRead) +# skip the test specification: +if s.readLine() == "discard \"\"\"": + while not s.readLine().endsWith("\"\"\""): + discard +else: + s.setPosition(0) + +let f = translate(fromSexp[NodeKind](parseSexp("(Module " & readAll(s) & ")"))) +writeFile("test.js", f) + +# run the test with Node.js: +if execShellCmd("node " & (getCurrentDir() / "test.js")) != 0: + quit(1) From 5ed60e8941597d5d6f0cbce15b6237beea9e50c5 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:40:59 +0000 Subject: [PATCH 3/5] tests: add a basic test --- tests/asmjs/test.test | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/asmjs/test.test diff --git a/tests/asmjs/test.test b/tests/asmjs/test.test new file mode 100644 index 00000000..c5258bf5 --- /dev/null +++ b/tests/asmjs/test.test @@ -0,0 +1,46 @@ +discard """ +""" +(TypeDefs + (Int 4) + (UInt 4) + (Float 4) + (Float 8) + (ProcTy (Type 0)) + (ProcTy (Type 0) (Type 0) (Type 1) (Type 2) (Type 3)) + (UInt 2) + (UInt 1) + (Int 1) +) +(GlobalDefs) +(ProcDefs + (ProcDef (Type 5) (Locals (Type 0) (Type 1) (Type 2) (Type 3)) (Continuations + (Continuation (Params) 0 (Continue 1 (Copy (Local 0)))) + (Continuation (Params (Type 0))) + )) + (ProcDef (Type 4) (Locals (Type 0) (Type 6)) (Continuations + (Continuation (Params) 4 + (Asgn (Local 0) (Call (Proc 0) (IntVal -1) (IntVal 1) (FloatVal 10.0) (FloatVal 11.0))) + (Store (Type 0) (Addr 0) (Copy (Local 0))) + (Select (Type 0) (IntVal 1) (Choice (IntVal 1) (Continue 1)) (Choice (IntVal 6) (Continue 2))) + ) + (Continuation (Params) 4 + (Drop (Reinterp (Type 1) (Type 2) (Neg (Type 2) (FloatVal 1.5)))) + (Drop (Div (Type 1) (IntVal -1) (IntVal 2))) + (Drop (Conv (Type 1) (Type 7) (BitNot (Type 7) (IntVal 2)))) + (Continue 5 (Conv (Type 0) (Type 8) (Shl (Type 8) (IntVal 1) (IntVal 7)))) + ) + (Continuation (Params) 0 + (SelectBool (Lt (Type 6) (Copy (Local 1)) (IntVal 10)) (Continue 3) (Continue 4)) + ) + (Continuation (Params) 0 + (Asgn (Local 1) (Add (Type 6) (Copy (Local 1)) (IntVal 1))) + (Loop 2) + ) + (Continuation (Params) 32 + (Clear (Addr 0) (IntVal 32)) + (Copy (Addr 0) (Addr 16) (IntVal 16)) + (Continue 3 (Call (Type 5) (ProcVal 0) (Conv (Type 0) (Type 6) (Mul (Type 6) (Copy (Local 1)) (IntVal 10))) (IntVal 1) (FloatVal 10.0) (FloatVal 11.0))) + ) + (Continuation (Params (Type 0))) + )) +) \ No newline at end of file From a7179da5fc553628362ca24b6b6fbbbff71473f0 Mon Sep 17 00:00:00 2001 From: zerbina <100542850+zerbina@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:15:41 +0000 Subject: [PATCH 4/5] asmjs: update the type handling `parseType` properly considers the new inlined types now. Tracking the position of the typedefs list (the `types` field) is removed; it was unnecessary from the start. --- passes/asmjs.nim | 87 +++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/passes/asmjs.nim b/passes/asmjs.nim index 793b76a1..09f3eaa4 100644 --- a/passes/asmjs.nim +++ b/passes/asmjs.nim @@ -38,9 +38,6 @@ type labels: seq[tuple[isLoop: bool, val: int]] PassCtx = object - # inputs: - types: NodeIndex - # per-procedure state: locals: NodeIndex returnType: JsType @@ -109,20 +106,24 @@ func typ(n: TreeNode[NodeKind]): TypeId {.inline.} = assert n.kind == Type TypeId(n.val) +func resolve(tree; typ: TypeId): NodeIndex = + tree.child(tree.child(0), typ.ord) + proc parseType(tree; at: NodeIndex): TypeInfo = + var at = at + if tree[at].kind == Type: + at = tree.resolve(tree[at].typ) # resolve the indirection + case tree[at].kind - of Int: TypeInfo(kind: jsSigned, size: tree[at, 0].imm) - of UInt: TypeInfo(kind: jsUnsigned, size: tree[at, 0].imm) + of Int: TypeInfo(kind: jsSigned, size: tree[at].val.int) + of UInt: TypeInfo(kind: jsUnsigned, size: tree[at].val.int) of Float: - case tree[at, 0].imm + case tree[at].val of 4: TypeInfo(kind: jsFloat, size: 4) of 8: TypeInfo(kind: jsDouble, size: 8) else: unreachable() else: unreachable() -proc parseType(tree; types: NodeIndex, id: TypeId): TypeInfo = - parseType(tree, tree.child(types, int(id))) - proc conv(f: var Formatter, typ: JsType, val: string) = case typ of jsSigned, jsInt: @@ -184,8 +185,8 @@ proc jump(f; target: int) = proc toJs(t: TypeInfo): range[jsSigned..jsDouble] = t.kind -proc toJs(tree; types: NodeIndex, t: TypeId): JsType = - toJs parseType(tree, types, t) +proc toJs(tree; n: NodeIndex): JsType = + toJs parseType(tree, n) proc generic(x: JsType): JsType {.inline.} = if x in {jsSigned, jsUnsigned}: @@ -193,12 +194,11 @@ proc generic(x: JsType): JsType {.inline.} = else: x -proc returnType(tree; types: NodeIndex, t: TypeId): JsType = - let n = tree.child(types, int(t)) +proc returnType(tree; t: TypeId): JsType = + let n = tree.child(tree.child(0), int(t)) case tree[n, 0].kind of Void: jsVoid - of Type: generic toJs(tree, types, tree[n, 0].typ) - else: unreachable() + else: generic toJs(tree, tree.child(n, 0)) proc genCall(c; f; tree; call: NodeIndex, start: int, fin: BackwardsIndex): JsType = @@ -207,17 +207,17 @@ proc genCall(c; f; tree; call: NodeIndex, if tree[call, start].kind == Proc: # it's a static call let id = tree[call, start].id - let typ = tree.child(c.types, tree[tree.child(tree.child(2), id), 0].val.int) + let typ = tree.resolve(tree[tree.child(tree.child(2), id), 0].typ) f.write "f" & $id & "(" var i = 1 for it in tree.items(call, start + 1, fin): if i > 1: f.write ", " - c.genExpr(f, tree, it, generic toJs(tree, c.types, tree[typ, i].typ)) + c.genExpr(f, tree, it, generic toJs(tree, tree.child(typ, i))) inc i f.write ")" - result = returnType(tree, c.types, tree[tree.child(tree.child(2), id), 0].typ) + result = returnType(tree, tree[tree.child(tree.child(2), id), 0].typ) else: # it's an indirect call let bucket = c.tables.bucketMap[tree[call, start].typ] @@ -226,16 +226,16 @@ proc genCall(c; f; tree; call: NodeIndex, f.write " - " & $(c.tables.buckets[bucket].start + 1) & ") & " f.write $(c.tables.buckets[bucket].elems.len-1) & "](" - let typ = tree.child(c.types, tree[call, start].val.int) + let typ = tree.resolve(tree[call, start].typ) var i = 1 for it in tree.items(call, start + 2, fin): if i > 1: f.write ", " - c.genExpr(f, tree, it, generic toJs(tree, c.types, tree[typ, i].typ)) + c.genExpr(f, tree, it, generic toJs(tree, tree.child(typ, i))) inc i f.write ")" - result = returnType(tree, c.types, tree[call, start].typ) + result = returnType(tree, tree[call, start].typ) proc signExtend(c; f; typ: TypeInfo, inTyp: JsType): JsType = if typ.size < 4: @@ -264,7 +264,7 @@ proc genBinaryOp(c; f; tree; op: NodeIndex, ## Generates the code for a two-operand operation, with the opcode picked ## based on the type. let (typ, a, b) = triplet(tree, op) - result = parseType(tree, c.types, tree[typ].typ) + result = parseType(tree, typ) genBinaryOp(c, f, tree, a, b, binop, s) proc genBinaryArithOp(c; f; tree; op: NodeIndex, binop: string, s,u,fl,d: JsType): JsType = @@ -311,7 +311,7 @@ proc heapAccess(c; f; tree; typ: TypeInfo, e: NodeIndex): JsType = of jsDouble: jsDouble # TODO: wrong, it's 'dobule?' proc typeof(c; tree; local: int): JsType = - generic toJs(tree, c.types, tree[c.locals, local].typ) + generic toJs(tree, tree.child(c.locals, local)) proc genExpr(c; f; tree; val: NodeIndex): JsType = ## Generates the code for an expression (`val`), which is ``value`` in the @@ -347,14 +347,14 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = else: unreachable() of Load: - let typ = parseType(tree, c.types, tree[val, 0].typ) - c.heapAccess(f, tree, typ, tree.child(val, 1)) + let (typ, e) = tree.pair(val) + c.heapAccess(f, tree, parseType(tree, typ), e) of Addr: f.write "(bp + " & $tree.getInt(tree.child(val, 0)) & ")" jsIntish of Neg: let (typ, operand) = pair(tree, val) - let t = toJs(tree, c.types, tree[typ].typ) + let t = toJs(tree, typ) f.write "-" c.genExpr(f, tree, operand, t) case t @@ -363,7 +363,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = else: jsIntish of Add: let (typ, a, b) = triplet(tree, val) - let t = parseType(tree, c.types, tree[typ].typ) + let t = parseType(tree, typ) case toJs(t) of jsSigned: c.genBinaryOp(f, tree, a, b, "+", jsInt); jsIntish of jsUnsigned: c.genBinaryOp(f, tree, a, b, "+", jsInt); c.mask(f, t, jsIntish) @@ -371,7 +371,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = of jsDouble: c.genBinaryOp(f, tree, a, b, "+", jsDouble); jsDouble of Sub: let (typ, a, b) = triplet(tree, val) - let t = parseType(tree, c.types, tree[typ].typ) + let t = parseType(tree, typ) case toJs(t) of jsSigned: c.genBinaryOp(f, tree, a, b, "-", jsInt); jsIntish of jsUnsigned: c.genBinaryOp(f, tree, a, b, "-", jsInt); c.mask(f, t, jsIntish) @@ -379,7 +379,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = of jsDouble: c.genBinaryOp(f, tree, a, b, "-", jsDouble); jsDouble of Mul: let (typ, a, b) = triplet(tree, val) - let t = parseType(tree, c.types, tree[typ].typ) + let t = parseType(tree, typ) case t.kind of jsSigned, jsUnsigned: f.write "imul(" @@ -399,8 +399,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = jsDouble of Div: let (typ, a, b) = triplet(tree, val) - let t = parseType(tree, c.types, tree[typ].typ) - case toJs(t) + case toJs(parseType(tree, typ)) of jsSigned: c.genBinaryOp(f, tree, a, b, "/", jsSigned); jsIntish of jsUnsigned: c.genBinaryOp(f, tree, a, b, "/", jsUnsigned); jsIntish of jsFloat: c.genBinaryOp(f, tree, a, b, "/", jsFloat); jsFloatish @@ -409,7 +408,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = toJs c.genBinaryOp(f, tree, val, "%", jsIntish, jsIntish, jsDouble, jsDouble) of BitNot: let (typ, a) = pair(tree, val) - let t = parseType(tree, c.types, tree[typ].typ) + let t = parseType(tree, typ) f.write "~" c.genExpr(f, tree, a, jsIntish) if t.kind == jsUnsigned: @@ -432,7 +431,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = c.genExpr(f, tree, a, jsIntish) f.write " << " c.genExpr(f, tree, b, jsIntish) - let t = parseType(tree, c.types, tree[typ].typ) + let t = parseType(tree, typ) if t.kind == jsSigned: c.signExtend(f, t, jsSigned) else: @@ -440,7 +439,7 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = of Shr: let (typ, a, b) = triplet(tree, val) c.genExpr(f, tree, a, jsIntish) - case parseType(tree, c.types, tree[typ].typ).kind + case parseType(tree, typ).kind of jsSigned: f.write " >> " # arithmetic right-shift of jsUnsigned: f.write " >>> " # logical right-shift else: unreachable() @@ -462,8 +461,8 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = of Reinterp: # reinterpret the bit pattern as another type let - dtyp = parseType(tree, c.types, tree[val, 0].typ) - styp = parseType(tree, c.types, tree[val, 1].typ) + dtyp = parseType(tree, tree.child(val, 0)) + styp = parseType(tree, tree.child(val, 1)) # sanity checks: assert dtyp.kind != styp.kind assert dtyp.size == styp.size @@ -487,8 +486,8 @@ proc genExpr(c; f; tree; val: NodeIndex): JsType = of Conv: # numeric conversion let - dtyp = parseType(tree, c.types, tree[val, 0].typ) - styp = parseType(tree, c.types, tree[val, 1].typ) + dtyp = parseType(tree, tree.child(val, 0)) + styp = parseType(tree, tree.child(val, 1)) case dtyp.kind of jsSigned: @@ -599,9 +598,8 @@ proc genStmt(f; c; tree; n: NodeIndex) = of Store: let (tn, a, b) = triplet(tree, n) - typ = parseType(tree, c.types, tree[tn].typ) - let jt = c.heapAccess(f, tree, typ, a) + let jt = c.heapAccess(f, tree, parseType(tree, tn), a) f.write " = " c.genExpr(f, tree, b, jt) # TODO: using `jt` is not really correct here f.writeLine ";" @@ -721,13 +719,12 @@ proc translate(f: var Formatter, tables: Tables, tree; types, def: NodeIndex) = ## provides the type environment. let (typ, locals, conts) = tree.triplet(def) - var c = PassCtx(types: types, locals: locals, tables: addr tables) + var c = PassCtx(locals: locals, tables: addr tables) let procTy = tree.child(types, tree[typ].val.int) let numParams = tree.len(procTy) - 1 - if tree[procTy, 0].kind != Void: - c.returnType = generic toJs(tree, types, tree[procTy, 0].typ) + c.returnType = returnType(tree, tree[typ].typ) f.write "(" # put all parameters into locals: @@ -740,7 +737,7 @@ proc translate(f: var Formatter, tables: Tables, tree; types, def: NodeIndex) = for i in 0.. Date: Sat, 30 Nov 2024 16:15:41 +0000 Subject: [PATCH 5/5] phy: integrate the `asmjs` pass The integration is very basic at the moment -- evaluation/execution is not yet implemented. --- phy/phy.nim | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/phy/phy.nim b/phy/phy.nim index fff2f5bd..c255f72e 100644 --- a/phy/phy.nim +++ b/phy/phy.nim @@ -22,6 +22,7 @@ import source_checks ], passes/[ + asmjs, changesets, debugutils, source2il, @@ -55,6 +56,7 @@ import passes/spec_source except NodeKind type Language = enum langBytecode = "vm" + langAsmJs = "asmjs" lang0 = "L0" lang1 = "L1" lang3 = "L3" @@ -194,7 +196,7 @@ proc compile(tree: var PackedTree[spec.NodeKind], source, target: Language) = var current = source while current != target: case current - of lang0, langBytecode, langSource: + of lang0, langBytecode, langSource, langAsmJs: assert false, "cannot be handled here: " & $current of lang1: syntaxCheck(tree, lang1_checks, module) @@ -317,12 +319,16 @@ proc main(args: openArray[string]) = else: code = fromSexp[spec.NodeKind](text) - if target == langBytecode: + case target + of langBytecode: # compile to L0 code and then translate to bytecode compile(code, newSource, lang0) syntaxCheck(code, lang0) link(env, hostProcedures(gRunner), [pass0.translate(code)]) # the bytecode is verified later + of langAsmJs: + compile(code, newSource, lang0) + syntaxCheck(code, lang0) else: compile(code, newSource, target) # make sure the output code is correct: @@ -354,5 +360,12 @@ proc main(args: openArray[string]) = else: # we don't have high-level type information stdout.write run(env, env.procs.high.ProcIndex) + elif target == langAsmJs: + let module = asmjs.translate(code) + + if langAsmJs in gShow: + stdout.write module + + # TODO: implement the Eval command for asmjs modules main(getExecArgs())