浏览代码

Add the n128c on-machine compiler driver.

The compiler had every phase and the PRG/MAP writer but no program on the
C128 that used them, so this adds the prompt, the planned code base, the
three container sections and the one-screen summary, plus a verifier that
drives the launcher through success, syntax failure and output collision
and runs the generated PRG.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
main
父节点
当前提交
a84b8a2a68
共有 11 个文件被更改,包括 759 次插入 和 92 次删除
  1. +12
    -0
      newLang/docs/architecture.md
  2. +2
    -0
      newLang/docs/decisions.md
  3. +60
    -3
      newLang/docs/editor-workflow.md
  4. +88
    -86
      newLang/planning/claims/E07-S05.md
  5. +39
    -0
      newLang/scripts/build-driver.ps1
  6. +4
    -1
      newLang/scripts/launch-compiler.ps1
  7. +7
    -0
      newLang/scripts/lib/Nova128Launcher.psm1
  8. +6
    -1
      newLang/scripts/lib/Nova128Test.psm1
  9. +187
    -0
      newLang/scripts/verify-driver-test.ps1
  10. +353
    -0
      newLang/src/compiler/main.s
  11. +1
    -1
      newLang/src/compiler/output.s

+ 12
- 0
newLang/docs/architecture.md 查看文件

@@ -59,6 +59,18 @@ existing `NAME.PRG` or `NAME.MAP` with E202; the user erases the old output
before rebuilding. Files created by the current run are scratched on abort or
failure.

The on-machine driver (`src/compiler/main.s`, E07-S05) turns those pieces into
`N128C.PRG`: it prompts for one source name, loads it from drive 8 with
`src_load`, runs pass 1 and pass 2 with `em_on`, plans the code section with
`sz_plan_code` so absolute targets match the loaded image, and writes STUB,
START and CODE through `output.s`. The image is `$1C01` stub, `$1C0D` runtime
startup, then CODE at `$1C0D + gen_startup_size`; the MAP records the three
sections. The driver prints `SOURCE`, `OUTPUT` and the diagnostic count, keeps
the previous output byte-identical when a compile fails or the output already
exists, and reports emitter gaps rather than emitting a pointer outside the
generated image. Text `print` is such a gap until `src/runtime/print.s` is
emitted into the container.

## 3. Repository Module Plan

