psyq-asm 0.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +56 -0
  3. package/README.md +220 -0
  4. package/bin/psyq-asm.mjs +149 -0
  5. package/dist/asm/assemble.d.ts +10 -0
  6. package/dist/asm/assemble.d.ts.map +1 -0
  7. package/dist/asm/assemble.js +42 -0
  8. package/dist/asm/assemble.js.map +1 -0
  9. package/dist/asm/diagnostics.d.ts +22 -0
  10. package/dist/asm/diagnostics.d.ts.map +1 -0
  11. package/dist/asm/diagnostics.js +40 -0
  12. package/dist/asm/diagnostics.js.map +1 -0
  13. package/dist/asm/directives.d.ts +67 -0
  14. package/dist/asm/directives.d.ts.map +1 -0
  15. package/dist/asm/directives.js +254 -0
  16. package/dist/asm/directives.js.map +1 -0
  17. package/dist/asm/expand.d.ts +21 -0
  18. package/dist/asm/expand.d.ts.map +1 -0
  19. package/dist/asm/expand.js +540 -0
  20. package/dist/asm/expand.js.map +1 -0
  21. package/dist/asm/hazards.d.ts +10 -0
  22. package/dist/asm/hazards.d.ts.map +1 -0
  23. package/dist/asm/hazards.js +196 -0
  24. package/dist/asm/hazards.js.map +1 -0
  25. package/dist/asm/layout.d.ts +17 -0
  26. package/dist/asm/layout.d.ts.map +1 -0
  27. package/dist/asm/layout.js +257 -0
  28. package/dist/asm/layout.js.map +1 -0
  29. package/dist/asm/lexer.d.ts +23 -0
  30. package/dist/asm/lexer.d.ts.map +1 -0
  31. package/dist/asm/lexer.js +81 -0
  32. package/dist/asm/lexer.js.map +1 -0
  33. package/dist/asm/object.d.ts +6 -0
  34. package/dist/asm/object.d.ts.map +1 -0
  35. package/dist/asm/object.js +46 -0
  36. package/dist/asm/object.js.map +1 -0
  37. package/dist/asm/operands.d.ts +49 -0
  38. package/dist/asm/operands.d.ts.map +1 -0
  39. package/dist/asm/operands.js +114 -0
  40. package/dist/asm/operands.js.map +1 -0
  41. package/dist/asm/parser.d.ts +28 -0
  42. package/dist/asm/parser.d.ts.map +1 -0
  43. package/dist/asm/parser.js +60 -0
  44. package/dist/asm/parser.js.map +1 -0
  45. package/dist/asm/relocations.d.ts +42 -0
  46. package/dist/asm/relocations.d.ts.map +1 -0
  47. package/dist/asm/relocations.js +214 -0
  48. package/dist/asm/relocations.js.map +1 -0
  49. package/dist/asm/sections.d.ts +23 -0
  50. package/dist/asm/sections.d.ts.map +1 -0
  51. package/dist/asm/sections.js +81 -0
  52. package/dist/asm/sections.js.map +1 -0
  53. package/dist/asm/stream.d.ts +77 -0
  54. package/dist/asm/stream.d.ts.map +1 -0
  55. package/dist/asm/stream.js +2 -0
  56. package/dist/asm/stream.js.map +1 -0
  57. package/dist/asm/symbols.d.ts +42 -0
  58. package/dist/asm/symbols.d.ts.map +1 -0
  59. package/dist/asm/symbols.js +122 -0
  60. package/dist/asm/symbols.js.map +1 -0
  61. package/dist/errors.d.ts +27 -0
  62. package/dist/errors.d.ts.map +1 -0
  63. package/dist/errors.js +34 -0
  64. package/dist/errors.js.map +1 -0
  65. package/dist/index.d.ts +14 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +10 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/isa/decode.d.ts +8 -0
  70. package/dist/isa/decode.d.ts.map +1 -0
  71. package/dist/isa/decode.js +92 -0
  72. package/dist/isa/decode.js.map +1 -0
  73. package/dist/isa/encode.d.ts +9 -0
  74. package/dist/isa/encode.d.ts.map +1 -0
  75. package/dist/isa/encode.js +124 -0
  76. package/dist/isa/encode.js.map +1 -0
  77. package/dist/isa/fields.d.ts +27 -0
  78. package/dist/isa/fields.d.ts.map +1 -0
  79. package/dist/isa/fields.js +49 -0
  80. package/dist/isa/fields.js.map +1 -0
  81. package/dist/isa/format.d.ts +14 -0
  82. package/dist/isa/format.d.ts.map +1 -0
  83. package/dist/isa/format.js +98 -0
  84. package/dist/isa/format.js.map +1 -0
  85. package/dist/isa/instruction.d.ts +15 -0
  86. package/dist/isa/instruction.d.ts.map +1 -0
  87. package/dist/isa/instruction.js +47 -0
  88. package/dist/isa/instruction.js.map +1 -0
  89. package/dist/isa/registers.d.ts +8 -0
  90. package/dist/isa/registers.d.ts.map +1 -0
  91. package/dist/isa/registers.js +55 -0
  92. package/dist/isa/registers.js.map +1 -0
  93. package/dist/isa/table.d.ts +179 -0
  94. package/dist/isa/table.d.ts.map +1 -0
  95. package/dist/isa/table.js +128 -0
  96. package/dist/isa/table.js.map +1 -0
  97. package/dist/options.d.ts +29 -0
  98. package/dist/options.d.ts.map +1 -0
  99. package/dist/options.js +0 -0
  100. package/dist/options.js.map +1 -0
  101. package/dist/program/decode-words.d.ts +9 -0
  102. package/dist/program/decode-words.d.ts.map +1 -0
  103. package/dist/program/decode-words.js +87 -0
  104. package/dist/program/decode-words.js.map +1 -0
  105. package/dist/program/format-program.d.ts +12 -0
  106. package/dist/program/format-program.d.ts.map +1 -0
  107. package/dist/program/format-program.js +37 -0
  108. package/dist/program/format-program.js.map +1 -0
  109. package/dist/public-types.d.ts +253 -0
  110. package/dist/public-types.d.ts.map +1 -0
  111. package/dist/public-types.js +2 -0
  112. package/dist/public-types.js.map +1 -0
  113. package/dist/util.d.ts +3 -0
  114. package/dist/util.d.ts.map +1 -0
  115. package/dist/util.js +11 -0
  116. package/dist/util.js.map +1 -0
  117. package/package.json +82 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ Fidelity changes, which alter the words `assemble` emits for some input without
