Quellcode durchsuchen

calc: run CALC.COM under qemu (high-image loader)

Make bootcom.s parameterised: default LOAD_SEG=0x1000, INLEN=0x9000,
SCRATCH_SEG=0x2000 and COPY_BYTES=INLEN.  The image is copied to
1000:0100, the input descriptor lives at 1000:9000, and the data area is at
1000:A100, so the boot sector and BIOS areas are untouched.  rt_exec.py keeps
the original low loader by passing LOAD_SEG=0 / INLEN=0x7000.

run_com_exec.py now uses the high layout by default; build_boot_sector and
build_floppy accept explicit layout arguments for rt_exec.  The existing
fixture suite still passes under the high loader.

Add tests/calc_smoke.py and wire it into run_all: it builds CALC.PAS.COM,
boots it under qemu, checks the welcome/grid/command menu, sends `\r\r/Q',
and requires exit code 0.  CALC.COM is 35,284 bytes and fits below the input
descriptor.

Verified: compile matrix 113/113, CALC ladder 1/1 OK, CALC execution PASS,
99 .COM executions under both oracles, rt_exec 276/276, nonvacuity 121/121,
run_all ALL PASS.
Eric Streit vor 3 Tagen
Ursprung
Commit
0912b2af73
7 geänderte Dateien mit 152 neuen und 62 gelöschten Zeilen
  1. 11 10
      CALC-COMPILE.md
  2. 11 9
      SUMMARY.md
  3. 62 0
      shell/tests/calc_smoke.py
  4. 41 28
      shell/tests/exec/bootcom.s
  5. 2 2
      shell/tests/rt_exec.py
  6. 1 0
      shell/tests/run_all.sh
  7. 24 13
      shell/tests/run_com_exec.py

+ 11 - 10
CALC-COMPILE.md

@@ -1257,13 +1257,14 @@ and TEXT file output is done (`t91_textout`), both compile-only because the
 harness has no filesystem.  `Str` real formatting is done (`t92_strreal`),
 pointer types with `New`/`Dispose`/dereference are done (`t93_pointer`), and
 text `Eof`/`Readln` plus integer `Str` are done (`t94_textread`/`t95_strint`).
-**CALC.PAS compiles.**  The runtime is 9,373 bytes; the generated program is
-25,908 bytes of code and 19,694 bytes of data, with `DataStart=0xA000` and
-`CodeMax=0xA000`.  The next task is execution, not front-end coverage: the
-current boot harness still places stdin at memory `7000h`, below the larger
-image, so the loader must be moved before CALC.COM can run under qemu.  The
-original CALC.COM is 27,901 bytes; ours is larger because the runtime and
-code layout differ.
+**CALC.PAS compiles and runs.**  The runtime is 9,373 bytes; the generated
+program is 25,908 bytes of code and 19,694 bytes of data, with
+`DataStart=0xA000` and `CodeMax=0xA000`.  The boot harness now loads images at
+segment `0x1000` and puts its input descriptor at `0x9000`, so the larger image
+no longer collides with the boot sector/BIOS areas.  `tests/calc_smoke.py`
+boots CALC.COM under qemu, checks the welcome/grid/command menu, sends `/Q`,
+and requires exit code 0.  The original CALC.COM is 27,901 bytes; ours is
+35,284 bytes because the runtime and code layout differ.
 
 **Stage 14 — the CRT identifiers.** `GotoXY` (25), `LowVideo`/`NormVideo`
 (16 + 16), `ClrEol` (10), `ClrScr` (6), `KeyPressed` (3), `Delay` (1),
@@ -1292,7 +1293,7 @@ rest.
 
 **Stage 16 — the layout (§3, the layout track). DONE
 (`v-TP3-CALC-LAYOUT`).** The data area is no longer immediately above the
-code: globals live at image `8000h` (memory `8100h`), `initmem` zeroes them,
+code: globals live at image `0A000h` (memory `0A100h`), `initmem` zeroes them,
 and the `.COM` is code-only (`LinkSize = ImageBytes`). This emits CALC's
 measured 16,560 B of code inside the 24,000-byte code buffer and removes the
 4 KiB window. `t43_codewindow` is deliberately re-baselined (`ERR 99 5093`,
@@ -1307,9 +1308,9 @@ remaining run-time capacity note.
 | limit | value | CALC needs | verdict |
 |---|---|---|---|
 | `TextLimit` (source buffer) | 62,903 | `CALC.PAS` + `CALC.INC`, normalised = **62,415** | **488 bytes of slack** |
-| `MaxCode` | 24,000 | 16,560 B program + 4,384 B runtime = **20,944 B** | **OK, 3,056 B spare** (§3.2) |
+| `MaxCode` | 0A000h | 25,908 B program + 9,373 B runtime = **35,281 B** | **OK, 5,679 B spare** |
 | code window | `CodeMax = 0A000h`, capped by `MaxCode` | 25,908 B | **OK**; no data gap (§3.2) |
-| data area | fixed image `8000h`, zeroed by `initmem` | **12,032 B** (measured, §3.1) | **OK for compile**; see the run-time cargo note (§3.2) |
+| data area | fixed image `0A000h`, zeroed by `initmem` | **19,694 B** (generated) | **OK**; CALC boots under qemu |
 | stack | 16,384 min / 655,360 max | 16,384 min | matches the original's `minstk`/`maxstk` |
 | `MaxSym` | 3,000 | 47 routines + locals + params | **inferred** OK, verify |
 | `strOff`/`strLen` | `[0..255]` | 57 distinct string literals (67 occurrences) | OK |

+ 11 - 9
SUMMARY.md

@@ -1132,10 +1132,11 @@ where DOS puts it.
 
 **Layout:** the old fixed `rtSz + 1000H` data base is gone. `DataStart = 0A000h`,
 `CodeMax = 0A000h`, and `MaxCode = 0A000h` bound the code image; the `.COM`
-writer emits `ImageBytes()` and no padding. This is the flat-segment equivalent
-of the original's separate CS/DS layout (see `CALC-COMPILE.md` §3.2). It is
-enough to compile CALC; running CALC with its full 16,384-byte stack and
-16,281-byte `Pages` heap is still a separate capacity item.
+writer emits `ImageBytes()` and no padding. The boot loader defaults to
+`LOAD_SEG=0x1000`, input at `0x9000`, so the larger image is clear of the boot
+sector. This is the flat-segment equivalent of the original's separate CS/DS
+layout (see `CALC-COMPILE.md` §3.2). CALC boots and exits through its own
+command menu; running its full interactive/file features is a separate item.
 
 ### String literals — `writeln('hi')`
 
@@ -2044,7 +2045,7 @@ the compiler *emitting* a 16,560-byte program). The ratchet and front-end
 features through **file control-block operations and the original memory
 layout** are landed: the ladder (`tests/calc_ladder.sh` +
 `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`; CALC.PAS now stops at
-**CALC.PAS compiles** (code 25,908, data 19,694), the `{$I}` include,
+**CALC.PAS compiles and runs** (code 25,908, data 19,694), the `{$I}` include,
 `^X`/`#nn`, scalar typed constants, sets, enums, records (CellRec = 80),
 subranges, arrays, element/field access, file types, string value parameters,
 `Length`, the CRT identifiers (`ClrScr`/`GotoXY`/`ClrEol`/`LowVideo`/
@@ -2071,10 +2072,11 @@ landed, along with `Delete` and local `set of Char` variables.  The math runtime
 was compacted to guest Horner/square loops, reducing it from 14,063 to 9,203
 bytes.  `Insert`, CASE/ELSE trailing semicolons and string-vs-CHAR comparisons
 are also landed.  **CALC.PAS now compiles to its final `.`** with code 25,908
-and data 19,694.  The runtime is 9,373 bytes / 56 entries; the rt_exec
-scaffold is at HDR=0x4000 / data 0x5000.  The next task is running the larger
-image, not front-end coverage.  Items 1-4 below are independent of both
-tracks.
+and data 19,694.  The boot loader now uses segment 0x1000 with input at
+0x9000; `tests/calc_smoke.py` boots CALC.COM, checks the welcome/grid/menu and
+quits via `/Q` with exit 0.  The runtime is 9,373 bytes / 56 entries; the
+rt_exec scaffold is at HDR=0x4000 / data 0x5000.  Items 1-4 below are
+independent of both tracks.
 
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`

+ 62 - 0
shell/tests/calc_smoke.py

@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+"""Boot the compiled CALC.COM and quit it through its own command menu.
+
+The compile ladder proves the front-end reaches the final '.'.  This proves the
+linked image is a runnable program: it is loaded above the boot sector, prints
+the MicroCalc welcome/grid, accepts `/Q' in the command menu and exits 0.
+
+The loader is the high-image variant of bootcom.s (LOAD_SEG=0x1000, input at
+0x9000); the ordinary fixture harness uses the same default now, while
+rt_exec.py keeps the original low loader for its own driver image.
+"""
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SHELL = os.path.dirname(HERE)
+RES = os.path.join(os.path.dirname(SHELL), "Resources", "TP3")
+sys.path.insert(0, HERE)
+import run_com_exec as r  # noqa: E402
+
+
+def main():
+    td = tempfile.mkdtemp(prefix="tpcalc-")
+    try:
+        for name in ("CALC.PAS", "CALC.INC"):
+            shutil.copy(os.path.join(RES, name), os.path.join(td, name))
+        exe = r.ensure_comtest()
+        src = os.path.join(td, "CALC.PAS")
+        p = subprocess.run([exe], cwd=td, input=(src + "\n").encode(),
+                           capture_output=True)
+        coms = [os.path.join(td, n) for n in os.listdir(td)
+                if n.upper().endswith(".COM")]
+        if p.returncode != 0 or not coms:
+            sys.exit("comtest did not produce CALC.COM:\n%s"
+                     % p.stdout.decode(errors="replace"))
+        com = open(coms[0], "rb").read()
+        if len(com) > r.COM_MAX:
+            sys.exit("CALC.COM is %d bytes, past the loader's %d"
+                     % (len(com), r.COM_MAX))
+        boot = r.build_boot_sector()
+        img = os.path.join(td, "calc.img")
+        open(img, "wb").write(r.build_floppy(boot, com, b"\r\r/Q"))
+        out, rc, note = r.run_qemu(img)
+        code = r.exit_code_of(rc)
+        text = out.decode("latin-1")
+        missing = [s for s in ("Welcome to MicroCalc", "AutoCalc: ON",
+                               "Update, Quit") if s not in text]
+        if code != 0 or missing:
+            sys.exit("CALC did not start/quit cleanly: exit=%r note=%s missing=%r\n%s"
+                     % (code, note, missing, r.visible_output(out[:2000])))
+        print("  CALC.COM             PASS  com=%d bytes, exit=0, welcome/grid/menu"
+              % len(com))
+        return 0
+    finally:
+        shutil.rmtree(td, ignore_errors=True)
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 41 - 28
shell/tests/exec/bootcom.s

@@ -7,9 +7,9 @@
 # the serial port so a test harness can read it.
 #
 #   1. install an INT 21h vector pointing at a handler below
-#   2. INT 13h AH=02h: read the .COM off drive A: to 0000:0100
-#   3. SS:SP = 0000:FFFE, the segment top -- where DOS puts a .COM's stack
-#   4. JMP 0000:0100
+#   2. INT 13h AH=02h: read the .COM off drive A: to a scratch segment
+#   3. copy it to LOAD_SEG:0100 (default 1000h, above the boot/BIOS areas)
+#   4. SS:SP = LOAD_SEG:FFFE and JMP LOAD_SEG:0100
 #
 # The INT 21h handler implements the four functions this runtime actually
 # calls, and nothing else:
@@ -35,11 +35,12 @@
 #     qemu exit code 255, which is unambiguous.  Every byte on the serial port
 #     is therefore program output, with nothing to strip.
 #
-#   * The program is loaded at 0000:0100, so segment 0 holds the IVT at
-#     0000:0000-00FF and the program from 0100 up.  The input descriptor at
-#     7000h sits above the largest image that may reach 6F00h and below the
-#     boot sector at 7C00h, and is simply part of the disk image the harness
-#     writes, so the addresses here are assembly constants.
+#   * The default program segment is 1000h, so the image starts at 1000:0100
+#     and the boot sector/IVT stay untouched.  The input descriptor is at
+#     1000:9000, above the largest current image (CALC's code ends below 8A00h)
+#     and below the data area at 1000:A100.  It is simply part of the disk image
+#     the harness writes.  rt_exec.py overrides LOAD_SEG/INLEN back to 0/7000
+#     for its own driver image.
 #
 #     The handler owns no scratch inside the loaded region.  It used to keep
 #     the AH=09h cursor in memory at 0702h -- image offset 602h, inside every
@@ -168,15 +169,19 @@ _start:
 	movw	$SCRATCH_SEG, %ax
 	movw	%ax, %ds
 	xorw	%si, %si		# source 1000:0000
-	xorw	%ax, %ax
+	movw	$LOAD_SEG, %ax
 	movw	%ax, %es
-	movw	$0x0100, %di		# destination 0000:0100
+	movw	$0x0100, %di		# destination LOAD_SEG:0100
 	movw	$(COPY_BYTES / 2), %cx	# bytes / 2
 	cld
 	rep	movsw
-	xorw	%ax, %ax
-	movw	%ax, %ds		# the .COM expects DS=0
-	.byte	0xEA, 0x00, 0x01, 0x00, 0x00	# jmp 0000:0100
+	movw	$LOAD_SEG, %ax
+	movw	%ax, %ds		# the .COM expects DS=CS
+	movw	%ax, %es
+	movw	%ax, %ss
+	movw	$0xfffe, %sp
+	.byte	0xEA, 0x00, 0x01
+	.word	LOAD_SEG		# jmp LOAD_SEG:0100
 
 # ---------------------------------------------------------------- INT 21h
 # Called with the program's registers.  DS is the caller's data segment
@@ -373,25 +378,33 @@ ser_put:
 	ret
 
 # --------------------------------------------------------------- layout
-# The input descriptor lives at 7000h.  The image may reach 6F00h, the copy
-# ends at 7100h, and the boot sector at 7C00h is never touched; the read covers
-# enough sectors to bring the descriptor down with the image.  So it is simply
-# part of the disk image the harness writes, not something it has to
-# patch into this boot sector afterwards -- which means the addresses here are
-# assembly constants and the harness only has to know where they land in the
-# file.  See run_com_exec.py, FLAT_OFF.
+# The defaults are LOAD_SEG=1000h and INLEN=9000h: the image is copied to
+# 1000:0100, the input descriptor is at 1000:9000, and the data area is at
+# 1000:A100.  The copy and the boot sector are in different segments, so the
+# boot sector at 0000:7C00h is never touched.  The constants are assembly
+# constants and the harness only has to know where they land in the disk image.
+# See run_com_exec.py, FLAT_OFF.
 #
 # SCRATCH is where the sector read parks the bytes before the copy moves them
-# to 0100h.  Nothing may ever be placed there: the region is not reserved by
+# to LOAD_SEG:0100.  Nothing may ever be placed there: the region is not reserved by
 # this file, it is merely unused, and a future change that put a table at
 # 10000h would be overwritten by the read and would not notice.
-	.set	SECTORS, 72		# 36,864 bytes read; only COPY_BYTES is moved down
-	.set	COPY_BYTES, 0x7000	# up to the input, below the boot sector at 7C00h
-	.set	SCRATCH_SEG, 0x1000	# read buffer at 1000:0000, outside the 64K
-	.set	INLEN, 0x7000		# word: number of input bytes
-	.set	INCUR, 0x7002		# word: cursor, starts at INBUF
-	.set	INBUF, 0x7004		# the bytes themselves
-	.set	INMAX, 0x00fc		# 7100h - 7004h, the most that fits
+	.ifndef LOAD_SEG
+	.set	LOAD_SEG, 0x1000	# load above the boot sector/BIOS areas
+	.endif
+	.ifndef INLEN
+	.set	INLEN, 0x9000		# word: number of input bytes
+	.endif
+	.ifndef SCRATCH_SEG
+	.set	SCRATCH_SEG, LOAD_SEG + 0x1000
+	.endif
+	.ifndef COPY_BYTES
+	.set	COPY_BYTES, INLEN
+	.endif
+	.set	SECTORS, 72		# 36,864 bytes read; input is inside this
+	.set	INCUR, INLEN + 2	# word: cursor, starts at INBUF
+	.set	INBUF, INLEN + 4	# the bytes themselves
+	.set	INMAX, 0x00fc		# INLEN+100h - INBUF, the most that fits
 
 	.org	510
 	.byte	0x55, 0xAA

+ 2 - 2
shell/tests/rt_exec.py

@@ -926,7 +926,7 @@ def run_case(c, blob, entries, boot):
     img = build_image(blob, rec)
     with tempfile.TemporaryDirectory() as td:
         floppy = os.path.join(td, "f.img")
-        open(floppy, "wb").write(build_floppy(boot, img, c["in"]))
+        open(floppy, "wb").write(build_floppy(boot, img, c["in"], load_seg=0, inlen_off=0x7000, inbuf_off=0x7004))
         return run_qemu(floppy)
 
 
@@ -942,7 +942,7 @@ def main(argv):
     only = [a for a in argv if not a.startswith("-")]
 
     blob, entries, code_end = load_runtime(RT_BASE)
-    boot = build_boot_sector()
+    boot = build_boot_sector(load_seg=0, inlen=0x7000, copy_bytes=0x7000, scratch=0x1000)
 
     # Before any machine is started: the contract the driver depends on.
     nscan, nborrow = check_bp_contract(blob, entries, RT_BASE, code_end)

+ 1 - 0
shell/tests/run_all.sh

@@ -178,6 +178,7 @@ run "terminal config" tests/term_config.sh
 # forward by each stage.  It reuses the matrix's build, so it is not a second
 # harness with a second stale-compiler bug.
 run "CALC ladder"     tests/calc_ladder.sh
+run "CALC execution"  python3 tests/calc_smoke.py
 run "COM linker"      tests/run_com_tests.sh
 # The byte checks above can only ask "are these bytes well formed".  This one
 # asks "does the program do the right thing": every fixture with a .out is

+ 24 - 13
shell/tests/run_com_exec.py

@@ -57,11 +57,11 @@ TIMEOUT = 15
 # so these are written out here and checked against each other; if the
 # assembly's layout changes, the mismatch shows up as a loud failure rather
 # than as a program loaded somewhere harmless.
-LOAD_SEG = 0x0000        # the .COM's load segment
+LOAD_SEG = 0x1000        # the .COM's load segment
 LOAD_OFF = 0x0100        # ...and offset; DOS puts a .COM at CS:0100
 FLAT_LOAD = LOAD_SEG * 16 + LOAD_OFF
-INLEN_OFF = 0x7000       # word: how many input bytes follow
-INBUF_OFF = 0x7004       # the input bytes
+INLEN_OFF = 0x9000       # word: how many input bytes follow
+INBUF_OFF = 0x9004       # the input bytes
 INBUF_MAX = 0x00FC       # 7100h - 7004h
 SECTORS = 72             # what bootcom.s reads off the disk
 FLOPPY_SECTORS = 2880    # 1.44M, 18 sectors per track
@@ -72,12 +72,14 @@ COM_MAX = INLEN_OFF - LOAD_OFF  # the image must end before the input
 # boot sector reads whatever the BIOS left there.  This is a check on the two
 # constant blocks agreeing, and it is the reason they are both written down.
 READ_END = FLAT_LOAD + SECTORS * 512
-if INBUF_OFF + INBUF_MAX > READ_END:
+INLEN_FLAT = LOAD_SEG * 16 + INLEN_OFF
+INBUF_FLAT = LOAD_SEG * 16 + INBUF_OFF
+if INBUF_FLAT + INBUF_MAX > READ_END:
     sys.exit("run_com_exec.py: the input descriptor at %04X..%04X runs past the "
              "%d sectors bootcom.s reads (which end at %04X)"
              % (INBUF_OFF, INBUF_OFF + INBUF_MAX, SECTORS, READ_END))
 # ...and past the end of any .COM, or it would land inside the program.
-if FLAT_LOAD + COM_MAX > INBUF_OFF:
+if FLAT_LOAD + COM_MAX > INLEN_FLAT:
     sys.exit("run_com_exec.py: COM_MAX ends at %04X, past the input "
              "descriptor at %04X"
              % (FLAT_LOAD + COM_MAX, INBUF_OFF))
@@ -88,13 +90,21 @@ if FLAT_LOAD + COM_MAX > INBUF_OFF:
 EXIT_BASE = 1
 
 
-def build_boot_sector():
+def build_boot_sector(load_seg=None, inlen=None, copy_bytes=None, scratch=None):
     """Assemble tests/exec/bootcom.s and return its 512 bytes."""
     with tempfile.TemporaryDirectory() as td:
         obj, raw = os.path.join(td, "b.o"), os.path.join(td, "b.bin")
-        r = subprocess.run(["as", "--32", "-o", obj,
-                            os.path.join(HERE, "exec", "bootcom.s")],
-                           capture_output=True)
+        cmd = ["as", "--32"]
+        if load_seg is not None:
+            cmd += ["--defsym", "LOAD_SEG=%d" % load_seg]
+        if inlen is not None:
+            cmd += ["--defsym", "INLEN=%d" % inlen]
+        if copy_bytes is not None:
+            cmd += ["--defsym", "COPY_BYTES=%d" % copy_bytes]
+        if scratch is not None:
+            cmd += ["--defsym", "SCRATCH_SEG=%d" % scratch]
+        cmd += ["-o", obj, os.path.join(HERE, "exec", "bootcom.s")]
+        r = subprocess.run(cmd, capture_output=True)
         if r.returncode != 0:
             sys.exit("as failed:\n" + r.stderr.decode(errors="replace"))
         r = subprocess.run(["objcopy", "-O", "binary", obj, raw],
@@ -129,11 +139,12 @@ def to_file(mem_off):
     return mem_off - LOAD_OFF + 512
 
 
-def build_floppy(boot, com, stdin=b""):
+def build_floppy(boot, com, stdin=b"", load_seg=LOAD_SEG, inlen_off=INLEN_OFF, inbuf_off=INBUF_OFF):
     """Assemble the 1.44M image: boot sector, then the .COM, then the input."""
-    if len(com) > COM_MAX:
+    com_max = inlen_off - LOAD_OFF
+    if len(com) > com_max:
         sys.exit("the .COM is %d bytes and the boot sector reads only %d"
-                 % (len(com), COM_MAX))
+                 % (len(com), com_max))
     if len(stdin) > INBUF_MAX:
         sys.exit("%d bytes of input, at most %d fit" % (len(stdin), INBUF_MAX))
     img = bytearray(FLOPPY_BYTES)
@@ -141,7 +152,7 @@ def build_floppy(boot, com, stdin=b""):
     # The .COM goes at disk sector 2, which is where bootcom.s reads from.
     # to_file(LOAD_OFF) == 512, so this agrees with the mapping above.
     img[512:512 + len(com)] = com
-    inlen_f, inbuf_f = to_file(INLEN_OFF), to_file(INBUF_OFF)
+    inlen_f, inbuf_f = to_file(inlen_off), to_file(inbuf_off)
     img[inlen_f:inlen_f + 2] = len(stdin).to_bytes(2, "little")
     img[inbuf_f:inbuf_f + len(stdin)] = stdin
     return bytes(img)