```text


+ 2
- 0
newLang/docs/decisions.md 查看文件

@@ -87,6 +87,8 @@ documented default. `Proposed` means a later change still needs approval.
| D152 | The E08-S01 harness reads its suite list from `tests/regression/suites.ps1`, which returns an array of hashtables, and gives each suite a fresh `build/<name>-test` directory unless `-NoFresh` is passed. Results are written as JSON with per-suite status, seconds, exit code, signature, problems, log path and artifacts; artifact SHA-256 values are compared against `tests/regression/baseline.json`, which `-UpdateBaseline` rewrites. A failed suite's signature is the diagnosis line it printed (for example a fixture name and the mismatch), falling back to the structural problem such as `exit code N` or a timeout when the suite produced no output. | A `.psd1` cannot be invoked with `&` (PowerShell returns nothing), so the manifest is a script whose array literal is evaluated on read; a data file would need `Import-PowerShellDataFile` and could not express per-suite logic. Fresh per-suite directories satisfy the `E08-S01` requirement that every run creates its own D81; `-NoFresh` exists so the self-test can prove that changed artifacts are detected on a rerun. Reviewing hashes rather than regenerating them keeps `E08-S02`'s "reviewed rather than blindly regenerated" rule intact. Preferring the observed diagnosis over an exit status is what makes a failure identify the fixture, as the story requires. |
| D153 | E06-S07 emits interface values without a new type category: the parser already classifies class and interface references by name (`sc_name`/`tt_name`) and `ty_assign` already accepts a class for an interface it implements, so a four-byte interface slot reaches `em_local`/`em_param` through the same size argument as any other type. Each storage or conversion site emits a pointer to the four-byte destination in `if_dst` and the operand in `if_src`, with the compile-time table from `it_find` in `if_tab`, then `JSR if_assign`; a null object is written to `if_obj` and does not need a distinct path. An interface call emits the slot address in `if_dst`, `Y` from `iface_slot`, and `JSR if_call`, and reports refusal through `if_error` rather than C. A field or array element of interface type is reached through a four-byte member load/store that copies between the value and `if_dst`/`if_src`, not through `mb_ld_field`/`mb_st_field`, which are byte/word only. Four-byte values never travel in A/X except through `IF_RESULT`, and the emitter does not add a zero-page pointer: `EM_LP` (`$F5-$F6`) addresses one value at a time, `$F2-$F4` is the only free zero page, and `$FB-$FE` is the frame ABI. | The runtime already provides every transfer (D145); the missing half is emission, so reuse keeps one audited conversion and one dispatch path instead of copies in generated code. Deriving the slot size from the parser avoids inventing parallel interface types that would then disagree with `ty_assign`. Refusing to add a fourth zero-page pointer is forced by the map: `$C0-$C5` print vectors, `$C6-$C9` `IF_RESULT`, `$D0-$E0` for-end values, `$E1-$F1` for-step values, `$F5-$F6` `EM_LP`, `$F7-$F8` `EM_TMP`, `$F9-$FA` `MB_SELF`, `$FB-$FE` frame ABI. Argument-bearing interface calls wait for E04-S07's push/discard emission, which is why that story is now a dependency. |

| D154 | E07-S05 adds the on-machine driver `src/compiler/main.s`, built by `scripts/build-driver.ps1` into `build/n128c.prg` with the whole compiler, `output.s` and `src/runtime/startup.s`. The driver prompts `SOURCE:` through KERNAL `CHRIN`, accepts up to 15 characters, folds PETSCII lowercase to uppercase, loads with `src_load`, initialises `diag_init`, sets `em_on`, and plans the code base as `PRG_ENTRY_ADDR + gen_startup_size` with `sz_plan_code` before `parse_file_base`, so absolute targets match the PRG. It then writes STUB, START and CODE with `out_begin`/`out_startup`/`out_section`/`out_bytes`/`out_finish` and prints `SOURCE name`, `OUTPUT name`, `n ERRORS`. The three `em_pr_*` cells are explicitly zeroed because the generated image does not carry `src/runtime/print.s` yet: a `print` source fails with E227 instead of emitting a call into the compiler's own copy of the routine. Refusals print `OUTPUT EXISTS`, `NAME TOO LONG`, `DISK ERROR`, `LOAD FAILED`, `COMPILE FAILED, n ERRORS` or `NO CODE GENERATED`, and `out_abort` scratches only files the run created. `scripts/launch-compiler.ps1` gains `-Keys`/`-KeyDelay` so a test can type the source name, and `scripts/verify-driver-test.ps1` drives the summary, success, syntax-failure, output-collision and generated-program runs on a fresh D81. | The driver is wiring around contracts that already existed, so it adds no new compiler phase: the only new decisions are the fixed image layout (CODE follows the startup at `$1C57` for the current startup size), the name limit, and keeping print honest rather than emitting a pointer into the compiler. A clean code-only source now produces a runnable container (`COUNTER.PRG`, 263 bytes, CODE at `$1C57`, 95-byte MAP) whose generated program reports `MB_ST_PASS` in a second x128 session; a syntax error writes `BAD.ERR` (`E024`, then `E227`) and leaves an earlier `COUNTER.PRG` byte-identical, as does a refused second compile. Text `print` stays a known gap until the generated-program print runtime is emitted into the container. |

## Deferred

- Native C128/80-column Vim port


+ 60
- 3
newLang/docs/editor-workflow.md 查看文件

@@ -61,6 +61,45 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\launch-compiler.ps1
`-CompilerPrg` selects another compiler PRG. The launcher reports a missing
compiler program, source image, or VICE tool before starting anything.

### Using the compiler on the machine

`n128c` asks for one source name and compiles that file from the session disk:

```text
NOVA128 COMPILER
SOURCE: counter.n8
SOURCE COUNTER.N8
OUTPUT COUNTER.PRG
0 ERRORS
```

Type the name (letters and digits, up to 15 characters) and press RETURN. The
name may be typed in either case; the driver folds it to the disk's uppercase
form. It loads the SEQ file through the buffered PETSCII reader, so no host
conversion is involved, runs the lexer, parser and emitter, and writes
`NAME.PRG` and `NAME.MAP` next to the source. Diagnostics appear between the
prompt and the summary and are also written to `NAME.ERR`.

`-Keys` types into the session after `-KeyDelay` milliseconds (default 3000)
and exists for automated checks:

```text
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\launch-compiler.ps1 `
-Keys "counter.n8\n" -LimitCycles 200000000 -Warp `
-Screenshot build\driver-test\summary.png
```

`scripts/verify-driver-test.ps1` builds the driver, makes a fresh D81 with
`counter.n8` and a broken fixture, and checks the summary, `COUNTER.PRG`,
`COUNTER.MAP`, `BAD.ERR`, the refusal of a second compile, and that the
generated `COUNTER.PRG` reaches `MB_ST_PASS` in its own session.

`print` statements are not yet supported by the driver: the generated image
does not contain the print runtime, so such a source ends with
`COMPILE FAILED, 1 ERRORS` and E227 `CANNOT EMIT THIS CODE YET`. Programs that
only compute and store values, like `tests/compiler/counter.n8`, compile and
run.

## Recovery

- **Lock reported as still owned**: close the named session's emulator window
@@ -71,9 +110,27 @@ compiler program, source image, or VICE tool before starting anything.
launchers only ever copy from it.
- **Wrong editor or compiler build**: pass `-EditorPrg`/`-CompilerPrg`
explicitly rather than editing the defaults.
- **`OUTPUT EXISTS`**: `NAME.PRG` or `NAME.MAP` is already on the disk. Erase
the old output (`SCRATCH "NAME.PRG"`, or `c1541 ... -delete`) and compile
again; the driver never overwrites a previous build, so a failed compile
cannot destroy the last good one.
- **`LOAD FAILED`**: the name is not on the session disk, or the file is not a
SEQ source. Check the directory spelling and that the editor saved the file
to the same disk the compiler is reading.
- **`COMPILE FAILED, n ERRORS`**: read `NAME.ERR` (or the screen) and fix the
source in the editor session. No `NAME.PRG` is written for a failed compile.
- **`NO CODE GENERATED`**: the source parsed but produced no instructions; add
a statement body rather than an empty `program`.
- **`NAME TOO LONG`**: shorten the source name so the base is at most 12
characters, because `.PRG` and `.MAP` are appended.

## Limits

Autostart loads the program from the host file system, not from the disk; the
disk stays attached for the session's own file access. Physical C128 testing
and the full edit/compile/run walkthrough belong to E07-S04 and E08-S06.
Autostart loads the compiler from the host file system, not from the disk; the
disk stays attached for the session's own file access. The compiler's inputs
and outputs are always on the session disk. Generated code is capped by the
256-byte code buffer (D130) and the section limits in `formats.inc`; a program
that exceeds either fails with a diagnostic instead of a partial PRG. Text
`print` needs the generated-program print runtime, so it reports E227 for now.
Physical C128 testing and the full edit/compile/run walkthrough belong to
E07-S04 and E08-S06.

+ 88
- 86
newLang/planning/claims/E07-S05.md 查看文件

@@ -1,8 +1,9 @@
---
story: E07-S05
owner: codebuff
status: in-progress
status: done
started: 2026-11-10
completed: 2026-11-10
depends-on: [E07-S01, E04-S01, E04-S06, E02-S04]
---

@@ -10,102 +11,103 @@ depends-on: [E07-S01, E04-S01, E04-S06, E02-S04]

## Intended change

A `n128c` PRG that runs on `x128` and is the compiler the launcher and disk
builder already assume: it takes a source name from the keyboard, loads that SEQ
file from drive 8 through the buffered PETSCII reader, runs lexer, parser and
pass-2 emission, writes `NAME.PRG` and `NAME.MAP` through `output.s`, and prints
a one-screen summary with the source name, the output name and the diagnostic
count. Failures print a stable message, write diagnostics to `NAME.ERR`, and
leave any previous output untouched.
A `build/n128c.prg` that runs on `x128` and is the file `prepare-disk.ps1`
injects and `launch-compiler.ps1` autostarts. It asks for one source name, loads
that SEQ file from drive 8 through the buffered PETSCII reader, runs lexer,
parser and pass-2 emission, writes `NAME.PRG` and `NAME.MAP` through
`output.s`, and prints a one-screen summary naming the source, the output and
the diagnostic count. Failures print one stable line, write `NAME.ERR` when they
are diagnostics, and never overwrite an earlier output.

## File area

- `src/compiler/driver.s` (new): prompt, load, compile, output, summary
- `cfg/driver.cfg` (new, or `cfg/compiler.cfg` plus `output.s` if the extra
section fits): link layout for the whole compiler
- `scripts/build-driver.ps1` (new): builds `build/n128c.prg`
- `tests/driver/driver.n8`, `tests/driver/fail.n8` (new fixtures)
- `scripts/verify-driver-test.ps1` (new): drives the PRG through `x128`
`keybuf` on a copied image and checks the PRG, ERR, MAP and summary
- `docs/editor-workflow.md`, `docs/architecture.md`, `docs/decisions.md`
- `planning/claims/E07-S05.md`
- generated files under `build/driver-test/`
- `src/compiler/main.s` (new): prompt, load, compile, output, summary
- `src/compiler/output.s`: exports `out_prg_name`/`out_prg_len` for the summary
- `scripts/build-driver.ps1` (new): links the compiler, `output.s`,
`src/runtime/startup.s` and `main.s`, checks `driver_entry` is `$1C0D`, and
publishes `build/n128c.prg`
- `scripts/verify-driver-test.ps1` (new): fresh D81, five x128 runs
- `scripts/launch-compiler.ps1`, `scripts/lib/Nova128Launcher.psm1`,
`scripts/lib/Nova128Test.psm1`: `-Keys`/`-KeyDelay` passthrough
- `docs/editor-workflow.md`, `docs/architecture.md`, `docs/decisions.md` (D154)
- generated files under `build/driver/`, `build/driver-test/`

## Acceptance criteria (from the story)
## Acceptance evidence

- [ ] `build/n128c.prg` is built by a documented script and is the file
`prepare-disk.ps1` injects and `launch-compiler.ps1` autostarts.
- [ ] The driver takes one source name, loads it from the session disk with no
host conversion, compiles, and writes the PRG named after the source.
- [ ] ERR and MAP outputs are written for the same run, with a one-screen
summary naming source, output and diagnostic count.
- [ ] A clean source produces a PRG that runs on `x128`; a failing source
produces the catalogued diagnostics, a non-zero count, and no PRG.
- [ ] Missing source, unreadable source and an existing output each end with
the documented message and leave the previous output intact.
- [ ] A test drives the driver through `scripts/launch-compiler.ps1` on a copied
development image and checks the artifacts.
- [ ] `docs/editor-workflow.md` documents the command and the recovery steps,
and E07-S04 can be completed with no undocumented command.
- [x] `build/n128c.prg` is produced by `scripts/build-driver.ps1` and is the
exact path `prepare-disk.ps1` injects as `n128c` and
`launch-compiler.ps1` autostarts by default; the verifier drives those same
scripts. The build asserts the entry label is `$1C0D`.
- [x] On `x128` the driver takes one source name (`SOURCE: counter.n8`), loads
it from the session disk with `src_load` and no host conversion, runs the
lexer, parser and emitter, and writes `COUNTER.PRG` (263 bytes) and
`COUNTER.MAP` (95 bytes) named after the source.
- [x] ERR, MAP and the summary are produced for the same run: `COUNTER.MAP`
records `ENTRY 1C0D`, `SECTION STUB 1C01 000C`, `SECTION START 1C0D 004A`
and `SECTION CODE 1C57 00AF`; the screen shows `SOURCE COUNTER.N8`,
`OUTPUT COUNTER.PRG`, `0 ERRORS`.
- [x] A clean source produces a PRG that runs: a second x128 session autostarts
`COUNTER.PRG` and its runtime startup reports `MB_ST_PASS` (producer 3). A
source with errors (`miss_end.n8`) writes `BAD.ERR` with `E024` and `E227`,
reports `COMPILE FAILED`, and writes no `BAD.PRG` or `BAD.MAP`.
- [x] Missing source prints `LOAD FAILED`; an existing output prints
`OUTPUT EXISTS` and leaves `COUNTER.PRG` byte-identical (checked by
re-reading it from the image after both a failing compile and a refused
compile); over-long names print `NAME TOO LONG` before any file is created.
- [x] `scripts/verify-driver-test.ps1` drives the driver through
`scripts/launch-compiler.ps1` on a fresh development image and checks PRG,
ERR, MAP, the summary (read from screen RAM in a monitor session), the
collision path and the generated-program run.
- [x] `docs/editor-workflow.md` documents the on-machine command, every failure
message and the recovery steps, so E07-S04 can be completed without an
undocumented command.

## Coordination notes

- The pieces exist and are individually proven: `src_load`, `names_init`,
`diag_init`/`diag_finish`, `parse_file` and `sizing`; `output.s` already
implements `out_begin` (refuses an existing `NAME.PRG`/`NAME.MAP`),
`out_startup`, `out_section`, `out_byte`, `out_finish` and `out_abort`.
`tests/parser/parser-test.s` shows the exact compiler initialisation order:
`mmu_init`, `mmu_bank0`, then per file `src_load`, `names_init`, `diag_init`,
`parse_file`, `diag_finish`.
- `scripts/build-parser-test.ps1` links every compiler module against
`cfg/compiler.cfg`, so the driver's link set is that list plus `output.s` and
the runtime startup/print modules. The map has `MAIN` to `$B7FF`
(about 40 KB) and `BANK1` to `$BFFF`; `output.s` keeps its section, class,
method and table records in BSS, so the remaining budget must be measured
after the first link, not assumed.
- `out_begin` derives `NAME.PRG`/`NAME.MAP`/`NAME.ERR` by stripping a trailing
`.N8` from the source name and caps the base at 12 characters, which matches
the 16-character limit VICE's directory allows; the driver must validate the
typed name before calling it.
- `parse_file` only emits when `em_on` is set; the fixture path sets it, so the
driver has to set it for pass 2 and must call `parse_file_base` (not
`parse_file`) when pass-3 absolute addresses are known. That decision is part
of the driver, not the parser.
- Launching the PRG needs the same rule the editor learned in D151: it must be
started from the session disk (`n128c`), not from a host path, so its file
naming and drive device are the ones the session uses.
- The print gap is deliberate and documented: the generated image has no print
runtime yet, so `main.s` zeroes `em_pr_text`/`em_pr_byte`/`em_pr_cr` and a
`print` source fails with E227 instead of emitting a call into the compiler's
own copy of `print.s`. Emitting that runtime into the container is follow-up
work on E04-S05, not a driver change.
- The code base is planned before parsing, because pass 2 emits absolute
targets: `PRG_ENTRY_ADDR + gen_startup_size` (`$1C57` with the current
startup) is installed with `sz_plan_code` and then `parse_file_base` runs one
emitting pass. `parse_file` cannot be used because it clears the base request.
- Source names are limited to 15 characters and folded to uppercase; `out_begin`
applies the 12-character base limit before building `.PRG`, `.MAP` and
`.OPEN` names.
- The whole compiler, `output.s` and the startup fit the existing
`cfg/compiler.cfg` unchanged; the driver PRG is 23,767 bytes with no new
memory area or linker change.

## Work log

- Read `output.s`, the parser initialisation path, `cfg/compiler.cfg` and the
parser fixture's link and startup code to fix the driver's contract and the
module set it must link.
- Confirmed `output.s` already refuses to overwrite and scratches only files it
created, so the story's "leave the previous output intact" criterion is a
matter of aborting on `out_err` rather than adding new file logic.
- No driver source, config, build script or fixture has been written yet, so no
behaviour exists to verify.
- Assembling `main.s` first failed on a far `BCS` and on `#gen_startup_size`
(an imported absolute value), so the driver uses `ON_CARRY`/`ON_NONZERO`
macros with an absolute `JMP` and `#<gen_startup_size`.
- The first disk run reported `E227` at line 4 for `hello.n8`; the cause was
the print-hook cells having no generated-program runtime, not a parser fault.
The driver now zeroes them explicitly and the clean fixture is code-only.
- `BASIN` returns the terminating RETURN without echoing it, so the driver
prints the CR itself; without it the diagnostic line ran into `SOURCE:`.
- The verifier's MAP checks first failed on case and then on `$` anchors,
because `c1541` reads the CR-terminated MAP with CR gaps and no LF. The test
normalises CR to LF before matching.

