@silverbulletmd/silverbullet 2.4.1

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 (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. package/plugs/builtin_plugs.ts +14 -0
@@ -0,0 +1,416 @@
1
+ // Goto/label resolution and validation for function bodies
2
+
3
+ import type {
4
+ ASTCtx,
5
+ LuaBlock,
6
+ LuaForInStatement,
7
+ LuaForStatement,
8
+ LuaGotoStatement,
9
+ LuaIfStatement,
10
+ LuaLabelStatement,
11
+ LuaLocalStatement,
12
+ LuaRepeatStatement,
13
+ LuaWhileStatement,
14
+ } from "./ast.ts";
15
+ import { LuaAttribute } from "./ast.ts";
16
+ import { asBlock } from "./ast_narrow.ts";
17
+
18
+ type BlockGotoMeta = {
19
+ labels: Map<string, number>; // in this block
20
+ };
21
+
22
+ type FunctionMeta = {
23
+ // Stores per-block data and drops entries automatically when a block
24
+ // is no longer used.
25
+ blockMeta: WeakMap<LuaBlock, BlockGotoMeta>;
26
+ funcHasGotos: boolean;
27
+ };
28
+
29
+ class LabelResolveError extends Error {
30
+ constructor(msg: string, public astCtx: ASTCtx) {
31
+ super(msg);
32
+ }
33
+ }
34
+
35
+ // Cache
36
+ const functionMetaByRoot = new WeakMap<LuaBlock, FunctionMeta>();
37
+ const functionMetaByAnyBlock = new WeakMap<LuaBlock, FunctionMeta>();
38
+
39
+ export function getBlockGotoMeta(
40
+ block: LuaBlock,
41
+ ): (BlockGotoMeta & { funcHasGotos: boolean }) | undefined {
42
+ let fm = functionMetaByAnyBlock.get(block);
43
+ if (!fm) {
44
+ fm = resolveFunction(block);
45
+ }
46
+ const bm = fm.blockMeta.get(block);
47
+ if (!bm) {
48
+ return undefined;
49
+ }
50
+ return { ...bm, funcHasGotos: fm.funcHasGotos };
51
+ }
52
+
53
+ type LocalID = number;
54
+
55
+ type GotoInfo = {
56
+ node: LuaGotoStatement;
57
+ active: Set<LocalID>;
58
+ block: LuaBlock;
59
+ };
60
+
61
+ type ValidationCtx = {
62
+ labelActiveByBlock: WeakMap<LuaBlock, Map<string, Set<LocalID>>>;
63
+ labelLocByBlock: WeakMap<
64
+ LuaBlock,
65
+ Map<string, { index: number; ctx: ASTCtx }>
66
+ >;
67
+ gotos: GotoInfo[];
68
+ hasGoto: boolean;
69
+ nextLocalId: number;
70
+ closeLocals: Set<LocalID>;
71
+ };
72
+
73
+ type BlockRole =
74
+ | "Root"
75
+ | "Do"
76
+ | "If"
77
+ | "While"
78
+ | "Repeat"
79
+ | "For"
80
+ | "ForIn";
81
+
82
+ function resolveFunction(root: LuaBlock): FunctionMeta {
83
+ const existing = functionMetaByRoot.get(root);
84
+ if (existing) {
85
+ return existing;
86
+ }
87
+
88
+ const blockMeta = new WeakMap<LuaBlock, BlockGotoMeta>();
89
+ const vctx: ValidationCtx = {
90
+ labelActiveByBlock: new WeakMap(),
91
+ labelLocByBlock: new WeakMap(),
92
+ gotos: [],
93
+ hasGoto: false,
94
+ nextLocalId: 1,
95
+ closeLocals: new Set<LocalID>(),
96
+ };
97
+
98
+ const seenBlocks = new Set<LuaBlock>();
99
+ const parentByBlock = new WeakMap<LuaBlock, LuaBlock | undefined>();
100
+ const roleByBlock = new WeakMap<LuaBlock, BlockRole>();
101
+
102
+ processBlock(
103
+ root,
104
+ undefined,
105
+ "Root",
106
+ new Set<LocalID>(),
107
+ blockMeta,
108
+ vctx,
109
+ seenBlocks,
110
+ parentByBlock,
111
+ roleByBlock,
112
+ );
113
+
114
+ // Validate gotos
115
+ for (const g of vctx.gotos) {
116
+ const target = g.node.name;
117
+
118
+ // Search current block for the label, then ancestors
119
+ let searchBlock: LuaBlock | undefined = g.block;
120
+ let labelIndex: number | undefined;
121
+ let labelDefBlock: LuaBlock | undefined;
122
+
123
+ while (searchBlock) {
124
+ const meta = blockMeta.get(searchBlock);
125
+ if (meta && meta.labels.has(target)) {
126
+ labelIndex = meta.labels.get(target);
127
+ labelDefBlock = searchBlock;
128
+ break;
129
+ }
130
+ searchBlock = parentByBlock.get(searchBlock);
131
+ }
132
+
133
+ if (labelIndex === undefined || !labelDefBlock) {
134
+ throw new LabelResolveError(
135
+ `no visible label '${target}' for goto`,
136
+ g.node.ctx,
137
+ );
138
+ }
139
+
140
+ const activeMap = vctx.labelActiveByBlock.get(labelDefBlock);
141
+ const locMap = vctx.labelLocByBlock.get(labelDefBlock);
142
+ if (!activeMap || !locMap) {
143
+ throw new LabelResolveError(
144
+ `no visible label '${target}' for goto`,
145
+ g.node.ctx,
146
+ );
147
+ }
148
+
149
+ const lset = activeMap.get(target);
150
+ const lloc = locMap.get(target);
151
+ if (!lset || !lloc) {
152
+ throw new LabelResolveError(
153
+ `no visible label '${target}' for goto`,
154
+ g.node.ctx,
155
+ );
156
+ }
157
+
158
+ // Local scope forward jump check
159
+ let entersLocalScope = false;
160
+ let entersCloseScope = false;
161
+
162
+ for (const id of lset) {
163
+ if (!g.active.has(id)) {
164
+ entersLocalScope = true;
165
+ if (vctx.closeLocals.has(id)) {
166
+ entersCloseScope = true;
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ if (entersCloseScope) {
173
+ throw new LabelResolveError(
174
+ `goto '${target}' jumps into the scope of a local variable`,
175
+ g.node.ctx,
176
+ );
177
+ }
178
+
179
+ if (entersLocalScope) {
180
+ const safeEnd = isSafeEndLabel(labelDefBlock, lloc.index, roleByBlock);
181
+ if (!safeEnd) {
182
+ throw new LabelResolveError(
183
+ `goto '${target}' jumps into the scope of a local variable`,
184
+ g.node.ctx,
185
+ );
186
+ }
187
+ }
188
+ }
189
+
190
+ const fm: FunctionMeta = {
191
+ blockMeta,
192
+ funcHasGotos: vctx.hasGoto,
193
+ };
194
+
195
+ functionMetaByRoot.set(root, fm);
196
+ for (const block of seenBlocks) {
197
+ functionMetaByAnyBlock.set(block, fm);
198
+ }
199
+
200
+ return fm;
201
+ }
202
+
203
+ function isSafeEndLabel(
204
+ block: LuaBlock,
205
+ labelIndex: number,
206
+ roleByBlock: WeakMap<LuaBlock, BlockRole>,
207
+ ): boolean {
208
+ const role = roleByBlock.get(block);
209
+ if (role === "Repeat") {
210
+ return false;
211
+ }
212
+ for (let i = labelIndex + 1; i < block.statements.length; i++) {
213
+ const t = block.statements[i].type;
214
+ if (t !== "Label" && t !== "Semicolon") {
215
+ return false;
216
+ }
217
+ }
218
+ return true;
219
+ }
220
+
221
+ function cloneSet<T>(s: Set<T>): Set<T> {
222
+ const c = new Set<T>();
223
+ for (const v of s) {
224
+ c.add(v);
225
+ }
226
+ return c;
227
+ }
228
+
229
+ function processBlock(
230
+ block: LuaBlock,
231
+ parent: LuaBlock | undefined,
232
+ role: BlockRole,
233
+ active: Set<LocalID>,
234
+ blockMeta: WeakMap<LuaBlock, BlockGotoMeta>,
235
+ vctx: ValidationCtx,
236
+ seen: Set<LuaBlock>,
237
+ parentByBlock: WeakMap<LuaBlock, LuaBlock | undefined>,
238
+ roleByBlock: WeakMap<LuaBlock, BlockRole>,
239
+ ): void {
240
+ const labels = new Map<string, number>();
241
+ blockMeta.set(block, { labels });
242
+
243
+ seen.add(block);
244
+ parentByBlock.set(block, parent);
245
+ roleByBlock.set(block, role);
246
+
247
+ const labelActiveMap = new Map<string, Set<LocalID>>();
248
+ const labelLocMap = new Map<string, { index: number; ctx: ASTCtx }>();
249
+ vctx.labelActiveByBlock.set(block, labelActiveMap);
250
+ vctx.labelLocByBlock.set(block, labelLocMap);
251
+
252
+ const curActive = cloneSet(active);
253
+
254
+ const stmts = block.statements;
255
+ for (let i = 0; i < stmts.length; i++) {
256
+ const s = stmts[i];
257
+ switch (s.type) {
258
+ case "Label": {
259
+ const lab = s as LuaLabelStatement;
260
+ if (labels.has(lab.name)) {
261
+ throw new LabelResolveError(
262
+ `label '${lab.name}' already defined`,
263
+ lab.ctx,
264
+ );
265
+ }
266
+ labels.set(lab.name, i);
267
+ const actSet = cloneSet(curActive);
268
+ labelActiveMap.set(lab.name, actSet);
269
+ labelLocMap.set(lab.name, { index: i, ctx: lab.ctx });
270
+ break;
271
+ }
272
+ case "Goto": {
273
+ const g = s as LuaGotoStatement;
274
+ vctx.hasGoto = true;
275
+ vctx.gotos.push({ node: g, active: cloneSet(curActive), block });
276
+ break;
277
+ }
278
+ case "Local": {
279
+ const l = s as LuaLocalStatement;
280
+ for (let j = 0; j < l.names.length; j++) {
281
+ const id = vctx.nextLocalId++;
282
+ curActive.add(id);
283
+
284
+ const isClose =
285
+ l.names[j].attributes?.includes(LuaAttribute.Close) ===
286
+ true;
287
+ if (isClose) {
288
+ vctx.closeLocals.add(id);
289
+ }
290
+ }
291
+ break;
292
+ }
293
+ case "LocalFunction": {
294
+ curActive.add(vctx.nextLocalId++);
295
+ break;
296
+ }
297
+ case "Function": {
298
+ break;
299
+ }
300
+ case "For": {
301
+ const fr = s as LuaForStatement;
302
+ const childActive = cloneSet(curActive);
303
+ childActive.add(vctx.nextLocalId++);
304
+ processBlock(
305
+ fr.block,
306
+ block,
307
+ "For",
308
+ childActive,
309
+ blockMeta,
310
+ vctx,
311
+ seen,
312
+ parentByBlock,
313
+ roleByBlock,
314
+ );
315
+ break;
316
+ }
317
+ case "ForIn": {
318
+ const fi = s as LuaForInStatement;
319
+ const childActive = cloneSet(curActive);
320
+ for (let j = 0; j < fi.names.length; j++) {
321
+ childActive.add(vctx.nextLocalId++);
322
+ }
323
+ processBlock(
324
+ fi.block,
325
+ block,
326
+ "ForIn",
327
+ childActive,
328
+ blockMeta,
329
+ vctx,
330
+ seen,
331
+ parentByBlock,
332
+ roleByBlock,
333
+ );
334
+ break;
335
+ }
336
+ case "While": {
337
+ const w = s as LuaWhileStatement;
338
+ processBlock(
339
+ w.block,
340
+ block,
341
+ "While",
342
+ cloneSet(curActive),
343
+ blockMeta,
344
+ vctx,
345
+ seen,
346
+ parentByBlock,
347
+ roleByBlock,
348
+ );
349
+ break;
350
+ }
351
+ case "Repeat": {
352
+ const r = s as LuaRepeatStatement;
353
+ processBlock(
354
+ r.block,
355
+ block,
356
+ "Repeat",
357
+ cloneSet(curActive),
358
+ blockMeta,
359
+ vctx,
360
+ seen,
361
+ parentByBlock,
362
+ roleByBlock,
363
+ );
364
+ break;
365
+ }
366
+ case "If": {
367
+ const iff = s as LuaIfStatement;
368
+ for (let k = 0; k < iff.conditions.length; k++) {
369
+ processBlock(
370
+ iff.conditions[k].block,
371
+ block,
372
+ "If",
373
+ cloneSet(curActive),
374
+ blockMeta,
375
+ vctx,
376
+ seen,
377
+ parentByBlock,
378
+ roleByBlock,
379
+ );
380
+ }
381
+ if (iff.elseBlock) {
382
+ processBlock(
383
+ iff.elseBlock,
384
+ block,
385
+ "If",
386
+ cloneSet(curActive),
387
+ blockMeta,
388
+ vctx,
389
+ seen,
390
+ parentByBlock,
391
+ roleByBlock,
392
+ );
393
+ }
394
+ break;
395
+ }
396
+ case "Block": {
397
+ const child = asBlock(s);
398
+ processBlock(
399
+ child,
400
+ block,
401
+ "Do",
402
+ cloneSet(curActive),
403
+ blockMeta,
404
+ vctx,
405
+ seen,
406
+ parentByBlock,
407
+ roleByBlock,
408
+ );
409
+ break;
410
+ }
411
+ default: {
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ }
@@ -0,0 +1,240 @@
1
+ import type { NumericType } from "./ast.ts";
2
+ import { luaToNumberDetailed } from "./tonumber.ts";
3
+ import { luaTypeName } from "./runtime.ts";
4
+
5
+ export interface LuaTaggedFloat {
6
+ readonly value: number;
7
+ readonly isFloat: true;
8
+ }
9
+
10
+ // Pre-allocated singletons for float zeros
11
+ const FLOAT_POS_ZERO: LuaTaggedFloat = { value: 0, isFloat: true };
12
+ const FLOAT_NEG_ZERO: LuaTaggedFloat = { value: -0, isFloat: true };
13
+
14
+ export const luaStringCoercionError: Error = new Error(
15
+ "LuaStringCoercionError",
16
+ );
17
+
18
+ export function isNegativeZero(n: number): boolean {
19
+ return n === 0 && 1 / n === -Infinity;
20
+ }
21
+
22
+ export function isTaggedFloat(v: unknown): v is LuaTaggedFloat {
23
+ return v !== null && typeof v === "object" && (v as any).isFloat === true;
24
+ }
25
+
26
+ function makeFloat(n: number): LuaTaggedFloat {
27
+ if (n === 0) {
28
+ return isNegativeZero(n) ? FLOAT_NEG_ZERO : FLOAT_POS_ZERO;
29
+ }
30
+ return { value: n, isFloat: true };
31
+ }
32
+
33
+ // Box a zero with a given kind tag.
34
+ export function makeLuaZero(
35
+ n: number,
36
+ numericType: NumericType,
37
+ ): any {
38
+ if (n !== 0) {
39
+ return n;
40
+ }
41
+ if (numericType !== "float") {
42
+ return 0;
43
+ }
44
+ return isNegativeZero(n) ? FLOAT_NEG_ZERO : FLOAT_POS_ZERO;
45
+ }
46
+
47
+ // Tag an integer-valued number as float.
48
+ // Only allocates for integer-valued results; non-integer floats
49
+ // (1.5, NaN, Inf) are already unambiguously float as plain `number`.
50
+ export function makeLuaFloat(n: number): any {
51
+ if (!Number.isInteger(n)) {
52
+ return n;
53
+ }
54
+ return makeFloat(n);
55
+ }
56
+
57
+ export function getZeroBoxKind(x: any): NumericType | undefined {
58
+ if (isTaggedFloat(x)) {
59
+ return "float";
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ // Unwrap a potentially tagged or boxed Number to a plain number.
65
+ export function untagNumber(n: any): number {
66
+ if (typeof n === "number") return n;
67
+ if (isTaggedFloat(n)) {
68
+ return n.value;
69
+ }
70
+ return n;
71
+ }
72
+
73
+ export function coerceToNumber(v: unknown): number | null {
74
+ if (typeof v === "number") {
75
+ return v;
76
+ }
77
+ if (isTaggedFloat(v)) {
78
+ return v.value;
79
+ }
80
+ if (typeof v === "string") {
81
+ const det = luaToNumberDetailed(v);
82
+ if (!det) {
83
+ return null;
84
+ }
85
+ return det.value;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ export function inferNumericType(n: number): NumericType {
91
+ if (!Number.isFinite(n)) {
92
+ return "float";
93
+ }
94
+ if (isNegativeZero(n)) {
95
+ return "float";
96
+ }
97
+ return Number.isInteger(n) ? "int" : "float";
98
+ }
99
+
100
+ export function combineNumericTypes(
101
+ a: NumericType | undefined,
102
+ b: NumericType | undefined,
103
+ ): NumericType {
104
+ if (a === "float" || b === "float") {
105
+ return "float";
106
+ }
107
+ return "int";
108
+ }
109
+
110
+ export function getNumericKind(
111
+ n: unknown,
112
+ ): NumericType | undefined {
113
+ if (typeof n === "number") {
114
+ return inferNumericType(n);
115
+ }
116
+ if (isTaggedFloat(n)) {
117
+ return "float";
118
+ }
119
+ return undefined;
120
+ }
121
+
122
+ export type CoerceNumericResult = {
123
+ n: number;
124
+ type: NumericType;
125
+ };
126
+
127
+ export function coerceNumeric(
128
+ val: unknown,
129
+ hint?: NumericType,
130
+ ): CoerceNumericResult {
131
+ if (typeof val === "number") {
132
+ return { n: val, type: hint ?? inferNumericType(val) };
133
+ }
134
+
135
+ if (isTaggedFloat(val)) {
136
+ return { n: val.value, type: hint ?? "float" };
137
+ }
138
+
139
+ if (typeof val === "string") {
140
+ const det = luaToNumberDetailed(val);
141
+ if (!det) {
142
+ throw luaStringCoercionError;
143
+ }
144
+ return { n: det.value, type: hint ?? det.numericType };
145
+ }
146
+
147
+ throw new Error(
148
+ `attempt to perform arithmetic on a ${luaTypeName(val)} value`,
149
+ );
150
+ }
151
+
152
+ export type CoerceNumericPairResult = {
153
+ left: number;
154
+ right: number;
155
+ resultType: NumericType;
156
+ };
157
+
158
+ export function coerceNumericPair(
159
+ a: unknown,
160
+ b: unknown,
161
+ leftType?: NumericType,
162
+ rightType?: NumericType,
163
+ op?: string,
164
+ ): CoerceNumericPairResult {
165
+ const forceFloat = op === "/" || op === "^";
166
+
167
+ // Both plain numbers
168
+ if (typeof a === "number" && typeof b === "number") {
169
+ const lt = leftType ?? inferNumericType(a);
170
+ const rt = rightType ?? inferNumericType(b);
171
+
172
+ return {
173
+ left: a,
174
+ right: b,
175
+ resultType: forceFloat
176
+ ? "float"
177
+ : ((lt === "float" || rt === "float") ? "float" : "int"),
178
+ };
179
+ }
180
+
181
+ // One tagged float, one plain number
182
+ if (typeof a === "number" && isTaggedFloat(b)) {
183
+ return {
184
+ left: a,
185
+ right: b.value,
186
+ resultType: "float",
187
+ };
188
+ }
189
+
190
+ if (isTaggedFloat(a) && typeof b === "number") {
191
+ return {
192
+ left: a.value,
193
+ right: b,
194
+ resultType: "float",
195
+ };
196
+ }
197
+
198
+ // Both tagged floats
199
+ if (isTaggedFloat(a) && isTaggedFloat(b)) {
200
+ return {
201
+ left: a.value,
202
+ right: b.value,
203
+ resultType: "float",
204
+ };
205
+ }
206
+
207
+ // General fallback
208
+ const A = coerceNumeric(a, leftType);
209
+ const B = coerceNumeric(b, rightType);
210
+
211
+ return {
212
+ left: A.n,
213
+ right: B.n,
214
+ resultType: forceFloat ? "float" : combineNumericTypes(A.type, B.type),
215
+ };
216
+ }
217
+
218
+ export function normalizeArithmeticResult(
219
+ n: number,
220
+ resultType: NumericType,
221
+ ): number {
222
+ if (n === 0) {
223
+ if (resultType === "int") {
224
+ return 0;
225
+ }
226
+ return isNegativeZero(n) ? -0 : 0;
227
+ }
228
+ return n;
229
+ }
230
+
231
+ export function toInteger(v: unknown): number | null {
232
+ if (typeof v === "number") {
233
+ return Number.isInteger(v) ? v : null;
234
+ }
235
+ if (isTaggedFloat(v)) {
236
+ const n = v.value;
237
+ return Number.isInteger(n) ? n : null;
238
+ }
239
+ return null;
240
+ }