8
+ changing the API, are listed under **Fidelity**, separately from API changes.
9
+ A fidelity fix is not a breaking change even when it changes output.
10
+
11
+ ## [Unreleased]
12
+
13
+ ### Added
14
+
15
+ - `assemble(source, options)`: PsyQ 4.4 `cc1psx` output to the words ASPSX 2.81
16
+ produces, with sections, relocations carrying field masks, symbols, function
17
+ ranges, small-data classification, and provenance for every word. Input
18
+ problems are diagnostics; only caller mistakes throw.
19
+ - `decode`, `encode`, and `format`: one instruction table covering the MIPS I
20
+ integer set, coprocessor 0 and GTE register moves, `lwc2`/`swc2`, raw GTE
21
+ commands, `rfe`, `break`, and `syscall`.
22
+ - `decodeWords` and `formatProgram`: a program view with labelled branch targets
23
+ that re-assembles to its words.
24
+ - The `psyq-asm` command line: listings, JSON output, and decoding.
25
+ - A demo page, published to GitHub Pages.
26
+ - Tests: unit, property, the ASPSX 2.81 ground truth, ported maspsx tests,
27
+ compiler fixtures, VERIFY probes, three browsers, and packaging with Node,
28
+ TypeScript, Vite, bare browser ESM, and Pages consumers.
29
+ - A differential oracle (`scripts/oracle.mjs`, `npm run test:differential`):
30
+ compares matched decompiled code with its original executable, keeps every
31
+ name in the ignored `tmp/` directory, and commits only an aggregate summary.
32
+ `scripts/oracle-manifest.rb` writes manifests from a symbol list; a synthetic
33
+ pipeline test runs in CI, and the real comparison runs on demand on a
34
+ self-hosted runner.
35
+ - Tooling for the real assembler: `scripts/aspsx-oracle.rb` runs ASPSX 2.81
36
+ under wine, locally or in an x86-64 Docker container
37
+ (`scripts/aspsx-wine.Dockerfile`), first replaying the 15 ground-truth
38
+ fixtures, then recording words for the VERIFY probes and compiler fixtures;
39
+ objects are read with `scripts/psyq-object.mjs`, and tests require any
40
+ recorded words exactly.
41
+
42
+ ### Fidelity
43
+
44
+ - The initial rule set, [docs/ASPSX-2.81.md](docs/ASPSX-2.81.md): all 15 ASPSX
45
+ 2.81 ground-truth fixtures are word-exact.
46
+ - A relocation against a symbol leaves its field 0 and carries the addend in the
47
+ relocation, as the `lwlw` fixture shows for `lw $2,Savemap+2944`.
48
+ - `break` carries two 10-bit codes. A single source code is split as maspsx does;
49
+ the divide traps are `break 7,0` and `break 6,0`.
50
+ - Checked against real ASPSX 2.81, run in Docker by `scripts/aspsx-oracle.rb`:
51
+ all 40 compiler fixtures and all 19 probes match word for word, settling 17
52
+ verification items (VERIFY-1, 6 to 20, and 22). The rest of this list is what
53
+ changed as a result.
54
+ - Relocated fields against local labels hold 0: `j $L9`, `%lo($LC1)`, and
55
+ jump-table `.word $L15` entries no longer carry the label's offset.
56
+ - `.extern sym,size` no longer makes a symbol small data;
57
+ `experimental.externSmallData` now defaults to false.
58
+ - A load made under `.set noreorder` gets no delay nop.
59
+ - The multiply gap: a load between `mflo`/`mfhi` and `mult`/`div` no longer
60
+ cancels the gap nop, and that one nop also serves as the load delay; a
61
+ multi-word expansion between them fills the gap.
62
+ - `b label` assembles as `bgez $0,label`, and `subu rd,rs,-32768` as a single
63
+ `addiu rd,rs,-0x8000`.
64
+
65
+ [Unreleased]: https://github.com/jaysonvirissimo/psyq-asm/commits/main
package/LICENSE ADDED
@@ -0,0 +1,56 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jayson Virissimo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ -------------------------------------------------------------------------------
24
+
25
+ Third-party material
26
+
27
+ The ground-truth fixtures in test/fixtures/aspsx/ (assembly sources and the
28
+ words real ASPSX binaries produce for them), the unit tests in
29
+ test/unit/maspsx/, and the behaviour rules they document in docs/ASPSX-2.81.md
30
+ are derived from maspsx (https://github.com/mkst/maspsx), used under the
31
+ following license:
32
+
33
+ MIT License
34
+
35
+ Copyright (c) 2023 Mark Street
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining a copy
38
+ of this software and associated documentation files (the "Software"), to deal
39
+ in the Software without restriction, including without limitation the rights
40
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
41
+ copies of the Software, and to permit persons to whom the Software is
42
+ furnished to do so, subject to the following conditions:
43
+
44
+ The above copyright notice and this permission notice shall be included in all
45
+ copies or substantial portions of the Software.
46
+
47
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
53
+ SOFTWARE.
54
+
55
+ No Sony software, SDK material, or game data is included in this package or
56
+ this repository.
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # psyq-asm
2
+
3
+ An ASPSX 2.81-compatible assembler and R3000 encoder/decoder in TypeScript, for
4
+ PlayStation 1 matching-decompilation tooling in the browser and Node.js.
5
+
6
+ [psyq-wasm](https://github.com/jaysonvirissimo/psyq-wasm) produces the exact
7
+ assembly text the PsyQ 4.4 compiler (`cc1psx`) emits. That text is not what a
8
+ PlayStation executable contains. Sony's assembler, ASPSX, expands macro
9
+ instructions such as `li` and `div`, inserts `nop`s for delay slots and hardware
10
+ hazards, and addresses small data through `$gp`. `psyq-asm` reproduces that step,
11
+ so a page or a build tool can turn compiler output into the machine words a
12
+ matching decompilation compares against.
13
+
14
+ It is synchronous, has no runtime dependencies, and uses no WebAssembly, no
15
+ worker, and no network. Every word it emits says where it came from: a source
16
+ instruction, a macro expansion, or a nop inserted for a stated reason.
17
+
18
+ ```
19
+ C source ──psyq-wasm──▶ cc1psx assembly ──psyq-asm──▶ R3000 words + relocations + provenance
20
+ ```
21
+
22
+ > **Status:** unreleased. See [the fidelity contract](#the-fidelity-contract) for
23
+ > what is verified.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm install psyq-asm
29
+ ```
30
+
31
+ Node.js 22 or later, or any current browser. ESM only.
32
+
33
+ ## Usage
34
+
35
+ ### Assembling
36
+
37
+ ```js
38
+ import { assemble } from 'psyq-asm';
39
+
40
+ const result = assemble(asmText, { gpSize: 8 });
41
+ if (!result.success) {
42
+ for (const d of result.diagnostics) console.error(`${d.file}:${d.line}: ${d.message}`);
43
+ } else {
44
+ const text = result.object.sections.find((s) => s.name === '.text');
45
+ text.words; // Uint32Array of little-endian words
46
+ text.relocations; // [{ offset, kind: 'HI16', fieldMask: 0xffff, target, fieldValue }, ...]
47
+ text.provenance; // [{ line: 21, kind: 'load-delay-nop', note: '$3 is written by lw and read by sll' }, ...]
48
+ result.object.functions; // [{ name: 'glob', section: '.text', start: 6, end: 11 }, ...]
49
+ }
50
+ ```
51
+
52
+ `source` may be a string or a `Uint8Array` (decoded as UTF-8); CRLF and LF line
53
+ endings give the same result. `gpSize` is the `-G` value of the build being
54
+ reproduced and is required: PsyQ projects use 0 or 8.
55
+
56
+ Straight from psyq-wasm:
57
+
58
+ ```js
59
+ import { createCompiler } from 'psyq-wasm';
60
+ import { assemble } from 'psyq-asm';
61
+
62
+ const compiler = await createCompiler();
63
+ const compiled = await compiler.compilePreprocessed(preprocessedC, {
64
+ gpSize: 8,
65
+ filename: 'codec.i',
66
+ rawFlags: ['-O2', '-g0', '-Wall'],
67
+ });
68
+ if (compiled.success) {
69
+ const result = assemble(compiled.asm, { gpSize: 8, filename: 'codec.s' });
70
+ }
71
+ ```
72
+
73
+ ### Comparing with target words
74
+
75
+ Assembled words hold relocation addends where a linked executable holds
76
+ addresses, so compare the bits outside each relocation's `fieldMask`:
77
+
78
+ ```js
79
+ function matches(section, target) {
80
+ const masks = new Map();
81
+ for (const r of section.relocations)
82
+ masks.set(r.offset, (masks.get(r.offset) ?? 0) | r.fieldMask);
83
+ return [...section.words].every((word, i) => {
84
+ const keep = ~(masks.get(i * 4) ?? 0) >>> 0;
85
+ return (word & keep) >>> 0 === (target[i] & keep) >>> 0;
86
+ });
87
+ }
88
+ ```
89
+
90
+ ### Single instructions
91
+
92
+ ```js
93
+ import { decode, encode, format } from 'psyq-asm';
94
+
95
+ const instruction = decode(0x27bdffa8);
96
+ format(instruction); // 'addiu $sp,$sp,-0x58'
97
+ format(instruction, { registers: 'numeric', hex: false }); // 'addiu $29,$29,-88'
98
+ instruction.reads; // [29]
99
+ encode(instruction); // 0x27bdffa8
100
+ format(decode(0x00000000), { pseudo: true }); // 'nop'
101
+ decode(0xffffffff); // { mnemonic: '.word', word: 0xffffffff, reason: 'unknown opcode 0x3F' }
102
+ ```
103
+
104
+ ### Programs
105
+
106
+ `decodeWords` decodes a run of words and labels the branch targets inside it;
107
+ `formatProgram` prints assembly that `assemble` accepts:
108
+
109
+ ```js
110
+ import { decodeWords, formatProgram } from 'psyq-asm';
111
+
112
+ const program = decodeWords([0x2442ffff, 0x1440fffe, 0x00000000, 0x03e00008]);
113
+ formatProgram(program);
114
+ // .set noreorder
115
+ // L_0:
116
+ // addiu $v0,$v0,-0x1
117
+ // bne $v0,$zero,L_0
118
+ // sll $zero,$zero,0
119
+ // jr $ra
120
+ ```
121
+
122
+ Pass `{ baseAddress: 0x800c56c0 }` to name labels by address and resolve `j`/`jal`
123
+ targets. Re-assembling the text reproduces the words whenever ASPSX would insert
124
+ no nop into them (no loads, `mflo`/`mfhi`, or `mfc2`/`cfc2` followed by a reader).
125
+
126
+ ## The fidelity contract
127
+
128
+ `psyq-asm` emulates one version pair: PsyQ 4.4 `cc1psx` output assembled by
129
+ ASPSX 2.81, invoked as `aspsx -q -G <n>`. The rule set is
130
+ [docs/ASPSX-2.81.md](docs/ASPSX-2.81.md), with the evidence for every rule.
131
+
132
+ What is verified today:
133
+
134
+ - all 15 ASPSX 2.81 ground-truth word lists from
135
+ [maspsx](https://github.com/mkst/maspsx) assemble word for word;
136
+ - the 2.81-applicable unit tests of maspsx are ported and pass;
137
+ - all 40 psyq-wasm compiler fixtures (t01 to t20 at `-G 0` and `-G 8`) and all
138
+ 19 verification probes match, word for word, what real ASPSX 2.81 emits for
139
+ them (recorded with `scripts/aspsx-oracle.rb`), with provenance for every word;
140
+ - property tests: every instruction encodes and decodes symmetrically, decoded
141
+ programs re-assemble to their words, output is deterministic, and no input
142
+ makes `assemble` throw.
143
+
144
+ What is not verified yet: two items, VERIFY-5 (how `.comm` allocations are laid
145
+ out in data sections) and VERIFY-21 (where debugging labels bind next to an
146
+ inserted nop, invisible at `-g0`). Both are listed at the end of the rule set,
147
+ with the 17 items real ASPSX 2.81 has settled. The switches in
148
+ `options.experimental` default to what ASPSX 2.81 does. A differential oracle,
149
+ which compares assembled words with an already-matched decompilation's original
150
+ executable, is in place (`scripts/oracle.mjs`).
151
+
152
+ ## Options
153
+
154
+ | Option | Default | Meaning |
155
+ | ------------------------------ | ----------- | ------------------------------------------------------------------- |
156
+ | `gpSize` | required | The `-G` small-data threshold: a non-negative integer. |
157
+ | `aspsxVersion` | `'2.81'` | The only accepted value. |
158
+ | `partialDivExpansion` | `false` | Reproduce `ASPSX -0`: `div`/`rem` without trap checks. |
159
+ | `filename` | `'input.s'` | The name diagnostics use. A single path segment. |
160
+ | `experimental.externSmallData` | `false` | Address small `.extern` symbols through `$gp`; ASPSX 2.81 does not. |
161
+ | `experimental.copMoveDelayNop` | `true` | A nop after `mfc2`/`cfc2` before a reader, as ASPSX 2.81 inserts. |
162
+
163
+ ## Output model
164
+
165
+ | Field | Contents |
166
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
167
+ | `object.sections[]` | `name`, `kind` (`code`, `data`, `bss`), `bytes`, `size`, `relocations`; code sections also `words` and `provenance`. First-appearance order; `.comm` allocations create `.sbss`/`.bss` last. |
168
+ | `relocations[]` | `offset` (bytes), `kind` (`HI16`, `LO16`, `GPREL16`, `MIPS26`, `WORD32`), `fieldMask`, `target` (a symbol and addend, or a section offset for a local label), and `fieldValue`. |
169
+ | `provenance[]` | One per word: source `line`, `kind` (`instruction`, `macro`, `branch-delay-nop`, `load-delay-nop`, `hilo-gap-nop`, `cop-delay-nop`, `align`, `data`), the `macro` that expanded, and a `note` for inserted nops. |
170
+ | `object.symbols[]` | `name`, `binding` (`global`, `local`, `extern`, `common`), `section`, `offset`, `size`. |
171
+ | `object.functions[]` | From `.ent`/`.end`: `name`, `start` and `end` word indices, `frame`, `mask`, `fmask`. |
172
+ | `object.smallData[]` | The symbols addressed through `$gp`, with the reason. |
173
+
174
+ ## Errors
175
+
176
+ Problems in the assembly are diagnostics in a failed result (`success: false`),
177
+ each with a `code` such as `unknown-mnemonic`, `undefined-label`,
178
+ `immediate-out-of-range`, or `unsupported-syntax`. Nothing that looks like an
179
+ instruction is ever skipped silently. Exceptions are reserved for caller
180
+ mistakes:
181
+
182
+ | Error | `code` | Thrown when |
183
+ | ------------------------- | --------------------- | ------------------------------------------------------------------------------- |
184
+ | `InvalidOptionsError` | `invalid-options` | `options` or `style` is malformed, or `source` is not a string or `Uint8Array`. |
185
+ | `InvalidInstructionError` | `invalid-instruction` | `encode` gets an operand that cannot be encoded, or `decode` a non-integer. |
186
+
187
+ Both extend `PsyqAsmError`, and `code` narrows to the subclass.
188
+
189
+ ## Command line
190
+
191
+ ```sh
192
+ psyq-asm -G 8 codec.s # listing: offset, word, instruction, why
193
+ psyq-asm -G 8 --json codec.json codec.s # the whole object as JSON
194
+ psyq-asm --decode --base 0x800C56C0 words.txt
195
+ ```
196
+
197
+ Decoding reads every eight-digit hexadecimal word in the file, so `dw 0x27BDFFA8`
198
+ lines work as they are.
199
+
200
+ ## Demo
201
+
202
+ `npm run build && npm run serve`, then open <http://127.0.0.1:4173/demo/>: paste
203
+ compiler output and see the words with the reason for each, or paste words and
204
+ see the program. The same page is published to GitHub Pages.
205
+
206
+ ## Stability
207
+
208
+ Semantic versioning covers the exported functions, types, options, and error
209
+ codes. A change to the words emitted for some input is a fidelity fix, not a
210
+ breaking change, and is listed under **Fidelity** in the
211
+ [changelog](CHANGELOG.md). The defaults of `experimental` switches may change in
212
+ any release.
213
+
214
+ ## License and credits
215
+
216
+ MIT; see [LICENSE](LICENSE). The ASPSX ground truth, the ported unit tests, and
217
+ much of the rule set come from [maspsx](https://github.com/mkst/maspsx) by Mark
218
+ Street (MIT). The compiler fixtures come from
219
+ [psyq-wasm](https://github.com/jaysonvirissimo/psyq-wasm) (MIT). No Sony software
220
+ or game data is included.
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * psyq-asm command line.
5
+ *
6
+ * psyq-asm -G <n> [--partial-div] [--json <out.json>] <input.s>
7
+ * psyq-asm --decode [--base <address>] [--numeric] [--pseudo] <words.txt>
8
+ *
9
+ * Assembling prints a listing (offset, word, instruction, and why the word
10
+ * exists) or, with --json, writes the whole object. Diagnostics go to stderr.
11
+ * Decoding reads every 8-digit hexadecimal word in the file (`0x27BDFFA8`,
12
+ * `dw 0x27BDFFA8`, or `27BDFFA8`) and prints assembly that re-assembles.
13
+ *
14
+ * Exit status: 0 on success, 1 when the input has errors, 2 for usage errors.
15
+ */
16
+ import { readFileSync, writeFileSync } from 'node:fs';
17
+ import { basename } from 'node:path';
18
+ import {
19
+ PsyqAsmError,
20
+ assemble,
21
+ decode,
22
+ decodeWords,
23
+ format,
24
+ formatProgram,
25
+ } from '../dist/index.js';
26
+
27
+ const USAGE = `usage: psyq-asm -G <n> [--partial-div] [--json <out.json>] <input.s>
28
+ psyq-asm --decode [--base <address>] [--numeric] [--pseudo] <words.txt>`;
29
+
30
+ function usage(message) {
31
+ console.error(`psyq-asm: ${message}`);
32
+ console.error(USAGE);
33
+ process.exit(2);
34
+ }
35
+
36
+ function parseArgs(argv) {
37
+ const options = { decode: false, partialDiv: false, numeric: false, pseudo: false };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const arg = argv[i];
40
+ const value = () => {
41
+ const next = argv[++i];
42
+ if (next === undefined) usage(`${arg} needs a value.`);
43
+ return next;
44
+ };
45
+ if (arg === '-h' || arg === '--help') {
46
+ console.log(USAGE);
47
+ process.exit(0);
48
+ } else if (arg === '-G') options.gpSize = value();
49
+ else if (/^-G\d+$/.test(arg)) options.gpSize = arg.slice(2);
50
+ else if (arg === '--partial-div') options.partialDiv = true;
51
+ else if (arg === '--json') options.json = value();
52
+ else if (arg === '--decode') options.decode = true;
53
+ else if (arg === '--base') options.base = value();
54
+ else if (arg === '--numeric') options.numeric = true;
55
+ else if (arg === '--pseudo') options.pseudo = true;
56
+ else if (arg.startsWith('-')) usage(`unknown option ${arg}.`);
57
+ else if (options.input === undefined) options.input = arg;
58
+ else usage(`unexpected argument ${arg}.`);
59
+ }
60
+ if (options.input === undefined) usage('no input file.');
61
+ return options;
62
+ }
63
+
64
+ const hex8 = (value) => `0x${(value >>> 0).toString(16).toUpperCase().padStart(8, '0')}`;
65
+
66
+ function toJson(object) {
67
+ return {
68
+ ...object,
69
+ sections: object.sections.map((section) => ({
70
+ name: section.name,
71
+ kind: section.kind,
72
+ size: section.size,
73
+ bytes: Buffer.from(section.bytes).toString('hex'),
74
+ ...(section.words === undefined ? {} : { words: [...section.words].map(hex8) }),
75
+ relocations: section.relocations,
76
+ ...(section.provenance === undefined ? {} : { provenance: section.provenance }),
77
+ })),
78
+ };
79
+ }
80
+
81
+ function printListing(object) {
82
+ for (const section of object.sections) {
83
+ if (section.words === undefined) continue;
84
+ console.log(`${section.name}:`);
85
+ const relocations = new Map(section.relocations.map((r) => [r.offset, r]));
86
+ [...section.words].forEach((word, index) => {
87
+ const origin = section.provenance[index];
88
+ const text = format(decode(word), { pseudo: true });
89
+ const why = [origin.kind, origin.macro, origin.note].filter(Boolean).join(': ');
90
+ console.log(
91
+ ` ${hex8(index * 4)} ${hex8(word)} ${text.padEnd(28)} ; line ${String(origin.line)} ${why}`,
92
+ );
93
+ const relocation = relocations.get(index * 4);
94
+ if (relocation !== undefined) {
95
+ const target =
96
+ relocation.target.kind === 'symbol'
97
+ ? `${relocation.target.name}${relocation.target.addend === 0 ? '' : `+${String(relocation.target.addend)}`}`
98
+ : `${relocation.target.section}+${hex8(relocation.target.offset)}`;
99
+ console.log(` ${' '.repeat(22)}${relocation.kind} ${target}`);
100
+ }
101
+ });
102
+ }
103
+ }
104
+
105
+ function runAssemble(options) {
106
+ if (options.gpSize === undefined) usage('-G <n> is required: PsyQ builds use -G 0 or -G 8.');
107
+ const result = assemble(readFileSync(options.input), {
108
+ gpSize: Number(options.gpSize),
109
+ partialDivExpansion: options.partialDiv,
110
+ filename: basename(options.input),
111
+ });
112
+ for (const d of result.diagnostics) {
113
+ const at = d.column === undefined ? String(d.line) : `${String(d.line)}:${String(d.column)}`;
114
+ console.error(`${d.file}:${at}: ${d.severity}: ${d.message} [${d.code}]`);
115
+ }
116
+ if (!result.success) process.exit(1);
117
+ if (options.json === undefined) printListing(result.object);
118
+ else writeFileSync(options.json, `${JSON.stringify(toJson(result.object), null, 2)}\n`);
119
+ }
120
+
121
+ function runDecode(options) {
122
+ const text = readFileSync(options.input, 'utf8');
123
+ const words = [...text.matchAll(/\b(?:0x)?([0-9a-f]{8})\b/gi)].map((m) =>
124
+ Number.parseInt(m[1], 16),
125
+ );
126
+ const program = decodeWords(
127
+ words,
128
+ options.base === undefined ? {} : { baseAddress: Number(options.base) },
129
+ );
130
+ process.stdout.write(
131
+ formatProgram(program, {
132
+ registers: options.numeric ? 'numeric' : 'abi',
133
+ pseudo: options.pseudo,
134
+ }),
135
+ );
136
+ }
137
+
138
+ const options = parseArgs(process.argv.slice(2));
139
+ try {
140
+ if (options.decode) runDecode(options);
141
+ else runAssemble(options);
142
+ } catch (error) {
143
+ if (error instanceof PsyqAsmError) usage(error.message);
144
+ if (error?.code === 'ENOENT') {
145
+ console.error(`psyq-asm: cannot read ${options.input}.`);
146
+ process.exit(2);
147
+ }
148
+ throw error;
149
+ }
@@ -0,0 +1,10 @@
1
+ import type { AssembleOptions, AssembleResult } from '../public-types.js';
2
+ /**
3
+ * Assemble PsyQ 4.4 `cc1psx` output into the words ASPSX 2.81 produces.
4
+ *
5
+ * Problems in the input are reported as diagnostics in a failed result; this
6
+ * function throws only `InvalidOptionsError`, for a malformed `options` or a
7
+ * `source` that is neither a string nor a `Uint8Array`.
8
+ */
9
+ export declare function assemble(source: string | Uint8Array, options: AssembleOptions): AssembleResult;
10
+ //# sourceMappingURL=assemble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.d.ts","sourceRoot":"","sources":["../../src/asm/assemble.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAS1E;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CA6B9F"}
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { decodeSource, validateAssembleOptions } from '../options.js';
3
+ import { Diagnostics } from './diagnostics.js';
4
+ import { expand } from './expand.js';
5
+ import { insertNops } from './hazards.js';
6
+ import { layout } from './layout.js';
7
+ import { buildObject } from './object.js';
8
+ import { parse } from './parser.js';
9
+ import { classifySmallData, collectSymbols } from './symbols.js';
10
+ /**
11
+ * Assemble PsyQ 4.4 `cc1psx` output into the words ASPSX 2.81 produces.
12
+ *
13
+ * Problems in the input are reported as diagnostics in a failed result; this
14
+ * function throws only `InvalidOptionsError`, for a malformed `options` or a
15
+ * `source` that is neither a string nor a `Uint8Array`.
16
+ */
17
+ export function assemble(source, options) {
18
+ const normalized = validateAssembleOptions(options);
19
+ const text = decodeSource(source);
20
+ const diagnostics = new Diagnostics(normalized.filename);
21
+ const statements = parse(text, diagnostics);
22
+ const symbols = collectSymbols(statements, diagnostics);
23
+ const smallData = classifySmallData(symbols, normalized.gpSize, normalized.experimental.externSmallData);
24
+ const items = expand(statements, {
25
+ gpSize: normalized.gpSize,
26
+ smallData,
27
+ partialDivExpansion: normalized.partialDivExpansion,
28
+ diagnostics,
29
+ });
30
+ const withNops = insertNops(items, {
31
+ copMoveDelayNop: normalized.experimental.copMoveDelayNop,
32
+ });
33
+ const laidOut = layout(withNops, symbols, normalized.gpSize, diagnostics);
34
+ if (diagnostics.hasErrors)
35
+ return { success: false, diagnostics: diagnostics.sorted() };
36
+ return {
37
+ success: true,
38
+ object: buildObject(normalized, laidOut, symbols, smallData),
39
+ diagnostics: diagnostics.sorted(),
40
+ };
41
+ }
42
+ //# sourceMappingURL=assemble.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble.js","sourceRoot":"","sources":["../../src/asm/assemble.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAEtE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,MAA2B,EAAE,OAAwB;IAC5E,MAAM,UAAU,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAEzD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,iBAAiB,CACjC,OAAO,EACP,UAAU,CAAC,MAAM,EACjB,UAAU,CAAC,YAAY,CAAC,eAAe,CACxC,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE;QAC/B,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,SAAS;QACT,mBAAmB,EAAE,UAAU,CAAC,mBAAmB;QACnD,WAAW;KACZ,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,EAAE;QACjC,eAAe,EAAE,UAAU,CAAC,YAAY,CAAC,eAAe;KACzD,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAE1E,IAAI,WAAW,CAAC,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;IACxF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,WAAW,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC;QAC5D,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE;KAClC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Diagnostic, DiagnosticCode } from '../public-types.js';
2
+ /** Where a diagnostic points: any statement or item carries these. */
3
+ export interface Position {
4
+ readonly line: number;
5
+ readonly column?: number;
6
+ }
7
+ /** Collects the diagnostics of one `assemble` call. */
8
+ export declare class Diagnostics {
9
+ private readonly file;
10
+ private readonly entries;
11
+ private readonly onceKeys;
12
+ constructor(file: string);
13
+ error(at: Position, code: DiagnosticCode, message: string): void;
14
+ warning(at: Position, code: DiagnosticCode, message: string): void;
15
+ /** A warning reported only for the first occurrence of `key` in the file. */
16
+ warnOnce(key: string, at: Position, code: DiagnosticCode, message: string): void;
17
+ get hasErrors(): boolean;
18
+ /** All diagnostics, sorted by line (stable within a line). */
19
+ sorted(): readonly Diagnostic[];
20
+ private push;
21
+ }
22
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/asm/diagnostics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErE,sEAAsE;AACtE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,uDAAuD;AACvD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,IAAI,EAAE,MAAM;IAIxB,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIhE,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIlE,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAMhF,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,8DAA8D;IAC9D,MAAM,IAAI,SAAS,UAAU,EAAE;IAI/B,OAAO,CAAC,IAAI;CAeb"}
@@ -0,0 +1,40 @@
1
+ /** Collects the diagnostics of one `assemble` call. */
2
+ export class Diagnostics {
3
+ file;
4
+ entries = [];
5
+ onceKeys = new Set();
6
+ constructor(file) {
7
+ this.file = file;
8
+ }
9
+ error(at, code, message) {
10
+ this.push('error', at, code, message);
11
+ }
12
+ warning(at, code, message) {
13
+ this.push('warning', at, code, message);
14
+ }
15
+ /** A warning reported only for the first occurrence of `key` in the file. */
16
+ warnOnce(key, at, code, message) {
17
+ if (this.onceKeys.has(key))
18
+ return;
19
+ this.onceKeys.add(key);
20
+ this.warning(at, code, message);
21
+ }
22
+ get hasErrors() {
23
+ return this.entries.some((d) => d.severity === 'error');
24
+ }
25
+ /** All diagnostics, sorted by line (stable within a line). */
26
+ sorted() {
27
+ return [...this.entries].sort((a, b) => a.line - b.line);
28
+ }
29
+ push(severity, at, code, message) {
30
+ this.entries.push({
31
+ severity,
32
+ file: this.file,
33
+ line: at.line,
34
+ ...(at.column === undefined ? {} : { column: at.column }),
35
+ code,
36
+ message,
37
+ });
38
+ }
39
+ }
40
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/asm/diagnostics.ts"],"names":[],"mappings":"AASA,uDAAuD;AACvD,MAAM,OAAO,WAAW;IACL,IAAI,CAAS;IACb,OAAO,GAAiB,EAAE,CAAC;IAC3B,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAE9C,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,EAAY,EAAE,IAAoB,EAAE,OAAe;QACvD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,CAAC,EAAY,EAAE,IAAoB,EAAE,OAAe;QACzD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,6EAA6E;IAC7E,QAAQ,CAAC,GAAW,EAAE,EAAY,EAAE,IAAoB,EAAE,OAAe;QACvE,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC1D,CAAC;IAED,8DAA8D;IAC9D,MAAM;QACJ,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;IAEO,IAAI,CACV,QAAgC,EAChC,EAAY,EACZ,IAAoB,EACpB,OAAe;QAEf,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,QAAQ;YACR,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,GAAG,CAAC,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC;YACzD,IAAI;YACJ,OAAO;SACR,CAAC,CAAC;IACL,CAAC;CACF"}