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