Repository navigation
Expand file tree
/
Copy pathapi.zig
More file actions
235 lines (212 loc) · 11 KB
/
Copy pathapi.zig
File metadata and controls
235 lines (212 loc) · 11 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
//! The declarations that scripts' packages and handles are made from
//! ([#498](https://github.com/OpenReliant/openreliant/issues/498)). A package, such as
//! `openreliant.world`, is a Zig namespace whose public declarations are its fields (`Field`) and
//! its functions (`Function`), and a handle's fields and methods are declared the same way. The
//! bindings, the reference page and the definitions for editors are all made from these
//! declarations at compile time, so what scripts see is declared in one place.
//!
//! - A field has a type, a description, and a getter; with a setter, scripts can change it.
//! - A function has a description and its parameters' names. Its Zig parameters give their types:
//! the first is a `Call`, and the rest are what scripts pass, read as `values.read` reads them.
//! Its result is what scripts get, pushed as `values.push` pushes it.
const std = @import("std");
const luau = @import("luau.zig");
const State = luau.State;
const values = @import("values.zig");
const script = @import("script.zig");
const runtime_module = @import("runtime.zig");
const Runtime = runtime_module.Runtime;
const Context = runtime_module.Context;
/// What a declared function gets besides what the script passes: the state it runs on, the script
/// that calls it, and what the script called.
pub const Call = struct {
state: *State,
context: *Context,
/// What the script called, as scripts name it, such as `hud.text`, `object:give_order` or
/// `object.throttle`, which each error the call raises starts with.
label: []const u8,
pub fn runtime(call: Call) *Runtime {
return call.context.runtime;
}
/// Raises an error from the script's call, after the call's label. Never returns.
pub fn raise(call: Call, comptime format: []const u8, arguments: anytype) noreturn {
call.state.raise("{s}: " ++ format, .{call.label} ++ arguments);
}
/// The calling mod's name `local` qualified with the mod's own, in `buffer`: `crt` in the mod
/// `retro` is `retro:crt`, and `retro:crt` stays as it is. Raises an error if the name isn't an
/// identifier.
pub fn qualified(call: Call, local: []const u8, buffer: *[runtime_module.max_name]u8) []const u8 {
const mod = call.context.modOf().qualifier();
const own = if (std.mem.startsWith(u8, local, mod) and local.len > mod.len and local[mod.len] == ':') local[mod.len + 1 ..] else local;
if (!@import("openreliant").dte.source.validId(own)) call.raise("a name must be an identifier, not '{s}'", .{local});
return std.mem.print(buffer, "{s}:{s}", .{ mod, own }) catch call.raise("the name '{s}' is too long", .{local});
}
/// The qualified name that `name` stands for, as scripts name what the mods register, in
/// `buffer`: the calling mod's own name, such as `crt` in the mod `retro`, becomes `retro:crt`,
/// and any mod's qualified name stays as it is. Null for a name that can't be one.
pub fn named(call: Call, name: []const u8, buffer: *[runtime_module.max_name]u8) ?[]const u8 {
if (std.mem.findScalar(u8, name, ':') != null) return name;
if (!@import("openreliant").dte.source.validId(name)) return null;
return std.mem.print(buffer, "{s}:{s}", .{ call.context.modOf().qualifier(), name }) catch null;
}
/// The call of `label` by the script running on `state`. Raises an error if no mod's script
/// runs there.
pub fn of(state: *State, label: []const u8) Call {
const context = state.threadData(Context) orelse state.raise("{s} can only be used by mod scripts", .{label});
if (context.closed) state.raise("{s}: this script has stopped", .{label});
return .{ .state = state, .context = context, .label = label };
}
};
/// What a declaration is.
pub const Declaration = enum { field, function };
/// A field of type `T`, described by `about`, which `access` reads with its `get`, and changes with
/// its `set` where it has one. For a package's field they take a `Call`; for a handle's, the
/// receiver's arguments (`objects.fields`).
pub fn Field(comptime T: type, comptime about: []const u8, comptime access: type) type {
return struct {
pub const declaration: Declaration = .field;
pub const Type = T;
pub const description = about;
pub const get = access.get;
pub const writable = @hasDecl(access, "set");
pub const set = if (writable) access.set else {};
};
}
/// A function described by `about`, whose parameters scripts pass are named `parameters`. `function`
/// takes a `Call`, then those parameters.
pub fn Function(comptime about: []const u8, comptime parameters: []const []const u8, comptime function: anytype) type {
const info = @typeInfo(@TypeOf(function)).@"fn";
if (info.param_types.len == 0 or info.param_types[0] != Call) @compileError("a declared function takes a Call first");
if (info.param_types.len - 1 != parameters.len) @compileError("name each parameter of a declared function");
return struct {
pub const declaration: Declaration = .function;
pub const description = about;
pub const names = parameters;
pub const Parameters = types: {
var found: [parameters.len]type = undefined;
for (&found, info.param_types[1..]) |*parameter, param| parameter.* = param.?;
break :types found;
};
pub const Result = info.return_type.?;
pub const call = function;
/// The C function that scripts call as `label`: reads the parameters, calls `function`,
/// and pushes its result.
pub fn wrapped(comptime label: []const u8) fn (*State) i32 {
return struct {
fn run(state: *State) i32 {
const caller: Call = .of(state, label);
var arguments: std.meta.ArgsTuple(@TypeOf(function)) = undefined;
arguments[0] = caller;
inline for (Parameters, parameters, 1..) |P, name, at| arguments[at] = values.read(state, P, at, name);
const result = @call(.auto, function, arguments);
if (Result == void) return 0;
values.push(state, Result, result);
return 1;
}
}.run;
}
};
}
/// What a function that returns nothing returns, as Luau writes it: the result of a `Native` that
/// returns nothing.
pub const nothing = "()";
/// A function described by `about` that reads what scripts pass it itself from the call's state,
/// such as one that takes a script's function, with the Luau types of its `parameters` and its
/// `result` for the reference.
pub fn Native(comptime about: []const u8, comptime parameters: []const u8, comptime result: []const u8, comptime function: fn (Call) i32) type {
return struct {
pub const declaration: Declaration = .function;
pub const description = about;
pub const luau_parameters = parameters;
pub const luau_result = result;
/// The C function that scripts call as `label`.
pub fn wrapped(comptime label: []const u8) fn (*State) i32 {
return nativeCalled(function, label);
}
};
}
/// The C function that scripts call as `label`, which hands `function` the call.
fn nativeCalled(comptime function: fn (Call) i32, comptime label: []const u8) fn (*State) i32 {
return struct {
fn run(state: *State) i32 {
return function(.of(state, label));
}
}.run;
}
/// A function like `Native`, whose Luau type is `luau_type`, a type the definitions name, such as
/// `HooksAdd`, the type of `hooks.add`.
pub fn NativeTyped(comptime about: []const u8, comptime luau_type: []const u8, comptime function: fn (Call) i32) type {
return struct {
pub const declaration: Declaration = .function;
pub const description = about;
pub const luau_function = luau_type;
/// The C function that scripts call as `label`.
pub fn wrapped(comptime label: []const u8) fn (*State) i32 {
return nativeCalled(function, label);
}
};
}
/// Whether `D` is a declaration of `kind` (`Field`, `Function`).
pub fn is(comptime D: anytype, comptime kind: Declaration) bool {
return @TypeOf(D) == type and @hasDecl(D, "declaration") and D.declaration == kind;
}
/// The names of `Namespace`'s declarations of `kind`, in the order they're declared.
pub fn declared(comptime Namespace: type, comptime kind: Declaration) []const []const u8 {
comptime {
var found: []const []const u8 = &.{};
for (std.meta.declarations(Namespace)) |decl_name| {
if (is(@field(Namespace, decl_name), kind)) found = found ++ .{decl_name};
}
return found;
}
}
/// Pushes `package`, declared by `Package`: a read-only table of its functions, which reads its
/// fields as scripts look them up. Scripts call its functions and fields by `prefix`, a dot and
/// their names, such as `hud.text`.
pub fn pushPackage(state: *State, comptime package: script.Package, comptime Package: type, comptime prefix: []const u8) void {
const functions = comptime declared(Package, .function);
state.newTable(0, functions.len);
inline for (functions) |name| {
state.pushFunction(luau.wrap(@field(Package, name).wrapped(prefix ++ "." ++ name)), name ++ "");
state.rawSetField(-2, name ++ "");
}
if (comptime declared(Package, .field).len > 0) {
state.newTable(0, 2);
state.pushFunction(luau.wrap(PackageFields(package, Package, prefix).get), "__index");
state.rawSetField(-2, "__index");
state.lockMetatable();
state.setReadonly(-1, true);
state.setMetatable(-2);
}
state.setReadonly(-1, true);
}
/// Reads the fields of `package`, declared by `Package`, which scripts call by `prefix`, a dot and
/// their names.
fn PackageFields(comptime package: script.Package, comptime Package: type, comptime prefix: []const u8) type {
return struct {
/// `__index`: a field's value.
fn get(state: *State) i32 {
const key = state.toString(2) orelse state.raise("expected a field's name, got {s}", .{state.typeName(2)});
inline for (comptime declared(Package, .field)) |name| {
if (std.mem.eql(u8, key, name)) {
const field = @field(Package, name);
values.push(state, field.Type, field.get(.of(state, prefix ++ "." ++ name)));
return 1;
}
}
state.raise("{s}{t} has no field '{s}'", .{ script.Package.prefix, package, key });
}
};
}
test Function {
const Example = struct {
fn twice(_: Call, number: f32, label: []const u8) f32 {
return if (std.mem.eql(u8, label, "double")) number * 2 else number;
}
};
const twice = Function("Doubles a number.", &.{ "number", "label" }, Example.twice);
try std.testing.expectEqual(2, twice.Parameters.len);
try std.testing.expectEqual(f32, twice.Result);
try std.testing.expect(is(twice, .function));
try std.testing.expect(!is(twice, .field));
}