@nodecraft/rendermite 1.7.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/atlas/registry.d.ts +1 -1
  2. package/dist/atlas/registry.js +1 -1
  3. package/dist/atlas/sources.d.ts +1 -1
  4. package/dist/atlas/sources.js +1 -1
  5. package/dist/blocks/cli.js +1 -1
  6. package/dist/blocks/ender-storage.d.ts +1 -1
  7. package/dist/blocks/entities.d.ts +1 -1
  8. package/dist/blocks/entity-models.d.ts +3 -3
  9. package/dist/blocks/entity-models.js +1 -1
  10. package/dist/blocks/export.d.ts +1 -1
  11. package/dist/blocks/export.js +1 -1
  12. package/dist/blocks/geo-blocks.d.ts +1 -1
  13. package/dist/blocks/item-sprites.d.ts +1 -1
  14. package/dist/blocks/light-bytecode.d.ts +174 -0
  15. package/dist/blocks/light-bytecode.js +2 -0
  16. package/dist/blocks/light-eval.d.ts +2 -0
  17. package/dist/blocks/light-eval.js +2 -0
  18. package/dist/blocks/light.d.ts +38 -0
  19. package/dist/blocks/light.js +2 -0
  20. package/dist/blocks/pack-format.d.ts +2 -2
  21. package/dist/blocks/sprite-sheet.d.ts +1 -1
  22. package/dist/{classfile-Bo7FUrrD.js → classfile-CiIDF3vc.js} +23 -1
  23. package/dist/{classfile-BdLIk4Pe.d.ts → classfile-CsLXlh5z.d.ts} +23 -0
  24. package/dist/{cli-BuKutEzb.js → cli-Dw1Q5CPR.js} +5 -4
  25. package/dist/cli.js +5 -5
  26. package/dist/{descriptor-JyXhfd-c.js → descriptor-CmkIkDKL.js} +2 -2
  27. package/dist/entities/classfile.d.ts +1 -1
  28. package/dist/entities/classfile.js +1 -1
  29. package/dist/entities/inherit.d.ts +2 -2
  30. package/dist/entities/inherit.js +1 -1
  31. package/dist/entities/jar-classes.js +1 -1
  32. package/dist/entities/link.d.ts +2 -2
  33. package/dist/entities/link.js +1 -1
  34. package/dist/entities/mappings.d.ts +2 -2
  35. package/dist/entities/mappings.js +2 -2
  36. package/dist/entities/model-bytecode.d.ts +7 -2
  37. package/dist/entities/model-bytecode.js +2 -2
  38. package/dist/entities/model-code.d.ts +1 -1
  39. package/dist/entities/model-code.js +1 -1
  40. package/dist/{entity-models-DUKiHpn1.js → entity-models-BGiy3pey.js} +5 -5
  41. package/dist/{export-B8tPsf4K.d.ts → export-J13L9Lqu.d.ts} +3 -1
  42. package/dist/{export-CVXvPLv8.js → export-t8X_oRws.js} +23 -3
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +7 -7
  45. package/dist/{inherit-BYLi3Q28.js → inherit-BUn0Cve9.js} +2 -2
  46. package/dist/{jar-W2cV2ZVO.js → jar-CBYPMbw7.js} +1 -1
  47. package/dist/{jar-classes-3GVCJZT7.js → jar-classes-CSm7qq1R.js} +2 -2
  48. package/dist/light-CyRhYulM.js +582 -0
  49. package/dist/light-bytecode-Cpyuxo9b.js +1749 -0
  50. package/dist/light-eval-BsJJoSv4.js +23 -0
  51. package/dist/light-eval-D9x0ufZx.d.ts +13 -0
  52. package/dist/{link-5GmBLTzk.js → link-Cuhyrz07.js} +1 -1
  53. package/dist/{mappings-BEPVaiwE.js → mappings-Ep6ko0rg.js} +77 -9
  54. package/dist/{mappings-DxX4KoZl.d.ts → mappings-USwcjtcH.d.ts} +28 -2
  55. package/dist/{model-bytecode-B43uSqet.js → model-bytecode-VaLUQfGO.js} +2 -2
  56. package/dist/{model-code-BY44ao27.d.ts → model-code-Csc3bs3h.d.ts} +1 -1
  57. package/dist/{model-code-1Vuk-zj_.js → model-code-DbN2uVhp.js} +1 -1
  58. package/dist/mods/prepare.js +1 -1
  59. package/dist/mods/tints.js +1 -1
  60. package/dist/{pack-format-Baoc_aSZ.d.ts → pack-format-BqWf-DNc.d.ts} +23 -1
  61. package/dist/pack.d.ts +3 -2
  62. package/dist/pack.js +2 -1
  63. package/dist/{pipeline-Cci24Kb4.js → pipeline-CRTQGba7.js} +1 -1
  64. package/dist/{prepare-D49Gu4Lv.js → prepare-C61BcwFA.js} +2 -2
  65. package/dist/{prepare-Myd_ec4v.js → prepare-DxPEu9mo.js} +2 -2
  66. package/dist/{registry-DlduyjtM.js → registry-DafxsAhG.js} +1 -1
  67. package/dist/render/pipeline.js +1 -1
  68. package/dist/resources/descriptor.js +1 -1
  69. package/dist/resources/jar.js +1 -1
  70. package/dist/resources/zip-reader.js +1 -1
  71. package/dist/{run--oW_h01E.js → run-DYcem_Vv.js} +2 -2
  72. package/dist/run.js +1 -1
  73. package/dist/{sources-BlUriDCs.js → sources-DYY5Qu-t.js} +11 -2
  74. package/dist/{sources-DYYiyvRW.d.ts → sources-e5D09Jz_.d.ts} +7 -0
  75. package/dist/versions/prepare.js +1 -1
  76. package/dist/workers/render-worker.js +2 -2
  77. package/package.json +6 -6
  78. package/dist/{tints-BspVCz2G.js → tints-CWQz6UkM.js} +1 -1
  79. package/dist/{zip-reader-BSbJKUT1.js → zip-reader-zx3kdSq4.js} +1 -1
