From d1fe2b92ff24e79b3db2e89c936d07884d04e9e3 Mon Sep 17 00:00:00 2001 From: Lexi Bromfield Date: Thu, 30 Jul 2026 22:28:09 +0000 Subject: [PATCH] [wasm2c] Add support for i32 arithmetic and basic variables This change adds the bare minimum support needed to make the i32.wast spec test suite pass. --- scripts/test/wasm2c.py | 1 + src/tools/wasm2c/assertion-emitter.cpp | 200 +++++++++++++- src/tools/wasm2c/wasm2c-builder.cpp | 351 +++++++++++++++++++++++++ 3 files changed, 543 insertions(+), 9 deletions(-) diff --git a/scripts/test/wasm2c.py b/scripts/test/wasm2c.py index a5643f34703..8b44b0ad0ff 100644 --- a/scripts/test/wasm2c.py +++ b/scripts/test/wasm2c.py @@ -18,6 +18,7 @@ from .shared import print_heading spec_tests = [ + 'i32.wast', ] diff --git a/src/tools/wasm2c/assertion-emitter.cpp b/src/tools/wasm2c/assertion-emitter.cpp index 895a40fdb27..d4f905670ad 100644 --- a/src/tools/wasm2c/assertion-emitter.cpp +++ b/src/tools/wasm2c/assertion-emitter.cpp @@ -14,8 +14,12 @@ * limitations under the License. */ +#include #include #include +#include +#include +#include #include "parser/wat-parser.h" #include "support/file.h" @@ -50,6 +54,42 @@ inline std::string getBasename(const std::string& path) { return path.substr(lastSlash + 1); } +std::string mangleName(const std::string& name) { + if (name.empty()) { + return ""; + } + std::string result; + bool isFirst = true; + for (char c : name) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9')) { + result += c; + isFirst = false; + } else if (c == '_') { + if (isFirst) { + result += "0x5F"; + isFirst = false; + } else { + result += '_'; + } + } else { + char buf[8]; + snprintf(buf, sizeof(buf), "0x%02X", (unsigned char)c); + result += buf; + isFirst = false; + } + } + return result; +} + +std::string literalToCLiteral(const Literal& lit) { + if (lit.type == Type::i32) { + return std::to_string(static_cast(lit.geti32())) + "u"; + } + Fatal() << "Unsupported literal type for C emission: " << lit.type; + return ""; +} + } // anonymous namespace AssertionEmitter::AssertionEmitter(WATParser::WASTScript& script, @@ -64,7 +104,13 @@ void AssertionEmitter::emit(std::ostream& cOut, outputCPath.empty() ? "spec" : stripExtension(outputCPath); std::string baseBasename = getBasename(basePath); - // Loop sequentially through WASTScript AST commands + std::vector modulePrefixes; + std::unordered_map moduleNameToPrefix; + std::unordered_map> + moduleExports; + std::string lastModulePrefix; + + // First pass: Process modules and generate files for (size_t i = 0; i < script.size(); i++) { auto& entry = script[i]; auto& cmd = entry.cmd; @@ -79,6 +125,21 @@ void AssertionEmitter::emit(std::ostream& cOut, auto wasm = *w; size_t currentIdx = moduleCounter++; std::string prefix = "spec_" + std::to_string(currentIdx); + modulePrefixes.push_back(prefix); + lastModulePrefix = prefix; + + if (wasm->name.is()) { + moduleNameToPrefix[wasm->name] = prefix; + } + + // Collect exports + std::unordered_set exports; + for (auto& exp : wasm->exports) { + if (exp->kind == ExternalKind::Function) { + exports.insert(exp->name.toString()); + } + } + moduleExports[prefix] = exports; // Generate separate files for this module std::string modHFilename = @@ -98,19 +159,140 @@ void AssertionEmitter::emit(std::ostream& cOut, builder.processWasm(wasm.get(), modCOut.getStream(), modHOut.getStream()); c << "#include \"" << modHFilename << "\"" << endl; - c << SpecTop << endl << endl; - - } else if (std::get_if(&cmd)) { - Fatal() << "register is not yet supported"; - } else if (std::get_if(&cmd)) { - Fatal() << "assertions are not yet supported"; - } else { - Fatal() << "unsupported command"; } } + c << SpecTop << endl << endl; + + // Declare static instances + for (const auto& prefix : modulePrefixes) { + c << "static w2c_" << prefix << " instance_" << prefix << ";" << endl; + } + c << endl; + // Write main execution entry point c << "void run_spec_tests() {" << endl; + c.indent(); + + // Instantiate modules + for (const auto& prefix : modulePrefixes) { + c << "wasm2c_" << prefix << "_instantiate(&instance_" << prefix << ");" + << endl; + } + c << endl; + + // Process assertions + for (size_t i = 0; i < script.size(); i++) { + auto& entry = script[i]; + auto& cmd = entry.cmd; + + if (auto* assertCmd = std::get_if(&cmd)) { + if (auto* assertReturn = + std::get_if(assertCmd)) { + auto* invoke = + std::get_if(&assertReturn->action); + if (!invoke) { + Fatal() << "Only InvokeAction is supported in AssertReturn"; + } + + std::string activePrefix; + if (invoke->base.has_value()) { + auto it = moduleNameToPrefix.find(invoke->base.value()); + if (it != moduleNameToPrefix.end()) { + activePrefix = it->second; + } else { + Fatal() << "Unknown module reference: " << invoke->base.value(); + } + } else { + activePrefix = lastModulePrefix; + } + + // Verify export exists + auto expIt = moduleExports.find(activePrefix); + if (expIt == moduleExports.end() || + !expIt->second.count(invoke->name.toString())) { + Fatal() << "Invoked function is not exported: " << invoke->name; + } + + std::string callStr = "w2c_" + activePrefix + "_" + + mangleName(invoke->name.toString()) + + "(&instance_" + activePrefix; + for (const auto& arg : invoke->args) { + callStr += ", " + literalToCLiteral(arg); + } + callStr += ")"; + + if (assertReturn->expected.empty()) { + c << "ASSERT_RETURN(" << callStr << ");" << endl; + } else if (assertReturn->expected.size() == 1) { + auto& alts = assertReturn->expected[0]; + if (alts.size() != 1) { + Fatal() << "Multiple alternatives in expected result not supported"; + } + auto& expectedRes = alts[0]; + if (auto* lit = std::get_if(&expectedRes)) { + if (lit->type == Type::i32) { + c << "ASSERT_RETURN_I32(" << callStr << ", " + << literalToCLiteral(*lit) << ");" << endl; + } else { + Fatal() << "Unsupported expected result type: " << lit->type; + } + } else { + Fatal() << "Unsupported expected result kind"; + } + } else { + Fatal() << "Multi-value return assertions not supported"; + } + + } else if (auto* assertAction = + std::get_if(assertCmd)) { + if (assertAction->type != WATParser::ActionAssertionType::Trap) { + Fatal() << "Only Trap assertion is supported in AssertAction"; + } + auto* invoke = + std::get_if(&assertAction->action); + if (!invoke) { + Fatal() << "Only InvokeAction is supported in AssertAction"; + } + + std::string activePrefix; + if (invoke->base.has_value()) { + auto it = moduleNameToPrefix.find(invoke->base.value()); + if (it != moduleNameToPrefix.end()) { + activePrefix = it->second; + } else { + Fatal() << "Unknown module reference: " << invoke->base.value(); + } + } else { + activePrefix = lastModulePrefix; + } + + // Verify export exists + auto expIt = moduleExports.find(activePrefix); + if (expIt == moduleExports.end() || + !expIt->second.count(invoke->name.toString())) { + Fatal() << "Invoked function is not exported: " << invoke->name; + } + + std::string callStr = "w2c_" + activePrefix + "_" + + mangleName(invoke->name.toString()) + + "(&instance_" + activePrefix; + for (const auto& arg : invoke->args) { + callStr += ", " + literalToCLiteral(arg); + } + callStr += ")"; + + c << "ASSERT_TRAP(" << callStr << ");" << endl; + } + } + } + + // Free modules + for (const auto& prefix : modulePrefixes) { + c << "wasm2c_" << prefix << "_free(&instance_" << prefix << ");" << endl; + } + + c.outdent(); c << "}" << endl; } diff --git a/src/tools/wasm2c/wasm2c-builder.cpp b/src/tools/wasm2c/wasm2c-builder.cpp index 9b6b2180bc5..d2c91753f3b 100644 --- a/src/tools/wasm2c/wasm2c-builder.cpp +++ b/src/tools/wasm2c/wasm2c-builder.cpp @@ -14,11 +14,18 @@ * limitations under the License. */ +#include +#include #include +#include #include +#include +#include #include "tools/wasm2c/c-printer.h" #include "tools/wasm2c/wasm2c-builder.h" +#include "wasm-stack.h" +#include "wasm-traversal.h" #include "wasm.h" // code to be inserted into the generated output @@ -59,6 +66,214 @@ std::string mangleName(const std::string& name) { return result; } +struct FunctionCompiler : public Visitor { + CPrinter& c; + Function* func; + std::string moduleName; + + // Virtual stack + size_t stackDepth = 0; + size_t maxStackDepth = 0; + + FunctionCompiler(CPrinter& c, Function* func, const std::string& moduleName) + : c(c), func(func), moduleName(moduleName) {} + + void push() { + stackDepth++; + if (stackDepth > maxStackDepth) { + maxStackDepth = stackDepth; + } + } + + void pop(size_t n = 1) { + assert(stackDepth >= n); + stackDepth -= n; + } + + std::string stackVar(size_t index) { return "i" + std::to_string(index); } + + std::string top() { + assert(stackDepth > 0); + return stackVar(stackDepth - 1); + } + + std::string popVal() { + std::string val = top(); + pop(); + return val; + } + + void pushVal(const std::string& val) { + push(); + c << top() << " = " << val << ";" << endl; + } + + // Visitor methods + void visitLocalGet(LocalGet* curr) { + std::string name; + if (func->isParam(curr->index)) { + name = "var_p" + std::to_string(curr->index); + } else { + name = "var_l" + std::to_string(curr->index - func->getNumParams()); + } + pushVal(name); + } + + void visitLocalSet(LocalSet* curr) { + std::string name; + if (func->isParam(curr->index)) { + name = "var_p" + std::to_string(curr->index); + } else { + name = "var_l" + std::to_string(curr->index - func->getNumParams()); + } + if (curr->isTee()) { + c << name << " = " << top() << ";" << endl; + } else { + c << name << " = " << popVal() << ";" << endl; + } + } + + void visitConst(Const* curr) { + if (curr->type == Type::i32) { + pushVal(std::to_string(curr->value.geti32()) + "u"); + } else { + Fatal() << "Unsupported const type: " << curr->type; + } + } + + void visitUnary(Unary* curr) { + if (curr->type == Type::i32) { + std::string val = popVal(); + switch (curr->op) { + case ClzInt32: + pushVal("I32_CLZ(" + val + ")"); + break; + case CtzInt32: + pushVal("I32_CTZ(" + val + ")"); + break; + case PopcntInt32: + pushVal("I32_POPCNT(" + val + ")"); + break; + case EqZInt32: + pushVal(val + " == 0"); + break; + case ExtendS8Int32: + pushVal("(uint32_t)(int32_t)(int8_t)" + val); + break; + case ExtendS16Int32: + pushVal("(uint32_t)(int32_t)(int16_t)" + val); + break; + default: + Fatal() << "Unsupported unary op: " << curr->op; + } + } else { + Fatal() << "Unsupported unary type: " << curr->type; + } + } + + void visitBinary(Binary* curr) { + std::string right = popVal(); + std::string left = popVal(); + + if (curr->left->type == Type::i32 && curr->right->type == Type::i32) { + switch (curr->op) { + case AddInt32: + pushVal(left + " + " + right); + break; + case SubInt32: + pushVal(left + " - " + right); + break; + case MulInt32: + pushVal(left + " * " + right); + break; + case DivSInt32: + pushVal("I32_DIV_S(" + left + ", " + right + ")"); + break; + case DivUInt32: + pushVal("DIV_U(" + left + ", " + right + ")"); + break; + case RemSInt32: + pushVal("I32_REM_S(" + left + ", " + right + ")"); + break; + case RemUInt32: + pushVal("REM_U(" + left + ", " + right + ")"); + break; + case AndInt32: + pushVal(left + " & " + right); + break; + case OrInt32: + pushVal(left + " | " + right); + break; + case XorInt32: + pushVal(left + " ^ " + right); + break; + case ShlInt32: + pushVal(left + " << (" + right + " & 31)"); + break; + case ShrSInt32: + pushVal("(uint32_t)((int32_t)" + left + " >> (" + right + " & 31))"); + break; + case ShrUInt32: + pushVal(left + " >> (" + right + " & 31)"); + break; + case RotLInt32: + pushVal("I32_ROTL(" + left + ", " + right + ")"); + break; + case RotRInt32: + pushVal("I32_ROTR(" + left + ", " + right + ")"); + break; + + // Relational + case EqInt32: + pushVal(left + " == " + right); + break; + case NeInt32: + pushVal(left + " != " + right); + break; + case LtSInt32: + pushVal("(int32_t)" + left + " < (int32_t)" + right); + break; + case LtUInt32: + pushVal(left + " < " + right); + break; + case LeSInt32: + pushVal("(int32_t)" + left + " <= (int32_t)" + right); + break; + case LeUInt32: + pushVal(left + " <= " + right); + break; + case GtSInt32: + pushVal("(int32_t)" + left + " > (int32_t)" + right); + break; + case GtUInt32: + pushVal(left + " > " + right); + break; + case GeSInt32: + pushVal("(int32_t)" + left + " >= (int32_t)" + right); + break; + case GeUInt32: + pushVal(left + " >= " + right); + break; + + default: + Fatal() << "Unsupported binary op: " << curr->op; + } + } else { + Fatal() << "Unsupported binary operand types"; + } + } + + void visitDrop(Drop* curr) { pop(); } + + void visitReturn(Return* curr) { + if (curr->value) { + c << "return " << popVal() << ";" << endl; + } else { + c << "return;" << endl; + } + } +}; + } // anonymous namespace void Wasm2CBuilder::processWasm(Module* wasm, @@ -91,6 +306,14 @@ void Wasm2CBuilder::processWasm(Module* wasm, } c << SourceDeclarations << endl; + // Track exported functions + std::unordered_map exportedFunctions; + for (auto& exp : wasm->exports) { + if (exp->kind == ExternalKind::Function) { + exportedFunctions[*exp->getInternalName()] = exp->name.toString(); + } + } + // Structure context definition h << "typedef struct w2c_" << moduleName << " {" << endl; h.indent(); @@ -98,6 +321,48 @@ void Wasm2CBuilder::processWasm(Module* wasm, h.outdent(); h << "} w2c_" << moduleName << ";" << endl << endl; + // Generate declarations in .h and static declarations in .c + bool printedHeaderDecl = false; + bool printedSourceDecl = false; + for (auto& func : wasm->functions) { + if (func->imported()) { + continue; + } + + std::string internalName = mangleName(func->name.toString()); + std::string resType = func->getResults() == Type::i32 ? "uint32_t" : "void"; + if (func->getResults() != Type::i32 && func->getResults() != Type::none) { + Fatal() << "Unsupported result type: " << func->getResults(); + } + + std::string paramsSig = "w2c_" + moduleName + "*"; + for (size_t i = 0; i < func->getNumParams(); i++) { + if (func->getLocalType(i) != Type::i32) { + Fatal() << "Unsupported param type: " << func->getLocalType(i); + } + paramsSig += ", uint32_t"; + } + + // Static declaration in .c + c << "static " << resType << " w2c_" << moduleName << "_" << internalName + << "_impl(" << paramsSig << ");" << endl; + printedSourceDecl = true; + + // Non-static declaration in .h if exported + if (exportedFunctions.count(func->name)) { + std::string exportedName = mangleName(exportedFunctions[func->name]); + h << resType << " w2c_" << moduleName << "_" << exportedName << "(" + << paramsSig << ");" << endl; + printedHeaderDecl = true; + } + } + if (printedHeaderDecl) { + h << endl; + } + if (printedSourceDecl) { + c << endl; + } + // Lifecycle signatures in header h << "void wasm2c_" << moduleName << "_instantiate(w2c_" << moduleName << "*"; h << ");" << endl; @@ -132,6 +397,92 @@ void Wasm2CBuilder::processWasm(Module* wasm, c.outdent(); c << "}" << endl << endl; + // Generate function definitions + PassOptions options; + ModuleStackIR moduleStackIR(*wasm, options); + + for (auto& func : wasm->functions) { + if (func->imported()) { + continue; + } + + std::string internalName = mangleName(func->name.toString()); + std::string resType = func->getResults() == Type::i32 ? "uint32_t" : "void"; + std::string paramsDecl = "w2c_" + moduleName + "* instance"; + std::string paramsCall = "instance"; + for (size_t i = 0; i < func->getNumParams(); i++) { + paramsDecl += ", uint32_t var_p" + std::to_string(i); + paramsCall += ", var_p" + std::to_string(i); + } + + // Wrapper definition (non-static) if exported + if (exportedFunctions.count(func->name)) { + std::string exportedName = mangleName(exportedFunctions[func->name]); + c << resType << " w2c_" << moduleName << "_" << exportedName << "(" + << paramsDecl << ") {" << endl; + c.indent(); + if (resType != "void") { + c << "return "; + } + c << "w2c_" << moduleName << "_" << internalName << "_impl(" << paramsCall + << ");" << endl; + c.outdent(); + c << "}" << endl << endl; + } + + // Actual implementation (static) + c << "static " << resType << " w2c_" << moduleName << "_" << internalName + << "_impl(" << paramsDecl << ") {" << endl; + c.indent(); + + StackIR* stackIR = moduleStackIR.getStackIROrNull(func.get()); + if (!stackIR) { + Fatal() << "Failed to generate Stack IR for function " << func->name; + } + + // Compile body to a stringstream first to count stack depth and locals + std::stringstream bodyStream; + CPrinter bodyPrinter(bodyStream); + bodyPrinter.indent(); // Match indentation + FunctionCompiler compiler(bodyPrinter, func.get(), moduleName); + + for (auto* inst : *stackIR) { + if (!inst) { + continue; + } + if (inst->op == StackInst::Basic) { + compiler.visit(inst->origin); + } else { + Fatal() << "Unsupported StackInst op: " << inst->op; + } + } + + // Declare virtual stack variables + for (size_t i = 0; i < compiler.maxStackDepth; i++) { + c << "uint32_t i" << i << ";" << endl; + } + // Declare locals + for (size_t i = 0; i < func->getNumVars(); i++) { + if (func->vars[i] != Type::i32) { + Fatal() << "Unsupported local type: " << func->vars[i]; + } + c << "uint32_t var_l" << i << " = 0u;" << endl; // Initialize to 0 + } + + // Append body + c << bodyStream.str(); + + // Return value + if (resType != "void") { + if (compiler.stackDepth > 0) { + c << "return " << compiler.top() << ";" << endl; + } + } + + c.outdent(); + c << "}" << endl << endl; + } + // Header file suffix h << endl; h << HeaderBottom << endl;