lib/choir/src/core/interfaces/effects.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ir = @import("../root.zig");
3 const interfaces = @import("root.zig");
4
5 /// Shared premise for value-level floating operations, independent of any dialect.
6 pub const ArithmeticPolicy = struct {
7 exceptions_masked: bool = true,
8 default_rounding: bool = true,
9 environment_observable: bool = false,
10
11 pub fn permitsFloatingValues(self: ArithmeticPolicy) bool {
12 return self.exceptions_masked and self.default_rounding and !self.environment_observable;
13 }
14 };
15
16 pub const ContextPremise = enum { floating_environment };
17
18 /// Subjects are local to the declaring operation. Different SSA values may alias.
19 pub const Subject = union(enum) {
20 operation,
21 operand: usize,
22 result: usize,
23 global: []const u8,
24 region: usize,
25 };
26
27 pub const Footprint = struct {
28 offset: u64,
29 length: u64,
30 };
31
32 pub const Scope = union(enum) {
33 unknown,
34 operation,
35 region: usize,
36 named: []const u8,
37 };
38
39 pub const Resource = struct {
40 subject: Subject = .operation,
41 address_space: ?u32 = null,
42 bytes: ?Footprint = null,
43 alias_scope: Scope = .unknown,
44 lifetime: Scope = .unknown,
45 allocator_domain: ?[]const u8 = null,
46 ordering_scope: Scope = .unknown,
47 state_key: ?[]const u8 = null,
48 };
49
50 pub const EventKind = enum {
51 read,
52 write,
53 failure,
54 trap,
55 diverge,
56 allocate,
57 free,
58 retain,
59 release,
60 borrow,
61 move,
62 foreign,
63 io,
64 state_observe,
65 state_update,
66 state_draw,
67 state_memo,
68 launch,
69 synchronize,
70 };
71
72 pub const Event = struct {
73 kind: EventKind,
74 resource: Resource = .{},
75 /// Atomic and volatile accesses are ordered even if their value is unused.
76 ordered: bool = false,
77 failure_name: ?[]const u8 = null,
78 };
79
80 pub const RequirementKind = enum {
81 nonzero,
82 quotient_representable,
83 in_bounds,
84 live,
85 terminates,
86 execution_context,
87 conversion_representable,
88 callee_contract,
89 };
90
91 /// A requirement is a premise to prove at the use, never a producer-supplied proof.
92 pub const Requirement = struct {
93 kind: RequirementKind,
94 subject: Subject,
95 related: ?Subject = null,
96 };
97
98 pub const Ownership = enum { none, owned, borrowed, transferred, unknown };
99
100 pub const ResultFact = struct {
101 index: usize,
102 fresh_identity: bool = false,
103 alias: ?Subject = null,
104 ownership: Ownership = .unknown,
105 lifetime: Scope = .unknown,
106 };
107
108 pub const Execution = enum { immediate, conditional, repeated, latent, unknown };
109
110 pub const RegionFact = struct {
111 index: usize,
112 execution: Execution = .unknown,
113 /// Repetition is not a termination proof, even when every child is total.
114 may_diverge: bool = true,
115 captures: bool = true,
116 };
117
118 pub const Binding = struct {
119 region: usize,
120 argument: usize,
121 source: Subject,
122 };
123
124 /// Order in this sequence is event order within the operation, not set membership.
125 pub const Fact = union(enum) {
126 event: Event,
127 requirement: Requirement,
128 result: ResultFact,
129 region: RegionFact,
130 binding: Binding,
131 premise: ContextPremise,
132 };
133
134 pub const Facts = struct {
135 records: []const Fact = &.{},
136 complete: bool = false,
137 };
138
139 /// Contract entries and arity determine storage before enumeration. No growth occurs.
140 pub const Capacity = struct {
141 entries: usize = 0,
142 per_operand: usize = 0,
143 per_result: usize = 0,
144 per_region: usize = 0,
145
146 pub fn count(self: Capacity, operands: usize, results: usize, regions: usize) ?usize {
147 var record_count = self.entries;
148 const counts = [_]usize{ operands, results, regions };
149 const rates = [_]usize{ self.per_operand, self.per_result, self.per_region };
150 for (counts, rates) |n, rate| {
151 const increment = std.math.mul(usize, n, rate) catch return null;
152 record_count = std.math.add(usize, record_count, increment) catch return null;
153 }
154 return record_count;
155 }
156 };
157
158 pub const Collector = struct {
159 storage: []Fact,
160 used: usize = 0,
161 complete: bool = false,
162 exhausted: bool = false,
163 observer: ?Observer = null,
164
165 const Observer = struct {
166 context: *anyopaque,
167 limit: usize,
168 visit: *const fn (*anyopaque, Fact) void,
169 };
170
171 pub fn append(self: *Collector, fact: Fact) void {
172 if (self.observer) |observer| {
173 if (self.used == observer.limit) {
174 self.exhausted = true;
175 return;
176 }
177 observer.visit(observer.context, fact);
178 self.used += 1;
179 return;
180 }
181 std.debug.assert(self.used <= self.storage.len);
182 if (self.used == self.storage.len) {
183 self.exhausted = true;
184 return;
185 }
186 self.storage[self.used] = fact;
187 self.used += 1;
188 std.debug.assert(self.used <= self.storage.len);
189 }
190
191 pub fn view(self: *const Collector) Facts {
192 if (self.observer != null) return .{};
193 return .{
194 .records = self.storage[0..self.used],
195 .complete = self.complete and !self.exhausted,
196 };
197 }
198
199 /// Scalar value results carry no identity or ownership. This does not assert completeness.
200 pub fn valueResults(self: *Collector, op: *const ir.Operation) void {
201 for (0..op.getNumResults()) |index| {
202 self.append(.{ .result = .{ .index = index, .ownership = .none } });
203 }
204 }
205 };
206
207 pub const Spec = struct {
208 facts: []const Fact = &.{},
209 complete: bool = false,
210 capacity: Capacity = .{},
211 enumerate: ?*const fn (*const ir.Operation, *Collector) void = null,
212 };
213
214 pub const EffectOpInterface = struct {
215 pub const interface_name = "ir.interface.effects";
216 pub const id = interfaces.interfaceId(interface_name);
217
218 pub const VTable = struct {
219 capacity: Capacity,
220 collect: *const fn (*const anyopaque, *Collector) void,
221 };
222
223 pub fn entry(vtable: *const VTable) interfaces.InterfaceEntry {
224 return .{ .id = id, .vtable = vtable };
225 }
226
227 pub fn entryFor(comptime spec: Spec) interfaces.InterfaceEntry {
228 const Implementation = struct {
229 fn collect(raw: *const anyopaque, collector: *Collector) void {
230 const op: *const ir.Operation = @ptrCast(@alignCast(raw));
231 collector.complete = spec.complete;
232 for (spec.facts) |fact| collector.append(fact);
233 if (spec.enumerate) |enumerate| enumerate(op, collector);
234 }
235
236 const vtable = VTable{
237 .capacity = .{
238 .entries = spec.facts.len + spec.capacity.entries,
239 .per_operand = spec.capacity.per_operand,
240 .per_result = spec.capacity.per_result,
241 .per_region = spec.capacity.per_region,
242 },
243 .collect = collect,
244 };
245 };
246 return entry(&Implementation.vtable);
247 }
248 };
249
250 pub const Declaration = struct {
251 storage: []Fact = &.{},
252 facts: Facts = .{},
253 exhausted: bool = false,
254
255 pub fn deinit(self: *Declaration, allocator: std.mem.Allocator) void {
256 allocator.free(self.storage);
257 self.* = .{};
258 }
259 };
260
261 pub fn inspect(allocator: std.mem.Allocator, op: *ir.Operation) !Declaration {
262 const vtable = op.getInterface(EffectOpInterface) orelse return .{};
263 const count = vtable.capacity.count(
264 op.getNumOperands(),
265 op.getNumResults(),
266 op.getNumRegions(),
267 ) orelse return .{ .exhausted = true };
268 const storage = try allocator.alloc(Fact, count);
269 errdefer allocator.free(storage);
270 var collector = Collector{ .storage = storage };
271 vtable.collect(op, &collector);
272 try validate(op, collector.view());
273 return .{ .storage = storage, .facts = collector.view(), .exhausted = collector.exhausted };
274 }
275
276 /// Collect into caller-admitted storage. Recollect after every semantic edit.
277 pub fn collectInto(op: *ir.Operation, storage: []Fact) !Facts {
278 const vtable = op.getInterface(EffectOpInterface) orelse return .{};
279 var collector = Collector{ .storage = storage };
280 vtable.collect(op, &collector);
281 try validate(op, collector.view());
282 return collector.view();
283 }
284
285 pub const ShapeError = error{
286 InvalidEffectPremise,
287 InvalidEffectSubject,
288 InvalidEffectFootprint,
289 InvalidEffectScope,
290 InvalidEffectResult,
291 InvalidEffectRegion,
292 InvalidEffectBinding,
293 DuplicateEffectResult,
294 DuplicateEffectRegion,
295 DuplicateEffectBinding,
296 MissingEffectResult,
297 MissingEffectRegion,
298 ContradictoryEffectResult,
299 };
300
301 fn validateSubject(op: *const ir.Operation, subject: Subject) ShapeError!void {
302 const valid = switch (subject) {
303 .operation => true,
304 .operand => |index| index < op.getNumOperands(),
305 .result => |index| index < op.getNumResults(),
306 .global => |name| name.len > 0,
307 .region => |index| index < op.getNumRegions(),
308 };
309 if (!valid) return error.InvalidEffectSubject;
310 }
311
312 fn validateScope(op: *const ir.Operation, scope: Scope) ShapeError!void {
313 switch (scope) {
314 .unknown, .operation => {},
315 .region => |index| if (index >= op.getNumRegions()) return error.InvalidEffectScope,
316 .named => |name| if (name.len == 0) return error.InvalidEffectScope,
317 }
318 }
319
320 fn validateResource(op: *const ir.Operation, resource: Resource) ShapeError!void {
321 try validateSubject(op, resource.subject);
322 try validateScope(op, resource.alias_scope);
323 try validateScope(op, resource.lifetime);
324 try validateScope(op, resource.ordering_scope);
325 if (resource.bytes) |bytes| {
326 _ = std.math.add(u64, bytes.offset, bytes.length) catch return error.InvalidEffectFootprint;
327 }
328 if (resource.allocator_domain) |name| {
329 if (name.len == 0) return error.InvalidEffectScope;
330 }
331 if (resource.state_key) |name| {
332 if (name.len == 0) return error.InvalidEffectScope;
333 }
334 }
335
336 fn validateResult(op: *const ir.Operation, fact: ResultFact) ShapeError!void {
337 if (fact.index >= op.getNumResults()) return error.InvalidEffectResult;
338 try validateScope(op, fact.lifetime);
339 if (fact.alias) |alias| {
340 try validateSubject(op, alias);
341 if (fact.fresh_identity) return error.ContradictoryEffectResult;
342 switch (alias) {
343 .result => |index| if (index == fact.index) return error.ContradictoryEffectResult,
344 .operand, .global => {},
345 .operation, .region => return error.ContradictoryEffectResult,
346 }
347 }
348 if (fact.fresh_identity and fact.ownership == .borrowed) {
349 return error.ContradictoryEffectResult;
350 }
351 }
352
353 fn validateFact(op: *const ir.Operation, fact: Fact) ShapeError!void {
354 switch (fact) {
355 .premise => switch (fact.premise) {
356 .floating_environment => {
357 if (!op.getContext().arithmetic_policy.permitsFloatingValues()) {
358 return error.InvalidEffectPremise;
359 }
360 },
361 },
362 .event => |event| try validateResource(op, event.resource),
363 .requirement => |requirement| {
364 try validateSubject(op, requirement.subject);
365 if (requirement.related) |related| try validateSubject(op, related);
366 },
367 .result => |result| try validateResult(op, result),
368 .region => |region| {
369 if (region.index >= op.getNumRegions()) return error.InvalidEffectRegion;
370 },
371 .binding => |binding| {
372 if (binding.region >= op.getNumRegions()) return error.InvalidEffectBinding;
373 const region = &op.regions.items[binding.region];
374 const block = region.blocks.head orelse return error.InvalidEffectBinding;
375 if (binding.argument >= block.arguments.items.len) return error.InvalidEffectBinding;
376 try validateSubject(op, binding.source);
377 },
378 }
379 }
380
381 pub fn validate(op: *const ir.Operation, facts: Facts) ShapeError!void {
382 for (facts.records, 0..) |fact, index| {
383 try validateFact(op, fact);
384 for (facts.records[0..index]) |prior| {
385 try validatePair(fact, prior);
386 }
387 }
388 if (!facts.complete) return;
389 for (0..op.getNumResults()) |index| {
390 if (!hasResult(facts, index)) return error.MissingEffectResult;
391 }
392 for (0..op.getNumRegions()) |index| {
393 if (!hasRegion(facts, index)) return error.MissingEffectRegion;
394 }
395 }
396
397 fn hasResult(facts: Facts, index: usize) bool {
398 for (facts.records) |fact| {
399 if (fact == .result and fact.result.index == index) return true;
400 }
401 return false;
402 }
403
404 fn hasRegion(facts: Facts, index: usize) bool {
405 for (facts.records) |fact| {
406 if (fact == .region and fact.region.index == index) return true;
407 }
408 return false;
409 }
410
411 fn validatePair(fact: Fact, prior: Fact) ShapeError!void {
412 if (fact == .result and prior == .result and fact.result.index == prior.result.index) {
413 return error.DuplicateEffectResult;
414 }
415 if (fact == .binding and prior == .binding and fact.binding.region == prior.binding.region and
416 fact.binding.argument == prior.binding.argument) return error.DuplicateEffectBinding;
417 if (fact == .region and prior == .region and fact.region.index == prior.region.index) {
418 return error.DuplicateEffectRegion;
419 }
420 }
421
422 const Verification = struct {
423 op: *ir.Operation,
424 vtable: *const EffectOpInterface.VTable,
425 limit: usize,
426 seen: usize = 0,
427 results: usize = 0,
428 regions: usize = 0,
429 failure: ?ShapeError = null,
430
431 fn visit(raw: *anyopaque, fact: Fact) void {
432 const self: *Verification = @ptrCast(@alignCast(raw));
433 if (self.failure != null) return;
434 self.check(fact) catch |err| {
435 self.failure = err;
436 };
437 }
438
439 fn check(self: *Verification, fact: Fact) ShapeError!void {
440 try validateFact(self.op, fact);
441 var prior = PriorFacts{ .fact = fact };
442 var collector = Collector{
443 .storage = &.{},
444 .observer = .{ .context = &prior, .limit = self.seen, .visit = PriorFacts.visit },
445 };
446 self.vtable.collect(self.op, &collector);
447 if (prior.failure) |err| return err;
448 if (fact == .result) self.results += 1;
449 if (fact == .region) self.regions += 1;
450 self.seen += 1;
451 std.debug.assert(self.seen <= self.limit);
452 }
453 };
454
455 const PriorFacts = struct {
456 fact: Fact,
457 failure: ?ShapeError = null,
458
459 fn visit(raw: *anyopaque, prior: Fact) void {
460 const self: *PriorFacts = @ptrCast(@alignCast(raw));
461 validatePair(self.fact, prior) catch |err| {
462 self.failure = err;
463 };
464 }
465 };
466
467 /// Shape checking replays bounded, read-only enumeration without allocating.
468 /// Each record is checked against its preceding records; no shared IR scratch is touched.
469 pub fn verify(op: *ir.Operation) ShapeError!void {
470 const vtable = op.getInterface(EffectOpInterface) orelse return;
471 const limit = vtable.capacity.count(
472 op.getNumOperands(),
473 op.getNumResults(),
474 op.getNumRegions(),
475 ) orelse return;
476 var verification = Verification{ .op = op, .vtable = vtable, .limit = limit };
477 var collector = Collector{
478 .storage = &.{},
479 .observer = .{ .context = &verification, .limit = limit, .visit = Verification.visit },
480 };
481 vtable.collect(op, &collector);
482 if (verification.failure) |err| return err;
483 if (!collector.complete or collector.exhausted) return;
484 if (verification.results != op.getNumResults()) return error.MissingEffectResult;
485 if (verification.regions != op.getNumRegions()) return error.MissingEffectRegion;
486 }
487
488 /// T: complete, total, terminating, with only ordinary memory events.
489 pub fn total(facts: Facts) bool {
490 if (!facts.complete) return false;
491 for (facts.records) |fact| switch (fact) {
492 .event => |event| {
493 if (event.ordered) return false;
494 if (event.kind != .read and event.kind != .write) return false;
495 },
496 .requirement => return false,
497 .result => |result| {
498 if (result.fresh_identity or result.ownership != .none) return false;
499 },
500 .region => |region| {
501 if (region.execution != .latent) return false;
502 },
503 .binding, .premise => {},
504 };
505 return true;
506 }
507
508 /// R: no writes or other observable events. Completeness and totality are separate.
509 pub fn readOnly(facts: Facts) bool {
510 if (!facts.complete) return false;
511 for (facts.records) |fact| switch (fact) {
512 .event => |event| if (event.kind != .read or event.ordered) return false,
513 else => {},
514 };
515 return true;
516 }
517
518 pub fn memoryFree(facts: Facts) bool {
519 if (!facts.complete) return false;
520 for (facts.records) |fact| {
521 if (fact == .event) return false;
522 }
523 return true;
524 }
525
526 pub const Stability = struct {
527 read_values: bool = false,
528 execution_context: bool = false,
529 };
530
531 /// S: memory-free total expressions need no read proof; reads require both premises.
532 pub fn stable(facts: Facts, proof: Stability) bool {
533 if (!total(facts)) return false;
534 return memoryFree(facts) or (proof.read_values and proof.execution_context);
535 }
536
537 pub fn discard(facts: Facts) bool {
538 return total(facts) and readOnly(facts);
539 }
540
541 pub fn duplicate(facts: Facts, proof: Stability) bool {
542 return discard(facts) and stable(facts, proof);
543 }
544
545 pub fn speculate(facts_at_destination: Facts, operands_available: bool) bool {
546 return operands_available and discard(facts_at_destination);
547 }
548
549 pub fn repeatableExpression(facts: Facts) bool {
550 return discard(facts) and memoryFree(facts);
551 }
552
553 pub const Crossing = struct {
554 no_dependencies: bool = false,
555 concurrency_exclusive: bool = false,
556 };
557
558 /// Caller proves disjointness. Resource labels and different SSA names prove nothing.
559 pub fn accessConflict(a: Event, b: Event, proven_disjoint: bool) bool {
560 if (a.ordered or b.ordered) return true;
561 const a_access = a.kind == .read or a.kind == .write;
562 const b_access = b.kind == .read or b.kind == .write;
563 if (!a_access or !b_access) return true;
564 if (a.kind == .read and b.kind == .read) return false;
565 return !proven_disjoint;
566 }
567
568 pub fn reorder(a: Facts, b: Facts, crossing: Crossing) bool {
569 if (!crossing.no_dependencies or !total(a) or !total(b)) return false;
570 if ((!memoryFree(a) or !memoryFree(b)) and !crossing.concurrency_exclusive) return false;
571 for (a.records) |left| {
572 if (left != .event) continue;
573 for (b.records) |right| {
574 if (right != .event) continue;
575 if (accessConflict(left.event, right.event, false)) return false;
576 }
577 }
578 return true;
579 }
580
581 fn expectNoPermissions(facts: Facts) !void {
582 try std.testing.expect(!discard(facts));
583 try std.testing.expect(!duplicate(facts, .{ .read_values = true, .execution_context = true }));
584 try std.testing.expect(!speculate(facts, true));
585 try std.testing.expect(!repeatableExpression(facts));
586 try std.testing.expect(!reorder(facts, .{ .complete = true }, .{
587 .no_dependencies = true,
588 .concurrency_exclusive = true,
589 }));
590 }
591
592 test "EffectOpInterface unknown and unresolved requirements deny every permission" {
593 try expectNoPermissions(.{});
594 try expectNoPermissions(.{ .records = &.{.{ .event = .{ .kind = .read } }} });
595 for (std.enums.values(RequirementKind)) |kind| {
596 try expectNoPermissions(.{
597 .complete = true,
598 .records = &.{.{ .requirement = .{ .kind = kind, .subject = .operation } }},
599 });
600 }
601 }
602
603 test "EffectOpInterface total expressions and safe reads have distinct permissions" {
604 const expression = Facts{ .complete = true };
605 try std.testing.expect(discard(expression));
606 try std.testing.expect(duplicate(expression, .{}));
607 try std.testing.expect(speculate(expression, true));
608 try std.testing.expect(!speculate(expression, false));
609 try std.testing.expect(repeatableExpression(expression));
610 const read = Facts{ .complete = true, .records = &.{.{ .event = .{ .kind = .read } }} };
611 try std.testing.expect(discard(read));
612 try std.testing.expect(speculate(read, true));
613 try std.testing.expect(!duplicate(read, .{}));
614 try std.testing.expect(!repeatableExpression(read));
615 try std.testing.expect(duplicate(read, .{ .read_values = true, .execution_context = true }));
616 try std.testing.expect(!duplicate(read, .{ .read_values = true }));
617 }
618
619 test "EffectOpInterface failure ownership allocation state and ordering remain barriers" {
620 for (std.enums.values(EventKind)) |kind| {
621 if (kind == .read or kind == .write) continue;
622 try expectNoPermissions(.{ .complete = true, .records = &.{.{
623 .event = .{ .kind = kind },
624 }} });
625 }
626 const ordered_read = Facts{ .complete = true, .records = &.{.{
627 .event = .{ .kind = .read, .ordered = true },
628 }} };
629 try expectNoPermissions(ordered_read);
630 const balanced = Facts{ .complete = true, .records = &.{
631 .{ .event = .{ .kind = .retain } },
632 .{ .event = .{ .kind = .release } },
633 } };
634 try expectNoPermissions(balanced);
635 try expectNoPermissions(.{ .complete = true, .records = &.{.{
636 .result = .{ .index = 0, .ownership = .none, .fresh_identity = true },
637 }} });
638 }
639
640 test "EffectOpInterface access conflict is symmetric and ignores resource names" {
641 const accesses = [_]Event{
642 .{ .kind = .read, .resource = .{ .subject = .{ .operand = 0 } } },
643 .{ .kind = .write, .resource = .{ .subject = .{ .operand = 1 } } },
644 .{ .kind = .read, .resource = .{ .subject = .{ .global = "different" } } },
645 };
646 for (accesses) |a| for (accesses) |b| {
647 const expected = a.kind == .write or b.kind == .write;
648 try std.testing.expectEqual(expected, accessConflict(a, b, false));
649 try std.testing.expectEqual(accessConflict(a, b, false), accessConflict(b, a, false));
650 try std.testing.expect(!accessConflict(a, b, true));
651 };
652 try std.testing.expect(accessConflict(.{ .kind = .release }, accesses[0], true));
653 const read = Facts{ .complete = true, .records = &.{.{ .event = accesses[0] }} };
654 try std.testing.expect(!reorder(read, read, .{ .no_dependencies = true }));
655 try std.testing.expect(reorder(read, read, .{
656 .no_dependencies = true,
657 .concurrency_exclusive = true,
658 }));
659 }
660
661 test "EffectOpInterface collector derives capacity and preserves event order on overflow" {
662 const capacity = Capacity{ .entries = 2, .per_operand = 2, .per_result = 1, .per_region = 3 };
663 try std.testing.expectEqual(@as(?usize, 16), capacity.count(3, 2, 2));
664 try std.testing.expectEqual(@as(?usize, null), capacity.count(std.math.maxInt(usize), 0, 0));
665 var storage: [2]Fact = undefined;
666 var collector = Collector{ .storage = &storage, .complete = true };
667 collector.append(.{ .event = .{ .kind = .write } });
668 collector.append(.{ .event = .{ .kind = .failure } });
669 try std.testing.expect(collector.view().complete);
670 try std.testing.expectEqual(EventKind.write, collector.view().records[0].event.kind);
671 try std.testing.expectEqual(EventKind.failure, collector.view().records[1].event.kind);
672 collector.append(.{ .event = .{ .kind = .read } });
673 try std.testing.expect(collector.exhausted);
674 try std.testing.expectEqual(@as(usize, 2), collector.view().records.len);
675 try expectNoPermissions(collector.view());
676 var empty = Collector{ .storage = &.{}, .complete = true };
677 empty.append(.{ .event = .{ .kind = .failure } });
678 try expectNoPermissions(empty.view());
679 }
680
681 fn testEnumerateValues(op: *const ir.Operation, collector: *Collector) void {
682 collector.valueResults(op);
683 collector.complete = op.getAttr("incomplete") == null;
684 }
685
686 test "EffectOpInterface registers static and dynamic facts through loadDialectSpec" {
687 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
688 defer ctx.deinit(std.testing.allocator);
689 try ir.dialects.loadDialectSpec(&ctx, .{
690 .name = "effects_fixture",
691 .operations = &.{
692 .{ .name = "effects_fixture.static", .interfaces = &.{EffectOpInterface.entryFor(.{
693 .complete = true,
694 })} },
695 .{ .name = "effects_fixture.dynamic", .interfaces = &.{EffectOpInterface.entryFor(.{
696 .capacity = .{ .per_result = 1 },
697 .enumerate = testEnumerateValues,
698 })} },
699 },
700 });
701 const static = try ctx.createOperation(ir.Operation.State.init(
702 "effects_fixture.static",
703 .unknown,
704 ));
705 const dynamic = try ctx.createOperation(ir.Operation.State.init(
706 "effects_fixture.dynamic",
707 .unknown,
708 ));
709 for ([_]*ir.Operation{ static, dynamic }) |op| {
710 var declaration = try inspect(std.testing.allocator, op);
711 defer declaration.deinit(std.testing.allocator);
712 try std.testing.expect(discard(declaration.facts));
713 try ir.verifyOperation(op, .{});
714 }
715 try dynamic.setAttr("incomplete", try ctx.getBoolAttr(true));
716 var incomplete = try inspect(std.testing.allocator, dynamic);
717 defer incomplete.deinit(std.testing.allocator);
718 try expectNoPermissions(incomplete.facts);
719 try std.testing.expectEqual(
720 interfaces.interfaceId("ir.interface.effects"),
721 EffectOpInterface.id,
722 );
723 }
724
725 fn testResultOperation(ctx: *ir.Context) !*ir.Operation {
726 try ctx.allowUnregistered();
727 _ = try ctx.registerOperation("effect_result", .{});
728 _ = try ctx.registerType("effect_value");
729 const typ = try ctx.getDialectTypeFromName("effect_value");
730 var state = ir.Operation.State.init("effect_result", .unknown);
731 state.addTypes(&.{typ});
732 state.addRegion();
733 return ctx.createOperation(state);
734 }
735
736 test "EffectOpInterface validates result and region coverage and contradictions" {
737 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
738 defer ctx.deinit(std.testing.allocator);
739 const op = try testResultOperation(&ctx);
740 const block = try op.getRegion(0).?.addBlock();
741 _ = try block.addArgument(op.getResult(0).?.type, .unknown);
742 const binding = Fact{ .binding = .{
743 .region = 0,
744 .argument = 0,
745 .source = .{ .global = "argument" },
746 } };
747 try std.testing.expectError(
748 error.DuplicateEffectBinding,
749 validate(op, .{ .records = &.{ binding, binding } }),
750 );
751 const result = Fact{ .result = .{ .index = 0, .ownership = .none } };
752 const region = Fact{ .region = .{ .index = 0, .execution = .latent } };
753 try validate(op, .{ .complete = true, .records = &.{ result, region } });
754 try std.testing.expectError(error.MissingEffectResult, validate(op, .{ .complete = true }));
755 try std.testing.expectError(error.MissingEffectRegion, validate(op, .{
756 .complete = true,
757 .records = &.{result},
758 }));
759 try std.testing.expectError(error.DuplicateEffectResult, validate(op, .{
760 .records = &.{ result, result },
761 }));
762 try std.testing.expectError(error.InvalidEffectSubject, validate(op, .{ .records = &.{.{
763 .event = .{ .kind = .read, .resource = .{ .subject = .{ .operand = 0 } } },
764 }} }));
765 try std.testing.expectError(error.ContradictoryEffectResult, validate(op, .{ .records = &.{.{
766 .result = .{ .index = 0, .fresh_identity = true, .alias = .{ .global = "escaped" } },
767 }} }));
768 try expectNoPermissions(.{ .complete = true, .records = &.{
769 result, .{ .region = .{ .index = 0, .execution = .repeated } },
770 } });
771 }
772
773 test "EffectOpInterface normal verifier reports malformed facts and OOM stays an error" {
774 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
775 defer ctx.deinit(std.testing.allocator);
776 try ctx.registerOperationInterface("effect_bad", EffectOpInterface.entryFor(.{
777 .complete = true,
778 .facts = &.{.{ .event = .{
779 .kind = .read,
780 .resource = .{ .subject = .{ .operand = 4 } },
781 } }},
782 }));
783 const op = try ctx.createOperation(ir.Operation.State.init("effect_bad", .unknown));
784 try std.testing.expectError(error.InvalidEffectSubject, ir.verifyOperation(op, .{}));
785 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
786 try std.testing.expectError(error.OutOfMemory, inspect(failing.allocator(), op));
787 }
788
789 fn testVerifyWithoutScratch(ctx: *ir.Context) !void {
790 try ctx.allowUnregistered();
791 _ = try ctx.registerOperation("effects.no_scratch", .{});
792 try ctx.registerOperationInterface("effects.no_scratch", EffectOpInterface.entryFor(.{
793 .complete = true,
794 .facts = &.{.{ .event = .{ .kind = .write } }},
795 }));
796 const op = try ctx.createOperation(ir.Operation.State.init("effects.no_scratch", .unknown));
797 const original = op.allocator;
798 defer op.allocator = original;
799 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
800 op.allocator = failing.allocator();
801 try verify(op);
802 try std.testing.expectEqual(@as(usize, 0), failing.allocations);
803 }
804
805 test "EffectOpInterface verification uses no shared operation scratch" {
806 var ctx = try ir.Context.init(std.testing.allocator, ir.Context.Limits.testing);
807 defer ctx.deinit(std.testing.allocator);
808 try testVerifyWithoutScratch(&ctx);
809 }