lib/choir/src/backends/wasm/emission/owner.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const ir = @import("../../../core/root.zig");
  4 const dialects = @import("../../../dialects/root.zig");
  5 const wasm = @import("../root.zig");
  6 const emission = @import("root.zig");
  7 
  8 const Allocator = std.mem.Allocator;
  9 
 10 pub const Module = struct {
 11     bytes: []u8,
 12 
 13     pub fn deinit(self: *Module, allocator: Allocator) void {
 14         allocator.free(self.bytes);
 15         self.* = undefined;
 16     }
 17 };
 18 
 19 pub const ModuleEmitter = struct {
 20     pub const claim: alloc_phase.capacity.Declaration = .{
 21         .source = .{
 22             .id = "choir.wasm_module_emitter",
 23             .kind = .phase_static,
 24             .limit_source = .caller,
 25             .storage = .{
 26                 .covered = &.{
 27                     .{
 28                         .id = "exact_function_and_value_plans_indices_and_control_frames",
 29                         .lifetime = .initialization,
 30                         .detail = "exact function and value plans, indices, and control frames",
 31                     },
 32                     .{
 33                         .id = "exact_count_and_fixed_encoded_output_snapshot",
 34                         .lifetime = .steady,
 35                         .detail = "exact Count and Fixed encoded output snapshot",
 36                     },
 37                     .{
 38                         .id = "the_same_exact_output_allocation_after_take",
 39                         .lifetime = .transferred,
 40                         .detail = "the same exact output allocation after take",
 41                     },
 42                 },
 43                 .excluded = &.{
 44                     "borrowed IR and options after initialization",
 45                     "dynamic artifact and linkage copies outside ModuleEmitter",
 46                 },
 47             },
 48             .capacity = .{
 49                 .inputs = &.{
 50                     alloc_phase.capacity.bindInput(Limits, "facts_definition_count", "facts.definition_count"),
 51                     alloc_phase.capacity.bindInput(Limits, "facts_function_count", "facts.function_count"),
 52                     alloc_phase.capacity.bindInput(Limits, "facts_import_count", "facts.import_count"),
 53                     alloc_phase.capacity.bindInput(Limits, "facts_local_count", "facts.local_count"),
 54                     alloc_phase.capacity.bindInput(Limits, "facts_nesting_depth", "facts.nesting_depth"),
 55                     alloc_phase.capacity.bindInput(Limits, "facts_value_count", "facts.value_count"),
 56                 },
 57                 .type_selectors = &.{},
 58                 .nodes = &.{
 59                     .{ .input = 0 },
 60                     .{ .input = 1 },
 61                     .{ .input = 2 },
 62                     .{ .input = 3 },
 63                     .{ .input = 4 },
 64                     .{ .input = 5 },
 65                     .{ .add = .{ .left = 0, .right = 1 } },
 66                     .{ .add = .{ .left = 6, .right = 2 } },
 67                     .{ .add = .{ .left = 7, .right = 3 } },
 68                     .{ .add = .{ .left = 8, .right = 4 } },
 69                     .{ .add = .{ .left = 9, .right = 5 } },
 70                 },
 71                 .assertions = &.{.{
 72                     .scope = .closure_total,
 73                     .measure = .retained,
 74                     .relation = .upper_bound,
 75                     .expression = 10,
 76                 }},
 77             },
 78             .overload = .{
 79                 .kind = .reject_before_seal,
 80                 .detail = "unsupported input, nesting, index, byte, arithmetic, or OOM failure occurs before activation",
 81             },
 82             .risks = .{
 83                 .transitive = .{
 84                     .status = .open,
 85                     .detail = "emit and take are source-reviewed simple roots, but the ledger does not fail closed over their complete call graph",
 86                 },
 87                 .foreign = .{
 88                     .status = .open,
 89                     .detail = "source review found no callback, indirect, policy, raw heap, or OS edge, but no machine witness excludes future foreign edges",
 90                 },
 91             },
 92             .obligations = &.{
 93                 .{ .key = "wasm_capacity", .role = .capacity_model },
 94                 .{ .key = "wasm_sealed_emit_transitive_risk", .role = .transitive_risk },
 95                 .{ .key = "wasm_sealed_emit_foreign_risk", .role = .foreign_risk },
 96                 .{ .key = "wasm_nesting_boundary", .role = .overload },
 97                 .{ .key = "wasm_snapshot_lifetime", .role = .custom },
 98                 .{ .key = "wasm_oom_retry", .role = .overload },
 99             },
100         },
101         .bindings = .{
102             .owner = @This(),
103             .seal = .{
104                 .family = alloc_phase.capacity.selector(@This().activate),
105                 .premise = .{
106                     .class = .checked_semantic_fact,
107                     .authority = .checker,
108                 },
109             },
110             .teardown = .{
111                 .family = alloc_phase.capacity.selector(@This().deinit),
112                 .premise = .{
113                     .class = .checked_semantic_fact,
114                     .authority = .checker,
115                 },
116             },
117         },
118     };
119     phase: alloc_phase.capacity.Phase,
120     capacity: Capacity,
121     output_capacity: OutputCapacity,
122     output: []u8,
123     output_taken: bool,
124 
125     pub const Limits = emission.Limits;
126     pub const Capacity = emission.Capacity;
127     pub const OutputCapacity = emission.OutputCapacity;
128 
129     const Self = @This();
130 
131     pub fn init(allocator: Allocator, limits: Limits) emission.Error!Self {
132         const capacity = try Capacity.derive(limits);
133 
134         const functions = try allocator.alloc(
135             emission.FunctionPlan,
136             capacity.facts.function_count,
137         );
138         errdefer allocator.free(functions);
139         const function_index = try allocator.alloc(u32, capacity.function_index_slots);
140         errdefer allocator.free(function_index);
141         const values = try allocator.alloc(emission.ValuePlan, capacity.facts.value_count);
142         errdefer allocator.free(values);
143         const value_index = try allocator.alloc(u32, capacity.value_index_slots);
144         errdefer allocator.free(value_index);
145         const frames = try allocator.alloc(emission.function.Frame, capacity.frame_count);
146         errdefer allocator.free(frames);
147 
148         var plan = try emission.plan.planning.fill(
149             limits.module,
150             functions,
151             function_index,
152             values,
153             value_index,
154             capacity.facts,
155         );
156         const measured = try emission.module_encoding.preparePlan(
157             &plan,
158             limits.module,
159             limits.options,
160             frames,
161         );
162         const output_capacity = try OutputCapacity.derive(capacity, measured);
163         const output = try allocator.alloc(u8, output_capacity.output_bytes);
164         errdefer allocator.free(output);
165 
166         var out = wasm.binary.Fixed.init(output);
167         try emission.module_encoding.write(
168             &out,
169             &plan,
170             limits.module,
171             limits.options,
172             frames,
173         );
174         _ = try out.finish();
175 
176         allocator.free(frames);
177         allocator.free(value_index);
178         allocator.free(values);
179         allocator.free(function_index);
180         allocator.free(functions);
181 
182         return .{
183             .phase = .initialization,
184             .capacity = capacity,
185             .output_capacity = output_capacity,
186             .output = output,
187             .output_taken = false,
188         };
189     }
190 
191     pub fn activate(self: *Self) emission.Error!void {
192         if (self.phase != .initialization) return error.AlreadyActive;
193         self.phase = .steady;
194     }
195 
196     pub fn emit(self: *const Self) emission.Error![]const u8 {
197         if (self.phase != .steady) return error.NotActive;
198         if (self.output_taken) return error.AlreadyEmitted;
199         return self.output;
200     }
201 
202     pub fn take(self: *Self) emission.Error!Module {
203         _ = try self.emit();
204         self.output_taken = true;
205         return .{ .bytes = self.output };
206     }
207 
208     pub fn deinit(self: *Self, allocator: Allocator) void {
209         if (self.phase == .teardown) @panic("WASM module emitter teardown is terminal");
210         self.phase = .teardown;
211         if (!self.output_taken) allocator.free(self.output);
212         self.output = undefined;
213     }
214 };
215 
216 comptime {
217     alloc_phase.capacity.requireAllocatorExactOwnerShape(ModuleEmitter);
218 }
219 
220 const ArithDialect = dialects.ArithDialect;
221 const BuiltinDialect = dialects.BuiltinDialect;
222 const FuncDialect = dialects.FuncDialect;
223 
224 fn buildScalarAddModule(context: *ir.Context) !BuiltinDialect.ModuleOp {
225     try dialects.registerAllDialects(context);
226 
227     const location = ir.Location.getUnknown();
228     const i32_type = try ArithDialect.getI32Type(context);
229     const module = try BuiltinDialect.ModuleOp.create(context, location);
230     var func = try FuncDialect.FuncOp.create(
231         context,
232         location,
233         "add_i32",
234         &.{ i32_type, i32_type },
235         &.{i32_type},
236     );
237     try module.getBodyBlock().addOperation(func.op);
238     var add = try ArithDialect.AddOp.create(
239         context,
240         location,
241         func.getArgument(0),
242         func.getArgument(1),
243     );
244     try func.getEntryBlock().addOperation(add.op);
245     const ret = try FuncDialect.ReturnOp.create(
246         context,
247         location,
248         &.{add.getResult()},
249     );
250     try func.getEntryBlock().addOperation(ret.op);
251     return module;
252 }
253 
254 fn emitTestModule(
255     allocator: Allocator,
256     operation: *ir.Operation,
257     options: emission.Options,
258 ) !Module {
259     const limits = try ModuleEmitter.Limits.inspect(operation, options);
260     var emitter = try ModuleEmitter.init(allocator, limits);
261     defer emitter.deinit(allocator);
262     try emitter.activate();
263     return emitter.take();
264 }
265 
266 fn buildNestedIfModule(context: *ir.Context, depth: usize) !BuiltinDialect.ModuleOp {
267     std.debug.assert(depth <= emission.max_nesting_depth + 1);
268     const ScfDialect = dialects.ScfDialect;
269     try dialects.registerAllDialects(context);
270     const location = ir.Location.getUnknown();
271     const i32_type = try ArithDialect.getI32Type(context);
272     const module = try BuiltinDialect.ModuleOp.create(context, location);
273     var func = try FuncDialect.FuncOp.create(
274         context,
275         location,
276         "nested",
277         &.{i32_type},
278         &.{},
279     );
280     try module.getBodyBlock().addOperation(func.op);
281     var then_blocks: [emission.max_nesting_depth + 1]*ir.Block = undefined;
282     var block = func.getEntryBlock();
283     for (0..depth) |index| {
284         var if_op = try ScfDialect.IfOp.create(
285             context,
286             location,
287             func.getArgument(0),
288             &.{},
289         );
290         try block.addOperation(if_op.op);
291         then_blocks[index] = if_op.getThenBlock();
292         const else_yield = try ScfDialect.YieldOp.create(context, location, &.{});
293         try if_op.getElseBlock().?.addOperation(else_yield.op);
294         block = if_op.getThenBlock();
295     }
296     for (then_blocks[0..depth]) |then_block| {
297         const yield = try ScfDialect.YieldOp.create(context, location, &.{});
298         try then_block.addOperation(yield.op);
299     }
300     const ret = try FuncDialect.ReturnOp.create(context, location, &.{});
301     try func.getEntryBlock().addOperation(ret.op);
302     return module;
303 }
304 
305 test "WASM module emitter seals exact storage before its first emission" {
306     comptime {
307         @stardustClaim(
308             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_sealed_emit_transitive_risk"),
309             null,
310             null,
311             null,
312             null,
313             null,
314             null,
315         );
316     }
317     comptime {
318         @stardustClaim(
319             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_sealed_emit_foreign_risk"),
320             null,
321             null,
322             null,
323             null,
324             null,
325             null,
326         );
327     }
328 
329     const allocator = std.testing.allocator;
330     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
331     defer context.deinit(allocator);
332     const source_module = try buildScalarAddModule(&context);
333     const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
334     const derived = try ModuleEmitter.Capacity.derive(limits);
335 
336     var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(allocator);
337     var maybe_emitter: ?ModuleEmitter = null;
338     var maybe_module: ?Module = null;
339     errdefer {
340         if (phase_allocator.phase() == .initialization) {
341             phase_allocator.abortInitialization();
342         }
343         if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
344         if (maybe_module) |*module| {
345             module.deinit(phase_allocator.teardownAllocator());
346         }
347         if (maybe_emitter) |*emitter| {
348             if (emitter.phase != .teardown) {
349                 emitter.deinit(phase_allocator.teardownAllocator());
350             }
351         }
352         if (phase_allocator.phase() == .teardown) phase_allocator.deinit();
353     }
354 
355     maybe_emitter = try ModuleEmitter.init(
356         phase_allocator.initializationAllocator(),
357         limits,
358     );
359     const emitter = &maybe_emitter.?;
360     const output_pointer = emitter.output.ptr;
361     const output_capacity = emitter.output_capacity;
362 
363     phase_allocator.seal();
364     try emitter.activate();
365     const first = try emitter.emit();
366     const second = try emitter.emit();
367     maybe_module = try emitter.take();
368     const module = &maybe_module.?;
369 
370     try std.testing.expectEqual(derived, emitter.capacity);
371     try std.testing.expectEqual(output_pointer, first.ptr);
372     try std.testing.expectEqual(first.ptr, second.ptr);
373     try std.testing.expectEqualSlices(u8, first, second);
374     try std.testing.expectEqual(output_pointer, module.bytes.ptr);
375     try std.testing.expectEqual(output_capacity.output_bytes, module.bytes.len);
376     try std.testing.expectEqual(output_capacity.output_bytes, output_capacity.steady_bytes);
377     try std.testing.expectEqual(
378         derived.working_bytes + output_capacity.output_bytes,
379         output_capacity.initialization_bytes,
380     );
381     try std.testing.expectEqualSlices(u8, &.{
382         0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
383         0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01,
384         0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x0b, 0x01,
385         0x07, 'a',  'd',  'd',  '_',  'i',  '3',  '2',
386         0x00, 0x00, 0x0a, 0x10, 0x01, 0x0e, 0x01, 0x01,
387         0x7f, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x21, 0x02,
388         0x20, 0x02, 0x0f, 0x0b,
389     }, module.bytes);
390     try std.testing.expectError(error.AlreadyEmitted, emitter.emit());
391     try std.testing.expectError(error.AlreadyEmitted, emitter.take());
392     try std.testing.expectEqual(
393         alloc_phase.PhaseViolations{},
394         phase_allocator.violations(),
395     );
396 
397     phase_allocator.beginTeardown();
398     module.deinit(phase_allocator.teardownAllocator());
399     maybe_module = null;
400     emitter.deinit(phase_allocator.teardownAllocator());
401     try std.testing.expectEqual(
402         alloc_phase.PhaseViolations{},
403         phase_allocator.violations(),
404     );
405     phase_allocator.deinit();
406 }
407 
408 test "WASM module emitter capacity matches an independent scalar model" {
409     comptime {
410         @stardustClaim(
411             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_capacity"),
412             null,
413             null,
414             null,
415             null,
416             null,
417             null,
418         );
419     }
420 
421     const allocator = std.testing.allocator;
422     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
423     defer context.deinit(allocator);
424     const source_module = try buildScalarAddModule(&context);
425     const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
426     const capacity = try ModuleEmitter.Capacity.derive(limits);
427     var emitter = try ModuleEmitter.init(allocator, limits);
428     defer emitter.deinit(allocator);
429 
430     try std.testing.expectEqual(@as(usize, 1), limits.facts.function_count);
431     try std.testing.expectEqual(@as(usize, 0), limits.facts.import_count);
432     try std.testing.expectEqual(@as(usize, 1), limits.facts.definition_count);
433     try std.testing.expectEqual(@as(usize, 3), limits.facts.value_count);
434     try std.testing.expectEqual(@as(usize, 1), limits.facts.local_count);
435     try std.testing.expectEqual(@as(usize, 0), limits.facts.nesting_depth);
436     try std.testing.expectEqual(@as(usize, 1), capacity.frame_count);
437 
438     const expected_working = @sizeOf(emission.FunctionPlan) +
439         4 * @sizeOf(u32) +
440         3 * @sizeOf(emission.ValuePlan) +
441         4 * @sizeOf(u32) +
442         @sizeOf(emission.function.Frame);
443     try std.testing.expectEqual(expected_working, capacity.working_bytes);
444     try std.testing.expectEqual(@as(usize, 52), emitter.output_capacity.output_bytes);
445     try std.testing.expectEqual(
446         expected_working + emitter.output_capacity.output_bytes,
447         emitter.output_capacity.initialization_bytes,
448     );
449     try std.testing.expectEqual(
450         emitter.output_capacity.output_bytes,
451         emitter.output_capacity.steady_bytes,
452     );
453 }
454 
455 test "WASM empty and declaration-only modules need no control or value workspace" {
456     const allocator = std.testing.allocator;
457     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
458     defer context.deinit(allocator);
459     try dialects.registerAllDialects(&context);
460     const location = ir.Location.getUnknown();
461     const empty = try BuiltinDialect.ModuleOp.create(&context, location);
462 
463     const empty_limits = try ModuleEmitter.Limits.inspect(empty.op, .{});
464     const empty_capacity = try ModuleEmitter.Capacity.derive(empty_limits);
465     try std.testing.expectEqual(@as(usize, 0), empty_capacity.frame_count);
466     try std.testing.expectEqual(@as(usize, 0), empty_capacity.function_index_slots);
467     try std.testing.expectEqual(@as(usize, 0), empty_capacity.value_index_slots);
468     var empty_emitter = try ModuleEmitter.init(allocator, empty_limits);
469     defer empty_emitter.deinit(allocator);
470     try std.testing.expectEqual(@as(usize, 11), empty_emitter.output_capacity.output_bytes);
471 
472     const i32_type = try ArithDialect.getI32Type(&context);
473     const declaration = try FuncDialect.FuncOp.createDeclaration(
474         &context,
475         location,
476         "external",
477         &.{i32_type},
478         &.{i32_type},
479     );
480     try empty.getBodyBlock().addOperation(declaration.op);
481     const declaration_limits = try ModuleEmitter.Limits.inspect(empty.op, .{});
482     const declaration_capacity = try ModuleEmitter.Capacity.derive(declaration_limits);
483     try std.testing.expectEqual(@as(usize, 0), declaration_capacity.frame_count);
484     try std.testing.expectEqual(@as(usize, 0), declaration_capacity.value_index_slots);
485     try std.testing.expectEqual(@as(usize, 4), declaration_capacity.function_index_slots);
486     var declaration_emitter = try ModuleEmitter.init(allocator, declaration_limits);
487     defer declaration_emitter.deinit(allocator);
488     try declaration_emitter.activate();
489     try std.testing.expect((try declaration_emitter.emit()).len > 11);
490     try std.testing.expectError(
491         error.FunctionNotFound,
492         ModuleEmitter.Limits.inspect(empty.op, .{ .entry = "external" }),
493     );
494 }
495 
496 test "WASM module emitter admits its nesting maximum and rejects one past it" {
497     comptime {
498         @stardustClaim(
499             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_nesting_boundary"),
500             null,
501             null,
502             null,
503             null,
504             null,
505             null,
506         );
507     }
508 
509     const allocator = std.testing.allocator;
510     var bounded_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
511     defer bounded_context.deinit(allocator);
512     const bounded = try buildNestedIfModule(&bounded_context, emission.max_nesting_depth);
513     const limits = try ModuleEmitter.Limits.inspect(bounded.op, .{});
514     const capacity = try ModuleEmitter.Capacity.derive(limits);
515     try std.testing.expectEqual(emission.max_nesting_depth, limits.facts.nesting_depth);
516     try std.testing.expectEqual(
517         2 * emission.max_nesting_depth + 1,
518         capacity.frame_count,
519     );
520 
521     var one_past_context = try ir.Context.init(allocator, ir.Context.Limits.testing);
522     defer one_past_context.deinit(allocator);
523     const one_past = try buildNestedIfModule(
524         &one_past_context,
525         emission.max_nesting_depth + 1,
526     );
527     try std.testing.expectError(
528         error.NestingLimitExceeded,
529         ModuleEmitter.Limits.inspect(one_past.op, .{}),
530     );
531 }
532 
533 test "WASM module emitter seals an input snapshot before activation" {
534     const allocator = std.testing.allocator;
535     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
536     defer context.deinit(allocator);
537     const source_module = try buildScalarAddModule(&context);
538     var entry = [_]u8{ 'a', 'd', 'd', '_', 'i', '3', '2' };
539     const limits = try ModuleEmitter.Limits.inspect(
540         source_module.op,
541         .{ .entry = &entry },
542     );
543     var emitter = try ModuleEmitter.init(allocator, limits);
544     defer emitter.deinit(allocator);
545 
546     entry[0] = 'x';
547     try emitter.activate();
548     const bytes = try emitter.emit();
549     try std.testing.expect(std.mem.indexOf(u8, bytes, "add_i32") != null);
550     try std.testing.expect(std.mem.indexOf(u8, bytes, "xdd_i32") == null);
551     var module = try emitter.take();
552     defer module.deinit(allocator);
553 }
554 
555 test "WASM module emitter rejects structural drift during indexed initialization" {
556     const allocator = std.testing.allocator;
557     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
558     defer context.deinit(allocator);
559     const source_module = try buildScalarAddModule(&context);
560     const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
561 
562     var operations = source_module.getBodyBlock().getOperations();
563     const function_op = operations.next().?;
564     const function = FuncDialect.FuncOp{ .op = function_op };
565     const i32_type = try ArithDialect.getI32Type(&context);
566     const extra = try ArithDialect.ConstantOp.createInt(
567         &context,
568         ir.Location.getUnknown(),
569         i32_type,
570         1,
571     );
572     try function.getEntryBlock().addOperation(extra.op);
573 
574     try std.testing.expectError(
575         error.InputChanged,
576         ModuleEmitter.init(allocator, limits),
577     );
578 }
579 
580 test "WASM module emitter captures same-capacity semantics during initialization" {
581     const allocator = std.testing.allocator;
582     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
583     defer context.deinit(allocator);
584     const source_module = try buildScalarAddModule(&context);
585     const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
586 
587     var operations = source_module.getBodyBlock().getOperations();
588     const function_op = operations.next().?;
589     try ir.SymbolTable.setSymbolName(function_op, "sub_i32");
590 
591     var emitter = try ModuleEmitter.init(allocator, limits);
592     defer emitter.deinit(allocator);
593     try emitter.activate();
594     const bytes = try emitter.emit();
595     try std.testing.expect(std.mem.indexOf(u8, bytes, "sub_i32") != null);
596     try std.testing.expect(std.mem.indexOf(u8, bytes, "add_i32") == null);
597 }
598 
599 test "WASM module snapshot outlives its IR and emitter" {
600     comptime {
601         @stardustClaim(
602             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_snapshot_lifetime"),
603             null,
604             null,
605             null,
606             null,
607             null,
608             null,
609         );
610     }
611 
612     const allocator = std.testing.allocator;
613     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
614     const source_module = try buildScalarAddModule(&context);
615     const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
616     var emitter = try ModuleEmitter.init(allocator, limits);
617     context.deinit(allocator);
618 
619     try emitter.activate();
620     var module = try emitter.take();
621     emitter.deinit(allocator);
622     try std.testing.expectEqualSlices(u8, &.{ 0x00, 0x61, 0x73, 0x6d }, module.bytes[0..4]);
623     module.deinit(allocator);
624 }
625 
626 test "WASM module emitter initialization survives every allocation failure" {
627     comptime {
628         @stardustClaim(
629             @import("alloc_phase").capacity.witness(ModuleEmitter, "wasm_oom_retry"),
630             null,
631             null,
632             null,
633             null,
634             null,
635             null,
636         );
637     }
638 
639     const Harness = struct {
640         fn run(allocator: Allocator) !void {
641             var context = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
642             defer context.deinit(std.testing.allocator);
643             const source_module = try buildScalarAddModule(&context);
644             const limits = try ModuleEmitter.Limits.inspect(source_module.op, .{});
645             var emitter = try ModuleEmitter.init(allocator, limits);
646             defer emitter.deinit(allocator);
647             try emitter.activate();
648             var module = try emitter.take();
649             defer module.deinit(allocator);
650             try std.testing.expectEqual(emitter.output_capacity.output_bytes, module.bytes.len);
651         }
652     };
653 
654     try std.testing.checkAllAllocationFailures(
655         std.testing.allocator,
656         Harness.run,
657         .{},
658     );
659 }
660 
661 test "WASM module emitter keeps imports before definitions in index space" {
662     const allocator = std.testing.allocator;
663     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
664     defer context.deinit(allocator);
665     try dialects.registerAllDialects(&context);
666 
667     const location = ir.Location.getUnknown();
668     const i32_type = try ArithDialect.getI32Type(&context);
669     const module = try BuiltinDialect.ModuleOp.create(&context, location);
670     var caller = try FuncDialect.FuncOp.create(
671         &context,
672         location,
673         "call_external",
674         &.{i32_type},
675         &.{i32_type},
676     );
677     try module.getBodyBlock().addOperation(caller.op);
678     var call = try FuncDialect.CallOp.create(
679         &context,
680         location,
681         "external_plus_one",
682         &.{caller.getArgument(0)},
683         &.{i32_type},
684     );
685     try caller.getEntryBlock().addOperation(call.op);
686     const ret = try FuncDialect.ReturnOp.create(
687         &context,
688         location,
689         &.{call.getResult(0).?},
690     );
691     try caller.getEntryBlock().addOperation(ret.op);
692     const imported = try FuncDialect.FuncOp.createDeclaration(
693         &context,
694         location,
695         "external_plus_one",
696         &.{i32_type},
697         &.{i32_type},
698     );
699     try module.getBodyBlock().addOperation(imported.op);
700 
701     var encoded = try emitTestModule(
702         allocator,
703         module.op,
704         .{ .entry = "call_external" },
705     );
706     defer encoded.deinit(allocator);
707     try std.testing.expectEqualSlices(u8, &.{
708         0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
709         0x01, 0x0b, 0x02, 0x60, 0x01, 0x7f, 0x01, 0x7f,
710         0x60, 0x01, 0x7f, 0x01, 0x7f, 0x02, 0x19, 0x01,
711         0x03, 'e',  'n',  'v',  0x11, 'e',  'x',  't',
712         'e',  'r',  'n',  'a',  'l',  '_',  'p',  'l',
713         'u',  's',  '_',  'o',  'n',  'e',  0x00, 0x01,
714         0x03, 0x02, 0x01, 0x00, 0x07, 0x11, 0x01, 0x0d,
715         'c',  'a',  'l',  'l',  '_',  'e',  'x',  't',
716         'e',  'r',  'n',  'a',  'l',  0x00, 0x01, 0x0a,
717         0x0f, 0x01, 0x0d, 0x01, 0x01, 0x7f, 0x20, 0x00,
718         0x10, 0x00, 0x21, 0x01, 0x20, 0x01, 0x0f, 0x0b,
719     }, encoded.bytes);
720 
721     var backend = try wasm.Backend.init(allocator, &context);
722     defer backend.deinit();
723     var artifact = try backend.compileModuleToArtifact(
724         module.op,
725         .{ .entry = "call_external" },
726     );
727     defer artifact.deinit();
728     try std.testing.expect(artifact.linkage.hasProvided("call_external"));
729     try std.testing.expect(artifact.linkage.hasRequired("external_plus_one"));
730 }
731 
732 test "WASM module emitter handles structured control and host memory" {
733     const ScfDialect = dialects.ScfDialect;
734     const MemrefDialect = dialects.MemrefDialect;
735     const allocator = std.testing.allocator;
736     var context = try ir.Context.init(allocator, ir.Context.Limits.testing);
737     defer context.deinit(allocator);
738     try dialects.registerAllDialects(&context);
739 
740     const location = ir.Location.getUnknown();
741     const i32_type = try ArithDialect.getI32Type(&context);
742     const index_type = try ArithDialect.getIndexType(&context);
743     const f32_type = try ArithDialect.getScalarType(&context, .f32);
744     const memref_type = try MemrefDialect.getMemrefTypeDynamic(
745         &context,
746         f32_type,
747         .host,
748     );
749     const module = try BuiltinDialect.ModuleOp.create(&context, location);
750 
751     var maximum = try FuncDialect.FuncOp.create(
752         &context,
753         location,
754         "max_i32",
755         &.{ i32_type, i32_type },
756         &.{i32_type},
757     );
758     try module.getBodyBlock().addOperation(maximum.op);
759     var cmp = try ArithDialect.CmpOp.create(
760         &context,
761         location,
762         .sgt,
763         maximum.getArgument(0),
764         maximum.getArgument(1),
765     );
766     try maximum.getEntryBlock().addOperation(cmp.op);
767     var if_op = try ScfDialect.IfOp.create(
768         &context,
769         location,
770         cmp.getResult(),
771         &.{i32_type},
772     );
773     try maximum.getEntryBlock().addOperation(if_op.op);
774     const then_yield = try ScfDialect.YieldOp.create(
775         &context,
776         location,
777         &.{maximum.getArgument(0)},
778     );
779     try if_op.getThenBlock().addOperation(then_yield.op);
780     const else_yield = try ScfDialect.YieldOp.create(
781         &context,
782         location,
783         &.{maximum.getArgument(1)},
784     );
785     try if_op.getElseBlock().?.addOperation(else_yield.op);
786     const maximum_ret = try FuncDialect.ReturnOp.create(
787         &context,
788         location,
789         &.{if_op.getResult(0).?},
790     );
791     try maximum.getEntryBlock().addOperation(maximum_ret.op);
792 
793     var sum = try FuncDialect.FuncOp.create(
794         &context,
795         location,
796         "sum_four",
797         &.{},
798         &.{index_type},
799     );
800     try module.getBodyBlock().addOperation(sum.op);
801     var lower = try ArithDialect.ConstantOp.createInt(&context, location, index_type, 0);
802     try sum.getEntryBlock().addOperation(lower.op);
803     var upper = try ArithDialect.ConstantOp.createInt(&context, location, index_type, 4);
804     try sum.getEntryBlock().addOperation(upper.op);
805     var step = try ArithDialect.ConstantOp.createInt(&context, location, index_type, 1);
806     try sum.getEntryBlock().addOperation(step.op);
807     var initial = try ArithDialect.ConstantOp.createInt(&context, location, index_type, 0);
808     try sum.getEntryBlock().addOperation(initial.op);
809     var loop = try ScfDialect.ForOp.create(
810         &context,
811         location,
812         lower.getResult(),
813         upper.getResult(),
814         step.getResult(),
815         &.{initial.getResult()},
816         &.{index_type},
817     );
818     try sum.getEntryBlock().addOperation(loop.op);
819     var next = try ArithDialect.AddOp.create(
820         &context,
821         location,
822         loop.getIterArgs()[0],
823         loop.getInductionVar(),
824     );
825     try loop.getBodyBlock().addOperation(next.op);
826     const loop_yield = try ScfDialect.YieldOp.create(
827         &context,
828         location,
829         &.{next.getResult()},
830     );
831     try loop.getBodyBlock().addOperation(loop_yield.op);
832     const sum_ret = try FuncDialect.ReturnOp.create(
833         &context,
834         location,
835         &.{loop.getResult(0).?},
836     );
837     try sum.getEntryBlock().addOperation(sum_ret.op);
838 
839     var copy = try FuncDialect.FuncOp.create(
840         &context,
841         location,
842         "copy_one",
843         &.{ memref_type, memref_type },
844         &.{},
845     );
846     try module.getBodyBlock().addOperation(copy.op);
847     var zero = try ArithDialect.ConstantOp.createInt(&context, location, index_type, 0);
848     try copy.getEntryBlock().addOperation(zero.op);
849     var loaded = try MemrefDialect.LoadOp.create(
850         &context,
851         location,
852         copy.getArgument(0),
853         zero.getResult(),
854         f32_type,
855     );
856     try copy.getEntryBlock().addOperation(loaded.op);
857     const store = try MemrefDialect.StoreOp.create(
858         &context,
859         location,
860         loaded.getResult(),
861         copy.getArgument(1),
862         zero.getResult(),
863     );
864     try copy.getEntryBlock().addOperation(store.op);
865     const copy_ret = try FuncDialect.ReturnOp.create(&context, location, &.{});
866     try copy.getEntryBlock().addOperation(copy_ret.op);
867 
868     const limits = try ModuleEmitter.Limits.inspect(module.op, .{});
869     try std.testing.expectEqual(@as(usize, 1), limits.facts.nesting_depth);
870     try std.testing.expect(limits.facts.needs_memory);
871     var encoded = try emitTestModule(allocator, module.op, .{});
872     defer encoded.deinit(allocator);
873     try std.testing.expect(encoded.bytes.len > 8);
874     try std.testing.expect(std.mem.indexOfScalar(
875         u8,
876         encoded.bytes,
877         @backingInt(@import("../binary/root.zig").Section.memory),
878     ) != null);
879 }