lib/choir/src/properties/calls.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const choir = @import("choir");
4
5 const Mode = enum {
6 valid,
7 operand_count,
8 operand_type,
9 result_count,
10 result_type,
11 };
12
13 const modes = std.enums.values(Mode);
14
15 fn settings() hypothesis.Settings {
16 var value = hypothesis.Settings.quick()
17 .withSeed(0xf00c_ca11_2026_0710)
18 .withDatabase("zig-out/hypothesis-failures/choir-func-call-signatures");
19 value.max_examples = 100;
20 value.target_examples = 100;
21 return value;
22 }
23
24 fn drawUsize(conjecture: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
25 return @intCast(try conjecture.drawInteger(@intCast(min), @intCast(max), @intCast(shrink_towards)));
26 }
27
28 fn flipped(ty: choir.Type, i32_type: choir.Type, i64_type: choir.Type) choir.Type {
29 return if (ty.eql(i32_type)) i64_type else i32_type;
30 }
31
32 pub const FuncCallSignatureProperty = struct {
33 pub fn property(conjecture: *hypothesis.ConjectureData, allocator: std.mem.Allocator) !void {
34 const mode = modes[try drawUsize(conjecture, 0, modes.len - 1, 0)];
35 const has_body = try drawUsize(conjecture, 0, 1, 0) != 0;
36 var argument_count = try drawUsize(conjecture, 0, 64, 0);
37 var result_count = try drawUsize(conjecture, 0, 16, 0);
38 if (mode == .operand_type and argument_count == 0) argument_count = 1;
39 if (mode == .result_type and result_count == 0) result_count = 1;
40 try conjecture.target(argument_count, "function argument count");
41 try conjecture.target(result_count, "function result count");
42 try conjecture.target(@backingInt(mode), "signature outcome");
43
44 var ctx = try choir.Context.init(allocator, choir.Context.Limits.testing);
45 defer ctx.deinit(allocator);
46 try choir.dialects.registerAllDialects(&ctx);
47
48 const loc = choir.Location.getUnknown();
49 const i32_type = try choir.dialects.ArithDialect.getI32Type(&ctx);
50 const i64_type = try choir.dialects.ArithDialect.getScalarType(&ctx, .i64);
51
52 const argument_types = try allocator.alloc(choir.Type, argument_count);
53 defer allocator.free(argument_types);
54 for (argument_types) |*ty| {
55 ty.* = if (try drawUsize(conjecture, 0, 1, 0) == 0) i32_type else i64_type;
56 }
57
58 const result_types = try allocator.alloc(choir.Type, result_count);
59 defer allocator.free(result_types);
60 for (result_types) |*ty| {
61 ty.* = if (try drawUsize(conjecture, 0, 1, 0) == 0) i32_type else i64_type;
62 }
63
64 const call_argument_count = if (mode == .operand_count)
65 if (argument_count == 0) 1 else argument_count - 1
66 else
67 argument_count;
68 const call_argument_types = try allocator.alloc(choir.Type, call_argument_count);
69 defer allocator.free(call_argument_types);
70 for (call_argument_types, 0..) |*ty, index| {
71 ty.* = if (index < argument_types.len) argument_types[index] else i32_type;
72 }
73 if (mode == .operand_type) {
74 const index = try drawUsize(conjecture, 0, call_argument_types.len - 1, 0);
75 call_argument_types[index] = flipped(call_argument_types[index], i32_type, i64_type);
76 }
77
78 const call_result_count = if (mode == .result_count)
79 if (result_count == 0) 1 else result_count - 1
80 else
81 result_count;
82 const call_result_types = try allocator.alloc(choir.Type, call_result_count);
83 defer allocator.free(call_result_types);
84 for (call_result_types, 0..) |*ty, index| {
85 ty.* = if (index < result_types.len) result_types[index] else i32_type;
86 }
87 if (mode == .result_type) {
88 const index = try drawUsize(conjecture, 0, call_result_types.len - 1, 0);
89 call_result_types[index] = flipped(call_result_types[index], i32_type, i64_type);
90 }
91
92 const module = try choir.dialects.BuiltinDialect.ModuleOp.create(&ctx, loc);
93 const module_block = module.getBodyBlock();
94 const callee = if (has_body)
95 try choir.dialects.FuncDialect.FuncOp.create(&ctx, loc, "callee", argument_types, result_types)
96 else
97 try choir.dialects.FuncDialect.FuncOp.createDeclaration(&ctx, loc, "callee", argument_types, result_types);
98 try module_block.addOperation(callee.op);
99
100 var caller = try choir.dialects.FuncDialect.FuncOp.create(&ctx, loc, "caller", call_argument_types, &.{});
101 try module_block.addOperation(caller.op);
102 const call_operands = try allocator.alloc(*choir.Value, call_argument_count);
103 defer allocator.free(call_operands);
104 for (call_operands, 0..) |*operand, index| {
105 operand.* = caller.getArgument(index);
106 }
107 const call = try choir.dialects.FuncDialect.CallOp.create(
108 &ctx,
109 loc,
110 "callee",
111 call_operands,
112 call_result_types,
113 );
114 try caller.getEntryBlock().addOperation(call.op);
115
116 const result = choir.ir.verifyOperation(module.op, choir.ir.verify.default_options);
117 switch (mode) {
118 .valid => try result,
119 .operand_count => try std.testing.expectError(choir.dialects.func.FuncVerifyError.CallOperandCountMismatch, result),
120 .operand_type => try std.testing.expectError(choir.dialects.func.FuncVerifyError.CallOperandTypeMismatch, result),
121 .result_count => try std.testing.expectError(choir.dialects.func.FuncVerifyError.CallResultCountMismatch, result),
122 .result_type => try std.testing.expectError(choir.dialects.func.FuncVerifyError.CallResultTypeMismatch, result),
123 }
124 }
125 };
126
127 test "property: func calls verify generated body and declaration signatures" {
128 try hypothesis.checkNamed(FuncCallSignatureProperty, "choir-func-call-signatures", settings());
129 }