romdevtools 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +18 -11
- package/CHANGELOG.md +94 -0
- package/README.md +1 -1
- package/examples/atari2600/main.asm +1 -1
- package/examples/atari2600/templates/default.asm +1 -1
- package/examples/atari2600/templates/paddle.asm +59 -47
- package/examples/atari7800/main.c +1 -1
- package/examples/atari7800/templates/default.c +1 -1
- package/examples/atari7800/templates/music_demo.c +1 -1
- package/examples/c64/main.c +1 -1
- package/examples/c64/templates/platformer.c +2 -2
- package/examples/c64/templates/puzzle.c +1 -1
- package/examples/c64/templates/racing.c +3 -3
- package/examples/c64/templates/shmup.c +6 -5
- package/examples/c64/templates/sports.c +4 -4
- package/examples/gb/main.asm +1 -1
- package/examples/gb/main.c +1 -1
- package/examples/gb/templates/puzzle.c +1 -1
- package/examples/gb/templates/racing.c +1 -1
- package/examples/gb/templates/shmup.c +1 -1
- package/examples/gba/templates/gba_hello.c +1 -1
- package/examples/gba/templates/maxmod_demo.c +1 -1
- package/examples/gba/templates/puzzle.c +17 -3
- package/examples/gba/templates/racing.c +16 -2
- package/examples/gba/templates/shmup.c +23 -4
- package/examples/gba/templates/tonc_hello.c +6 -4
- package/examples/gbc/main.asm +1 -1
- package/examples/gbc/templates/puzzle.c +1 -1
- package/examples/gbc/templates/racing.c +1 -1
- package/examples/gbc/templates/shmup.c +1 -1
- package/examples/genesis/main.s +1 -1
- package/examples/genesis/templates/puzzle.c +1 -1
- package/examples/genesis/templates/racing.c +45 -1
- package/examples/genesis/templates/shmup.c +12 -3
- package/examples/genesis/templates/shmup_2p.c +2 -2
- package/examples/genesis/templates/sports.c +39 -0
- package/examples/gg/templates/hello_sprite.c +38 -23
- package/examples/gg/templates/music_demo.c +11 -8
- package/examples/gg/templates/platformer.c +37 -15
- package/examples/gg/templates/racing.c +25 -12
- package/examples/gg/templates/shmup.c +12 -6
- package/examples/gg/templates/sports.c +30 -16
- package/examples/gg/templates/tile_engine.c +24 -10
- package/examples/lynx/templates/platformer.c +7 -1
- package/examples/lynx/templates/puzzle.c +8 -2
- package/examples/lynx/templates/racing.c +7 -1
- package/examples/lynx/templates/sports.c +7 -1
- package/examples/nes/main.c +2 -2
- package/examples/nes/space-shooter/nes_runtime.h +1 -1
- package/examples/nes/templates/default.c +4 -1
- package/examples/nes/templates/racing.c +50 -1
- package/examples/pce/main.c +1 -1
- package/examples/sms/templates/hello_sprite.c +1 -1
- package/examples/sms/templates/music_demo.c +1 -1
- package/examples/sms/templates/puzzle.c +1 -1
- package/examples/sms/templates/racing.c +1 -1
- package/examples/sms/templates/shmup.c +1 -1
- package/examples/sms/templates/shmup_2p.c +2 -2
- package/examples/snes/main.asm +1 -1
- package/examples/snes/templates/c-hello-data.asm +309 -14
- package/examples/snes/templates/c-hello.c +13 -2
- package/examples/snes/templates/default.c +1 -1
- package/examples/snes/templates/hello_sprite-data.asm +300 -2
- package/examples/snes/templates/hello_sprite.c +10 -1
- package/examples/snes/templates/music_demo-data.asm +300 -2
- package/examples/snes/templates/music_demo.c +10 -1
- package/examples/snes/templates/platformer-data.asm +300 -2
- package/examples/snes/templates/platformer.c +10 -1
- package/examples/snes/templates/puzzle-data.asm +300 -2
- package/examples/snes/templates/puzzle.c +11 -1
- package/examples/snes/templates/racing-data.asm +300 -2
- package/examples/snes/templates/racing.c +40 -4
- package/examples/snes/templates/shmup-data.asm +299 -6
- package/examples/snes/templates/shmup.c +11 -7
- package/examples/snes/templates/sports-data.asm +300 -2
- package/examples/snes/templates/sports.c +40 -5
- package/package.json +1 -1
- package/src/http/skill-doc.js +1 -1
- package/src/http/tool-registry.js +1 -1
- package/src/mcp/tools/index.js +4 -4
- package/src/mcp/tools/project.js +33 -22
- package/src/mcp/tools/toolchain.js +196 -20
- package/src/observer/livestream.html +34 -4
- package/src/platforms/gg/MENTAL_MODEL.md +14 -13
- package/src/platforms/gg/lib/c/vdp_init.c +10 -8
- package/src/platforms/msx/MENTAL_MODEL.md +1 -1
- package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
- package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
- package/src/platforms/pce/MENTAL_MODEL.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +1 -0
- package/src/platforms/pce/lib/c/pce_video.c +26 -0
- package/src/platforms/sms/MENTAL_MODEL.md +12 -12
- package/src/platforms/sms/lib/c/vdp_init.c +10 -8
- package/src/platforms/sms/lib/vdp_init.s +1 -1
- package/src/toolchains/cc65/cc65.js +8 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
- package/src/toolchains/gba-c/gba-c.js +6 -1
- package/src/toolchains/genesis-c/README.md +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +10 -2
- package/src/toolchains/parse-errors.js +67 -5
- package/src/toolchains/sdcc/preflight-lint.js +47 -7
- package/src/toolchains/snes-c/snes-c.js +3 -7
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Writes the 11 mode-4 registers to a sane baseline:
|
|
4
4
|
* display OFF, vblank IRQ off, 192-line mode 4, name table at $3800,
|
|
5
5
|
* BG tile data at $0000, sprite attr table at $3F00, sprite tile data
|
|
6
|
-
* at $
|
|
6
|
+
* at $2000 (R6=0xFF → SA13 set → tiles read from $2000-$3FFF). Call
|
|
7
7
|
* once after reset before uploading palette/tiles/map.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sprite tiles
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Sprite-tile base: R6 bit 2 (SA13) selects $0000 (clear) vs $2000 (set).
|
|
10
|
+
* We default to R6=0xFF ($2000) because EVERY bundled scaffold uploads its
|
|
11
|
+
* sprite tiles to $2000 (sms_load_tiles(0x2000, ...)) — so the baseline must
|
|
12
|
+
* match what consumers do, or sprites read from the empty/BG bank and render
|
|
13
|
+
* invisible. (Many SMS references say "R6=0xFB → $2000", which is backwards:
|
|
14
|
+
* 0xFB has SA13 CLEAR = $0000.) If you instead keep sprite tiles in the BG
|
|
15
|
+
* bank at $0000, set R6=0xFB. sprites({op:'inspect'}) → spriteTileDataBase
|
|
16
|
+
* shows the address the VDP is actually reading from.
|
|
15
17
|
*
|
|
16
18
|
* After loading assets, enable display by re-writing R1 with bit 6 set:
|
|
17
19
|
* sms_vdp_display_on();
|
|
@@ -33,7 +35,7 @@ void sms_vdp_init(void) {
|
|
|
33
35
|
0xFF, /* R3: color table (ignored in M4) */
|
|
34
36
|
0xFF, /* R4: BG tile data at $0000 */
|
|
35
37
|
0xFF, /* R5: sprite attr table at $3F00 */
|
|
36
|
-
|
|
38
|
+
0xFF, /* R6: sprite tile data at $2000 (SA13 set; scaffolds upload here) */
|
|
37
39
|
0x00, /* R7: border = sprite palette entry 0 */
|
|
38
40
|
0x00, /* R8: BG X scroll */
|
|
39
41
|
0x00, /* R9: BG Y scroll */
|
|
@@ -40,7 +40,7 @@ _vdp_init_regs:
|
|
|
40
40
|
.db $FF ; R3: color table — M4 ignores
|
|
41
41
|
.db $FF ; R4: BG tile data — M4: bit 2 selects $0000 vs $2000
|
|
42
42
|
.db $FF ; R5: sprite attr table base ($3F00 = $7E << 7)
|
|
43
|
-
.db $
|
|
43
|
+
.db $FF ; R6: sprite tile data at $2000 (SA13 set; scaffolds upload here)
|
|
44
44
|
.db $00 ; R7: border color = sprite palette entry 0
|
|
45
45
|
.db $00 ; R8: BG X scroll
|
|
46
46
|
.db $00 ; R9: BG Y scroll
|
|
@@ -257,7 +257,14 @@ export function parseRamUsage(mapText) {
|
|
|
257
257
|
* @param {string} [args.linkerConfig] custom ld65 .cfg (overrides per-target default)
|
|
258
258
|
*/
|
|
259
259
|
export async function buildC(args) {
|
|
260
|
-
|
|
260
|
+
// Enable cc65's high-value warnings so the agent SEES real bugs (parsed into
|
|
261
|
+
// structured issues[]). These are the valid cc65 -W names that catch actual
|
|
262
|
+
// mistakes; unused-param is left off (scaffold callbacks commonly ignore
|
|
263
|
+
// params). cc65's warning set is thin to begin with, but errors always surface
|
|
264
|
+
// and these are pure upside. (NOTE: cc65 errors on an unknown -W name, so this
|
|
265
|
+
// list is verified valid against the bundled cc65.)
|
|
266
|
+
const ccWarn = ["-W", "unused-var,unused-func,unused-label,const-comparison,struct-param,pointer-sign"];
|
|
267
|
+
const ccOpts = args.debug ? ["-g", ...ccWarn] : [...ccWarn];
|
|
261
268
|
const caOpts = args.debug ? ["-g"] : [];
|
|
262
269
|
const sources = normalizeSources(args, "main.c");
|
|
263
270
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# runtime. There is no CHARS segment.
|
|
13
13
|
#
|
|
14
14
|
# To use this preset:
|
|
15
|
-
# 1. linkerConfig: "chr-ram" on
|
|
15
|
+
# 1. linkerConfig: "chr-ram" on build({output:'rom'})
|
|
16
16
|
# 2. Supply your own crt0/header source that writes the 16-byte iNES
|
|
17
17
|
# header with byte 5 = 0. Easiest: paste the snippet
|
|
18
18
|
# getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# runtime. There is no CHARS segment.
|
|
13
13
|
#
|
|
14
14
|
# To use this preset:
|
|
15
|
-
# 1. linkerConfig: "chr-ram" on
|
|
15
|
+
# 1. linkerConfig: "chr-ram" on build({output:'rom'})
|
|
16
16
|
# 2. Supply your own crt0/header source that writes the 16-byte iNES
|
|
17
17
|
# header with byte 5 = 0. Easiest: paste the snippet
|
|
18
18
|
# getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
|
|
@@ -91,7 +91,7 @@ _exit: jsr donelib
|
|
|
91
91
|
; rti
|
|
92
92
|
;
|
|
93
93
|
; ; then DELETE the `nmi: rti` line below from this crt0 (or load a
|
|
94
|
-
; ; modified crt0 via your own
|
|
94
|
+
; ; modified crt0 via your own build({output:'rom'}) sources entry).
|
|
95
95
|
|
|
96
96
|
.segment "STARTUP"
|
|
97
97
|
|
|
@@ -92,7 +92,12 @@ export async function buildGbaC(args) {
|
|
|
92
92
|
// per-symbol `.bss.<name>`/`.data.<name>` line — that's what lets
|
|
93
93
|
// symbols({op:'resolve'}) turn a static C global's name into an address on GBA
|
|
94
94
|
// (same as SGDK does for Genesis). Pure metadata; no codegen change to what's kept.
|
|
95
|
-
|
|
95
|
+
// -Wall -Wextra so the agent SEES warnings (unused vars, implicit decls,
|
|
96
|
+
// sign-compare, etc.) — they're parsed into structured issues[]. Without these
|
|
97
|
+
// gcc is silent and agents build blind. -Wno-unused-parameter keeps the common
|
|
98
|
+
// intentional `(void)`-style scaffold params from being noise. Applied to USER
|
|
99
|
+
// .c only (the libtonc/maxmod SDK is a prebuilt seed, not recompiled here).
|
|
100
|
+
const cc1Options = args.cc1Options ?? ["-O2", "-mthumb", "-ffunction-sections", "-fdata-sections", "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
96
101
|
const sources = normalizeGbaSources(args);
|
|
97
102
|
const binaryIncludes = args.binaryIncludes ?? {};
|
|
98
103
|
|
|
@@ -23,7 +23,7 @@ once the WASM artifacts ship. Pipeline shape:
|
|
|
23
23
|
- **WASM port of cc1/as/ld (stage 2)**: pending.
|
|
24
24
|
- **SGDK native build against the cross-toolchain (stage 3)**: pending.
|
|
25
25
|
- **JS driver `buildGenesisC()`**: pending.
|
|
26
|
-
- **
|
|
26
|
+
- **build({output:'rom'}) wiring**: pending.
|
|
27
27
|
|
|
28
28
|
## Why a 3-stage build
|
|
29
29
|
|
|
@@ -264,6 +264,12 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
264
264
|
"-fdata-sections",
|
|
265
265
|
...cc1Options,
|
|
266
266
|
];
|
|
267
|
+
// USER source gets warnings on so the agent SEES its bugs (unused vars,
|
|
268
|
+
// implicit decls, …) parsed into structured issues[]. The SGDK runtime is
|
|
269
|
+
// compiled WITHOUT these (sgdkCc1Options) — we can't fix SDK warnings and they'd
|
|
270
|
+
// bury the agent's own. -Wno-unused-parameter avoids the common `(void)hard`
|
|
271
|
+
// scaffold-param noise.
|
|
272
|
+
const userCc1Options = [...sgdkCc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
267
273
|
|
|
268
274
|
// ── Stage A: gather SGDK headers (visible to tcc via tcc-style flat mount) ──
|
|
269
275
|
// cc1's -iquote /work picks up sibling files mounted alongside main.c.
|
|
@@ -280,7 +286,7 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
280
286
|
const cc = await runCc1m68k({
|
|
281
287
|
source: sources[cName],
|
|
282
288
|
headers: tccHeaders,
|
|
283
|
-
options:
|
|
289
|
+
options: userCc1Options,
|
|
284
290
|
});
|
|
285
291
|
log += `--- cc1 (${cName}) ---\n` + (cc.log || "(ok)") + "\n";
|
|
286
292
|
if (cc.exitCode !== 0 || !cc.asmSource) {
|
|
@@ -469,12 +475,14 @@ async function buildMinimal(args) {
|
|
|
469
475
|
// ── Stage 1: compile each .c file via cc1 → .s ─────────────────
|
|
470
476
|
/** @type {Record<string, Uint8Array>} */
|
|
471
477
|
const userObjs = {};
|
|
478
|
+
// User .c gets warnings on (minimal path has no SDK to flood). See buildWithSgdk.
|
|
479
|
+
const userCc1Options = [...cc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
472
480
|
const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
|
|
473
481
|
for (const cName of cFiles) {
|
|
474
482
|
const cc = await runCc1m68k({
|
|
475
483
|
source: sources[cName],
|
|
476
484
|
headers,
|
|
477
|
-
options:
|
|
485
|
+
options: userCc1Options,
|
|
478
486
|
});
|
|
479
487
|
log += `--- cc1 (${cName}) ---\n` + (cc.log || "(ok)") + "\n";
|
|
480
488
|
if (cc.exitCode !== 0 || !cc.asmSource) {
|
|
@@ -41,10 +41,12 @@ export function parseBuildLog(log) {
|
|
|
41
41
|
} else if (/^vasm/.test(baseStage)) {
|
|
42
42
|
issues.push(...parseVasm(text));
|
|
43
43
|
} else if (/^sdcc$|^sdasz80$|^sdasgb$|^sdld$|^mcpp$/.test(baseStage)) {
|
|
44
|
-
// SDCC family: sdcc / sdasz80 / sdasgb / sdld / mcpp
|
|
45
|
-
// `file:line:
|
|
46
|
-
//
|
|
44
|
+
// SDCC family: sdcc / sdasz80 / sdasgb / sdld / mcpp. Some diagnostics use
|
|
45
|
+
// the cc65-style `file:line: Error: msg`; SDCC's frontend ALSO emits a
|
|
46
|
+
// keyword-less form — `main.c:2: syntax error: token -> ';' ; column 44`
|
|
47
|
+
// and `main.c:N: warning NNN: msg` — which parseCc65Like misses. Run both.
|
|
47
48
|
issues.push(...parseCc65Like(text, baseStage));
|
|
49
|
+
issues.push(...parseSdcc(text, baseStage));
|
|
48
50
|
} else if (/^wla|^wlalink|^wladx/.test(baseStage)) {
|
|
49
51
|
// SNES C path: wla-65816 assembler + wlalink linker. wlalink floods a
|
|
50
52
|
// symbol-table dump on failure — parseWla extracts just the diagnostics.
|
|
@@ -60,11 +62,18 @@ export function parseBuildLog(log) {
|
|
|
60
62
|
// Tag everything with the (possibly empty) actual stage name so an
|
|
61
63
|
// assembler error doesn't mistakenly report as "asar" on a non-SNES
|
|
62
64
|
// build.
|
|
65
|
+
// Try EVERY parser — some toolchains (vasm genesis-asm) emit no
|
|
66
|
+
// "--- stage ---" marker, so the whole log lands here unnamed; if we skip
|
|
67
|
+
// a parser the error is silently swallowed (issues[] empty on a real
|
|
68
|
+
// failure). Include vasm + sdcc + wla, which the old fallback omitted.
|
|
63
69
|
const tag = baseStage || "unknown";
|
|
64
70
|
issues.push(...parseCc65Like(text, tag));
|
|
71
|
+
issues.push(...parseSdcc(text, tag));
|
|
65
72
|
issues.push(...parseDasm(text));
|
|
66
73
|
issues.push(...parseAsar(text, tag));
|
|
67
74
|
issues.push(...parseRgbds(text, tag));
|
|
75
|
+
issues.push(...parseVasm(text));
|
|
76
|
+
issues.push(...parseWla(text, tag));
|
|
68
77
|
issues.push(...parseGnuToolchain(text, tag));
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -122,6 +131,57 @@ function parseCc65Like(text, stage) {
|
|
|
122
131
|
return out;
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
// SDCC's keyword-less diagnostics that parseCc65Like (which requires an explicit
|
|
135
|
+
// "Error:"/"Warning:" word) doesn't catch:
|
|
136
|
+
// /work/main.c:2: syntax error: token -> ';' ; column 44
|
|
137
|
+
// /work/main.c:7: warning 112: function 'foo' implicit declaration
|
|
138
|
+
// /work/main.c:9: error 20: undefined identifier 'x'
|
|
139
|
+
// We classify by the leading word after `file:line:` (syntax error / error / warning).
|
|
140
|
+
function parseSdcc(text, stage) {
|
|
141
|
+
const out = [];
|
|
142
|
+
const re = /^(?<file>[^\n:]+):(?<line>\d+):\s*(?<kind>syntax error|error(?:\s+\d+)?|warning(?:\s+\d+)?):?\s*(?<msg>.*)$/gm;
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = re.exec(text))) {
|
|
145
|
+
const kind = m.groups.kind.toLowerCase();
|
|
146
|
+
// Skip the forms parseCc65Like already caught ("Error:" capitalized w/ colon)
|
|
147
|
+
// — this regex is case-insensitive on `error`/`warning`, but parseCc65Like
|
|
148
|
+
// only matches when a colon immediately follows the keyword AND it's
|
|
149
|
+
// capitalized; SDCC's lowercase keyword-less form is what we add here.
|
|
150
|
+
const severity = kind.startsWith("warning") ? "warning"
|
|
151
|
+
: (kind.startsWith("error") || kind === "syntax error") ? "error" : "info";
|
|
152
|
+
const message = kind === "syntax error"
|
|
153
|
+
? ("syntax error: " + m.groups.msg).trim().replace(/\s*;\s*$/, "")
|
|
154
|
+
: m.groups.msg.trim();
|
|
155
|
+
out.push({
|
|
156
|
+
severity,
|
|
157
|
+
file: m.groups.file,
|
|
158
|
+
line: parseInt(m.groups.line, 10),
|
|
159
|
+
message: message.replace(/\x1b\[[0-9;]*m/g, ""),
|
|
160
|
+
stage,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// sdld/ASlink linker diagnostics have NO file:line — they reference a symbol +
|
|
164
|
+
// module. The most common is an undefined symbol (a call to a function that was
|
|
165
|
+
// never defined/linked). Without parsing these the agent sees "build failed"
|
|
166
|
+
// with no reason in issues[] (the error lived only in the raw log).
|
|
167
|
+
// ?ASlink-Warning-Undefined Global '_foo' referenced by module '_main'
|
|
168
|
+
// ?ASlink-Error-...
|
|
169
|
+
const linkRe = /^\?ASlink-(?<sev>Warning|Error)-(?<msg>.+)$/gm;
|
|
170
|
+
let lm;
|
|
171
|
+
while ((lm = linkRe.exec(text))) {
|
|
172
|
+
const msg = lm.groups.msg.trim();
|
|
173
|
+
// An "Undefined Global" is effectively an error even though ASlink labels it
|
|
174
|
+
// a warning — the ROM won't run. Promote it so the agent treats it as fatal.
|
|
175
|
+
const isUndef = /undefined\s+global/i.test(msg);
|
|
176
|
+
out.push({
|
|
177
|
+
severity: lm.groups.sev === "Error" || isUndef ? "error" : "warning",
|
|
178
|
+
message: "linker: " + msg,
|
|
179
|
+
stage: "sdld",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
125
185
|
// dasm example:
|
|
126
186
|
// main.asm (1): error: Unknown Mnemonic 'is'.
|
|
127
187
|
function parseDasm(text) {
|
|
@@ -248,13 +308,15 @@ function parseWla(text, stage = "wla") {
|
|
|
248
308
|
// vasm example:
|
|
249
309
|
// error 22 in line 5 of "/work/main.s": ...
|
|
250
310
|
// warning 1003 in line 8 of "main.s": ...
|
|
311
|
+
// fatal error 13 in line 1 of "/work/main.s": could not open <x.bin> for input
|
|
312
|
+
// (← a MISSING incbin asset: the #1 thing an agent forgets to pass)
|
|
251
313
|
function parseVasm(text) {
|
|
252
314
|
const out = [];
|
|
253
|
-
const re = /^(?<sev>error|warning)\s+\d+\s+in\s+line\s+(?<line>\d+)\s+of\s+"(?<file>[^"]+)":\s*(?<msg>.+)$/gm;
|
|
315
|
+
const re = /^(?<sev>fatal error|error|warning)\s+\d+\s+in\s+line\s+(?<line>\d+)\s+of\s+"(?<file>[^"]+)":\s*(?<msg>.+)$/gm;
|
|
254
316
|
let m;
|
|
255
317
|
while ((m = re.exec(text))) {
|
|
256
318
|
out.push({
|
|
257
|
-
severity: m.groups.sev,
|
|
319
|
+
severity: m.groups.sev === "warning" ? "warning" : "error",
|
|
258
320
|
file: m.groups.file,
|
|
259
321
|
line: parseInt(m.groups.line, 10),
|
|
260
322
|
message: m.groups.msg.trim(),
|
|
@@ -68,13 +68,53 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
|
|
|
68
68
|
// CONSERVATIVE: only flag when we can SEE the counter declared u8 and
|
|
69
69
|
// the bound is a constant we can evaluate — never guess.
|
|
70
70
|
{
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// Build a SCOPE-AWARE map of where each name is declared and at what
|
|
72
|
+
// width. A name like `i` is commonly re-declared in several functions
|
|
73
|
+
// — some as uint8_t, some as uint16_t. A flat "is this name ever u8"
|
|
74
|
+
// set wrongly flags the uint16_t loop just because a DIFFERENT
|
|
75
|
+
// function declared its own `i` as uint8_t (the SMS/GG default
|
|
76
|
+
// scaffold false-positive). Instead we record EVERY declaration's
|
|
77
|
+
// line + width, then for each loop consult the nearest declaration of
|
|
78
|
+
// the counter that appears ABOVE the loop — i.e. the one actually in
|
|
79
|
+
// scope — and only flag it when that declaration is 8-bit.
|
|
80
|
+
//
|
|
81
|
+
// decls: Map<name, Array<{line:number, u8:boolean}>> (line is 1-based)
|
|
82
|
+
const decls = new Map();
|
|
83
|
+
const addDecl = (names, line, u8) => {
|
|
84
|
+
for (const raw of names.split(",")) {
|
|
85
|
+
const n = raw.trim();
|
|
86
|
+
if (!n) continue;
|
|
87
|
+
if (!decls.has(n)) decls.set(n, []);
|
|
88
|
+
decls.get(n).push({ line, u8 });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const u8re = /\b(?:unsigned\s+char|char|u8|uint8_t|uint8|int8_t|int8|signed\s+char)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
|
|
92
|
+
// 16-bit-or-wider integer declarations (and float/double, which also
|
|
93
|
+
// can't overflow at 255). Anything not 8-bit is "wide" for our purpose.
|
|
94
|
+
const wideRe = /\b(?:unsigned\s+(?:short|int|long)|signed\s+(?:short|int|long)|short|int|long|u16|u32|u64|uint16_t|uint16|int16_t|int16|uint32_t|uint32|int32_t|int32|uint64_t|uint64|int64_t|int64|size_t|ptrdiff_t)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const code = lines[i].replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "");
|
|
97
|
+
const m8 = code.match(u8re);
|
|
98
|
+
if (m8) { addDecl(m8[1], i + 1, true); continue; }
|
|
99
|
+
const mw = code.match(wideRe);
|
|
100
|
+
if (mw) addDecl(mw[1], i + 1, false);
|
|
77
101
|
}
|
|
102
|
+
// Is the counter declared 8-bit in the scope visible at `loopLine`?
|
|
103
|
+
// Use the NEAREST declaration above the loop. If the nearest visible
|
|
104
|
+
// declaration is wide (uint16_t etc.), the loop is fine.
|
|
105
|
+
const counterIsU8AtLine = (counter, loopLine) => {
|
|
106
|
+
const ds = decls.get(counter);
|
|
107
|
+
if (!ds) return false;
|
|
108
|
+
let best = null;
|
|
109
|
+
for (const d of ds) {
|
|
110
|
+
if (d.line <= loopLine && (best === null || d.line > best.line)) best = d;
|
|
111
|
+
}
|
|
112
|
+
// No declaration above the loop (e.g. param/global declared after, or
|
|
113
|
+
// out-of-order) — fall back to "flag only if EVERY decl is u8" so we
|
|
114
|
+
// never wolf-cry on a name that is also declared wide somewhere.
|
|
115
|
+
if (best === null) return ds.every((d) => d.u8);
|
|
116
|
+
return best.u8;
|
|
117
|
+
};
|
|
78
118
|
const evalConst = (expr) => {
|
|
79
119
|
// Only literals and pure `A * B [* C]` products of decimal/hex ints.
|
|
80
120
|
const t = expr.trim();
|
|
@@ -91,7 +131,7 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
|
|
|
91
131
|
const m = code.match(/\bfor\s*\([^;]*;\s*([A-Za-z_]\w*)\s*<\s*([^;]+?)\s*;/);
|
|
92
132
|
if (!m) continue;
|
|
93
133
|
const [, counter, boundExpr] = m;
|
|
94
|
-
if (!
|
|
134
|
+
if (!counterIsU8AtLine(counter, i + 1)) continue;
|
|
95
135
|
const bound = evalConst(boundExpr);
|
|
96
136
|
if (bound !== null && bound > 255) {
|
|
97
137
|
issues.push({
|
|
@@ -148,13 +148,9 @@ function normalizeSnesSources(args) {
|
|
|
148
148
|
if (cFiles.length === 0) {
|
|
149
149
|
throw new Error("buildSnesC: `sources` must include at least one .c file.");
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`Today only one .c file is supported per build — combine via #include or wait for ` +
|
|
155
|
-
`multi-TU support. .asm/.s siblings work fine.`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
151
|
+
// Multiple .c files ARE supported: buildWithPvSnesLib compiles each to its
|
|
152
|
+
// own .obj (tcc→wla) and links them all (Stage 1 + Stage 3). The genre
|
|
153
|
+
// scaffolds ship main.c + snes_sfx.c and rely on this.
|
|
158
154
|
return args.sources;
|
|
159
155
|
}
|
|
160
156
|
if (typeof args.source === "string") {
|