@volter/twin-upstash 0.1.0 → 0.1.2

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.
@@ -1,1229 +1,15 @@
1
- // A TINY LUA-SUBSET INTERPRETER — the engine behind this twin's EVAL/EVALSHA.
2
- //
3
- // ── WHY AN INTERPRETER AND NOT SCRIPT-RECOGNITION (the honest choice, stated up front) ──────────
4
- // The build brief allowed either (a) recognising known script SHA1s and dispatching to hand-written
5
- // TypeScript equivalents, or (b) actually EXECUTING the Lua. (a) is a fake-success generator: it
6
- // passes for the exact bytes it memorised and silently mis-answers for any script one character
7
- // different — and the whole point of `EVAL` is that the caller supplies the program. So this file
8
- // takes (b): it PARSES and EVALUATES the script text, calling back into the twin's real Redis
9
- // command core for every `redis.call`. Nothing about @upstash/ratelimit's scripts is special-cased
10
- // anywhere in this pack; they run because the interpreter runs Lua, and their SHA1s are only ever
11
- // used the way real Redis uses them (the EVALSHA script cache).
12
- //
13
- // ── WHAT IS AND IS NOT SUPPORTED (loud failure, never a fake success) ───────────────────────────
14
- // SUPPORTED: local/global assignment (incl. multiple assignment and `local x` with no value),
15
- // numeric `for`, generic `for ... in ipairs/pairs`, `while`, `repeat/until`, `if/elseif/else`,
16
- // `break`, `return`, table constructors (array-style and `[k]=v`/`k=v`), indexing (`t[i]`, `t.k`),
17
- // the full operator set Lua 5.1 has (`+ - * / % ^ .. == ~= < <= > >= and or not #` and unary minus),
18
- // user-defined `function` values, and the standard-library subset real Redis scripts actually use:
19
- // `tonumber tostring type error assert unpack ipairs pairs rawget rawset select`,
20
- // `math.floor/ceil/min/max/abs/huge/random/randomseed/pow/sqrt/fmod/modf`,
21
- // `table.insert/remove/getn/concat`, `string.format/len/sub/lower/upper/rep/find/gsub/byte/char`,
22
- // plus `redis.call`, `redis.pcall`, `redis.error_reply`, `redis.status_reply`, `redis.sha1hex`,
23
- // `redis.breakpoint`/`redis.debug` (no-ops), `redis.LOG_*`, `redis.log` (no-op), `KEYS`, `ARGV`.
24
- //
25
- // NOT SUPPORTED — and every one of these throws a LOUD `LuaError` naming the construct rather than
26
- // degrading to a wrong answer: `goto`/labels, coroutines, metatables/`setmetatable`, varargs `...`
27
- // in a user function, `cjson`/`cmsgpack`/`bit`/`struct`/`os`/`io`, string methods called with `:`
28
- // syntax on a value (`s:sub(1,2)`), and any identifier the environment does not define. An
29
- // unsupported script therefore fails the way an unmodeled operation must — visibly.
30
- //
31
- // ── LUA SEMANTICS THAT ARE EASY TO GET WRONG AND MATTER HERE ────────────────────────────────────
32
- // 1. ONLY `nil` and `false` are falsy. `0` and `""` are TRUE. (`if requestsInCurrentWindow == false`
33
- // in the sliding-window script depends on the Redis nil→false conversion, not on falsiness.)
34
- // 2. `==` does NOT coerce across types: `"3" == 3` is FALSE. This is load-bearing — @upstash/
35
- // ratelimit's `cachedFixedWindowLimitScript` compares a number against a raw `ARGV` STRING
36
- // (`if r == incrementBy`), which in real Redis never matches and therefore never arms the
37
- // PEXPIRE. Coercing here would make this twin MORE "correct" than the vendor and hide the
38
- // caller's real behaviour, so the comparison is kept strict.
39
- // 3. Arithmetic DOES coerce numeric strings (`"5" + 1 == 6`); a non-numeric string is an error.
40
- // 4. `/` is float division; `math.floor` rounds toward -inf; `%` is `a - floor(a/b)*b` (sign of the
41
- // divisor), which differs from JavaScript's `%` for negative operands.
42
- // 5. Tables are 1-based and `#t` is the array length.
43
- // 6. Lua→Redis conversion truncates a number toward ZERO (`3.7` → `3`, `-3.7` → `-3`) and stops
44
- // an array reply at the first `nil`.
45
- import { createHash } from 'node:crypto';
46
- /** A Lua runtime/parse error. Surfaces to the caller as a Redis script error, never as a result. */
47
- export class LuaError extends Error {
48
- constructor(message) {
49
- super(message);
50
- this.name = 'LuaError';
51
- }
52
- }
53
- /** A Lua table. Array part + hash part, exactly like the real thing (1-based array indices). */
54
- export class LuaTable {
55
- map = new Map();
56
- get(k) {
57
- if (k === null)
58
- return null;
59
- return this.map.get(k) ?? null;
60
- }
61
- set(k, v) {
62
- if (k === null)
63
- throw new LuaError('table index is nil');
64
- if (v === null)
65
- this.map.delete(k);
66
- else
67
- this.store(k, v);
68
- }
69
- /** A non-nil value at a non-nil key. */
70
- store(k, v) {
71
- this.map.set(k, v);
72
- }
73
- /** `#t` — the border of the array part: the largest n where t[1..n] are all non-nil. */
74
- get length() {
75
- let n = 0;
76
- while (this.map.has(n + 1))
77
- n++;
78
- return n;
79
- }
80
- static fromArray(values) {
81
- const t = new LuaTable();
82
- values.forEach((v, i) => { if (v !== null)
83
- t.map.set(i + 1, v); });
84
- return t;
85
- }
86
- toArray() {
87
- const out = [];
88
- for (let i = 1; this.map.has(i); i++)
89
- out.push(this.map.get(i));
90
- return out;
91
- }
92
- }
93
- const KEYWORDS = new Set([
94
- 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if', 'in',
95
- 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
96
- ]);
97
- /** Reserved words this interpreter deliberately refuses rather than silently mis-running. */
98
- const UNSUPPORTED_KEYWORDS = new Set(['goto']);
99
- // Longest-first: `...` must win over `..`, `==` over `=`, etc.
100
- const OPERATORS = [
101
- '...', '..', '==', '~=', '<=', '>=',
102
- '+', '-', '*', '/', '%', '^', '#', '<', '>', '=', '(', ')', '{', '}', '[', ']', ';', ':', ',', '.',
103
- ];
104
- export function tokenize(src) {
105
- const out = [];
106
- let i = 0;
107
- let line = 1;
108
- const isDigit = (c) => c >= '0' && c <= '9';
109
- const isNameStart = (c) => /[A-Za-z_]/.test(c);
110
- const isNamePart = (c) => /[A-Za-z0-9_]/.test(c);
111
- while (i < src.length) {
112
- const c = src[i];
113
- if (c === '\n') {
114
- line++;
115
- i++;
116
- continue;
117
- }
118
- if (c === ' ' || c === '\t' || c === '\r') {
119
- i++;
120
- continue;
121
- }
122
- // long comment --[[ ... ]] / line comment -- ...
123
- if (c === '-' && src[i + 1] === '-') {
124
- if (src[i + 2] === '[' && src[i + 3] === '[') {
125
- const end = src.indexOf(']]', i + 4);
126
- if (end < 0)
127
- throw new LuaError(`unfinished long comment at line ${line}`);
128
- for (let k = i; k < end; k++)
129
- if (src[k] === '\n')
130
- line++;
131
- i = end + 2;
132
- continue;
133
- }
134
- while (i < src.length && src[i] !== '\n')
135
- i++;
136
- continue;
137
- }
138
- // long string [[ ... ]]
139
- if (c === '[' && src[i + 1] === '[') {
140
- const end = src.indexOf(']]', i + 2);
141
- if (end < 0)
142
- throw new LuaError(`unfinished long string at line ${line}`);
143
- const value = src.slice(i + 2, end);
144
- for (const ch of value)
145
- if (ch === '\n')
146
- line++;
147
- out.push({ kind: 'string', value, line });
148
- i = end + 2;
149
- continue;
150
- }
151
- if (c === '"' || c === "'") {
152
- const quote = c;
153
- let value = '';
154
- i++;
155
- while (i < src.length && src[i] !== quote) {
156
- if (src[i] === '\\') {
157
- const esc = src[i + 1];
158
- const simple = { n: '\n', t: '\t', r: '\r', a: '\x07', b: '\b', f: '\f', v: '\v', '\\': '\\', '"': '"', "'": "'", '\n': '\n' };
159
- if (esc in simple) {
160
- value += simple[esc];
161
- i += 2;
162
- continue;
163
- }
164
- if (isDigit(esc)) {
165
- let digits = '';
166
- i++;
167
- while (digits.length < 3 && isDigit(src[i] ?? '')) {
168
- digits += src[i];
169
- i++;
170
- }
171
- value += String.fromCharCode(Number(digits));
172
- continue;
173
- }
174
- throw new LuaError(`unsupported string escape \\${esc} at line ${line}`);
175
- }
176
- if (src[i] === '\n')
177
- line++;
178
- value += src[i];
179
- i++;
180
- }
181
- if (i >= src.length)
182
- throw new LuaError(`unfinished string at line ${line}`);
183
- i++; // closing quote
184
- out.push({ kind: 'string', value, line });
185
- continue;
186
- }
187
- if (isDigit(c) || (c === '.' && isDigit(src[i + 1] ?? ''))) {
188
- let j = i;
189
- if (c === '0' && (src[i + 1] === 'x' || src[i + 1] === 'X')) {
190
- j = i + 2;
191
- while (j < src.length && /[0-9a-fA-F]/.test(src[j]))
192
- j++;
193
- out.push({ kind: 'number', value: src.slice(i, j), line });
194
- i = j;
195
- continue;
196
- }
197
- while (j < src.length && /[0-9.]/.test(src[j]))
198
- j++;
199
- if (src[j] === 'e' || src[j] === 'E') {
200
- j++;
201
- if (src[j] === '+' || src[j] === '-')
202
- j++;
203
- while (j < src.length && isDigit(src[j]))
204
- j++;
205
- }
206
- out.push({ kind: 'number', value: src.slice(i, j), line });
207
- i = j;
208
- continue;
209
- }
210
- if (isNameStart(c)) {
211
- let j = i;
212
- while (j < src.length && isNamePart(src[j]))
213
- j++;
214
- const word = src.slice(i, j);
215
- if (UNSUPPORTED_KEYWORDS.has(word))
216
- throw new LuaError(`unsupported Lua construct '${word}' at line ${line} — this twin runs a documented Lua SUBSET (see upstash-lua.ts)`);
217
- out.push({ kind: KEYWORDS.has(word) ? 'keyword' : 'name', value: word, line });
218
- i = j;
219
- continue;
220
- }
221
- const op = OPERATORS.find((o) => src.startsWith(o, i));
222
- if (!op)
223
- throw new LuaError(`unexpected character '${c}' at line ${line}`);
224
- out.push({ kind: 'op', value: op, line });
225
- i += op.length;
226
- }
227
- out.push({ kind: 'eof', value: '<eof>', line });
228
- return out;
229
- }
230
- class Parser {
231
- toks;
232
- pos = 0;
233
- constructor(toks) {
234
- this.toks = toks;
235
- }
236
- peek() { return this.toks[this.pos]; }
237
- next() { return this.toks[this.pos++]; }
238
- check(kind, value) {
239
- const t = this.peek();
240
- return t.kind === kind && (value === undefined || t.value === value);
241
- }
242
- accept(kind, value) {
243
- if (this.check(kind, value)) {
244
- this.pos++;
245
- return true;
246
- }
247
- return false;
248
- }
249
- expect(kind, value) {
250
- if (!this.check(kind, value)) {
251
- const t = this.peek();
252
- throw new LuaError(`syntax error near '${t.value}' at line ${t.line} — expected ${value ?? kind}`);
253
- }
254
- return this.next();
255
- }
256
- parseChunk() {
257
- const body = this.parseBlock();
258
- this.expect('eof');
259
- return body;
260
- }
261
- blockEnds() {
262
- const t = this.peek();
263
- return t.kind === 'eof' || (t.kind === 'keyword' && ['end', 'else', 'elseif', 'until'].includes(t.value));
264
- }
265
- parseBlock() {
266
- const out = [];
267
- while (!this.blockEnds()) {
268
- if (this.accept('op', ';'))
269
- continue;
270
- const stat = this.parseStatement();
271
- out.push(stat);
272
- if (stat.t === 'return' || stat.t === 'break') {
273
- this.accept('op', ';');
274
- break; // `return`/`break` must be the last statement of a block in Lua 5.1
275
- }
276
- }
277
- return out;
278
- }
279
- parseStatement() {
280
- const t = this.peek();
281
- if (t.kind === 'keyword') {
282
- switch (t.value) {
283
- case 'local': return this.parseLocal();
284
- case 'if': return this.parseIf();
285
- case 'while': return this.parseWhile();
286
- case 'repeat': return this.parseRepeat();
287
- case 'for': return this.parseFor();
288
- case 'do': {
289
- this.next();
290
- const body = this.parseBlock();
291
- this.expect('keyword', 'end');
292
- return { t: 'do', body };
293
- }
294
- case 'return': {
295
- this.next();
296
- const exprs = this.blockEnds() || this.check('op', ';') ? [] : this.parseExprList();
297
- return { t: 'return', exprs };
298
- }
299
- case 'break':
300
- this.next();
301
- return { t: 'break' };
302
- case 'function': {
303
- this.next();
304
- // `function a.b.c(...)` — sugar for `a.b.c = function(...)`
305
- let target = { t: 'name', v: this.expect('name').value };
306
- while (this.accept('op', '.'))
307
- target = { t: 'index', obj: target, key: { t: 'str', v: this.expect('name').value } };
308
- if (this.check('op', ':'))
309
- throw new LuaError(`unsupported Lua construct 'function ...:method' at line ${t.line} — this twin runs a documented Lua SUBSET`);
310
- const fn = this.parseFunctionBody();
311
- return { t: 'assign', targets: [target], exprs: [fn] };
312
- }
313
- default: throw new LuaError(`unexpected '${t.value}' at line ${t.line}`);
314
- }
315
- }
316
- // expression statement: either a call, or an assignment
317
- const first = this.parseSuffixed();
318
- if (this.check('op', '=') || this.check('op', ',')) {
319
- const targets = [first];
320
- while (this.accept('op', ','))
321
- targets.push(this.parseSuffixed());
322
- this.expect('op', '=');
323
- const exprs = this.parseExprList();
324
- for (const target of targets) {
325
- if (target.t !== 'name' && target.t !== 'index')
326
- throw new LuaError(`cannot assign to this expression at line ${t.line}`);
327
- }
328
- return { t: 'assign', targets, exprs };
329
- }
330
- if (first.t !== 'call')
331
- throw new LuaError(`syntax error at line ${t.line} — statement is neither a call nor an assignment`);
332
- return { t: 'callstat', call: first };
333
- }
334
- parseLocal() {
335
- this.expect('keyword', 'local');
336
- if (this.check('keyword', 'function')) {
337
- this.next();
338
- const name = this.expect('name').value;
339
- const fn = this.parseFunctionBody();
340
- return { t: 'local', names: [name], exprs: [fn] };
341
- }
342
- const names = [this.expect('name').value];
343
- while (this.accept('op', ','))
344
- names.push(this.expect('name').value);
345
- const exprs = this.accept('op', '=') ? this.parseExprList() : [];
346
- return { t: 'local', names, exprs };
347
- }
348
- parseIf() {
349
- this.expect('keyword', 'if');
350
- const branches = [];
351
- const cond = this.parseExpr();
352
- this.expect('keyword', 'then');
353
- branches.push({ cond, body: this.parseBlock() });
354
- let orelse = null;
355
- for (;;) {
356
- if (this.accept('keyword', 'elseif')) {
357
- const c = this.parseExpr();
358
- this.expect('keyword', 'then');
359
- branches.push({ cond: c, body: this.parseBlock() });
360
- continue;
361
- }
362
- if (this.accept('keyword', 'else'))
363
- orelse = this.parseBlock();
364
- break;
365
- }
366
- this.expect('keyword', 'end');
367
- return { t: 'if', branches, orelse };
368
- }
369
- parseWhile() {
370
- this.expect('keyword', 'while');
371
- const cond = this.parseExpr();
372
- this.expect('keyword', 'do');
373
- const body = this.parseBlock();
374
- this.expect('keyword', 'end');
375
- return { t: 'while', cond, body };
376
- }
377
- parseRepeat() {
378
- this.expect('keyword', 'repeat');
379
- const body = this.parseBlock();
380
- this.expect('keyword', 'until');
381
- return { t: 'repeat', body, cond: this.parseExpr() };
382
- }
383
- parseFor() {
384
- this.expect('keyword', 'for');
385
- const first = this.expect('name').value;
386
- if (this.accept('op', '=')) {
387
- const from = this.parseExpr();
388
- this.expect('op', ',');
389
- const to = this.parseExpr();
390
- const step = this.accept('op', ',') ? this.parseExpr() : null;
391
- this.expect('keyword', 'do');
392
- const body = this.parseBlock();
393
- this.expect('keyword', 'end');
394
- return { t: 'fornum', name: first, from, to, step, body };
395
- }
396
- const names = [first];
397
- while (this.accept('op', ','))
398
- names.push(this.expect('name').value);
399
- this.expect('keyword', 'in');
400
- const exprs = this.parseExprList();
401
- this.expect('keyword', 'do');
402
- const body = this.parseBlock();
403
- this.expect('keyword', 'end');
404
- return { t: 'forin', names, exprs, body };
405
- }
406
- parseFunctionBody() {
407
- this.expect('op', '(');
408
- const params = [];
409
- if (!this.check('op', ')')) {
410
- do {
411
- if (this.check('op', '...'))
412
- throw new LuaError('unsupported Lua construct: varargs `...` in a function definition — this twin runs a documented Lua SUBSET');
413
- params.push(this.expect('name').value);
414
- } while (this.accept('op', ','));
415
- }
416
- this.expect('op', ')');
417
- const body = this.parseBlock();
418
- this.expect('keyword', 'end');
419
- return { t: 'func', params, body };
420
- }
421
- parseExprList() {
422
- const out = [this.parseExpr()];
423
- while (this.accept('op', ','))
424
- out.push(this.parseExpr());
425
- return out;
426
- }
427
- // Lua 5.1 binary precedence. `..` and `^` are RIGHT-associative.
428
- static BINARY = {
429
- or: [1, 1], and: [2, 2],
430
- '<': [3, 3], '>': [3, 3], '<=': [3, 3], '>=': [3, 3], '~=': [3, 3], '==': [3, 3],
431
- '..': [5, 4],
432
- '+': [6, 6], '-': [6, 6],
433
- '*': [7, 7], '/': [7, 7], '%': [7, 7],
434
- '^': [10, 9],
435
- };
436
- static UNARY_PRIORITY = 8;
437
- parseExpr(limit = 0) {
438
- let left;
439
- const t = this.peek();
440
- if ((t.kind === 'op' && (t.value === '-' || t.value === '#')) || (t.kind === 'keyword' && t.value === 'not')) {
441
- this.next();
442
- left = { t: 'un', op: t.value, e: this.parseExpr(Parser.UNARY_PRIORITY) };
443
- }
444
- else {
445
- left = this.parseSimple();
446
- }
447
- for (;;) {
448
- const op = this.peek();
449
- const isBin = (op.kind === 'op' || op.kind === 'keyword') && Parser.BINARY[op.value] !== undefined;
450
- if (!isBin)
451
- break;
452
- const [leftPri, rightPri] = Parser.BINARY[op.value];
453
- if (leftPri <= limit)
454
- break;
455
- this.next();
456
- left = { t: 'bin', op: op.value, l: left, r: this.parseExpr(rightPri) };
457
- }
458
- return left;
459
- }
460
- parseSimple() {
461
- const t = this.peek();
462
- if (t.kind === 'number') {
463
- this.next();
464
- return { t: 'num', v: parseLuaNumber(t.value) };
465
- }
466
- if (t.kind === 'string') {
467
- this.next();
468
- return { t: 'str', v: t.value };
469
- }
470
- if (t.kind === 'keyword') {
471
- if (t.value === 'nil') {
472
- this.next();
473
- return { t: 'nil' };
474
- }
475
- if (t.value === 'true') {
476
- this.next();
477
- return { t: 'true' };
478
- }
479
- if (t.value === 'false') {
480
- this.next();
481
- return { t: 'false' };
482
- }
483
- if (t.value === 'function') {
484
- this.next();
485
- return this.parseFunctionBody();
486
- }
487
- }
488
- if (t.kind === 'op' && t.value === '{')
489
- return this.parseTable();
490
- if (t.kind === 'op' && t.value === '...')
491
- throw new LuaError(`unsupported Lua construct '...' at line ${t.line} — this twin runs a documented Lua SUBSET`);
492
- return this.parseSuffixed();
493
- }
494
- parseTable() {
495
- this.expect('op', '{');
496
- const array = [];
497
- const pairs = [];
498
- while (!this.check('op', '}')) {
499
- if (this.check('op', '[')) {
500
- this.next();
501
- const k = this.parseExpr();
502
- this.expect('op', ']');
503
- this.expect('op', '=');
504
- pairs.push({ k, v: this.parseExpr() });
505
- }
506
- else if (this.check('name') && this.toks[this.pos + 1]?.kind === 'op' && this.toks[this.pos + 1]?.value === '=') {
507
- const k = this.next().value;
508
- this.next(); // '='
509
- pairs.push({ k: { t: 'str', v: k }, v: this.parseExpr() });
510
- }
511
- else {
512
- array.push(this.parseExpr());
513
- }
514
- if (!this.accept('op', ',') && !this.accept('op', ';'))
515
- break;
516
- }
517
- this.expect('op', '}');
518
- return { t: 'table', array, pairs };
519
- }
520
- /** primary + any chain of `.name`, `[expr]`, `(args)`, `"str"`, `{table}` suffixes. */
521
- parseSuffixed() {
522
- let e;
523
- if (this.accept('op', '(')) {
524
- e = this.parseExpr();
525
- this.expect('op', ')');
526
- }
527
- else {
528
- e = { t: 'name', v: this.expect('name').value };
529
- }
530
- for (;;) {
531
- if (this.accept('op', '.')) {
532
- e = { t: 'index', obj: e, key: { t: 'str', v: this.expect('name').value } };
533
- continue;
534
- }
535
- if (this.accept('op', '[')) {
536
- const k = this.parseExpr();
537
- this.expect('op', ']');
538
- e = { t: 'index', obj: e, key: k };
539
- continue;
540
- }
541
- if (this.check('op', ':'))
542
- throw new LuaError(`unsupported Lua construct 'method call with :' at line ${this.peek().line} — this twin runs a documented Lua SUBSET`);
543
- if (this.check('op', '(')) {
544
- const line = this.next().line;
545
- const args = this.check('op', ')') ? [] : this.parseExprList();
546
- this.expect('op', ')');
547
- e = { t: 'call', fn: e, args, line };
548
- continue;
549
- }
550
- if (this.check('string')) {
551
- e = { t: 'call', fn: e, args: [{ t: 'str', v: this.next().value }] };
552
- continue;
553
- }
554
- if (this.check('op', '{')) {
555
- e = { t: 'call', fn: e, args: [this.parseTable()] };
556
- continue;
557
- }
558
- break;
559
- }
560
- return e;
561
- }
562
- }
563
- function parseLuaNumber(text) {
564
- const n = /^0[xX]/.test(text) ? Number.parseInt(text, 16) : Number(text);
565
- if (!Number.isFinite(n) && text !== 'inf')
566
- throw new LuaError(`malformed number near '${text}'`);
567
- return n;
568
- }
569
- /** Parse Lua source into a chunk. Exported so a test can prove a bad script fails at PARSE time. */
570
- export function parseLua(src) {
571
- return new Parser(tokenize(src)).parseChunk();
572
- }
573
- // ─────────────────────────────────────────────────────────────────────────────────────────────
574
- // EVALUATOR
575
- // ─────────────────────────────────────────────────────────────────────────────────────────────
576
- class Scope {
577
- parent;
578
- vars = new Map();
579
- constructor(parent) {
580
- this.parent = parent;
581
- }
582
- declare(name, value) { this.vars.set(name, value); }
583
- has(name) { return this.vars.has(name) || (this.parent?.has(name) ?? false); }
584
- get(name) {
585
- if (this.vars.has(name))
586
- return this.vars.get(name);
587
- return this.parent ? this.parent.get(name) : null;
588
- }
589
- /** Assign to the nearest enclosing declaration; otherwise declare at the ROOT (a Lua global). */
590
- set(name, value) {
591
- if (this.vars.has(name)) {
592
- this.vars.set(name, value);
593
- return;
594
- }
595
- if (this.parent) {
596
- this.parent.set(name, value);
597
- return;
598
- }
599
- this.vars.set(name, value);
600
- }
601
- }
602
- const BREAK = Symbol('lua-break');
603
- export function luaTruthy(v) {
604
- return v !== null && v !== false;
605
- }
606
- export function luaTypeName(v) {
607
- if (v === null)
608
- return 'nil';
609
- if (typeof v === 'boolean')
610
- return 'boolean';
611
- if (typeof v === 'number')
612
- return 'number';
613
- if (typeof v === 'string')
614
- return 'string';
615
- if (v instanceof LuaTable)
616
- return 'table';
617
- return 'function';
618
- }
619
- /** Lua's arithmetic string→number coercion. Throws (as Lua does) on a non-numeric operand. */
620
- function arith(v, op) {
621
- if (typeof v === 'number')
622
- return v;
623
- if (typeof v === 'string') {
624
- const n = v.trim() === '' ? Number.NaN : Number(v);
625
- if (Number.isFinite(n))
626
- return n;
627
- }
628
- throw new LuaError(`attempt to perform arithmetic (${op}) on a ${luaTypeName(v)} value`);
629
- }
630
- /** Lua's `..` coercion: numbers stringify, everything else but a string is an error. */
631
- function concatOperand(v) {
632
- if (typeof v === 'string')
633
- return v;
634
- if (typeof v === 'number')
635
- return numberToLuaString(v);
636
- throw new LuaError(`attempt to concatenate a ${luaTypeName(v)} value`);
637
- }
638
- /** Lua 5.1's `%.14g` number formatting — `3` prints as "3", not "3.0". */
639
- export function numberToLuaString(n) {
640
- if (Number.isInteger(n) && Math.abs(n) < 1e15)
641
- return String(n);
642
- if (!Number.isFinite(n))
643
- return n > 0 ? 'inf' : Number.isNaN(n) ? 'nan' : '-inf';
644
- const g = n.toPrecision(14);
645
- return g.includes('e') ? String(Number(g)) : String(Number(g));
646
- }
647
- /** `tonumber`, exactly: a number passes through, a numeric string converts, anything else is nil. */
648
- export function luaToNumber(v, base) {
649
- if (base !== undefined && base !== 10) {
650
- if (typeof v !== 'string')
651
- return null;
652
- const n = Number.parseInt(v.trim(), base);
653
- return Number.isNaN(n) ? null : n;
654
- }
655
- if (typeof v === 'number')
656
- return v;
657
- if (typeof v !== 'string')
658
- return null;
659
- const s = v.trim();
660
- if (s === '')
661
- return null;
662
- const n = /^0[xX][0-9a-fA-F]+$/.test(s) ? Number.parseInt(s, 16) : Number(s);
663
- return Number.isFinite(n) ? n : null;
664
- }
665
- class Interpreter {
666
- host;
667
- steps = 0;
668
- /** The script line of the call being evaluated: where a `redis.call` error says it happened. */
669
- line = 1;
670
- /** A runaway script must not hang the twin. Real Redis has `busy-reply-threshold`; this is the
671
- * deterministic equivalent — a hard step ceiling that fails LOUDLY rather than spinning. */
672
- static MAX_STEPS = 2_000_000;
673
- constructor(host) {
674
- this.host = host;
675
- }
676
- tick() {
677
- if (++this.steps > Interpreter.MAX_STEPS)
678
- throw new LuaError('script exceeded the interpreter step limit (possible infinite loop)');
679
- }
680
- run(chunk) {
681
- const globals = new Scope(null);
682
- this.installStdlib(globals);
683
- const signal = this.execBlock(chunk, new Scope(globals));
684
- if (signal === BREAK)
685
- throw new LuaError("'break' outside a loop");
686
- return signal && 'ret' in signal ? (signal.ret[0] ?? null) : null;
687
- }
688
- installStdlib(g) {
689
- const host = this.host;
690
- g.declare('KEYS', LuaTable.fromArray(host.keys));
691
- g.declare('ARGV', LuaTable.fromArray(host.argv));
692
- const one = (v) => [v];
693
- g.declare('tonumber', (a) => one(luaToNumber(a[0] ?? null, a[1] === undefined || a[1] === null ? undefined : Number(arith(a[1], 'tonumber')))));
694
- function luaTostring(a) {
695
- const v = a[0] ?? null;
696
- if (typeof v === 'string')
697
- return one(v);
698
- if (typeof v === 'number')
699
- return one(numberToLuaString(v));
700
- if (v === null)
701
- return one('nil');
702
- if (typeof v === 'boolean')
703
- return one(String(v));
704
- return one(luaTypeName(v));
705
- }
706
- g.declare('tostring', luaTostring);
707
- g.declare('type', (a) => one(luaTypeName(a[0] ?? null)));
708
- function luaError(a) {
709
- const v = a[0] ?? null;
710
- if (v instanceof LuaTable) {
711
- const err = v.get('err');
712
- throw new LuaError(typeof err === 'string' ? err : 'script raised a table error');
713
- }
714
- throw new LuaError(typeof v === 'string' ? v : `script error: ${luaTypeName(v)}`);
715
- }
716
- g.declare('error', luaError);
717
- function luaAssert(a) {
718
- if (!luaTruthy(a[0] ?? null))
719
- throw new LuaError(typeof a[1] === 'string' ? a[1] : 'assertion failed!');
720
- return a;
721
- }
722
- g.declare('assert', luaAssert);
723
- function luaSelect(a) {
724
- const sel = a[0] ?? null;
725
- const rest = a.slice(1);
726
- if (sel === '#')
727
- return one(rest.length);
728
- const n = Number(arith(sel, 'select'));
729
- return rest.slice(n - 1);
730
- }
731
- g.declare('select', luaSelect);
732
- g.declare('rawget', (a) => one(a[0] instanceof LuaTable ? a[0].get(a[1] ?? null) : null));
733
- g.declare('rawset', (a) => { if (a[0] instanceof LuaTable)
734
- a[0].set(a[1] ?? null, a[2] ?? null); return one(a[0] ?? null); });
735
- g.declare('unpack', (a) => (a[0] instanceof LuaTable ? a[0].toArray() : []));
736
- function luaIpairs(a) {
737
- const t = a[0];
738
- if (!(t instanceof LuaTable))
739
- throw new LuaError('bad argument #1 to \'ipairs\' (table expected)');
740
- const iter = (args) => {
741
- const i = Number(arith(args[1] ?? 0, 'ipairs')) + 1;
742
- const v = t.get(i);
743
- return v === null ? [null] : [i, v];
744
- };
745
- return [iter, t, 0];
746
- }
747
- g.declare('ipairs', luaIpairs);
748
- function luaPairs(a) {
749
- const t = a[0];
750
- if (!(t instanceof LuaTable))
751
- throw new LuaError('bad argument #1 to \'pairs\' (table expected)');
752
- const entries = [...t.map.entries()];
753
- let idx = 0;
754
- const iter = () => {
755
- if (idx >= entries.length)
756
- return [null];
757
- const [k, v] = entries[idx++];
758
- return [k, v];
759
- };
760
- return [iter, t, null];
761
- }
762
- g.declare('pairs', luaPairs);
763
- const math = new LuaTable();
764
- const num = (a, i, who) => Number(arith(a[i] ?? null, who));
765
- math.set('floor', (a) => one(Math.floor(num(a, 0, 'math.floor'))));
766
- math.set('ceil', (a) => one(Math.ceil(num(a, 0, 'math.ceil'))));
767
- math.set('abs', (a) => one(Math.abs(num(a, 0, 'math.abs'))));
768
- math.set('sqrt', (a) => one(Math.sqrt(num(a, 0, 'math.sqrt'))));
769
- math.set('pow', (a) => one(num(a, 0, 'math.pow') ** num(a, 1, 'math.pow')));
770
- math.set('fmod', (a) => one(num(a, 0, 'math.fmod') % num(a, 1, 'math.fmod')));
771
- math.set('modf', (a) => { const v = num(a, 0, 'math.modf'); const i = v < 0 ? Math.ceil(v) : Math.floor(v); return [i, v - i]; });
772
- math.set('min', (a) => one(Math.min(...a.map((_, i) => num(a, i, 'math.min')))));
773
- math.set('max', (a) => one(Math.max(...a.map((_, i) => num(a, i, 'math.max')))));
774
- math.set('huge', Number.POSITIVE_INFINITY);
775
- // DETERMINISM: real Redis seeds Lua's RNG identically for every script run so replicas agree.
776
- // This twin does the same with a fixed LCG — a script that calls math.random gets a repeatable
777
- // sequence rather than the twin's determinism being silently broken by Math.random().
778
- let seed = 0x2545_f491;
779
- const nextRandom = () => { seed = (seed * 1_103_515_245 + 12_345) >>> 0; return seed / 0x1_0000_0000; };
780
- math.set('randomseed', (a) => { seed = (num(a, 0, 'math.randomseed') >>> 0) || 1; return []; });
781
- function luaMathRandom(a) {
782
- if (a.length === 0)
783
- return one(nextRandom());
784
- const lo = a.length === 1 ? 1 : num(a, 0, 'math.random');
785
- const hi = a.length === 1 ? num(a, 0, 'math.random') : num(a, 1, 'math.random');
786
- return one(lo + Math.floor(nextRandom() * (hi - lo + 1)));
787
- }
788
- math.set('random', luaMathRandom);
789
- g.declare('math', math);
790
- const table = new LuaTable();
791
- function luaTableInsert(a) {
792
- const t = a[0];
793
- if (!(t instanceof LuaTable))
794
- throw new LuaError("bad argument #1 to 'table.insert' (table expected)");
795
- if (a.length >= 3) {
796
- const at = Number(arith(a[1] ?? null, 'table.insert'));
797
- for (let i = t.length; i >= at; i--)
798
- t.set(i + 1, t.get(i));
799
- t.set(at, a[2] ?? null);
800
- }
801
- else {
802
- t.set(t.length + 1, a[1] ?? null);
803
- }
804
- return [];
805
- }
806
- table.set('insert', luaTableInsert);
807
- function luaTableRemove(a) {
808
- const t = a[0];
809
- if (!(t instanceof LuaTable))
810
- throw new LuaError("bad argument #1 to 'table.remove' (table expected)");
811
- const n = t.length;
812
- const at = a.length >= 2 ? Number(arith(a[1] ?? null, 'table.remove')) : n;
813
- const removed = t.get(at);
814
- for (let i = at; i < n; i++)
815
- t.set(i, t.get(i + 1));
816
- t.set(n, null);
817
- return one(removed);
818
- }
819
- table.set('remove', luaTableRemove);
820
- table.set('getn', (a) => one(a[0] instanceof LuaTable ? a[0].length : 0));
821
- function luaTableConcat(a) {
822
- const t = a[0];
823
- if (!(t instanceof LuaTable))
824
- throw new LuaError("bad argument #1 to 'table.concat' (table expected)");
825
- const sep = typeof a[1] === 'string' ? a[1] : a[1] === undefined || a[1] === null ? '' : concatOperand(a[1]);
826
- return one(t.toArray().map(concatOperand).join(sep));
827
- }
828
- table.set('concat', luaTableConcat);
829
- g.declare('table', table);
830
- const string = new LuaTable();
831
- const str = (a, i, who) => {
832
- const v = a[i] ?? null;
833
- if (typeof v === 'string')
834
- return v;
835
- if (typeof v === 'number')
836
- return numberToLuaString(v);
837
- throw new LuaError(`bad argument #${i + 1} to '${who}' (string expected, got ${luaTypeName(v)})`);
838
- };
839
- string.set('len', (a) => one(str(a, 0, 'string.len').length));
840
- string.set('lower', (a) => one(str(a, 0, 'string.lower').toLowerCase()));
841
- string.set('upper', (a) => one(str(a, 0, 'string.upper').toUpperCase()));
842
- string.set('rep', (a) => one(str(a, 0, 'string.rep').repeat(Math.max(0, Number(arith(a[1] ?? 0, 'string.rep'))))));
843
- function luaStringSub(a) {
844
- const s = str(a, 0, 'string.sub');
845
- let i = a.length >= 2 ? Number(arith(a[1] ?? 1, 'string.sub')) : 1;
846
- let j = a.length >= 3 && a[2] !== null ? Number(arith(a[2], 'string.sub')) : -1;
847
- if (i < 0)
848
- i = Math.max(s.length + i + 1, 1);
849
- else if (i === 0)
850
- i = 1;
851
- if (j < 0)
852
- j = s.length + j + 1;
853
- else if (j > s.length)
854
- j = s.length;
855
- return one(i > j ? '' : s.slice(i - 1, j));
856
- }
857
- string.set('sub', luaStringSub);
858
- string.set('byte', (a) => one(str(a, 0, 'string.byte').charCodeAt(Math.max(0, (a.length >= 2 ? Number(arith(a[1], 'string.byte')) : 1) - 1)) || 0));
859
- string.set('char', (a) => one(a.map((v) => String.fromCharCode(Number(arith(v, 'string.char')))).join('')));
860
- function luaStringFormat(a) {
861
- const fmt = str(a, 0, 'string.format');
862
- let argIndex = 1;
863
- return one(fmt.replace(/%[-+ #0]*\d*(?:\.\d+)?[sdifgqxX%]/g, (spec) => {
864
- if (spec.endsWith('%'))
865
- return '%';
866
- const v = a[argIndex++] ?? null;
867
- const verb = spec[spec.length - 1];
868
- if (verb === 's' || verb === 'q')
869
- return typeof v === 'string' ? v : concatOperand(v);
870
- if (verb === 'd' || verb === 'i')
871
- return String(Math.trunc(arith(v, 'string.format')));
872
- if (verb === 'x')
873
- return Math.trunc(arith(v, 'string.format')).toString(16);
874
- if (verb === 'X')
875
- return Math.trunc(arith(v, 'string.format')).toString(16).toUpperCase();
876
- return numberToLuaString(arith(v, 'string.format'));
877
- }));
878
- }
879
- string.set('format', luaStringFormat);
880
- // `find`/`gsub` are supported ONLY in their plain-text form. Lua PATTERNS are a different
881
- // language from JS regexes and a silent mistranslation would be exactly the fake success this
882
- // pack forbids, so a pattern with any magic character is REFUSED by name.
883
- const PLAIN_ONLY = /[.%^$*+?()[\]-]/;
884
- function luaStringFind(a) {
885
- const s = str(a, 0, 'string.find');
886
- const pattern = str(a, 1, 'string.find');
887
- const plain = a.length >= 4 && luaTruthy(a[3] ?? null);
888
- if (!plain && PLAIN_ONLY.test(pattern))
889
- throw new LuaError(`unsupported Lua construct: string.find with a Lua PATTERN ('${pattern}') — this twin supports plain-text find only`);
890
- const from = a.length >= 3 && a[2] !== null ? Math.max(1, Number(arith(a[2], 'string.find'))) : 1;
891
- const idx = s.indexOf(pattern, from - 1);
892
- return idx < 0 ? [null] : [idx + 1, idx + pattern.length];
893
- }
894
- string.set('find', luaStringFind);
895
- function luaStringGsub(a) {
896
- const s = str(a, 0, 'string.gsub');
897
- const pattern = str(a, 1, 'string.gsub');
898
- if (PLAIN_ONLY.test(pattern))
899
- throw new LuaError(`unsupported Lua construct: string.gsub with a Lua PATTERN ('${pattern}') — this twin supports plain-text substitution only`);
900
- const repl = str(a, 2, 'string.gsub');
901
- const parts = s.split(pattern);
902
- return [parts.join(repl), parts.length - 1];
903
- }
904
- string.set('gsub', luaStringGsub);
905
- g.declare('string', string);
906
- // ── the `redis` table: the bridge back into this twin's real command core ──────────────────
907
- const redis = new LuaTable();
908
- const toArg = (v) => {
909
- if (typeof v === 'string')
910
- return v;
911
- // Real Redis converts a Lua NUMBER argument to a string by TRUNCATING it toward zero
912
- // (lua_cmsgpack-free path in scripting.c) — 3.7 becomes "3", not "3.7".
913
- if (typeof v === 'number')
914
- return String(Math.trunc(v));
915
- throw new LuaError('Lua redis lib command arguments must be strings or integers');
916
- };
917
- redis.set('call', (a) => {
918
- if (a.length === 0)
919
- throw new LuaError('Please specify at least one argument for redis.call()');
920
- return one(host.call(a.map(toArg), this.line));
921
- });
922
- const redisPcall = (a) => {
923
- if (a.length === 0)
924
- throw new LuaError('Please specify at least one argument for redis.pcall()');
925
- try {
926
- return one(host.call(a.map(toArg), this.line));
927
- }
928
- catch (e) {
929
- const t = new LuaTable();
930
- t.set('err', e instanceof Error ? e.message : String(e));
931
- return one(t);
932
- }
933
- };
934
- redis.set('pcall', redisPcall);
935
- redis.set('error_reply', (a) => { const t = new LuaTable(); t.set('err', typeof a[0] === 'string' ? a[0] : 'ERR'); return one(t); });
936
- redis.set('status_reply', (a) => { const t = new LuaTable(); t.set('ok', typeof a[0] === 'string' ? a[0] : 'OK'); return one(t); });
937
- redis.set('sha1hex', (a) => one(createHash('sha1').update(typeof a[0] === 'string' ? a[0] : concatOperand(a[0] ?? '')).digest('hex')));
938
- redis.set('log', () => []);
939
- redis.set('breakpoint', () => one(false));
940
- redis.set('debug', () => []);
941
- redis.set('setresp', () => []);
942
- redis.set('replicate_commands', () => one(true));
943
- redis.set('LOG_DEBUG', 0);
944
- redis.set('LOG_VERBOSE', 1);
945
- redis.set('LOG_NOTICE', 2);
946
- redis.set('LOG_WARNING', 3);
947
- g.declare('redis', redis);
948
- g.declare('server', redis); // Redis 7 alias
949
- }
950
- execBlock(stats, scope) {
951
- for (const s of stats) {
952
- const sig = this.execStat(s, scope);
953
- if (sig)
954
- return sig;
955
- }
956
- return null;
957
- }
958
- execStat(s, scope) {
959
- this.tick();
960
- switch (s.t) {
961
- case 'local': {
962
- const values = this.evalList(s.exprs, scope, s.names.length);
963
- s.names.forEach((n, i) => scope.declare(n, values[i] ?? null));
964
- return null;
965
- }
966
- case 'assign': {
967
- const values = this.evalList(s.exprs, scope, s.targets.length);
968
- s.targets.forEach((target, i) => {
969
- const v = values[i] ?? null;
970
- if (target.t === 'name') {
971
- scope.set(target.v, v);
972
- return;
973
- }
974
- if (target.t === 'index') {
975
- const obj = this.evalExpr(target.obj, scope);
976
- if (!(obj instanceof LuaTable))
977
- throw new LuaError(`attempt to index a ${luaTypeName(obj)} value`);
978
- obj.set(this.evalExpr(target.key, scope), v);
979
- return;
980
- }
981
- throw new LuaError('invalid assignment target');
982
- });
983
- return null;
984
- }
985
- case 'callstat':
986
- this.evalMulti(s.call, scope);
987
- return null;
988
- case 'do': return this.execBlock(s.body, new Scope(scope));
989
- case 'if': {
990
- for (const b of s.branches) {
991
- if (luaTruthy(this.evalExpr(b.cond, scope)))
992
- return this.execBlock(b.body, new Scope(scope));
993
- }
994
- return s.orelse ? this.execBlock(s.orelse, new Scope(scope)) : null;
995
- }
996
- case 'while': {
997
- while (luaTruthy(this.evalExpr(s.cond, scope))) {
998
- this.tick();
999
- const sig = this.execBlock(s.body, new Scope(scope));
1000
- if (sig === BREAK)
1001
- break;
1002
- if (sig)
1003
- return sig;
1004
- }
1005
- return null;
1006
- }
1007
- case 'repeat': {
1008
- for (;;) {
1009
- this.tick();
1010
- // Lua scopes the `until` condition INSIDE the loop body's scope.
1011
- const inner = new Scope(scope);
1012
- const sig = this.execBlock(s.body, inner);
1013
- if (sig === BREAK)
1014
- break;
1015
- if (sig)
1016
- return sig;
1017
- if (luaTruthy(this.evalExpr(s.cond, inner)))
1018
- break;
1019
- }
1020
- return null;
1021
- }
1022
- case 'fornum': {
1023
- const from = arith(this.evalExpr(s.from, scope), "'for' initial value");
1024
- const to = arith(this.evalExpr(s.to, scope), "'for' limit");
1025
- const step = s.step ? arith(this.evalExpr(s.step, scope), "'for' step") : 1;
1026
- if (step === 0)
1027
- throw new LuaError("'for' step is zero");
1028
- for (let i = from; step > 0 ? i <= to : i >= to; i += step) {
1029
- this.tick();
1030
- const inner = new Scope(scope);
1031
- inner.declare(s.name, i);
1032
- const sig = this.execBlock(s.body, inner);
1033
- if (sig === BREAK)
1034
- break;
1035
- if (sig)
1036
- return sig;
1037
- }
1038
- return null;
1039
- }
1040
- case 'forin': {
1041
- const [iter, state, initial] = this.evalList(s.exprs, scope, 3);
1042
- if (typeof iter !== 'function')
1043
- throw new LuaError(`attempt to call a ${luaTypeName(iter ?? null)} value in a generic for`);
1044
- let control = initial ?? null;
1045
- for (;;) {
1046
- this.tick();
1047
- const results = iter([state ?? null, control]);
1048
- if ((results[0] ?? null) === null)
1049
- break;
1050
- control = results[0];
1051
- const inner = new Scope(scope);
1052
- s.names.forEach((n, i) => inner.declare(n, results[i] ?? null));
1053
- const sig = this.execBlock(s.body, inner);
1054
- if (sig === BREAK)
1055
- break;
1056
- if (sig)
1057
- return sig;
1058
- }
1059
- return null;
1060
- }
1061
- case 'return': return { ret: this.evalList(s.exprs, scope, s.exprs.length) };
1062
- case 'break': return BREAK;
1063
- }
1064
- }
1065
- /** Evaluate an expression list, expanding the LAST expression's multiple returns (Lua rule). */
1066
- evalList(exprs, scope, want) {
1067
- const out = [];
1068
- exprs.forEach((e, i) => {
1069
- if (i === exprs.length - 1)
1070
- out.push(...this.evalMulti(e, scope));
1071
- else
1072
- out.push(this.evalExpr(e, scope));
1073
- });
1074
- while (out.length < want)
1075
- out.push(null);
1076
- return out;
1077
- }
1078
- /** Evaluate an expression in "multiple results" position. */
1079
- evalMulti(e, scope) {
1080
- if (e.t !== 'call')
1081
- return [this.evalExpr(e, scope)];
1082
- const fn = this.evalExpr(e.fn, scope);
1083
- if (typeof fn !== 'function') {
1084
- const name = e.fn.t === 'name' ? ` (global '${e.fn.v}')` : e.fn.t === 'index' && e.fn.key.t === 'str' ? ` (field '${e.fn.key.v}')` : '';
1085
- throw new LuaError(`attempt to call a ${luaTypeName(fn)} value${name} — unsupported or undefined in this twin's Lua subset`);
1086
- }
1087
- const args = this.evalList(e.args, scope, e.args.length);
1088
- this.tick();
1089
- if (e.line !== undefined)
1090
- this.line = e.line;
1091
- return fn(args);
1092
- }
1093
- evalExpr(e, scope) {
1094
- this.tick();
1095
- switch (e.t) {
1096
- case 'nil': return null;
1097
- case 'true': return true;
1098
- case 'false': return false;
1099
- case 'num': return e.v;
1100
- case 'str': return e.v;
1101
- case 'name': {
1102
- if (!scope.has(e.v)) {
1103
- // A Lua global that was never assigned reads as nil. That is correct Lua — but for the
1104
- // handful of names that are LIBRARIES this interpreter deliberately does not ship, a
1105
- // silent nil turns into "attempt to call a nil value" three lines later, which reads as
1106
- // a script bug rather than a twin limitation. Name them here instead.
1107
- if (['cjson', 'cmsgpack', 'bit', 'struct', 'os', 'io', 'setmetatable', 'getmetatable', 'pcall', 'xpcall', 'loadstring', 'require', 'coroutine'].includes(e.v)) {
1108
- throw new LuaError(`unsupported Lua library '${e.v}' — this twin runs a documented Lua SUBSET (see upstash-lua.ts)`);
1109
- }
1110
- return null;
1111
- }
1112
- return scope.get(e.v);
1113
- }
1114
- case 'index': {
1115
- const obj = this.evalExpr(e.obj, scope);
1116
- if (!(obj instanceof LuaTable)) {
1117
- if (typeof obj === 'string')
1118
- throw new LuaError('unsupported Lua construct: indexing a string value (string metatable methods are not modeled)');
1119
- throw new LuaError(`attempt to index a ${luaTypeName(obj)} value`);
1120
- }
1121
- return obj.get(this.evalExpr(e.key, scope));
1122
- }
1123
- case 'call': return this.evalMulti(e, scope)[0] ?? null;
1124
- case 'table': {
1125
- const t = new LuaTable();
1126
- e.array.forEach((item, i) => {
1127
- // The LAST array item expands its multiple returns, exactly as Lua does.
1128
- if (i === e.array.length - 1)
1129
- this.evalMulti(item, scope).forEach((v, j) => t.set(i + 1 + j, v));
1130
- else
1131
- t.set(i + 1, this.evalExpr(item, scope));
1132
- });
1133
- for (const p of e.pairs)
1134
- t.set(this.evalExpr(p.k, scope), this.evalExpr(p.v, scope));
1135
- return t;
1136
- }
1137
- case 'un': {
1138
- if (e.op === 'not')
1139
- return !luaTruthy(this.evalExpr(e.e, scope));
1140
- const v = this.evalExpr(e.e, scope);
1141
- if (e.op === '-')
1142
- return -arith(v, 'unary -');
1143
- if (e.op === '#') {
1144
- if (typeof v === 'string')
1145
- return v.length;
1146
- if (v instanceof LuaTable)
1147
- return v.length;
1148
- throw new LuaError(`attempt to get length of a ${luaTypeName(v)} value`);
1149
- }
1150
- throw new LuaError(`unsupported unary operator '${e.op}'`);
1151
- }
1152
- case 'bin': return this.evalBinary(e, scope);
1153
- case 'func': {
1154
- const params = e.params;
1155
- const body = e.body;
1156
- const closure = (args) => {
1157
- const inner = new Scope(scope);
1158
- params.forEach((p, i) => inner.declare(p, args[i] ?? null));
1159
- const sig = this.execBlock(body, inner);
1160
- if (sig === BREAK)
1161
- throw new LuaError("'break' outside a loop");
1162
- return sig && 'ret' in sig ? sig.ret : [];
1163
- };
1164
- return closure;
1165
- }
1166
- }
1167
- }
1168
- evalBinary(e, scope) {
1169
- // `and`/`or` short-circuit and return an OPERAND, not a boolean.
1170
- if (e.op === 'and') {
1171
- const l = this.evalExpr(e.l, scope);
1172
- return luaTruthy(l) ? this.evalExpr(e.r, scope) : l;
1173
- }
1174
- if (e.op === 'or') {
1175
- const l = this.evalExpr(e.l, scope);
1176
- return luaTruthy(l) ? l : this.evalExpr(e.r, scope);
1177
- }
1178
- const l = this.evalExpr(e.l, scope);
1179
- const r = this.evalExpr(e.r, scope);
1180
- switch (e.op) {
1181
- case '+': return arith(l, '+') + arith(r, '+');
1182
- case '-': return arith(l, '-') - arith(r, '-');
1183
- case '*': return arith(l, '*') * arith(r, '*');
1184
- case '/': return arith(l, '/') / arith(r, '/');
1185
- case '^': return arith(l, '^') ** arith(r, '^');
1186
- case '%': {
1187
- // Lua's `%` is `a - floor(a/b)*b` — the result takes the SIGN OF THE DIVISOR, unlike JS.
1188
- const a = arith(l, '%');
1189
- const b = arith(r, '%');
1190
- return a - Math.floor(a / b) * b;
1191
- }
1192
- case '..': return concatOperand(l) + concatOperand(r);
1193
- // STRICT equality across types — `"3" == 3` is FALSE in Lua. See the file header (2).
1194
- case '==': return luaEquals(l, r);
1195
- case '~=': return !luaEquals(l, r);
1196
- case '<':
1197
- case '<=':
1198
- case '>':
1199
- case '>=': {
1200
- if (typeof l === 'number' && typeof r === 'number') {
1201
- return e.op === '<' ? l < r : e.op === '<=' ? l <= r : e.op === '>' ? l > r : l >= r;
1202
- }
1203
- if (typeof l === 'string' && typeof r === 'string') {
1204
- const c = l < r ? -1 : l > r ? 1 : 0;
1205
- return e.op === '<' ? c < 0 : e.op === '<=' ? c <= 0 : e.op === '>' ? c > 0 : c >= 0;
1206
- }
1207
- throw new LuaError(`attempt to compare ${luaTypeName(l)} with ${luaTypeName(r)}`);
1208
- }
1209
- default: throw new LuaError(`unsupported binary operator '${e.op}'`);
1210
- }
1211
- }
1212
- }
1213
- /** Lua `==`: same type required (no string/number coercion); tables compare by identity. */
1214
- export function luaEquals(l, r) {
1215
- if (l === null || r === null)
1216
- return l === r;
1217
- if (typeof l !== typeof r)
1218
- return false;
1219
- return l === r;
1220
- }
1221
- /** Parse + run a script. Returns the Lua value the chunk returned (or `nil`). */
1
+ // Upstash's Lua: the kernel's interpreter (@volter/world-core/redis, lua.ts), with the command API named as Redis's
2
+ // scripts call it (`redis`, and `server` since Redis 7). The interpreter, its supported subset and its loud refusals
3
+ // of what it does not model are the kernel's; Upstash's scripts load none of Redis's runtime libraries beyond it.
4
+ import { runLua as runOnKernel } from '@volter/world-core/redis';
5
+ export { LuaError, LuaTable, luaEquals, luaScriptSha1, luaToNumber, luaTruthy, luaTypeName, numberToLuaString, parseLua, tokenize, } from '@volter/world-core/redis';
6
+ /** The command API under the names Redis's scripts call it by. */
7
+ export function nameCommandApi(g) {
8
+ g.set('redis', g.commandApi);
9
+ g.set('server', g.commandApi);
10
+ }
11
+ /** Parse + run a script with the command API named: the kernel's `runLua`. */
1222
12
  export function runLua(script, host) {
1223
- const chunk = parseLua(script);
1224
- return new Interpreter(host).run(chunk);
1225
- }
1226
- /** The SHA1 real Redis keys the script cache by — the same hex digest `SCRIPT LOAD` returns. */
1227
- export function luaScriptSha1(script) {
1228
- return createHash('sha1').update(script).digest('hex');
13
+ // Upstash's scripts: a Lua number argument truncated toward zero (live-probed), unless the caller says otherwise
14
+ return runOnKernel(script, { numberArg: (n) => String(Math.trunc(n)), ...host, extend: (g) => { nameCommandApi(g); host.extend?.(g); } });
1229
15
  }