@homie-rocks/studio 0.32.1 → 0.33.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.
- package/CHANGELOG.md +67 -1
- package/README.md +24 -6
- package/agents/agents.ts +57 -24
- package/bin/homie-studio.mjs +3 -3
- package/lib/build.mjs +64 -9
- package/lib/cf-usage.mjs +76 -0
- package/lib/cloudflare.mjs +25 -2
- package/lib/dev.mjs +45 -4
- package/lib/lab-check.mjs +3 -2
- package/lib/mcp-tools.mjs +1 -1
- package/lib/mcp.mjs +1 -1
- package/lib/office.mjs +7 -0
- package/lib/perf.mjs +8 -3
- package/lib/player-image.mjs +40 -0
- package/lib/rules-build.mjs +184 -0
- package/lib/rules-guard.mjs +770 -0
- package/lib/scaffold.mjs +43 -16
- package/lib/shop-catalog.mjs +10 -9
- package/lib/shop.mjs +32 -15
- package/lib/studio.mjs +22 -1
- package/lib/template-history.json +90 -0
- package/lib/upgrade.mjs +19 -15
- package/lib/version.mjs +2 -7
- package/netplay/NETPLAY.md +249 -4
- package/netplay/netplay.ts +55 -13
- package/package.json +23 -1
- package/rules/core.ts +1250 -0
- package/rules/guard.ts +419 -0
- package/rules/host.ts +707 -0
- package/rules/math.ts +266 -0
- package/rules/pack.ts +292 -0
- package/rules/rules.ts +544 -0
- package/rules/view.ts +447 -0
- package/shop/SELLING.md +14 -16
- package/shop/SHOP.md +119 -30
- package/shop/shop.ts +6 -8
- package/site/SITE.md +119 -2
- package/starters/coin-dash/game.json +11 -0
- package/starters/coin-dash/index.html +17 -0
- package/starters/coin-dash/map/main.json +16 -0
- package/starters/coin-dash/src/move.ts +27 -0
- package/starters/coin-dash/src/rules.ts +86 -0
- package/starters/coin-dash/src/view.ts +106 -0
- package/starters/coin-dash/tunables.json +5 -0
- package/starters/ember-vale/index.html +11 -0
- package/worker/brain.mjs +8 -7
- package/worker/chat-page.mjs +1 -1
- package/worker/embed.mjs +135 -0
- package/worker/hosted.mjs +56 -0
- package/worker/index.mjs +413 -22
- package/worker/office-page.mjs +1 -0
- package/worker/office.mjs +24 -7
- package/worker/pages.mjs +189 -43
- package/worker/players.mjs +10 -6
- package/worker/referrals.mjs +192 -57
- package/worker/room-store.mjs +54 -0
- package/worker/room.mjs +294 -119
- package/worker/saves-shell.mjs +7 -1
- package/worker/schema.mjs +4 -3
- package/worker/shop-page.mjs +103 -37
- package/worker/shop-rules.mjs +156 -117
- package/worker/shop-store.mjs +70 -9
- package/worker/shop.mjs +275 -143
- package/worker/site.mjs +26 -5
- package/worker/stripe.mjs +9 -6
- package/worker/version.mjs +1 -1
package/rules/guard.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* guard.ts — what guarded rules call while they run: the step budget, the size cap and the guarded accesses.
|
|
3
|
+
* =============================================================================
|
|
4
|
+
*
|
|
5
|
+
* `homie-studio build` rewrites every rules module (lib/rules-guard.mjs): a counter at the top of every loop and every
|
|
6
|
+
* function, every computed key through `g`/`s`/`u`, every method call through `c`. This file is the other half: the
|
|
7
|
+
* functions those rewrites call. It runs wherever rules run: inside the `Table`, in a browser (a view's `move`), and in
|
|
8
|
+
* Node for the build check. No imports, erasable TypeScript only.
|
|
9
|
+
*
|
|
10
|
+
* It is a wall against a careless module written by the studio's own AI, not a sandbox against code written to attack.
|
|
11
|
+
* Rules cannot catch what it throws, because the build refuses `try` in rules.
|
|
12
|
+
*
|
|
13
|
+
* THE BUDGET. `G.left` is the units the running handler may still use. The host runtime sets it before each handler
|
|
14
|
+
* and reads it after. Every charge takes from it; below zero, `BudgetError` is thrown and the runtime abandons that
|
|
15
|
+
* handler, as it does for any throw. Outside a handler it is Infinity, so module load is never charged.
|
|
16
|
+
* =============================================================================
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** No array, string, `Map` or `Set` in rules may pass this many entries. */
|
|
20
|
+
export const SIZE_CAP = 65_536;
|
|
21
|
+
|
|
22
|
+
/** The methods rules may call, by the kind of value they are called on. Any other method name is refused. */
|
|
23
|
+
export const ARRAY_METHODS: ReadonlySet<string> = new Set(['push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'concat', 'indexOf', 'lastIndexOf', 'includes', 'find', 'findIndex', 'some', 'every', 'map', 'filter', 'reduce', 'forEach', 'flat', 'flatMap', 'sort', 'reverse', 'join', 'at']);
|
|
24
|
+
export const STRING_METHODS: ReadonlySet<string> = new Set(['slice', 'indexOf', 'includes', 'startsWith', 'endsWith', 'split', 'trim', 'toUpperCase', 'toLowerCase', 'charCodeAt', 'at']);
|
|
25
|
+
export const MAPSET_METHODS: ReadonlySet<string> = new Set(['get', 'set', 'has', 'add', 'delete', 'clear', 'forEach', 'keys', 'values', 'entries']);
|
|
26
|
+
/** Property names no rule may name, in any position (and anything beginning `toLocale`). */
|
|
27
|
+
export const REFUSED_NAMES: ReadonlySet<string> = new Set(['constructor', 'prototype', '__proto__', 'stack', 'localeCompare']);
|
|
28
|
+
/**
|
|
29
|
+
* The names JavaScript itself calls when it turns an object into a number or a text. A plain value may sit under one
|
|
30
|
+
* (it is only a field with an odd name); a function may not, so no code of the rules can be run by a conversion.
|
|
31
|
+
*/
|
|
32
|
+
export const HOOK_NAMES: ReadonlySet<string> = new Set(['valueOf', 'toString', 'toJSON']);
|
|
33
|
+
export const refusedName = (k: string): boolean => REFUSED_NAMES.has(k) || k.startsWith('toLocale');
|
|
34
|
+
|
|
35
|
+
/** The running handler's budget. One per isolate: handlers never run at the same time. */
|
|
36
|
+
export const G = { left: Infinity };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An object's own data property, read without running anything: a property that is a getter reads as absent, and so
|
|
40
|
+
* does one the object only inherits. The runtime reads everything the rules hand it this way (pack.ts, "THE BOUNDARY").
|
|
41
|
+
*/
|
|
42
|
+
export function own(o: unknown, key: string | number): unknown {
|
|
43
|
+
if (o === null || typeof o !== 'object') return undefined;
|
|
44
|
+
const d = Object.getOwnPropertyDescriptor(o, key);
|
|
45
|
+
return d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The other way: a value written to an object the rules handed over, as plain data. A property that is a setter is never run: the write is refused. */
|
|
49
|
+
export function put(o: object, key: string, v: unknown): void {
|
|
50
|
+
const d = Object.getOwnPropertyDescriptor(o, key);
|
|
51
|
+
if (d !== undefined && !Object.hasOwn(d, 'value')) throw new TypeError(`"${key}" is not a plain property of this value`);
|
|
52
|
+
(o as Record<string, unknown>)[key] = v;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Plain data with no declared shape (a game's own answer to `world.ask`, the `init` of a declared field): numbers,
|
|
57
|
+
* texts, true, false, lists and objects of them, `left.n` values at most and `deep` levels deep at most. Nothing of the
|
|
58
|
+
* value is run to read it, and anything else is null. The count is of values read, so a list that holds another twice
|
|
59
|
+
* is copied twice only for as long as the count lasts: no shape of value makes this take long.
|
|
60
|
+
*/
|
|
61
|
+
export function plainData(v: unknown, left: { n: number }, deep = 4, text = 256): unknown {
|
|
62
|
+
left.n -= 1;
|
|
63
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
|
|
64
|
+
if (typeof v === 'boolean' || v === null) return v;
|
|
65
|
+
if (typeof v === 'string') return v.slice(0, text);
|
|
66
|
+
if (typeof v !== 'object' || deep <= 0 || left.n <= 0) return null;
|
|
67
|
+
if (Array.isArray(v)) {
|
|
68
|
+
const out: unknown[] = [];
|
|
69
|
+
for (let i = 0; i < v.length && left.n > 0; i += 1) out.push(plainData(own(v, i), left, deep - 1, text));
|
|
70
|
+
return Object.freeze(out);
|
|
71
|
+
}
|
|
72
|
+
const out: Record<string, unknown> = {};
|
|
73
|
+
let seen = 0;
|
|
74
|
+
// A constant's keys are all listed before the first is read: they are paid for first. (Anything else the handler made, and paid for.)
|
|
75
|
+
const known = keyCount(v);
|
|
76
|
+
if (known !== undefined) charge(known);
|
|
77
|
+
for (const key in v) {
|
|
78
|
+
// Keys are counted as they are met, so an object with very many is left after the first few.
|
|
79
|
+
if ((seen += 1) > 64 || left.n <= 0) break;
|
|
80
|
+
if (Object.hasOwn(v, key) && key.length <= 32 && !refusedName(key)) out[key] = plainData(own(v, key), left, deep - 1, text);
|
|
81
|
+
}
|
|
82
|
+
return Object.freeze(out);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Thrown when a handler has used its share of the tick. Rules cannot catch it. */
|
|
86
|
+
export class BudgetError extends Error {
|
|
87
|
+
constructor() { super('this handler ran too long: it used its whole share of the tick (a loop that never ends, or too much work in one step)'); this.name = 'BudgetError'; }
|
|
88
|
+
}
|
|
89
|
+
/** The one BudgetError there is: thrown again each time, so stopping a handler makes nothing (a full room's bodies may each be stopped on every tick). */
|
|
90
|
+
const STOP = new BudgetError();
|
|
91
|
+
/** Thrown when rules reach for something outside the wall at run time (a built key, a method that is not allowed, a size past the cap). */
|
|
92
|
+
export class GuardError extends Error {
|
|
93
|
+
constructor(message: string) { super(message); this.name = 'GuardError'; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Take `n` units from the running handler. The host runtime's own functions call it with the weights of the design. */
|
|
97
|
+
export function charge(n: number): void {
|
|
98
|
+
// Written so that anything but a count still inside the budget stops the handler: a charge that is not a number can never switch the counter off.
|
|
99
|
+
if (!((G.left -= n) >= 0)) throw STOP;
|
|
100
|
+
}
|
|
101
|
+
/** One loop turn, or one call of a function in rules. */
|
|
102
|
+
export function t(): void {
|
|
103
|
+
if (!((G.left -= 1) >= 0)) throw STOP;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What one key of an object costs when the whole object is copied or gone through: a spread, a rest in a pattern, a constant's keys. */
|
|
107
|
+
export const KEY = 24;
|
|
108
|
+
/** What one guarded method call costs before anything it does: finding which kind of value it is called on, and the call itself. */
|
|
109
|
+
export const CALL = 3;
|
|
110
|
+
|
|
111
|
+
/** Objects the host runtime made (`world`, `ctx`, `world.math`, the map, the round): rules may call their own functions. */
|
|
112
|
+
const HOST = new WeakSet<object>();
|
|
113
|
+
export function brand<T extends object>(o: T): T { HOST.add(o); return o; }
|
|
114
|
+
|
|
115
|
+
const at = (line: number | undefined): string => (line ? ` (line ${line})` : '');
|
|
116
|
+
function refuse(what: string, line?: number): never { throw new GuardError(`${what}${at(line)}`); }
|
|
117
|
+
const sized = (n: number, line?: number): void => { if (n > SIZE_CAP) refuse(`a list, text, Map or Set in rules holds at most ${SIZE_CAP} entries`, line); };
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A value about to be used where JavaScript wants a number or a text (an operand of `-`, `<`, `+`; an argument of
|
|
121
|
+
* `Number()`, `String()` or `Math.max()`; a part of a template): it must be one already. Turning a list into a text
|
|
122
|
+
* walks every entry of it and of every list inside it, at a cost no counter sees (a list that holds itself twice,
|
|
123
|
+
* thirty levels deep, is a thousand million entries), so a list, an object or a function is refused here instead.
|
|
124
|
+
*/
|
|
125
|
+
export function p<T>(x: T, line?: number): T {
|
|
126
|
+
const kind = typeof x;
|
|
127
|
+
// A long text is read end to end by whatever uses it here (a comparison, a conversion): a unit for every 64 characters.
|
|
128
|
+
if (kind === 'string') { if ((x as string).length >= 64) charge((x as string).length >> 6); return x; }
|
|
129
|
+
if ((kind === 'object' && x !== null) || kind === 'function' || kind === 'bigint' || kind === 'symbol') refuse(`${Array.isArray(x) ? 'a list' : kind === 'object' ? 'an object' : `a ${kind}`} was used where a number or a text belongs: name the entry or the field you mean`, line);
|
|
130
|
+
return x;
|
|
131
|
+
}
|
|
132
|
+
/** `a === b`: two long texts of one length are compared character by character, so that is charged, a unit for every 64. */
|
|
133
|
+
export function eq(a: unknown, b: unknown): boolean {
|
|
134
|
+
if (typeof a === 'string' && typeof b === 'string' && a.length === b.length && a.length >= 64) charge(a.length >> 6);
|
|
135
|
+
return a === b;
|
|
136
|
+
}
|
|
137
|
+
/** What a `switch` turns on: a long text is compared with each case in turn, and charged for each. */
|
|
138
|
+
export function sw<T>(x: T, cases: number): T {
|
|
139
|
+
if (typeof x === 'string' && x.length >= 64) charge((x.length >> 6) * cases);
|
|
140
|
+
return x;
|
|
141
|
+
}
|
|
142
|
+
/** The value of a property named `valueOf`, `toString` or `toJSON`: never a function. */
|
|
143
|
+
export function nh<T>(v: T, line?: number): T {
|
|
144
|
+
if (typeof v === 'function') refuse('a function may not sit under the name valueOf, toString or toJSON: JavaScript would call it by itself, outside any handler', line);
|
|
145
|
+
return v;
|
|
146
|
+
}
|
|
147
|
+
/** The object of a write, `o.name = v`: never a function. So nothing can be kept on a function, and no built-in function can be changed. */
|
|
148
|
+
export function w<T>(o: T, line?: number): T {
|
|
149
|
+
if (typeof o === 'function') refuse('nothing is kept on a function: state lives in declared fields', line);
|
|
150
|
+
return o;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `new Error(message)`. Making an Error has JavaScript record where it was made, which costs thousands of loop turns
|
|
155
|
+
* and which rules can never read (`stack` is a refused name). So one is made with nothing recorded, and charged.
|
|
156
|
+
*/
|
|
157
|
+
export function err(message?: unknown): Error {
|
|
158
|
+
charge(16);
|
|
159
|
+
const E = Error as unknown as { stackTraceLimit?: number };
|
|
160
|
+
const limit = E.stackTraceLimit;
|
|
161
|
+
E.stackTraceLimit = 0;
|
|
162
|
+
try { return message === undefined ? new Error() : new Error(message as string); } finally { E.stackTraceLimit = limit; }
|
|
163
|
+
}
|
|
164
|
+
/** A number used as a key is a whole number: one with a fraction would be turned into a text to be looked up, which costs as much as fifty loop turns. */
|
|
165
|
+
function whole(k: number, line?: number): void {
|
|
166
|
+
if (!(Number.isInteger(k) && k > -2147483648 && k < 4294967296)) refuse('a number used as a key is a whole number', line);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A computed read, `a[k]`: a number, or a string naming an own property that is not a function. Anything else throws. Never a prototype. */
|
|
170
|
+
export function g(o: any, k: unknown, line?: number): unknown {
|
|
171
|
+
if (o === null || o === undefined) throw new TypeError(`cannot read a key of ${o}${at(line)}`);
|
|
172
|
+
// A unit for the guarded access itself: a loop that does nothing but read keys is then paid for at the rate of any other.
|
|
173
|
+
if (!((G.left -= 1) >= 0)) throw STOP;
|
|
174
|
+
if (typeof k === 'number') { whole(k, line); return o[k]; }
|
|
175
|
+
if (typeof k !== 'string') refuse('a computed key is a number or a string', line);
|
|
176
|
+
if (refusedName(k)) refuse(`the key "${k}" is not allowed in rules`, line);
|
|
177
|
+
const v = o[k];
|
|
178
|
+
if (typeof v === 'function') refuse(`"${k}" is a function: a method may be called, never read as a value`, line);
|
|
179
|
+
// Only what the value holds itself: a key it does not have never falls through to a prototype.
|
|
180
|
+
if (!Object.hasOwn(o, k)) refuse(`there is no "${k}" on this value: a computed key names something the value holds itself (test with \`key in value\` first)`, line);
|
|
181
|
+
return v;
|
|
182
|
+
}
|
|
183
|
+
/** An optional computed read, `a?.[k]`. */
|
|
184
|
+
export function go(o: any, k: unknown, line?: number): unknown {
|
|
185
|
+
return o === null || o === undefined ? undefined : g(o, k, line);
|
|
186
|
+
}
|
|
187
|
+
/** A computed key that is only named (an object literal's `{ [k]: 1 }`, a destructuring pattern's `{ [k]: v }`). */
|
|
188
|
+
export function k(key: unknown, line?: number): string | number {
|
|
189
|
+
if (typeof key === 'number') { whole(key, line); return key; }
|
|
190
|
+
if (typeof key !== 'string') refuse('a computed key is a number or a string', line);
|
|
191
|
+
if (refusedName(key) || HOOK_NAMES.has(key)) refuse(`the key "${key}" is not allowed in rules`, line);
|
|
192
|
+
return key;
|
|
193
|
+
}
|
|
194
|
+
/** A computed write, `a[k] = v`: a number (inside the size cap), or a string other than the refused names. */
|
|
195
|
+
export function s(o: any, key: unknown, v: unknown, line?: number): unknown {
|
|
196
|
+
if (o === null || typeof o !== 'object') throw new TypeError(`cannot write a key of ${o === null ? 'null' : typeof o}${at(line)}`);
|
|
197
|
+
if (!((G.left -= 1) >= 0)) throw STOP;
|
|
198
|
+
if (typeof key === 'number') { whole(key, line); if (Array.isArray(o) && key >= SIZE_CAP) sized(key + 1, line); } else if (typeof key === 'string' && HOOK_NAMES.has(key)) nh(v, line); else k(key, line);
|
|
199
|
+
o[key as string] = v;
|
|
200
|
+
return v;
|
|
201
|
+
}
|
|
202
|
+
/** A computed update, `a[k] += v` or `a[k]++`: the key is read once. `post`: the old value is the result (`a[k]++`). */
|
|
203
|
+
export function u(o: any, key: unknown, fn: (old: any) => unknown, post?: boolean, line?: number): unknown {
|
|
204
|
+
const old = g(o, key, line);
|
|
205
|
+
const next = fn(old);
|
|
206
|
+
s(o, key, next, line);
|
|
207
|
+
return post ? old : next;
|
|
208
|
+
}
|
|
209
|
+
/** A static read of a name that is also a method name (`hit.at`, `e.keys`): the value, unless it is a function. */
|
|
210
|
+
export function rd(o: any, name: string, line?: number): unknown {
|
|
211
|
+
const v = o[name];
|
|
212
|
+
if (typeof v === 'function') refuse(`"${name}" is a function here: a method may be called, never read as a value`, line);
|
|
213
|
+
return v;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** An optional static read, `o?.at`. */
|
|
217
|
+
export function rdo(o: any, name: string, line?: number): unknown {
|
|
218
|
+
return o === null || o === undefined ? undefined : rd(o, name, line);
|
|
219
|
+
}
|
|
220
|
+
/** A value taken out of an object by a destructuring pattern under a method's name: never a function. */
|
|
221
|
+
export function nf<T>(v: T, line?: number): T {
|
|
222
|
+
if (typeof v === 'function') refuse('a destructuring pattern took a function out of a value: a method may be called, never read as a value', line);
|
|
223
|
+
return v;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const lengthOf = (x: unknown): number => (typeof x === 'string' || Array.isArray(x) ? x.length : x instanceof Map || x instanceof Set ? x.size : 1);
|
|
227
|
+
const plainArg = (x: unknown): boolean => { const kind = typeof x; return !((kind === 'object' && x !== null) || kind === 'function' || kind === 'bigint' || kind === 'symbol'); };
|
|
228
|
+
/** The arguments of a built-in method that JavaScript would turn into a number or a text: each must be one already (see `p`). */
|
|
229
|
+
function plainArgs(name: string, args: unknown[], from: number, to: number, line?: number): void {
|
|
230
|
+
for (let i = from; i < to && i < args.length; i += 1) if (!plainArg(args[i])) refuse(`${name}() was handed a list or an object where a number or a text belongs`, line);
|
|
231
|
+
}
|
|
232
|
+
/** How many entries `flat(depth)` would read, stopping as soon as it is past `cap`: a list reached twice is read twice, as `flat` would. */
|
|
233
|
+
function flatSteps(a: unknown[], depth: number, cap: number): number {
|
|
234
|
+
let n = 0;
|
|
235
|
+
// Each list is paid for before it is read.
|
|
236
|
+
charge(a.length);
|
|
237
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
238
|
+
const e = a[i];
|
|
239
|
+
n += 1;
|
|
240
|
+
if (depth >= 1 && Array.isArray(e)) n += flatSteps(e, depth - 1, cap - n);
|
|
241
|
+
if (n > cap) return n;
|
|
242
|
+
}
|
|
243
|
+
return n;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A method call, `o.name(...args)`: an allowed method of a list, a text, a Map or a Set, or a function the host runtime made. */
|
|
247
|
+
export function c(o: any, name: string, args: unknown[], line?: number): unknown {
|
|
248
|
+
if (o === null || o === undefined) throw new TypeError(`cannot call ${name} on ${o}${at(line)}`);
|
|
249
|
+
charge(CALL);
|
|
250
|
+
if (Array.isArray(o)) {
|
|
251
|
+
if (!ARRAY_METHODS.has(name)) refuse(`a list has no method "${name}" that rules may call`, line);
|
|
252
|
+
const n = o.length;
|
|
253
|
+
// Every cost below is charged before the method runs, so a handler on its last unit starts nothing it cannot pay for.
|
|
254
|
+
if (name === 'push') { charge(1 + args.length); sized(n + args.length, line); } else if (name === 'unshift') { charge(1 + n + args.length); sized(n + args.length, line); } else if (name === 'splice') { plainArgs(name, args, 0, 2, line); charge(1 + n + args.length); sized(n + args.length, line); } else if (name === 'concat') {
|
|
255
|
+
let total = n;
|
|
256
|
+
for (const a of args) total += Array.isArray(a) ? a.length : 1;
|
|
257
|
+
charge(1 + total); sized(total, line);
|
|
258
|
+
} else if (name === 'join') {
|
|
259
|
+
// A list of texts and numbers only: a list inside it would be joined in turn, to any depth, uncounted.
|
|
260
|
+
plainArgs(name, args, 0, 1, line);
|
|
261
|
+
let total = 0;
|
|
262
|
+
const sep = args[0] === undefined ? 1 : String(args[0]).length;
|
|
263
|
+
charge(1 + n);
|
|
264
|
+
for (let i = 0; i < n; i += 1) {
|
|
265
|
+
const e = o[i];
|
|
266
|
+
if (!plainArg(e)) refuse('join() takes a list of texts and numbers: this list holds a list or an object', line);
|
|
267
|
+
total += (typeof e === 'string' ? e.length : 24) + sep; if (total > SIZE_CAP) sized(total, line);
|
|
268
|
+
}
|
|
269
|
+
charge(total);
|
|
270
|
+
} else if (name === 'sort') {
|
|
271
|
+
charge(1 + 16 * n);
|
|
272
|
+
const cmp = args[0];
|
|
273
|
+
if (typeof cmp === 'function') {
|
|
274
|
+
// What the comparison returns is turned into a number by the sort itself: it must be one.
|
|
275
|
+
return o.sort((a: unknown, b: unknown) => { const r = cmp(a, b); if (typeof r !== 'number') refuse('the function handed to sort() returns a number: below zero, zero or above', line); return r; });
|
|
276
|
+
}
|
|
277
|
+
// With no comparison the sort compares entries as texts: each must be a text or a number already, and long texts are paid for by their length.
|
|
278
|
+
let chars = 0;
|
|
279
|
+
for (let i = 0; i < n; i += 1) { const e = o[i]; if (!plainArg(e)) refuse('sort() with no comparison takes a list of texts and numbers: hand it a function that compares two entries', line); if (typeof e === 'string') chars += e.length; }
|
|
280
|
+
charge((chars >> 6) * 16);
|
|
281
|
+
} else if (name === 'flat') {
|
|
282
|
+
plainArgs(name, args, 0, 1, line);
|
|
283
|
+
const depth = args[0] === undefined ? 1 : Number(args[0]);
|
|
284
|
+
sized(flatSteps(o, depth >= 1 ? depth : 0, SIZE_CAP), line);
|
|
285
|
+
} else if (name === 'flatMap') {
|
|
286
|
+
// Done here, entry by entry, so the list being made is charged and capped as it grows.
|
|
287
|
+
const fn = args[0];
|
|
288
|
+
if (typeof fn !== 'function') throw new TypeError(`flatMap takes a function${at(line)}`);
|
|
289
|
+
charge(1 + n);
|
|
290
|
+
const out: unknown[] = [];
|
|
291
|
+
for (let i = 0; i < n; i += 1) {
|
|
292
|
+
const r = fn(o[i], i, o);
|
|
293
|
+
if (Array.isArray(r)) { charge(r.length); sized(out.length + r.length, line); for (let j = 0; j < r.length; j += 1) out.push(r[j]); } else out.push(r);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
} else if (name === 'pop') charge(1);
|
|
297
|
+
else if (name === 'at') { plainArgs(name, args, 0, 1, line); charge(1); } else {
|
|
298
|
+
if (name === 'slice') plainArgs(name, args, 0, 2, line);
|
|
299
|
+
else if (name === 'indexOf' || name === 'lastIndexOf' || name === 'includes') {
|
|
300
|
+
plainArgs(name, args, 1, 2, line);
|
|
301
|
+
// Looking for a long text compares it with every entry.
|
|
302
|
+
if (typeof args[0] === 'string' && args[0].length >= 64) charge(n * (args[0].length >> 6));
|
|
303
|
+
}
|
|
304
|
+
charge(1 + n);
|
|
305
|
+
}
|
|
306
|
+
return (o as any)[name](...args);
|
|
307
|
+
}
|
|
308
|
+
if (typeof o === 'string') {
|
|
309
|
+
if (!STRING_METHODS.has(name)) refuse(`a text has no method "${name}" that rules may call`, line);
|
|
310
|
+
plainArgs(name, args, 0, args.length, line);
|
|
311
|
+
// A search for a long text inside a long text can compare every character of one at every place in the other.
|
|
312
|
+
const needle = typeof args[0] === 'string' && (name === 'indexOf' || name === 'includes' || name === 'split') ? args[0].length : 0;
|
|
313
|
+
charge(name === 'at' || name === 'charCodeAt' ? 1 : 1 + o.length + Math.floor((o.length * needle) / 16));
|
|
314
|
+
return (o as any)[name](...args);
|
|
315
|
+
}
|
|
316
|
+
if (o instanceof Map || o instanceof Set) {
|
|
317
|
+
if (!MAPSET_METHODS.has(name)) refuse(`a Map or Set has no method "${name}" that rules may call`, line);
|
|
318
|
+
// keys, values and entries hand back a list, paid for by its length: no iterator reaches rules, so nothing can read one out uncounted.
|
|
319
|
+
if (name === 'keys' || name === 'values' || name === 'entries') { charge(1 + o.size); return Array.from((o as any)[name]()); }
|
|
320
|
+
if (name === 'set' || name === 'add') { charge(1); sized(o.size + 1, line); } else if (name === 'forEach' || name === 'clear') charge(1 + o.size);
|
|
321
|
+
else charge(1);
|
|
322
|
+
// A long text as a key is compared, character by character, with the key it finds.
|
|
323
|
+
if (typeof args[0] === 'string' && args[0].length >= 64) charge(args[0].length >> 6);
|
|
324
|
+
return (o as any)[name](...args);
|
|
325
|
+
}
|
|
326
|
+
if (HOST.has(o) && Object.hasOwn(o, name) && typeof o[name] === 'function') return o[name](...args);
|
|
327
|
+
return refuse(`"${name}" is not a method rules may call on this value`, line);
|
|
328
|
+
}
|
|
329
|
+
/** An optional method call, `o?.name(...args)`. */
|
|
330
|
+
export function co(o: any, name: string, args: unknown[], line?: number): unknown {
|
|
331
|
+
return o === null || o === undefined ? undefined : c(o, name, args, line);
|
|
332
|
+
}
|
|
333
|
+
/** A method call through a computed key, `o[key](...args)`. */
|
|
334
|
+
export function cc(o: any, key: unknown, args: unknown[], line?: number): unknown {
|
|
335
|
+
if (typeof key !== 'string' || refusedName(key)) refuse('a method is called by a name that is a plain string', line);
|
|
336
|
+
return c(o, key as string, args, line);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** A spread, `...x`: a list, a text, a Map or a Set, charged a unit an entry. */
|
|
340
|
+
export function sp<T>(x: T, line?: number): T {
|
|
341
|
+
if (!(typeof x === 'string' || Array.isArray(x) || x instanceof Map || x instanceof Set)) {
|
|
342
|
+
// An object spread (`{ ...o }`) copies own keys; anything else has no business being spread.
|
|
343
|
+
if (x === null || typeof x !== 'object') refuse('only a list, a text, a Map, a Set or a plain object can be spread', line);
|
|
344
|
+
hostless(x, line);
|
|
345
|
+
const proto = Object.getPrototypeOf(x);
|
|
346
|
+
if (proto !== Object.prototype && proto !== null) refuse('only a list, a text, a Map, a Set or a plain object can be spread', line);
|
|
347
|
+
// Copying an object key by key is dear: `KEY` units a key, as for a constant's.
|
|
348
|
+
if (!prepaid(x)) charge(1 + KEY * Object.keys(x as object).length);
|
|
349
|
+
return x;
|
|
350
|
+
}
|
|
351
|
+
const n = lengthOf(x);
|
|
352
|
+
charge(1 + n); sized(n, line);
|
|
353
|
+
return x;
|
|
354
|
+
}
|
|
355
|
+
/** A spread into `Math.max(...x)` and the like: a list, charged a unit an entry, of numbers and texts only (see `p`). */
|
|
356
|
+
export function spp<T>(x: T, line?: number): T {
|
|
357
|
+
if (!Array.isArray(x)) refuse('only a list of numbers can be spread into a Math function', line);
|
|
358
|
+
const a = x as unknown[];
|
|
359
|
+
charge(1 + a.length); sized(a.length, line);
|
|
360
|
+
for (let i = 0; i < a.length; i += 1) if (!plainArg(a[i])) refuse('a list or an object was used where a number belongs', line);
|
|
361
|
+
return x;
|
|
362
|
+
}
|
|
363
|
+
/** A list literal that held a spread: its length is checked once it is made. */
|
|
364
|
+
export function lit<T extends unknown[]>(a: T, line?: number): T { sized(a.length, line); return a; }
|
|
365
|
+
/** `a + b`: a number as ever; a text is charged a unit a character and held to the size cap. */
|
|
366
|
+
export function add(a: any, b: any, line?: number): any {
|
|
367
|
+
p(a, line); p(b, line);
|
|
368
|
+
const out = a + b;
|
|
369
|
+
if (typeof out === 'string') { charge(out.length); sized(out.length, line); }
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
/** A template literal's text, charged and capped like `+`. */
|
|
373
|
+
export function tpl(text: string, line?: number): string { charge(text.length); sized(text.length, line); return text; }
|
|
374
|
+
/** `Object.keys`, `values` and `entries`, charged a unit a key. */
|
|
375
|
+
export function keys(o: object): string[] { const paid = prepaid(o); const out = Object.keys(o); if (!paid) charge(1 + out.length); return out; }
|
|
376
|
+
export function values(o: object): unknown[] { hostless(o); const paid = prepaid(o); const out = Object.values(o); if (!paid) charge(1 + 2 * out.length); return out; }
|
|
377
|
+
export function entries(o: object): [string, unknown][] { hostless(o); const paid = prepaid(o); const out = Object.entries(o); if (!paid) charge(1 + 8 * out.length); return out; }
|
|
378
|
+
/**
|
|
379
|
+
* How many keys a value has, when that is known without counting them: a list or a text by its length, and a
|
|
380
|
+
* module-level constant by the count taken when it was frozen at load.
|
|
381
|
+
*/
|
|
382
|
+
const KEYS = new WeakMap<object, number>();
|
|
383
|
+
export function keyCount(o: unknown): number | undefined {
|
|
384
|
+
if (typeof o === 'string' || Array.isArray(o)) return o.length;
|
|
385
|
+
return o !== null && typeof o === 'object' ? KEYS.get(o) : undefined;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Going through all of a value's keys, charged BEFORE it is done when the count is known (`KEY` units a key for a
|
|
389
|
+
* constant, whose keys cost the handler nothing to make). True when it was. A value whose count is not known was made
|
|
390
|
+
* by the running handler, which paid a unit and more for every key it has: that one is charged after.
|
|
391
|
+
*/
|
|
392
|
+
export function prepaid(o: unknown): boolean {
|
|
393
|
+
const n = keyCount(o);
|
|
394
|
+
if (n === undefined) return false;
|
|
395
|
+
charge(1 + (typeof o === 'string' || Array.isArray(o) ? n : KEY * n));
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
/** `for (const k in o)`: JavaScript lists every key before the first turn, so a constant's are paid for here. */
|
|
399
|
+
export function fi<T>(o: T, line?: number): T {
|
|
400
|
+
if (o !== null && typeof o === 'object') { hostless(o, line); prepaid(o); }
|
|
401
|
+
return o;
|
|
402
|
+
}
|
|
403
|
+
/** `world`, `ctx` and what hangs from them hold functions: their values are not taken out in a list or copied by a spread, so no host function is ever held as a value. */
|
|
404
|
+
function hostless(o: unknown, line?: number): void {
|
|
405
|
+
if (typeof o === 'object' && o !== null && HOST.has(o)) refuse('world and ctx are not lists of values: call what they offer by name', line);
|
|
406
|
+
}
|
|
407
|
+
/** `new Map(...)` and `new Set(...)`, inside a handler only (the build checks where). */
|
|
408
|
+
export function map(init?: Iterable<readonly [unknown, unknown]>): Map<unknown, unknown> { charge(1 + (init === undefined ? 0 : lengthOf(init))); const m = new Map(init); sized(m.size); return m; }
|
|
409
|
+
export function set(init?: Iterable<unknown>): Set<unknown> { charge(1 + (init === undefined ? 0 : lengthOf(init))); const m = new Set(init); sized(m.size); return m; }
|
|
410
|
+
|
|
411
|
+
/** A value frozen all the way down (module-level constants, event data, query results). Functions are left alone. */
|
|
412
|
+
export function deepFreeze<T>(v: T): T {
|
|
413
|
+
if (v === null || typeof v !== 'object' || Object.isFrozen(v)) return v;
|
|
414
|
+
const list = Object.keys(v as object);
|
|
415
|
+
// The count is kept (`keyCount`): a handler that goes through a constant's keys is charged for them before it starts.
|
|
416
|
+
if (!Array.isArray(v)) KEYS.set(v, list.length);
|
|
417
|
+
for (const key of list) deepFreeze((v as Record<string, unknown>)[key]);
|
|
418
|
+
return Object.freeze(v);
|
|
419
|
+
}
|