sci2-ts 0.0.0-stage → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/apps/viewer/editor.html +43 -0
- package/apps/viewer/explore.html +27 -0
- package/apps/viewer/index.html +26 -0
- package/apps/viewer/package.json +27 -0
- package/apps/viewer/play.html +26 -0
- package/apps/viewer/plugin.d.ts +18 -0
- package/apps/viewer/plugin.js +75 -0
- package/apps/viewer/plugin.ts +84 -0
- package/apps/viewer/src/adlib.d.ts +20 -0
- package/apps/viewer/src/adlib.js +55 -0
- package/apps/viewer/src/adlib.ts +62 -0
- package/apps/viewer/src/app.d.ts +5 -0
- package/apps/viewer/src/app.js +5 -0
- package/apps/viewer/src/app.ts +5 -0
- package/apps/viewer/src/asset-index.d.ts +22 -0
- package/apps/viewer/src/asset-index.js +74 -0
- package/apps/viewer/src/asset-index.ts +90 -0
- package/apps/viewer/src/audio.d.ts +28 -0
- package/apps/viewer/src/audio.js +102 -0
- package/apps/viewer/src/audio.ts +110 -0
- package/apps/viewer/src/editor-handles.d.ts +63 -0
- package/apps/viewer/src/editor-handles.js +332 -0
- package/apps/viewer/src/editor-handles.ts +368 -0
- package/apps/viewer/src/editor-languages.d.ts +23 -0
- package/apps/viewer/src/editor-languages.js +157 -0
- package/apps/viewer/src/editor-languages.ts +135 -0
- package/apps/viewer/src/editor.css +213 -0
- package/apps/viewer/src/editor.d.ts +1 -0
- package/apps/viewer/src/editor.js +393 -0
- package/apps/viewer/src/editor.ts +412 -0
- package/apps/viewer/src/explore/classes.d.ts +7 -0
- package/apps/viewer/src/explore/classes.js +63 -0
- package/apps/viewer/src/explore/classes.ts +61 -0
- package/apps/viewer/src/explore/context.d.ts +33 -0
- package/apps/viewer/src/explore/context.js +96 -0
- package/apps/viewer/src/explore/context.ts +124 -0
- package/apps/viewer/src/explore/explore.css +93 -0
- package/apps/viewer/src/explore/explore.d.ts +1 -0
- package/apps/viewer/src/explore/explore.js +102 -0
- package/apps/viewer/src/explore/explore.ts +104 -0
- package/apps/viewer/src/explore/inspect.d.ts +5 -0
- package/apps/viewer/src/explore/inspect.js +208 -0
- package/apps/viewer/src/explore/inspect.ts +279 -0
- package/apps/viewer/src/explore/live.d.ts +22 -0
- package/apps/viewer/src/explore/live.js +318 -0
- package/apps/viewer/src/explore/live.ts +365 -0
- package/apps/viewer/src/explore/map.d.ts +11 -0
- package/apps/viewer/src/explore/map.js +182 -0
- package/apps/viewer/src/explore/map.ts +193 -0
- package/apps/viewer/src/files.d.ts +21 -0
- package/apps/viewer/src/files.js +69 -0
- package/apps/viewer/src/files.ts +72 -0
- package/apps/viewer/src/game-profile.d.ts +19 -0
- package/apps/viewer/src/game-profile.js +4 -0
- package/apps/viewer/src/game-profile.ts +26 -0
- package/apps/viewer/src/main.d.ts +1 -0
- package/apps/viewer/src/main.js +473 -0
- package/apps/viewer/src/main.ts +527 -0
- package/apps/viewer/src/music.d.ts +14 -0
- package/apps/viewer/src/music.js +32 -0
- package/apps/viewer/src/music.ts +34 -0
- package/apps/viewer/src/opl-worklet.d.ts +1 -0
- package/apps/viewer/src/opl-worklet.js +47 -0
- package/apps/viewer/src/opl-worklet.ts +55 -0
- package/apps/viewer/src/overlay.d.ts +31 -0
- package/apps/viewer/src/overlay.js +103 -0
- package/apps/viewer/src/overlay.ts +123 -0
- package/apps/viewer/src/play.css +14 -0
- package/apps/viewer/src/play.d.ts +1 -0
- package/apps/viewer/src/play.js +179 -0
- package/apps/viewer/src/play.ts +177 -0
- package/apps/viewer/src/saves.d.ts +10 -0
- package/apps/viewer/src/saves.js +50 -0
- package/apps/viewer/src/saves.ts +51 -0
- package/apps/viewer/src/session.d.ts +91 -0
- package/apps/viewer/src/session.js +301 -0
- package/apps/viewer/src/session.ts +321 -0
- package/apps/viewer/src/sound-preview.d.ts +7 -0
- package/apps/viewer/src/sound-preview.js +214 -0
- package/apps/viewer/src/sound-preview.ts +228 -0
- package/apps/viewer/src/style.css +64 -0
- package/apps/viewer/vite.config.ts +25 -0
- package/bin/sci-ts.mjs +9 -0
- package/lib/994.sc +49 -0
- package/lib/995.sc +53 -0
- package/lib/996.sc +299 -0
- package/lib/997.sc +198 -0
- package/lib/998.sc +338 -0
- package/lib/999.sc +189 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +84 -0
- package/lib/resources.ts +88 -0
- package/lib/system.sh +62 -0
- package/package.json +75 -4
- package/packages/content/room.schema.json +267 -0
- package/packages/content/src/index.d.ts +3 -0
- package/packages/content/src/index.js +3 -0
- package/packages/content/src/index.ts +3 -0
- package/packages/content/src/room.d.ts +111 -0
- package/packages/content/src/room.js +886 -0
- package/packages/content/src/room.ts +1011 -0
- package/packages/content/src/target.d.ts +80 -0
- package/packages/content/src/target.js +1 -0
- package/packages/content/src/target.ts +60 -0
- package/packages/content/src/yarn.d.ts +90 -0
- package/packages/content/src/yarn.js +214 -0
- package/packages/content/src/yarn.ts +261 -0
- package/packages/sci/src/analysis/game.d.ts +90 -0
- package/packages/sci/src/analysis/game.js +221 -0
- package/packages/sci/src/analysis/game.ts +305 -0
- package/packages/sci/src/analysis/runtime.d.ts +58 -0
- package/packages/sci/src/analysis/runtime.js +133 -0
- package/packages/sci/src/analysis/runtime.ts +163 -0
- package/packages/sci/src/audio/adlib.d.ts +73 -0
- package/packages/sci/src/audio/adlib.js +280 -0
- package/packages/sci/src/audio/adlib.ts +329 -0
- package/packages/sci/src/audio/index.d.ts +43 -0
- package/packages/sci/src/audio/index.js +99 -0
- package/packages/sci/src/audio/index.ts +122 -0
- package/packages/sci/src/audio/midi.d.ts +92 -0
- package/packages/sci/src/audio/midi.js +242 -0
- package/packages/sci/src/audio/midi.ts +262 -0
- package/packages/sci/src/audio/opl2.d.ts +74 -0
- package/packages/sci/src/audio/opl2.js +283 -0
- package/packages/sci/src/audio/opl2.ts +283 -0
- package/packages/sci/src/audio/sol.d.ts +32 -0
- package/packages/sci/src/audio/sol.js +81 -0
- package/packages/sci/src/audio/sol.ts +100 -0
- package/packages/sci/src/audio/write.d.ts +35 -0
- package/packages/sci/src/audio/write.js +91 -0
- package/packages/sci/src/audio/write.ts +108 -0
- package/packages/sci/src/gfx/cel.d.ts +36 -0
- package/packages/sci/src/gfx/cel.js +82 -0
- package/packages/sci/src/gfx/cel.ts +110 -0
- package/packages/sci/src/gfx/palette.d.ts +24 -0
- package/packages/sci/src/gfx/palette.js +49 -0
- package/packages/sci/src/gfx/palette.ts +56 -0
- package/packages/sci/src/gfx/pic-file.d.ts +46 -0
- package/packages/sci/src/gfx/pic-file.js +114 -0
- package/packages/sci/src/gfx/pic-file.ts +155 -0
- package/packages/sci/src/gfx/pic.d.ts +27 -0
- package/packages/sci/src/gfx/pic.js +34 -0
- package/packages/sci/src/gfx/pic.ts +53 -0
- package/packages/sci/src/gfx/quantize.d.ts +42 -0
- package/packages/sci/src/gfx/quantize.js +199 -0
- package/packages/sci/src/gfx/quantize.ts +218 -0
- package/packages/sci/src/gfx/view-file.d.ts +62 -0
- package/packages/sci/src/gfx/view-file.js +182 -0
- package/packages/sci/src/gfx/view-file.ts +233 -0
- package/packages/sci/src/gfx/view.d.ts +28 -0
- package/packages/sci/src/gfx/view.js +45 -0
- package/packages/sci/src/gfx/view.ts +62 -0
- package/packages/sci/src/index.d.ts +38 -0
- package/packages/sci/src/index.js +38 -0
- package/packages/sci/src/index.ts +38 -0
- package/packages/sci/src/motion/pathfinding.d.ts +40 -0
- package/packages/sci/src/motion/pathfinding.js +140 -0
- package/packages/sci/src/motion/pathfinding.ts +147 -0
- package/packages/sci/src/resource/archive.d.ts +16 -0
- package/packages/sci/src/resource/archive.js +51 -0
- package/packages/sci/src/resource/archive.ts +56 -0
- package/packages/sci/src/resource/lzs.d.ts +12 -0
- package/packages/sci/src/resource/lzs.js +64 -0
- package/packages/sci/src/resource/lzs.ts +66 -0
- package/packages/sci/src/resource/manager.d.ts +45 -0
- package/packages/sci/src/resource/manager.js +158 -0
- package/packages/sci/src/resource/manager.ts +184 -0
- package/packages/sci/src/resource/map.d.ts +18 -0
- package/packages/sci/src/resource/map.js +34 -0
- package/packages/sci/src/resource/map.ts +44 -0
- package/packages/sci/src/resource/types.d.ts +37 -0
- package/packages/sci/src/resource/types.js +45 -0
- package/packages/sci/src/resource/types.ts +57 -0
- package/packages/sci/src/script/assembly.d.ts +67 -0
- package/packages/sci/src/script/assembly.js +748 -0
- package/packages/sci/src/script/assembly.ts +714 -0
- package/packages/sci/src/script/compiler.d.ts +75 -0
- package/packages/sci/src/script/compiler.js +993 -0
- package/packages/sci/src/script/compiler.ts +1040 -0
- package/packages/sci/src/script/disasm.d.ts +81 -0
- package/packages/sci/src/script/disasm.js +318 -0
- package/packages/sci/src/script/disasm.ts +323 -0
- package/packages/sci/src/script/kernel-names.d.ts +6 -0
- package/packages/sci/src/script/kernel-names.js +165 -0
- package/packages/sci/src/script/kernel-names.ts +165 -0
- package/packages/sci/src/script/opcodes.d.ts +22 -0
- package/packages/sci/src/script/opcodes.js +45 -0
- package/packages/sci/src/script/opcodes.ts +72 -0
- package/packages/sci/src/script/script.d.ts +56 -0
- package/packages/sci/src/script/script.js +63 -0
- package/packages/sci/src/script/script.ts +111 -0
- package/packages/sci/src/script/sexpr.d.ts +45 -0
- package/packages/sci/src/script/sexpr.js +151 -0
- package/packages/sci/src/script/sexpr.ts +139 -0
- package/packages/sci/src/script/vocab.d.ts +15 -0
- package/packages/sci/src/script/vocab.js +44 -0
- package/packages/sci/src/script/vocab.ts +48 -0
- package/packages/sci/src/script/world.d.ts +25 -0
- package/packages/sci/src/script/world.js +65 -0
- package/packages/sci/src/script/world.ts +70 -0
- package/packages/sci/src/text/font.d.ts +37 -0
- package/packages/sci/src/text/font.js +103 -0
- package/packages/sci/src/text/font.ts +125 -0
- package/packages/sci/src/text/message.d.ts +69 -0
- package/packages/sci/src/text/message.js +171 -0
- package/packages/sci/src/text/message.ts +205 -0
- package/packages/sci/src/vm/kernels/arrays.d.ts +19 -0
- package/packages/sci/src/vm/kernels/arrays.js +182 -0
- package/packages/sci/src/vm/kernels/arrays.ts +164 -0
- package/packages/sci/src/vm/kernels/audio.d.ts +70 -0
- package/packages/sci/src/vm/kernels/audio.js +347 -0
- package/packages/sci/src/vm/kernels/audio.ts +349 -0
- package/packages/sci/src/vm/kernels/core.d.ts +5 -0
- package/packages/sci/src/vm/kernels/core.js +87 -0
- package/packages/sci/src/vm/kernels/core.ts +91 -0
- package/packages/sci/src/vm/kernels/graphics.d.ts +141 -0
- package/packages/sci/src/vm/kernels/graphics.js +467 -0
- package/packages/sci/src/vm/kernels/graphics.ts +502 -0
- package/packages/sci/src/vm/kernels/index.d.ts +8 -0
- package/packages/sci/src/vm/kernels/index.js +29 -0
- package/packages/sci/src/vm/kernels/index.ts +32 -0
- package/packages/sci/src/vm/kernels/lists.d.ts +6 -0
- package/packages/sci/src/vm/kernels/lists.js +127 -0
- package/packages/sci/src/vm/kernels/lists.ts +111 -0
- package/packages/sci/src/vm/kernels/motion.d.ts +7 -0
- package/packages/sci/src/vm/kernels/motion.js +204 -0
- package/packages/sci/src/vm/kernels/motion.ts +194 -0
- package/packages/sci/src/vm/kernels/palette.d.ts +82 -0
- package/packages/sci/src/vm/kernels/palette.js +292 -0
- package/packages/sci/src/vm/kernels/palette.ts +303 -0
- package/packages/sci/src/vm/kernels/saves.d.ts +45 -0
- package/packages/sci/src/vm/kernels/saves.js +123 -0
- package/packages/sci/src/vm/kernels/saves.ts +149 -0
- package/packages/sci/src/vm/kernels/system.d.ts +62 -0
- package/packages/sci/src/vm/kernels/system.js +110 -0
- package/packages/sci/src/vm/kernels/system.ts +128 -0
- package/packages/sci/src/vm/kernels/text.d.ts +13 -0
- package/packages/sci/src/vm/kernels/text.js +338 -0
- package/packages/sci/src/vm/kernels/text.ts +328 -0
- package/packages/sci/src/vm/memory.d.ts +138 -0
- package/packages/sci/src/vm/memory.js +168 -0
- package/packages/sci/src/vm/memory.ts +266 -0
- package/packages/sci/src/vm/replace.d.ts +11 -0
- package/packages/sci/src/vm/replace.js +14 -0
- package/packages/sci/src/vm/replace.ts +16 -0
- package/packages/sci/src/vm/savegame.d.ts +89 -0
- package/packages/sci/src/vm/savegame.js +161 -0
- package/packages/sci/src/vm/savegame.ts +188 -0
- package/packages/sci/src/vm/value.d.ts +17 -0
- package/packages/sci/src/vm/value.js +12 -0
- package/packages/sci/src/vm/value.ts +25 -0
- package/packages/sci/src/vm/vm.d.ts +155 -0
- package/packages/sci/src/vm/vm.js +1053 -0
- package/packages/sci/src/vm/vm.ts +914 -0
- package/tools/adlib-render.d.ts +1 -0
- package/tools/adlib-render.js +49 -0
- package/tools/adlib-render.ts +42 -0
- package/tools/analyze.d.ts +1 -0
- package/tools/analyze.js +25 -0
- package/tools/analyze.ts +25 -0
- package/tools/art/build.d.ts +34 -0
- package/tools/art/build.js +175 -0
- package/tools/art/build.ts +153 -0
- package/tools/art.d.ts +1 -0
- package/tools/art.js +31 -0
- package/tools/art.ts +29 -0
- package/tools/asm.d.ts +1 -0
- package/tools/asm.js +61 -0
- package/tools/asm.ts +55 -0
- package/tools/cli.d.ts +1 -0
- package/tools/cli.js +99 -0
- package/tools/cli.ts +100 -0
- package/tools/editor-server.d.ts +29 -0
- package/tools/editor-server.js +162 -0
- package/tools/editor-server.ts +176 -0
- package/tools/explore.d.ts +1 -0
- package/tools/explore.js +305 -0
- package/tools/explore.ts +250 -0
- package/tools/game/audio.d.ts +19 -0
- package/tools/game/audio.js +143 -0
- package/tools/game/audio.ts +130 -0
- package/tools/game/build.d.ts +46 -0
- package/tools/game/build.js +259 -0
- package/tools/game/build.ts +283 -0
- package/tools/game/defaults.d.ts +26 -0
- package/tools/game/defaults.js +82 -0
- package/tools/game/defaults.ts +83 -0
- package/tools/game/font.d.ts +3 -0
- package/tools/game/font.js +124 -0
- package/tools/game/font.ts +126 -0
- package/tools/game/kit.d.ts +7 -0
- package/tools/game/kit.js +7 -0
- package/tools/game/kit.ts +10 -0
- package/tools/game/new.d.ts +8 -0
- package/tools/game/new.js +39 -0
- package/tools/game/new.ts +38 -0
- package/tools/game/target.d.ts +7 -0
- package/tools/game/target.js +41 -0
- package/tools/game/target.ts +42 -0
- package/tools/game/template/resources.ts.tmpl +43 -0
- package/tools/game/template/rooms/1.room.yaml +9 -0
- package/tools/game/template/rooms/1.yarn +17 -0
- package/tools/game/template/scripts/0.sc +24 -0
- package/tools/game.d.ts +1 -0
- package/tools/game.js +77 -0
- package/tools/game.ts +70 -0
- package/tools/mod-build.d.ts +28 -0
- package/tools/mod-build.js +173 -0
- package/tools/mod-build.ts +179 -0
- package/tools/mod-target.d.ts +16 -0
- package/tools/mod-target.js +26 -0
- package/tools/mod-target.ts +32 -0
- package/tools/mod.d.ts +1 -0
- package/tools/mod.js +47 -0
- package/tools/mod.ts +45 -0
- package/tools/msg.d.ts +1 -0
- package/tools/msg.js +40 -0
- package/tools/msg.ts +36 -0
- package/tools/node-files.d.ts +3 -0
- package/tools/node-files.js +34 -0
- package/tools/node-files.ts +33 -0
- package/tools/patches.d.ts +3 -0
- package/tools/patches.js +13 -0
- package/tools/patches.ts +13 -0
- package/tools/pic-build.d.ts +9 -0
- package/tools/pic-build.js +26 -0
- package/tools/pic-build.ts +27 -0
- package/tools/pic.d.ts +1 -0
- package/tools/pic.js +64 -0
- package/tools/pic.ts +61 -0
- package/tools/play.d.ts +1 -0
- package/tools/play.js +54 -0
- package/tools/play.ts +54 -0
- package/tools/png.d.ts +16 -0
- package/tools/png.js +124 -0
- package/tools/png.ts +123 -0
- package/tools/res.d.ts +1 -0
- package/tools/res.js +48 -0
- package/tools/res.ts +50 -0
- package/tools/scenario.d.ts +1 -0
- package/tools/scenario.js +74 -0
- package/tools/scenario.ts +80 -0
- package/tools/site/build.d.ts +8 -0
- package/tools/site/build.js +110 -0
- package/tools/site/build.ts +116 -0
- package/tools/site/markdown.d.ts +11 -0
- package/tools/site/markdown.js +88 -0
- package/tools/site/markdown.ts +84 -0
- package/tools/view-build.d.ts +34 -0
- package/tools/view-build.js +47 -0
- package/tools/view-build.ts +71 -0
- package/tools/view.d.ts +1 -0
- package/tools/view.js +54 -0
- package/tools/view.ts +52 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { decodeInstruction, disassemble } from "./disasm.js";
|
|
2
|
+
import { opcodes } from "./opcodes.js";
|
|
3
|
+
import { ObjectSlot, readString } from "./script.js";
|
|
4
|
+
export class AsmError extends Error {
|
|
5
|
+
line;
|
|
6
|
+
constructor(message, line) {
|
|
7
|
+
super(line ? `line ${line}: ${message}` : message);
|
|
8
|
+
this.line = line;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const hex = (n, w = 4) => n.toString(16).padStart(w, "0");
|
|
12
|
+
const RELATIVE = (k) => k === "R";
|
|
13
|
+
const SIZED = (k) => k === "W" || k === "S" || k === "R";
|
|
14
|
+
/** Opcodes whose default form is 16-bit. */
|
|
15
|
+
const WORD_DEFAULT = new Set(["send", "self", "lofsa", "lofss"]);
|
|
16
|
+
const HEAP_OPERAND = new Set(["lofsa", "lofss"]);
|
|
17
|
+
const HEADER_SLOTS = ObjectSlot.Name + 1;
|
|
18
|
+
/** Instructions whose operand is a property of self, as a byte offset. */
|
|
19
|
+
const PROPERTY_OPS = new Set(["pToa", "aTop", "pTos", "sTop", "ipToa", "dpToa", "ipTos", "dpTos"]);
|
|
20
|
+
/**
|
|
21
|
+
* The name for a property byte offset in a layout, if it maps back unambiguously (so the
|
|
22
|
+
* assembler reproduces the same offset). Names never look like numbers or labels.
|
|
23
|
+
*/
|
|
24
|
+
function propertyName(layout, offset, sel) {
|
|
25
|
+
if (!layout || offset % 2 || offset / 2 >= layout.length)
|
|
26
|
+
return undefined;
|
|
27
|
+
const selector = layout[offset / 2];
|
|
28
|
+
if (layout.indexOf(selector) !== layout.lastIndexOf(selector))
|
|
29
|
+
return undefined;
|
|
30
|
+
const name = sel(selector);
|
|
31
|
+
return /^[A-Za-z_-][\w-]*$/.test(name) && !name.startsWith("sel_") ? name : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Would these operand values fit the byte form? */
|
|
34
|
+
function fitsSmall(kinds, values) {
|
|
35
|
+
return kinds.every((k, i) => {
|
|
36
|
+
const v = values[i];
|
|
37
|
+
if (k === "W")
|
|
38
|
+
return v >= 0 && v <= 0xff;
|
|
39
|
+
if (k === "S" || k === "R")
|
|
40
|
+
return v >= -128 && v <= 127;
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// --- Script → text ----------------------------------------------------------------------
|
|
45
|
+
export function toAssembly(script, ctx) {
|
|
46
|
+
const { code, heap } = script;
|
|
47
|
+
const cw = (o) => code[o] | (code[o + 1] << 8);
|
|
48
|
+
const hw = (o) => heap[o] | (heap[o + 1] << 8);
|
|
49
|
+
const sel = (n) => ctx.selectorNames[n] ?? `sel_${n}`;
|
|
50
|
+
const out = [`script ${script.number}`];
|
|
51
|
+
// Heap labels: objects by name, strings by offset.
|
|
52
|
+
const heapReloc = hw(0);
|
|
53
|
+
const heapRelocs = new Set(Array.from({ length: hw(heapReloc) }, (_, i) => hw(heapReloc + 2 + i * 2)));
|
|
54
|
+
const codeReloc = cw(0);
|
|
55
|
+
const codeRelocs = new Set(Array.from({ length: cw(codeReloc) }, (_, i) => cw(codeReloc + 2 + i * 2)));
|
|
56
|
+
const objectsEnd = script.objects.length ? script.objects.at(-1).offset + script.objects.at(-1).properties.length * 2 : 4 + script.locals.length * 2;
|
|
57
|
+
const strings = [];
|
|
58
|
+
for (let p = objectsEnd + 2; p < heapReloc;) {
|
|
59
|
+
let e = p;
|
|
60
|
+
while (e < heapReloc && heap[e] !== 0)
|
|
61
|
+
e++;
|
|
62
|
+
strings.push({ offset: p, bytes: heap.subarray(p, e) });
|
|
63
|
+
p = e + 1;
|
|
64
|
+
}
|
|
65
|
+
const objectLabels = uniqueObjectLabels(script.objects);
|
|
66
|
+
const heapLabel = (offset) => {
|
|
67
|
+
const o = script.objects.findIndex((x) => x.offset === offset);
|
|
68
|
+
if (o >= 0)
|
|
69
|
+
return `@${objectLabels[o]}`;
|
|
70
|
+
const s = strings.find((x) => x.offset === offset);
|
|
71
|
+
if (s)
|
|
72
|
+
return `@s_${hex(offset)}`;
|
|
73
|
+
// Inside something: the nearest label before it, plus an offset.
|
|
74
|
+
const before = [...script.objects.map((x, i) => ({ at: x.offset, label: objectLabels[i] })), ...strings.map((x) => ({ at: x.offset, label: `s_${hex(x.offset)}` }))]
|
|
75
|
+
.filter((x) => x.at <= offset).sort((a, b) => b.at - a.at)[0];
|
|
76
|
+
if (before)
|
|
77
|
+
return `@${before.label}+${offset - before.at}`;
|
|
78
|
+
return `@${offset}`;
|
|
79
|
+
};
|
|
80
|
+
// Code labels: functions from the disassembler, jump targets from a linear sweep.
|
|
81
|
+
const fns = disassemble(script, ctx);
|
|
82
|
+
const labels = new Map();
|
|
83
|
+
for (const f of fns)
|
|
84
|
+
labels.set(f.entry, f.label);
|
|
85
|
+
let dictEnd = 8 + script.exports.length * 2;
|
|
86
|
+
for (const o of script.objects) {
|
|
87
|
+
if (o.isClass)
|
|
88
|
+
dictEnd = o.properties[ObjectSlot.PropDict] + o.properties.length * 2;
|
|
89
|
+
const md = o.properties[ObjectSlot.MethDict];
|
|
90
|
+
dictEnd = md + 2 + cw(md) * 4;
|
|
91
|
+
}
|
|
92
|
+
const sweep = [];
|
|
93
|
+
for (let pc = dictEnd; pc < codeReloc;) {
|
|
94
|
+
const ins = decodeInstruction(code, pc);
|
|
95
|
+
sweep.push(ins);
|
|
96
|
+
pc += ins.length;
|
|
97
|
+
}
|
|
98
|
+
for (const ins of sweep)
|
|
99
|
+
if (ins.target !== undefined && !labels.has(ins.target))
|
|
100
|
+
labels.set(ins.target, ins.name === "call" ? `proc_${hex(ins.target)}` : `.L${hex(ins.target)}`);
|
|
101
|
+
const codeLabel = (offset) => labels.get(offset) ?? `${offset}`;
|
|
102
|
+
// Locals
|
|
103
|
+
if (script.locals.length)
|
|
104
|
+
out.push(`locals ${script.locals.map((v) => (v > 9 ? `$${hex(v)}` : `${v}`)).join(" ")}`);
|
|
105
|
+
// Exports
|
|
106
|
+
out.push("exports");
|
|
107
|
+
for (const e of script.exports) {
|
|
108
|
+
if (e.kind === "empty")
|
|
109
|
+
out.push(" 0");
|
|
110
|
+
else if (e.kind === "object")
|
|
111
|
+
out.push(` ${heapLabel(e.offset)}`);
|
|
112
|
+
else
|
|
113
|
+
out.push(` ${codeLabel(e.offset)}`);
|
|
114
|
+
}
|
|
115
|
+
// Objects
|
|
116
|
+
for (const [i, o] of script.objects.entries()) {
|
|
117
|
+
const label = objectLabels[i];
|
|
118
|
+
const species = o.isClass ? o.species : o.superclass;
|
|
119
|
+
const superName = (n) => (n === 0xffff ? "-" : ctx.className(n) ?? `$${hex(n)}`);
|
|
120
|
+
const propSels = o.isClass ? o.propSelectors : ctx.classPropSelectors(o.superclass);
|
|
121
|
+
out.push("");
|
|
122
|
+
out.push(o.isClass ? `class ${label} of ${superName(o.superclass)} species ${species}` : `instance ${label} of ${superName(o.superclass)}`);
|
|
123
|
+
const slotValue = (slot) => {
|
|
124
|
+
const at = o.offset + slot * 2;
|
|
125
|
+
const v = o.properties[slot];
|
|
126
|
+
return heapRelocs.has(at) ? heapLabel(v) : v > 9 ? `$${hex(v)}` : `${v}`;
|
|
127
|
+
};
|
|
128
|
+
out.push(` classScript ${slotValue(ObjectSlot.ClassScript)}`);
|
|
129
|
+
out.push(` info ${slotValue(ObjectSlot.Info)}`);
|
|
130
|
+
out.push(` name ${slotValue(ObjectSlot.Name)}`);
|
|
131
|
+
if (o.isClass) {
|
|
132
|
+
// A class declares its own layout: each property with its selector.
|
|
133
|
+
const headerSels = o.propSelectors.slice(0, HEADER_SLOTS);
|
|
134
|
+
out.push(` header ${headerSels.map(sel).join(" ")}`);
|
|
135
|
+
for (let slot = HEADER_SLOTS; slot < o.properties.length; slot++)
|
|
136
|
+
out.push(` prop ${sel(o.propSelectors[slot])} ${slotValue(slot)}`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
for (let slot = HEADER_SLOTS; slot < o.properties.length; slot++) {
|
|
140
|
+
const s = propSels?.[slot];
|
|
141
|
+
out.push(` ${s === undefined ? `#${slot}` : sel(s)} ${slotValue(slot)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const m of o.methods)
|
|
145
|
+
out.push(` method ${sel(m.selector)} ${codeLabel(m.offset)}`);
|
|
146
|
+
}
|
|
147
|
+
// Strings
|
|
148
|
+
out.push("", "strings");
|
|
149
|
+
for (const s of strings)
|
|
150
|
+
out.push(` s_${hex(s.offset)} ${quote(s.bytes)}`);
|
|
151
|
+
// Code
|
|
152
|
+
out.push("", "code");
|
|
153
|
+
const selectorPushes = new Set();
|
|
154
|
+
for (const f of fns)
|
|
155
|
+
for (const ins of f.instructions)
|
|
156
|
+
if (ins.name === "pushi" && f.comments.get(ins.offset) === sel(ins.operands[0]))
|
|
157
|
+
selectorPushes.add(ins.offset);
|
|
158
|
+
// Property operands are named against the layout of the code's owner (self). The owner
|
|
159
|
+
// follows the rule the assembler uses: a label `Obj::method` sets it, an `owner Obj` line
|
|
160
|
+
// sets it (emitted for procedures, whose labels don't say), `.L` labels keep it, and any
|
|
161
|
+
// other label clears it.
|
|
162
|
+
const fnAt = new Map(fns.map((f) => [f.entry, f]));
|
|
163
|
+
const labelOf = new Map(script.objects.map((o, i) => [o, objectLabels[i]]));
|
|
164
|
+
const layoutOf = (o) => (o.isClass ? o.propSelectors : ctx.classPropSelectors(o.superclass));
|
|
165
|
+
let owner;
|
|
166
|
+
for (const ins of sweep) {
|
|
167
|
+
const label = labels.get(ins.offset);
|
|
168
|
+
if (label) {
|
|
169
|
+
out.push(label.startsWith(".") ? `${label}:` : `\n${label}:`);
|
|
170
|
+
if (!label.startsWith(".")) {
|
|
171
|
+
const fnOwner = fnAt.get(ins.offset)?.owner;
|
|
172
|
+
const prefix = label.includes("::") ? label.slice(0, label.lastIndexOf("::")) : undefined;
|
|
173
|
+
const byLabel = prefix === undefined ? undefined : script.objects.find((o) => labelOf.get(o) === prefix);
|
|
174
|
+
owner = byLabel;
|
|
175
|
+
if (fnOwner && fnOwner !== byLabel) {
|
|
176
|
+
out.push(` owner ${labelOf.get(fnOwner)}`);
|
|
177
|
+
owner = fnOwner;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const info = opcodes[ins.opcode];
|
|
182
|
+
const small = (code[ins.offset] & 1) === 1;
|
|
183
|
+
const hasSized = info.operands.some(SIZED);
|
|
184
|
+
let suffix = "";
|
|
185
|
+
if (hasSized) {
|
|
186
|
+
const defaultSmall = WORD_DEFAULT.has(ins.name) ? false : fitsSmall(info.operands, ins.operands);
|
|
187
|
+
if (small !== defaultSmall)
|
|
188
|
+
suffix = small ? ".b" : ".w";
|
|
189
|
+
}
|
|
190
|
+
else if (small)
|
|
191
|
+
suffix = ".b"; // the size bit on an opcode without sized operands
|
|
192
|
+
const args = info.operands.map((k, i) => {
|
|
193
|
+
const v = ins.operands[i];
|
|
194
|
+
if (RELATIVE(k))
|
|
195
|
+
return codeLabel(ins.target);
|
|
196
|
+
if (i === 0 && HEAP_OPERAND.has(ins.name))
|
|
197
|
+
return codeRelocs.has(ins.offset + 1) ? heapLabel(v) : `${v}`;
|
|
198
|
+
if (i === 0 && ins.name === "pushi" && selectorPushes.has(ins.offset))
|
|
199
|
+
return `#${sel(v)}`;
|
|
200
|
+
if (i === 0 && ins.name === "callk") {
|
|
201
|
+
const name = ctx.kernelNames[v];
|
|
202
|
+
return name && ctx.kernelNames.indexOf(name) === ctx.kernelNames.lastIndexOf(name) ? name : `${v}`;
|
|
203
|
+
}
|
|
204
|
+
if (i === 0 && (ins.name === "class" || ins.name === "super"))
|
|
205
|
+
return ctx.className(v) ?? `$${hex(v)}`;
|
|
206
|
+
if (i === 0 && PROPERTY_OPS.has(ins.name) && owner) {
|
|
207
|
+
const name = propertyName(layoutOf(owner), v, sel);
|
|
208
|
+
if (name)
|
|
209
|
+
return name;
|
|
210
|
+
}
|
|
211
|
+
return k === "S" ? `${v}` : v > 9 ? `$${hex(v)}` : `${v}`;
|
|
212
|
+
});
|
|
213
|
+
out.push(` ${ins.name}${suffix}${args.length ? ` ${args.join(" ")}` : ""}`);
|
|
214
|
+
}
|
|
215
|
+
return `${out.join("\n")}\n`;
|
|
216
|
+
}
|
|
217
|
+
/** Object labels: names, disambiguated when a script reuses one. */
|
|
218
|
+
function uniqueObjectLabels(objects) {
|
|
219
|
+
const seen = new Map();
|
|
220
|
+
return objects.map((o) => {
|
|
221
|
+
const n = seen.get(o.name) ?? 0;
|
|
222
|
+
seen.set(o.name, n + 1);
|
|
223
|
+
const safe = /^[A-Za-z_][\w:$-]*$/.test(o.name) ? o.name : `obj_${hex(o.offset)}`;
|
|
224
|
+
return n ? `${safe}~${n}` : safe;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function quote(bytes) {
|
|
228
|
+
let s = '"';
|
|
229
|
+
for (const b of bytes) {
|
|
230
|
+
if (b === 0x22)
|
|
231
|
+
s += '\\"';
|
|
232
|
+
else if (b === 0x5c)
|
|
233
|
+
s += "\\\\";
|
|
234
|
+
else if (b === 0x0a)
|
|
235
|
+
s += "\\n";
|
|
236
|
+
else if (b >= 0x20 && b < 0x7f)
|
|
237
|
+
s += String.fromCharCode(b);
|
|
238
|
+
else
|
|
239
|
+
s += `\\x${hex(b, 2)}`;
|
|
240
|
+
}
|
|
241
|
+
return `${s}"`;
|
|
242
|
+
}
|
|
243
|
+
function tokenize(text) {
|
|
244
|
+
const lines = [];
|
|
245
|
+
text.split("\n").forEach((raw, i) => {
|
|
246
|
+
const tokens = [];
|
|
247
|
+
for (let p = 0; p < raw.length;) {
|
|
248
|
+
const c = raw[p];
|
|
249
|
+
if (c === ";")
|
|
250
|
+
break;
|
|
251
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
252
|
+
p++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (c === '"') {
|
|
256
|
+
let e = p + 1;
|
|
257
|
+
while (e < raw.length && raw[e] !== '"')
|
|
258
|
+
e += raw[e] === "\\" ? 2 : 1;
|
|
259
|
+
tokens.push(raw.slice(p, e + 1));
|
|
260
|
+
p = e + 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
let e = p;
|
|
264
|
+
while (e < raw.length && !" \t\r;".includes(raw[e]))
|
|
265
|
+
e++;
|
|
266
|
+
tokens.push(raw.slice(p, e));
|
|
267
|
+
p = e;
|
|
268
|
+
}
|
|
269
|
+
if (tokens.length)
|
|
270
|
+
lines.push({ no: i + 1, tokens });
|
|
271
|
+
});
|
|
272
|
+
return lines;
|
|
273
|
+
}
|
|
274
|
+
function unquote(token, line) {
|
|
275
|
+
if (!token.startsWith('"') || !token.endsWith('"') || token.length < 2)
|
|
276
|
+
throw new AsmError(`expected a string, got ${token}`, line);
|
|
277
|
+
const out = [];
|
|
278
|
+
for (let i = 1; i < token.length - 1; i++) {
|
|
279
|
+
const c = token[i];
|
|
280
|
+
if (c !== "\\") {
|
|
281
|
+
out.push(c.charCodeAt(0) & 0xff);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const n = token[++i];
|
|
285
|
+
if (n === "n")
|
|
286
|
+
out.push(0x0a);
|
|
287
|
+
else if (n === "x") {
|
|
288
|
+
out.push(parseInt(token.slice(i + 1, i + 3), 16));
|
|
289
|
+
i += 2;
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
out.push((n ?? "").charCodeAt(0));
|
|
293
|
+
}
|
|
294
|
+
return Uint8Array.from(out);
|
|
295
|
+
}
|
|
296
|
+
export function assemble(text, ctx) {
|
|
297
|
+
const lines = tokenize(text);
|
|
298
|
+
const selectorIds = new Map(ctx.selectorNames.map((n, i) => [n, i]));
|
|
299
|
+
const selector = (name, line) => {
|
|
300
|
+
let id = selectorIds.get(name) ?? (name.startsWith("sel_") ? Number(name.slice(4)) : undefined);
|
|
301
|
+
if (id === undefined && ctx.newSelector && /^[A-Za-z_-][\w-]*$/.test(name))
|
|
302
|
+
selectorIds.set(name, (id = ctx.newSelector(name)));
|
|
303
|
+
if (id === undefined || Number.isNaN(id))
|
|
304
|
+
throw new AsmError(`unknown selector ${name}`, line);
|
|
305
|
+
return id;
|
|
306
|
+
};
|
|
307
|
+
const number = (t, line) => {
|
|
308
|
+
const v = t.startsWith("$") ? parseInt(t.slice(1), 16) : t.startsWith("-$") ? -parseInt(t.slice(2), 16) : Number(t);
|
|
309
|
+
if (!Number.isFinite(v) || t === "")
|
|
310
|
+
throw new AsmError(`expected a number, got ${t}`, line);
|
|
311
|
+
return v;
|
|
312
|
+
};
|
|
313
|
+
const value = (t, line) => {
|
|
314
|
+
if (t.startsWith("#") && t.length > 1)
|
|
315
|
+
return { kind: "num", value: selector(t.slice(1), line) };
|
|
316
|
+
if (t.startsWith("@")) {
|
|
317
|
+
const [label, off] = t.slice(1).split("+");
|
|
318
|
+
return { kind: "heap", label: label, offset: off ? Number(off) : 0 };
|
|
319
|
+
}
|
|
320
|
+
if (/^-?(\$[0-9a-fA-F]+|\d+)$/.test(t))
|
|
321
|
+
return { kind: "num", value: number(t, line) };
|
|
322
|
+
return { kind: "code", label: t };
|
|
323
|
+
};
|
|
324
|
+
const classNumber = (t, line) => {
|
|
325
|
+
if (t === "-")
|
|
326
|
+
return 0xffff;
|
|
327
|
+
if (t.startsWith("$"))
|
|
328
|
+
return number(t, line);
|
|
329
|
+
const local = objects.find((o) => o.isClass && o.label === t);
|
|
330
|
+
const n = local ? local.species : ctx.classSpecies(t);
|
|
331
|
+
if (n === undefined)
|
|
332
|
+
throw new AsmError(`unknown class ${t}`, line);
|
|
333
|
+
return n;
|
|
334
|
+
};
|
|
335
|
+
let scriptNumber;
|
|
336
|
+
let locals = [];
|
|
337
|
+
const exports = [];
|
|
338
|
+
const objects = [];
|
|
339
|
+
const strings = [];
|
|
340
|
+
const instructions = [];
|
|
341
|
+
const codeLabels = new Map(); // label → instruction index
|
|
342
|
+
let section = "";
|
|
343
|
+
let current;
|
|
344
|
+
let owner;
|
|
345
|
+
const opcodeByName = new Map(opcodes.map((o, i) => [o.name, i]));
|
|
346
|
+
const kernelIds = new Map(ctx.kernelNames.map((n, i) => [n, i]));
|
|
347
|
+
for (const { no, tokens } of lines) {
|
|
348
|
+
const [head, ...rest] = tokens;
|
|
349
|
+
// Keywords double as property names (`script`, `code`...): inside an object, a line
|
|
350
|
+
// with a value is a property. Section headers stand alone.
|
|
351
|
+
const inObject = section === "object";
|
|
352
|
+
if (head === "script" && !inObject && section === "") {
|
|
353
|
+
scriptNumber = number(rest[0] ?? "", no);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (head === "locals" && !inObject && section === "") {
|
|
357
|
+
locals = rest.map((t) => value(t, no));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if ((head === "exports" || head === "strings" || head === "code") && rest.length === 0) {
|
|
361
|
+
section = head;
|
|
362
|
+
current = undefined;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// In code, `class Name` is the instruction that loads a class object.
|
|
366
|
+
if ((head === "class" || head === "instance") && rest[1] === "of" && section !== "code") {
|
|
367
|
+
section = "object";
|
|
368
|
+
const [label, of, superName, speciesKw, speciesNo] = rest;
|
|
369
|
+
if (!label || of !== "of" || !superName)
|
|
370
|
+
throw new AsmError(`expected: ${head} Name of Super`, no);
|
|
371
|
+
const isClass = head === "class";
|
|
372
|
+
if (isClass && speciesKw !== "species")
|
|
373
|
+
throw new AsmError("classes need: species N", no);
|
|
374
|
+
current = {
|
|
375
|
+
line: no, label, isClass, superclass: 0, species: isClass ? number(speciesNo ?? "", no) : 0xffff,
|
|
376
|
+
classScript: { kind: "num", value: 0 }, info: { kind: "num", value: isClass ? 0x8000 : 0 }, name: { kind: "num", value: 0 },
|
|
377
|
+
props: new Map(), propSelectors: [], methods: [], heapOffset: 0, size: 0,
|
|
378
|
+
};
|
|
379
|
+
// Resolved after all objects are read (a class may be declared later in the file).
|
|
380
|
+
current.superName = superName;
|
|
381
|
+
objects.push(current);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (section === "exports") {
|
|
385
|
+
exports.push(value(head, no));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (section === "strings") {
|
|
389
|
+
if (!rest[0])
|
|
390
|
+
throw new AsmError("expected: label \"text\"", no);
|
|
391
|
+
strings.push({ label: head, bytes: unquote(rest[0], no), line: no });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (section === "object" && current) {
|
|
395
|
+
const o = current;
|
|
396
|
+
if (head === "classScript")
|
|
397
|
+
o.classScript = value(rest[0] ?? "", no);
|
|
398
|
+
else if (head === "info")
|
|
399
|
+
o.info = value(rest[0] ?? "", no);
|
|
400
|
+
else if (head === "name")
|
|
401
|
+
o.name = value(rest[0] ?? "", no);
|
|
402
|
+
else if (head === "header")
|
|
403
|
+
o.headerSelectors = rest.map((t) => selector(t, no));
|
|
404
|
+
else if (head === "layout")
|
|
405
|
+
o.layoutSelectors = rest.map((t) => selector(t, no));
|
|
406
|
+
else if (head === "method")
|
|
407
|
+
o.methods.push({ selector: selector(rest[0] ?? "", no), label: rest[1] ?? "" });
|
|
408
|
+
else if (head === "prop") {
|
|
409
|
+
if (!o.isClass)
|
|
410
|
+
throw new AsmError("only classes declare new properties with `prop`", no);
|
|
411
|
+
o.propSelectors.push(selector(rest[0] ?? "", no));
|
|
412
|
+
o.props.set(HEADER_SLOTS + o.propSelectors.length - 1, value(rest[1] ?? "", no));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// An instance's property, by name (resolved against its class later) or #slot.
|
|
416
|
+
o.named ??= [];
|
|
417
|
+
o.named.push({ name: head, value: value(rest[0] ?? "", no), line: no });
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (section === "code") {
|
|
422
|
+
if (head.endsWith(":") && rest.length === 0) {
|
|
423
|
+
const label = head.slice(0, -1);
|
|
424
|
+
if (codeLabels.has(label))
|
|
425
|
+
throw new AsmError(`duplicate label ${label}`, no);
|
|
426
|
+
codeLabels.set(label, instructions.length);
|
|
427
|
+
// `Obj::method` code runs with self = Obj; local labels keep the owner; others clear it.
|
|
428
|
+
if (!label.startsWith("."))
|
|
429
|
+
owner = label.includes("::") ? label.slice(0, label.lastIndexOf("::")) : undefined;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (head === "owner") {
|
|
433
|
+
if (!rest[0])
|
|
434
|
+
throw new AsmError("expected: owner ObjectOrClass", no);
|
|
435
|
+
owner = rest[0];
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const [mnemonic, form] = head.split(".");
|
|
439
|
+
const opcode = opcodeByName.get(mnemonic);
|
|
440
|
+
if (opcode === undefined)
|
|
441
|
+
throw new AsmError(`unknown instruction ${head}`, no);
|
|
442
|
+
const info = opcodes[opcode];
|
|
443
|
+
if (rest.length !== info.operands.length)
|
|
444
|
+
throw new AsmError(`${mnemonic} takes ${info.operands.length} operand(s)`, no);
|
|
445
|
+
const args = info.operands.map((k, i) => {
|
|
446
|
+
const t = rest[i];
|
|
447
|
+
if (k === "R")
|
|
448
|
+
return { label: t, heap: false, offset: 0 };
|
|
449
|
+
if (t.startsWith("@")) {
|
|
450
|
+
const [label, off] = t.slice(1).split("+");
|
|
451
|
+
return { label: label, heap: true, offset: off ? Number(off) : 0 };
|
|
452
|
+
}
|
|
453
|
+
if (t.startsWith("#"))
|
|
454
|
+
return selector(t.slice(1), no);
|
|
455
|
+
if (i === 0 && mnemonic === "callk" && !/^-?\$?\d/.test(t)) {
|
|
456
|
+
const id = kernelIds.get(t);
|
|
457
|
+
if (id === undefined)
|
|
458
|
+
throw new AsmError(`unknown kernel ${t}`, no);
|
|
459
|
+
return id;
|
|
460
|
+
}
|
|
461
|
+
if (i === 0 && (mnemonic === "class" || mnemonic === "super") && !/^\$?\d/.test(t))
|
|
462
|
+
return classNumber(t, no);
|
|
463
|
+
if (i === 0 && PROPERTY_OPS.has(mnemonic) && !/^-?\$?\d/.test(t))
|
|
464
|
+
return { property: t };
|
|
465
|
+
return number(t, no);
|
|
466
|
+
});
|
|
467
|
+
instructions.push({ line: no, name: mnemonic, opcode, forced: form === "b" ? true : form === "w" ? false : undefined, args, owner, offset: 0, small: false });
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
throw new AsmError(`unexpected ${head}`, no);
|
|
471
|
+
}
|
|
472
|
+
if (scriptNumber === undefined)
|
|
473
|
+
throw new AsmError("missing `script N`");
|
|
474
|
+
// --- Resolve object layouts --------------------------------------------------------
|
|
475
|
+
for (const o of objects) {
|
|
476
|
+
o.superclass = classNumber(o.superName, o.line);
|
|
477
|
+
if (o.isClass) {
|
|
478
|
+
const header = o.headerSelectors ?? ctx.classPropSelectors(o.superclass)?.slice(0, HEADER_SLOTS);
|
|
479
|
+
if (!header)
|
|
480
|
+
throw new AsmError(`class ${o.label} needs a header line (its superclass layout is unknown)`, o.line);
|
|
481
|
+
o.propSelectors = [...header, ...o.propSelectors];
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const sels = o.layoutSelectors ?? ctx.classPropSelectors(o.superclass) ?? objects.find((c) => c.isClass && c.species === o.superclass)?.propSelectors;
|
|
485
|
+
if (!sels)
|
|
486
|
+
throw new AsmError(`unknown layout for the class of ${o.label}`, o.line);
|
|
487
|
+
o.propSelectors = [...sels];
|
|
488
|
+
// Unlisted properties keep the class's defaults.
|
|
489
|
+
const cls = objects.find((c) => c.isClass && c.species === o.superclass);
|
|
490
|
+
if (cls)
|
|
491
|
+
for (const [slot, v] of cls.props)
|
|
492
|
+
o.props.set(slot, v);
|
|
493
|
+
else
|
|
494
|
+
ctx.classDefaults(o.superclass)?.forEach((v, slot) => slot >= HEADER_SLOTS && o.props.set(slot, { kind: "num", value: v }));
|
|
495
|
+
for (const { name, value: v, line } of o.named ?? []) {
|
|
496
|
+
const slot = name.startsWith("#") ? Number(name.slice(1)) : sels.indexOf(selector(name, line));
|
|
497
|
+
if (slot < HEADER_SLOTS || slot >= sels.length)
|
|
498
|
+
throw new AsmError(`${o.label} has no property ${name}`, line);
|
|
499
|
+
o.props.set(slot, v);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
o.size = o.propSelectors.length;
|
|
503
|
+
}
|
|
504
|
+
// --- Property names: byte offsets in the owner's layout -------------------------------
|
|
505
|
+
for (const ins of instructions) {
|
|
506
|
+
const a = ins.args[0];
|
|
507
|
+
if (!a || typeof a !== "object" || !("property" in a))
|
|
508
|
+
continue;
|
|
509
|
+
if (!ins.owner)
|
|
510
|
+
throw new AsmError(`${ins.name} ${a.property}: no owner here (add \`owner Name\` or put it in a Name::method)`, ins.line);
|
|
511
|
+
const local = objects.find((o) => o.label === ins.owner);
|
|
512
|
+
const species = local ? undefined : ctx.classSpecies(ins.owner);
|
|
513
|
+
const layout = local?.propSelectors ?? (species === undefined ? undefined : ctx.classPropSelectors(species));
|
|
514
|
+
if (!layout)
|
|
515
|
+
throw new AsmError(`unknown owner ${ins.owner}`, ins.line);
|
|
516
|
+
const slot = layout.indexOf(selector(a.property, ins.line));
|
|
517
|
+
if (slot < 0)
|
|
518
|
+
throw new AsmError(`${ins.owner} has no property ${a.property}`, ins.line);
|
|
519
|
+
ins.args[0] = slot * 2;
|
|
520
|
+
}
|
|
521
|
+
// --- Heap layout ---------------------------------------------------------------------
|
|
522
|
+
const heapLabels = new Map();
|
|
523
|
+
let hp = 4 + locals.length * 2;
|
|
524
|
+
for (const o of objects) {
|
|
525
|
+
o.heapOffset = hp;
|
|
526
|
+
if (heapLabels.has(o.label))
|
|
527
|
+
throw new AsmError(`duplicate object ${o.label}`, o.line);
|
|
528
|
+
heapLabels.set(o.label, hp);
|
|
529
|
+
hp += o.size * 2;
|
|
530
|
+
}
|
|
531
|
+
hp += 2; // the 0 word ending the object list
|
|
532
|
+
const stringOffsets = [];
|
|
533
|
+
for (const s of strings) {
|
|
534
|
+
if (heapLabels.has(s.label))
|
|
535
|
+
throw new AsmError(`duplicate label ${s.label}`, s.line);
|
|
536
|
+
heapLabels.set(s.label, hp);
|
|
537
|
+
stringOffsets.push(hp);
|
|
538
|
+
hp += s.bytes.length + 1;
|
|
539
|
+
}
|
|
540
|
+
const heapRelocOffset = hp;
|
|
541
|
+
const heapAddress = (label, offset, line) => {
|
|
542
|
+
const base = heapLabels.get(label) ?? (/^\d+$/.test(label) ? Number(label) : undefined);
|
|
543
|
+
if (base === undefined)
|
|
544
|
+
throw new AsmError(`unknown heap label @${label}`, line);
|
|
545
|
+
return base + offset;
|
|
546
|
+
};
|
|
547
|
+
// --- Code layout: dictionaries, then instructions (with jump relaxation) -----------------
|
|
548
|
+
let cp = 8 + exports.length * 2;
|
|
549
|
+
const propDictAt = new Map(), methDictAt = new Map();
|
|
550
|
+
for (const o of objects) {
|
|
551
|
+
if (o.isClass) {
|
|
552
|
+
propDictAt.set(o, cp);
|
|
553
|
+
cp += o.size * 2;
|
|
554
|
+
}
|
|
555
|
+
methDictAt.set(o, cp);
|
|
556
|
+
cp += 2 + o.methods.length * 4;
|
|
557
|
+
}
|
|
558
|
+
const codeStart = cp;
|
|
559
|
+
const labelOffset = (label, line) => {
|
|
560
|
+
if (/^\d+$/.test(label))
|
|
561
|
+
return Number(label);
|
|
562
|
+
const idx = codeLabels.get(label);
|
|
563
|
+
if (idx === undefined)
|
|
564
|
+
throw new AsmError(`unknown code label ${label}`, line);
|
|
565
|
+
return idx < instructions.length ? instructions[idx].offset : codeEnd;
|
|
566
|
+
};
|
|
567
|
+
let codeEnd = codeStart;
|
|
568
|
+
const sizeOf = (ins) => {
|
|
569
|
+
const info = opcodes[ins.opcode];
|
|
570
|
+
let size = 1;
|
|
571
|
+
for (const k of info.operands)
|
|
572
|
+
size += k === "B" ? 1 : k === "U" ? 2 : ins.small ? 1 : 2;
|
|
573
|
+
return size;
|
|
574
|
+
};
|
|
575
|
+
// Start with each instruction's default/forced form; unforced relative jumps start small
|
|
576
|
+
// and grow until everything fits (sizes only grow, so this terminates).
|
|
577
|
+
for (const ins of instructions) {
|
|
578
|
+
const info = opcodes[ins.opcode];
|
|
579
|
+
if (!info.operands.some(SIZED))
|
|
580
|
+
ins.small = ins.forced ?? false;
|
|
581
|
+
else if (ins.forced !== undefined)
|
|
582
|
+
ins.small = ins.forced;
|
|
583
|
+
else if (WORD_DEFAULT.has(ins.name))
|
|
584
|
+
ins.small = false;
|
|
585
|
+
else if (info.operands.some(RELATIVE))
|
|
586
|
+
ins.small = true;
|
|
587
|
+
else
|
|
588
|
+
ins.small = fitsSmall(info.operands, ins.args.map((a) => (typeof a === "number" ? a : 0x7fff)));
|
|
589
|
+
}
|
|
590
|
+
for (let pass = 0;; pass++) {
|
|
591
|
+
let pc = codeStart;
|
|
592
|
+
for (const ins of instructions) {
|
|
593
|
+
ins.offset = pc;
|
|
594
|
+
pc += sizeOf(ins);
|
|
595
|
+
}
|
|
596
|
+
codeEnd = pc;
|
|
597
|
+
let grew = false;
|
|
598
|
+
for (const ins of instructions) {
|
|
599
|
+
const info = opcodes[ins.opcode];
|
|
600
|
+
if (!ins.small || ins.forced !== undefined || !info.operands.some(RELATIVE))
|
|
601
|
+
continue;
|
|
602
|
+
const target = labelOffset(ins.args[0].label, ins.line);
|
|
603
|
+
const rel = target - (ins.offset + sizeOf(ins));
|
|
604
|
+
if (rel < -128 || rel > 127) {
|
|
605
|
+
ins.small = false;
|
|
606
|
+
grew = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!grew)
|
|
610
|
+
break;
|
|
611
|
+
if (pass > 1000)
|
|
612
|
+
throw new AsmError("jump relaxation did not settle");
|
|
613
|
+
}
|
|
614
|
+
// --- Emit code -----------------------------------------------------------------------------
|
|
615
|
+
const codeRelocs = [];
|
|
616
|
+
const codeBytes = [];
|
|
617
|
+
const w16 = (arr, at, v) => { arr[at] = v & 0xff; arr[at + 1] = (v >> 8) & 0xff; };
|
|
618
|
+
const resolveValue = (v, line) => {
|
|
619
|
+
if (v.kind === "num")
|
|
620
|
+
return { value: v.value & 0xffff, heap: false };
|
|
621
|
+
if (v.kind === "heap")
|
|
622
|
+
return { value: heapAddress(v.label, v.offset, line), heap: true };
|
|
623
|
+
return { value: labelOffset(v.label, line), heap: false };
|
|
624
|
+
};
|
|
625
|
+
w16(codeBytes, 0, 0); // relocation table offset, patched below
|
|
626
|
+
w16(codeBytes, 2, 0);
|
|
627
|
+
w16(codeBytes, 4, 0);
|
|
628
|
+
w16(codeBytes, 6, exports.length);
|
|
629
|
+
exports.forEach((e, i) => {
|
|
630
|
+
const at = 8 + i * 2;
|
|
631
|
+
if (e.kind === "num") {
|
|
632
|
+
w16(codeBytes, at, e.value);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (e.kind === "heap") {
|
|
636
|
+
w16(codeBytes, at, heapAddress(e.label, e.offset, 0));
|
|
637
|
+
codeRelocs.push(at);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// A bare name: an object if there is one, else a code label.
|
|
641
|
+
if (heapLabels.has(e.label) && objects.some((o) => o.label === e.label)) {
|
|
642
|
+
w16(codeBytes, at, heapLabels.get(e.label));
|
|
643
|
+
codeRelocs.push(at);
|
|
644
|
+
}
|
|
645
|
+
else
|
|
646
|
+
w16(codeBytes, at, labelOffset(e.label, 0));
|
|
647
|
+
});
|
|
648
|
+
for (const o of objects) {
|
|
649
|
+
if (o.isClass)
|
|
650
|
+
o.propSelectors.forEach((s, i) => w16(codeBytes, propDictAt.get(o) + i * 2, s));
|
|
651
|
+
const md = methDictAt.get(o);
|
|
652
|
+
w16(codeBytes, md, o.methods.length);
|
|
653
|
+
o.methods.forEach((m, i) => {
|
|
654
|
+
w16(codeBytes, md + 2 + i * 4, m.selector);
|
|
655
|
+
w16(codeBytes, md + 4 + i * 4, labelOffset(m.label, o.line));
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
for (const ins of instructions) {
|
|
659
|
+
const info = opcodes[ins.opcode];
|
|
660
|
+
let pc = ins.offset;
|
|
661
|
+
codeBytes[pc++] = (ins.opcode << 1) | (ins.small ? 1 : 0);
|
|
662
|
+
const next = ins.offset + sizeOf(ins);
|
|
663
|
+
info.operands.forEach((k, i) => {
|
|
664
|
+
const a = ins.args[i];
|
|
665
|
+
let v;
|
|
666
|
+
if (typeof a === "number")
|
|
667
|
+
v = a;
|
|
668
|
+
else if ("property" in a)
|
|
669
|
+
throw new AsmError(`unresolved property ${a.property}`, ins.line);
|
|
670
|
+
else if (a.heap) {
|
|
671
|
+
v = heapAddress(a.label, a.offset, ins.line);
|
|
672
|
+
if (ins.small)
|
|
673
|
+
throw new AsmError(`${ins.name} with a heap address can't be .b (it's relocated)`, ins.line);
|
|
674
|
+
codeRelocs.push(pc);
|
|
675
|
+
}
|
|
676
|
+
else
|
|
677
|
+
v = k === "R" ? labelOffset(a.label, ins.line) - next : labelOffset(a.label, ins.line);
|
|
678
|
+
const width = k === "B" ? 1 : k === "U" ? 2 : ins.small ? 1 : 2;
|
|
679
|
+
if (width === 1) {
|
|
680
|
+
const ok = k === "W" || k === "B" ? v >= 0 && v <= 0xff : v >= -128 && v <= 127;
|
|
681
|
+
if (!ok)
|
|
682
|
+
throw new AsmError(`${ins.name}: ${v} doesn't fit in a byte`, ins.line);
|
|
683
|
+
codeBytes[pc++] = v & 0xff;
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
if (v < -0x8000 || v > 0xffff)
|
|
687
|
+
throw new AsmError(`${ins.name}: ${v} doesn't fit in 16 bits`, ins.line);
|
|
688
|
+
w16(codeBytes, pc, v & 0xffff);
|
|
689
|
+
pc += 2;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const codeRelocOffset = codeEnd;
|
|
694
|
+
w16(codeBytes, 0, codeRelocOffset);
|
|
695
|
+
codeRelocs.sort((a, b) => a - b);
|
|
696
|
+
w16(codeBytes, codeRelocOffset, codeRelocs.length);
|
|
697
|
+
codeRelocs.forEach((r, i) => w16(codeBytes, codeRelocOffset + 2 + i * 2, r));
|
|
698
|
+
// --- Emit heap -----------------------------------------------------------------------------
|
|
699
|
+
const heapBytes = [];
|
|
700
|
+
const heapRelocs = [];
|
|
701
|
+
w16(heapBytes, 0, heapRelocOffset);
|
|
702
|
+
w16(heapBytes, 2, locals.length);
|
|
703
|
+
locals.forEach((v, i) => {
|
|
704
|
+
const r = resolveValue(v, 0);
|
|
705
|
+
w16(heapBytes, 4 + i * 2, r.value);
|
|
706
|
+
if (r.heap)
|
|
707
|
+
heapRelocs.push(4 + i * 2);
|
|
708
|
+
});
|
|
709
|
+
for (const o of objects) {
|
|
710
|
+
const slot = (n, v) => {
|
|
711
|
+
const at = o.heapOffset + n * 2;
|
|
712
|
+
if (typeof v === "number") {
|
|
713
|
+
w16(heapBytes, at, v);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const r = resolveValue(v, o.line);
|
|
717
|
+
w16(heapBytes, at, r.value);
|
|
718
|
+
if (r.heap)
|
|
719
|
+
heapRelocs.push(at);
|
|
720
|
+
};
|
|
721
|
+
slot(ObjectSlot.Magic, 0x1234);
|
|
722
|
+
slot(ObjectSlot.Size, o.size);
|
|
723
|
+
// Instances point both dictionary slots at their method dictionary.
|
|
724
|
+
slot(ObjectSlot.PropDict, o.isClass ? propDictAt.get(o) : methDictAt.get(o));
|
|
725
|
+
slot(ObjectSlot.MethDict, methDictAt.get(o));
|
|
726
|
+
slot(ObjectSlot.ClassScript, o.classScript);
|
|
727
|
+
slot(ObjectSlot.Species, o.species);
|
|
728
|
+
slot(ObjectSlot.Super, o.superclass);
|
|
729
|
+
slot(ObjectSlot.Info, o.info);
|
|
730
|
+
slot(ObjectSlot.Name, o.name);
|
|
731
|
+
for (let n = HEADER_SLOTS; n < o.size; n++)
|
|
732
|
+
slot(n, o.props.get(n) ?? 0);
|
|
733
|
+
}
|
|
734
|
+
const objectsEnd = objects.length ? objects.at(-1).heapOffset + objects.at(-1).size * 2 : 4 + locals.length * 2;
|
|
735
|
+
w16(heapBytes, objectsEnd, 0);
|
|
736
|
+
strings.forEach((s, i) => {
|
|
737
|
+
const at = stringOffsets[i];
|
|
738
|
+
s.bytes.forEach((b, j) => (heapBytes[at + j] = b));
|
|
739
|
+
heapBytes[at + s.bytes.length] = 0;
|
|
740
|
+
});
|
|
741
|
+
heapRelocs.sort((a, b) => a - b);
|
|
742
|
+
w16(heapBytes, heapRelocOffset, heapRelocs.length);
|
|
743
|
+
heapRelocs.forEach((r, i) => w16(heapBytes, heapRelocOffset + 2 + i * 2, r));
|
|
744
|
+
const fill = (a) => Uint8Array.from({ length: a.length }, (_, i) => a[i] ?? 0);
|
|
745
|
+
return { number: scriptNumber, code: fill(codeBytes), heap: fill(heapBytes) };
|
|
746
|
+
}
|
|
747
|
+
/** Reads the heap string at an address (for tools that show assembled output). */
|
|
748
|
+
export const heapString = (heap, offset) => readString(heap, offset);
|