authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2021-12-29 20:00:39+01:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-01-01 12:59:18+01:00
logf644c8b0478126484ce2780eb98336c2b4ff0177
tree3253184b67a641f8d72d306280c0f3876ce9ff2a
parent29164a31cc1dece981588b5dd34482804524df13
signaturelock-open Commit is signed but in an unrecognized format.

wasm: Implement `array_to_slice` and bug fixes:

- Add method to easily create local for virtual stack - Ensure function pointers are passed correctly - Correctly handle slices as return types and values - Fix wrapping error sets/payloads. - Handle ptr-like optionals correctly, by using address '0' as null. - Implement `array_to_slice` - linker: Always emit a table, so call_indirect inside bodies do not fail if there's no table. TODO: Only do this when we emit a call_indirect but the relocation cannot be resolved.

3 files changed, 171 insertions(+), 38 deletions(-)

src/arch/wasm/CodeGen.zig+165-35
...@@ -1122,6 +1122,26 @@ fn moveStack(self: *Self, offset: u32, local: u32) !void {...@@ -1122,6 +1122,26 @@ fn moveStack(self: *Self, offset: u32, local: u32) !void {
1122 try self.addLabel(.global_set, 0);1122 try self.addLabel(.global_set, 0);
1123}1123}
11241124
1125/// From a given type, will create space on the virtual stack to store the value of such type.
1126/// This returns a `WValue` with its active tag set to `local`, containing the index to the local
1127/// that points to the position on the virtual stack. This function should be used instead of
1128/// moveStack unless a local was already created to store the point.
1129///
1130/// Asserts Type has codegenbits
1131fn allocStack(self: *Self, ty: Type) !WValue {
1132 assert(ty.hasCodeGenBits());
1133
1134 // calculate needed stack space
1135 const abi_size = std.math.cast(u32, ty.abiSize(self.target)) catch {
1136 return self.fail("Given type '{}' too big to fit into stack frame", .{ty});
1137 };
1138
1139 // allocate a local using wasm's pointer size
1140 const local = try self.allocLocal(Type.@"usize");
1141 try self.moveStack(abi_size, local.local);
1142 return local;
1143}
1144
1125/// From given zig bitsize, returns the wasm bitsize1145/// From given zig bitsize, returns the wasm bitsize
1126fn toWasmIntBits(bits: u16) ?u16 {1146fn toWasmIntBits(bits: u16) ?u16 {
1127 return for ([_]u16{ 32, 64 }) |wasm_bits| {1147 return for ([_]u16{ 32, 64 }) |wasm_bits| {
...@@ -1238,21 +1258,24 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -1238,21 +1258,24 @@ fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1238 try self.initializeStack();1258 try self.initializeStack();
1239 }1259 }
12401260
1241 const abi_size = child_type.abiSize(self.target);1261 if (child_type.abiSize(self.target) == 0) return WValue{ .none = {} };
1242 if (abi_size == 0) return WValue{ .none = {} };
12431262
1244 // local, containing the offset to the stack position1263 return self.allocStack(child_type);
1245 const local = try self.allocLocal(Type.initTag(.i32)); // always pointer therefore i32
1246 try self.moveStack(@intCast(u32, abi_size), local.local);
1247 return local;
1248}1264}
12491265
1250fn airRetLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {1266fn airRetLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1251 const un_op = self.air.instructions.items(.data)[inst].un_op;1267 const un_op = self.air.instructions.items(.data)[inst].un_op;
1252 const operand = self.resolveInst(un_op);1268 const operand = self.resolveInst(un_op);
1269 const ret_ty = self.air.typeOf(un_op).childType();
1270 if (!ret_ty.hasCodeGenBits()) return WValue.none;
1271
1272 if (ret_ty.isSlice() or ret_ty.zigTypeTag() == .ErrorUnion) {
1273 try self.emitWValue(operand);
1274 } else {
1275 const result = try self.load(operand, ret_ty, 0);
1276 try self.emitWValue(result);
1277 }
12531278
1254 const result = try self.load(operand, self.air.typeOf(un_op).childType(), 0);
1255 try self.emitWValue(result);
1256 try self.restoreStackPointer();1279 try self.restoreStackPointer();
1257 try self.addTag(.@"return");1280 try self.addTag(.@"return");
1258 return .none;1281 return .none;
...@@ -1287,10 +1310,22 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -1287,10 +1310,22 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
12871310
1288 const arg_ty = self.air.typeOf(arg_ref);1311 const arg_ty = self.air.typeOf(arg_ref);
1289 if (!arg_ty.hasCodeGenBits()) continue;1312 if (!arg_ty.hasCodeGenBits()) continue;
1313 // Passing constant function pointers must be turned into a stack pointer first.
1314 // This is because function pointers are stored as function table indexes,
1315 // Which means we would try to attempt to load a function pointer's value by reading
1316 // from the table index, rather than an address.
1317 var is_fn_ptr = false;
1318 if (arg_val == .constant) {
1319 if (arg_val.constant.val.castTag(.decl_ref)) |decl| {
1320 if (decl.data.ty.zigTypeTag() == .Fn) {
1321 is_fn_ptr = true;
1322 }
1323 }
1324 }
1290 switch (arg_ty.zigTypeTag()) {1325 switch (arg_ty.zigTypeTag()) {
1291 .Struct, .Pointer, .Optional, .ErrorUnion => {1326 .Struct, .Pointer, .Optional, .ErrorUnion => {
1292 // single pointer can be passed directly1327 // single pointer can be passed directly
1293 if (arg_ty.isSinglePointer() or arg_val != .constant) {1328 if ((arg_ty.isSinglePointer() and !is_fn_ptr) or arg_val != .constant) {
1294 if (arg_val == .none) {1329 if (arg_val == .none) {
1295 // when the argument is a 0-sized value, but the function1330 // when the argument is a 0-sized value, but the function
1296 // expects a non-zero typed value (such as a slice), we must emit an argument1331 // expects a non-zero typed value (such as a slice), we must emit an argument
...@@ -1302,9 +1337,7 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -1302,9 +1337,7 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1302 try self.emitWValue(arg_val);1337 try self.emitWValue(arg_val);
1303 continue;1338 continue;
1304 }1339 }
1305 const abi_size = arg_ty.abiSize(self.target);1340 const arg_local = try self.allocStack(arg_ty);
1306 const arg_local = try self.allocLocal(Type.initTag(.i32));
1307 try self.moveStack(@intCast(u32, abi_size), arg_local.local);
1308 try self.store(arg_local, arg_val, arg_ty, 0);1341 try self.store(arg_local, arg_val, arg_ty, 0);
1309 try self.emitWValue(arg_local);1342 try self.emitWValue(arg_local);
1310 },1343 },
...@@ -1336,9 +1369,28 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -1336,9 +1369,28 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1336 const ret_ty = fn_ty.fnReturnType();1369 const ret_ty = fn_ty.fnReturnType();
1337 if (!ret_ty.hasCodeGenBits()) return WValue.none;1370 if (!ret_ty.hasCodeGenBits()) return WValue.none;
13381371
1372 // slices are stored on the virtual stack, so we must pull out both ptr and len
1373 // to not overwrite the stack
1374 if (ret_ty.isSlice()) {
1375 // first load the values onto the regular stack, before we move the stack pointer
1376 // to prevent overwriting the return value.
1377 const tmp = try self.allocLocal(ret_ty);
1378 try self.addLabel(.local_set, tmp.local);
1379 const field_ty = Type.@"usize";
1380 const offset = @intCast(u32, field_ty.abiSize(self.target));
1381 const ptr_local = try self.load(tmp, field_ty, 0);
1382 const len_local = try self.load(tmp, field_ty, offset);
1383
1384 // As our values are now safe, we reserve space on the virtual stack and
1385 // store the values there.
1386 const result = try self.allocStack(ret_ty);
1387 try self.store(result, ptr_local, field_ty, 0);
1388 try self.store(result, len_local, field_ty, offset);
1389 return result;
1390 }
1391
1339 const result_local = try self.allocLocal(ret_ty);1392 const result_local = try self.allocLocal(ret_ty);
1340 try self.addLabel(.local_set, result_local.local);1393 try self.addLabel(.local_set, result_local.local);
1341 // if the result was allocated on the virtual stack, we must load
1342 return result_local;1394 return result_local;
1343}1395}
13441396
...@@ -1349,14 +1401,8 @@ fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -1349,14 +1401,8 @@ fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
1349 if (self.initial_stack_value == .none) {1401 if (self.initial_stack_value == .none) {
1350 try self.initializeStack();1402 try self.initializeStack();
1351 }1403 }
13521404 if (child_type.abiSize(self.target) == 0) return WValue{ .none = {} };
1353 const abi_size = child_type.abiSize(self.target);1405 return self.allocStack(child_type);
1354 if (abi_size == 0) return WValue{ .none = {} };
1355
1356 // local, containing the offset to the stack position
1357 const local = try self.allocLocal(Type.initTag(.i32)); // always pointer therefore i32
1358 try self.moveStack(@intCast(u32, abi_size), local.local);
1359 return local;
1360}1406}
13611407
1362fn airStore(self: *Self, inst: Air.Inst.Index) InnerError!WValue {1408fn airStore(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
...@@ -1453,12 +1499,33 @@ fn store(self: *Self, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerErro...@@ -1453,12 +1499,33 @@ fn store(self: *Self, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerErro
1453 .Pointer => {1499 .Pointer => {
1454 if (ty.isSlice() and rhs == .constant) {1500 if (ty.isSlice() and rhs == .constant) {
1455 try self.emitWValue(rhs);1501 try self.emitWValue(rhs);
1502
1503 const val = rhs.constant.val;
1456 const len_local = try self.allocLocal(Type.usize);1504 const len_local = try self.allocLocal(Type.usize);
1457 const ptr_local = try self.allocLocal(Type.usize);1505 const ptr_local = try self.allocLocal(Type.usize);
1506 const len_offset = self.target.cpu.arch.ptrBitWidth() / 8;
1507 if (val.castTag(.decl_ref)) |decl| {
1508 // for decl references we also need to retrieve the length and the original decl's pointer
1509 try self.addMemArg(.i32_load, .{ .offset = 0, .alignment = Type.@"usize".abiAlignment(self.target) });
1510 try self.addLabel(.memory_address, decl.data.link.wasm.sym_index);
1511 try self.addMemArg(
1512 .i32_load,
1513 .{ .offset = len_offset, .alignment = Type.@"usize".abiAlignment(self.target) },
1514 );
1515 }
1458 try self.addLabel(.local_set, len_local.local);1516 try self.addLabel(.local_set, len_local.local);
1459 try self.addLabel(.local_set, ptr_local.local);1517 try self.addLabel(.local_set, ptr_local.local);
1460 try self.store(lhs, ptr_local, Type.usize, 0);1518 try self.store(lhs, ptr_local, Type.usize, 0);
1461 try self.store(lhs, len_local, Type.usize, self.target.cpu.arch.ptrBitWidth() / 8);1519 try self.store(lhs, len_local, Type.usize, len_offset);
1520 return;
1521 } else if (ty.isSlice()) {
1522 // store pointer first
1523 const ptr_local = try self.load(rhs, Type.@"usize", 0);
1524 try self.store(lhs, ptr_local, Type.@"usize", 0);
1525
1526 // retrieve length from rhs, and store that alongside lhs as well
1527 const len_local = try self.load(rhs, Type.@"usize", 4);
1528 try self.store(lhs, len_local, Type.@"usize", 4);
1462 return;1529 return;
1463 }1530 }
1464 },1531 },
...@@ -1732,6 +1799,20 @@ fn emitConstant(self: *Self, val: Value, ty: Type) InnerError!void {...@@ -1732,6 +1799,20 @@ fn emitConstant(self: *Self, val: Value, ty: Type) InnerError!void {
1732 try self.addImm32(0);1799 try self.addImm32(0);
1733 }1800 }
1734 },1801 },
1802 .Struct => {
1803 const struct_data = val.castTag(.@"struct").?;
1804 // in case of structs, we reserve stack space and store it there.
1805 const result = try self.allocStack(ty);
1806
1807 const fields = ty.structFields();
1808 for (fields.values()) |field, index| {
1809 const tmp = try self.allocLocal(field.ty);
1810 try self.emitConstant(struct_data.data[index], field.ty);
1811 try self.addLabel(.local_set, tmp.local);
1812 try self.store(result, tmp, field.ty, field.offset);
1813 }
1814 try self.addLabel(.local_get, result.local);
1815 },
1735 else => |zig_type| return self.fail("Wasm TODO: emitConstant for zigTypeTag {s}", .{zig_type}),1816 else => |zig_type| return self.fail("Wasm TODO: emitConstant for zigTypeTag {s}", .{zig_type}),
1736 }1817 }
1737}1818}
...@@ -1748,7 +1829,14 @@ fn emitUndefined(self: *Self, ty: Type) InnerError!void {...@@ -1748,7 +1829,14 @@ fn emitUndefined(self: *Self, ty: Type) InnerError!void {
1748 33...64 => try self.addFloat64(@bitCast(f64, @as(u64, 0xaaaaaaaaaaaaaaaa))),1829 33...64 => try self.addFloat64(@bitCast(f64, @as(u64, 0xaaaaaaaaaaaaaaaa))),
1749 else => |bits| return self.fail("Wasm TODO: emitUndefined for float bitsize: {d}", .{bits}),1830 else => |bits| return self.fail("Wasm TODO: emitUndefined for float bitsize: {d}", .{bits}),
1750 },1831 },
1751 .Array => try self.addImm32(@bitCast(i32, @as(u32, 0xaaaaaaaa))),1832 // As arrays point to linear memory, we cannot use 0xaaaaaaaa as the wasm
1833 // validator will not accept it due to out-of-bounds memory access);
1834 .Array => try self.addImm32(@bitCast(i32, @as(u32, 0xaa))),
1835 .Struct => {
1836 // TODO: Write 0xaa to each field
1837 const result = try self.allocStack(ty);
1838 try self.addLabel(.local_get, result.local);
1839 },
1752 else => return self.fail("Wasm TODO: emitUndefined for type: {}\n", .{ty}),1840 else => return self.fail("Wasm TODO: emitUndefined for type: {}\n", .{ty}),
1753 }1841 }
1754}1842}
...@@ -2221,24 +2309,43 @@ fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -2221,24 +2309,43 @@ fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
2221 const operand = self.resolveInst(ty_op.operand);2309 const operand = self.resolveInst(ty_op.operand);
22222310
2223 const op_ty = self.air.typeOf(ty_op.operand);2311 const op_ty = self.air.typeOf(ty_op.operand);
2224 if (!op_ty.hasCodeGenBits()) return WValue.none;2312 if (!op_ty.hasCodeGenBits()) return operand;
2225 const err_ty = self.air.getRefType(ty_op.ty);2313 const err_ty = self.air.getRefType(ty_op.ty);
2226 const offset = err_ty.errorUnionSet().abiSize(self.target);2314 const offset = err_ty.errorUnionSet().abiSize(self.target);
22272315
2228 return WValue{ .local_with_offset = .{2316 const err_union = try self.allocStack(err_ty);
2229 .local = operand.local,2317 const to_store = switch (op_ty.zigTypeTag()) {
2230 .offset = @intCast(u32, offset),2318 // for those types we must load the pointer and then store
2231 } };2319 // its value
2320 .Pointer, .Optional => blk: {
2321 if (!op_ty.isPtrLikeOptional()) {
2322 return self.fail("TODO: airWrapErrUnionPayload for optional type {}", .{op_ty});
2323 }
2324 break :blk try self.load(operand, op_ty, 0);
2325 },
2326 .Int => operand,
2327 else => return self.fail("TODO: airWrapErrUnionPayload for type {}", .{op_ty}),
2328 };
2329
2330 try self.store(err_union, to_store, op_ty, @intCast(u32, offset));
2331
2332 // ensure we also write '0' to the error part, so any present stack value gets overwritten by it.
2333 const tmp_local = try self.allocLocal(err_ty.errorUnionSet()); // locals are '0' by default.
2334 try self.store(err_union, tmp_local, err_ty.errorUnionSet(), 0);
2335
2336 return err_union;
2232}2337}
22332338
2234fn airWrapErrUnionErr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {2339fn airWrapErrUnionErr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
2235 if (self.liveness.isUnused(inst)) return WValue.none;2340 if (self.liveness.isUnused(inst)) return WValue.none;
2236 const ty_op = self.air.instructions.items(.data)[inst].ty_op;2341 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
2237 const operand = self.resolveInst(ty_op.operand);2342 const operand = self.resolveInst(ty_op.operand);
2238 return WValue{ .local_with_offset = .{2343 const err_ty = self.air.getRefType(ty_op.ty);
2239 .local = operand.local,2344
2240 .offset = 0,2345 const err_union = try self.allocStack(err_ty);
2241 } };2346 // TODO: Also write 'undefined' to the payload
2347 try self.store(err_union, operand, err_ty.errorUnionSet(), 0);
2348 return err_union;
2242}2349}
22432350
2244fn airIntcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue {2351fn airIntcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
...@@ -2276,9 +2383,11 @@ fn airIsNull(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!...@@ -2276,9 +2383,11 @@ fn airIsNull(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!
2276 const un_op = self.air.instructions.items(.data)[inst].un_op;2383 const un_op = self.air.instructions.items(.data)[inst].un_op;
2277 const operand = self.resolveInst(un_op);2384 const operand = self.resolveInst(un_op);
22782385
2279 // load the null tag value2386 const op_ty = self.air.typeOf(un_op);
2280 try self.emitWValue(operand);2387 try self.emitWValue(operand);
2281 try self.addMemArg(.i32_load8_u, .{ .offset = 0, .alignment = 1 });2388 if (!op_ty.isPtrLikeOptional()) {
2389 try self.addMemArg(.i32_load8_u, .{ .offset = 0, .alignment = 1 });
2390 }
22822391
2283 // Compare the error value with '0'2392 // Compare the error value with '0'
2284 try self.addImm32(0);2393 try self.addImm32(0);
...@@ -2469,5 +2578,26 @@ fn airBoolToInt(self: *Self, inst: Air.Inst.Index) InnerError!WValue {...@@ -2469,5 +2578,26 @@ fn airBoolToInt(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
24692578
2470fn airArrayToSlice(self: *Self, inst: Air.Inst.Index) InnerError!WValue {2579fn airArrayToSlice(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
2471 const ty_op = self.air.instructions.items(.data)[inst].ty_op;2580 const ty_op = self.air.instructions.items(.data)[inst].ty_op;
2472 return self.resolveInst(ty_op.operand);2581 const operand = self.resolveInst(ty_op.operand);
2582 const array_ty = self.air.typeOf(ty_op.operand).childType();
2583 const ty = Type.@"usize";
2584 const ptr_width = @intCast(u32, ty.abiSize(self.target));
2585 const slice_ty = self.air.getRefType(ty_op.ty);
2586
2587 // create a slice on the stack
2588 const slice_local = try self.allocStack(slice_ty);
2589
2590 // store the array ptr in the slice
2591 if (array_ty.hasCodeGenBits()) {
2592 try self.store(slice_local, operand, ty, 0);
2593 }
2594
2595 // store the length of the array in the slice
2596 const len = array_ty.arrayLen();
2597 try self.addImm32(@bitCast(i32, @intCast(u32, len)));
2598 const len_local = try self.allocLocal(ty);
2599 try self.addLabel(.local_set, len_local.local);
2600 try self.store(slice_local, len_local, ty, ptr_width);
2601
2602 return slice_local;
2473}2603}
src/link/Wasm.zig+2-1
...@@ -257,6 +257,7 @@ pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {...@@ -257,6 +257,7 @@ pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {
257 if (build_options.have_llvm) {257 if (build_options.have_llvm) {
258 if (self.llvm_object) |llvm_object| return llvm_object.updateDecl(module, decl);258 if (self.llvm_object) |llvm_object| return llvm_object.updateDecl(module, decl);
259 }259 }
260 if (!decl.ty.hasCodeGenBits()) return;
260 assert(decl.link.wasm.sym_index != 0); // Must call allocateDeclIndexes()261 assert(decl.link.wasm.sym_index != 0); // Must call allocateDeclIndexes()
261262
262 decl.link.wasm.clear();263 decl.link.wasm.clear();
...@@ -700,7 +701,7 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {...@@ -700,7 +701,7 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
700701
701 // Table section702 // Table section
702 const export_table = self.base.options.export_table;703 const export_table = self.base.options.export_table;
703 if (!import_table and (self.function_table.count() > 0 or export_table)) {704 if (!import_table) {
704 const header_offset = try reserveVecSectionHeader(file);705 const header_offset = try reserveVecSectionHeader(file);
705 const writer = file.writer();706 const writer = file.writer();
706707
test/behavior.zig+4-2
...@@ -21,7 +21,11 @@ test {...@@ -21,7 +21,11 @@ test {
21 _ = @import("behavior/bugs/4769_b.zig");21 _ = @import("behavior/bugs/4769_b.zig");
22 _ = @import("behavior/bugs/4954.zig");22 _ = @import("behavior/bugs/4954.zig");
23 _ = @import("behavior/bugs/6850.zig");23 _ = @import("behavior/bugs/6850.zig");
24 _ = @import("behavior/byval_arg_var.zig");
25 _ = @import("behavior/call.zig");
26 _ = @import("behavior/defer.zig");
24 _ = @import("behavior/enum.zig");27 _ = @import("behavior/enum.zig");
28 _ = @import("behavior/error.zig");
25 _ = @import("behavior/hasdecl.zig");29 _ = @import("behavior/hasdecl.zig");
26 _ = @import("behavior/hasfield.zig");30 _ = @import("behavior/hasfield.zig");
27 _ = @import("behavior/import.zig");31 _ = @import("behavior/import.zig");
...@@ -49,8 +53,6 @@ test {...@@ -49,8 +53,6 @@ test {
49 _ = @import("behavior/byval_arg_var.zig");53 _ = @import("behavior/byval_arg_var.zig");
50 _ = @import("behavior/call.zig");54 _ = @import("behavior/call.zig");
51 _ = @import("behavior/cast.zig");55 _ = @import("behavior/cast.zig");
52 _ = @import("behavior/defer.zig");
53 _ = @import("behavior/error.zig");
54 _ = @import("behavior/fn_in_struct_in_comptime.zig");56 _ = @import("behavior/fn_in_struct_in_comptime.zig");
55 _ = @import("behavior/for.zig");57 _ = @import("behavior/for.zig");
56 _ = @import("behavior/generics.zig");58 _ = @import("behavior/generics.zig");