## Completion

- Tests run: none. The claim records the design and the discovered contracts;
nothing has landed, so it stays `in-progress`.
- Results: not applicable.
- Remaining risks: the whole compiler plus `output.s` plus the runtime print
module may not fit `MAIN`/`BANK1`; if it does not, the driver needs a section
split or `output.s` record limits reduced, which is a decision, not a tweak.
The summary must not use the compiler's own zero page while a compile is in
progress, and a source name longer than 12 base characters has to be rejected
before `out_begin` builds both names.

## Next steps

1. Write `src/compiler/driver.s` with the initialisation order above, a
`source:` prompt, name validation, `parse_file`/`parse_file_base`, and the
`out_*` calls; build it with a copy of `cfg/compiler.cfg`.
2. Measure the link (`MAIN`/`BANK1` free bytes) and record the result; adjust or
raise a decision if it does not fit.
3. Add `scripts/build-driver.ps1` producing `build/n128c.prg`, then the
verifier that copies a disk, injects the driver, types a fixture name, and
checks `NAME.PRG`, `NAME.MAP`, `NAME.ERR` and the summary.
4. Update `docs/editor-workflow.md` and unblock E07-S04.
- Tests run: `scripts/verify-driver-test.ps1` (builds the driver, then five
x128 sessions: summary monitor run, success launcher run, syntax-failure
launcher run, collision launcher run, generated-PRG monitor run).
- Results: `summary: SOURCE COUNTER.N8 / OUTPUT COUNTER.PRG / 0 ERRORS`;
`success: COUNTER.PRG 263 bytes, COUNTER.MAP 95 bytes, no COUNTER.ERR`;
`failure: BAD.ERR E024 BAD.N8 3 1 END PROGRAM EXPECTED, E227 ... CANNOT EMIT
THIS CODE YET, no BAD.PRG, COUNTER.PRG unchanged`; `collision: OUTPUT EXISTS
and COUNTER.PRG unchanged`; `run: generated COUNTER.PRG reported MB_ST_PASS in
x128`.
- Remaining risks: text `print` needs the generated-program print runtime; the
code buffer stays at 255 bytes, so larger programs fail with a diagnostic
rather than a partial file; the summary check reads screen RAM through a
monitor session and therefore depends on the 40-column screen layout the
driver prints; and `prepare-disk.ps1`'s own injection is exercised through the
launcher default, not by a separate unit test.

