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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +56 -0
- package/README.md +220 -0
- package/bin/psyq-asm.mjs +149 -0
- package/dist/asm/assemble.d.ts +10 -0
- package/dist/asm/assemble.d.ts.map +1 -0
- package/dist/asm/assemble.js +42 -0
- package/dist/asm/assemble.js.map +1 -0
- package/dist/asm/diagnostics.d.ts +22 -0
- package/dist/asm/diagnostics.d.ts.map +1 -0
- package/dist/asm/diagnostics.js +40 -0
- package/dist/asm/diagnostics.js.map +1 -0
- package/dist/asm/directives.d.ts +67 -0
- package/dist/asm/directives.d.ts.map +1 -0
- package/dist/asm/directives.js +254 -0
- package/dist/asm/directives.js.map +1 -0
- package/dist/asm/expand.d.ts +21 -0
- package/dist/asm/expand.d.ts.map +1 -0
- package/dist/asm/expand.js +540 -0
- package/dist/asm/expand.js.map +1 -0
- package/dist/asm/hazards.d.ts +10 -0
- package/dist/asm/hazards.d.ts.map +1 -0
- package/dist/asm/hazards.js +196 -0
- package/dist/asm/hazards.js.map +1 -0
- package/dist/asm/layout.d.ts +17 -0
- package/dist/asm/layout.d.ts.map +1 -0
- package/dist/asm/layout.js +257 -0
- package/dist/asm/layout.js.map +1 -0
- package/dist/asm/lexer.d.ts +23 -0
- package/dist/asm/lexer.d.ts.map +1 -0
- package/dist/asm/lexer.js +81 -0
- package/dist/asm/lexer.js.map +1 -0
- package/dist/asm/object.d.ts +6 -0
- package/dist/asm/object.d.ts.map +1 -0
- package/dist/asm/object.js +46 -0
- package/dist/asm/object.js.map +1 -0
- package/dist/asm/operands.d.ts +49 -0
- package/dist/asm/operands.d.ts.map +1 -0
- package/dist/asm/operands.js +114 -0
- package/dist/asm/operands.js.map +1 -0
- package/dist/asm/parser.d.ts +28 -0
- package/dist/asm/parser.d.ts.map +1 -0
- package/dist/asm/parser.js +60 -0
- package/dist/asm/parser.js.map +1 -0
- package/dist/asm/relocations.d.ts +42 -0
- package/dist/asm/relocations.d.ts.map +1 -0
- package/dist/asm/relocations.js +214 -0
- package/dist/asm/relocations.js.map +1 -0
- package/dist/asm/sections.d.ts +23 -0
- package/dist/asm/sections.d.ts.map +1 -0
- package/dist/asm/sections.js +81 -0
- package/dist/asm/sections.js.map +1 -0
- package/dist/asm/stream.d.ts +77 -0
- package/dist/asm/stream.d.ts.map +1 -0
- package/dist/asm/stream.js +2 -0
- package/dist/asm/stream.js.map +1 -0
- package/dist/asm/symbols.d.ts +42 -0
- package/dist/asm/symbols.d.ts.map +1 -0
- package/dist/asm/symbols.js +122 -0
- package/dist/asm/symbols.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/isa/decode.d.ts +8 -0
- package/dist/isa/decode.d.ts.map +1 -0
- package/dist/isa/decode.js +92 -0
- package/dist/isa/decode.js.map +1 -0
- package/dist/isa/encode.d.ts +9 -0
- package/dist/isa/encode.d.ts.map +1 -0
- package/dist/isa/encode.js +124 -0
- package/dist/isa/encode.js.map +1 -0
- package/dist/isa/fields.d.ts +27 -0
- package/dist/isa/fields.d.ts.map +1 -0
- package/dist/isa/fields.js +49 -0
- package/dist/isa/fields.js.map +1 -0
- package/dist/isa/format.d.ts +14 -0
- package/dist/isa/format.d.ts.map +1 -0
- package/dist/isa/format.js +98 -0
- package/dist/isa/format.js.map +1 -0
- package/dist/isa/instruction.d.ts +15 -0
- package/dist/isa/instruction.d.ts.map +1 -0
- package/dist/isa/instruction.js +47 -0
- package/dist/isa/instruction.js.map +1 -0
- package/dist/isa/registers.d.ts +8 -0
- package/dist/isa/registers.d.ts.map +1 -0
- package/dist/isa/registers.js +55 -0
- package/dist/isa/registers.js.map +1 -0
- package/dist/isa/table.d.ts +179 -0
- package/dist/isa/table.d.ts.map +1 -0
- package/dist/isa/table.js +128 -0
- package/dist/isa/table.js.map +1 -0
- package/dist/options.d.ts +29 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +0 -0
- package/dist/options.js.map +1 -0
- package/dist/program/decode-words.d.ts +9 -0
- package/dist/program/decode-words.d.ts.map +1 -0
- package/dist/program/decode-words.js +87 -0
- package/dist/program/decode-words.js.map +1 -0
- package/dist/program/format-program.d.ts +12 -0
- package/dist/program/format-program.d.ts.map +1 -0
- package/dist/program/format-program.js +37 -0
- package/dist/program/format-program.js.map +1 -0
- package/dist/public-types.d.ts +253 -0
- package/dist/public-types.d.ts.map +1 -0
- package/dist/public-types.js +2 -0
- package/dist/public-types.js.map +1 -0
- package/dist/util.d.ts +3 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +11 -0
- package/dist/util.js.map +1 -0
- 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.
|
package/bin/psyq-asm.mjs
ADDED
|
@@ -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"}
|