@trioforge/crucible 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/client/package.json +4 -0
  3. package/emit/package.json +4 -0
  4. package/out/all.d.ts +7 -0
  5. package/out/all.luau +2 -0
  6. package/out/attribute.d.ts +7 -0
  7. package/out/attribute.luau +7 -0
  8. package/out/client/index.d.ts +10 -0
  9. package/out/client/init.luau +55 -0
  10. package/out/configs.d.ts +37 -0
  11. package/out/configs.luau +46 -0
  12. package/out/crucible-behaviour.d.ts +16 -0
  13. package/out/crucible-behaviour.luau +78 -0
  14. package/out/crucible-object.d.ts +10 -0
  15. package/out/crucible-object.luau +69 -0
  16. package/out/data.d.ts +224 -0
  17. package/out/data.luau +97 -0
  18. package/out/emit/configs.d.ts +4 -0
  19. package/out/emit/configs.luau +22 -0
  20. package/out/emit/data.d.ts +6 -0
  21. package/out/emit/data.luau +19 -0
  22. package/out/emit/index.d.ts +45 -0
  23. package/out/emit/init.luau +101 -0
  24. package/out/emit/network.d.ts +6 -0
  25. package/out/emit/network.luau +36 -0
  26. package/out/emit/replicated.d.ts +4 -0
  27. package/out/emit/replicated.luau +13 -0
  28. package/out/index.d.ts +20 -0
  29. package/out/init.luau +34 -0
  30. package/out/inject.d.ts +1 -0
  31. package/out/inject.luau +7 -0
  32. package/out/internal/binding.d.ts +1 -0
  33. package/out/internal/binding.luau +615 -0
  34. package/out/internal/boot.d.ts +1 -0
  35. package/out/internal/boot.luau +562 -0
  36. package/out/internal/configs/limits.d.ts +10 -0
  37. package/out/internal/configs/limits.luau +25 -0
  38. package/out/internal/configs/port.d.ts +29 -0
  39. package/out/internal/configs/port.luau +99 -0
  40. package/out/internal/configs/runtime.d.ts +21 -0
  41. package/out/internal/configs/runtime.luau +981 -0
  42. package/out/internal/construction-claim.d.ts +1 -0
  43. package/out/internal/construction-claim.luau +26 -0
  44. package/out/internal/data/admission.d.ts +1 -0
  45. package/out/internal/data/admission.luau +673 -0
  46. package/out/internal/data/declarations.d.ts +1 -0
  47. package/out/internal/data/declarations.luau +384 -0
  48. package/out/internal/data/envelope.d.ts +1 -0
  49. package/out/internal/data/envelope.luau +318 -0
  50. package/out/internal/data/handles.d.ts +1 -0
  51. package/out/internal/data/handles.luau +255 -0
  52. package/out/internal/data/inbox-transforms.d.ts +1 -0
  53. package/out/internal/data/inbox-transforms.luau +458 -0
  54. package/out/internal/data/inbox.d.ts +1 -0
  55. package/out/internal/data/inbox.luau +437 -0
  56. package/out/internal/data/lease.d.ts +1 -0
  57. package/out/internal/data/lease.luau +304 -0
  58. package/out/internal/data/limits.d.ts +1 -0
  59. package/out/internal/data/limits.luau +234 -0
  60. package/out/internal/data/messages.d.ts +1 -0
  61. package/out/internal/data/messages.luau +69 -0
  62. package/out/internal/data/operation-id.d.ts +1 -0
  63. package/out/internal/data/operation-id.luau +61 -0
  64. package/out/internal/data/paths.d.ts +2 -0
  65. package/out/internal/data/paths.luau +323 -0
  66. package/out/internal/data/port.d.ts +1 -0
  67. package/out/internal/data/port.luau +290 -0
  68. package/out/internal/data/profiles.d.ts +1 -0
  69. package/out/internal/data/profiles.luau +862 -0
  70. package/out/internal/data/receipts.d.ts +1 -0
  71. package/out/internal/data/receipts.luau +190 -0
  72. package/out/internal/data/record-bindings.d.ts +1 -0
  73. package/out/internal/data/record-bindings.luau +92 -0
  74. package/out/internal/data/record-declarations.d.ts +1 -0
  75. package/out/internal/data/record-declarations.luau +140 -0
  76. package/out/internal/data/records.d.ts +1 -0
  77. package/out/internal/data/records.luau +400 -0
  78. package/out/internal/data/runtime.d.ts +1 -0
  79. package/out/internal/data/runtime.luau +918 -0
  80. package/out/internal/data/scheduler.d.ts +1 -0
  81. package/out/internal/data/scheduler.luau +507 -0
  82. package/out/internal/data/session.d.ts +1 -0
  83. package/out/internal/data/session.luau +234 -0
  84. package/out/internal/data/shutdown.d.ts +1 -0
  85. package/out/internal/data/shutdown.luau +91 -0
  86. package/out/internal/data/waiters.d.ts +1 -0
  87. package/out/internal/data/waiters.luau +65 -0
  88. package/out/internal/dense-list.d.ts +1 -0
  89. package/out/internal/dense-list.luau +155 -0
  90. package/out/internal/diagnostics.d.ts +1 -0
  91. package/out/internal/diagnostics.luau +29 -0
  92. package/out/internal/hooks.d.ts +1 -0
  93. package/out/internal/hooks.luau +53 -0
  94. package/out/internal/implementers.d.ts +1 -0
  95. package/out/internal/implementers.luau +119 -0
  96. package/out/internal/lifecycle.d.ts +1 -0
  97. package/out/internal/lifecycle.luau +912 -0
  98. package/out/internal/log-output.d.ts +1 -0
  99. package/out/internal/log-output.luau +106 -0
  100. package/out/internal/network/catalog.d.ts +26 -0
  101. package/out/internal/network/catalog.luau +182 -0
  102. package/out/internal/network/claims.d.ts +1 -0
  103. package/out/internal/network/claims.luau +154 -0
  104. package/out/internal/network/client-session.d.ts +1 -0
  105. package/out/internal/network/client-session.luau +403 -0
  106. package/out/internal/network/codec.d.ts +63 -0
  107. package/out/internal/network/codec.luau +1183 -0
  108. package/out/internal/network/envelope.d.ts +1 -0
  109. package/out/internal/network/envelope.luau +247 -0
  110. package/out/internal/network/limits.d.ts +26 -0
  111. package/out/internal/network/limits.luau +55 -0
  112. package/out/internal/network/port.d.ts +1 -0
  113. package/out/internal/network/port.luau +19 -0
  114. package/out/internal/network/roblox-network-port.d.ts +1 -0
  115. package/out/internal/network/roblox-network-port.luau +105 -0
  116. package/out/internal/network/schema.d.ts +72 -0
  117. package/out/internal/network/schema.luau +386 -0
  118. package/out/internal/network/server-session.d.ts +1 -0
  119. package/out/internal/network/server-session.luau +393 -0
  120. package/out/internal/network/session.d.ts +1 -0
  121. package/out/internal/network/session.luau +266 -0
  122. package/out/internal/port.d.ts +1 -0
  123. package/out/internal/port.luau +12 -0
  124. package/out/internal/reactive/context.d.ts +1 -0
  125. package/out/internal/reactive/context.luau +149 -0
  126. package/out/internal/reactive/flush.d.ts +1 -0
  127. package/out/internal/reactive/flush.luau +355 -0
  128. package/out/internal/reactive/graph.d.ts +1 -0
  129. package/out/internal/reactive/graph.luau +626 -0
  130. package/out/internal/reactive/limits.d.ts +1 -0
  131. package/out/internal/reactive/limits.luau +54 -0
  132. package/out/internal/reactive/ownership.d.ts +1 -0
  133. package/out/internal/reactive/ownership.luau +626 -0
  134. package/out/internal/registry.d.ts +1 -0
  135. package/out/internal/registry.luau +455 -0
  136. package/out/internal/roblox-port.d.ts +1 -0
  137. package/out/internal/roblox-port.luau +117 -0
  138. package/out/internal/runner.d.ts +1 -0
  139. package/out/internal/runner.luau +86 -0
  140. package/out/internal/scheduler.d.ts +1 -0
  141. package/out/internal/scheduler.luau +207 -0
  142. package/out/internal/state.d.ts +1 -0
  143. package/out/internal/state.luau +41 -0
  144. package/out/internal/sync/client-sync.d.ts +1 -0
  145. package/out/internal/sync/client-sync.luau +157 -0
  146. package/out/internal/sync/declarations.d.ts +1 -0
  147. package/out/internal/sync/declarations.luau +276 -0
  148. package/out/internal/sync/frame.d.ts +1 -0
  149. package/out/internal/sync/frame.luau +91 -0
  150. package/out/internal/sync/server-sync.d.ts +1 -0
  151. package/out/internal/sync/server-sync.luau +502 -0
  152. package/out/internal/sync/values.d.ts +1 -0
  153. package/out/internal/sync/values.luau +930 -0
  154. package/out/log-level.d.ts +7 -0
  155. package/out/log-level.luau +21 -0
  156. package/out/logging.d.ts +34 -0
  157. package/out/logging.luau +233 -0
  158. package/out/network.d.ts +66 -0
  159. package/out/network.luau +24 -0
  160. package/out/reactive.d.ts +76 -0
  161. package/out/reactive.luau +136 -0
  162. package/out/replicated.d.ts +38 -0
  163. package/out/replicated.luau +45 -0
  164. package/out/server/data.d.ts +54 -0
  165. package/out/server/data.luau +44 -0
  166. package/out/server/index.d.ts +18 -0
  167. package/out/server/init.luau +62 -0
  168. package/out/server/publish.d.ts +20 -0
  169. package/out/server/publish.luau +24 -0
  170. package/out/subsystem.d.ts +4 -0
  171. package/out/subsystem.luau +30 -0
  172. package/out/vide/index.d.ts +7 -0
  173. package/out/vide/init.luau +50 -0
  174. package/package.json +52 -5
  175. package/server/package.json +4 -0
  176. package/transformer/data.cjs +72 -0
  177. package/transformer/declarations.cjs +153 -0
  178. package/transformer/diagnostics.cjs +19 -0
  179. package/transformer/index.js +741 -0
  180. package/transformer/network.cjs +961 -0
  181. package/transformer/type-id.cjs +80 -0
  182. package/vide/package.json +4 -0
  183. package/README.md +0 -3
