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 }