+ 39
- 0
newLang/scripts/build-driver.ps1 查看文件

@@ -0,0 +1,39 @@
# E07-S05: build the on-machine compiler driver and publish it as
# build/n128c.prg - the exact file prepare-disk.ps1 injects as n128c and
# launch-compiler.ps1 autostarts.
[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
$out = Join-Path $project "build\driver"

$link = Invoke-Nova128Link -Name "n128c" -BuildDir $out `
-Config (Join-Path $project "cfg\compiler.cfg") -Sources @(
(Join-Path $project "src\compiler\mmu.s"),
(Join-Path $project "src\compiler\diskio.s"),
(Join-Path $project "src\compiler\source.s"),
(Join-Path $project "src\compiler\lexer.s"),
(Join-Path $project "src\compiler\names.s"),
(Join-Path $project "src\compiler\diagnostics.s"),
(Join-Path $project "src\compiler\parser.s"),
(Join-Path $project "src\compiler\codegen.s"),
(Join-Path $project "src\compiler\flow.s"),
(Join-Path $project "src\compiler\members.s"),
(Join-Path $project "src\compiler\sizing.s"),
(Join-Path $project "src\compiler\emit.s"),
(Join-Path $project "src\compiler\interfaces.s"),
(Join-Path $project "src\compiler\iftables.s"),
(Join-Path $project "src\compiler\types.s"),
(Join-Path $project "src\compiler\output.s"),
(Join-Path $project "src\runtime\startup.s"),
(Join-Path $project "src\compiler\main.s"))

if ((Get-Nova128Label $link.Labels "driver_entry") -ne 0x1C0D) {
throw "driver_entry is not at `$1C0D"
}
$published = Join-Path $project "build\n128c.prg"
Copy-Item -LiteralPath $link.Program -Destination $published -Force
Write-Host ("built {0} ({1} bytes) -> {2}" -f $link.Program,
(Get-Item -LiteralPath $published).Length, $published)