@@ -0,0 +1,961 @@
1
+ const path = require("node:path");
2
+ const { packageOf } = require("./type-id.cjs");
3
+ const {
4
+ limits,
5
+ dataLimits,
6
+ kinds,
7
+ depthLimits,
8
+ replica,
9
+ serverOnlyPaths,
10
+ viewNameProblems,
11
+ } = require("./declarations.cjs");
12
+
13
+ const declarationMacros = Object.keys(kinds);
14
+ const macros = [...declarationMacros, "receive"];
15
+
16
+ function createNetwork({ ts, program, checker, api, kindOf, instanceType, modifiers, isMacro }) {
17
+ const unalias = (s) => s && (s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s);
18
+ const standard = (symbol, pkg) =>
19
+ symbol?.declarations?.some((d) => d.getSourceFile().fileName.replaceAll("\\", "/").includes(`/@rbxts/${pkg}/`));
20
+ const exempt = (file) =>
21
+ program.isSourceFileDefaultLibrary(file) || /node_modules[\\/]@rbxts[\\/]/.test(file.fileName);
22
+ const global = (decl) => {
23
+ if (!ts.isExternalModule(decl.getSourceFile())) return true;
24
+ for (let p = decl.parent; p; p = p.parent)
25
+ if (ts.isModuleDeclaration(p) && p.flags & ts.NodeFlags.GlobalAugmentation) return true;
26
+ return false;
27
+ };
28
+ const flags = ts.TypeFlags;
29
+ const root = path.dirname(
30
+ program.getCompilerOptions().configFilePath ?? path.join(program.getCurrentDirectory(), "tsconfig.json"),
31
+ );
32
+ const absent = (type) => !!(type.flags & flags.Undefined) || (type.isUnion() && type.types.some(absent));
33
+ const literal = (type) => {
34
+ if (type.isStringLiteral() || type.isNumberLiteral()) return type.value;
35
+ if (type.flags & flags.BooleanLiteral) return type.intrinsicName === "true";
36
+ };
37
+ const compare = (a, b) => {
38
+ const rank = { boolean: 0, number: 1, string: 2 };
39
+ return (
40
+ rank[typeof a] - rank[typeof b] ||
41
+ (typeof a === "string" ? Buffer.compare(Buffer.from(a), Buffer.from(b)) : Number(a) - Number(b))
42
+ );
43
+ };
44
+ const sorted = (names) => names.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
45
+ const widths = new Set();
46
+ for (const sf of program.getSourceFiles()) {
47
+ if (packageOf(sf.fileName)?.json.name !== "@trioforge/crucible") continue;
48
+ const walk = (node) => {
49
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "width")
50
+ widths.add(checker.getSymbolAtLocation(node.name));
51
+ ts.forEachChild(node, walk);
52
+ };
53
+ walk(sf);
54
+ }
55
+ const width = (type) => {
56
+ if (!type.isIntersection()) return;
57
+ if (!type.types.some((t) => t.flags & flags.Number)) return;
58
+ const found = new Set();
59
+ for (const part of type.types) {
60
+ if (part.flags & flags.Number) continue;
61
+ const props = checker.getPropertiesOfType(part);
62
+ if (props.length !== 1) return;
63
+ const prop = props[0];
64
+ const name = prop.declarations?.[0]?.name;
65
+ if (
66
+ !name ||
67
+ !ts.isComputedPropertyName(name) ||
68
+ !widths.has(unalias(checker.getSymbolAtLocation(name.expression)))
69
+ )
70
+ return;
71
+ const value = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration ?? prop.declarations[0]);
72
+ for (const t of value.isUnion() ? value.types : [value]) if (t.isStringLiteral()) found.add(t.value);
73
+ }
74
+ return found.size === 1 ? [...found][0] : "conflicting";
75
+ };
76
+ const engine = (type) => {
77
+ const symbol = type.getSymbol();
78
+ if (!standard(symbol, "types")) return;
79
+ const name = symbol.name;
80
+ if (["Vector3", "Color3", "CFrame"].includes(name)) return { kind: name.toLowerCase(), runtime: name };
81
+ const instances = checker.resolveName("Instances", symbol.declarations[0], ts.SymbolFlags.Type, false);
82
+ if (instances && checker.getPropertyOfType(checker.getDeclaredTypeOfSymbol(instances), name))
83
+ return { kind: "instance", classes: [name], runtime: "Instance" };
84
+ return { kind: "unsupported" };
85
+ };
86
+ const collection = (type) => {
87
+ if (checker.isTupleType(type)) return "tuple";
88
+ if (checker.isArrayType(type)) return "array";
89
+ const symbol = type.getSymbol();
90
+ if (["Map", "ReadonlyMap"].includes(symbol?.name) && standard(symbol, "compiler-types")) return "map";
91
+ };
92
+ let declarations;
93
+ const index = () => {
94
+ if (declarations) return declarations;
95
+ declarations = [];
96
+ for (const sf of program.getSourceFiles()) {
97
+ if (sf.isDeclarationFile) continue;
98
+ const walk = (node) => {
99
+ const kind = declarationMacros.find((name) => isMacro(node, name));
100
+ if (kind)
101
+ declarations.push({
102
+ node,
103
+ kind,
104
+ name:
105
+ node.arguments[0] && ts.isStringLiteral(node.arguments[0])
106
+ ? node.arguments[0].text
107
+ : undefined,
108
+ });
109
+ ts.forEachChild(node, walk);
110
+ };
111
+ walk(sf);
112
+ }
113
+ return declarations;
114
+ };
115
+ const serverOnly = (type) => api(type.getSymbol()) === "ServerOnly";
116
+ const plainIntersection = (type) =>
117
+ type.isIntersection() &&
118
+ type.types.every(
119
+ (part) =>
120
+ part.flags & flags.Object && !part.getCallSignatures().length && !part.getConstructSignatures().length,
121
+ );
122
+ const targetOf = (node) => {
123
+ if (node.typeArguments?.length) return checker.getTypeFromTypeNode(node.typeArguments[0]);
124
+ const signature = checker.getResolvedSignature(node);
125
+ const returned = signature && checker.getReturnTypeOfSignature(signature);
126
+ if (returned && returned.objectFlags & ts.ObjectFlags.Reference) return checker.getTypeArguments(returned)[0];
127
+ };
128
+ function lowerDeclaration(node, kind, report, reached = new Set()) {
129
+ const spec = kinds[kind];
130
+ const state = spec.position !== "payload";
131
+ const persist = spec.position === "persist";
132
+ const depthLimit = depthLimits[spec.position];
133
+ const active = new Set();
134
+ const fail = (at, location, reason, code = "CRU019") => {
135
+ report(at, code, `${node.arguments[0]?.text ?? kind} ${location}: ${reason}`);
136
+ return { kind: "nil" };
137
+ };
138
+ const checkSymbols = (symbols, location) => {
139
+ for (const symbol of symbols) {
140
+ if (!symbol) continue;
141
+ reached.add(symbol);
142
+ const decls = (symbol.declarations ?? []).filter((d) => !exempt(d.getSourceFile()));
143
+ for (const decl of decls)
144
+ if (ts.isEnumMember(decl)) checkSymbols([checker.getSymbolAtLocation(decl.parent.name)], location);
145
+ if (decls.some(global) || new Set(decls.map((d) => d.getSourceFile().fileName)).size > 1)
146
+ fail(
147
+ node,
148
+ location,
149
+ `${symbol.name} must be declared in one imported module; incremental builds cannot see global declarations or merges.`,
150
+ "CRU010",
151
+ );
152
+ for (const decl of decls)
153
+ if (ts.isInterfaceDeclaration(decl) || ts.isClassDeclaration(decl))
154
+ for (const clause of decl.heritageClauses ?? [])
155
+ if (clause.token === ts.SyntaxKind.ExtendsKeyword)
156
+ for (const base of clause.types) {
157
+ references(base, location);
158
+ mention(checker.getTypeAtLocation(base), location);
159
+ }
160
+ }
161
+ };
162
+ const mentioned = new Set();
163
+ const mention = (type, location) => {
164
+ if (mentioned.has(type)) return;
165
+ mentioned.add(type);
166
+ checkSymbols([type.aliasSymbol, type.getSymbol()], location);
167
+ if (type.isUnionOrIntersection()) for (const part of type.types) mention(part, location);
168
+ if (type.objectFlags & ts.ObjectFlags.Reference)
169
+ for (const argument of checker.getTypeArguments(type)) mention(argument, location);
170
+ if (
171
+ type.objectFlags & ts.ObjectFlags.Anonymous &&
172
+ !type.getCallSignatures().length &&
173
+ !type.getConstructSignatures().length
174
+ )
175
+ for (const prop of checker.getPropertiesOfType(type)) mention(checker.getTypeOfSymbol(prop), location);
176
+ };
177
+ const declared = (type, location) => {
178
+ checkSymbols([type.aliasSymbol, type.getSymbol()], location);
179
+ for (const argument of type.aliasTypeArguments ?? []) mention(argument, location);
180
+ };
181
+ const referenced = new Set();
182
+ const references = (syntax, location) => {
183
+ if (!syntax || referenced.has(syntax)) return;
184
+ referenced.add(syntax);
185
+ if (ts.isTypeReferenceNode(syntax) || ts.isExpressionWithTypeArguments(syntax)) {
186
+ const symbol = unalias(
187
+ checker.getSymbolAtLocation(ts.isTypeReferenceNode(syntax) ? syntax.typeName : syntax.expression),
188
+ );
189
+ checkSymbols([symbol], location);
190
+ for (const decl of symbol?.declarations ?? []) {
191
+ if (exempt(decl.getSourceFile())) continue;
192
+ if (ts.isTypeAliasDeclaration(decl)) references(decl.type, location);
193
+ for (const parameter of (decl.typeParameters ?? []).slice(syntax.typeArguments?.length ?? 0))
194
+ references(parameter.default, location);
195
+ }
196
+ }
197
+ if (ts.isTypeQueryNode(syntax)) {
198
+ checkSymbols([unalias(checker.getSymbolAtLocation(syntax.exprName))], location);
199
+ const queried = checker.getTypeAtLocation(syntax.exprName);
200
+ mention(queried, location);
201
+ for (const signature of [...queried.getCallSignatures(), ...queried.getConstructSignatures()])
202
+ mention(checker.getReturnTypeOfSignature(signature), location);
203
+ }
204
+ ts.forEachChild(syntax, (child) => references(child, location));
205
+ };
206
+ for (const type of node.typeArguments ?? []) references(type, state ? "value" : "payload");
207
+ const instance = (classes, location, position) => {
208
+ if (state)
209
+ return fail(
210
+ node,
211
+ location,
212
+ persist
213
+ ? "Instances cannot be saved or replicated; store a stable id"
214
+ : "replicated state cannot hold Instances; replicate a stable id",
215
+ );
216
+ if (position.toClient && !position.nilable && !(classes.length === 1 && classes[0] === "Player"))
217
+ fail(
218
+ node,
219
+ location,
220
+ "a client may not have streamed this Instance in; admit undefined or wrap collection elements in objects with an optional field.",
221
+ "CRU020",
222
+ );
223
+ return { kind: "instance", classes: sorted(classes) };
224
+ };
225
+ const fields = (type, location, position, depth, omit) => {
226
+ if (type.getCallSignatures().length || type.getConstructSignatures().length)
227
+ return fail(node, location, "functions cannot cross the network");
228
+ const props = checker
229
+ .getPropertiesOfType(type)
230
+ .filter((p) => p.name !== omit)
231
+ .sort((a, b) => Buffer.compare(Buffer.from(a.name), Buffer.from(b.name)));
232
+ if (!props.length && omit === undefined)
233
+ return fail(
234
+ node,
235
+ location,
236
+ spec.handles
237
+ ? "an empty object type accepts anything; annotate an empty template table: {} as Record<string, number>"
238
+ : "an empty object type accepts anything; use undefined",
239
+ );
240
+ return {
241
+ kind: "object",
242
+ fields: props.map((prop) => {
243
+ const at = prop.valueDeclaration ?? prop.declarations?.[0] ?? node;
244
+ for (const decl of prop.declarations ?? [])
245
+ if (
246
+ (ts.isInterfaceDeclaration(decl.parent) || ts.isClassDeclaration(decl.parent)) &&
247
+ decl.parent.name
248
+ )
249
+ reached.add(checker.getSymbolAtLocation(decl.parent.name));
250
+ references(at.type, `${location}.${prop.name}`);
251
+ if (prop.escapedName.toString().startsWith("__@"))
252
+ fail(node, location, "symbol property names cannot cross the network");
253
+ if (ts.isNumericLiteralName(prop.name))
254
+ fail(node, `${location}.${prop.name}`, "numeric property names are not supported; use a Map");
255
+ if (prop.declarations?.some((d) => ts.isMethodSignature(d) || ts.isMethodDeclaration(d)))
256
+ fail(node, `${location}.${prop.name}`, "functions cannot cross the network");
257
+ if (spec.handles && dataLimits.RESERVED_HANDLE_NAMES.includes(prop.name))
258
+ fail(
259
+ node,
260
+ `${location}.${prop.name}`,
261
+ `\`${prop.name}\` is a handle method name (set, update, insert, remove); rename the field`,
262
+ "CRU026",
263
+ );
264
+ const value = checker.getTypeOfSymbolAtLocation(prop, at);
265
+ if (value.flags & flags.Undefined)
266
+ return [
267
+ prop.name,
268
+ fail(
269
+ node,
270
+ `${location}.${prop.name}`,
271
+ spec.handles
272
+ ? "a position that is always undefined; annotate it: undefined as string | undefined"
273
+ : "a field of type undefined carries no information; remove it",
274
+ ),
275
+ ];
276
+ return [
277
+ prop.name,
278
+ lower(
279
+ value,
280
+ `${location}.${prop.name}`,
281
+ {
282
+ toClient: position.toClient,
283
+ nilable: !!(prop.flags & ts.SymbolFlags.Optional) || absent(value),
284
+ path: position.path,
285
+ marked: position.marked,
286
+ },
287
+ depth + 1,
288
+ ),
289
+ ];
290
+ }),
291
+ };
292
+ };
293
+ const key = (type, location) => {
294
+ declared(type, location);
295
+ if (type.flags & flags.String) return { kind: "string" };
296
+ if (state && !(type.isUnion() ? type.types : [type]).every((t) => t.isStringLiteral()))
297
+ return fail(
298
+ node,
299
+ location,
300
+ "state maps are keyed by string or a union of string literals; frames carry string-keyed records",
301
+ );
302
+ const w = width(type);
303
+ if (["u8", "u16", "u32"].includes(w)) return { kind: w };
304
+ const parts = type.isUnion() ? type.types : [type];
305
+ if (parts.length > limits.MAX_UNION_BRANCHES) return fail(node, location, "too many union branches");
306
+ if (
307
+ parts.every(
308
+ (t) =>
309
+ ["string", "number"].includes(typeof literal(t)) &&
310
+ (typeof literal(t) !== "number" || Number.isFinite(literal(t))),
311
+ )
312
+ )
313
+ return {
314
+ kind: "union",
315
+ branches: parts
316
+ .map((t) => literal(t))
317
+ .sort(compare)
318
+ .map((value) => ({ kind: "literal", value })),
319
+ };
320
+ return fail(
321
+ node,
322
+ location,
323
+ "map keys must be string, u8, u16, u32 or a literal union; float keys can round to a duplicate",
324
+ );
325
+ };
326
+ const union = (type, location, position, depth) => {
327
+ const parts = type.types;
328
+ const missing = parts.some((p) => p.flags & flags.Undefined);
329
+ position = { ...position, nilable: position.nilable || missing, path: false };
330
+ const branches = missing ? [{ kind: "nil" }] : [];
331
+ const literals = [],
332
+ typed = [],
333
+ objects = [],
334
+ classes = [];
335
+ const bool = parts.filter((p) => p.flags & flags.BooleanLiteral).length === 2;
336
+ if (bool) typed.push({ runtime: "boolean", node: { kind: "bool" } });
337
+ for (const part of parts) {
338
+ declared(part, location);
339
+ if (part.flags & flags.Undefined || (bool && part.flags & flags.BooleanLiteral)) continue;
340
+ const value = literal(part);
341
+ if (value !== undefined) {
342
+ literals.push({ kind: "literal", value });
343
+ continue;
344
+ }
345
+ const e = engine(part);
346
+ if (e?.kind === "instance") {
347
+ classes.push(...e.classes);
348
+ continue;
349
+ }
350
+ if (
351
+ (part.flags & flags.Object || (state && plainIntersection(part))) &&
352
+ !e &&
353
+ !collection(part) &&
354
+ !checker.getIndexInfosOfType(part).length &&
355
+ !part.getCallSignatures().length &&
356
+ !part.getConstructSignatures().length
357
+ )
358
+ objects.push(part);
359
+ else {
360
+ const descriptor = lower(part, location, position, depth + 1);
361
+ const runtime =
362
+ e?.runtime ??
363
+ {
364
+ bool: "boolean",
365
+ f64: "number",
366
+ f32: "number",
367
+ u8: "number",
368
+ u16: "number",
369
+ u32: "number",
370
+ string: "string",
371
+ array: "table",
372
+ tuple: "table",
373
+ map: "table",
374
+ object: "table",
375
+ dynamic: "dynamic",
376
+ }[descriptor.kind];
377
+ typed.push({ runtime, node: descriptor });
378
+ }
379
+ }
380
+ if (classes.length)
381
+ typed.push({ runtime: "Instance", node: instance([...new Set(classes)], location, position) });
382
+ const variants = [];
383
+ if (objects.length > 1) {
384
+ const candidates = sorted(checker.getPropertiesOfType(objects[0]).map((p) => p.name));
385
+ let discriminant, values;
386
+ for (const name of candidates) {
387
+ const seen = new Set();
388
+ const groups = objects.map((object) => {
389
+ const prop = checker.getPropertyOfType(object, name);
390
+ if (!prop || prop.flags & ts.SymbolFlags.Optional) return;
391
+ references((prop.valueDeclaration ?? prop.declarations?.[0])?.type, `${location}.${name}`);
392
+ const t = checker.getTypeOfSymbolAtLocation(
393
+ prop,
394
+ prop.valueDeclaration ?? prop.declarations?.[0] ?? node,
395
+ );
396
+ const list = (t.isUnion() ? t.types : [t]).map(literal);
397
+ if (list.some((v) => v === undefined || seen.has(`${typeof v}:${v}`))) return;
398
+ for (const v of list) seen.add(`${typeof v}:${v}`);
399
+ return list;
400
+ });
401
+ if (groups.every(Boolean)) {
402
+ discriminant = name;
403
+ values = groups;
404
+ break;
405
+ }
406
+ }
407
+ if (discriminant === undefined)
408
+ return fail(
409
+ node,
410
+ location,
411
+ `union members ${objects.map((o) => checker.typeToString(o)).join(", ")} have no discriminant; give each a distinct literal property`,
412
+ );
413
+ if (ts.isNumericLiteralName(discriminant) || discriminant.startsWith("__@"))
414
+ return fail(node, location, "numeric or symbol property names are not supported; use a Map");
415
+ for (let i = 0; i < objects.length; i++)
416
+ for (const value of values[i])
417
+ variants.push({
418
+ kind: "variant",
419
+ key: discriminant,
420
+ value,
421
+ body: fields(objects[i], location, position, depth + 2, discriminant),
422
+ });
423
+ } else if (objects.length)
424
+ typed.push({ runtime: "table", node: lower(objects[0], location, position, depth + 1) });
425
+ if (
426
+ new Set(typed.map((t) => t.runtime)).size !== typed.length ||
427
+ (variants.length && typed.some((t) => t.runtime === "table"))
428
+ )
429
+ return fail(node, location, "union has ambiguous runtime kinds");
430
+ branches.push(
431
+ ...literals.sort((a, b) => compare(a.value, b.value)),
432
+ ...typed
433
+ .sort((a, b) => Buffer.compare(Buffer.from(a.runtime ?? ""), Buffer.from(b.runtime ?? "")))
434
+ .map((t) => t.node),
435
+ ...variants.sort((a, b) => compare(a.value, b.value)),
436
+ );
437
+ if (branches.length > limits.MAX_UNION_BRANCHES) return fail(node, location, "too many union branches");
438
+ return { kind: "union", branches };
439
+ };
440
+ const lower = (type, location, position, depth = 1) => {
441
+ declared(type, location);
442
+ if (depth > depthLimit) return fail(node, location, `schema exceeds the depth limit of ${depthLimit}`);
443
+ if (state && type.isUnion()) {
444
+ const present = type.types.filter((t) => !(t.flags & flags.Undefined));
445
+ if (present.length === 1 && present.length < type.types.length && serverOnly(present[0])) {
446
+ const inner = lower(present[0], location, position, depth);
447
+ return inner.serverOnly
448
+ ? {
449
+ kind: "union",
450
+ branches: [{ kind: "nil" }, { ...inner, serverOnly: false }],
451
+ serverOnly: true,
452
+ }
453
+ : inner;
454
+ }
455
+ }
456
+ if (state && serverOnly(type)) {
457
+ if (!spec.handles)
458
+ return fail(
459
+ node,
460
+ location,
461
+ "serverOnly marks a profileStore template field; it means nothing here",
462
+ );
463
+ if (position.root)
464
+ fail(
465
+ node,
466
+ location,
467
+ "serverOnly marks a template field, never the store's whole value; wrap the value in an object and mark the field",
468
+ );
469
+ if (position.marked) return fail(node, location, "this field is already inside a serverOnly field");
470
+ if (!position.path)
471
+ return fail(
472
+ node,
473
+ location,
474
+ "serverOnly marks a field on an object path from the template root, never an array element, a map value or a union member",
475
+ );
476
+ const inner = lower(checker.getTypeArguments(type)[0], location, { ...position, marked: true }, depth);
477
+ return { ...inner, serverOnly: true };
478
+ }
479
+ if (active.has(type)) return fail(node, location, "recursive payload type");
480
+ active.add(type);
481
+ try {
482
+ if (type.flags & flags.Any) return fail(node, location, "any cannot be checked; use unknown");
483
+ if (type.flags & flags.Unknown)
484
+ return state
485
+ ? fail(
486
+ node,
487
+ location,
488
+ "unknown cannot be stored or replicated state; give the value a declared type",
489
+ )
490
+ : { kind: "dynamic" };
491
+ if (type.flags & flags.Undefined) return { kind: "nil" };
492
+ if (type.flags & flags.Void) return fail(node, location, "use undefined for no payload");
493
+ if (
494
+ type.flags &
495
+ (flags.Never | flags.ESSymbolLike | flags.BigIntLike | flags.TemplateLiteral | flags.TypeParameter)
496
+ )
497
+ return fail(node, location, `cannot encode ${checker.typeToString(type)}`);
498
+ if (type.flags & flags.Boolean) return { kind: "bool" };
499
+ const value = literal(type);
500
+ if (value !== undefined)
501
+ return typeof value === "number" && !Number.isFinite(value)
502
+ ? fail(node, location, "nonfinite literal")
503
+ : { kind: "literal", value };
504
+ if (type.flags & flags.String) return { kind: "string" };
505
+ if (type.flags & flags.Number) return { kind: "f64" };
506
+ const w = width(type);
507
+ if (w)
508
+ return ["f64", "f32", "u8", "u16", "u32"].includes(w)
509
+ ? { kind: w }
510
+ : fail(node, location, "conflicting widths");
511
+ if (type.isUnion()) return union(type, location, position, depth);
512
+ const flat = state && plainIntersection(type);
513
+ if (type.isIntersection() && !flat)
514
+ return fail(
515
+ node,
516
+ location,
517
+ type.types.some((t) => t.flags & flags.NumberLike)
518
+ ? "only a Crucible width brand may intersect number; this brand is foreign"
519
+ : "only numeric width intersections are supported",
520
+ );
521
+ const e = engine(type);
522
+ if (e?.kind === "instance") return instance(e.classes, location, position);
523
+ if (e?.kind === "unsupported") return fail(node, location, "Roblox datatype not supported in v1");
524
+ if (e && persist)
525
+ return fail(
526
+ node,
527
+ location,
528
+ `${e.runtime} cannot be saved: a save holds JSON; store its components`,
529
+ );
530
+ if (e) return { kind: e.kind };
531
+ const c = collection(type);
532
+ const args = type.objectFlags & ts.ObjectFlags.Reference ? checker.getTypeArguments(type) : [];
533
+ const element = (t, location, value = false) => {
534
+ if (state && value) t = checker.getNonNullableType(t);
535
+ else if (absent(t))
536
+ fail(node, location, "collection elements and map values cannot admit undefined");
537
+ return lower(t, location, { toClient: position.toClient, nilable: false }, depth + 1);
538
+ };
539
+ if (c === "tuple") {
540
+ if (
541
+ type.target.elementFlags.some(
542
+ (ef) => ef & (ts.ElementFlags.Optional | ts.ElementFlags.Rest | ts.ElementFlags.Variadic),
543
+ )
544
+ )
545
+ return fail(node, location, "tuples must be fixed length with no optional or rest elements");
546
+ return { kind: "tuple", elements: args.map((t, i) => element(t, `${location}[${i}]`)) };
547
+ }
548
+ if (c === "array" && state && args[0].flags & flags.Never)
549
+ return fail(
550
+ node,
551
+ `${location}[]`,
552
+ "an empty array has no element type; annotate it: [] as string[]",
553
+ );
554
+ if (c === "array") return { kind: "array", element: element(args[0], `${location}[]`) };
555
+ if (c === "map")
556
+ return {
557
+ kind: "map",
558
+ key: key(args[0], `${location}.key`),
559
+ value: element(args[1], `${location}.value`, true),
560
+ };
561
+ if (type.flags & flags.Object || flat) {
562
+ const indices = checker.getIndexInfosOfType(type);
563
+ if (
564
+ indices.length === 1 &&
565
+ indices[0].keyType.flags & flags.String &&
566
+ !checker.getPropertiesOfType(type).length
567
+ )
568
+ return {
569
+ kind: "map",
570
+ key: { kind: "string" },
571
+ value: element(indices[0].type, `${location}.value`, true),
572
+ };
573
+ if (indices.length)
574
+ return fail(node, location, "only a string index signature can encode a record");
575
+ return fields(type, location, position, depth);
576
+ }
577
+ return fail(node, location, `cannot encode ${checker.typeToString(type)}`);
578
+ } finally {
579
+ active.delete(type);
580
+ }
581
+ };
582
+ let result;
583
+ if (!state)
584
+ result = (node.typeArguments ?? []).map((type, i) =>
585
+ lower(
586
+ checker.getTypeFromTypeNode(type),
587
+ i === 1 ? "response" : kind === "askServer" ? "request" : "payload",
588
+ { toClient: kind === "toClient" || i === 1, nilable: false },
589
+ ),
590
+ );
591
+ else {
592
+ const target = targetOf(node);
593
+ if (!target) return [];
594
+ const value = lower(target, "value", { toClient: false, nilable: false, path: true, root: true });
595
+ if (spec.store && value.kind !== "object" && value.kind !== "map")
596
+ fail(node, "value", `a store's value is a record; wrap a ${checker.typeToString(target)} in an object`);
597
+ else if (spec.handles && value.kind === "map")
598
+ fail(
599
+ node,
600
+ "value",
601
+ "a profile store's value is an object with declared fields; wrap the record in a field",
602
+ );
603
+ result = [value];
604
+ }
605
+ const cost = (n, depth = 1) => {
606
+ if (depth > depthLimit) fail(node, "value", `schema exceeds the depth limit of ${depthLimit}`);
607
+ return (
608
+ 1 +
609
+ (n.fields?.reduce((v, [, child]) => v + cost(child, depth + 1), 0) ?? 0) +
610
+ (n.branches ?? n.elements ?? []).reduce((v, child) => v + cost(child, depth + 1), 0) +
611
+ [
612
+ n.element,
613
+ n.key && typeof n.key === "object" ? n.key : undefined,
614
+ n.value && typeof n.value === "object" ? n.value : undefined,
615
+ n.body,
616
+ ]
617
+ .filter(Boolean)
618
+ .reduce((v, child) => v + cost(child, depth + 1), 0)
619
+ );
620
+ };
621
+ if (result.reduce((v, n) => v + cost(n), 0) > limits.MAX_SCHEMA_NODES)
622
+ fail(node, state ? "value" : "payload", "schema exceeds MAX_SCHEMA_NODES");
623
+ if (!spec.handles) return result;
624
+ const view = replica(result[0]);
625
+ for (const [location, reason] of viewNameProblems(node.arguments[0]?.text, view))
626
+ fail(node, location, reason, "CRU018");
627
+ return [result[0], view];
628
+ }
629
+ let reached;
630
+ let entryFiles;
631
+ const entryExports = new Map();
632
+ const canonicalFiles = new Map();
633
+ const canonical = (file) => {
634
+ if (!canonicalFiles.has(file)) canonicalFiles.set(file, path.normalize(ts.sys.realpath?.(file) ?? file));
635
+ return canonicalFiles.get(file);
636
+ };
637
+ return ({ source, report, runtime, side, visitInput }) => {
638
+ const f = ts.factory;
639
+ const str = (value) => f.createStringLiteral(value);
640
+ const arr = (values) => f.createArrayLiteralExpression(values);
641
+ const value = (v) =>
642
+ typeof v === "string"
643
+ ? str(v)
644
+ : typeof v === "boolean"
645
+ ? v
646
+ ? f.createTrue()
647
+ : f.createFalse()
648
+ : v < 0
649
+ ? f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(-v))
650
+ : f.createNumericLiteral(v);
651
+ const emit = (n) => {
652
+ const target = f.createPropertyAccessExpression(runtime("n"), n.kind);
653
+ const args = {
654
+ literal: () => [value(n.value)],
655
+ instance: () => [arr(n.classes.map(str))],
656
+ object: () => [arr(n.fields.map(([name, child]) => arr([str(name), emit(child)])))],
657
+ array: () => [emit(n.element)],
658
+ tuple: () => [arr(n.elements.map(emit))],
659
+ map: () => [emit(n.key), emit(n.value)],
660
+ union: () => [arr(n.branches.map(emit))],
661
+ variant: () => [str(n.key), value(n.value), emit(n.body)],
662
+ }[n.kind];
663
+ return args ? f.createCallExpression(target, undefined, args()) : target;
664
+ };
665
+ if (!entryFiles) {
666
+ entryFiles = new Map();
667
+ for (const s of ["server", "client"]) {
668
+ const resolved = ts.resolveModuleName(
669
+ `@trioforge/crucible/${s}`,
670
+ source.fileName,
671
+ program.getCompilerOptions(),
672
+ ts.sys,
673
+ ).resolvedModule;
674
+ if (resolved) {
675
+ entryFiles.set(canonical(resolved.resolvedFileName), s);
676
+ const module = program.getSourceFile(resolved.resolvedFileName);
677
+ const symbol = module && checker.getSymbolAtLocation(module);
678
+ for (const exported of symbol ? checker.getExportsOfModule(symbol) : [])
679
+ entryExports.set(unalias(exported), s);
680
+ }
681
+ }
682
+ }
683
+ const exportSide = (node, symbol) => {
684
+ if (!symbol) return;
685
+ if (entryExports.has(symbol)) return entryExports.get(symbol);
686
+ if (!(symbol.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property))) return;
687
+ const type = checker.getTypeAtLocation(node);
688
+ return entryExports.get(unalias(type.aliasSymbol)) ?? entryExports.get(type.getSymbol());
689
+ };
690
+ const wrongSide = (node, file) => {
691
+ const s = entryFiles.get(canonical(file));
692
+ if (s && s !== side(source))
693
+ report(
694
+ node,
695
+ "CRU024",
696
+ `This file cannot reference @trioforge/crucible/${s}; move it under a ${s} folder.`,
697
+ );
698
+ };
699
+ const callback = (expr) => {
700
+ if (!expr) return;
701
+ if (ts.isFunctionExpression(expr) || ts.isArrowFunction(expr)) return expr;
702
+ if (!ts.isIdentifier(expr) && !ts.isPropertyAccessExpression(expr)) return;
703
+ const symbol = unalias(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(expr) ? expr.name : expr));
704
+ if (symbol?.declarations?.length !== 1) return;
705
+ const decl = symbol.declarations[0];
706
+ if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) return decl;
707
+ if (
708
+ ts.isVariableDeclaration(decl) &&
709
+ ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const &&
710
+ decl.initializer &&
711
+ (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
712
+ )
713
+ return decl.initializer;
714
+ };
715
+ const receive = (node) => {
716
+ const field = node.parent;
717
+ if (
718
+ !ts.isPropertyDeclaration(field) ||
719
+ field.initializer !== node ||
720
+ !modifiers(field, ts.ModifierFlags.Readonly) ||
721
+ modifiers(field, ts.ModifierFlags.Static) ||
722
+ kindOf(instanceType(field.parent)) !== "subsystem"
723
+ ) {
724
+ report(node, "CRU021", "receive() must initialize a readonly instance field of a Subsystem.");
725
+ return;
726
+ }
727
+ const name = checker.getSymbolAtLocation(field.name)?.name;
728
+ const seen = new Set();
729
+ const shadow = (type) => {
730
+ if (seen.has(type)) return false;
731
+ seen.add(type);
732
+ const prop = checker.getPropertyOfType(type, name);
733
+ return (
734
+ prop?.declarations?.some(
735
+ (d) => ts.isPropertyDeclaration(d) && d.initializer && isMacro(d.initializer, "receive"),
736
+ ) || (type.getBaseTypes?.() ?? []).some(shadow)
737
+ );
738
+ };
739
+ if ((instanceType(field.parent).getBaseTypes?.() ?? []).some(shadow))
740
+ report(node, "CRU021", "A receiver field cannot shadow an inherited receiver.");
741
+ const target = node.arguments[0] && checker.getTypeAtLocation(node.arguments[0]);
742
+ const kind = target && !target.isUnionOrIntersection() && api(target.getSymbol());
743
+ if (!["ToServer", "ToClient", "AskServer", "ClientReady"].includes(kind))
744
+ report(node, "CRU021", "receive target must be exactly one message token or ClientReady.");
745
+ else {
746
+ const required = kind === "ToClient" ? "client" : "server";
747
+ if (side(field) !== required)
748
+ report(node, "CRU022", `${kind} must be received by a ${required} Subsystem.`);
749
+ const fn = callback(node.arguments[1]);
750
+ const signature = fn && checker.getSignatureFromDeclaration(fn);
751
+ if (signature) {
752
+ const ret = checker.getReturnTypeOfSignature(signature);
753
+ const parts = ret.isUnion() ? ret.types : [ret];
754
+ if (
755
+ modifiers(fn, ts.ModifierFlags.Async) ||
756
+ parts.some(
757
+ (t) => t.getSymbol()?.name === "Promise" && standard(t.getSymbol(), "compiler-types"),
758
+ )
759
+ )
760
+ report(
761
+ node,
762
+ "CRU012",
763
+ "Network callbacks cannot be async or return a Promise; spawn a thread inside the callback.",
764
+ );
765
+ else if (
766
+ kind !== "AskServer" &&
767
+ parts.some(
768
+ (t) =>
769
+ !(t.flags & (flags.Void | flags.Undefined | flags.Never)) &&
770
+ !(kind === "ClientReady" && t.getCallSignatures().length),
771
+ )
772
+ )
773
+ report(
774
+ node,
775
+ "CRU012",
776
+ `${kind} callbacks return nothing${kind === "ClientReady" ? " or a cleanup function" : "; use a block body"}; this one can return ${checker.typeToString(ret)}.`,
777
+ );
778
+ }
779
+ }
780
+ return name;
781
+ };
782
+ const isCallee = (expression) => {
783
+ while (ts.isParenthesizedExpression(expression.parent)) expression = expression.parent;
784
+ return ts.isCallExpression(expression.parent) && expression.parent.expression === expression;
785
+ };
786
+ const inspect = (node) => {
787
+ if (ts.isElementAccessExpression(node)) {
788
+ const name = api(checker.getTypeAtLocation(node).getSymbol());
789
+ if (macros.includes(name) && !isCallee(node))
790
+ report(node, "CRU023", `${name} is a declaration macro and cannot be used as a value.`);
791
+ }
792
+ if (
793
+ ts.isModuleDeclaration(node) &&
794
+ (node.flags & ts.NodeFlags.GlobalAugmentation || ts.isStringLiteral(node.name))
795
+ ) {
796
+ if (!reached) {
797
+ reached = new Set();
798
+ for (const declaration of index())
799
+ lowerDeclaration(declaration.node, declaration.kind, () => {}, reached);
800
+ }
801
+ const added = (body) =>
802
+ (!body ? [] : ts.isModuleBlock(body) ? body.statements : [body]).flatMap((statement) => [
803
+ ...(ts.isVariableStatement(statement) ? statement.declarationList.declarations : [statement]),
804
+ ...(ts.isModuleDeclaration(statement) ? added(statement.body) : []),
805
+ ]);
806
+ if (added(node.body).some((d) => reached.has(checker.getSymbolAtLocation(d.name))))
807
+ report(
808
+ node,
809
+ "CRU010",
810
+ "This augmentation changes a type a Crucible declaration lowers; incremental builds cannot see the merge. Declare the type in one module.",
811
+ );
812
+ }
813
+ if (
814
+ ts.isStringLiteral(node) &&
815
+ (ts.isImportDeclaration(node.parent) ||
816
+ ts.isExportDeclaration(node.parent) ||
817
+ (ts.isLiteralTypeNode(node.parent) && ts.isImportTypeNode(node.parent.parent)) ||
818
+ (ts.isCallExpression(node.parent) && node.parent.expression.kind === ts.SyntaxKind.ImportKeyword))
819
+ ) {
820
+ const resolved = ts.resolveModuleName(
821
+ node.text,
822
+ source.fileName,
823
+ program.getCompilerOptions(),
824
+ ts.sys,
825
+ ).resolvedModule;
826
+ if (resolved) wrongSide(node, resolved.resolvedFileName);
827
+ }
828
+ if (ts.isIdentifier(node)) {
829
+ const symbol = unalias(checker.getSymbolAtLocation(node));
830
+ for (const decl of symbol?.declarations ?? []) wrongSide(node, decl.getSourceFile().fileName);
831
+ const owner = exportSide(node, symbol);
832
+ if (
833
+ owner &&
834
+ owner !== side(source) &&
835
+ !symbol.declarations?.some((d) => entryFiles.has(canonical(d.getSourceFile().fileName)))
836
+ )
837
+ report(
838
+ node,
839
+ "CRU024",
840
+ `This file cannot reference @trioforge/crucible/${owner} through ${node.text}; move it under a ${owner} folder.`,
841
+ );
842
+ const name = api(symbol);
843
+ if (macros.includes(name)) {
844
+ const p = node.parent;
845
+ const expression = ts.isPropertyAccessExpression(p) && p.name === node ? p : node;
846
+ if (!isCallee(expression) && !ts.isImportSpecifier(p) && !ts.isExportSpecifier(p))
847
+ report(node, "CRU023", `${name} is a declaration macro and cannot be used as a value.`);
848
+ }
849
+ }
850
+ };
851
+ const visit = (node) => {
852
+ if (isMacro(node, "receive")) {
853
+ const name = receive(node);
854
+ return (
855
+ name &&
856
+ f.createCallExpression(runtime("receive"), node.typeArguments, [
857
+ f.createThis(),
858
+ str(name),
859
+ visitInput(node.arguments[0]),
860
+ visitInput(node.arguments[1]),
861
+ ])
862
+ );
863
+ }
864
+ const kind = declarationMacros.find((name) => isMacro(node, name));
865
+ if (!kind) return;
866
+ const spec = kinds[kind];
867
+ const decl = node.parent,
868
+ list = decl.parent,
869
+ statement = list?.parent;
870
+ if (
871
+ !ts.isVariableDeclaration(decl) ||
872
+ decl.initializer !== node ||
873
+ !ts.isIdentifier(decl.name) ||
874
+ !ts.isVariableDeclarationList(list) ||
875
+ !(list.flags & ts.NodeFlags.Const) ||
876
+ !ts.isVariableStatement(statement) ||
877
+ statement.parent !== source ||
878
+ !modifiers(statement, ts.ModifierFlags.Export) ||
879
+ !ts.isExternalModule(source) ||
880
+ side(node) !== spec.side
881
+ )
882
+ report(
883
+ node,
884
+ "CRU017",
885
+ `Declare ${spec.label} as an exported top-level const in a ${spec.side} module.`,
886
+ );
887
+ const typeArguments = node.typeArguments?.length ?? 0;
888
+ if (spec.typeArguments === "optional" ? typeArguments > 1 : typeArguments !== spec.typeArguments)
889
+ report(
890
+ node,
891
+ "CRU017",
892
+ spec.position === "payload"
893
+ ? `${kind} needs explicit payload types; use undefined for no payload.`
894
+ : `${kind} needs ${spec.typeArguments === "optional" ? "at most one" : "an explicit"} type argument.`,
895
+ );
896
+ const name = node.arguments[0];
897
+ if (
898
+ !name ||
899
+ !ts.isStringLiteral(name) ||
900
+ Buffer.byteLength(name.text) > spec.maxBytes ||
901
+ !/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/.test(name.text) ||
902
+ name.text.split(".")[0] === "Crucible"
903
+ )
904
+ report(
905
+ node,
906
+ "CRU018",
907
+ `${spec.names} names are 1 to ${spec.maxBytes} bytes of dot-separated segments [A-Za-z][A-Za-z0-9_]*; Crucible is reserved.`,
908
+ );
909
+ else {
910
+ const other = index().find(
911
+ (d) => d.node !== node && d.name === name.text && kinds[d.kind].names === spec.names,
912
+ );
913
+ if (other) {
914
+ const sf = other.node.getSourceFile();
915
+ report(
916
+ node,
917
+ "CRU018",
918
+ `${name.text} is already declared at ${path.relative(root, sf.fileName).replaceAll("\\", "/")}:${sf.getLineAndCharacterOfPosition(other.node.getStart()).line + 1}.`,
919
+ );
920
+ }
921
+ }
922
+ let timeout = f.createIdentifier("undefined");
923
+ const options = node.arguments[1];
924
+ if (kind === "askServer" && options) {
925
+ const prop =
926
+ ts.isObjectLiteralExpression(options) && options.properties.length === 1 && options.properties[0];
927
+ if (
928
+ !prop ||
929
+ !ts.isPropertyAssignment(prop) ||
930
+ prop.name.getText().replaceAll('"', "").replaceAll("'", "") !== "timeout" ||
931
+ !ts.isNumericLiteral(prop.initializer) ||
932
+ !(
933
+ Number(prop.initializer.text) > 0 &&
934
+ Number(prop.initializer.text) <= limits.MAX_ASK_TIMEOUT_SECONDS
935
+ )
936
+ )
937
+ report(node, "CRU017", "askServer options must be { timeout: <number literal in (0, 120]> }.");
938
+ else timeout = prop.initializer;
939
+ }
940
+ if (node.arguments.length > spec.arguments) report(node, "CRU017", "Unexpected declaration arguments.");
941
+ const descriptors = lowerDeclaration(node, kind, report);
942
+ if (spec.position !== "payload" && !descriptors.length)
943
+ report(node, "CRU017", `Cannot read ${kind}'s value type; give it an explicit type argument.`);
944
+ const tail =
945
+ kind === "askServer"
946
+ ? [timeout]
947
+ : spec.handles && descriptors.length
948
+ ? [arr(serverOnlyPaths(descriptors[0]).map((segments) => arr(segments.map(str))))]
949
+ : [];
950
+ return f.createCallExpression(runtime(kind), node.typeArguments, [
951
+ name ?? str("invalid"),
952
+ ...spec.passed.map((i) => node.arguments[i] && visitInput(node.arguments[i])).filter(Boolean),
953
+ ...descriptors.map(emit),
954
+ ...tail,
955
+ ]);
956
+ };
957
+ return { inspect, visit };
958
+ };
959
+ }
960
+
961
+ module.exports = { createNetwork, limits };