Repository navigation
Expand file tree
/
Copy pathvalues.zig
More file actions
536 lines (495 loc) · 23.8 KB
/
Copy pathvalues.zig
File metadata and controls
536 lines (495 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
//! Converts Zig values to Luau values and back, for the records (`bind.zig`) and the hooks
//! (`hooks.zig`):
//!
//! - Numbers are numbers, and `bool` is a boolean.
//! - An enum value is its tag's name. A value without a name is a number, and so is one whose tag
//! starts with an underscore, since those hold values whose meaning isn't known yet.
//! - Byte arrays and slices of bytes are strings, and `@Vector(3, f32)` is a Luau vector.
//! - An optional that holds nothing is nil.
//! - An object (`engine.hooks.Object`) is a handle (`objects.zig`).
//! - A list (`List`) is a table of its values in order, such as handles for a list of objects
//! (`objects.List`). Scripts can give one too, as a table of values in order.
//! - A union of booleans, numbers and strings is whichever of them the value is, as a boolean, a
//! number or a string.
//! - Plain data passed on (`data.Data`) is the copy made of it.
//! - Any other struct is a read-only table of its fields.
//!
//! Values read back are checked: a value of the wrong type, out of range or not finite raises an
//! error that names it.
const std = @import("std");
const assert = std.debug.assert;
const openreliant = @import("openreliant");
const Object = openreliant.engine.hooks.Object;
const Missile = openreliant.engine.hooks.Missile;
const Turret = openreliant.engine.hooks.Turret;
const luau = @import("luau.zig");
const State = luau.State;
const objects = @import("objects.zig");
const missiles = @import("missiles.zig");
const turrets = @import("turrets.zig");
const data = @import("data.zig");
const runtime_module = @import("runtime.zig");
/// A table scripts handed over, such as an interface, held by a reference that its holder lets go
/// (`Runtime.release`).
pub const Table = struct {
ref: luau.Ref,
};
/// A list of at most `capacity` values of `T`, which scripts get as a table of them in order.
pub fn List(comptime T: type, comptime capacity: usize) type {
return struct {
items: [capacity]T = undefined,
len: usize = 0,
pub const Item = T;
const Self = @This();
pub fn append(list: *Self, item: T) void {
list.items[list.len] = item;
list.len += 1;
}
pub fn slice(list: *const Self) []const T {
return list.items[0..list.len];
}
/// Its items, and `fill` in the places after them.
pub fn padded(list: *const Self, fill: T) [capacity]T {
var all: [capacity]T = @splat(fill);
@memcpy(all[0..list.len], list.slice());
return all;
}
};
}
/// How many values a `List` type holds at most.
pub fn capacityOf(comptime T: type) usize {
return @typeInfo(@FieldType(T, "items")).array.len;
}
/// Whether `T` is a `List`.
pub fn isList(comptime T: type) bool {
return @typeInfo(T) == .@"struct" and @hasDecl(T, "Item") and T == List(T.Item, capacityOf(T));
}
/// Pushes `value`.
pub fn push(state: *State, comptime T: type, value: T) void {
if (T == Object) return objects.push(state, value.slot());
if (T == Missile) return missiles.push(state, value.record());
if (T == Turret) return turrets.push(state, value);
if (comptime isList(T)) return pushList(state, T, value);
if (@typeInfo(T) == .@"union") {
switch (value) {
inline else => |held| return push(state, @TypeOf(held), held),
}
}
if (T == data.Data or T == Table) {
_ = state.pushRef(value.ref);
return;
}
switch (@typeInfo(T)) {
.float => state.pushNumber(value),
.int => state.pushNumber(@floatFromInt(value)),
.bool => state.pushBoolean(value),
.@"enum" => if (name(T, value)) |tag_name| state.pushString(tag_name) else state.pushNumber(@floatFromInt(@backingInt(value))),
.optional => |optional| if (value) |held| push(state, optional.child, held) else state.pushNil(),
.vector => state.pushVector(value),
.array => |array| if (array.child == u8) state.pushString(std.mem.sliceTo(&value, 0)) else pushArray(state, array.child, &value),
.pointer => |pointer| {
comptime assert(pointer.size == .slice and pointer.child == u8);
state.pushString(value);
},
.@"struct" => pushTable(state, T, value),
else => @compileError("scripts can't read a " ++ @typeName(T)),
}
}
/// Pushes a table of `list`'s values, in order.
fn pushList(state: *State, comptime T: type, list: T) void {
state.newTable(@intCast(list.len), 0);
for (list.slice(), 1..) |item, at| {
push(state, T.Item, item);
state.rawSetIndex(-2, @intCast(at));
}
}
/// Pushes `items`, an array of anything but bytes, as a read-only table of them in order.
fn pushArray(state: *State, comptime T: type, items: []const T) void {
state.newTable(@intCast(items.len), 0);
for (items, 1..) |item, at| {
push(state, T, item);
state.rawSetIndex(-2, @intCast(at));
}
state.setReadonly(-1, true);
}
/// Pushes `value`, a struct, as a read-only table of the fields scripts see (`shown`).
pub fn pushTable(state: *State, comptime T: type, value: T) void {
const fields = comptime shownFields(T);
state.newTable(0, fields.len);
inline for (fields) |field| {
push(state, field.type, @field(value, field.name));
state.rawSetField(-2, field.name);
}
state.setReadonly(-1, true);
}
/// A field of a struct, gathered from `@typeInfo`'s lists of names, types and attributes.
pub const Field = struct {
name: [:0]const u8,
type: type,
attrs: std.lang.Type.Struct.FieldAttributes,
/// The field's default value, if it has one.
pub inline fn defaultValue(comptime field: Field) ?field.type {
return field.attrs.defaultValue(field.type);
}
};
/// The fields of `T` that scripts see: all except those starting with an underscore (`shown`).
pub fn shownFields(comptime T: type) []const Field {
comptime {
const info = @typeInfo(T).@"struct";
var fields: []const Field = &.{};
for (info.field_names, info.field_types, info.field_attrs) |field_name, Type, attrs| {
if (shown(field_name)) fields = fields ++ .{Field{ .name = field_name, .type = Type, .attrs = attrs }};
}
return fields;
}
}
/// The value at `given` as a `T`. Raises an error if it doesn't fit; `label` names the value in
/// the message.
pub fn read(state: *State, comptime T: type, given: i32, comptime label: []const u8) T {
if (T == Object) return .of(objects.read(state, given, label));
if (T == objects.Handle) return (state.toUserdata(objects.Handle, given, objects.Handle.tag) orelse wrongType(state, label, "an object", given)).*;
if (T == Missile) return .of(missiles.read(state, given, label));
if (T == missiles.Handle) return (state.toUserdata(missiles.Handle, given, missiles.Handle.tag) orelse wrongType(state, label, "a missile", given)).*;
if (T == Turret) return turrets.read(state, given, label);
if (T == turrets.Handle) return (state.toUserdata(turrets.Handle, given, turrets.Handle.tag) orelse wrongType(state, label, "a turret", given)).*;
if (T == data.Data) {
data.copy(state, given, label);
defer state.pop(1);
return .{ .ref = state.ref(-1) };
}
if (comptime isList(T)) return readList(state, T, given, label);
switch (@typeInfo(T)) {
.float => {
const number = state.toNumber(given) orelse wrongType(state, label, "a number", given);
const narrowed = std.math.lossyCast(T, number);
if (!std.math.isFinite(narrowed)) state.raise("{s}: expected a finite number, got {d}", .{ label, number });
return narrowed;
},
.int => {
const number = state.toNumber(given) orelse wrongType(state, label, "a number", given);
if (number != @floor(number) or number < std.math.minInt(T) or number > std.math.maxInt(T)) {
state.raise("{s}: expected a whole number from {d} to {d}, got {d}", .{ label, std.math.minInt(T), std.math.maxInt(T), number });
}
return @intFromFloat(number);
},
.bool => {
if (state.typeOf(given) != .boolean) wrongType(state, label, "a boolean", given);
return state.toBoolean(given);
},
.@"enum" => |info| {
if (state.toString(given)) |tag_name| {
if (lookUp(state, T, tag_name)) |value| return value;
state.raise("{s}: expected {s}, got '{s}'", .{ label, comptime choices(T), tag_name });
}
const number = state.toNumber(given) orelse wrongType(state, label, comptime choices(T), given);
if (number == @floor(number) and number >= std.math.minInt(info.tag_type) and number <= std.math.maxInt(info.tag_type)) {
const raw: info.tag_type = @intFromFloat(number);
if (info.mode == .nonexhaustive) return @fromBackingInt(raw);
inline for (comptime std.enums.values(T)) |named| {
if (@backingInt(named) == raw) return named;
}
}
state.raise("{s}: expected {s}, got {d}", .{ label, comptime choices(T), number });
},
.optional => |optional| {
// Left out of a call, or nil.
switch (state.typeOf(given)) {
.nil, .none => return null,
else => {},
}
return read(state, optional.child, given, label);
},
.vector => {
const vector = state.toVector(given) orelse wrongType(state, label, "a vector", given);
if (!@reduce(.And, @abs(vector) <= @as(T, @splat(std.math.floatMax(f32))))) state.raise("{s}: expected a finite vector", .{label});
return vector;
},
.array => |array| {
if (array.child != u8) return readArray(state, T, given, label);
const text = state.toString(given) orelse wrongType(state, label, "a string", given);
if (text.len >= array.len) state.raise("{s}: expected at most {d} bytes, got {d}", .{ label, array.len - 1, text.len });
var bytes: T = @splat(0);
@memcpy(bytes[0..text.len], text);
return bytes;
},
// A string is read in place, so it lasts only as long as the call that reads it.
.pointer => |pointer| {
comptime assert(pointer.size == .slice and pointer.child == u8 and pointer.attrs.@"const");
return state.toString(given) orelse wrongType(state, label, "a string", given);
},
.@"struct" => return readTable(state, T, given, label),
.@"union" => |info| {
const kind = state.typeOf(given);
inline for (info.field_names, info.field_types) |field_name, Variant| {
if (kind == comptime luauKind(Variant)) return @unionInit(T, field_name, read(state, Variant, given, label));
}
wrongType(state, label, comptime kindNames(T), given);
},
else => @compileError("scripts can't write a " ++ @typeName(T)),
}
}
/// The kind of Luau value read for a union variant: boolean, number, string or struct table.
fn luauKind(comptime T: type) luau.Type {
return switch (@typeInfo(T)) {
.@"struct" => .table,
.bool => .boolean,
.float, .int => .number,
.pointer => .string,
else => @compileError("a union scripts give holds booleans, numbers, strings or struct tables, not " ++ @typeName(T)),
};
}
/// What a union of booleans, numbers and strings takes, for error messages: "a boolean, a number or
/// a string".
fn kindNames(comptime T: type) []const u8 {
comptime {
const fields = @typeInfo(T).@"union".field_types;
var text: []const u8 = "";
for (fields, 0..) |Variant, at| {
const separator = if (at == 0) "" else if (at == fields.len - 1) " or " else ", ";
text = text ++ separator ++ switch (luauKind(Variant)) {
.boolean => "a boolean",
.number => "a number",
.string => "a string",
.table => "a table",
else => unreachable,
};
}
return text;
}
}
/// The table at `given` as a list of `T`, its values read in order: the table's array part, up to
/// its first gap.
fn readList(state: *State, comptime T: type, given: i32, comptime label: []const u8) T {
if (state.typeOf(given) != .table) wrongType(state, label, "a list", given);
const at = state.absolute(given);
var list: T = .{};
var n: i32 = 1;
while (true) : (n += 1) {
if (state.rawGetIndex(at, n) == .nil) {
state.pop(1);
return list;
}
if (list.len == capacityOf(T)) state.raise("{s}: at most {d} values", .{ label, capacityOf(T) });
list.append(read(state, T.Item, -1, label));
state.pop(1);
}
}
/// The list at `given` as a `T`, an array of anything but bytes: the list's values in order, and
/// zero past them.
fn readArray(state: *State, comptime T: type, given: i32, comptime label: []const u8) T {
if (state.typeOf(given) != .table) wrongType(state, label, "a list", given);
const at = state.absolute(given);
const Item = @typeInfo(T).array.child;
var items: T = std.mem.zeroes(T);
for (&items, 1..) |*item, n| {
defer state.pop(1);
if (state.rawGetIndex(at, @intCast(n)) == .nil) return items;
item.* = read(state, Item, -1, label);
}
defer state.pop(1);
if (state.rawGetIndex(at, @intCast(items.len + 1)) != .nil) state.raise("{s}: at most {d} values", .{ label, items.len });
return items;
}
/// The table at `given` as a `T`: each field the table names, and the rest at their defaults. A
/// field without a default must be given, and a name `T` has no field of is an error, so a
/// misspelt one is reported. The fields scripts don't see take their defaults.
fn readTable(state: *State, comptime T: type, given: i32, comptime label: []const u8) T {
if (state.typeOf(given) != .table) wrongType(state, label, "a table", given);
const at = state.absolute(given);
const fields = comptime shownFields(T);
var value: T = undefined;
const info = @typeInfo(T).@"struct";
inline for (info.field_names, info.field_types, info.field_attrs) |field_name, Type, attrs| {
if (attrs.defaultValue(Type)) |default| @field(value, field_name) = default else if (comptime !shown(field_name)) @compileError(field_name ++ " is hidden from scripts, so it needs a default");
}
var named: std.bit_set.Static(fields.len) = .empty;
state.pushNil();
while (state.next(at)) {
// The key's type is checked first, as reading a number as a string would change it.
const key = (if (state.typeOf(-2) == .string) state.toString(-2) else null) orelse
state.raise("{s}: a table of fields has names for keys, not {s}", .{ label, state.typeName(-2) });
const found = inline for (fields, 0..) |field, place| {
if (std.mem.eql(u8, key, field.name)) {
@field(value, field.name) = read(state, field.type, -1, label ++ "." ++ field.name);
named.set(place);
break true;
}
} else false;
if (!found) state.raise("{s}: there's no field '{s}' ({s})", .{ label, key, comptime fieldNames(T) });
state.pop(1);
}
inline for (fields, 0..) |field, place| {
if (field.attrs.default_value_ptr == null and !named.isSet(place)) state.raise("{s}: the field '{s}' is missing", .{ label, field.name });
}
return value;
}
/// The names of `T`'s fields scripts see, for error messages.
fn fieldNames(comptime T: type) []const u8 {
comptime {
var text: []const u8 = "";
for (shownFields(T), 0..) |field, at| text = text ++ (if (at == 0) "" else ", ") ++ field.name;
return text;
}
}
fn wrongType(state: *State, comptime label: []const u8, comptime expected: []const u8, given: i32) noreturn {
state.raise("{s}: expected {s}, got {s}", .{ label, expected, state.typeName(given) });
}
/// The name scripts see for `value`: its tag's, but for a tag that starts with an underscore.
pub fn name(comptime T: type, value: T) ?[]const u8 {
if (comptime @hasDecl(T, "scriptName")) return value.scriptName();
const tag_name = std.enums.tagName(T, value) orelse return null;
return if (shown(tag_name)) tag_name else null;
}
/// The value named `tag_name`, if scripts see a value by that name.
pub fn byName(comptime T: type, tag_name: []const u8) ?T {
if (!shown(tag_name)) return null;
if (comptime @hasDecl(T, "fromScriptName")) return T.fromScriptName(tag_name);
return std.meta.stringToEnum(T, tag_name);
}
/// The value of `T` that a script names `text`: by its name (`byName`), or for one a mod adds, by
/// its own name in that mod's scripts (`ownName`); null where none has the name.
pub fn lookUp(state: *State, comptime T: type, text: []const u8) ?T {
if (byName(T, text)) |value| return value;
if (comptime !takesModNames(T)) return null;
var buffer: [runtime_module.max_name]u8 = undefined;
return byName(T, ownName(state, text, &buffer) orelse return null);
}
/// Whether scripts see a tag or a field by its name: all but the placeholders' names, which start
/// with an underscore and hold what isn't understood yet (`layout.isPlaceholder`).
pub fn shown(tag_name: []const u8) bool {
return tag_name.len > 0 and !openreliant.layout.isPlaceholder(tag_name);
}
/// The names of `T`'s values that scripts see.
pub fn names(comptime T: type) []const []const u8 {
// A type whose values are numbers, some with names, such as `gameobj.Type`, names them by
// another enum's tags.
if (@hasDecl(T, "Named")) return names(T.Named);
comptime {
@setEvalBranchQuota(std.enums.values(T).len * 100);
var found: []const []const u8 = &.{};
for (std.enums.values(T)) |value| {
if (shown(@tagName(value))) found = found ++ .{@tagName(value)};
}
return found;
}
}
/// Whether scripts may give a number for a value of `T`: an open enum takes any, and a value
/// without a name is only given as one.
pub fn takesNumbers(comptime T: type) bool {
if (@hasDecl(T, "Named")) return true;
return @typeInfo(T).@"enum".mode == .nonexhaustive or names(T).len < std.enums.values(T).len;
}
/// Whether a value of `T` can also be one a mod adds, by its qualified name, such as
/// `"teapot:teapot"`, or in that mod's scripts by its own name, such as `"teapot"`.
pub fn takesModNames(comptime T: type) bool {
return @hasDecl(T, "fromScriptName");
}
/// The calling mod's own name `text` qualified with the mod's, in `buffer`: `teapot` in the mod
/// `teapot` is `teapot:teapot`. Null for a name that is qualified already, or outside a mod's
/// script. The game's names come first, so a name is only taken as the mod's own where it isn't
/// one of the game's.
pub fn ownName(state: *State, text: []const u8, buffer: []u8) ?[]const u8 {
if (std.mem.findScalar(u8, text, ':') != null) return null;
const context = state.threadData(runtime_module.Context) orelse return null;
return std.fmt.bufPrint(buffer, "{s}:{s}", .{ context.modOf().qualifier(), text }) catch null;
}
/// The values an enum field accepts, for error messages.
fn choices(comptime T: type) []const u8 {
comptime {
@setEvalBranchQuota(std.enums.values(T).len * 100);
const listed = names(T);
const numbers = takesNumbers(T);
var text: []const u8 = "";
for (listed, 0..) |tag_name, at| {
const separator = if (at == 0) "" else if (at == listed.len - 1 and !numbers) " or " else ", ";
text = text ++ separator ++ "'" ++ tag_name ++ "'";
}
return if (numbers) text ++ " or a number" else text;
}
}
test "a list padded to its capacity" {
var numbers: List(f32, 4) = .{};
numbers.append(1);
numbers.append(2);
try std.testing.expectEqual([4]f32{ 1, 2, 0, 0 }, numbers.padded(0));
try std.testing.expectEqual([4]f32{ 5, 5, 5, 5 }, (List(f32, 4){}).padded(5));
}
test choices {
const Level = enum(u8) { low, high, _unknown_2 };
try std.testing.expectEqualStrings("'low', 'high' or a number", comptime choices(Level));
const Exact = enum { left, right, fore };
try std.testing.expectEqualStrings("'left', 'right' or 'fore'", comptime choices(Exact));
try std.testing.expectEqual(null, name(Level, ._unknown_2));
try std.testing.expectEqualStrings("high", name(Level, .high).?);
}
test "lists and unions of plain values are read" {
const state = State.create(luau.testing.allocate, null).?;
defer state.close();
state.openLibraries();
const Plain = union(enum) { on: bool, number: f64, text: []const u8 };
const Row = struct { name: []const u8, value: Plain };
const Rows = List(Row, 2);
const bind = @import("bind.zig");
state.pushFunction(luau.wrap(struct {
fn run(called: *State) i32 {
const rows = read(called, Rows, 1, "rows");
called.pushNumber(@floatFromInt(rows.len));
push(called, Plain, rows.items[0].value);
push(called, Plain, rows.items[1].value);
return 3;
}
}.run), "rows");
state.setGlobal("rows");
state.sandbox();
const thread = state.newSandboxedThread();
try bind.testing.runSource(thread,
\\local n, a, b = rows({ { name = "a", value = true }, { name = "b", value = "two" } })
\\assert(n == 2 and a == true and b == "two")
\\local n2, c = rows({ { name = "a", value = 3.5 }, { name = "b", value = 1 } })
\\assert(n2 == 2 and c == 3.5)
);
try bind.testing.expectSourceError(thread, "rows({ { name = 'a', value = 1 }, { name = 'b', value = 2 }, { name = 'c', value = 3 } })", "at most 2 values");
try bind.testing.expectSourceError(thread, "rows({ { name = 'a', value = {} }, { name = 'b', value = 2 } })", "a boolean, a number or a string");
try bind.testing.expectSourceError(thread, "rows(5)", "expected a list");
}
test "a fixed list of numbers goes to scripts and back" {
const state = State.create(luau.testing.allocate, null).?;
defer state.close();
const bind = @import("bind.zig");
const Three = [3]u32;
state.pushFunction(luau.wrap(struct {
fn run(called: *State) i32 {
const given = read(called, Three, 1, "numbers");
push(called, Three, .{ given[0] * 2, given[1] * 2, given[2] * 2 });
return 1;
}
}.run), "double");
state.setGlobal("double");
state.sandbox();
const thread = state.newSandboxedThread();
// Values left out are 0, and the list scripts get is read-only.
try bind.testing.runSource(thread,
\\local twice = double({ 1, 2 })
\\assert(#twice == 3 and twice[1] == 2 and twice[2] == 4 and twice[3] == 0)
);
try bind.testing.expectSourceError(thread, "double({ 1 })[1] = 5", "readonly");
try bind.testing.expectSourceError(thread, "double({ 1, 2, 3, 4 })", "at most 3 values");
try bind.testing.expectSourceError(thread, "double(5)", "expected a list");
}
test "values go to scripts and back" {
const state = State.create(luau.testing.allocate, null).?;
defer state.close();
const Level = enum(u8) { low, high, _unknown_2, _ };
push(state, Level, .high);
try std.testing.expectEqualStrings("high", state.toString(-1).?);
try std.testing.expectEqual(Level.high, read(state, Level, -1, "level"));
push(state, Level, ._unknown_2);
try std.testing.expectEqual(2, state.toNumber(-1).?);
try std.testing.expectEqual(Level._unknown_2, read(state, Level, -1, "level"));
push(state, ?f32, null);
try std.testing.expectEqual(null, read(state, ?f32, -1, "number"));
push(state, @Vector(3, f32), .{ 1, 2, 3 });
try std.testing.expectEqual(@Vector(3, f32){ 1, 2, 3 }, read(state, @Vector(3, f32), -1, "vector"));
push(state, []const u8, "mission40.dte");
try std.testing.expectEqualStrings("mission40.dte", state.toString(-1).?);
}