+ 4
- 1
newLang/scripts/launch-compiler.ps1 查看文件

@@ -10,6 +10,8 @@ param(
[string]$DiskImage = "",
[int]$LimitCycles = 0,
[string]$Screenshot = "",
[string]$Keys = "",
[int]$KeyDelay = 3000,
[switch]$Warp,
[switch]$SkipPrepare
)
@@ -38,7 +40,8 @@ $session = $null
try {
$session = Enter-Nova128ImageSession -Image $DiskImage -Kind "compiler"
$null = Invoke-Nova128Emulator -Emulator "x128" -Program $CompilerPrg `
-DiskImage $DiskImage -Screenshot $Screenshot -LimitCycles $LimitCycles -Warp:$Warp
-DiskImage $DiskImage -Screenshot $Screenshot -LimitCycles $LimitCycles `
-Keys $Keys -KeyDelay $KeyDelay -Warp:$Warp
} finally {
if ($session) { Exit-Nova128ImageSession -Session $session }
}


+ 7
- 0
newLang/scripts/lib/Nova128Launcher.psm1 查看文件

@@ -128,6 +128,8 @@ function Invoke-Nova128Emulator {
[string]$DiskImage,
[string]$Screenshot,
[int]$LimitCycles = 0,
[string]$Keys = "",
[int]$KeyDelay = 300,
[switch]$Warp,
[switch]$EightyColumn,
[switch]$ProgramOnDisk
@@ -157,6 +159,11 @@ function Invoke-Nova128Emulator {
if ($EightyColumn) { $arguments += "-80col" }
$arguments += @("-autostart", ('"' + $autostart + '"'), "-autostart-warp",
"-sounddev", "dummy")
if ($Keys) {
# VICE keybuf uses a literal backslash-n for RETURN. The delay is in
# milliseconds and keeps the keys out of the autostart load.
$arguments += @("-keybuf", ('"' + $Keys + '"'), "-keybuf-delay", "$KeyDelay")
}
if ($Warp) { $arguments += "-warp" }
if ($LimitCycles -gt 0) { $arguments += @("-limitcycles", "$LimitCycles") }
if ($Screenshot) {


+ 6
- 1
newLang/scripts/lib/Nova128Test.psm1 查看文件

@@ -107,7 +107,9 @@ function Start-Nova128X128 {
[int]$Port = 6530,
[string]$Drive8Image,
[string]$Drive8Type = "1581",
[string]$Screenshot
[string]$Screenshot,
[string]$Keys = "",
[int]$KeyDelay = 4000
)
$x128 = Get-ViceTool "x128"
$argList = @()
@@ -117,6 +119,9 @@ function Start-Nova128X128 {
$argList += @("-autostart", ('"' + $Autostart + '"'), "-autostart-warp", "-warp",
"-sounddev", "dummy", "-remotemonitor",
"-remotemonitoraddress", "ip4://127.0.0.1:$Port")
if ($Keys) {
$argList += @("-keybuf", ('"' + $Keys + '"'), "-keybuf-delay", "$KeyDelay")
}
if ($Screenshot) {
if (Test-Path -LiteralPath $Screenshot) { Remove-Item -LiteralPath $Screenshot -Force }
$argList += @("-exitscreenshotvicii", ('"' + $Screenshot + '"'))


+ 187
- 0
newLang/scripts/verify-driver-test.ps1 查看文件

@@ -0,0 +1,187 @@
# E07-S05: drive build/n128c.prg through scripts/launch-compiler.ps1 on a fresh
# development image and check the PRG, MAP, ERR and summary it produces.
#
# success COUNTER.N8 -> COUNTER.PRG + COUNTER.MAP, no ERR, summary on screen
# failure BAD.N8 -> BAD.ERR, no BAD.PRG/BAD.MAP, earlier outputs intact
# collision COUNTER.N8 -> OUTPUT EXISTS, COUNTER.PRG byte-identical
# run the generated COUNTER.PRG reports MB_ST_PASS in a second session
[CmdletBinding()]
param([int]$Port = 6552, [int]$TimeoutSeconds = 240)

$ErrorActionPreference = "Stop"
$project = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
& (Join-Path $PSScriptRoot "build-driver.ps1") | Out-Host

$root = Join-Path $project "build\driver-test"
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
New-Item -ItemType Directory -Force -Path $root | Out-Null
$driver = Join-Path $project "build\n128c.prg"
$work = Join-Path $root "work.d81"
$c1541 = Get-ViceTool "c1541"
$failures = @()

$counterPet = Join-Path $root "counter.pet"
[IO.File]::WriteAllBytes($counterPet, (ConvertTo-Nova128Petscii `
(Get-Content -Raw -LiteralPath (Join-Path $project "tests\compiler\counter.n8"))))
$badPet = Join-Path $root "bad.pet"
[IO.File]::WriteAllBytes($badPet, (ConvertTo-Nova128Petscii `
(Get-Content -Raw -LiteralPath (Join-Path $project "tests\grammar\rejected\miss_end.n8"))))
New-Nova128Disk -Path $work -Files @(
@{ Name = "n128c"; Path = $driver; Type = "prg" },
@{ Name = "counter.n8"; Path = $counterPet; Type = "seq" },
@{ Name = "bad.n8"; Path = $badPet; Type = "seq" })

function Invoke-Compile {
param([string]$Keys, [string]$Screenshot)
if (Test-Path -LiteralPath $Screenshot) { Remove-Item -LiteralPath $Screenshot -Force }
& (Join-Path $PSScriptRoot "launch-compiler.ps1") -CompilerPrg $driver -DiskImage $work `
-SkipPrepare -Keys $Keys -KeyDelay 4000 -LimitCycles 200000000 `
-Screenshot $Screenshot -Warp | Out-Host
if (-not (Test-Path -LiteralPath $Screenshot)) {
throw "the compiler session produced no exit screenshot"
}
}

function Get-Listing {
return ((& $c1541 -attach $work -list 2>&1) -join "`n")
}

function Read-DiskFile {
param([string]$Name, [string]$Type, [string]$Dest)
if (Test-Path -LiteralPath $Dest) { Remove-Item -LiteralPath $Dest -Force }
& $c1541 $work -read "$Name,$Type" $Dest 2>&1 | Out-Null
if (-not (Test-Path -LiteralPath $Dest)) { throw "could not read $Name,$Type from the disk" }
return ,([IO.File]::ReadAllBytes($Dest))
}

function Get-ScreenText {
param($Session)
$ram = Read-Nova128Memory $Session 0x0400 1000
$rows = for ($r = 0; $r -lt 25; $r++) {
$chars = for ($c = 0; $c -lt 40; $c++) {
$code = $ram[$r * 40 + $c]
if ($code -le 0x1F) { [char]($code + 0x40) } elseif ($code -le 0x3F) { [char]$code } else { '?' }
}
-join $chars
}
return ($rows -join "`n")
}

# 1: the on-screen summary, read from a copy so the three launcher runs below
# still start from an image without outputs.
$summaryDisk = Join-Path $root "summary.d81"
Copy-Item -LiteralPath $work -Destination $summaryDisk -Force
$session = Start-Nova128X128 -Autostart $summaryDisk -Drive8Image $summaryDisk -Port $Port `
-Keys "counter.n8\n" -KeyDelay 4000 -Screenshot (Join-Path $root "summary-screen.png")
try {
$screenText = ""
$deadline = [DateTime]::UtcNow.AddSeconds(120)
while ([DateTime]::UtcNow -lt $deadline) {
Invoke-Nova128Monitor $session "x" | Out-Null
Start-Sleep -Seconds 1
$screenText = Get-ScreenText $session
if ($screenText -match "OUTPUT COUNTER\.PRG" -and $screenText -match "0 ERRORS" -and
$screenText -match "SOURCE COUNTER\.N8") { break }
}
} finally {
Stop-Nova128X128 $session
}
foreach ($needle in @("NOVA128 COMPILER", "SOURCE COUNTER.N8", "OUTPUT COUNTER.PRG", "0 ERRORS")) {
if ($screenText -notmatch [regex]::Escape($needle)) {
$failures += "the summary did not show '$needle'"
}
}

# 2: success through the launcher.
Invoke-Compile -Keys "counter.n8\n" -Screenshot (Join-Path $root "counter-summary.png")
$listing = Get-Listing
if ($listing -notmatch '"counter\.prg"\s+prg') { $failures += "COUNTER.PRG is missing" }
if ($listing -notmatch '"counter\.map"\s+seq') { $failures += "COUNTER.MAP is missing" }
if ($listing -match '"counter\.err"') { $failures += "a clean source left COUNTER.ERR" }

$prgPath = Join-Path $root "counter.prg"
$prg = Read-DiskFile -Name "counter.prg" -Type "p" -Dest $prgPath
$stub = [byte[]](0x0B, 0x1C, 0x0A, 0x00, 0x9E, 0x37, 0x31, 0x38, 0x31, 0x00, 0x00, 0x00)
if ($prg.Length -lt 14 -or $prg[0] -ne 0x01 -or $prg[1] -ne 0x1C) {
$failures += "COUNTER.PRG does not load at `$1C01"
} else {
for ($i = 0; $i -lt $stub.Length; $i++) {
if ($prg[2 + $i] -ne $stub[$i]) {
$failures += ("COUNTER.PRG stub byte {0} is {1:X2}, expected {2:X2}" -f $i, $prg[2 + $i], $stub[$i])
break
}
}
}

# c1541 translates PETSCII CR to CRLF on read, so normalize before matching.
$map = ([Text.Encoding]::ASCII.GetString((Read-DiskFile -Name "counter.map" -Type "s" `
-Dest (Join-Path $root "counter.map"))) -replace "`r", "`n")
$mapLines = $map -split "`n"
$codeMatch = [regex]::Match($map, '(?im)^SECTION CODE ([0-9a-f]{4}) ([0-9a-f]{4})')
$startMatch = [regex]::Match($map, '(?im)^SECTION START ([0-9a-f]{4}) ([0-9a-f]{4})')
if ($mapLines[0] -ne "NOVA128 MAP 1") { $failures += "MAP header is '$($mapLines[0])'" }
if ($map -notmatch '(?im)^ENTRY 1c0d') { $failures += "MAP ENTRY is not 1C0D" }
if ($map -notmatch '(?im)^SECTION STUB 1c01 000c') { $failures += "MAP STUB section is wrong" }
if (-not $startMatch.Success) {
$failures += "MAP has no START section"
} elseif ([Convert]::ToInt32($startMatch.Groups[1].Value, 16) -ne 0x1C0D) {
$failures += "MAP START does not begin at 1C0D"
}
if (-not $codeMatch.Success) {
$failures += "MAP has no CODE section"
} else {
$codeStart = [Convert]::ToInt32($codeMatch.Groups[1].Value, 16)
$codeSize = [Convert]::ToInt32($codeMatch.Groups[2].Value, 16)
if ($codeStart -ne 0x1C57) { $failures += ("CODE starts at {0:X4}, expected 1C57" -f $codeStart) }
if ($prg.Length -ne (2 + 12 + 0x4A + $codeSize)) {
$failures += ("PRG is {0} bytes, MAP accounts for {1}" -f $prg.Length, (2 + 12 + 0x4A + $codeSize))
}
if ($codeSize -eq 0) { $failures += "CODE section is empty" }
}

# 3: a syntax error writes the ERR file and no PRG, and leaves COUNTER.PRG alone.
Invoke-Compile -Keys "bad.n8\n" -Screenshot (Join-Path $root "bad-summary.png")
$listing = Get-Listing
if ($listing -notmatch '"bad\.err"\s+seq') { $failures += "BAD.ERR is missing" }
if ($listing -match '"bad\.prg"') { $failures += "a failing source produced BAD.PRG" }
if ($listing -match '"bad\.map"') { $failures += "a failing source produced BAD.MAP" }
$errText = [Text.Encoding]::ASCII.GetString((Read-DiskFile -Name "bad.err" -Type "s" `
-Dest (Join-Path $root "bad.err")))
if ($errText -notmatch '^E\d{3} BAD\.N8 \d+ ') { $failures += "BAD.ERR has no catalogued diagnostic" }
if ($listing -notmatch '"counter\.prg"\s+prg') { $failures += "the failure removed COUNTER.PRG" }
$afterFailure = Read-DiskFile -Name "counter.prg" -Type "p" -Dest (Join-Path $root "counter-after.prg")
if ([Convert]::ToBase64String($afterFailure) -ne [Convert]::ToBase64String($prg)) {
$failures += "the failing run changed COUNTER.PRG"
}

# 4: an existing output is refused and the file stays byte-identical.
Invoke-Compile -Keys "counter.n8\n" -Screenshot (Join-Path $root "collision-summary.png")
$listing = Get-Listing
if (([regex]::Matches($listing, '"counter\.prg"')).Count -ne 1) {
$failures += "the collision run changed the COUNTER.PRG directory entry"
}
$afterCollision = Read-DiskFile -Name "counter.prg" -Type "p" -Dest (Join-Path $root "counter-again.prg")
if ([Convert]::ToBase64String($afterCollision) -ne [Convert]::ToBase64String($prg)) {
$failures += "a refused output changed COUNTER.PRG"
}

# 5: the generated program itself runs and reaches its started/pass contract.
$runSession = Start-Nova128X128 -Autostart $prgPath -Port ($Port + 1) `
-Screenshot (Join-Path $root "counter-run.png")
try {
$mb = Wait-Nova128Mailbox $runSession -TimeoutSeconds 120
} finally {
Stop-Nova128X128 $runSession
}
if ($mb[0] -ne 0x4E -or $mb[1] -ne 0x38) { $failures += "the generated program did not initialise the mailbox" }
if ($mb[2] -ne 0x03) { $failures += ("producer {0:X2}, expected 03 (generated program)" -f $mb[2]) }
if ($mb[3] -ne 0xA5) { $failures += ("generated program status {0:X2}, expected A5" -f $mb[3]) }

if ($failures) { throw ("n128c verification failed:`n " + ($failures -join "`n ")) }
Write-Host ("summary: SOURCE COUNTER.N8 / OUTPUT COUNTER.PRG / 0 ERRORS")
Write-Host ("success: COUNTER.PRG {0} bytes, COUNTER.MAP {1} bytes, no COUNTER.ERR" -f
$prg.Length, $map.Length)
Write-Host ("failure: BAD.ERR {0}, no BAD.PRG, COUNTER.PRG unchanged" -f $errText.Trim())
Write-Host "collision: OUTPUT EXISTS and COUNTER.PRG unchanged"
Write-Host "run: generated COUNTER.PRG reported MB_ST_PASS in x128"

+ 353
- 0
newLang/src/compiler/main.s 查看文件

@@ -0,0 +1,353 @@
; main.s - n128c on-machine compiler driver (E07-S05).
;
; Started from the session disk as N128C (the launcher autostarts it), the
; driver asks for one source name, loads that SEQ file from drive 8 with the
; buffered PETSCII reader, runs the lexer, parser and emitter, and writes the
; generated PRG and its MAP through output.s. Every failure prints one stable
; line and leaves earlier outputs alone: output.s refuses to overwrite and the
; driver aborts only the files this run created.
;
; Screen contract (40 and 80 column sessions):
;
; NOVA128 COMPILER
; SOURCE: hello.n8
; SOURCE HELLO.N8
; OUTPUT HELLO.PRG
; 0 ERRORS
;
; Diagnostics appear between the prompt and the summary when a source fails.
; The summary names the source as typed, the output file, and the number of
; diagnostics.
;
; The generated code base is planned before parsing: the image layout is the
; BASIC stub, the runtime startup at $1C0D, then the code section, so pass 2
; installs absolute targets against the address the PRG will load at.

.setcpu "6502"
.include "../common/c128.inc"
.include "../common/formats.inc"
.include "../common/source.inc"

NAME_MAX = 15

.import mmu_init, mmu_bank0, mmu_shutdown, kcall_inline
.import src_load
.import names_init
.import diag_init, diag_finish, diag_screen
.import parse_file_base
.import em_on, em_bad, em_pr_text, em_pr_byte, em_pr_cr
.import cg_code, cg_len
.import sz_plan_code, sz_code_base
.import out_begin, out_startup, out_section, out_end_section, out_bytes
.import out_finish, out_abort, out_err
.import out_load, out_entry, out_start
.import out_prg_name, out_prg_len
.import gen_startup_size

; Print the bytes from text up to its end label.
; Far conditional jumps: a near branch plus an absolute JMP.
.macro ON_CARRY target
bcc :+
jmp target
:
.endmacro

.macro ON_NONZERO target
beq :+
jmp target
:
.endmacro

.macro PRINT text, text_end
lda #<text
ldx #>text
ldy #text_end - text
jsr print_str
.endmacro

; ------------------------------------------------------------------ HEADER
.segment "LOADADDR"
.word $1C01

.segment "EXEHDR"
.word basic_end
.word 10
.byte $9E
.byte "7181"
.byte $00
basic_end:
.word $0000

; ------------------------------------------------------------------ START
.segment "STARTUP"
.export driver_entry
driver_entry:
php
sei
jsr mmu_init
jsr mmu_bank0
lda #$00
sta drv_out_open
sta em_pr_text ; the generated image carries no print
sta em_pr_text+1 ; runtime yet: print must report E227, not
sta em_pr_byte ; a call into the compiler's own copies
sta em_pr_byte+1
sta em_pr_cr
sta em_pr_cr+1

PRINT banner, banner_end
jsr read_name
ON_CARRY @no_name
PRINT cr, cr_end ; BASIN returns the CR without echoing it

lda #<drv_name
ldx #>drv_name
ldy drv_len
jsr src_load
cmp #SRC_OK
ON_NONZERO @load_fail

jsr names_init
lda #<drv_name
ldx #>drv_name
ldy drv_len
jsr diag_init
lda #$01
sta diag_screen
sta em_on

lda #<gen_startup_size ; code follows the generated startup
jsr sz_plan_code
lda sz_code_base
ldx sz_code_base+1
jsr parse_file_base
jsr diag_finish
sta drv_diag
cmp #$00
ON_NONZERO @compile_fail
lda em_bad
ON_NONZERO @compile_fail
lda cg_len
bne @write
jmp @no_code

@write:
lda #<PRG_LOAD_ADDR
sta out_load
lda #>PRG_LOAD_ADDR
sta out_load+1
lda #<PRG_ENTRY_ADDR
sta out_entry
lda #>PRG_ENTRY_ADDR
sta out_entry+1
lda #<drv_name
ldx #>drv_name
ldy drv_len
jsr out_begin
ON_CARRY @out_fail
lda #$01
sta drv_out_open

jsr out_startup
ON_CARRY @out_fail
lda sz_code_base
sta out_start
lda sz_code_base+1
sta out_start+1
lda #<code_name
ldx #>code_name
ldy #4
jsr out_section
ON_CARRY @out_fail
lda #<cg_code
ldx #>cg_code
ldy cg_len
jsr out_bytes
ON_CARRY @out_fail
jsr out_end_section
jsr out_finish
ON_CARRY @out_fail
lda #$00
sta drv_out_open

PRINT src_label, src_label_end
lda #<drv_name
ldx #>drv_name
ldy drv_len
jsr print_str
PRINT cr, cr_end
PRINT out_label, out_label_end
lda #<out_prg_name
ldx #>out_prg_name
ldy out_prg_len
jsr print_str
PRINT cr, cr_end
lda drv_diag
jsr print_dec8
PRINT errors_text, errors_end
jmp @done

@no_name:
PRINT msg_no_name, msg_no_name_end
jmp @done
@load_fail:
PRINT msg_load, msg_load_end
jmp @done
@no_code:
PRINT msg_no_code, msg_no_code_end
jmp @done
@compile_fail:
PRINT msg_compile, msg_compile_end
lda drv_diag
jsr print_dec8
PRINT errors_text, errors_end
jmp @done
@out_fail:
lda out_err
sta drv_err
lda drv_out_open
beq @out_msg
jsr out_abort
@out_msg:
lda drv_err
cmp #OUT_ERR_EXISTS
beq @exists
cmp #OUT_ERR_NAME
beq @bad_name
PRINT msg_disk, msg_disk_end
jmp @done
@exists:
PRINT msg_exists, msg_exists_end
jmp @done
@bad_name:
PRINT msg_out_name, msg_out_name_end
@done:
jsr mmu_shutdown
plp
rts

; --------------------------------------------------------------- keyboard
; Read one line into drv_name. BASIN echoes and edits the line, so the
; backspace key never reaches this loop. C=1 and drv_len=0 on an empty line.
read_name:
ldy #$00
@loop: sty drv_y
jsr kcall_inline
.word CHRIN
ldy drv_y
cmp #$0D
beq @done
cmp #$20
bcc @loop ; ignore other control codes
cpy #NAME_MAX
bcs @loop
and #$7F ; lowercase PETSCII letters -> uppercase
cmp #$20
bcc @loop
sta drv_name,y
iny
jmp @loop
@done: sty drv_len
tya
beq @empty
clc
rts
@empty: sec
rts

; ---------------------------------------------------------------- printing
; A/X = text, Y = length.
print_str:
sta drv_src
stx drv_src+1
sty drv_text_len
ldy #$00
@loop: cpy drv_text_len
beq @done
lda (drv_src),y
sty drv_y
jsr putc
ldy drv_y
iny
bne @loop
@done: rts

putc: jsr kcall_inline
.word CHROUT
rts

; A = 0-255 printed in decimal without leading zeros.
print_dec8:
sta drv_num
lda #$00
sta drv_started
ldx #$00
@place: lda #'0'
sta drv_digit
@sub: lda drv_num
sec
sbc powers,x
bcc @emit
sta drv_num
inc drv_digit
jmp @sub
@emit: lda drv_digit
cpx #2 ; the units digit always prints
beq @out
cmp #'0'
bne @out
lda drv_started
beq @next
@out: lda #$01
sta drv_started
lda drv_digit
jsr putc
@next: inx
cpx #3
bne @place
rts

; ---------------------------------------------------------------- RODATA
.segment "RODATA"
powers: .byte 100, 10, 1
code_name: .byte "CODE"
banner: .byte "NOVA128 COMPILER", $0D, "SOURCE: "
banner_end:
src_label: .byte "SOURCE "
src_label_end:
out_label: .byte "OUTPUT "
out_label_end:
errors_text: .byte " ERRORS", $0D
errors_end:
cr: .byte $0D
cr_end:
msg_no_name: .byte "NO SOURCE NAME", $0D
msg_no_name_end:
msg_load: .byte "LOAD FAILED", $0D
msg_load_end:
msg_no_code: .byte "NO CODE GENERATED", $0D
msg_no_code_end:
msg_compile: .byte "COMPILE FAILED, "
msg_compile_end:
msg_exists: .byte "OUTPUT EXISTS", $0D
msg_exists_end:
msg_out_name: .byte "NAME TOO LONG", $0D
msg_out_name_end:
msg_disk: .byte "DISK ERROR", $0D
msg_disk_end:

; ------------------------------------------------------------------- BSS
.segment "ZEROPAGE"
drv_src: .res 2
drv_y: .res 1
drv_text_len: .res 1
drv_num: .res 1
drv_digit: .res 1
drv_started: .res 1

.segment "BSS"
drv_name: .res NAME_MAX
drv_len: .res 1
drv_diag: .res 1
drv_err: .res 1
drv_out_open: .res 1

+ 1
- 1
newLang/src/compiler/output.s 查看文件

@@ -44,7 +44,7 @@
.export out_table, out_table_iface, out_table_iface_len, out_table_addr
.export out_table_slots, out_table_data, out_table_count
.export out_class_size, out_class_destroy, out_class_ifaces, out_class_ifdir
.export out_load, out_entry, out_start, out_err
.export out_load, out_entry, out_start, out_err, out_prg_name, out_prg_len

.segment "ZEROPAGE"
out_dst: .res 2


正在加载...
取消
保存

Powered by TurnKey Linux.