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