From a069f70d816a601dd9ed1fb28bedffe6184d4868 Mon Sep 17 00:00:00 2001 From: butterunderflow Date: Sun, 27 Oct 2024 01:15:52 +0800 Subject: [PATCH 1/6] try support some webassembly script grammar --- benchmarks/wasm/script/script_basic.wast | 8 ++ src/main/scala/wasm/AST.scala | 14 ++++ src/main/scala/wasm/MiniWasmScript.scala | 23 ++++++ src/main/scala/wasm/Parser.scala | 83 ++++++++++++++++++++- src/test/scala/genwasym/TestScriptRun.scala | 19 +++++ src/test/scala/genwasym/TestSyntax.scala | 17 +++++ 6 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 benchmarks/wasm/script/script_basic.wast create mode 100644 src/main/scala/wasm/MiniWasmScript.scala create mode 100644 src/test/scala/genwasym/TestScriptRun.scala create mode 100644 src/test/scala/genwasym/TestSyntax.scala diff --git a/benchmarks/wasm/script/script_basic.wast b/benchmarks/wasm/script/script_basic.wast new file mode 100644 index 000000000..4d1d1cb55 --- /dev/null +++ b/benchmarks/wasm/script/script_basic.wast @@ -0,0 +1,8 @@ +(module + (func $one (result i32) + i32.const 1) + (export "one" (func 0)) +) + +(assert_return (invoke "one") (i32.const 1)) + diff --git a/src/main/scala/wasm/AST.scala b/src/main/scala/wasm/AST.scala index 434acb8ec..6a2d13828 100644 --- a/src/main/scala/wasm/AST.scala +++ b/src/main/scala/wasm/AST.scala @@ -276,3 +276,17 @@ case class ExportFunc(i: Int) extends ExportDesc case class ExportTable(i: Int) extends ExportDesc case class ExportMemory(i: Int) extends ExportDesc case class ExportGlobal(i: Int) extends ExportDesc + +case class Script(cmds: List[Cmd]) extends WIR +abstract class Cmd extends WIR +// TODO: can we turn abstract class sealed? +case class CmdModule(module: Module) extends Cmd + +abstract class Action extends WIR +case class Invoke(instName: Option[String], name: String, args: List[Num]) extends Action + +abstract class Assertion extends Cmd +case class AssertReturn(action: Action, expect: List[Num] /* TODO: support multiple expect result type*/) + extends Assertion +case class AssertTrap(action: Action, message: String) extends Assertion + diff --git a/src/main/scala/wasm/MiniWasmScript.scala b/src/main/scala/wasm/MiniWasmScript.scala new file mode 100644 index 000000000..b35f960db --- /dev/null +++ b/src/main/scala/wasm/MiniWasmScript.scala @@ -0,0 +1,23 @@ +package gensym.wasm.miniwasmscript + +import gensym.wasm.miniwasm._ +import gensym.wasm.ast.{Script, Cmd} +import gensym.wasm.ast.AssertReturn +import gensym.wasm.ast.AssertTrap + +sealed class ScriptRunner { + val instances: List[ModuleInstance] = List() + + def runCmd(cmd: Cmd): Unit = { + cmd match { + case AssertReturn(action, results) => + case AssertTrap(action, message) => + } + } + + def run(script: Script): Unit = { + for (cmd <- script.cmds) { + runCmd(cmd) + } + } +} diff --git a/src/main/scala/wasm/Parser.scala b/src/main/scala/wasm/Parser.scala index 10e3dbb8f..25972ff2f 100644 --- a/src/main/scala/wasm/Parser.scala +++ b/src/main/scala/wasm/Parser.scala @@ -608,18 +608,95 @@ class GSWasmVisitor extends WatParserBaseVisitor[WIR] { } + override def visitScriptModule(ctx: ScriptModuleContext): Module = { + if (ctx.module_() != null) { + visitModule_(ctx.module_).asInstanceOf[Module] + } + else { + throw new RuntimeException("Unsupported") + } + } + + override def visitAction_(ctx: Action_Context): Action = { + if (ctx.INVOKE() != null) { + val instName = if (ctx.VAR() != null) Some(ctx.VAR().getText) else None + var name = ctx.name.getText.substring(1).dropRight(1) + var args = for (constCtx <- ctx.constList.wconst.asScala) yield { + val Array(ty, _) = constCtx.CONST.getText.split("\\.") + visitLiteralWithType(constCtx.literal, toNumType(ty)) + } + Invoke(instName, name, args.toList) + } else { + throw new RuntimeException("Unsupported") + } + } + + override def visitAssertion(ctx: AssertionContext): Assertion = { + if (ctx.ASSERT_RETURN() != null) { + val action = visitAction_(ctx.action_) + val expect = for (constCtx <- ctx.constList.wconst.asScala) yield { + val Array(ty, _) = constCtx.CONST.getText.split("\\.") + visitLiteralWithType(constCtx.literal, toNumType(ty)) + } + println(s"expect = $expect") + AssertReturn(action, expect.toList) + } + else { + throw new RuntimeException("Unsupported") + } + } + + override def visitCmd(ctx: CmdContext): Cmd = { + if (ctx.assertion() != null) { + visitAssertion(ctx.assertion) + } + else if (ctx.scriptModule() != null) { + CmdModule(visitScriptModule(ctx.scriptModule)) + } + else { + throw new RuntimeException("Unsupported") + } + } + + override def visitScript(ctx: ScriptContext): WIR = { + val cmds = for (cmd <- ctx.cmd.asScala) yield { + visitCmd(cmd) + } + Script(cmds.toList) + } } object Parser { - def parse(input: String): Module = { + private def makeWatVisitor(input: String) = { val charStream = new ANTLRInputStream(input) val lexer = new WatLexer(charStream) val tokens = new CommonTokenStream(lexer) - val parser = new WatParser(tokens) + new WatParser(tokens) + } + + def parse(input: String): Module = { + val parser = makeWatVisitor(input) val visitor = new GSWasmVisitor() - val res: Module = visitor.visit(parser.module).asInstanceOf[Module] + val res: Module = visitor.visit(parser.module()).asInstanceOf[Module] res } def parseFile(filepath: String): Module = parse(scala.io.Source.fromFile(filepath).mkString) + + // parse extended webassembly script language + def parseScript(input: String): Option[Script] = { + val parser = makeWatVisitor(input) + val visitor = new GSWasmVisitor() + val tree = parser.script() + val errorNumer = parser.getNumberOfSyntaxErrors() + if (errorNumer != 0) + None + else { + val res: Script = visitor.visitScript(tree).asInstanceOf[Script] + Some(res) + } + } + + def parseScriptFile(filepath: String): Option[Script] = + parseScript(scala.io.Source.fromFile(filepath).mkString) } diff --git a/src/test/scala/genwasym/TestScriptRun.scala b/src/test/scala/genwasym/TestScriptRun.scala new file mode 100644 index 000000000..d64f91e54 --- /dev/null +++ b/src/test/scala/genwasym/TestScriptRun.scala @@ -0,0 +1,19 @@ +package gensym.wasm + +import gensym.wasm.parser.Parser +import gensym.wasm.miniwasmscript.ScriptRunner + +import org.scalatest.FunSuite + + +class TestScriptRun extends FunSuite { + def testFile(filename: String): Unit = { + val script = Parser.parseScriptFile(filename).get + val runner = new ScriptRunner() + runner.run(script) + } + + test("simple script") { + testFile("./benchmarks/wasm/script/script_basic.wabt") + } +} diff --git a/src/test/scala/genwasym/TestSyntax.scala b/src/test/scala/genwasym/TestSyntax.scala new file mode 100644 index 000000000..8fecf874e --- /dev/null +++ b/src/test/scala/genwasym/TestSyntax.scala @@ -0,0 +1,17 @@ +package gensym.wasm + +import gensym.wasm.parser.Parser +import org.scalatest.FunSuite + +class TestSyntax extends FunSuite { + def testFile(filename: String) = { + val script = Parser.parseScriptFile(filename) + println(s"script = $script") + assert(script != None, "this syntax is not defined in antlr grammar") + } + + test("basic script") { + testFile("./benchmarks/wasm/script/script_basic.wabt") + } +} + From b10925e3c805f0aae088a9d51da5da35c2a03159 Mon Sep 17 00:00:00 2001 From: butterunderflow Date: Sun, 27 Oct 2024 04:28:33 +0800 Subject: [PATCH 2/6] a tiny example of wast --- src/main/scala/wasm/AST.scala | 2 +- src/main/scala/wasm/MiniWasm.scala | 66 ++++++++++++++---------- src/main/scala/wasm/MiniWasmScript.scala | 38 +++++++++++--- 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/main/scala/wasm/AST.scala b/src/main/scala/wasm/AST.scala index 6a2d13828..3f4699575 100644 --- a/src/main/scala/wasm/AST.scala +++ b/src/main/scala/wasm/AST.scala @@ -283,7 +283,7 @@ abstract class Cmd extends WIR case class CmdModule(module: Module) extends Cmd abstract class Action extends WIR -case class Invoke(instName: Option[String], name: String, args: List[Num]) extends Action +case class Invoke(instName: Option[String], name: String, args: List[Value]) extends Action abstract class Assertion extends Cmd case class AssertReturn(action: Action, expect: List[Num] /* TODO: support multiple expect result type*/) diff --git a/src/main/scala/wasm/MiniWasm.scala b/src/main/scala/wasm/MiniWasm.scala index 95438b83a..cb7a962dc 100644 --- a/src/main/scala/wasm/MiniWasm.scala +++ b/src/main/scala/wasm/MiniWasm.scala @@ -17,6 +17,44 @@ case class ModuleInstance( exports: List[Export] = List() ) +object ModuleInstance { + def apply(module: Module): ModuleInstance = { + val types = List() + val funcs = module.definitions + .collect({ + case FuncDef(_, fndef @ FuncBodyDef(_, _, _, _)) => fndef + }) + .toList + + val globals = module.definitions + .collect({ + case Global(_, GlobalValue(ty, e)) => + (e.head) match { + case Const(c) => RTGlobal(ty, c) + // Q: What is the default behavior if case in non-exhaustive + case _ => ??? + } + }) + .toList + + // TODO: correct the behavior for memory + val memory = module.definitions + .collect({ + case Memory(id, MemoryType(min, max_opt)) => + RTMemory(min, max_opt) + }) + .toList + + val exports = module.definitions + .collect({ + case e @ Export(_, ExportFunc(_)) => e + }) + .toList + + ModuleInstance(types, module.funcEnv, memory, globals, exports) + } +} + object Primtives { def evalBinOp(op: BinOp, lhs: Value, rhs: Value): Value = op match { case Add(_) => @@ -391,33 +429,7 @@ object Evaluator { if (instrs.isEmpty) println("Warning: nothing is executed") - val types = List() - val funcs = module.definitions - .collect({ - case FuncDef(_, fndef @ FuncBodyDef(_, _, _, _)) => fndef - }) - .toList - - val globals = module.definitions - .collect({ - case Global(_, GlobalValue(ty, e)) => - (e.head) match { - case Const(c) => RTGlobal(ty, c) - // Q: What is the default behavior if case in non-exhaustive - case _ => ??? - } - }) - .toList - - // TODO: correct the behavior for memory - val memory = module.definitions - .collect({ - case Memory(id, MemoryType(min, max_opt)) => - RTMemory(min, max_opt) - }) - .toList - - val moduleInst = ModuleInstance(types, module.funcEnv, memory, globals) + val moduleInst = ModuleInstance(module) Evaluator.eval(instrs, List(), Frame(moduleInst, ArrayBuffer(I32V(0))), halt, List(halt)) } diff --git a/src/main/scala/wasm/MiniWasmScript.scala b/src/main/scala/wasm/MiniWasmScript.scala index b35f960db..e01de9a5b 100644 --- a/src/main/scala/wasm/MiniWasmScript.scala +++ b/src/main/scala/wasm/MiniWasmScript.scala @@ -1,17 +1,43 @@ package gensym.wasm.miniwasmscript import gensym.wasm.miniwasm._ -import gensym.wasm.ast.{Script, Cmd} -import gensym.wasm.ast.AssertReturn -import gensym.wasm.ast.AssertTrap +import gensym.wasm.ast._ +import scala.collection.mutable sealed class ScriptRunner { - val instances: List[ModuleInstance] = List() + val instances: mutable.ListBuffer[ModuleInstance] = mutable.ListBuffer() + val instanceMap: mutable.Map[String, ModuleInstance] = mutable.Map() + + def getInstance(instName: Option[String]): ModuleInstance = { + instName match { + case Some(name) => instanceMap(name) + case None => instances.head + } + } + + def assertReturn(action: Action, expect: List[Value]): Unit = { + action match { + case Invoke(instName, name, args) => + val module = getInstance(instName) + val func = module.exports.collectFirst({ + case Export(`name`, ExportFunc(index)) => + module.funcs(index) + case _ => throw new RuntimeException("Not Supported") + }).get + val instrs = func match { + case FuncDef(_, FuncBodyDef(ty, _, locals, body)) => body + } + val k = (retStack: List[Value]) => retStack + val actual = Evaluator.eval(instrs, List(), Frame(module, mutable.ArrayBuffer(args: _*)), k, List(k)) + assert(actual == expect) + } + } def runCmd(cmd: Cmd): Unit = { cmd match { - case AssertReturn(action, results) => - case AssertTrap(action, message) => + case CmdModule(module) => instances.+=(ModuleInstance(module)) + case AssertReturn(action, expect) => assertReturn(action, expect) + case AssertTrap(action, message) => ??? } } From e1d5674c1ee09998be5468b7eb2e071863c84e4d Mon Sep 17 00:00:00 2001 From: butterunderflow Date: Thu, 31 Oct 2024 00:02:07 +0800 Subject: [PATCH 3/6] fix test --- src/test/scala/genwasym/TestScriptRun.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/genwasym/TestScriptRun.scala b/src/test/scala/genwasym/TestScriptRun.scala index d64f91e54..2c1e3f9d2 100644 --- a/src/test/scala/genwasym/TestScriptRun.scala +++ b/src/test/scala/genwasym/TestScriptRun.scala @@ -14,6 +14,6 @@ class TestScriptRun extends FunSuite { } test("simple script") { - testFile("./benchmarks/wasm/script/script_basic.wabt") + testFile("./benchmarks/wasm/script/script_basic.wast") } } From afc6775fe5ffa9bb7335cdf04ae56fad69805f26 Mon Sep 17 00:00:00 2001 From: butterunderflow Date: Thu, 31 Oct 2024 00:10:51 +0800 Subject: [PATCH 4/6] add wast test to ci --- .github/workflows/scala.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml index 6594dc8ae..a691b7444 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/scala.yml @@ -71,3 +71,4 @@ jobs: sbt 'testOnly gensym.TestImpCPSGS_Z3' sbt 'testOnly gensym.TestLibrary' sbt 'testOnly gensym.wasm.TestEval' + sbt 'testOnly gensym.wasm.TestScriptRun' From ac78e3bf23d2057f18f5b857bd66c4c43f46cf2a Mon Sep 17 00:00:00 2001 From: Guannan Wei Date: Thu, 31 Oct 2024 01:39:22 +0100 Subject: [PATCH 5/6] cosmetic stuff --- src/main/scala/wasm/MiniWasmScript.scala | 10 ++++----- src/main/scala/wasm/Parser.scala | 28 ++++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main/scala/wasm/MiniWasmScript.scala b/src/main/scala/wasm/MiniWasmScript.scala index e01de9a5b..26103f080 100644 --- a/src/main/scala/wasm/MiniWasmScript.scala +++ b/src/main/scala/wasm/MiniWasmScript.scala @@ -2,11 +2,11 @@ package gensym.wasm.miniwasmscript import gensym.wasm.miniwasm._ import gensym.wasm.ast._ -import scala.collection.mutable +import scala.collection.mutable.{ListBuffer, Map, ArrayBuffer} sealed class ScriptRunner { - val instances: mutable.ListBuffer[ModuleInstance] = mutable.ListBuffer() - val instanceMap: mutable.Map[String, ModuleInstance] = mutable.Map() + val instances: ListBuffer[ModuleInstance] = ListBuffer() + val instanceMap: Map[String, ModuleInstance] = Map() def getInstance(instName: Option[String]): ModuleInstance = { instName match { @@ -28,14 +28,14 @@ sealed class ScriptRunner { case FuncDef(_, FuncBodyDef(ty, _, locals, body)) => body } val k = (retStack: List[Value]) => retStack - val actual = Evaluator.eval(instrs, List(), Frame(module, mutable.ArrayBuffer(args: _*)), k, List(k)) + val actual = Evaluator.eval(instrs, List(), Frame(module, ArrayBuffer(args: _*)), k, List(k)) assert(actual == expect) } } def runCmd(cmd: Cmd): Unit = { cmd match { - case CmdModule(module) => instances.+=(ModuleInstance(module)) + case CmdModule(module) => instances += ModuleInstance(module) case AssertReturn(action, expect) => assertReturn(action, expect) case AssertTrap(action, message) => ??? } diff --git a/src/main/scala/wasm/Parser.scala b/src/main/scala/wasm/Parser.scala index 25972ff2f..3ab3047ec 100644 --- a/src/main/scala/wasm/Parser.scala +++ b/src/main/scala/wasm/Parser.scala @@ -605,21 +605,19 @@ class GSWasmVisitor extends WatParserBaseVisitor[WIR] { else if (ctx.MEMORY != null) ExportMemory(id) else if (ctx.GLOBAL != null) ExportGlobal(id) else error - } override def visitScriptModule(ctx: ScriptModuleContext): Module = { - if (ctx.module_() != null) { + if (ctx.module_ != null) { visitModule_(ctx.module_).asInstanceOf[Module] - } - else { + } else { throw new RuntimeException("Unsupported") } } override def visitAction_(ctx: Action_Context): Action = { - if (ctx.INVOKE() != null) { - val instName = if (ctx.VAR() != null) Some(ctx.VAR().getText) else None + if (ctx.INVOKE != null) { + val instName = if (ctx.VAR != null) Some(ctx.VAR().getText) else None var name = ctx.name.getText.substring(1).dropRight(1) var args = for (constCtx <- ctx.constList.wconst.asScala) yield { val Array(ty, _) = constCtx.CONST.getText.split("\\.") @@ -632,7 +630,7 @@ class GSWasmVisitor extends WatParserBaseVisitor[WIR] { } override def visitAssertion(ctx: AssertionContext): Assertion = { - if (ctx.ASSERT_RETURN() != null) { + if (ctx.ASSERT_RETURN != null) { val action = visitAction_(ctx.action_) val expect = for (constCtx <- ctx.constList.wconst.asScala) yield { val Array(ty, _) = constCtx.CONST.getText.split("\\.") @@ -640,20 +638,17 @@ class GSWasmVisitor extends WatParserBaseVisitor[WIR] { } println(s"expect = $expect") AssertReturn(action, expect.toList) - } - else { + } else { throw new RuntimeException("Unsupported") } } override def visitCmd(ctx: CmdContext): Cmd = { - if (ctx.assertion() != null) { + if (ctx.assertion != null) { visitAssertion(ctx.assertion) - } - else if (ctx.scriptModule() != null) { + } else if (ctx.scriptModule != null) { CmdModule(visitScriptModule(ctx.scriptModule)) - } - else { + } else { throw new RuntimeException("Unsupported") } } @@ -677,7 +672,7 @@ object Parser { def parse(input: String): Module = { val parser = makeWatVisitor(input) val visitor = new GSWasmVisitor() - val res: Module = visitor.visit(parser.module()).asInstanceOf[Module] + val res: Module = visitor.visit(parser.module).asInstanceOf[Module] res } @@ -689,8 +684,7 @@ object Parser { val visitor = new GSWasmVisitor() val tree = parser.script() val errorNumer = parser.getNumberOfSyntaxErrors() - if (errorNumer != 0) - None + if (errorNumer != 0) None else { val res: Script = visitor.visitScript(tree).asInstanceOf[Script] Some(res) From 9c697c80554e1e94b253d9a8338992cbd112a965 Mon Sep 17 00:00:00 2001 From: butterunderflow Date: Thu, 31 Oct 2024 19:09:35 +0800 Subject: [PATCH 6/6] remove TestSyntax.scala --- src/test/scala/genwasym/TestSyntax.scala | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/test/scala/genwasym/TestSyntax.scala diff --git a/src/test/scala/genwasym/TestSyntax.scala b/src/test/scala/genwasym/TestSyntax.scala deleted file mode 100644 index 8fecf874e..000000000 --- a/src/test/scala/genwasym/TestSyntax.scala +++ /dev/null @@ -1,17 +0,0 @@ -package gensym.wasm - -import gensym.wasm.parser.Parser -import org.scalatest.FunSuite - -class TestSyntax extends FunSuite { - def testFile(filename: String) = { - val script = Parser.parseScriptFile(filename) - println(s"script = $script") - assert(script != None, "this syntax is not defined in antlr grammar") - } - - test("basic script") { - testFile("./benchmarks/wasm/script/script_basic.wabt") - } -} -