@@ -0,0 +1,1749 @@
1
+ import { r as parseDescriptor } from "./model-bytecode-VaLUQfGO.js";
2
+ //#region src/blocks/light-bytecode.ts
3
+ /**
4
+ * Block light emission, read out of the code that registers blocks.
5
+ *
6
+ * No jar states how bright a torch is. Emission is a property set in Java — `Properties.lightLevel(state
7
+ * -> 14)` in the registry class since 1.16, `lightValue(14)` before it — so the only automatic source
8
+ * is the bytecode, exactly as for entity geometry (see docs/entity-geometry.md). The interpreter here
9
+ * is its own rather than the geometry one because the questions differ: geometry follows a builder
10
+ * vocabulary and abstracts everything else, where this follows *values* — a string naming a block, a
11
+ * `Properties` object carrying its light, and a lambda from a state to a level — through whatever
12
+ * code carries them, and abstracts the rest.
13
+ *
14
+ * Three ideas do the work:
15
+ *
16
+ * - **Taint.** Every object the interpreter cannot see into remembers the strings that went into it.
17
+ * `ResourceLocation.withDefaultNamespace("torch")`, a 26.x `BlockItemId.create("torch")` and a mod's
18
+ * `new ResourceLocation(MODID, "glow")` all become a value that knows it is about `torch` or `glow`.
19
+ * - **Registration by shape.** A call whose arguments carry a name *and* a block or its `Properties`
20
+ * is a registration, whatever it is called and whichever class it is on. That covers vanilla's
21
+ * `register(String, Block)`, `register(String, Function, Properties)`, `Registry.register(Registry,
22
+ * ResourceLocation, T)`, and a mod's `DeferredRegister.register(String, Supplier)`, where the
23
+ * supplier is run to find the block.
24
+ * - **Concrete evaluation.** A light *function* is run once per combination of the blockstate
25
+ * properties it reads, with real values, so `lit ? 3 * candles : 0` comes out as the eight cases it
26
+ * is rather than as a guess.
27
+ *
28
+ * Whatever this cannot follow is recorded, never guessed: a block whose registration was not found, or
29
+ * whose light function reads something the interpreter does not model, is a failure entry, not a
30
+ * dark block.
31
+ */
32
+ /** Whether a call is to one of the block's light methods. */
33
+ function matchesGetter(getter, name, descriptor) {
34
+ if (getter.name !== name) return false;
35
+ if (getter.descriptor !== "") return getter.descriptor === descriptor;
36
+ const shape = parseDescriptor(descriptor);
37
+ return shape.args.length === 3 && shape.returns === "I";
38
+ }
39
+ /** The light method a block class declares or inherits, resolved to a real descriptor. */
40
+ function findGetter(source, owner, getter) {
41
+ if (getter.descriptor === "") {
42
+ const found = findMethodNamed(source, owner, getter.name, 3, "I");
43
+ return found ? {
44
+ classFile: found.classFile,
45
+ descriptor: found.descriptor
46
+ } : null;
47
+ }
48
+ const found = findMethod(source, owner, getter.name, getter.descriptor);
49
+ return found ? {
50
+ classFile: found.classFile,
51
+ descriptor: getter.descriptor
52
+ } : null;
53
+ }
54
+ const UNKNOWN = { kind: "unknown" };
55
+ const TO_INT_FUNCTION = "Ljava/util/function/ToIntFunction;";
56
+ /** JDK collections a mod fills with the names it registers, modelled as their elements. */
57
+ const LIST_CLASSES = /^java\/util\/(?:ArrayList|LinkedList|HashSet|LinkedHashSet|TreeSet|ArrayDeque|Vector|Stack)$/;
58
+ /** Instructions one root method may spend, every frame it inlines included. */
59
+ const STEP_BUDGET = 4e6;
60
+ /** Instructions one `<clinit>` may spend. Kept apart so a registry's own initialiser cannot starve the root. */
61
+ const CLINIT_BUDGET = 4e5;
62
+ const CLINIT_FRAMES = 8e3;
63
+ /** Instructions a constructor followed for its `super(...)` may spend. */
64
+ const CTOR_BUDGET = 4e4;
65
+ /** Frames one root may open. `Blocks.<clinit>` inlines a few calls per block, for a thousand blocks. */
66
+ const FRAME_BUDGET = 6e4;
67
+ const MAX_DEPTH = 14;
68
+ const MAX_ARRAY = 4096;
69
+ /** Times round a backward jump before a frame gives up on it. */
70
+ const MAX_ITERATIONS = 512;
71
+ const MAX_METHOD_HOPS = 8;
72
+ const STATIC = 8;
73
+ function isTracked(value) {
74
+ return [
75
+ "object",
76
+ "lambda",
77
+ "state"
78
+ ].includes(value.kind);
79
+ }
80
+ /** Whether a string could be a registry name: a path, or `namespace:path`. */
81
+ function looksLikeId(text) {
82
+ return /^[a-z0-9_.\-/]+(?::[a-z0-9_.\-/]+)?$/.test(text) && text.length < 128;
83
+ }
84
+ const LOAD_CONSTANT = /* @__PURE__ */ new Set([
85
+ 18,
86
+ 19,
87
+ 20
88
+ ]);
89
+ const RETURNS = /* @__PURE__ */ new Set([
90
+ 172,
91
+ 173,
92
+ 174,
93
+ 175,
94
+ 176,
95
+ 177
96
+ ]);
97
+ const ARRAY_LOADS = /* @__PURE__ */ new Set([
98
+ 46,
99
+ 47,
100
+ 48,
101
+ 49,
102
+ 50,
103
+ 51,
104
+ 52,
105
+ 53
106
+ ]);
107
+ const ARRAY_STORES = /* @__PURE__ */ new Set([
108
+ 79,
109
+ 80,
110
+ 81,
111
+ 82,
112
+ 83,
113
+ 84,
114
+ 85,
115
+ 86
116
+ ]);
117
+ const INVOKES = /* @__PURE__ */ new Set([
118
+ 182,
119
+ 183,
120
+ 184,
121
+ 185
122
+ ]);
123
+ /** The wide (long/double) opcodes among the constants, loads and arithmetic, for stack bookkeeping. */
124
+ const WIDE_CONSTANTS = /* @__PURE__ */ new Set([
125
+ 9,
126
+ 10,
127
+ 14,
128
+ 15,
129
+ 20
130
+ ]);
131
+ /** Java `int` arithmetic: truncate, then wrap to 32 bits. */
132
+ function toInt(value) {
133
+ return Number.isFinite(value) ? Number(BigInt.asIntN(32, BigInt(Math.trunc(value)))) : 0;
134
+ }
135
+ function compare(op, difference) {
136
+ switch (op) {
137
+ case 153:
138
+ case 159: return difference === 0;
139
+ case 154:
140
+ case 160: return difference !== 0;
141
+ case 155:
142
+ case 161: return difference < 0;
143
+ case 156:
144
+ case 162: return difference >= 0;
145
+ case 157:
146
+ case 163: return difference > 0;
147
+ default: return difference <= 0;
148
+ }
149
+ }
150
+ /** The method a call names, where the JVM would look: the owner, then its ancestors. */
151
+ function findMethod(source, owner, name, descriptor) {
152
+ let at = owner;
153
+ for (let hops = 0; at && hops < MAX_METHOD_HOPS; hops++) {
154
+ const classFile = source.resolve(at);
155
+ if (!classFile) return null;
156
+ const method = classFile.methods.find((candidate) => candidate.name === name && candidate.descriptor === descriptor && candidate.code);
157
+ if (method?.code) return {
158
+ code: method.code,
159
+ classFile,
160
+ accessFlags: method.accessFlags
161
+ };
162
+ at = classFile.superClass ? source.remapClass(classFile.superClass) : null;
163
+ }
164
+ return null;
165
+ }
166
+ /** A method by name alone, for a loader extension a mod overrides; the descriptor is the loader's. */
167
+ function findMethodNamed(source, owner, name, argCount, returns) {
168
+ let at = owner;
169
+ for (let hops = 0; at && hops < MAX_METHOD_HOPS; hops++) {
170
+ const classFile = source.resolve(at);
171
+ if (!classFile) return null;
172
+ const method = classFile.methods.find((candidate) => {
173
+ if (candidate.name !== name || !candidate.code) return false;
174
+ const shape = parseDescriptor(candidate.descriptor);
175
+ return shape.args.length === argCount && shape.returns === returns;
176
+ });
177
+ if (method?.code) return {
178
+ code: method.code,
179
+ classFile,
180
+ descriptor: method.descriptor
181
+ };
182
+ at = classFile.superClass ? source.remapClass(classFile.superClass) : null;
183
+ }
184
+ return null;
185
+ }
186
+ var Interpreter = class Interpreter {
187
+ stack = [];
188
+ locals = /* @__PURE__ */ new Map();
189
+ loops = /* @__PURE__ */ new Map();
190
+ /**
191
+ * Whether a conditional since the last backward jump could not be decided. A loop whose head this
192
+ * cannot evaluate — `for (Block block : registry)` over an iterator it never saw — would otherwise
193
+ * fall through its exit test every time round and spin to the iteration cap, and two of them nested
194
+ * is the whole step budget.
195
+ */
196
+ undecided = false;
197
+ returned = null;
198
+ /** The class whose code this frame runs, for saying who made a registration. */
199
+ caller = null;
200
+ shared;
201
+ depth;
202
+ constructor(shared, depth = 0) {
203
+ this.shared = shared;
204
+ this.depth = depth;
205
+ }
206
+ handle(type) {
207
+ return {
208
+ id: this.shared.nextHandle++,
209
+ type,
210
+ strings: [],
211
+ numbers: [],
212
+ classes: [],
213
+ fields: /* @__PURE__ */ new Map(),
214
+ isProps: type !== null && this.isPropsType(type),
215
+ light: null,
216
+ props: null,
217
+ ctorArgs: null
218
+ };
219
+ }
220
+ isPropsType(name) {
221
+ const { propsClasses, remapClass } = this.shared.source;
222
+ return propsClasses.has(name) || propsClasses.has(remapClass(name));
223
+ }
224
+ note(message) {
225
+ if (!this.shared.notes.includes(message)) this.shared.notes.push(message);
226
+ }
227
+ pop() {
228
+ return this.stack.pop() ?? UNKNOWN;
229
+ }
230
+ push(value) {
231
+ if (value) this.stack.push(value);
232
+ }
233
+ number(value) {
234
+ return value.kind === "number" ? value.value : 0;
235
+ }
236
+ isWide(value) {
237
+ return value.kind === "number" && value.wide === true;
238
+ }
239
+ /** Puts receiver and arguments where the callee reads them; longs and doubles take two slots. */
240
+ seed(receiver, descriptor, args) {
241
+ let slot = 0;
242
+ if (receiver) this.locals.set(slot++, receiver);
243
+ for (const [index, shape] of parseDescriptor(descriptor).args.entries()) {
244
+ this.locals.set(slot, args[index] ?? UNKNOWN);
245
+ slot += shape === "J" || shape === "D" ? 2 : 1;
246
+ }
247
+ }
248
+ /** Copies the strings, numbers and classes that flowed through the arguments onto a result. */
249
+ taint(target, values) {
250
+ for (const value of values) {
251
+ if (target.strings.length > 64) break;
252
+ if (value.kind === "string") target.strings.push(value.value);
253
+ else if (value.kind === "number") target.numbers.push(value.value);
254
+ else if (value.kind === "class") target.classes.push(value.name);
255
+ else if (value.kind === "object" && value.handle !== target) {
256
+ target.strings.push(...value.handle.strings);
257
+ target.numbers.push(...value.handle.numbers);
258
+ target.classes.push(...value.handle.classes);
259
+ if (value.handle.builder) target.strings.push(value.handle.builder.join(""));
260
+ }
261
+ }
262
+ }
263
+ /** What an opaque call leaves behind, by its return type: nothing, a number, or a tainted object. */
264
+ opaqueResult(returns, inputs) {
265
+ if (returns === "V") return null;
266
+ if (returns.startsWith("L") || returns.startsWith("[")) {
267
+ if (returns === "Ljava/lang/String;") {
268
+ const strings = inputs.flatMap((value) => value.kind === "string" ? [value.value] : value.kind === "object" ? value.handle.strings : []);
269
+ return strings.length > 0 ? {
270
+ kind: "string",
271
+ value: strings.at(-1)
272
+ } : UNKNOWN;
273
+ }
274
+ const created = this.handle(returns.startsWith("L") ? returns.slice(1, -1) : null);
275
+ created.origin = `opaque ${returns} from ${inputs.map((value) => value.kind).join(",")}`;
276
+ this.taint(created, inputs);
277
+ if (/(?:List|Set|Collection|Iterable);$/.test(returns)) {
278
+ const from = inputs.find((value) => value.kind === "array" || value.kind === "object" && value.handle.items);
279
+ created.items = from?.kind === "array" ? [...from.items] : from?.kind === "object" ? [...from.handle.items ?? []] : inputs.filter((value) => value.kind !== "unknown");
280
+ }
281
+ if (returns.endsWith("Map;")) {
282
+ const source = inputs.find((value) => value.kind === "object" && value.handle.map);
283
+ if (source?.kind === "object" && source.handle.map) created.map = [...source.handle.map];
284
+ else if (inputs.length > 0 && inputs.length % 2 === 0 && inputs.every((value) => value.kind !== "unknown")) {
285
+ created.map = [];
286
+ for (let index = 0; index < inputs.length; index += 2) created.map.push({
287
+ key: inputs[index],
288
+ value: inputs[index + 1]
289
+ });
290
+ }
291
+ }
292
+ if (created.isProps) {
293
+ const sources = inputs.filter((value) => value.kind === "object" && (value.handle.isProps || value.handle.props));
294
+ if (sources.length > 0) {
295
+ const from = sources[0].handle;
296
+ created.light = from.isProps ? from.light : from.props?.light ?? null;
297
+ } else if (inputs.some((value) => value.kind === "unknown" || value.kind === "null")) created.light = {
298
+ kind: "opaque",
299
+ reason: "properties copied from a block the interpreter could not identify"
300
+ };
301
+ }
302
+ return {
303
+ kind: "object",
304
+ handle: created
305
+ };
306
+ }
307
+ return UNKNOWN;
308
+ }
309
+ /**
310
+ * Runs a class's `<clinit>` once, on its own budget and at its own depth.
311
+ *
312
+ * A registry class's initialiser is the point of the whole exercise, and a `getstatic` from it
313
+ * reaches sound types, map colours and — through `BuiltInRegistries` — the entire registry system.
314
+ * Each gets its own allowance so a sprawling one cannot spend what the block table needs.
315
+ *
316
+ * Its own depth, because an initialiser runs once and its results stand for the rest of the run:
317
+ * `Direction`'s was first reached fourteen frames down inside a codec builder, its `$values()` could
318
+ * not be followed from there, and every later read of `Direction.values()` was an opaque object.
319
+ */
320
+ initialise(owner) {
321
+ if (this.shared.initialised.has(owner)) return;
322
+ this.shared.initialised.add(owner);
323
+ const target = findMethod(this.shared.source, owner, "<clinit>", "()V");
324
+ if (!target) return;
325
+ const saved = {
326
+ steps: this.shared.steps,
327
+ frames: this.shared.frames,
328
+ probe: this.shared.probe
329
+ };
330
+ this.shared.steps = CLINIT_BUDGET;
331
+ this.shared.frames = CLINIT_FRAMES;
332
+ this.shared.probe = null;
333
+ try {
334
+ new Interpreter(this.shared, 0).run(target.code, target.classFile);
335
+ } finally {
336
+ if (this.depth === 0) {
337
+ const key = `${owner}.<clinit>`;
338
+ this.shared.frameCounts.set(key, (this.shared.frameCounts.get(key) ?? 0) + (CLINIT_BUDGET - this.shared.steps));
339
+ }
340
+ this.shared.steps = saved.steps;
341
+ this.shared.frames = saved.frames;
342
+ this.shared.probe = saved.probe;
343
+ }
344
+ }
345
+ inline(owner, name, descriptor, receiver, args) {
346
+ if (this.depth >= MAX_DEPTH || this.shared.frames <= 0) return { ok: false };
347
+ const target = findMethod(this.shared.source, owner, name, descriptor);
348
+ if (!target) return { ok: false };
349
+ this.shared.frames--;
350
+ const frame = new Interpreter(this.shared, this.depth + 1);
351
+ frame.seed((target.accessFlags & STATIC) === 0 ? receiver : null, descriptor, args);
352
+ const before = this.shared.steps;
353
+ frame.run(target.code, target.classFile);
354
+ if (this.depth === 0) {
355
+ const key = `${owner}.${name}${descriptor}`;
356
+ this.shared.frameCounts.set(key, (this.shared.frameCounts.get(key) ?? 0) + (before - this.shared.steps));
357
+ }
358
+ return {
359
+ ok: true,
360
+ value: frame.returned
361
+ };
362
+ }
363
+ /** Applies a constructor to a fresh object: records what went in, and follows it for `super(...)`. */
364
+ construct(created, reference, args) {
365
+ created.ctorArgs = args;
366
+ this.taint(created, args);
367
+ for (const arg of args) if (arg.kind === "object" && arg.handle.isProps) created.props = arg.handle;
368
+ if (created.type === "java/lang/StringBuilder") {
369
+ created.builder = args.flatMap((arg) => arg.kind === "string" || arg.kind === "number" ? [arg.value] : []);
370
+ return;
371
+ }
372
+ if (created.type !== null && LIST_CLASSES.test(created.type)) {
373
+ const from = args.find((arg) => arg.kind === "array" || arg.kind === "object" && arg.handle.items);
374
+ created.items = from?.kind === "array" ? [...from.items] : from?.kind === "object" ? [...from.handle.items ?? []] : [];
375
+ return;
376
+ }
377
+ if (created.props) return;
378
+ const saved = this.shared.steps;
379
+ this.shared.steps = Math.min(saved, CTOR_BUDGET);
380
+ try {
381
+ this.inline(reference.owner, reference.name, reference.descriptor, {
382
+ kind: "object",
383
+ handle: created
384
+ }, args);
385
+ } finally {
386
+ this.shared.steps = saved - Math.max(0, Math.min(saved, CTOR_BUDGET) - this.shared.steps);
387
+ }
388
+ }
389
+ /**
390
+ * Runs a lambda with the values it captured followed by the arguments it is called with.
391
+ *
392
+ * A method reference to a constructor — `TorchBlock::new` — is a lambda too, and calling it
393
+ * constructs: which is how a 1.21.2+ factory hands the interpreter the block class.
394
+ */
395
+ invokeLambda(lambda, args) {
396
+ const reference = this.shared.source.remap({
397
+ owner: lambda.owner,
398
+ name: lambda.name,
399
+ descriptor: lambda.descriptor
400
+ }, false);
401
+ const all = [...lambda.captured, ...args];
402
+ if (reference.name === "<init>") {
403
+ const created = this.handle(reference.owner);
404
+ this.construct(created, reference, all);
405
+ return {
406
+ kind: "object",
407
+ handle: created
408
+ };
409
+ }
410
+ const target = findMethod(this.shared.source, reference.owner, reference.name, reference.descriptor);
411
+ if (!target) return null;
412
+ const [receiver, ...rest] = all;
413
+ const result = (target.accessFlags & STATIC) === 0 ? this.inline(reference.owner, reference.name, reference.descriptor, receiver ?? UNKNOWN, rest) : this.inline(reference.owner, reference.name, reference.descriptor, null, all);
414
+ return result.ok ? result.value : null;
415
+ }
416
+ /**
417
+ * A call whose arguments carry a name and a block (or its properties) is a registration, whatever
418
+ * it is called. Recorded first-come: a helper that registers on the caller's behalf fires first,
419
+ * from inside, and the outer call then agrees with it.
420
+ */
421
+ /** Whether a handle is an enum constant, whose name-like strings (`"white"`) name nothing registered. */
422
+ isEnumConstant(handle) {
423
+ if (!handle.type || !handle.ctorArgs) return false;
424
+ const parent = this.shared.source.resolve(handle.type)?.superClass ?? null;
425
+ return parent === "java/lang/Enum" || parent !== null && this.shared.source.resolve(parent)?.superClass === "java/lang/Enum";
426
+ }
427
+ detectRegistration(args, site) {
428
+ const names = [];
429
+ for (const arg of args) if (arg.kind === "string") names.push(arg.value);
430
+ else if (arg.kind === "object" && !arg.handle.props && !arg.handle.isProps && !this.isEnumConstant(arg.handle)) {
431
+ names.push(...arg.handle.strings);
432
+ if (arg.handle.builder) names.push(arg.handle.builder.join(""));
433
+ }
434
+ const ids = names.filter((name) => looksLikeId(name));
435
+ if (ids.length === 0) return null;
436
+ const known = ids.map((name) => this.shared.registrations.get(name)).find((entry) => entry !== void 0);
437
+ if (known && ids.every((name) => this.shared.registrations.has(name))) return known.block ?? this.blockFor(known);
438
+ let props = null;
439
+ let block = null;
440
+ let blockType = null;
441
+ for (const arg of args) if (arg.kind === "object" && arg.handle.isProps) props ??= arg.handle;
442
+ else if (arg.kind === "object" && arg.handle.props) block ??= arg.handle;
443
+ else if (arg.kind === "lambda" && arg.name === "<init>") blockType ??= this.shared.source.remapClass(arg.owner);
444
+ if (!props && !block) for (const arg of args) {
445
+ if (arg.kind !== "lambda") continue;
446
+ const made = this.invokeLambda(arg, []);
447
+ if (made?.kind === "object" && made.handle.props) {
448
+ block = made.handle;
449
+ break;
450
+ }
451
+ }
452
+ if (!props && !block) return null;
453
+ const registration = {
454
+ props: props ?? block?.props ?? null,
455
+ block,
456
+ blockType: blockType ?? block?.type ?? null,
457
+ site,
458
+ vanilla: this.caller !== null && this.shared.source.isVanillaClass(this.caller)
459
+ };
460
+ for (const name of ids) if (!this.shared.registrations.has(name)) this.shared.registrations.set(name, registration);
461
+ return block ?? this.blockFor(registration);
462
+ }
463
+ /**
464
+ * The block a registration stands for, when the call was handed properties and a factory rather than
465
+ * a built block. What `Blocks.TORCH` then holds has to carry the properties, or a later
466
+ * `ofFullCopy(TORCH)` copies nothing.
467
+ */
468
+ blockFor(registration) {
469
+ if (registration.block) return registration.block;
470
+ const created = this.handle(registration.blockType);
471
+ created.props = registration.props;
472
+ created.ctorArgs = [];
473
+ registration.block = created;
474
+ return created;
475
+ }
476
+ /** The property a `getValue` names, and the values it can take, from what flowed into its handle. */
477
+ domainOf(property) {
478
+ const name = property.strings.find((text) => /^[a-z][a-z0-9_]*$/.test(text));
479
+ if (!name) {
480
+ this.note(`property ${property.type ?? "?"} carries no name: strings ${JSON.stringify(property.strings)} origin ${property.origin ?? (property.ctorArgs ? `new with ${property.ctorArgs.map((arg) => arg.kind).join(",")}` : "new, never constructed")} id ${property.id}`);
481
+ return null;
482
+ }
483
+ const probe = this.shared.probe;
484
+ const known = probe.domains.get(name);
485
+ if (known) return known;
486
+ const valueClass = property.classes.find((name) => name !== "java/lang/Boolean" && name !== "java/lang/Integer");
487
+ let domain;
488
+ if (valueClass) {
489
+ const constants = this.enumConstants(this.shared.source.remapClass(valueClass));
490
+ if (constants.size === 0) {
491
+ this.note(`enum ${valueClass} for property ${name} yielded no constants`);
492
+ return null;
493
+ }
494
+ domain = {
495
+ name,
496
+ values: constants.keys().toArray(),
497
+ constants
498
+ };
499
+ } else if (property.numbers.length >= 2 && !property.classes.includes("java/lang/Boolean")) {
500
+ const [min, max] = property.numbers;
501
+ if (max - min > 64) return null;
502
+ const values = [];
503
+ for (let value = min; value <= max; value++) values.push(String(value));
504
+ domain = {
505
+ name,
506
+ values
507
+ };
508
+ } else domain = {
509
+ name,
510
+ values: ["false", "true"]
511
+ };
512
+ probe.domains.set(name, domain);
513
+ return domain;
514
+ }
515
+ /**
516
+ * An enum's constants by serialized name, in ordinal order.
517
+ *
518
+ * Each is an object its `<clinit>` constructed with `(name, ordinal, ...)`, and the name a blockstate
519
+ * writes is a lower-case constructor argument after those — or the constant's own name lowered where
520
+ * there is none.
521
+ */
522
+ enumConstants(owner) {
523
+ this.initialise(owner);
524
+ const constants = [];
525
+ for (const [key, value] of this.shared.statics) {
526
+ if (!key.startsWith(`${owner}.`) || value.kind !== "object" || !value.handle.ctorArgs) continue;
527
+ const type = value.handle.type;
528
+ if (type === owner || type !== null && this.shared.source.resolve(type)?.superClass === owner) constants.push(value.handle);
529
+ }
530
+ constants.sort((left, right) => this.number(left.ctorArgs?.[1] ?? UNKNOWN) - this.number(right.ctorArgs?.[1] ?? UNKNOWN));
531
+ const byName = /* @__PURE__ */ new Map();
532
+ for (const constant of constants) {
533
+ const args = constant.ctorArgs ?? [];
534
+ const serialized = args.slice(2).find((arg) => arg.kind === "string" && /^[a-z][a-z0-9_]*$/.test(arg.value));
535
+ const name = serialized?.kind === "string" ? serialized.value : args[0]?.kind === "string" ? args[0].value.toLowerCase() : `#${byName.size}`;
536
+ byName.set(name, constant);
537
+ }
538
+ return byName;
539
+ }
540
+ /** `state.getValue(PROPERTY)` against the probe: the assigned value, or the first if none yet. */
541
+ readProperty(probe, property) {
542
+ const domain = this.domainOf(property);
543
+ if (!domain) {
544
+ probe.failure = `reads a property the interpreter cannot resolve (${property.type ?? "unknown class"})`;
545
+ return UNKNOWN;
546
+ }
547
+ probe.reads.add(domain.name);
548
+ const value = probe.values.get(domain.name) ?? domain.values[0];
549
+ if (domain.constants) {
550
+ const constant = domain.constants.get(value);
551
+ return constant ? {
552
+ kind: "object",
553
+ handle: constant
554
+ } : UNKNOWN;
555
+ }
556
+ if (value === "true" || value === "false") return {
557
+ kind: "number",
558
+ value: value === "true" ? 1 : 0
559
+ };
560
+ return {
561
+ kind: "number",
562
+ value: Number(value)
563
+ };
564
+ }
565
+ /** The handful of JDK calls a light function or a registry can lean on. */
566
+ jdkCall(reference, receiver, args) {
567
+ const { owner, name } = reference;
568
+ const numbers = args.map((arg) => this.number(arg));
569
+ if (owner === "java/lang/Math") switch (name) {
570
+ case "max": return {
571
+ kind: "number",
572
+ value: Math.max(...numbers)
573
+ };
574
+ case "min": return {
575
+ kind: "number",
576
+ value: Math.min(...numbers)
577
+ };
578
+ case "abs": return {
579
+ kind: "number",
580
+ value: Math.abs(numbers[0])
581
+ };
582
+ case "floor": return {
583
+ kind: "number",
584
+ value: Math.floor(numbers[0]),
585
+ wide: true
586
+ };
587
+ case "ceil": return {
588
+ kind: "number",
589
+ value: Math.ceil(numbers[0]),
590
+ wide: true
591
+ };
592
+ case "round": return {
593
+ kind: "number",
594
+ value: Math.round(numbers[0])
595
+ };
596
+ default: return;
597
+ }
598
+ if (/^java\/lang\/(?:Integer|Boolean|Float|Double|Long|Short|Byte|Character)$/.test(owner)) {
599
+ if (name === "valueOf" && args.length === 1) return args[0];
600
+ if (receiver && name.endsWith("Value")) return receiver;
601
+ }
602
+ if (receiver?.kind === "object" && receiver.handle.builder) {
603
+ if (name === "append") {
604
+ const arg = args[0];
605
+ if (arg?.kind === "string" || arg?.kind === "number") receiver.handle.builder.push(arg.value);
606
+ else if (arg?.kind === "object") receiver.handle.builder.push(...arg.handle.strings.slice(-1));
607
+ return receiver;
608
+ }
609
+ if (name === "toString") return {
610
+ kind: "string",
611
+ value: receiver.handle.builder.join("")
612
+ };
613
+ }
614
+ if (receiver?.kind === "string" && reference.descriptor.endsWith("Ljava/lang/String;")) {
615
+ if ((name === "replace" || name === "replaceAll") && args.length === 2 && args[0].kind === "string" && args[1].kind === "string") {
616
+ const replacement = args[1].value;
617
+ return {
618
+ kind: "string",
619
+ value: receiver.value.replaceAll(args[0].value, () => replacement)
620
+ };
621
+ }
622
+ if (name === "concat" && args[0]?.kind === "string") return {
623
+ kind: "string",
624
+ value: receiver.value + args[0].value
625
+ };
626
+ return receiver;
627
+ }
628
+ if (owner === "java/lang/String" && name === "format" && args[0]?.kind === "string") {
629
+ const operands = args[1]?.kind === "array" ? args[1].items : args.slice(1);
630
+ let index = 0;
631
+ let known = true;
632
+ const text = args[0].value.replaceAll(/%[sd]/g, () => {
633
+ const value = operands[index++];
634
+ if (value?.kind === "string" || value?.kind === "number") return String(value.value);
635
+ known = false;
636
+ return "";
637
+ });
638
+ return known ? {
639
+ kind: "string",
640
+ value: text
641
+ } : UNKNOWN;
642
+ }
643
+ if (receiver?.kind === "array" && name === "clone") return {
644
+ kind: "array",
645
+ items: [...receiver.items]
646
+ };
647
+ if (owner === "java/util/Arrays" && (name === "stream" || name === "asList") || (owner === "java/util/List" || owner === "java/util/Set") && name === "of") return args.length === 1 && args[0].kind === "array" ? args[0] : {
648
+ kind: "array",
649
+ items: args
650
+ };
651
+ if (receiver?.kind === "object" && receiver.handle.iterator) {
652
+ const cursor = receiver.handle.iterator;
653
+ if (name === "hasNext") return {
654
+ kind: "number",
655
+ value: cursor.at < cursor.items.length ? 1 : 0
656
+ };
657
+ if (name === "next") return cursor.items[cursor.at++] ?? UNKNOWN;
658
+ }
659
+ if (receiver?.kind === "object" && receiver.handle.items && !receiver.handle.isProps) {
660
+ const items = receiver.handle.items;
661
+ if (name === "add" && args.length === 1) {
662
+ items.push(args[0]);
663
+ return {
664
+ kind: "number",
665
+ value: 1
666
+ };
667
+ }
668
+ if (name === "addAll" && args.length === 1) {
669
+ const more = args[0].kind === "array" ? args[0].items : args[0].kind === "object" ? args[0].handle.items ?? [] : [];
670
+ items.push(...more);
671
+ return {
672
+ kind: "number",
673
+ value: 1
674
+ };
675
+ }
676
+ if (name === "get" && args.length === 1 && args[0].kind === "number") return items[args[0].value] ?? UNKNOWN;
677
+ if (name === "size" || name === "isEmpty") return {
678
+ kind: "number",
679
+ value: name === "size" ? items.length : items.length === 0 ? 1 : 0
680
+ };
681
+ if (name === "iterator") {
682
+ const created = this.handle("java/util/Iterator");
683
+ created.iterator = {
684
+ items,
685
+ at: 0
686
+ };
687
+ return {
688
+ kind: "object",
689
+ handle: created
690
+ };
691
+ }
692
+ if ([
693
+ "stream",
694
+ "toArray",
695
+ "toList",
696
+ "values",
697
+ "keySet"
698
+ ].includes(name)) return {
699
+ kind: "array",
700
+ items: [...items]
701
+ };
702
+ if (name === "forEach" && args[0]?.kind === "lambda") {
703
+ for (const item of items) this.invokeLambda(args[0], [item]);
704
+ return null;
705
+ }
706
+ }
707
+ if (receiver?.kind === "array") {
708
+ if ([
709
+ "stream",
710
+ "toList",
711
+ "collect",
712
+ "sorted",
713
+ "distinct"
714
+ ].includes(name)) return receiver;
715
+ if (name === "iterator") {
716
+ const created = this.handle("java/util/Iterator");
717
+ created.iterator = {
718
+ items: receiver.items,
719
+ at: 0
720
+ };
721
+ return {
722
+ kind: "object",
723
+ handle: created
724
+ };
725
+ }
726
+ if (name === "forEach" && args[0]?.kind === "lambda") {
727
+ for (const item of receiver.items) this.invokeLambda(args[0], [item]);
728
+ return null;
729
+ }
730
+ if (name === "map" && args[0]?.kind === "lambda") return {
731
+ kind: "array",
732
+ items: receiver.items.map((item) => this.invokeLambda(args[0], [item]) ?? UNKNOWN)
733
+ };
734
+ if (name === "filter" && args[0]?.kind === "lambda") return {
735
+ kind: "array",
736
+ items: receiver.items.filter((item) => {
737
+ const verdict = this.invokeLambda(args[0], [item]);
738
+ return verdict?.kind === "number" ? verdict.value !== 0 : true;
739
+ })
740
+ };
741
+ if ([
742
+ "anyMatch",
743
+ "allMatch",
744
+ "noneMatch"
745
+ ].includes(name) && args[0]?.kind === "lambda") {
746
+ const predicate = args[0];
747
+ const results = receiver.items.map((item) => this.invokeLambda(predicate, [item]));
748
+ if (results.some((result) => result?.kind !== "number")) return UNKNOWN;
749
+ const hits = results.filter((result) => result?.kind === "number" && result.value !== 0).length;
750
+ let value = hits === 0;
751
+ if (name === "anyMatch") value = hits > 0;
752
+ else if (name === "allMatch") value = hits === receiver.items.length;
753
+ return {
754
+ kind: "number",
755
+ value: value ? 1 : 0
756
+ };
757
+ }
758
+ if (name === "count" || name === "size") return {
759
+ kind: "number",
760
+ value: receiver.items.length,
761
+ wide: name === "count"
762
+ };
763
+ }
764
+ }
765
+ /**
766
+ * A map the code fills and reads back, kept as its entries.
767
+ *
768
+ * `MultifaceBlock` decides whether glow lichen has any face through `PROPERTY_BY_DIRECTION.get(d)`,
769
+ * a map its initialiser filled from `Direction` to property. Keys compare as the JVM would: an enum
770
+ * constant is the same object, a string or number the same value.
771
+ */
772
+ mapCall(target, name, args) {
773
+ const same = (left, right) => left.kind === "object" && right.kind === "object" ? left.handle === right.handle : (left.kind === "string" || left.kind === "number") && left.kind === right.kind && left.value === right.value;
774
+ target.map ??= [];
775
+ if (name === "put" && args.length === 2) {
776
+ const existing = target.map.find((entry) => same(entry.key, args[0]));
777
+ if (existing) existing.value = args[1];
778
+ else target.map.push({
779
+ key: args[0],
780
+ value: args[1]
781
+ });
782
+ return UNKNOWN;
783
+ }
784
+ if ((name === "get" || name === "getOrDefault") && args.length > 0) return target.map.find((entry) => same(entry.key, args[0]))?.value ?? args[1] ?? UNKNOWN;
785
+ if (name === "containsKey" && args.length === 1) return {
786
+ kind: "number",
787
+ value: target.map.some((entry) => same(entry.key, args[0])) ? 1 : 0
788
+ };
789
+ }
790
+ call(reference, hasReceiver, special = false) {
791
+ const remapped = this.shared.source.remap(reference, false);
792
+ const { args: shapes, returns } = parseDescriptor(remapped.descriptor);
793
+ const args = [];
794
+ for (let index = 0; index < shapes.length; index++) args.unshift(this.pop());
795
+ const receiver = hasReceiver ? this.pop() : null;
796
+ const { source } = this.shared;
797
+ if (receiver?.kind === "lambda") {
798
+ const result = this.invokeLambda(receiver, args);
799
+ this.push(returns === "V" ? null : result ?? UNKNOWN);
800
+ return;
801
+ }
802
+ if (receiver?.kind === "state") {
803
+ const probe = receiver.probe;
804
+ if ((args.length === 1 || args.length === 2) && args[0].kind === "object" && !args[0].handle.isProps) {
805
+ if (returns === "Z") {
806
+ this.push({
807
+ kind: "number",
808
+ value: 1
809
+ });
810
+ return;
811
+ }
812
+ this.push(this.readProperty(probe, args[0].handle));
813
+ return;
814
+ }
815
+ probe.failure ??= `calls ${remapped.owner}.${remapped.name}${remapped.descriptor} on the state with (${args.map((arg) => arg.kind === "object" ? `object ${arg.handle.type ?? "?"}` : arg.kind).join(", ")}), which the interpreter does not model`;
816
+ this.push(this.opaqueResult(returns, args));
817
+ return;
818
+ }
819
+ if (receiver?.kind === "object" && remapped.owner.endsWith("Map") && !receiver.handle.isProps) {
820
+ const handled = this.mapCall(receiver.handle, remapped.name, args);
821
+ if (handled !== void 0) {
822
+ this.push(returns === "V" ? null : handled);
823
+ return;
824
+ }
825
+ }
826
+ if (remapped.owner.startsWith("java/") || receiver?.kind === "string" || receiver?.kind === "number" || receiver?.kind === "array" || receiver?.kind === "object" && (receiver.handle.builder || receiver.handle.items || receiver.handle.iterator)) {
827
+ const handled = this.jdkCall(remapped, receiver, args);
828
+ if (handled !== void 0) {
829
+ this.push(returns === "V" ? null : handled);
830
+ return;
831
+ }
832
+ if (receiver?.kind === "number" && args.length === 0) {
833
+ this.push(returns === "V" ? null : receiver);
834
+ return;
835
+ }
836
+ if (remapped.owner.startsWith("java/")) {
837
+ this.push(this.opaqueResult(returns, receiver ? [receiver, ...args] : args));
838
+ return;
839
+ }
840
+ }
841
+ const registered = this.detectRegistration(args, `${remapped.owner}.${remapped.name}${remapped.descriptor}`);
842
+ if (registered) {
843
+ this.push(returns === "V" ? null : returns.startsWith("L") ? {
844
+ kind: "object",
845
+ handle: registered
846
+ } : UNKNOWN);
847
+ return;
848
+ }
849
+ if (receiver?.kind === "object") {
850
+ const target = receiver.handle;
851
+ const getter = source.lightGetters.find((candidate) => matchesGetter(candidate, remapped.name, remapped.descriptor));
852
+ if (getter && target.props && args[0]?.kind === "state") {
853
+ const found = findGetter(source, target.type ?? remapped.owner, getter);
854
+ if (!(found !== null && found.classFile.thisClass !== getter.root && !special)) {
855
+ const light = target.props.light;
856
+ let answer = UNKNOWN;
857
+ if (light === null) answer = {
858
+ kind: "number",
859
+ value: 0
860
+ };
861
+ else if (light.kind === "constant") answer = {
862
+ kind: "number",
863
+ value: light.level
864
+ };
865
+ else if (light.kind === "function") answer = this.invokeLambda(light.fn, [args[0]]) ?? UNKNOWN;
866
+ this.push(answer);
867
+ return;
868
+ }
869
+ }
870
+ if (target.isProps) {
871
+ if (shapes.length === 1 && shapes[0] === TO_INT_FUNCTION) {
872
+ const fn = args[0];
873
+ target.light = fn.kind === "lambda" ? {
874
+ kind: "function",
875
+ fn
876
+ } : {
877
+ kind: "opaque",
878
+ reason: "light function is a value the interpreter did not build"
879
+ };
880
+ this.push(returns === "V" ? null : receiver);
881
+ return;
882
+ }
883
+ if (shapes.length === 1 && shapes[0] === "I" && this.isPropsType(returns.slice(1, -1))) {
884
+ target.light = args[0].kind === "number" ? {
885
+ kind: "constant",
886
+ level: args[0].value
887
+ } : {
888
+ kind: "opaque",
889
+ reason: "light level is a value the interpreter did not compute"
890
+ };
891
+ this.push(receiver);
892
+ return;
893
+ }
894
+ if (returns.startsWith("L") && this.isPropsType(returns.slice(1, -1))) {
895
+ this.push(receiver);
896
+ return;
897
+ }
898
+ }
899
+ if (target.ctorArgs && args.length === 0 && (remapped.name === "ordinal" || remapped.name === "name")) {
900
+ this.push(target.ctorArgs[remapped.name === "ordinal" ? 1 : 0] ?? UNKNOWN);
901
+ return;
902
+ }
903
+ }
904
+ const returnsProps = returns.startsWith("L") && this.isPropsType(returns.slice(1, -1));
905
+ if (returnsProps && !hasReceiver && args.some((arg) => arg.kind === "object" && (arg.handle.isProps || arg.handle.props))) {
906
+ this.push(this.opaqueResult(returns, args));
907
+ return;
908
+ }
909
+ const helper = !hasReceiver && !args.some((arg) => arg.kind === "unknown") && (returns.startsWith("L") && !returns.startsWith("Ljava/") && source.resolve(returns.slice(1, -1)) !== null || returns.startsWith("[L") && source.resolve(remapped.owner) !== null || args.length > 0 && args.every((arg) => arg.kind === "number") && !returns.startsWith("L") && !returns.startsWith("["));
910
+ if (receiver?.kind === "object" || args.some((arg) => isTracked(arg)) || returnsProps || returns === TO_INT_FUNCTION || helper) {
911
+ const inlined = this.inline(remapped.owner, remapped.name, remapped.descriptor, receiver, args);
912
+ if (inlined.ok) {
913
+ this.push(returns === "V" ? null : inlined.value ?? this.opaqueResult(returns, receiver ? [receiver, ...args] : args));
914
+ return;
915
+ }
916
+ }
917
+ this.push(this.opaqueResult(returns, receiver ? [receiver, ...args] : args));
918
+ }
919
+ /** Runs one method's code. Returns false when it stopped early. */
920
+ run(code, constants) {
921
+ this.caller = constants.thisClass;
922
+ let at = 0;
923
+ let wide = false;
924
+ while (at < code.length) {
925
+ if (this.shared.steps-- <= 0) return false;
926
+ const op = code[at++];
927
+ switch (true) {
928
+ case op === 0: break;
929
+ case op === 1:
930
+ this.push({ kind: "null" });
931
+ break;
932
+ case op >= 2 && op <= 8:
933
+ this.push({
934
+ kind: "number",
935
+ value: op - 3
936
+ });
937
+ break;
938
+ case WIDE_CONSTANTS.has(op) && op !== 20:
939
+ this.push({
940
+ kind: "number",
941
+ value: op === 10 || op === 15 ? 1 : 0,
942
+ wide: true
943
+ });
944
+ break;
945
+ case op >= 11 && op <= 13:
946
+ this.push({
947
+ kind: "number",
948
+ value: op - 11
949
+ });
950
+ break;
951
+ case op === 16:
952
+ this.push({
953
+ kind: "number",
954
+ value: code[at++] << 24 >> 24
955
+ });
956
+ break;
957
+ case op === 17:
958
+ this.push({
959
+ kind: "number",
960
+ value: (code[at] << 8 | code[at + 1]) << 16 >> 16
961
+ });
962
+ at += 2;
963
+ break;
964
+ case LOAD_CONSTANT.has(op): {
965
+ let index;
966
+ if (op === 18) index = code[at++];
967
+ else {
968
+ index = code[at] << 8 | code[at + 1];
969
+ at += 2;
970
+ }
971
+ if (constants.constants[index]?.tag === "class") {
972
+ this.push({
973
+ kind: "class",
974
+ name: constants.utf8(index)
975
+ });
976
+ break;
977
+ }
978
+ const value = constants.literal(index);
979
+ if (typeof value === "string") this.push({
980
+ kind: "string",
981
+ value
982
+ });
983
+ else if (typeof value === "number") this.push({
984
+ kind: "number",
985
+ value,
986
+ wide: op === 20
987
+ });
988
+ else if (typeof value === "bigint") this.push({
989
+ kind: "number",
990
+ value: Number(value),
991
+ wide: true
992
+ });
993
+ else this.push(UNKNOWN);
994
+ break;
995
+ }
996
+ case op >= 21 && op <= 25: {
997
+ const index = wide ? code[at] << 8 | code[at + 1] : code[at];
998
+ at += wide ? 2 : 1;
999
+ wide = false;
1000
+ this.push(this.locals.get(index) ?? UNKNOWN);
1001
+ break;
1002
+ }
1003
+ case op >= 26 && op <= 45:
1004
+ this.push(this.locals.get((op - 26) % 4) ?? UNKNOWN);
1005
+ break;
1006
+ case op >= 54 && op <= 58: {
1007
+ const index = wide ? code[at] << 8 | code[at + 1] : code[at];
1008
+ at += wide ? 2 : 1;
1009
+ wide = false;
1010
+ this.locals.set(index, this.pop());
1011
+ break;
1012
+ }
1013
+ case op >= 59 && op <= 78:
1014
+ this.locals.set((op - 59) % 4, this.pop());
1015
+ break;
1016
+ case ARRAY_LOADS.has(op): {
1017
+ const index = this.pop();
1018
+ const array = this.pop();
1019
+ const position = index.kind === "number" ? index.value : -1;
1020
+ this.push(array.kind === "array" ? array.items[position] ?? UNKNOWN : UNKNOWN);
1021
+ break;
1022
+ }
1023
+ case ARRAY_STORES.has(op): {
1024
+ const value = this.pop();
1025
+ const index = this.pop();
1026
+ const array = this.pop();
1027
+ if (array.kind === "array" && index.kind === "number" && index.value >= 0 && index.value < MAX_ARRAY) array.items[index.value] = value;
1028
+ break;
1029
+ }
1030
+ case op === 87:
1031
+ this.pop();
1032
+ break;
1033
+ case op === 88:
1034
+ if (!this.isWide(this.pop())) this.pop();
1035
+ break;
1036
+ case op === 89: {
1037
+ const top = this.pop();
1038
+ this.push(top);
1039
+ this.push(top);
1040
+ break;
1041
+ }
1042
+ case op === 90: {
1043
+ const top = this.pop();
1044
+ const under = this.pop();
1045
+ this.push(top);
1046
+ this.push(under);
1047
+ this.push(top);
1048
+ break;
1049
+ }
1050
+ case op === 91: {
1051
+ const top = this.pop();
1052
+ const second = this.pop();
1053
+ if (this.isWide(second)) this.push(top);
1054
+ else {
1055
+ const third = this.pop();
1056
+ this.push(top);
1057
+ this.push(third);
1058
+ }
1059
+ this.push(second);
1060
+ this.push(top);
1061
+ break;
1062
+ }
1063
+ case op === 92: {
1064
+ const top = this.pop();
1065
+ if (this.isWide(top)) this.push(top);
1066
+ else {
1067
+ const second = this.pop();
1068
+ this.push(second);
1069
+ this.push(top);
1070
+ this.push(second);
1071
+ }
1072
+ this.push(top);
1073
+ break;
1074
+ }
1075
+ case op === 93: {
1076
+ const top = this.pop();
1077
+ const second = this.pop();
1078
+ if (this.isWide(top)) this.push(top);
1079
+ else {
1080
+ const third = this.pop();
1081
+ this.push(second);
1082
+ this.push(top);
1083
+ this.push(third);
1084
+ }
1085
+ this.push(second);
1086
+ this.push(top);
1087
+ break;
1088
+ }
1089
+ case op === 94: {
1090
+ const top = this.pop();
1091
+ const second = this.pop();
1092
+ if (this.isWide(top)) {
1093
+ if (this.isWide(second)) this.push(top);
1094
+ else {
1095
+ const third = this.pop();
1096
+ this.push(top);
1097
+ this.push(third);
1098
+ }
1099
+ } else {
1100
+ const third = this.pop();
1101
+ if (this.isWide(third)) {
1102
+ this.push(second);
1103
+ this.push(top);
1104
+ } else {
1105
+ const fourth = this.pop();
1106
+ this.push(second);
1107
+ this.push(top);
1108
+ this.push(fourth);
1109
+ }
1110
+ this.push(third);
1111
+ }
1112
+ this.push(second);
1113
+ this.push(top);
1114
+ break;
1115
+ }
1116
+ case op === 95: {
1117
+ const top = this.pop();
1118
+ const under = this.pop();
1119
+ this.push(top);
1120
+ this.push(under);
1121
+ break;
1122
+ }
1123
+ case op >= 96 && op <= 111: {
1124
+ const right = this.number(this.pop());
1125
+ const left = this.number(this.pop());
1126
+ const which = Math.floor((op - 96) / 4);
1127
+ const type = (op - 96) % 4;
1128
+ const integral = type <= 1;
1129
+ let value;
1130
+ switch (which) {
1131
+ case 0:
1132
+ value = left + right;
1133
+ break;
1134
+ case 1:
1135
+ value = left - right;
1136
+ break;
1137
+ case 2:
1138
+ value = left * right;
1139
+ break;
1140
+ default: value = integral ? right === 0 ? 0 : Math.trunc(left / right) : left / right;
1141
+ }
1142
+ if (type === 0) value = toInt(value);
1143
+ this.push({
1144
+ kind: "number",
1145
+ value,
1146
+ wide: type === 1 || type === 3
1147
+ });
1148
+ break;
1149
+ }
1150
+ case op >= 112 && op <= 115: {
1151
+ const right = this.number(this.pop());
1152
+ const left = this.number(this.pop());
1153
+ const type = op - 112;
1154
+ this.push({
1155
+ kind: "number",
1156
+ value: right === 0 ? 0 : left % right,
1157
+ wide: type === 1 || type === 3
1158
+ });
1159
+ break;
1160
+ }
1161
+ case op >= 116 && op <= 119: {
1162
+ const type = op - 116;
1163
+ this.push({
1164
+ kind: "number",
1165
+ value: -this.number(this.pop()),
1166
+ wide: type === 1 || type === 3
1167
+ });
1168
+ break;
1169
+ }
1170
+ case op >= 120 && op <= 131: {
1171
+ const right = this.number(this.pop());
1172
+ const left = this.number(this.pop());
1173
+ const isLong = (op - 120) % 2 === 1;
1174
+ let value;
1175
+ switch (op) {
1176
+ case 120:
1177
+ case 121:
1178
+ value = isLong ? left * 2 ** (right & 63) : left << (right & 31);
1179
+ break;
1180
+ case 122:
1181
+ case 123:
1182
+ value = isLong ? Math.floor(left / 2 ** (right & 63)) : left >> (right & 31);
1183
+ break;
1184
+ case 124:
1185
+ case 125:
1186
+ value = isLong ? Math.floor(left / 2 ** (right & 63)) : left >>> (right & 31);
1187
+ break;
1188
+ case 126:
1189
+ case 127:
1190
+ value = Number(BigInt(toInt(left)) & BigInt(toInt(right)));
1191
+ break;
1192
+ case 128:
1193
+ case 129:
1194
+ value = Number(BigInt(toInt(left)) | BigInt(toInt(right)));
1195
+ break;
1196
+ default: value = Number(BigInt(toInt(left)) ^ BigInt(toInt(right)));
1197
+ }
1198
+ this.push({
1199
+ kind: "number",
1200
+ value,
1201
+ wide: isLong
1202
+ });
1203
+ break;
1204
+ }
1205
+ case op === 132: {
1206
+ const slot = wide ? code[at] << 8 | code[at + 1] : code[at];
1207
+ const step = wide ? (code[at + 2] << 8 | code[at + 3]) << 16 >> 16 : code[at + 1] << 24 >> 24;
1208
+ at += wide ? 4 : 2;
1209
+ wide = false;
1210
+ this.locals.set(slot, {
1211
+ kind: "number",
1212
+ value: this.number(this.locals.get(slot) ?? UNKNOWN) + step
1213
+ });
1214
+ break;
1215
+ }
1216
+ case op >= 133 && op <= 147: {
1217
+ const value = this.pop();
1218
+ if (value.kind !== "number") {
1219
+ this.push(UNKNOWN);
1220
+ break;
1221
+ }
1222
+ let result = value.value;
1223
+ let isWide = false;
1224
+ switch (op) {
1225
+ case 133:
1226
+ case 143:
1227
+ isWide = true;
1228
+ break;
1229
+ case 135:
1230
+ case 138:
1231
+ case 141:
1232
+ isWide = true;
1233
+ break;
1234
+ case 136:
1235
+ case 139:
1236
+ case 142:
1237
+ result = toInt(result);
1238
+ break;
1239
+ case 145:
1240
+ result = toInt(result) << 24 >> 24;
1241
+ break;
1242
+ case 146:
1243
+ result = toInt(result) & 65535;
1244
+ break;
1245
+ case 147: result = toInt(result) << 16 >> 16;
1246
+ }
1247
+ this.push({
1248
+ kind: "number",
1249
+ value: result,
1250
+ wide: isWide
1251
+ });
1252
+ break;
1253
+ }
1254
+ case op >= 148 && op <= 152: {
1255
+ const right = this.pop();
1256
+ const left = this.pop();
1257
+ if (left.kind !== "number" || right.kind !== "number") {
1258
+ this.push(UNKNOWN);
1259
+ break;
1260
+ }
1261
+ this.push({
1262
+ kind: "number",
1263
+ value: Math.sign(left.value - right.value)
1264
+ });
1265
+ break;
1266
+ }
1267
+ case op >= 153 && op <= 166: {
1268
+ const twoOperands = op >= 159;
1269
+ const right = this.pop();
1270
+ const left = twoOperands ? this.pop() : {
1271
+ kind: "number",
1272
+ value: 0
1273
+ };
1274
+ const offset = (code[at] << 8 | code[at + 1]) << 16 >> 16;
1275
+ at += 2;
1276
+ let decided = null;
1277
+ if (op >= 165) {
1278
+ let same = false;
1279
+ if (left.kind === "object" && right.kind === "object") same = left.handle === right.handle;
1280
+ else if (left.kind === "null" && right.kind === "null") same = true;
1281
+ else if (left.kind === "unknown" || right.kind === "unknown") same = null;
1282
+ if (same !== null) decided = op === 165 ? same : !same;
1283
+ } else if (left.kind === "number" && right.kind === "number") decided = compare(op, twoOperands ? left.value - right.value : right.value);
1284
+ if (decided === null) {
1285
+ this.undecided = true;
1286
+ if (this.shared.probe) this.shared.probe.failure ??= "branches on a value the interpreter could not compute";
1287
+ break;
1288
+ }
1289
+ if (decided) at += offset - 3;
1290
+ break;
1291
+ }
1292
+ case op === 198 || op === 199: {
1293
+ const value = this.pop();
1294
+ const offset = (code[at] << 8 | code[at + 1]) << 16 >> 16;
1295
+ at += 2;
1296
+ if (value.kind === "unknown") {
1297
+ this.undecided = true;
1298
+ break;
1299
+ }
1300
+ const isNull = value.kind === "null";
1301
+ if (op === 198 ? isNull : !isNull) at += offset - 3;
1302
+ break;
1303
+ }
1304
+ case op === 167 || op === 200: {
1305
+ const offset = op === 167 ? (code[at] << 8 | code[at + 1]) << 16 >> 16 : code[at] << 24 | code[at + 1] << 16 | code[at + 2] << 8 | code[at + 3];
1306
+ const size = op === 167 ? 3 : 5;
1307
+ at += size - 1;
1308
+ if (offset > 0) {
1309
+ at += offset - size;
1310
+ break;
1311
+ }
1312
+ const site = at - size;
1313
+ const round = (this.loops.get(site) ?? 0) + 1;
1314
+ this.loops.set(site, round);
1315
+ if (this.undecided) {
1316
+ this.undecided = false;
1317
+ break;
1318
+ }
1319
+ if (round > MAX_ITERATIONS) {
1320
+ this.note(`a loop at ${site} went round more than ${MAX_ITERATIONS} times`);
1321
+ break;
1322
+ }
1323
+ at += offset - size;
1324
+ break;
1325
+ }
1326
+ case op === 170 || op === 171: {
1327
+ const start = at - 1;
1328
+ let cursor = at;
1329
+ while ((cursor & 3) !== 0) cursor++;
1330
+ const int32 = (position) => code[position] << 24 | code[position + 1] << 16 | code[position + 2] << 8 | code[position + 3];
1331
+ const fallback = int32(cursor);
1332
+ cursor += 4;
1333
+ const key = this.pop();
1334
+ let target = fallback;
1335
+ if (op === 170) {
1336
+ const low = int32(cursor);
1337
+ const high = int32(cursor + 4);
1338
+ cursor += 8;
1339
+ if (key.kind === "number" && key.value >= low && key.value <= high) target = int32(cursor + (key.value - low) * 4);
1340
+ cursor += (high - low + 1) * 4;
1341
+ } else {
1342
+ const pairs = int32(cursor);
1343
+ cursor += 4;
1344
+ for (let pair = 0; pair < pairs; pair++) {
1345
+ const match = int32(cursor);
1346
+ if (key.kind === "number" && key.value === match) target = int32(cursor + 4);
1347
+ cursor += 8;
1348
+ }
1349
+ }
1350
+ if (key.kind !== "number") {
1351
+ this.undecided = true;
1352
+ if (this.shared.probe) this.shared.probe.failure ??= "switches on a value the interpreter could not compute";
1353
+ at = cursor;
1354
+ break;
1355
+ }
1356
+ at = start + target;
1357
+ break;
1358
+ }
1359
+ case RETURNS.has(op):
1360
+ if (op !== 177) this.returned = this.pop();
1361
+ return true;
1362
+ case op === 178: {
1363
+ const index = code[at] << 8 | code[at + 1];
1364
+ at += 2;
1365
+ const raw = constants.reference(index);
1366
+ if (!raw) {
1367
+ this.push(UNKNOWN);
1368
+ break;
1369
+ }
1370
+ const reference = this.shared.source.remap(raw, true);
1371
+ if (reference.owner === "java/lang/Boolean" && (reference.name === "TRUE" || reference.name === "FALSE")) {
1372
+ this.push({
1373
+ kind: "number",
1374
+ value: reference.name === "TRUE" ? 1 : 0
1375
+ });
1376
+ break;
1377
+ }
1378
+ const key = `${reference.owner}.${reference.name}`;
1379
+ if (!this.shared.statics.has(key)) this.initialise(reference.owner);
1380
+ this.push(this.shared.statics.get(key) ?? UNKNOWN);
1381
+ break;
1382
+ }
1383
+ case op === 179: {
1384
+ const index = code[at] << 8 | code[at + 1];
1385
+ at += 2;
1386
+ const value = this.pop();
1387
+ const raw = constants.reference(index);
1388
+ if (raw) {
1389
+ const reference = this.shared.source.remap(raw, true);
1390
+ this.shared.statics.set(`${reference.owner}.${reference.name}`, value);
1391
+ }
1392
+ break;
1393
+ }
1394
+ case op === 180: {
1395
+ const index = code[at] << 8 | code[at + 1];
1396
+ at += 2;
1397
+ const target = this.pop();
1398
+ const reference = constants.reference(index);
1399
+ this.push(target.kind === "object" && reference ? target.handle.fields.get(reference.name) ?? UNKNOWN : UNKNOWN);
1400
+ break;
1401
+ }
1402
+ case op === 181: {
1403
+ const index = code[at] << 8 | code[at + 1];
1404
+ at += 2;
1405
+ const value = this.pop();
1406
+ const target = this.pop();
1407
+ const reference = constants.reference(index);
1408
+ if (target.kind === "object" && reference) {
1409
+ target.handle.fields.set(reference.name, value);
1410
+ if (value.kind === "object" && value.handle.isProps) target.handle.props = value.handle;
1411
+ }
1412
+ break;
1413
+ }
1414
+ case INVOKES.has(op): {
1415
+ const index = code[at] << 8 | code[at + 1];
1416
+ at += op === 185 ? 4 : 2;
1417
+ const reference = constants.reference(index);
1418
+ if (!reference) break;
1419
+ if (op === 183 && reference.name === "<init>") {
1420
+ const remapped = this.shared.source.remap(reference, false);
1421
+ const { args: shapes } = parseDescriptor(remapped.descriptor);
1422
+ const args = [];
1423
+ for (let count = 0; count < shapes.length; count++) args.unshift(this.pop());
1424
+ const objectRef = this.pop();
1425
+ if (objectRef.kind === "object") {
1426
+ if (objectRef.handle.ctorArgs === null) this.construct(objectRef.handle, remapped, args);
1427
+ else {
1428
+ this.taint(objectRef.handle, args);
1429
+ for (const arg of args) if (arg.kind === "object" && arg.handle.isProps) objectRef.handle.props = arg.handle;
1430
+ this.inline(remapped.owner, remapped.name, remapped.descriptor, objectRef, args);
1431
+ }
1432
+ }
1433
+ break;
1434
+ }
1435
+ this.call(reference, op !== 184, op === 183);
1436
+ break;
1437
+ }
1438
+ case op === 187: {
1439
+ const index = code[at] << 8 | code[at + 1];
1440
+ at += 2;
1441
+ const type = this.shared.source.remapClass(constants.utf8(index));
1442
+ this.push({
1443
+ kind: "object",
1444
+ handle: this.handle(type)
1445
+ });
1446
+ break;
1447
+ }
1448
+ case op === 188 || op === 189: {
1449
+ at += op === 188 ? 1 : 2;
1450
+ const length = this.pop();
1451
+ const size = length.kind === "number" ? Math.min(Math.max(length.value, 0), MAX_ARRAY) : 0;
1452
+ this.push({
1453
+ kind: "array",
1454
+ items: Array.from({ length: size }, () => UNKNOWN)
1455
+ });
1456
+ break;
1457
+ }
1458
+ case op === 197: {
1459
+ at += 2;
1460
+ const dimensions = code[at++];
1461
+ for (let count = 0; count < dimensions; count++) this.pop();
1462
+ this.push({
1463
+ kind: "array",
1464
+ items: []
1465
+ });
1466
+ break;
1467
+ }
1468
+ case op === 190: {
1469
+ const array = this.pop();
1470
+ this.push(array.kind === "array" ? {
1471
+ kind: "number",
1472
+ value: array.items.length
1473
+ } : UNKNOWN);
1474
+ break;
1475
+ }
1476
+ case op === 191: return true;
1477
+ case op === 192:
1478
+ at += 2;
1479
+ break;
1480
+ case op === 193:
1481
+ at += 2;
1482
+ this.pop();
1483
+ this.push(UNKNOWN);
1484
+ break;
1485
+ case op === 194 || op === 195:
1486
+ this.pop();
1487
+ break;
1488
+ case op === 196:
1489
+ wide = true;
1490
+ break;
1491
+ case op === 186: {
1492
+ const index = code[at] << 8 | code[at + 1];
1493
+ at += 4;
1494
+ const site = constants.invokeDynamic(index);
1495
+ const { args: shapes, returns } = site ? parseDescriptor(site.descriptor) : {
1496
+ args: [],
1497
+ returns: "V"
1498
+ };
1499
+ const captured = [];
1500
+ for (let count = 0; count < shapes.length; count++) captured.unshift(this.pop());
1501
+ const row = constants.bootstrap(index);
1502
+ if (row?.factory?.name === "makeConcatWithConstants") {
1503
+ const recipe = row.args.find((argument) => typeof argument === "string") ?? "";
1504
+ let operand = 0;
1505
+ let known = true;
1506
+ const text = recipe.replaceAll("", () => {
1507
+ const value = captured[operand++];
1508
+ if (value?.kind === "string" || value?.kind === "number") return String(value.value);
1509
+ if (value?.kind === "object" && value.handle.strings.length > 0) return value.handle.strings.at(-1);
1510
+ known = false;
1511
+ return "";
1512
+ });
1513
+ this.push(known ? {
1514
+ kind: "string",
1515
+ value: text
1516
+ } : UNKNOWN);
1517
+ break;
1518
+ }
1519
+ const target = constants.lambdaTarget(index);
1520
+ this.push(returns === "V" ? null : target ? {
1521
+ kind: "lambda",
1522
+ ...target,
1523
+ captured
1524
+ } : UNKNOWN);
1525
+ break;
1526
+ }
1527
+ default:
1528
+ this.note(`unsupported opcode 0x${op.toString(16)} at ${at - 1}`);
1529
+ return false;
1530
+ }
1531
+ }
1532
+ return true;
1533
+ }
1534
+ /** Runs a method as a root: unknown arguments, a fresh receiver for an instance method. */
1535
+ runRoot(method, classFile) {
1536
+ const receiver = (method.accessFlags & STATIC) === 0 ? {
1537
+ kind: "object",
1538
+ handle: this.handle(classFile.thisClass)
1539
+ } : null;
1540
+ this.seed(receiver, method.descriptor, []);
1541
+ try {
1542
+ if (!this.run(method.code, classFile) && (this.shared.steps <= 0 || this.shared.frames <= 0)) {
1543
+ const busiest = this.shared.frameCounts.entries().toArray().toSorted((left, right) => right[1] - left[1]).slice(0, 6).map(([key, count]) => `${key} ${count} steps`).join(", ");
1544
+ this.note(`${classFile.thisClass}.${method.name} ran past the ${this.shared.steps <= 0 ? "step" : "frame"} budget; busiest: ${busiest}`);
1545
+ }
1546
+ } catch (err) {
1547
+ this.note(`${classFile.thisClass}: ${err.message}`);
1548
+ }
1549
+ }
1550
+ /** Evaluates a light spec against one state; null where the interpreter lost the value. */
1551
+ evaluate(spec, probe) {
1552
+ this.shared.probe = probe;
1553
+ this.shared.steps = STEP_BUDGET;
1554
+ this.shared.frames = FRAME_BUDGET;
1555
+ try {
1556
+ const state = {
1557
+ kind: "state",
1558
+ probe
1559
+ };
1560
+ let result;
1561
+ if (spec.kind === "function") result = this.invokeLambda(spec.fn, [state]);
1562
+ else if (spec.kind === "method") {
1563
+ const method = findMethod(this.shared.source, spec.owner, spec.getter.name, spec.getter.descriptor);
1564
+ if (!method) return null;
1565
+ const receiver = spec.receiver ? {
1566
+ kind: "object",
1567
+ handle: spec.receiver
1568
+ } : {
1569
+ kind: "object",
1570
+ handle: this.handle(spec.owner)
1571
+ };
1572
+ const extra = parseDescriptor(spec.getter.descriptor).args.length - 1;
1573
+ const inlined = this.inline(method.classFile.thisClass, spec.getter.name, spec.getter.descriptor, receiver, [state, ...Array.from({ length: extra }, () => UNKNOWN)]);
1574
+ result = inlined.ok ? inlined.value : null;
1575
+ } else return spec.kind === "constant" ? spec.level : null;
1576
+ if (probe.failure || result?.kind !== "number" || !Number.isFinite(result.value)) return null;
1577
+ return Math.max(0, Math.min(15, Math.round(result.value)));
1578
+ } finally {
1579
+ this.shared.probe = null;
1580
+ }
1581
+ }
1582
+ };
1583
+ /** All combinations of the given properties' values, as assignments. */
1584
+ function combinations(domains) {
1585
+ let out = [/* @__PURE__ */ new Map()];
1586
+ for (const domain of domains) {
1587
+ const next = [];
1588
+ for (const partial of out) for (const value of domain.values) next.push(new Map([...partial, [domain.name, value]]));
1589
+ out = next;
1590
+ }
1591
+ return out;
1592
+ }
1593
+ /** The largest state table a light function may need; anything bigger is not a real block. */
1594
+ const MAX_STATES = 4096;
1595
+ /**
1596
+ * Folds a table of (assignment → level) into the pack's shape: a number when every state agrees,
1597
+ * otherwise the commonest level as the default and the rest as cases, each stripped of the properties
1598
+ * that do not change its answer.
1599
+ */
1600
+ function foldCases(table, properties) {
1601
+ if (new Set(table.map((row) => row.level)).size === 1) return table[0].level;
1602
+ const counts = /* @__PURE__ */ new Map();
1603
+ for (const row of table) counts.set(row.level, (counts.get(row.level) ?? 0) + 1);
1604
+ const fallback = [...counts].toSorted((left, right) => right[1] - left[1] || left[0] - right[0])[0][0];
1605
+ const key = (values, keep) => keep.map((name) => `${name}=${values.get(name)}`).join(",");
1606
+ const cases = /* @__PURE__ */ new Map();
1607
+ for (const row of table) {
1608
+ if (row.level === fallback) continue;
1609
+ let keep = [...properties];
1610
+ for (const name of properties) {
1611
+ const without = keep.filter((other) => other !== name);
1612
+ if (table.filter((other) => key(other.values, without) === key(row.values, without)).every((other) => other.level === row.level)) keep = without;
1613
+ }
1614
+ const when = {};
1615
+ const kept = properties.filter((name) => keep.includes(name)).toSorted((left, right) => left.localeCompare(right));
1616
+ for (const name of kept) when[name] = row.values.get(name);
1617
+ cases.set(key(row.values, keep), {
1618
+ when,
1619
+ level: row.level
1620
+ });
1621
+ }
1622
+ const order = (left, right) => {
1623
+ for (const name of properties) {
1624
+ const [leftValue, rightValue] = [left[name] ?? "", right[name] ?? ""];
1625
+ const difference = leftValue !== "" && rightValue !== "" && !Number.isNaN(Number(leftValue)) && !Number.isNaN(Number(rightValue)) ? Number(leftValue) - Number(rightValue) : leftValue.localeCompare(rightValue);
1626
+ if (difference !== 0) return difference;
1627
+ }
1628
+ return 0;
1629
+ };
1630
+ return {
1631
+ default: fallback,
1632
+ cases: cases.values().toArray().toSorted((left, right) => order(left.when, right.when))
1633
+ };
1634
+ }
1635
+ /**
1636
+ * Runs the registry code of a set of classes and answers, per block, what it emits.
1637
+ *
1638
+ * `roots` are the classes whose methods register blocks; every method of each is run, because where
1639
+ * the registrations sit differs by era and mod — `<clinit>` for a modern registry, a static
1640
+ * `registerBlocks()` in 1.13, an instance `init()` in a mod. The order is `<clinit>` first, since the
1641
+ * others read what it set up.
1642
+ */
1643
+ function extractBlockLight(roots, source) {
1644
+ const shared = {
1645
+ source,
1646
+ statics: /* @__PURE__ */ new Map(),
1647
+ initialised: /* @__PURE__ */ new Set(),
1648
+ registrations: /* @__PURE__ */ new Map(),
1649
+ steps: STEP_BUDGET,
1650
+ frames: FRAME_BUDGET,
1651
+ notes: [],
1652
+ nextHandle: 1,
1653
+ probe: null,
1654
+ frameCounts: /* @__PURE__ */ new Map()
1655
+ };
1656
+ for (const classFile of roots) {
1657
+ shared.initialised.add(classFile.thisClass);
1658
+ const methods = classFile.methods.filter((method) => method.code).toSorted((left, right) => Number(right.name === "<clinit>") - Number(left.name === "<clinit>"));
1659
+ for (const method of methods) {
1660
+ shared.steps = STEP_BUDGET;
1661
+ shared.frames = FRAME_BUDGET;
1662
+ shared.frameCounts.clear();
1663
+ new Interpreter(shared).runRoot(method, classFile);
1664
+ }
1665
+ }
1666
+ /**
1667
+ * The registration for a block: under its full id, or its bare path when the code that made it is the
1668
+ * block's own side — the game's for `minecraft:`, a mod's for anything else.
1669
+ */
1670
+ const registrationOf = (blockId) => {
1671
+ const colon = blockId.indexOf(":");
1672
+ const namespace = colon === -1 ? "minecraft" : blockId.slice(0, colon);
1673
+ const path = colon === -1 ? blockId : blockId.slice(colon + 1);
1674
+ const byPath = shared.registrations.get(path);
1675
+ const wantVanilla = namespace === "minecraft";
1676
+ return shared.registrations.get(blockId) ?? (byPath && byPath.vanilla === wantVanilla ? byPath : null);
1677
+ };
1678
+ const lightOf = (blockId) => {
1679
+ const registration = registrationOf(blockId);
1680
+ if (!registration) return { failure: "no registration found in the code; light unknown" };
1681
+ let spec = registration.props?.light ?? null;
1682
+ if (registration.blockType) for (const getter of source.lightGetters) {
1683
+ const found = findGetter(source, registration.blockType, getter);
1684
+ if (found && found.classFile.thisClass !== getter.root) {
1685
+ spec = {
1686
+ kind: "method",
1687
+ owner: registration.blockType,
1688
+ getter: {
1689
+ ...getter,
1690
+ descriptor: found.descriptor
1691
+ },
1692
+ receiver: registration.block
1693
+ };
1694
+ break;
1695
+ }
1696
+ }
1697
+ if (spec === null) {
1698
+ if (registration.props === null) return { failure: `registered without a recognisable properties builder (at ${registration.site}); light unknown` };
1699
+ return { light: 0 };
1700
+ }
1701
+ if (spec.kind === "constant") return { light: Math.max(0, Math.min(15, Math.round(spec.level))) };
1702
+ if (spec.kind === "opaque") return { failure: `light could not be read: ${spec.reason}` };
1703
+ const domains = /* @__PURE__ */ new Map();
1704
+ let properties = [];
1705
+ const evaluator = new Interpreter(shared);
1706
+ for (let rounds = 0; rounds < 8; rounds++) {
1707
+ const table = [];
1708
+ const discovered = /* @__PURE__ */ new Set();
1709
+ const assignments = combinations(properties.map((name) => domains.get(name)));
1710
+ if (assignments.length > MAX_STATES) return { failure: `light function depends on ${assignments.length} states; too many to be a real block` };
1711
+ for (const values of assignments) {
1712
+ const probe = {
1713
+ values,
1714
+ domains,
1715
+ reads: /* @__PURE__ */ new Set(),
1716
+ failure: null
1717
+ };
1718
+ const before = shared.notes.length;
1719
+ const level = evaluator.evaluate(spec, probe);
1720
+ if (level === null) {
1721
+ const detail = shared.notes.slice(before).join("; ");
1722
+ return { failure: `light function ${probe.failure ?? "did not return a number"}${detail ? ` (${detail})` : ""}` };
1723
+ }
1724
+ table.push({
1725
+ values,
1726
+ level
1727
+ });
1728
+ for (const read of probe.reads) if (!properties.includes(read)) discovered.add(read);
1729
+ }
1730
+ if (discovered.size === 0) return { light: foldCases(table, properties) };
1731
+ properties = [...properties, ...discovered];
1732
+ }
1733
+ return { failure: "light function reads more properties than a blockstate can have" };
1734
+ };
1735
+ const describe = (blockId) => {
1736
+ const registration = registrationOf(blockId);
1737
+ if (!registration) return `${blockId}: not registered`;
1738
+ const light = registration.props?.light;
1739
+ return `${blockId}: registered at ${registration.site}; class ${registration.blockType ?? "?"}, properties ${registration.props ? `#${registration.props.id}` : "none"}, light ${light ? light.kind === "function" ? `function ${light.fn.owner}.${light.fn.name}${light.fn.descriptor}` : JSON.stringify(light) : "unset"}`;
1740
+ };
1741
+ return {
1742
+ registrations: shared.registrations,
1743
+ notes: shared.notes,
1744
+ lightOf,
1745
+ describe
1746
+ };
1747
+ }
1748
+ //#endregion
1749
+ export { extractBlockLight as t };