diff --git a/src/engine/BinParser.v3 b/src/engine/BinParser.v3 index 873fbf18e..515e55d03 100644 --- a/src/engine/BinParser.v3 +++ b/src/engine/BinParser.v3 @@ -113,6 +113,11 @@ class BinParser(extensions: Extension.set, limits: Limits, err: ErrorGen, filena } Start => readStartSection(); DataCount => readDataCountSection(); + Environment => { + if (!extensions.JIT_INTERFACE) return err.at(d).InvalidSectionCode(kind.code); + var count = readCountAndReserve("environments", module.environments, limits.max_num_environments); + readLoop("environments", count, readEnvironDecl, if(outline != null, outline.environments.set(startAddr, size))); + } _ => return err.at(d).InvalidSectionCode(kind.code); } if (Trace.binparse) { @@ -692,6 +697,47 @@ class BinParser(extensions: Extension.set, limits: Limits, err: ErrorGen, filena var d = DataDecl.new(mode, data); module.data.put(d); } + def readEnvironDecl(index: int) { + var pt = decoder.pos; + var flags = parser.readByte("flags", BpConstants.renderNone); + if (flags != 0) err.rel(decoder, pt + 1).ExpectedEnvironFlagsZeroByte(flags); + var environ = EnvironDecl.new(module, extensions, limits); + environ.module = Module.new(Strings.format2("%s.env[%d]", module.filename, index)); + module.addDecl(environ); + var count = readCountAndReserve("imports", environ.mapping, limits.max_num_imports); + readLoop("imports", count, readEnvironImportDecl(environ, _), null); + } + def readEnvironImportDecl(environ: EnvironDecl, index: int) { + var pt = decoder.pos; + var kind = parser.readByte("import kind", BpConstants.renderImportKind); + match (kind) { + BpImportExportKind.Function.code => { + var func = parser.readFuncRef(); + if (func != null) environ.refOuterFunc(func); + } + BpImportExportKind.Table.code => { + var table = parser.readTableRef(); + if (table != null) environ.refOuterTable(table); + } + BpImportExportKind.Memory.code => { + var mem = parser.readMemoryRef(extensions.MULTI_MEMORY); + if (mem != null) environ.refOuterMemory(mem); + } + BpImportExportKind.Global.code => { + var global = parser.readGlobalRef(); + if (global != null) environ.refOuterGlobal(global); + } + BpImportExportKind.Tag.code => { + var tag = parser.readTagRef(); + if (tag != null) environ.refOuterTag(tag); + } + BpImportExportKind.HeapType.code => { + var ty = parser.readHeapTypeRef(); + if (ty != null) environ.module.addDecl(ty); + } + _ => err.rel(decoder, pt).InvalidImportKind(kind); + } + } def checkElemsType(pt: int, table: TableDecl, vt: ValueType) { if (table == null) return; if (ValueTypes.isAssignable(vt, table.elemtype)) return; diff --git a/src/engine/BpConstants.v3 b/src/engine/BpConstants.v3 index 1cdbf5c79..0c732441b 100644 --- a/src/engine/BpConstants.v3 +++ b/src/engine/BpConstants.v3 @@ -15,6 +15,7 @@ enum BpSection(code: byte) { Start(8), Element(9), DataCount(12), + Environment(14), Code(10), Data(11), } diff --git a/src/engine/BytecodeIterator.v3 b/src/engine/BytecodeIterator.v3 index 37af6878d..48e012b00 100644 --- a/src/engine/BytecodeIterator.v3 +++ b/src/engine/BytecodeIterator.v3 @@ -186,6 +186,7 @@ class BytecodeIterator { def read_ARRAYT = read_uleb; def read_HEAPT = read_ileb; def read_SIG = read_uleb; + def read_ENV = read_uleb; def read_FUNC = read_uleb; def read_FIELD = read_uleb; def read_TAG = read_uleb; @@ -430,6 +431,7 @@ class BytecodeIterator { ARRAY_INIT_ELEM => v.visit_ARRAY_INIT_ELEM(read_ARRAYT(), read_ELEM()); ANY_CONVERT_EXTERN => v.visit_ANY_CONVERT_EXTERN(); EXTERN_CONVERT_ANY => v.visit_EXTERN_CONVERT_ANY(); + FUNC_NEW => { read_ZEROB(); v.visit_FUNC_NEW(read_MEMORY(), read_SIG(), read_ENV()); } I32_TRUNC_SAT_F32_S => v.visit_I32_TRUNC_SAT_F32_S(); I32_TRUNC_SAT_F32_U => v.visit_I32_TRUNC_SAT_F32_U(); I32_TRUNC_SAT_F64_S => v.visit_I32_TRUNC_SAT_F64_S(); diff --git a/src/engine/CodePtr.v3 b/src/engine/CodePtr.v3 index ceec5e3d6..76c722a8a 100644 --- a/src/engine/CodePtr.v3 +++ b/src/engine/CodePtr.v3 @@ -203,7 +203,8 @@ class CodePtr extends DataReader { U32, I32, I64, - CONT_INDEX => skip_leb(); + CONT_INDEX, + ENV_INDEX => skip_leb(); BLOCK_TYPE => skip_block_type(); VALUE_TYPES => skip_value_types(); LABELS => { diff --git a/src/engine/CodeValidator.v3 b/src/engine/CodeValidator.v3 index 74209c9c4..4eb0932ff 100644 --- a/src/engine/CodeValidator.v3 +++ b/src/engine/CodeValidator.v3 @@ -1002,6 +1002,22 @@ class CodeValidator(extensions: Extension.set, limits: Limits, module: Module, e var et = if(nullable, ValueTypes.EXTERNREF, ValueTypes.EXTERNREF_NONNULL); push(et); } + FUNC_NEW => { + if (!checkExtension(Extension.JIT_INTERFACE, opcode)) return; + var flags_pos = codeptr.pos; + var flags = codeptr.read1(); + if (flags != 0) err_atpos(flags_pos).ExpectedFuncNewZeroByte(flags); + var mem = parser.readMemoryRef(extensions.MULTI_MEMORY); + if (mem == null) return; + var sig = parser.readSigRef(); + if (sig == null) return; + var env = parser.readEnvRef(); + if (env == null) return; + var it = mem.size.indexType(); + popE(it); + popE(it); + push(ValueType.Ref(false, HeapType.Func(sig))); + } I32_TRUNC_SAT_F32_S => checkSignature(Opcode.I32_TRUNC_SAT_F32_S.sig); I32_TRUNC_SAT_F32_U => checkSignature(Opcode.I32_TRUNC_SAT_F32_U.sig); I32_TRUNC_SAT_F64_S => checkSignature(Opcode.I32_TRUNC_SAT_F64_S.sig); diff --git a/src/engine/Extension.v3 b/src/engine/Extension.v3 index 4ab13efc8..1a5872d6d 100644 --- a/src/engine/Extension.v3 +++ b/src/engine/Extension.v3 @@ -17,6 +17,7 @@ enum Extension(short_name: string, help: string) { EXTENDED_CONST("extended-const", "Extended constant expressions"), RELAXED_SIMD("relaxed-simd", "Relaxed SIMD"), WASM_3_0("wasm-3.0", "All Wasm 3.0 features"), + JIT_INTERFACE("jit-interface", "Runtime code generation with func.new"), WIZENG("wizeng", "Wizard-specific engine capabilities"), } component Extensions { @@ -50,6 +51,7 @@ component Extensions { set |= Extension.EXCEPTION_HANDLING; set |= Extension.EXTENDED_CONST; } + if (set.JIT_INTERFACE) set |= Extension.FUNCTION_REFERENCES; // func.new produces a (ref $ft) if (set.GC) set |= Extension.FUNCTION_REFERENCES; if (set.FUNCTION_REFERENCES) set |= Extension.TAIL_CALL; if (set.EXCEPTION_HANDLING) set |= Extension.TAIL_CALL; diff --git a/src/engine/Instance.v3 b/src/engine/Instance.v3 index dea08298f..358d9c77f 100644 --- a/src/engine/Instance.v3 +++ b/src/engine/Instance.v3 @@ -79,6 +79,7 @@ class Instance(module: Module, imports: Array) { def dropped_elems = Array.new(module.elems.length); def elems = Array>.new(module.elems.length); def dropped_data = Array.new(module.data.length); + def environments = Array.new(module.environments.length); def getFunctionAsVal(func_index: int) -> Value.Ref { return Value.Ref(functions[func_index]); diff --git a/src/engine/Instantiator.v3 b/src/engine/Instantiator.v3 index ba1bb0594..cb811b003 100644 --- a/src/engine/Instantiator.v3 +++ b/src/engine/Instantiator.v3 @@ -128,6 +128,22 @@ class Instantiator(extensions: Extension.set, module: Module, var imports: Array Continuation(index) => { // TODO-SS: implement } + Environment(index) => { + // Map the environment's inner module's imports to the outer declarations. + var env = module.environments[index]; + var inner = Instance.new(env.module, Array.new(env.module.imports.length)); + instance.environments[index] = inner; + for (i < env.mapping.length) { + var t = env.mapping[i], d = t.0, nd = t.1; + match (nd) { + x: FuncDecl => inner.functions[x.func_index] = instance.functions[FuncDecl.!(d).func_index]; + x: TableDecl => inner.tables[x.table_index] = instance.tables[TableDecl.!(d).table_index]; + x: MemoryDecl => inner.memories[x.memory_index] = instance.memories[MemoryDecl.!(d).memory_index]; + x: GlobalDecl => inner.globals[x.global_index] = instance.globals[GlobalDecl.!(d).global_index]; + x: TagDecl => inner.tags[x.tag_index] = instance.tags[TagDecl.!(d).tag_index]; + } + } + } } } if (error.error()) return null; diff --git a/src/engine/Limits.v3 b/src/engine/Limits.v3 index 6585b28d3..19491946a 100644 --- a/src/engine/Limits.v3 +++ b/src/engine/Limits.v3 @@ -31,6 +31,7 @@ class Limits { var max_grow_size = 1000000; var max_supertypes = 1u; var max_log2_page_size = 16u; + var max_num_environments = 100000u; var ok_memory_flags = BpMemoryFlag.HasMax.mask | BpMemoryFlag.Shared.mask; var ok_table_flags = BpTableFlag.HasMax.mask; diff --git a/src/engine/Module.v3 b/src/engine/Module.v3 index 90d85a4ae..84acdb813 100644 --- a/src/engine/Module.v3 +++ b/src/engine/Module.v3 @@ -18,6 +18,7 @@ class Module(filename: string) { def elems = Vector.new(); def data = Vector.new(); def custom_sections = Vector.new(); + def environments = Vector.new(); var probes: Array>; var dyn_probes: Vector<(int, int, Probe)>; var outline: ModuleOutline; @@ -86,6 +87,11 @@ class Module(filename: string) { decls.put(Decl2.Continuation(index)); heaptypes.put(x); } + x: EnvironDecl => { + var index = environments.length; + decls.put(Decl2.Environment(x.environ_index = index)); + environments.put(x); + } } } // Add a new import declaration to this module. Adds this declaration to {imports} as well. @@ -293,6 +299,24 @@ type SegmentMode { class DataDecl(mode: SegmentMode, data: Array) { } +// An environment is a nested submodule that refers to members in the outer module. +class EnvironDecl(outer: Module, extensions: Extension.set, limits: Limits) extends Decl { + def mapping = Vector<(Decl, Decl)>.new(); // maps outer decl to inner decl + var environ_index = -1; + var module: Module; // inner module + var validator: CodeValidator; // runtime cache + + def refOuterFunc(x: FuncDecl) { var nf = FuncDecl.new(x.sig_index); refOuter(x, nf); nf.sig = x.sig; } + def refOuterTable(x: TableDecl) { refOuter(x, TableDecl.new(x.elemtype, x.size)); } + def refOuterMemory(x: MemoryDecl) { refOuter(x, MemoryDecl.new(x.size, x.shared, x.log2_pageSize)); } + def refOuterGlobal(x: GlobalDecl) { refOuter(x, GlobalDecl.new(x.valtype, x.mutable, x.init)); } + def refOuterTag(x: TagDecl) { var nt = TagDecl.new(x.sig_index); refOuter(x, nt); nt.fields = x.fields; } + private def refOuter(d: Decl, nd: Decl) { + mapping.put(d, nd); + module.addImport(outer.name, null, nd); + } +} + // An uninterpreted custom section. class CustomSection(name: string, payload: Array) { } @@ -366,6 +390,7 @@ type Decl2 { case Global(index: int); case Tag(index: int); case Continuation(index: int); + case Environment(index: int); } // Stores handler information for efficient runtime lookup. @@ -391,6 +416,7 @@ class ModuleOutline { def elements = SectionOutline.new(); def data = SectionOutline.new(); def custom_sections = Vector<(u64, u64)>.new(); + def environments = SectionOutline.new(); } // For sections that have repeated entries, this helper simplifies getting a range of // a specific entry. diff --git a/src/engine/Opcodes.v3 b/src/engine/Opcodes.v3 index b0a78d735..f3e45e4a4 100644 --- a/src/engine/Opcodes.v3 +++ b/src/engine/Opcodes.v3 @@ -255,6 +255,7 @@ enum Opcode(prefix: u8, code: u16, mnemonic: string, imms: Array, sig: REF_I31 (0xFB, 0x1C, "ref.i31", imm.NONE, null), // XXX: could have sig I31_GET_S (0xFB, 0x1D, "i31.get_s", imm.NONE, null), // XXX: could have sig I31_GET_U (0xFB, 0x1E, "i31.get_u", imm.NONE, null), // XXX: could have sig + FUNC_NEW (0xFB, 0x27, "func.new", imm.ZERO_MEMORY_SIG_ENV, null), // 0xFC prefix: saturating truncations. I32_TRUNC_SAT_F32_S (0xFC, 0x00, "i32.trunc_sat_f32_s", imm.NONE, sig.f_i), I32_TRUNC_SAT_F32_U (0xFC, 0x01, "i32.trunc_sat_f32_u", imm.NONE, sig.f_i), @@ -642,8 +643,9 @@ enum ImmKind { BR_CAST, // BR_CAST CATCHES, // CATCH CONT_INDEX, // CONT - EX_HANDLERS // EX_HANDLERS - SUS_HANDLERS // SUS_HANDLERS + EX_HANDLERS, // EX_HANDLERS + SUS_HANDLERS, // SUS_HANDLERS + ENV_INDEX } // Cached immediate signatures component ImmSigs { @@ -695,6 +697,7 @@ component ImmSigs { def CONT_HANDLE = [ImmKind.CONT_INDEX, ImmKind.SUS_HANDLERS]; def CONT_TAG_HANDLE = [ImmKind.CONT_INDEX, ImmKind.TAG_INDEX, ImmKind.SUS_HANDLERS]; def CONT_TAG = [ImmKind.CONT_INDEX, ImmKind.TAG_INDEX]; + def ZERO_MEMORY_SIG_ENV = [ImmKind.ZERO_BYTE, ImmKind.MEMORY_INDEX, ImmKind.SIG_INDEX, ImmKind.ENV_INDEX]; } // Internal opcodes used by the interpreter. @@ -1212,6 +1215,7 @@ class InstrTracer { TAG_INDEX => out.put1("tag=%d", codeptr.read_uleb32()); DATA_INDEX => out.put1("data=%d", codeptr.read_uleb32()); LANE_INDEX => out.put1("lane=%d", codeptr.read1()); + ENV_INDEX => out.put1("env=%d", codeptr.read_uleb32()); U32 => out.put1("%d", codeptr.read_uleb32()); I32 => out.put1("%d", codeptr.read_sleb32()); I64 => out.put1("%d", codeptr.read_sleb64()); diff --git a/src/engine/Runtime.v3 b/src/engine/Runtime.v3 index b306bfb07..0328191fd 100644 --- a/src/engine/Runtime.v3 +++ b/src/engine/Runtime.v3 @@ -223,6 +223,42 @@ component Runtime { if (!r) return stack.trap(TrapReason.ARRAY_OOB); return null; } + def FUNC_NEW(stack: ExecStack, outer: Instance, mem_index: u31, sig_index: u31, env_index: u31) -> Throwable { + // Get new code from outer module's memory. + var memory = outer.memories[mem_index], sc = memory.decl.size; + var size = stack.popa(sc); + var src = stack.popa(sc); + var r = memory.range_ol_64(src, size).then(Ranges.dup); // copy the code to avoid data races if multiple threads + if (r.reason != TrapReason.NONE) return stack.trap(r.reason); // trap if out-of-bounds + var instance = outer.environments[env_index], module = instance.module; + // Create a new function. + var func = FuncDecl.new(sig_index); + func.setOrigCode(r.result); + func.sig = SigDecl.!(outer.heaptypes[sig_index]); + func.func_index = module.functions.length + module.new_funcs++; + // Get the environment and create or use cached CodeValidator. + var env = outer.module.environments[env_index], validator = env.validator; + if (validator == null) { + env.validator = validator = CodeValidator.new(env.extensions, env.limits, module, + ErrorGen.new(module.filename)); + } + var err = validator.err.reset(); + // Inform the tiering configuration of new code, prior to validation. + Execute.tiering.onFuncBody(module, u32.!(func.func_index), func.orig_bytecode, err); + // Run validation and handle errors. + match (validator.validateFunc(func, 0)) { + Ok => ; + Error(error_code, error_pos, error_msg) => { +// TODO Trace.OUT.put3("error[%s]: %s @ +%d", error_code.name, error_msg, error_pos).ln(); + return stack.trap(TrapReason.FUNC_INVALID); // trap if validation failed + } + } + // Create a new function instance wrapping the innner instance. + var wf = WasmFunction.new(instance, func); + Execute.tiering.onNewFunction(wf, err); // inform tiering a new function is validated and finished + stack.push(Value.Ref(wf)); // return funcref result + return null; + } // --- Table operations ----------------------------------------------------------------------------------------- def TABLE_INIT(stack: ExecStack, instance: Instance, elem_index: u31, table_index: u31) -> Throwable { var evals = instance.getElems(elem_index); diff --git a/src/engine/WasmParser.v3 b/src/engine/WasmParser.v3 index 86f6dc0fc..a5d024b07 100644 --- a/src/engine/WasmParser.v3 +++ b/src/engine/WasmParser.v3 @@ -214,6 +214,9 @@ class WasmParser(extensions: Extension.set, limits: Limits, module: Module, _ => return HeapType.ANY; } } + def readHeapTypeRef() -> HeapTypeDecl { + return readIndex("heap type", module.heaptypes); + } def readStructType() -> StructDecl { var pt = decoder.pos; var ht = readIndex("struct", module.heaptypes); @@ -239,6 +242,9 @@ class WasmParser(extensions: Extension.set, limits: Limits, module: Module, err.rel(decoder, pt).ExpectedArrayType(ht); return null; } + def readEnvRef() -> EnvironDecl { + return readIndex("environment", module.environments); + } def readU32_i(quantity: string, max: u32) -> int { return int.!(readU32(quantity, max)); } diff --git a/src/engine/v3/V3Interpreter.v3 b/src/engine/v3/V3Interpreter.v3 index 95c72b94f..674875b10 100644 --- a/src/engine/v3/V3Interpreter.v3 +++ b/src/engine/v3/V3Interpreter.v3 @@ -923,6 +923,13 @@ class V3Interpreter extends WasmStack { ANY_CONVERT_EXTERN, EXTERN_CONVERT_ANY => { // nop } + FUNC_NEW => { + codeptr.read1(); // skip reserved flags + var mem_index = codeptr.read_uleb31(); + var sig_index = codeptr.read_uleb31(); + var env_index = codeptr.read_uleb31(); + Runtime.FUNC_NEW(this, frame.func.instance, mem_index, sig_index, env_index); + } I32_TRUNC_SAT_F32_S => pushi(i32.truncf(popf())); I32_TRUNC_SAT_F32_U => pushu(u32.truncf(popf())); I32_TRUNC_SAT_F64_S => pushi(i32.truncd(popd())); diff --git a/src/engine/x86-64/X86_64Interpreter.v3 b/src/engine/x86-64/X86_64Interpreter.v3 index c34906ad7..1f0dba0b9 100644 --- a/src/engine/x86-64/X86_64Interpreter.v3 +++ b/src/engine/x86-64/X86_64Interpreter.v3 @@ -3005,6 +3005,22 @@ class X86_64InterpreterGen(ic: X86_64InterpreterCode, w: DataWriter) { restoreCallerIVars(); endHandler(); } + + // ext:jit-interface: {func.new} has a reserved zero byte and 3 LEBs, and can trap. + // TODO: r_tmp3 is clobbered by reading an LEB + bindHandler(Opcode.FUNC_NEW); { + computePcFromCurIp(); + asm.q.inc_r(r_ip); // skip reserved flags byte + genReadUleb32(r_tmp0); // memory index + genReadUleb32(r_tmp1); // signature index + genReadUleb32(r_tmp4); // environment index + masm.emit_get_curstack(xenv.tmp3); + saveCallerIVars(); + callRuntime(refRuntimeCall(RT.FUNC_NEW), + [r_tmp3, r_instance, r_tmp0, r_tmp1, r_tmp4], true); + restoreCallerIVars(); + endHandler(); + } } // helper method to move values from vsp to registers diff --git a/src/engine/x86-64/X86_64Target.v3 b/src/engine/x86-64/X86_64Target.v3 index 2f36db8b7..6dea11c15 100644 --- a/src/engine/x86-64/X86_64Target.v3 +++ b/src/engine/x86-64/X86_64Target.v3 @@ -252,6 +252,12 @@ class X86_64SpcStrategy extends X86_64ExecutionStrategy { if (!applyJitFilter(wf.instance.module, wf.decl, how)) return SpcResultForStub(wf, X86_64Spc.setInterpreterFallback(wf.decl), null); var module = wf.instance.module; + // A module that was never prepared for this target has no code region yet. This + // happens for the inner module of an environment, whose functions are created at + // runtime by {func.new}; allocate a region for it on demand. + if (module.target_module.spc_code == null) { + allocateCodeForModule(module, MINIMUM_CODE_SIZE + X86_64Spc.estimateCodeSizeFor(wf.decl)); + } var code = module.target_module.spc_code; var compiler = newCompiler(module.filename); // XXX: cache per-thread var masm = X86_64MacroAssembler.!(compiler.masm), w = masm.asm.w; diff --git a/src/modules/ReflectorModule.v3 b/src/modules/ReflectorModule.v3 index b7734a14a..34a5e5ab1 100644 --- a/src/modules/ReflectorModule.v3 +++ b/src/modules/ReflectorModule.v3 @@ -25,6 +25,7 @@ class ReflectorModule extends HostModule { ("get_num_exports", SigCache.v_i, get_num_exports), ("get_num_elements", SigCache.v_i, get_num_elements), ("get_num_data", SigCache.v_i, get_num_data), + ("get_num_environments", SigCache.v_i, get_num_environments), ("get_num_custom_sections", SigCache.v_i, get_num_custom_sections), ("get_import_index", SigCache.ii_i, get_import_index), ("read_module_path", SigCache.ii_i, read_module_path), @@ -43,6 +44,7 @@ class ReflectorModule extends HostModule { ("read_export_def", SigCache.iii_i, read_export_def), ("read_element_def", SigCache.iii_i, read_element_def), ("read_data_def", SigCache.iii_i, read_data_def), + ("read_environment_def", SigCache.iii_i, read_environment_def), ("read_custom_section", SigCache.iii_i, read_custom_section) ]) { map[t.0] = HostFunction.new(t.0, t.1, t.2); @@ -69,6 +71,7 @@ class ReflectorModule extends HostModule { def get_num_exports(args: Range) => i(module.outline.exports.offsets.length); def get_num_elements(args: Range) => i(module.outline.elements.offsets.length); def get_num_data(args: Range) => i(module.outline.data.offsets.length); + def get_num_environments(args: Range) => i(module.outline.environments.offsets.length); def get_num_custom_sections(args: Range) => i(module.outline.custom_sections.length); def get_import_index(args: Range) -> HostResult { @@ -100,6 +103,7 @@ class ReflectorModule extends HostModule { def read_export_def(args: Range) => readDeclFromSection(args, module.outline.exports); def read_element_def(args: Range) => readDeclFromSection(args, module.outline.elements); def read_data_def(args: Range) => readDeclFromSection(args, module.outline.data); + def read_environment_def(args: Range) => readDeclFromSection(args, module.outline.environments); def read_custom_section(args: Range) -> HostResult { var i = Values.unbox_i(args[0]); if (i < 0 || i >= module.outline.custom_sections.length) return THROW_OOB; diff --git a/src/util/BytecodeVisitor.v3 b/src/util/BytecodeVisitor.v3 index fbef4b056..d105b47dc 100644 --- a/src/util/BytecodeVisitor.v3 +++ b/src/util/BytecodeVisitor.v3 @@ -298,6 +298,7 @@ class BytecodeVisitor { def visit_REF_I31 () { visitGc(Opcode.REF_I31); } def visit_I31_GET_S () { visitGc(Opcode.I31_GET_S); } def visit_I31_GET_U () { visitGc(Opcode.I31_GET_U); } + def visit_FUNC_NEW (memory_index: u31, sig_index: u31, env_index: u31) { visitGc(Opcode.FUNC_NEW); } // FC prefix: saturating truncations. def visit_I32_TRUNC_SAT_F32_S () { visitConvert(Opcode.I32_TRUNC_SAT_F32_S); } def visit_I32_TRUNC_SAT_F32_U () { visitConvert(Opcode.I32_TRUNC_SAT_F32_U); } diff --git a/src/util/ErrorGen.v3 b/src/util/ErrorGen.v3 index b1c2b7cba..1782f0dc3 100644 --- a/src/util/ErrorGen.v3 +++ b/src/util/ErrorGen.v3 @@ -207,6 +207,14 @@ class ErrorGen(filename: string) { setc(WasmError.EXPECTED_ZERO_BYTE, Strings.format1("expected zero byte for atomic fence, got 0x%x", val)); } + def ExpectedEnvironFlagsZeroByte(flags: byte) { + setc(WasmError.EXPECTED_ZERO_BYTE, + Strings.format1("expected zero byte for environment flags, got 0x%x", flags)); + } + def ExpectedFuncNewZeroByte(flags: byte) { + setc(WasmError.EXPECTED_ZERO_BYTE, + Strings.format1("expected zero byte for func.new flags, got 0x%x", flags)); + } private def safeRender(ht: HeapTypeDecl) -> StringBuilder -> StringBuilder { // XXX: factor out if (ht == null) return StringBuilder.puts(_, ""); return ht.render; diff --git a/src/util/Whamm.v3 b/src/util/Whamm.v3 index ae1649d8b..467a3a643 100644 --- a/src/util/Whamm.v3 +++ b/src/util/Whamm.v3 @@ -642,6 +642,7 @@ class WhammVarBinder(wi: WhammInstance, err: ErrorGen) { CONT_INDEX => ; // TODO EX_HANDLERS => ; // TODO SUS_HANDLERS => ; // TODO + ENV_INDEX => vals.put(Values.box_u(parser.readU32("environment index", u32.max))); } } diff --git a/test/regress.failures.x86-64-linux.spc b/test/regress.failures.x86-64-linux.spc index c576607e4..4911dd843 100644 --- a/test/regress.failures.x86-64-linux.spc +++ b/test/regress.failures.x86-64-linux.spc @@ -25,3 +25,12 @@ test/regress/ext:legacy-eh/try_delegate1.bin.wast test/regress/ext:legacy-eh/try_delegate3.bin.wast test/regress/ext:legacy-eh/try_delegate4.bin.wast test/regress/ext:legacy-eh/try_delegate5.bin.wast + +test/regress/ext:jit-interface/func_new0.bin.wast +test/regress/ext:jit-interface/func_new_args.bin.wast +test/regress/ext:jit-interface/func_new_invalid_env.bin.wast +test/regress/ext:jit-interface/func_new_scope_func.bin.wast +test/regress/ext:jit-interface/func_new_scope_global.bin.wast +test/regress/ext:jit-interface/func_new_scope_memory.bin.wast +test/regress/ext:jit-interface/func_new_trap_invalid.bin.wast +test/regress/ext:jit-interface/func_new_trap_oob.bin.wast diff --git a/test/regress/ext:jit-interface/func_new0.bin.wast b/test/regress/ext:jit-interface/func_new0.bin.wast new file mode 100644 index 000000000..0cd0c2936 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new0.bin.wast @@ -0,0 +1,10 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\89\80\80\80\00\02\60" + "\00\01\7f\60\00\01\7f\03\82\80\80\80\00\01\00\05" + "\83\80\80\80\00\01\00\01\07\88\80\80\80\00\01\04" + "\6d\61\69\6e\00\00\0e\83\80\80\80\00\01\00\00\0a" + "\90\80\80\80\00\01\0e\00\41\10\41\04\fb\27\00\00" + "\01\00\14\01\0b\0b\8a\80\80\80\00\01\00\41\10\0b" + "\04\00\41\2a\0b" +) +(assert_return (invoke "main") (i32.const 42)) diff --git a/test/regress/ext:jit-interface/func_new0.wast b/test/regress/ext:jit-interface/func_new0.wast new file mode 100644 index 000000000..1572fbe6b --- /dev/null +++ b/test/regress/ext:jit-interface/func_new0.wast @@ -0,0 +1,11 @@ +(module + (type $ft (func (result i32))) + (memory 1) + (data (i32.const 16) "\00\41\2a\0b") ;; (func (result i32) (i32.const 42)) + (scope $s) + (func (export "main") (result i32) + (call_ref $ft + (func.new 0 $ft $s (i32.const 16) (i32.const 4))) + ) +) +(assert_return (invoke "main") (i32.const 42)) diff --git a/test/regress/ext:jit-interface/func_new_args.bin.wast b/test/regress/ext:jit-interface/func_new_args.bin.wast new file mode 100644 index 000000000..62724ad5b --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_args.bin.wast @@ -0,0 +1,11 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\8d\80\80\80\00\02\60" + "\02\7f\7f\01\7f\60\02\7f\7f\01\7f\03\82\80\80\80" + "\00\01\00\05\83\80\80\80\00\01\00\01\07\88\80\80" + "\80\00\01\04\6d\61\69\6e\00\00\0e\83\80\80\80\00" + "\01\00\00\0a\94\80\80\80\00\01\12\00\20\00\20\01" + "\41\10\41\04\fb\27\00\00\01\00\14\01\0b\0b\8a\80" + "\80\80\00\01\00\41\10\0b\04\00\20\01\0b" +) +(assert_return (invoke "main" (i32.const 11) (i32.const 22)) (i32.const 22)) +(assert_return (invoke "main" (i32.const 33) (i32.const 44)) (i32.const 44)) diff --git a/test/regress/ext:jit-interface/func_new_args.wast b/test/regress/ext:jit-interface/func_new_args.wast new file mode 100644 index 000000000..43b6c600d --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_args.wast @@ -0,0 +1,12 @@ +(module + (type $ft (func (param i32 i32) (result i32))) + (memory 1) + (data (i32.const 16) "\00\20\01\0b") ;; (func (param i32 i32) (result i32) (local.get 1)) + (scope $s) + (func (export "main") (param i32 i32) (result i32) + (call_ref $ft (local.get 0) (local.get 1) + (func.new 0 $ft $s (i32.const 16) (i32.const 4))) + ) +) +(assert_return (invoke "main" (i32.const 11) (i32.const 22)) (i32.const 22)) +(assert_return (invoke "main" (i32.const 33) (i32.const 44)) (i32.const 44)) diff --git a/test/regress/ext:jit-interface/func_new_invalid_env.bin.wast b/test/regress/ext:jit-interface/func_new_invalid_env.bin.wast new file mode 100644 index 000000000..a81d8fd97 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_invalid_env.bin.wast @@ -0,0 +1,11 @@ +(assert_invalid + (module binary + "\00\61\73\6d\01\00\00\00\01\88\80\80\80\00\02\60" + "\00\00\60\00\01\7f\03\82\80\80\80\00\01\00\05\83" + "\80\80\80\00\01\00\01\07\88\80\80\80\00\01\04\6d" + "\61\69\6e\00\00\0e\83\80\80\80\00\01\00\00\0a\8f" + "\80\80\80\00\01\0d\00\41\10\41\04\fb\27\00\00\01" + "\03\1a\0b" + ) + "unknown environment" +) diff --git a/test/regress/ext:jit-interface/func_new_invalid_env.wast b/test/regress/ext:jit-interface/func_new_invalid_env.wast new file mode 100644 index 000000000..6e1107c08 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_invalid_env.wast @@ -0,0 +1,13 @@ +(assert_invalid + (module + (type $mt (func)) + (type $ft (func (result i32))) + (memory 1) + (scope $s) + (func (export "main") + ;; environment index 3 is out of bounds + (drop (func.new 0 $ft 3 (i32.const 16) (i32.const 4))) + ) + ) + "unknown environment" +) diff --git a/test/regress/ext:jit-interface/func_new_scope_func.bin.wast b/test/regress/ext:jit-interface/func_new_scope_func.bin.wast new file mode 100644 index 000000000..13177a0cf --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_func.bin.wast @@ -0,0 +1,10 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\89\80\80\80\00\02\60" + "\00\01\7f\60\00\01\7f\03\83\80\80\80\00\02\00\00" + "\05\83\80\80\80\00\01\00\01\07\88\80\80\80\00\01" + "\04\6d\61\69\6e\00\00\0e\85\80\80\80\00\01\00\01" + "\00\01\0a\95\80\80\80\00\02\0e\00\41\10\41\04\fb" + "\27\00\00\01\00\14\01\0b\04\00\41\07\0b\0b\8a\80" + "\80\80\00\01\00\41\10\0b\04\00\10\00\0b" +) +(assert_return (invoke "main") (i32.const 7)) diff --git a/test/regress/ext:jit-interface/func_new_scope_func.wast b/test/regress/ext:jit-interface/func_new_scope_func.wast new file mode 100644 index 000000000..3b5907e9e --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_func.wast @@ -0,0 +1,13 @@ +(module + (type $ft (func (result i32))) + (memory 1) + (data (i32.const 16) "\00\10\00\0b") ;; (func (result i32) (call 0)) + (scope $s + (func $seven)) ;; only $seven is in scope, renumbered to index 0 + (func (export "main") (result i32) + (call_ref $ft + (func.new 0 $ft $s (i32.const 16) (i32.const 4))) + ) + (func $seven (result i32) (i32.const 7)) +) +(assert_return (invoke "main") (i32.const 7)) diff --git a/test/regress/ext:jit-interface/func_new_scope_global.bin.wast b/test/regress/ext:jit-interface/func_new_scope_global.bin.wast new file mode 100644 index 000000000..17d96c07b --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_global.bin.wast @@ -0,0 +1,11 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\89\80\80\80\00\02\60" + "\00\01\7f\60\00\01\7f\03\82\80\80\80\00\01\00\05" + "\83\80\80\80\00\01\00\01\06\87\80\80\80\00\01\7f" + "\00\41\d2\09\0b\07\88\80\80\80\00\01\04\6d\61\69" + "\6e\00\00\0e\85\80\80\80\00\01\00\01\03\00\0a\90" + "\80\80\80\00\01\0e\00\41\10\41\04\fb\27\00\00\01" + "\00\14\01\0b\0b\8a\80\80\80\00\01\00\41\10\0b\04" + "\00\23\00\0b" +) +(assert_return (invoke "main") (i32.const 1234)) diff --git a/test/regress/ext:jit-interface/func_new_scope_global.wast b/test/regress/ext:jit-interface/func_new_scope_global.wast new file mode 100644 index 000000000..30968e579 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_global.wast @@ -0,0 +1,13 @@ +(module + (type $ft (func (result i32))) + (memory 1) + (global $g i32 (i32.const 1234)) + (data (i32.const 16) "\00\23\00\0b") ;; (func (result i32) (global.get 0)) + (scope $s + (global $g)) + (func (export "main") (result i32) + (call_ref $ft + (func.new 0 $ft $s (i32.const 16) (i32.const 4))) + ) +) +(assert_return (invoke "main") (i32.const 1234)) diff --git a/test/regress/ext:jit-interface/func_new_scope_memory.bin.wast b/test/regress/ext:jit-interface/func_new_scope_memory.bin.wast new file mode 100644 index 000000000..3c034773b --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_memory.bin.wast @@ -0,0 +1,11 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\89\80\80\80\00\02\60" + "\00\01\7f\60\00\01\7f\03\82\80\80\80\00\01\00\05" + "\83\80\80\80\00\01\00\01\07\88\80\80\80\00\01\04" + "\6d\61\69\6e\00\00\0e\85\80\80\80\00\01\00\01\02" + "\00\0a\90\80\80\80\00\01\0e\00\41\10\41\07\fb\27" + "\00\00\01\00\14\01\0b\0b\96\80\80\80\00\02\00\41" + "\08\0b\04\44\33\22\11\00\41\10\0b\07\00\41\00\28" + "\02\08\0b" +) +(assert_return (invoke "main") (i32.const 0x11223344)) diff --git a/test/regress/ext:jit-interface/func_new_scope_memory.wast b/test/regress/ext:jit-interface/func_new_scope_memory.wast new file mode 100644 index 000000000..64b622750 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_scope_memory.wast @@ -0,0 +1,13 @@ +(module + (type $ft (func (result i32))) + (memory 1) + (data (i32.const 8) "\44\33\22\11") + (data (i32.const 16) "\00\41\00\28\02\08\0b") ;; (func (result i32) (i32.load offset=8 (i32.const 0))) + (scope $s + (memory 0)) + (func (export "main") (result i32) + (call_ref $ft + (func.new 0 $ft $s (i32.const 16) (i32.const 7))) + ) +) +(assert_return (invoke "main") (i32.const 0x11223344)) diff --git a/test/regress/ext:jit-interface/func_new_trap_invalid.bin.wast b/test/regress/ext:jit-interface/func_new_trap_invalid.bin.wast new file mode 100644 index 000000000..3d1d7e035 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_trap_invalid.bin.wast @@ -0,0 +1,10 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\88\80\80\80\00\02\60" + "\00\00\60\00\01\7f\03\82\80\80\80\00\01\00\05\83" + "\80\80\80\00\01\00\01\07\88\80\80\80\00\01\04\6d" + "\61\69\6e\00\00\0e\83\80\80\80\00\01\00\00\0a\8f" + "\80\80\80\00\01\0d\00\41\10\41\03\fb\27\00\00\01" + "\00\1a\0b\0b\89\80\80\80\00\01\00\41\10\0b\03\00" + "\7c\0b" +) +(assert_trap (invoke "main") "function did not validate") diff --git a/test/regress/ext:jit-interface/func_new_trap_invalid.wast b/test/regress/ext:jit-interface/func_new_trap_invalid.wast new file mode 100644 index 000000000..f3b3b124d --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_trap_invalid.wast @@ -0,0 +1,11 @@ +(module + (type $mt (func)) + (type $ft (func (result i32))) + (memory 1) + (data (i32.const 16) "\00\7c\0b") ;; (func (result i32) (i64.add)) -- does not validate + (scope $s) + (func (export "main") + (drop (func.new 0 $ft $s (i32.const 16) (i32.const 3))) + ) +) +(assert_trap (invoke "main") "function did not validate") diff --git a/test/regress/ext:jit-interface/func_new_trap_oob.bin.wast b/test/regress/ext:jit-interface/func_new_trap_oob.bin.wast new file mode 100644 index 000000000..bcf7448a2 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_trap_oob.bin.wast @@ -0,0 +1,14 @@ +(module binary + "\00\61\73\6d\01\00\00\00\01\8a\80\80\80\00\02\60" + "\02\7f\7f\00\60\00\01\7f\03\82\80\80\80\00\01\00" + "\05\83\80\80\80\00\01\00\01\07\88\80\80\80\00\01" + "\04\6d\61\69\6e\00\00\0e\83\80\80\80\00\01\00\00" + "\0a\8f\80\80\80\00\01\0d\00\20\00\20\01\fb\27\00" + "\00\01\00\1a\0b\0b\8a\80\80\80\00\01\00\41\10\0b" + "\04\00\41\2a\0b" +) +(assert_return (invoke "main" (i32.const 16) (i32.const 4))) +(assert_trap (invoke "main" (i32.const 65536) (i32.const 1)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const 65535) (i32.const 2)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const 0) (i32.const 65537)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const -1) (i32.const 1)) "out of bounds memory access") diff --git a/test/regress/ext:jit-interface/func_new_trap_oob.wast b/test/regress/ext:jit-interface/func_new_trap_oob.wast new file mode 100644 index 000000000..4d52aca92 --- /dev/null +++ b/test/regress/ext:jit-interface/func_new_trap_oob.wast @@ -0,0 +1,15 @@ +(module + (type $mt (func (param i32 i32))) + (type $ft (func (result i32))) + (memory 1) + (data (i32.const 16) "\00\41\2a\0b") ;; (func (result i32) (i32.const 42)) + (scope $s) + (func (export "main") (param i32 i32) + (drop (func.new 0 $ft $s (local.get 0) (local.get 1))) + ) +) +(assert_return (invoke "main" (i32.const 16) (i32.const 4))) +(assert_trap (invoke "main" (i32.const 65536) (i32.const 1)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const 65535) (i32.const 2)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const 0) (i32.const 65537)) "out of bounds memory access") +(assert_trap (invoke "main" (i32.const -1) (i32.const 1)) "out of bounds memory access") diff --git a/test/regress/ext:jit-interface/gen.main.v3 b/test/regress/ext:jit-interface/gen.main.v3 new file mode 100644 index 000000000..daf517457 --- /dev/null +++ b/test/regress/ext:jit-interface/gen.main.v3 @@ -0,0 +1,481 @@ +// Copyright 2026 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +// Generates the regression tests for ext:jit-interface. +// +// Every other directory under test/regress produces its {.bin.wast} files with {build.sh}, +// which runs the reference interpreter over a {.wast} source. That does two jobs: it +// validates the test itself against the reference, and it encodes the module. Neither the +// specification nor the reference interpreter implements this proposal yet, so neither job +// can be done that way today. +// +// This generator therefore writes both files directly. The {.wast} is the intended source +// in the text syntax of the proposal explainer; nothing consumes it yet, but it records +// what each test means and is the input for later. The {.bin.wast} is what test/regress.sh +// actually runs. Note the consequence: these tests have no independent oracle. They check +// that Wizard decodes, instantiates, and runs a real binary end to end -- a path the unit +// tests cannot reach, since those either decode without running or run modules built +// in memory -- but they cannot check that the encoding agrees with the specification, +// because the encoding came from the same reading of it that the decoder did. +// +// When the reference interpreter gains support, build.sh should regenerate the {.bin.wast} +// files from the {.wast} sources; the diff then verifies everything encoded here by hand. +// +// The proposal calls an environment a "scope"; Wizard's implementation calls it an +// environment. The text syntax below follows the proposal. + +def OUT = Trace.OUT; + +// ------ binary encoding helpers --------------------------------------------------------- +def I32C = u8.!(Opcode.I32_CONST.code); +def END = u8.!(Opcode.END.code); + +// A complete function body: a local declaration vector, the code, and a terminating {end}. +def body(code: Array) -> Array { + var b = BinBuilder.new(); + b.put_u32leb(0); // no local declaration groups + b.puta(code); + b.put(END); + return b.extract(); +} +// `func.new 0:byte mem:memoryidx sig:typeidx env:environidx` +def func_new(mem: byte, sig: byte, env: byte) -> Array { + var code: Array = [Opcode.FUNC_NEW.prefix, u8.!(Opcode.FUNC_NEW.code), /*flags=*/0, mem, sig, env]; + return code; +} +def i32_const(val: int) -> Array { + var b = BinBuilder.new(); + b.put(I32C).put_s32leb(val); + return b.extract(); +} +def cat(a: Array, b: Array) -> Array { + return Arrays.concat(a, b); +} +def cat3(a: Array, b: Array, c: Array) -> Array { + return Arrays.concat(Arrays.concat(a, b), c); +} + +// Assembles a binary module. Sections must be emitted in the order the format requires. +// Note that the environment section is ordered between the datacount and code sections, +// which is not the order of its id (14). +class ModuleGen { + private def b = BinBuilder.new().reset_header(); + + def types(sigs: Array<(Array, Array)>) -> this { + b.beginSection(BpSection.Type); + b.put_u32leb(u32.!(sigs.length)); + for (s in sigs) { + b.put(BpDefTypeCode.Function.code); + b.put_u32leb(u32.!(s.0.length)); + b.puta(s.0); + b.put_u32leb(u32.!(s.1.length)); + b.puta(s.1); + } + } + def funcs(sig_indexes: Array) -> this { + b.beginSection(BpSection.Function); + b.put_u32leb(u32.!(sig_indexes.length)); + b.puta(sig_indexes); + } + def memory(min: u32) -> this { + b.beginSection(BpSection.Memory); + b.put_u32leb(1); + b.put(0); // no maximum + b.put_u32leb(min); + } + def i32Global(val: int) -> this { + b.beginSection(BpSection.Global); + b.put_u32leb(1); + b.put(BpTypeCode.I32.code); + b.put(0); // immutable + b.put(I32C).put_s32leb(val); + b.put(END); + } + def exportFunc(name: string, index: u32) -> this { + b.beginSection(BpSection.Export); + b.put_u32leb(1); + b.put_string(name); + b.put(BpImportExportKind.Function.code); + b.put_u32leb(index); + } + // Each environment is a reserved flags byte followed by a vector of references to + // declarations of the enclosing module. A memory reference is a reserved zero byte + // unless ext:multi-memory is enabled. + def environs(envs: Array>) -> this { + b.beginSection(BpSection.Environment); + b.put_u32leb(u32.!(envs.length)); + for (env in envs) { + b.put(0); // reserved flags + b.put_u32leb(u32.!(env.length)); + for (imp in env) { + b.put(imp.0); + if (imp.0 == BpImportExportKind.Memory.code) b.put(0); + else b.put_u32leb(imp.1); + } + } + } + def code(bodies: Array>) -> this { + b.beginSection(BpSection.Code); + b.put_u32leb(u32.!(bodies.length)); + for (body in bodies) { + b.put_u32leb(u32.!(body.length)); + b.puta(body); + } + } + def data(entries: Array<(int, Array)>) -> this { + b.beginSection(BpSection.Data); + b.put_u32leb(u32.!(entries.length)); + for (e in entries) { + b.put(0); // active, memory 0 + b.put(I32C).put_s32leb(e.0); + b.put(END); + b.put_u32leb(u32.!(e.1.length)); + b.puta(e.1); + } + } + def finish() -> Array { + b.endSection(); + return b.extract(); + } +} + + +// ------ test registry ------------------------------------------------------------------- +// A single test. If {invalid_msg} is non-null, the module is expected to be rejected and +// is emitted wrapped in an {assert_invalid} rather than standalone. +class Test(name: string, wast: Array, asserts: Array, bin: Array, invalid_msg: string) { } +def NO_LINES: Array = []; +def tests = Vector.new(); +def add(name: string, wast: Array, asserts: Array, bin: Array) { + tests.put(Test.new(name, wast, asserts, bin, null)); +} +def addInvalid(name: string, wast: Array, msg: string, bin: Array) { + tests.put(Test.new(name, wast, NO_LINES, bin, msg)); +} + +def buf = StringBuilder.new(); +def write(name: string, contents: string) { + OUT.put1("##+gen %s", name).ln(); + var fd = System.fileOpen(name, false); + if (fd < 0) { + OUT.puts("##-failed (could not open file)").ln(); + return; + } + buf.reset().puts(contents).out(System.fileWriteK(fd, _, _, _)); + System.fileClose(fd); + OUT.puts("##-ok").ln(); +} +def putLines(b: StringBuilder, lines: Array) { + for (l in lines) b.puts(l).ln(); +} +// The text-format source, which no tool consumes yet; see the header comment. +def writeWast(t: Test) { + var b = buf.reset(); + putLines(b, t.wast); + putLines(b, t.asserts); + write(Strings.format1("%s.wast", t.name), b.toString()); +} +// The binary form, which test/regress.sh runs. +def writeBinWast(t: Test) { + var b = buf.reset(); + var indent = if(t.invalid_msg != null, " ", ""); + if (t.invalid_msg != null) b.puts("(assert_invalid").ln(); + b.puts(indent).puts("(module binary").ln(); + for (i = 0; i < t.bin.length; i = i + 16) { + b.puts(indent).puts(" \""); + for (j = i; j < t.bin.length && j < i + 16; j++) { + var byte = t.bin[j]; + b.putc('\\'); + b.putc(Chars.hexMap_l[byte >> 4]); + b.putc(Chars.hexMap_l[byte & 0xF]); + } + b.puts("\"").ln(); + } + b.puts(indent).puts(")").ln(); + if (t.invalid_msg != null) b.put1(" \"%s\"", t.invalid_msg).ln().puts(")").ln(); + putLines(b, t.asserts); + write(Strings.format1("%s.bin.wast", t.name), b.toString()); +} + +def main(args: Array) -> int { + genAll(); + for (i < tests.length) { + writeWast(tests[i]); + writeBinWast(tests[i]); + } + return 0; +} + +// ------ tests --------------------------------------------------------------------------- +def V: Array = []; +def I: Array = [BpTypeCode.I32.code]; +def II: Array = [BpTypeCode.I32.code, BpTypeCode.I32.code]; + +def CODE_ADDR = 16; // where each test stores the code to be compiled + +// Emits code that compiles the {len} bytes at {CODE_ADDR} into a function of type 1 using +// environment 0, and calls it. ext:jit-interface implies ext:function-references, so +// call_ref is available on the (ref $ft) that func.new produces. +def newAndCall(len: int) -> Array { + return cat3( + cat(i32_const(CODE_ADDR), i32_const(len)), + func_new(0, 1, 0), + [u8.!(Opcode.CALL_REF.code), 1]); +} + +def genAll() { + gen_func_new0(); + gen_func_new_args(); + gen_func_new_scope_func(); + gen_func_new_scope_global(); + gen_func_new_scope_memory(); + gen_func_new_trap_oob(); + gen_func_new_trap_invalid(); + gen_func_new_invalid_env(); +} + +// The simplest case: compile a constant function out of memory and call it. +def gen_func_new0() { + var gen = body(i32_const(42)); + var m = ModuleGen.new() + .types([(V, I), (V, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[]]) + .code([body(newAndCall(gen.length))]) + .data([(CODE_ADDR, gen)]); + add("func_new0", [ + "(module", + " (type $ft (func (result i32)))", + " (memory 1)", + " (data (i32.const 16) \"\\00\\41\\2a\\0b\") ;; (func (result i32) (i32.const 42))", + " (scope $s)", + " (func (export \"main\") (result i32)", + " (call_ref $ft", + " (func.new 0 $ft $s (i32.const 16) (i32.const 4)))", + " )", + ")" + ], [ + "(assert_return (invoke \"main\") (i32.const 42))" + ], m.finish()); +} + +// A generated function that takes parameters, forwarded from the caller. +def gen_func_new_args() { + var gen = body([u8.!(Opcode.LOCAL_GET.code), 1]); + var main_code = cat3( + cat([u8.!(Opcode.LOCAL_GET.code), 0], [u8.!(Opcode.LOCAL_GET.code), 1]), + cat(cat(i32_const(CODE_ADDR), i32_const(gen.length)), func_new(0, 1, 0)), + [u8.!(Opcode.CALL_REF.code), 1]); + var m = ModuleGen.new() + .types([(II, I), (II, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[]]) + .code([body(main_code)]) + .data([(CODE_ADDR, gen)]); + add("func_new_args", [ + "(module", + " (type $ft (func (param i32 i32) (result i32)))", + " (memory 1)", + " (data (i32.const 16) \"\\00\\20\\01\\0b\") ;; (func (param i32 i32) (result i32) (local.get 1))", + " (scope $s)", + " (func (export \"main\") (param i32 i32) (result i32)", + " (call_ref $ft (local.get 0) (local.get 1)", + " (func.new 0 $ft $s (i32.const 16) (i32.const 4)))", + " )", + ")" + ], [ + "(assert_return (invoke \"main\" (i32.const 11) (i32.const 22)) (i32.const 22))", + "(assert_return (invoke \"main\" (i32.const 33) (i32.const 44)) (i32.const 44))" + ], m.finish()); +} + +// The generated code calls a function the scope maps into its index space. The enclosing +// module declares two functions, but only the second is in scope, renumbered to index 0. +def gen_func_new_scope_func() { + var gen = body([u8.!(Opcode.CALL.code), 0]); + var m = ModuleGen.new() + .types([(V, I), (V, I)]) + .funcs([0, 0]) + .memory(1) + .exportFunc("main", 0) + .environs([[(BpImportExportKind.Function.code, 1)]]) + .code([body(newAndCall(gen.length)), body(i32_const(7))]) + .data([(CODE_ADDR, gen)]); + add("func_new_scope_func", [ + "(module", + " (type $ft (func (result i32)))", + " (memory 1)", + " (data (i32.const 16) \"\\00\\10\\00\\0b\") ;; (func (result i32) (call 0))", + " (scope $s", + " (func $seven)) ;; only $seven is in scope, renumbered to index 0", + " (func (export \"main\") (result i32)", + " (call_ref $ft", + " (func.new 0 $ft $s (i32.const 16) (i32.const 4)))", + " )", + " (func $seven (result i32) (i32.const 7))", + ")" + ], [ + "(assert_return (invoke \"main\") (i32.const 7))" + ], m.finish()); +} + +// The generated code reads a global the scope maps into its index space. +def gen_func_new_scope_global() { + var gen = body([u8.!(Opcode.GLOBAL_GET.code), 0]); + var m = ModuleGen.new() + .types([(V, I), (V, I)]) + .funcs([0]) + .memory(1) + .i32Global(1234) + .exportFunc("main", 0) + .environs([[(BpImportExportKind.Global.code, 0)]]) + .code([body(newAndCall(gen.length))]) + .data([(CODE_ADDR, gen)]); + add("func_new_scope_global", [ + "(module", + " (type $ft (func (result i32)))", + " (memory 1)", + " (global $g i32 (i32.const 1234))", + " (data (i32.const 16) \"\\00\\23\\00\\0b\") ;; (func (result i32) (global.get 0))", + " (scope $s", + " (global $g))", + " (func (export \"main\") (result i32)", + " (call_ref $ft", + " (func.new 0 $ft $s (i32.const 16) (i32.const 4)))", + " )", + ")" + ], [ + "(assert_return (invoke \"main\") (i32.const 1234))" + ], m.finish()); +} + +// The generated code loads from a memory the scope maps into its index space, and sees the +// same contents as the enclosing instance. +def gen_func_new_scope_memory() { + var gen = body(cat(i32_const(0), [u8.!(Opcode.I32_LOAD.code), /*align=*/2, /*offset=*/8])); + var data: Array = [0x44, 0x33, 0x22, 0x11]; + var m = ModuleGen.new() + .types([(V, I), (V, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[(BpImportExportKind.Memory.code, 0)]]) + .code([body(newAndCall(gen.length))]) + .data([(8, data), (CODE_ADDR, gen)]); + add("func_new_scope_memory", [ + "(module", + " (type $ft (func (result i32)))", + " (memory 1)", + " (data (i32.const 8) \"\\44\\33\\22\\11\")", + " (data (i32.const 16) \"\\00\\41\\00\\28\\02\\08\\0b\") ;; (func (result i32) (i32.load offset=8 (i32.const 0)))", + " (scope $s", + " (memory 0))", + " (func (export \"main\") (result i32)", + " (call_ref $ft", + " (func.new 0 $ft $s (i32.const 16) (i32.const 7)))", + " )", + ")" + ], [ + "(assert_return (invoke \"main\") (i32.const 0x11223344))" + ], m.finish()); +} + +// An out-of-bounds code range traps rather than compiling. +def gen_func_new_trap_oob() { + var gen = body(i32_const(42)); + var main_code = cat3( + cat([u8.!(Opcode.LOCAL_GET.code), 0], [u8.!(Opcode.LOCAL_GET.code), 1]), + func_new(0, 1, 0), + [u8.!(Opcode.DROP.code)]); + var m = ModuleGen.new() + .types([(II, V), (V, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[]]) + .code([body(main_code)]) + .data([(CODE_ADDR, gen)]); + add("func_new_trap_oob", [ + "(module", + " (type $mt (func (param i32 i32)))", + " (type $ft (func (result i32)))", + " (memory 1)", + " (data (i32.const 16) \"\\00\\41\\2a\\0b\") ;; (func (result i32) (i32.const 42))", + " (scope $s)", + " (func (export \"main\") (param i32 i32)", + " (drop (func.new 0 $ft $s (local.get 0) (local.get 1)))", + " )", + ")" + ], [ + "(assert_return (invoke \"main\" (i32.const 16) (i32.const 4)))", + "(assert_trap (invoke \"main\" (i32.const 65536) (i32.const 1)) \"out of bounds memory access\")", + "(assert_trap (invoke \"main\" (i32.const 65535) (i32.const 2)) \"out of bounds memory access\")", + "(assert_trap (invoke \"main\" (i32.const 0) (i32.const 65537)) \"out of bounds memory access\")", + "(assert_trap (invoke \"main\" (i32.const -1) (i32.const 1)) \"out of bounds memory access\")" + ], m.finish()); +} + +// Code that does not validate traps: here the body is an i64.add with an empty stack. +def gen_func_new_trap_invalid() { + var gen = body([u8.!(Opcode.I64_ADD.code)]); + var main_code = cat3( + cat(i32_const(CODE_ADDR), i32_const(gen.length)), + func_new(0, 1, 0), + [u8.!(Opcode.DROP.code)]); + var m = ModuleGen.new() + .types([(V, V), (V, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[]]) + .code([body(main_code)]) + .data([(CODE_ADDR, gen)]); + add("func_new_trap_invalid", [ + "(module", + " (type $mt (func))", + " (type $ft (func (result i32)))", + " (memory 1)", + " (data (i32.const 16) \"\\00\\7c\\0b\") ;; (func (result i32) (i64.add)) -- does not validate", + " (scope $s)", + " (func (export \"main\")", + " (drop (func.new 0 $ft $s (i32.const 16) (i32.const 3)))", + " )", + ")" + ], [ + "(assert_trap (invoke \"main\") \"function did not validate\")" + ], m.finish()); +} + +// A func.new naming an environment that does not exist does not validate. +def gen_func_new_invalid_env() { + var main_code = cat3( + cat(i32_const(CODE_ADDR), i32_const(4)), + func_new(0, 1, /*no such environment=*/3), + [u8.!(Opcode.DROP.code)]); + var m = ModuleGen.new() + .types([(V, V), (V, I)]) + .funcs([0]) + .memory(1) + .exportFunc("main", 0) + .environs([[]]) + .code([body(main_code)]); + addInvalid("func_new_invalid_env", [ + "(assert_invalid", + " (module", + " (type $mt (func))", + " (type $ft (func (result i32)))", + " (memory 1)", + " (scope $s)", + " (func (export \"main\")", + " ;; environment index 3 is out of bounds", + " (drop (func.new 0 $ft 3 (i32.const 16) (i32.const 4)))", + " )", + " )", + " \"unknown environment\"", + ")" + ], "unknown environment", m.finish()); +} diff --git a/test/regress/ext:jit-interface/gen.sh b/test/regress/ext:jit-interface/gen.sh new file mode 100755 index 000000000..b34175493 --- /dev/null +++ b/test/regress/ext:jit-interface/gen.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Regenerates the tests in this directory. +# +# Unlike the other extension directories, these tests are not produced by running the +# reference interpreter over a .wast source, because it does not implement this proposal +# yet. See the header comment of gen.main.v3. When the reference interpreter gains support, +# this script should be replaced by a build.sh that regenerates the .bin.wast files from +# the .wast sources. + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + HERE="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$HERE/$SOURCE" +done +HERE="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" + +WIZENG_LOC=$(cd $HERE/../../../ && pwd) + +V3I=${V3I:=$(which v3i)} +if [ ! -x "$V3I" ]; then + echo "Virgil interpreter (v3i) not found in \$PATH, and \$V3I not set" + exit 1 +fi + +if [ "$VIRGIL_LIB_UTIL" = "" ]; then + VIRGIL_LOC=${VIRGIL_LOC:=$(cd $(dirname $V3I)/../ && pwd)} + VIRGIL_LIB_UTIL=$VIRGIL_LOC/lib/util +fi + +cd $HERE + +# Mirrors the source set that build.sh uses for the v3i target, plus BinBuilder, which is +# reused from the unit tests to encode sections and patch their LEB sizes. +exec $V3I -fun-exprs -simple-bodies \ + $VIRGIL_LIB_UTIL/*.v3 \ + $WIZENG_LOC/src/engine/*.v3 \ + $WIZENG_LOC/src/engine/compression/*.v3 \ + $WIZENG_LOC/src/engine/continuation/UnboxedContinuation.v3 \ + $WIZENG_LOC/src/engine/v3/*.v3 \ + $WIZENG_LOC/src/util/*.v3 \ + $WIZENG_LOC/test/unittest/BinBuilder.v3 \ + ./gen.main.v3 "$@" diff --git a/test/unittest/BinParserTest.v3 b/test/unittest/BinParserTest.v3 index 848e6c9d6..442bbced5 100644 --- a/test/unittest/BinParserTest.v3 +++ b/test/unittest/BinParserTest.v3 @@ -18,6 +18,7 @@ def Z = void( T("sect_empty", test_sect_empty), T("sect_empty_s", test_sect_empty_s), T("sect_dup", test_sect_dup), + T("sect_repeat", test_sect_repeat), T("sect_underflow", test_sect_underflow), T("sect_size0", test_sect_size0), T("sect_eof", test_sect_eof), @@ -88,6 +89,14 @@ def Z = void( T("bad_sub1", test_bad_sub1), T("bad_sub2", test_bad_sub2), T("bad_sub3", test_bad_sub3), + T("environ0", test_environ0), + T("environ_kinds", test_environ_kinds), + T("environ_flags", test_environ_flags), + T("environ_oob", test_environ_oob), + T("environ_trunc", test_environ_trunc), + T("environ_bad_kind", test_environ_bad_kind), + T("environ_disabled", test_environ_disabled), + T("environsN", test_environsN), // TODO: tests for proper decoding of struct and array types () ); @@ -274,6 +283,8 @@ def test_hdr_split(t: BinParserTester) { t.valid(template); } +// Note: {BpSection.Environment} is gated behind ext:jit-interface, so tests that iterate +// this list must enable that extension. def COUNTED_SECTIONS = [ BpSection.Type, BpSection.Import, @@ -283,6 +294,7 @@ def COUNTED_SECTIONS = [ BpSection.Global, BpSection.Export, BpSection.Element, + BpSection.Environment, // ext:jit-interface BpSection.Code, BpSection.Data ]; @@ -333,6 +345,7 @@ class SingleEmptySectionTemplate { } def test_sect_empty(t: BinParserTester) { + t.extensions |= Extension.set.all; var s = SingleEmptySectionTemplate.new(); for (sect in COUNTED_SECTIONS) { @@ -343,6 +356,7 @@ def test_sect_empty(t: BinParserTester) { } def test_sect_empty_s(t: BinParserTester) { + t.extensions |= Extension.set.all; var s = SingleEmptySectionTemplate.new(); t.test_split = true; @@ -354,6 +368,7 @@ def test_sect_empty_s(t: BinParserTester) { } def test_sect_dup(t: BinParserTester) { + t.extensions |= (Extension.set.all - Extension.REPEAT_SECTIONS); t.test_split = true; var b = BinBuilder.new(); @@ -366,7 +381,22 @@ def test_sect_dup(t: BinParserTester) { } } +def test_sect_repeat(t: BinParserTester) { + t.extensions |= Extension.set.all; + t.test_split = true; + var b = BinBuilder.new(); + + for (sect in COUNTED_SECTIONS) { + b.reset_header(); + b.puta([sect.code, 1, 0]); + t.validN(b.storage()); + b.puta([sect.code, 1, 0]); + t.validN(b.storage()); + } +} + def test_sect_underflow(t: BinParserTester) { + t.extensions |= Extension.set.all; var s = SingleEmptySectionTemplate.new(); for (sect in COUNTED_SECTIONS) { @@ -377,6 +407,7 @@ def test_sect_underflow(t: BinParserTester) { } def test_sect_size0(t: BinParserTester) { + t.extensions |= Extension.set.all; var s = SingleEmptySectionTemplate.new(); for (sect in COUNTED_SECTIONS) { @@ -387,6 +418,7 @@ def test_sect_size0(t: BinParserTester) { } def test_sect_eof(t: BinParserTester) { + t.extensions |= Extension.set.all; var s = SingleEmptySectionTemplate.new(); for (sect in COUNTED_SECTIONS) { @@ -1155,6 +1187,7 @@ def test_data0(t: BinParserTester) { } def test_count_limits(t: BinParserTester) { + t.extensions |= Extension.set.all; var limits = DEFAULT_LIMITS; var pairs = [ (BpSection.Type, limits.max_num_types), @@ -1164,7 +1197,8 @@ def test_count_limits(t: BinParserTester) { (BpSection.Global, limits.max_num_globals), (BpSection.Data, limits.max_num_data_segments), (BpSection.Table, limits.max_num_tables), - (BpSection.Memory, limits.max_num_memories) + (BpSection.Memory, limits.max_num_memories), + (BpSection.Environment, limits.max_num_environments) ]; var b = BinBuilder.new(); for (p in pairs) { @@ -1650,3 +1684,204 @@ def test_bad_sub3(t: BinParserTester) { BpDefTypeCode.Function.code, 0, 1, BpTypeCode.I32.code ]); } + +// ------ ext:jit-interface ----------------------------------------------------------------- +// An environment declaration consists of a flags byte followed by a vector of references +// to declarations of the enclosing module. Each reference is mapped into the corresponding +// index space of the environment's inner module, renumbered from 0. +def ONE_TAG_SECTION: Array = [ + BpSection.Tag.code, 3, 1, /*attribute=*/0, /*sig=*/0 +]; + +def assert_environs(t: BinParserTester, m: Module, count: int) -> Module { + if (m == null) return m; + if (m.environments.length != count) { + t.t.fail2("expected %d environments, got %d", count, m.environments.length); + return m; + } + for (i < count) { + var env = m.environments[i]; + if (env == null) { + t.t.fail1("expected non-null environment #%d", i); + continue; + } + if (env.environ_index != i) t.t.fail2("expected environ_index=%d, got %d", i, env.environ_index); + if (env.outer != m) t.t.fail1("expected environment #%d to refer to the enclosing module", i); + if (env.module == null) t.t.fail1("expected an inner module for environment #%d", i); + // Every mapped declaration becomes an import of the inner module. + else if (env.module.imports.length != env.mapping.length) { + t.t.fail3("expected environment #%d to have %d inner imports, got %d", + i, env.mapping.length, env.module.imports.length); + } + } + return m; +} + +// Asserts the sizes of the index spaces of the inner module of environment {index}. +def assert_inner(t: BinParserTester, m: Module, index: int, + funcs: int, tables: int, memories: int, globals: int, tags: int, heaptypes: int) { + if (m == null) return; + if (index >= m.environments.length) return t.t.fail1("expected environment #%d", index); + var inner = m.environments[index].module; + if (inner == null) return t.t.fail1("expected an inner module for environment #%d", index); + var got = [inner.functions.length, inner.tables.length, inner.memories.length, + inner.globals.length, inner.tags.length, inner.heaptypes.length]; + var expected = [funcs, tables, memories, globals, tags, heaptypes]; + var names = ["functions", "tables", "memories", "globals", "tags", "heaptypes"]; + for (i < expected.length) { + if (expected[i] != got[i]) t.t.fail3("expected %d inner %s, got %d", expected[i], names[i], got[i]); + } +} + +def test_environ0(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + t.test_split = true; + var s = BinSectionTester.new(t, BpSection.Environment); + assert_environs(t, s.valid([0]), 0); // no environments + assert_environs(t, s.valid([1, 0, 0]), 1); // one empty environment + assert_environs(t, s.valid([2, 0, 0, 0, 0]), 2); // two empty environments + assert_environs(t, s.valid([3, 0, 0, 0, 0, 0, 0]), 3); // three empty environments +} + +def test_environ_kinds(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + var s = BinSectionTester.new(t, BpSection.Environment); + s.b.puta(ONE_FUNC_TYPES_SECTION); + s.b.puta(ONE_IMPORTED_FUNC_SECTION); + s.b.puta(ONE_TABLE_SECTION); + s.b.puta(ONE_PAGE_MEMORY_SECTION); + s.b.puta(ONE_TAG_SECTION); + s.b.puta(ONE_GLOBAL_SECTION); + + // Each declaration kind can be mapped into an environment by itself. + var m = assert_environs(t, s.valid([1, 0, 1, BpImportExportKind.Function.code, 0]), 1); + assert_inner(t, m, 0, 1, 0, 0, 0, 0, 0); + + m = assert_environs(t, s.valid([1, 0, 1, BpImportExportKind.Table.code, 0]), 1); + assert_inner(t, m, 0, 0, 1, 0, 0, 0, 0); + + m = assert_environs(t, s.valid([1, 0, 1, BpImportExportKind.Memory.code, 0]), 1); + assert_inner(t, m, 0, 0, 0, 1, 0, 0, 0); + + m = assert_environs(t, s.valid([1, 0, 1, BpImportExportKind.Global.code, 0]), 1); + assert_inner(t, m, 0, 0, 0, 0, 1, 0, 0); + + m = assert_environs(t, s.valid([1, 0, 1, BpImportExportKind.Tag.code, 0]), 1); + assert_inner(t, m, 0, 0, 0, 0, 0, 1, 0); + + // All kinds together, in one environment. + m = assert_environs(t, s.valid([1, 0, 5, + BpImportExportKind.Function.code, 0, + BpImportExportKind.Table.code, 0, + BpImportExportKind.Memory.code, 0, + BpImportExportKind.Global.code, 0, + BpImportExportKind.Tag.code, 0]), 1); + assert_inner(t, m, 0, 1, 1, 1, 1, 1, 0); + + // Two environments referring to the same outer declaration. + m = assert_environs(t, s.valid([2, + 0, 1, BpImportExportKind.Function.code, 0, + 0, 1, BpImportExportKind.Function.code, 0]), 2); + assert_inner(t, m, 0, 1, 0, 0, 0, 0, 0); + assert_inner(t, m, 1, 1, 0, 0, 0, 0, 0); +} + +def test_environ_flags(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + var s = BinSectionTester.new(t, BpSection.Environment); + // The flags byte of an environment declaration is reserved and must be zero. + for (flags = 1; flags < 5; flags++) { + s.invalid(WasmError.EXPECTED_ZERO_BYTE, [1, byte.!(flags), 0]); + } + s.invalid(WasmError.EXPECTED_ZERO_BYTE, [1, 0xFF, 0]); +} + +def test_environ_oob(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + var s = BinSectionTester.new(t, BpSection.Environment); + // Nothing is declared in the enclosing module, so every reference is out of bounds. + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Function.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Table.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Memory.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Global.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Tag.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.HeapType.code, 0]); + + // One declaration of each kind exists, so index 1 is out of bounds. + s.b.puta(ONE_FUNC_TYPES_SECTION); + s.b.puta(ONE_IMPORTED_FUNC_SECTION); + s.b.puta(ONE_TABLE_SECTION); + s.b.puta(ONE_PAGE_MEMORY_SECTION); + s.b.puta(ONE_TAG_SECTION); + s.b.puta(ONE_GLOBAL_SECTION); + + s.valid([1, 0, 1, BpImportExportKind.Function.code, 0]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Function.code, 1]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Table.code, 1]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Global.code, 1]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.Tag.code, 1]); + s.invalid(WasmError.OOB_INDEX, [1, 0, 1, BpImportExportKind.HeapType.code, 1]); + + // Without ext:multi-memory the memory reference is a reserved zero byte. + s.invalid(WasmError.EXPECTED_ZERO_BYTE, [1, 0, 1, BpImportExportKind.Memory.code, 1]); +} + +def test_environ_trunc(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + var s = BinSectionTester.new(t, BpSection.Environment); + s.b.puta(ONE_FUNC_TYPES_SECTION); + s.b.puta(ONE_IMPORTED_FUNC_SECTION); + + s.invalid(WasmError.OVERFLOW_SECTION, [1]); // missing flags byte + s.invalid(WasmError.OVERFLOW_SECTION, [1, 0]); // missing import count + s.invalid(WasmError.OVERFLOW_SECTION, [1, 0, 1]); // missing import kind + s.invalid(WasmError.OVERFLOW_SECTION, [1, 0, 1, // missing function index + BpImportExportKind.Function.code]); + s.invalid(WasmError.OVERFLOW_SECTION, [2, 0, 0]); // missing second environment +} + +def test_environsN(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + testCountedSection(t, 3, BpSection.Environment, ONE_FUNC_TYPES_SECTION, [ + /*flags=*/0, /*imports=*/0 + ]); +} + +def test_environ_bad_kind(t: BinParserTester) { + t.extensions |= Extension.JIT_INTERFACE; + var s = BinSectionTester.new(t, BpSection.Environment); + s.b.puta(ONE_FUNC_TYPES_SECTION); + s.b.puta(ONE_IMPORTED_FUNC_SECTION); + + // Only the import/export kind codes name something that can be mapped. + for (kind = 6; kind < 12; kind++) { + s.invalid(WasmError.INVALID_IMPORT_KIND, [1, 0, 1, byte.!(kind), 0]); + } + s.invalid(WasmError.INVALID_IMPORT_KIND, [1, 0, 1, 0x7F, 0]); + s.invalid(WasmError.INVALID_IMPORT_KIND, [1, 0, 1, 0xFF, 0]); + + // An invalid kind in a later entry is caught too. + s.invalid(WasmError.INVALID_IMPORT_KIND, [1, 0, 2, + BpImportExportKind.Function.code, 0, + /*invalid=*/9, 0]); +} + +// Without ext:jit-interface, the environment section is not a known section at all. +def test_environ_disabled(t: BinParserTester) { + var empty: Array = [/*count=*/0]; + var one: Array = [/*count=*/1, /*flags=*/0, /*imports=*/0]; + var b = BinBuilder.new(); + for (body in [empty, one]) { + b.reset_header(); + b.beginShortSection(BpSection.Environment); + b.puta(body); + b.endSection(); + + // Every other extension is on, so only ext:jit-interface can make this decode. + t.extensions = Extensions.getDefaults(); + t.invalidN(WasmError.INVALID_SECTION, b.storage()); + + t.extensions |= Extension.JIT_INTERFACE; + t.validN(b.storage()); + } +} diff --git a/test/unittest/BytecodeIteratorTest.v3 b/test/unittest/BytecodeIteratorTest.v3 index 93ee27334..6fb1cbefa 100644 --- a/test/unittest/BytecodeIteratorTest.v3 +++ b/test/unittest/BytecodeIteratorTest.v3 @@ -11,6 +11,7 @@ def X_ = void( T("imm0", test_imm0), T("imm1", test_imm1), T("locals1", test_locals1), + T("imm_func_new", test_imm_func_new), () ); @@ -217,3 +218,33 @@ def test_locals1(t: BytecodeIteratorTester) { (11, Opcode.END, 12) ]); } + +// ext:jit-interface: {func.new} has a reserved zero byte followed by three LEB immediates. +def test_imm_func_new(t: BytecodeIteratorTester) { + def I32C = u8.!(Opcode.I32_CONST.code); + def NOP = u8.!(Opcode.NOP.code); + t.code([ + I32C, 0, + I32C, 0, + Opcode.FUNC_NEW.prefix, u8.!(Opcode.FUNC_NEW.code), /*flags=*/0, /*mem=*/0, /*sig=*/0, /*env=*/0, + NOP + ]); + t.assert_pcs([ + (1, Opcode.I32_CONST, 2), + (3, Opcode.I32_CONST, 4), + (5, Opcode.FUNC_NEW, 7), + (11, Opcode.NOP, 12), + (12, Opcode.END, 13) + ]); + + // The three index immediates are LEBs and may be encoded in more than one byte. + t.code([ + Opcode.FUNC_NEW.prefix, u8.!(Opcode.FUNC_NEW.code), 0, 0x80, 0x00, 0x81, 0x01, 0x82, 0x80, 0x00, + NOP + ]); + t.assert_pcs([ + (1, Opcode.FUNC_NEW, 3), + (11, Opcode.NOP, 12), + (12, Opcode.END, 13) + ]); +} diff --git a/test/unittest/CodeValidatorTest.v3 b/test/unittest/CodeValidatorTest.v3 index 34c97ce07..e7663e598 100644 --- a/test/unittest/CodeValidatorTest.v3 +++ b/test/unittest/CodeValidatorTest.v3 @@ -99,6 +99,11 @@ def X_ = void( T("simd.binop1", test_simd_binop1), T("simd.binop2", test_simd_binop2), T("simd.binop3", test_simd_binop3), + T("func.new", test_func_new), + T("func.new_imms", test_func_new_imms), + T("func.new_types", test_func_new_types), + T("func.new_mem64", test_func_new_mem64), + T("func.new_disabled", test_func_new_disabled), T("memarg_stolen_is64", test_memarg_stolen_is64), T("end_empty_ctl", test_end_empty_ctl), () @@ -2424,3 +2429,140 @@ def test_end_empty_ctl(t: CodeValidatorTester) { t.sig(SigCache.v_v); t.invalid(WasmError.OOB_LABEL, [u8.!(Opcode.END.code)], 2); } + +// ------ ext:jit-interface ----------------------------------------------------------------- +// {func.new} has the immediates `0:byte x:memoryidx y:typeidx z:environidx`, pops the +// (start, length) of the code in memory {x}, and pushes a `(ref y)`. +def I32C = u8.!(Opcode.I32_CONST.code); +def I64C = u8.!(Opcode.I64_CONST.code); +def DROP = u8.!(Opcode.DROP.code); + +def func_new(mem: byte, sig: byte, env: byte) -> Array { + var code: Array = [Opcode.FUNC_NEW.prefix, u8.!(Opcode.FUNC_NEW.code), /*flags=*/0, mem, sig, env]; + return code; +} +// Pushes a (start, length) pair with the given constant opcodes and then does {func.new}. +// In the resulting function body, the {func.new} opcode is at +5, and the memory, signature, +// and environment immediates are at +8, +9, and +10, respectively. +def push_func_new(c0: byte, c1: byte, mem: byte, sig: byte, env: byte) -> Array { + var operands: Array = [c0, 0, c1, 0]; + return Arrays.concat(operands, func_new(mem, sig, env)); +} +def then_drop(code: Array) -> Array { + var drop: Array = [DROP]; + return Arrays.concat(code, drop); +} + +def test_func_new(t: CodeValidatorTester) { + t.setExtensions(Extension.JIT_INTERFACE); + t.addMemory(1, Max.Set(1)); + var sig = t.newSig([ValueType.F32], [ValueType.F64]); + var env = t.newEnviron(); + var e = byte.!(env.environ_index), s = heapIndexByte(sig); + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig)]); + + t.valid(push_func_new(I32C, I32C, 0, s, e)); + + // The result is assignable to the nullable and to the abstract funcref type. + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(true, sig)]); + t.valid(push_func_new(I32C, I32C, 0, s, e)); + t.sig0(SigCache.arr_v, [ValueTypes.FUNCREF]); + t.valid(push_func_new(I32C, I32C, 0, s, e)); + + // The result can simply be dropped. + t.sig0(SigCache.arr_v, SigCache.arr_v); + t.valid(then_drop(push_func_new(I32C, I32C, 0, s, e))); + + // More than one environment can be declared and referred to. + var env2 = t.newEnviron(); + t.valid(then_drop(push_func_new(I32C, I32C, 0, s, byte.!(env2.environ_index)))); +} + +def test_func_new_imms(t: CodeValidatorTester) { + t.setExtensions(Extension.JIT_INTERFACE); + var sig = t.newSig([ValueType.F32], [ValueType.F64]); + var env = t.newEnviron(); + var e = byte.!(env.environ_index), s = heapIndexByte(sig); + t.sig0(SigCache.arr_v, SigCache.arr_v); + + // No memory is declared yet. + t.invalid(WasmError.OOB_INDEX, then_drop(push_func_new(I32C, I32C, 0, s, e)), 8); + + // The flags byte of the instruction is reserved and must be zero. It sits at +7 of the + // function body (raw index 6) and is reported ahead of any later immediate error. + for (flags in [1, 2, 0x40, 0x80, 0xFF]) { + var code = then_drop(push_func_new(I32C, I32C, 0, s, e)); + code[6] = byte.!(flags); + t.invalid(WasmError.EXPECTED_ZERO_BYTE, code, 7); + } + + t.addMemory(1, Max.Set(1)); + // Without ext:multi-memory, the memory immediate is a reserved zero byte. + t.invalid(WasmError.EXPECTED_ZERO_BYTE, then_drop(push_func_new(I32C, I32C, 1, s, e)), 8); + t.invalid(WasmError.OOB_INDEX, then_drop(push_func_new(I32C, I32C, 0, 99, e)), 9); + t.invalid(WasmError.OOB_INDEX, then_drop(push_func_new(I32C, I32C, 0, s, 99)), 10); + + // The type immediate must name a signature, not some other heap type. + t.setExtensions(Extension.JIT_INTERFACE | Extension.GC); + var str = t.newStruct([StorageType(ValueType.I32, Packedness.UNPACKED, true)]); + t.invalid(WasmError.ILLEGAL_TYPE, then_drop(push_func_new(I32C, I32C, 0, heapIndexByte(str), e)), 9); +} + +def test_func_new_types(t: CodeValidatorTester) { + t.setExtensions(Extension.JIT_INTERFACE); + t.addMemory(1, Max.Set(1)); + var sig = t.newSig([ValueType.F32], [ValueType.F64]); + var env = t.newEnviron(); + var e = byte.!(env.environ_index), s = heapIndexByte(sig); + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig)]); + + // Both operands must be present and must be i32 for a 32-bit memory. + t.TypeError(func_new(0, s, e), 1); + t.TypeError(Arrays.concat(func_new(0, s, e), func_new(0, s, e)), 1); + t.TypeError(push_func_new(I32C, I64C, 0, s, e), 5); + t.TypeError(push_func_new(I64C, I32C, 0, s, e), 5); + t.TypeError(push_func_new(I64C, I64C, 0, s, e), 5); + + // The result is a `(ref sig)`, which is not assignable to a different signature. + var sig2 = t.newSig([ValueType.I32], [ValueType.I64]); + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig2)]); + t.TypeError(push_func_new(I32C, I32C, 0, s, e), 11); + + // Nor is a null funcref assignable to the non-nullable result. + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig)]); + t.TypeError([u8.!(Opcode.REF_NULL.code), BpTypeCode.FUNCREF.code], 3); +} + +def test_func_new_mem64(t: CodeValidatorTester) { + t.setExtensions(Extension.JIT_INTERFACE | Extension.MEMORY64); + t.module.addDecl(MemoryDecl.new(SizeConstraint(true, 1, Max.Set(1)), false, BpConstants.log2_WASM_PAGE_SIZE)); + var sig = t.newSig([ValueType.F32], [ValueType.F64]); + var env = t.newEnviron(); + var e = byte.!(env.environ_index), s = heapIndexByte(sig); + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig)]); + + // A 64-bit memory takes i64 operands. + t.valid(push_func_new(I64C, I64C, 0, s, e)); + t.TypeError(push_func_new(I32C, I32C, 0, s, e), 5); + t.TypeError(push_func_new(I32C, I64C, 0, s, e), 5); + t.TypeError(push_func_new(I64C, I32C, 0, s, e), 5); +} + +// Without ext:jit-interface, {func.new} is not a valid opcode. The extension is checked +// before the immediates are read, so the error is reported at the opcode itself. +def test_func_new_disabled(t: CodeValidatorTester) { + t.setExtensions(Extensions.getDefaults()); // everything but ext:jit-interface + t.addMemory(1, Max.Set(1)); + var sig = t.newSig([ValueType.F32], [ValueType.F64]); + var env = t.newEnviron(); + var e = byte.!(env.environ_index), s = heapIndexByte(sig); + t.sig0(SigCache.arr_v, [ValueTypes.RefFunc(false, sig)]); + + t.invalid(WasmError.INVALID_OPCODE, push_func_new(I32C, I32C, 0, s, e), 5); + // Even an otherwise malformed instruction reports the missing extension first. + t.invalid(WasmError.INVALID_OPCODE, push_func_new(I32C, I32C, 99, 99, 99), 5); + + // Turning the extension on makes the same code valid. + t.setExtensions(Extensions.getDefaults() | Extension.JIT_INTERFACE); + t.valid(push_func_new(I32C, I32C, 0, s, e)); +} diff --git a/test/unittest/ExeFuncNewTest.v3 b/test/unittest/ExeFuncNewTest.v3 new file mode 100644 index 000000000..278a0ca48 --- /dev/null +++ b/test/unittest/ExeFuncNewTest.v3 @@ -0,0 +1,288 @@ +// Copyright 2026 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +// ext:jit-interface: execution tests for the {func.new} bytecode, which compiles a +// function body out of a memory and binds it to the declarations of an environment. +def X_ = TestTiers.addTests([ + ("func.new", test_func_new), + ("func.new_args", test_func_new_args), + ("func.new_offset", test_func_new_offset), + ("func.new_twice", test_func_new_twice), + ("func.new_env_func", test_func_new_env_func), + ("func.new_env_global", test_func_new_env_global), + ("func.new_env_memory", test_func_new_env_memory), + ("func.new_env_no_memory", test_func_new_env_no_memory), + ("func.new_two_memories", test_func_new_two_memories), + ("func.new_env_isolated", test_func_new_env_isolated), + ("func.new_oob", test_func_new_oob), + ("func.new_invalid", test_func_new_invalid), + ("func.new_sig_mismatch", test_func_new_sig_mismatch), + ("func.new_recompile", test_func_new_recompile) +]); + +def I32C = u8.!(Opcode.I32_CONST.code); +def END = u8.!(Opcode.END.code); +def OFFSET = 16; // where the generated code is placed in memory + +// Wraps {code} as a complete function body: an empty local declaration vector, the code, +// and a terminating {end}. Note that, unlike an entry in the code section, the body given +// to {func.new} has no size prefix. +def body(code: Array) -> Array { + var b = BinBuilder.new(); + b.put_u32leb(0); // no local declaration groups + b.puta(code); + b.put(END); + return b.extract(); +} + +// Emits `i32.const {start}; i32.const {length}; func.new 0 {mem} {sig} {env}`. +def emit_func_new(start: int, length: int, mem: byte, sig: SigDecl, env: EnvironDecl) -> BinBuilder { + var b = BinBuilder.new(); + b.put(I32C).put_s32leb(start); + b.put(I32C).put_s32leb(length); + return emit_func_new0(b, mem, sig, env); +} +def emit_func_new0(b: BinBuilder, mem: byte, sig: SigDecl, env: EnvironDecl) -> BinBuilder { + b.put(Opcode.FUNC_NEW.prefix).put_u32leb(Opcode.FUNC_NEW.code); + b.put(0); // reserved flags + b.put(mem); // memory index, in the index space of the enclosing module + b.put_u32leb(u32.!(sig.heaptype_index)); + b.put_u32leb(u32.!(env.environ_index)); + return b; +} +def emit_call_ref(b: BinBuilder, sig: SigDecl) -> BinBuilder { + b.put(u8.!(Opcode.CALL_REF.code)).put_u32leb(u32.!(sig.heaptype_index)); + return b; +} + +// Gives {i} a one-page memory containing the generated function {code} at {OFFSET}. +def add_code(i: ExeTester, code: Array) { + // ext:jit-interface implies ext:function-references, which {call_ref} on the result of + // {func.new} needs. Implications are applied when parsing engine options, not by the + // tests, so apply them here. + i.extensions |= Extensions.setImplications(Extension.JIT_INTERFACE); + i.addMemory(1, Max.Set(1)); + i.addData(OFFSET, code); +} +// Declares an environment. Like the environment section of a binary module, this must come +// after the declarations it maps into scope. Note that the memory holding the code need not +// be mapped: the memory index immediate of {func.new} names a memory of the enclosing module. +def new_env(i: ExeTester) -> EnvironDecl { + return i.newEnviron(); +} + +// Creates a function from the code at {OFFSET} of memory 0 and immediately calls it. +def new_and_call(i: ExeTester, length: int, sig: SigDecl, env: EnvironDecl) { + i.codev(emit_call_ref(emit_func_new(OFFSET, length, 0, sig, env), sig).extract()); +} + +// A generated function that simply returns a constant. +def test_func_new(i: ExeTester) { + var code = body([I32C, 42]); + add_code(i, code); + var env = new_env(i); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(42); +} + +// A generated function that takes and returns arguments. +def test_func_new_args(i: ExeTester) { + var code = body([u8.!(Opcode.LOCAL_GET.code), 1]); + add_code(i, code); + var env = new_env(i); + i.sig(SigCache.ii_i); + var sig = i.newSig([ValueType.I32, ValueType.I32], [ValueType.I32]); + + var b = BinBuilder.new(); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(0); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(1); + b.put(I32C).put_s32leb(OFFSET); + b.put(I32C).put_s32leb(code.length); + i.codev(emit_call_ref(emit_func_new0(b, 0, sig, env), sig).extract()); + + i.args_ii(11, 22).assert2_i(22); + i.args_ii(33, 44).assert2_i(44); +} + +// Only the requested range of memory is compiled; surrounding bytes are ignored. +def test_func_new_offset(i: ExeTester) { + var code = body([I32C, 7]); + add_code(i, code); + var env = new_env(i); + i.addData(OFFSET - 4, [0xFF, 0xFF, 0xFF, 0xFF]); // garbage before + i.addData(OFFSET + code.length, [0xFF, 0xFF, 0xFF, 0xFF]); // garbage after + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(7); +} + +// The same code compiled twice yields two distinct, independently callable functions. +def test_func_new_twice(i: ExeTester) { + var code = body([I32C, 5]); + add_code(i, code); + var env = new_env(i); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + + var b = emit_call_ref(emit_func_new(OFFSET, code.length, 0, sig, env), sig); + b.puta(emit_call_ref(emit_func_new(OFFSET, code.length, 0, sig, env), sig).extract()); + b.put(u8.!(Opcode.I32_ADD.code)); + i.codev(b.extract()); + i.noargs().assert2_i(10); +} + +// A generated function can call a function mapped into its environment. +def test_func_new_env_func(i: ExeTester) { + var code = body([u8.!(Opcode.CALL.code), 0]); + add_code(i, code); + var callee = i.newFunction(SigCache.v_i, [I32C, 45]); + var env = new_env(i); + env.refOuterFunc(callee); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(45); +} + +// A generated function can read a global mapped into its environment. +def test_func_new_env_global(i: ExeTester) { + var code = body([u8.!(Opcode.GLOBAL_GET.code), 0]); + add_code(i, code); + var global = i.newImmGlobal(ValueType.I32, InitExpr.I32(1234)); + var env = new_env(i); + env.refOuterGlobal(global); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(1234); +} + +// A generated function can access the memory mapped into its environment, and sees the +// same memory contents as the enclosing instance. +def test_func_new_env_memory(i: ExeTester) { + var code = body([I32C, 0, u8.!(Opcode.I32_LOAD.code), /*align=*/2, /*offset=*/8]); + add_code(i, code); + i.addData(8, [0x44, 0x33, 0x22, 0x11]); + var env = new_env(i); + env.refOuterMemory(i.module.memories[0]); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(0x11223344); +} + +// The environment's index spaces are renumbered from 0 and do not include declarations of +// the enclosing module that were not mapped. Here the enclosing module has two functions, +// but only the second is in scope, at environment index 0. +def test_func_new_env_isolated(i: ExeTester) { + var code = body([u8.!(Opcode.CALL.code), 0]); + add_code(i, code); + i.newFunction(SigCache.v_i, [I32C, 1]); + var second = i.newFunction(SigCache.v_i, [I32C, 2]); + var env = new_env(i); + env.refOuterFunc(second); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(2); +} + +// An out-of-bounds code range traps rather than compiling. +def test_func_new_oob(i: ExeTester) { + var code = body([I32C, 42]); + add_code(i, code); + var env = new_env(i); + i.sig0([ValueType.I32, ValueType.I32], SigCache.arr_v); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + + var b = BinBuilder.new(); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(0); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(1); + emit_func_new0(b, 0, sig, env); + b.put(u8.!(Opcode.DROP.code)); + i.codev(b.extract()); + + i.args_uu(u32.!(OFFSET), u32.view(code.length)).assert2_none(); + i.args_uu(65536, 1).assert2_trap(TrapReason.MEMORY_OOB); + i.args_uu(65535, 2).assert2_trap(TrapReason.MEMORY_OOB); + i.args_uu(0, 65537).assert2_trap(TrapReason.MEMORY_OOB); + i.args_uu(0xFFFFFFFF, 1).assert2_trap(TrapReason.MEMORY_OOB); + i.args_uu(1, 0xFFFFFFFF).assert2_trap(TrapReason.MEMORY_OOB); +} + +// Code that does not validate traps. +def test_func_new_invalid(i: ExeTester) { + var code = body([u8.!(Opcode.I64_ADD.code)]); // no operands on the stack + add_code(i, code); + var env = new_env(i); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_trap(TrapReason.FUNC_INVALID); +} + +// Code whose body does not match the requested signature traps. +def test_func_new_sig_mismatch(i: ExeTester) { + var code = body([u8.!(Opcode.I64_CONST.code), 42]); + add_code(i, code); + var env = new_env(i); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_trap(TrapReason.FUNC_INVALID); +} + +// A failed compilation must not poison later compilations in the same environment. +def test_func_new_recompile(i: ExeTester) { + def BAD = OFFSET + 64; + var code = body([I32C, 42]); + var bad = body([u8.!(Opcode.I64_ADD.code)]); + add_code(i, code); + var env = new_env(i); + i.addData(BAD, bad); + i.sig0([ValueType.I32, ValueType.I32], [ValueType.I32]); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + + var b = BinBuilder.new(); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(0); + b.put(u8.!(Opcode.LOCAL_GET.code)).put(1); + i.codev(emit_call_ref(emit_func_new0(b, 0, sig, env), sig).extract()); + + i.args_uu(u32.!(OFFSET), u32.view(code.length)).assert2_i(42); + i.args_uu(u32.!(BAD), u32.view(bad.length)).assert2_trap(TrapReason.FUNC_INVALID); + i.args_uu(u32.!(OFFSET), u32.view(code.length)).assert2_i(42); +} + +// The memory holding the code does not have to be mapped into the environment: the memory +// index immediate names a memory of the enclosing module, not one of the environment. +def test_func_new_env_no_memory(i: ExeTester) { + var code = body([I32C, 27]); + add_code(i, code); + var env = new_env(i); // maps nothing at all + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); + i.noargs().assert2_i(27); + if (env.module.memories.length != 0) i.t.fail("expected an environment with no memories"); +} + +// The memory index immediate of {func.new} and the memory indices inside the generated code +// live in different index spaces. Here the code is read from memory 0 of the enclosing +// module, while the generated function loads from memory 1, which the environment maps to +// its own memory 0. +def test_func_new_two_memories(i: ExeTester) { + var code = body([I32C, 0, u8.!(Opcode.I32_LOAD.code), /*align=*/2, /*offset=*/8]); + add_code(i, code); // memory 0 holds the code + i.extensions |= Extension.MULTI_MEMORY; + i.addMemory(1, Max.Set(1)); // memory 1 holds the data + i.module.data.put(DataDecl.new(SegmentMode.Active(1, InitExpr.I32(8)), [0x44, 0x33, 0x22, 0x11])); + var env = new_env(i); + env.refOuterMemory(i.module.memories[1]); + i.sig(SigCache.v_i); + var sig = i.newSig(SigCache.arr_v, [ValueType.I32]); + new_and_call(i, code.length, sig, env); // reads the code from memory 0 + i.noargs().assert2_i(0x11223344); +} diff --git a/test/unittest/ExtensionTest.v3 b/test/unittest/ExtensionTest.v3 new file mode 100644 index 000000000..82e8c6720 --- /dev/null +++ b/test/unittest/ExtensionTest.v3 @@ -0,0 +1,34 @@ +// Copyright 2026 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +def T = UnitTests.register; +def X_ = void( + T("extension:jit_interface", test_jit_interface), + () +); + +def contains(set: Extension.set, e: Extension) -> bool { + for (x in set) if (x == e) return true; + return false; +} + +def test_jit_interface(t: Tester) { + var set = Extensions.setImplications(Extension.JIT_INTERFACE); + + // {func.new} produces a (ref $ft), so ext:jit-interface needs typed function + // references, which in turn need tail calls. + for (e in [Extension.JIT_INTERFACE, Extension.FUNCTION_REFERENCES, Extension.TAIL_CALL]) { + if (!contains(set, e)) t.fail1("expected ext:jit-interface to imply %s", e.short_name); + } + + // It should not drag in the rest of Wasm 3.0. + for (e in [Extension.GC, Extension.MEMORY64, Extension.MULTI_MEMORY, Extension.EXCEPTION_HANDLING]) { + if (contains(set, e)) t.fail1("expected ext:jit-interface not to imply %s", e.short_name); + } + + // The proposal is not ratified, so it is off unless asked for. The tests that check + // func.new is inert without it depend on this. + if (contains(Extensions.getDefaults(), Extension.JIT_INTERFACE)) { + t.fail("expected ext:jit-interface to be off by default"); + } +} diff --git a/test/unittest/ModuleBuilder.v3 b/test/unittest/ModuleBuilder.v3 index ee1847f64..54dc3bce7 100644 --- a/test/unittest/ModuleBuilder.v3 +++ b/test/unittest/ModuleBuilder.v3 @@ -174,6 +174,14 @@ class ModuleBuilder { module.elems.put(elem); return (decl, elem); } + // Add a new (initially empty) environment to this module. Outer declarations are + // mapped into the environment's inner module with {EnvironDecl.refOuterXXX()}. + def newEnviron() -> EnvironDecl { + var decl = EnvironDecl.new(module, extensions, Limits.new().set(extensions)); + decl.module = Module.new(""); + module.addDecl(decl); + return decl; + } def addMemory(initial: u32, maximum: Max) -> this { var decl = MemoryDecl.new(SizeConstraint(false, initial, maximum), false, BpConstants.log2_WASM_PAGE_SIZE); module.addDecl(decl); diff --git a/test/unittest/OutlineTest.v3 b/test/unittest/OutlineTest.v3 index 712fbef0c..add4b6f3c 100644 --- a/test/unittest/OutlineTest.v3 +++ b/test/unittest/OutlineTest.v3 @@ -12,6 +12,7 @@ def Z = void( T("memories0", test_memories0), T("globals0", test_globals0), T("exports0", test_exports0), + T("environments0", test_environments0), () ); @@ -66,6 +67,7 @@ def test_empty(t: OutlineTester) { t.assert_empty(outline.exports); t.assert_empty(outline.data); t.assert_empty(outline.elements); + t.assert_empty(outline.environments); t.t.assert_eq(0, outline.custom_sections.length); } @@ -176,3 +178,21 @@ def test_exports0(t: OutlineTester) { var outline = t.outline(b.extract()); t.assert_entries(outline.exports, 32, [33, 38], 43); } + +// ext:jit-interface: the environment section outline backs the {read_environment_def} +// and {get_num_environments} reflection calls. +def ENVIRON: Array = [ + /*flags=*/0, /*imports=*/0 +]; + +def test_environments0(t: OutlineTester) { + var b = BinBuilder.new().reset_header(); + b.beginSection(BpSection.Environment); + b.put(3); + b.puta(ENVIRON); + b.puta(ENVIRON); + b.puta(ENVIRON); + b.endSection(); + var outline = t.outline(b.extract()); + t.assert_entries(outline.environments, 14, [15, 17, 19], 21); +}