Преглед на файлове

calc: file control-block operations (ladder 22706)

Exist's body now compiles: Assign, Reset, Rewrite, Close and IOResult are
builtins, backed by runtime entries 34..38.  The file variable grows to 132
bytes (handle, mode, 128-byte NUL-terminated name); Assign copies the counted
filename into it, Reset/Rewrite issue DOS AH=3Dh/3Ch, Close issues AH=3Eh, and
IOResult returns and clears the last status.

The boot floppy and Exec86 have no filesystem.  Both now answer AH=3Dh with
CF=1 and AX=2, the DOS "file not found" result, so t88_fileops (assign,
reset, IOResult, close) runs under qemu and Exec86.  rt_exec gets a
file_assign case that checks the copied name; the runtime golden is
re-blessed for the new entries and data offsets.  Eof and data read/write are
still refused (next file increment), and are not on this rung's ladder path.

The CALC ladder moves 22280 -> 22706; the next blocker is `var Line :
AnyString' in GetFileName, a VAR string parameter.  Runtime is now 3907 bytes
(39 entries), 45 bytes below the probe's HDR, so the next runtime addition
must move that scaffold.

Verified: compile matrix 89/89, ladder 1/1, 80 .COM linked, 78 executed under
both qemu and Exec86, rt_exec 276/276, nonvacuity 121/121, run_all ALL PASS.
Eric Streit преди 1 ден
родител
ревизия
d54d7cbbd7

+ 49 - 39
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **22280** | `assign(F, FileN)` in `Exist` — file operations (`ERR 41`) |
+| **22706** | `var Line : AnyString` in `GetFileName` — VAR string parameters (`ERR 102`) |
 
 Rungs behind it (now passed):
 
@@ -133,7 +133,8 @@ Rungs ahead:
 
 | position | blocker |
 |---|---|
-| 22280 | file operations (`Assign`, `Reset`, `Rewrite`, `Close`, `IoResult`, `EndOfFile`) |
+| 22280 | file operations (`Assign`, `Reset`, `Rewrite`, `Close`, `IoResult`) - done by the file control-block rung |
+| 22706 | `var Line : AnyString` - a by-reference string parameter |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -342,7 +343,7 @@ recipe above regenerates them, and the numbers are what this document keeps.
    grow into the other. CALC's code is **16,560 bytes**, which the old fixed
    4 KiB window could not emit; that was a port artefact, not a TP3 limit.
 2. **`MaxCode = 24,000` is not the binding constraint for the compile goal.**
-   16,560 + 3,667 (our current runtime) = 20,227, comfortably inside 24,000.
+   16,560 + 3,907 (our current runtime) = 20,467, comfortably inside 24,000.
    The old `dataBase` alone had to move; `t43_codewindow` was the fixture that
    policed it and has been re-baselined (§3.2).
 3. **The §3 data budget is confirmed, not inferred.** Reported data is
@@ -355,7 +356,7 @@ recipe above regenerates them, and the numbers are what this document keeps.
    **DS size `0315h` (12,624 B)**, and **heap/stack `0400h` (16,384 B)**, with
    `A000h` as the maximum (640 KiB, in paragraphs). A `.COM` still uses
    separate CS/DS segments; the original proves it.
-5. **The runtime is 11,341 bytes** in the original. Ours is 3,667 B. It
+5. **The runtime is 11,341 bytes** in the original. Ours is 3,907 B. It
    explains where the original's fixed `pc := $2D7C` / `dc := $0240` came
    from, and why `TPSRC7:81-82` looks the way it does.
 
@@ -422,7 +423,7 @@ inline initmem    mode 0006, CS 0813, DS 11D7,
 
 The file is runtime + code, exactly the shape we now emit; the data is in the
 separate DS described by the inline parameters. Our CALC image will be smaller
-in runtime (3,667 B instead of 11,341 B) but has the same code-only, high-data
+in runtime (3,907 B instead of 11,341 B) but has the same code-only, high-data
 shape. The ladder moved **20,912 → 22,253** on this change; the next blocker is
 not layout but the untyped `file` declaration; that is fixed in the next rung.
 
@@ -453,9 +454,10 @@ calling convention and string value parameters, `Length`, the CRT identifiers,
 subscripts, write field widths, WITH with n-dimensional subscripts, set-field
 assignment, string fields through DI, REAL constants, UpCase, run-time set
 membership, string fields in WRITE, real output, run-time set algebra,
-`Succ`/`Pred`, the original memory layout, and the untyped file type). The
-ladder has moved from the first typed constant at 7,791 to **22,280**, where
-`assign(F, FileN)` is the first file operation.
+`Succ`/`Pred`, the original memory layout, the untyped file type, and the
+file control-block operations). The
+ladder has moved from the first typed constant at 7,791 to **22,706**, where
+`var Line : AnyString` is the first by-reference string parameter.
 
 The rung commits and their tags, in order (each is one rung with its own
 fixture, red proof, and — where the harness allows — execution under qemu and
@@ -491,7 +493,8 @@ Exec86):
 | `17f7f7c` | `v-TP3-CALC-UPCASE` | **`ERR 102 18267`** (real output) |
 | `203f77e` | `v-TP3-CALC-SETOPS` | `ERR 98 20912` (layout limit reached) |
 | `ff05141` | `v-TP3-CALC-LAYOUT` | `ERR 1 22253` (untyped file, initially misread as a function) |
-| *(this commit)* | `v-TP3-CALC-UNTYPEDFILE` | **`ERR 41 22280`** (`assign`) |
+| `ed9e790` | `v-TP3-CALC-UNTYPEDFILE` | `ERR 41 22280` (`assign`) |
+| *(this commit)* | `v-TP3-CALC-FILEOPS` | **`ERR 102 22706`** (VAR string parameter) |
 
 - **Code HEAD** is the layout rung (`v-TP3-CALC-LAYOUT`).  Nothing is pushed.
   The terminal-config work (`v-TP3-TERMCFG`) and the editor key map
@@ -533,13 +536,16 @@ Exec86):
   Fixture `t64_arrays` (compile-only because it has no `.out`; its ~16 KB
   image now fits the raised 0x7000-byte read). `t14` re-baselined: the array
   DECLARES now; the refusal moved to element access.
-- **File TYPES are landed (Stage 13, first half).** `text` and `file of T`; a
-  file variable is our control block (`FileVarSize` = 4). Fixture `t66_files`.
-  This is the step that carried the compiler **inside `CALC.INC`**: the whole
+- **File TYPES and the control-block OPERATIONS are landed (Stage 13).**
+  `text`, `file of T`, and untyped `file`; a file variable is our control block
+  (`FileVarSize` = 132: handle, mode, 128-byte NUL-terminated name).  The
+  operations `Assign`/`Reset`/`Rewrite`/`Close`/`IoResult` are runtime entries
+  using real DOS `INT 21h`; `t88_fileops` runs the missing-file path under both
+  qemu and Exec86, and `rt_exec` pins the name copy.  Data `Read`/`Write` with
+  a leading file argument is the remaining file increment and is still loud.
+  File types were what carried the compiler **inside `CALC.INC`**: the whole
   `var` block passed and the `{$I}` expanded, so the rung jumped from 9202 to
-  **10350**, where the first blocker is a string VALUE parameter in a nested
-  procedure. File OPERATIONS (`Assign`/`Reset`/…, read/write with a file
-  argument) are the next increment and are loud until then.
+  **10350**.
 - **String VALUE parameters are landed** (TP3's whole-slot copy).  Three
   commits: slot sizes (`v-TP3-CALC-PARAMSLOTS`), the parameter list on the proc
   symbol (`v-TP3-CALC-PARAMLIST`), and the copy + `size > 2` for strings
@@ -599,7 +605,7 @@ Exec86):
   `ERR 1 22253`, terminal config **4/4**, 78 `.COM` linked and **76 executed
   under both qemu and Exec86** (`t64_arrays` remains compile-only because it
   has no `.out`).  `tests/nonvacuity.sh`: **121 ok, 0 failed**.  The runtime is
-  3,667 bytes (wrintw, real_from_int, real_add, real_sub, real_mul, real_div,
+  3,907 bytes (wrintw, real_from_int, real_add, real_sub, real_mul, real_div,
   real_cmp, real_int, real_fmt, real_val, real_write, set_op, set_cmp,
   set_copy; the golden was re-blessed) and `tests/rt_exec.py` passes
   **275/275**, including the MBF arithmetic/output/parse cases and eleven
@@ -662,10 +668,11 @@ commit.
 2. **The rest of the declaration part**, if the rung is still there: local
    `type`/`const`/`var` inside a nested function (`CALC.INC:767-778`, the
    enum-indexed array of strings), `Succ`/`Pred`/`Ord`/`Chr`.
-3. **File OPERATIONS** (`Assign`, `Reset`, `Rewrite`, `Close`, `EndOfFile`,
-   `IoResult`; `read`/`write` with a leading file argument).  Runtime entries
-   doing `INT 21h` open/create/read/write/close, text-mode newline and `^Z`.
-   The file *types* and variables are already in place (`FileVarSize`).
+3. **File OPERATIONS — control block DONE** (`v-TP3-CALC-FILEOPS`):
+   `Assign`, `Reset`, `Rewrite`, `Close`, `IoResult` are runtime entries over
+   real DOS `INT 21h`; `t88_fileops` checks the missing-file path under both
+   oracles.  **Still open:** `EndOfFile` and `read`/`write` with a leading file
+   argument, which need text-mode newline/`^Z` and typed-file block I/O.
 4. **Array subscripts and `with` — DONE** (`v-TP3-CALC-ARRVAR`,
    `v-TP3-CALC-WITH`): n-dimensional subscripts use per-dimension strides
    (the last varies fastest), the element's address rides in DI, and `with`
@@ -816,11 +823,11 @@ Then the two that are *outside* the ladder and gate the goal as stated:
       * **STILL TO COME:** function declarations/returns, then `Str`/`Val`
         wiring, real expressions (a 6-byte temporary/value model), real
         comparisons and real function results.
-      * **HEADROOM:** the runtime is 3,667 bytes and the probe's scaffold
+      * **HEADROOM:** the runtime is 3,907 bytes and the probe's scaffold
         starts at HDR=0x1100 with RT_BASE=0x0190, so the ceiling is 3,952
-        bytes - about 285 bytes left.  The boot read allows a .COM up to
-        0x6F00, so the next additions can move HDR again (e.g. 0x2000) with
-        plenty of room below the input at 0x7000.
+        bytes - only about 45 bytes left.  The next runtime addition must move
+        HDR (and the probe's data block with it); the boot read allows a .COM
+        up to 0x6F00, so there is room below the input at 0x7000.
       * **Exec86** needed the FE group (INC/DEC r/m8) and CMC/CLC/STC/LAHF/
         SAHF for the formatter; t84 is the first fixture that executes the
         real conversion under the interpreter, and it cross-checks qemu.
@@ -1090,20 +1097,23 @@ to the operand, or reused.
 **Stage 13 — files. Types DONE, including untyped `file`; operations next.**
 
 Done: `text`, `file of T`, and untyped `file` (`t87_untypedfile`).  A file
-variable is our own control block (`FileVarSize = 4`: the DOS handle word and a
-mode word), because this runtime
-does unbuffered `INT 21h` I/O where TP3 keeps a 128-byte buffer inside the
-variable - a recorded divergence.  `file of T` parses its element type so the
-RECORD size a Read/Write must move is known.  Fixture `t66_files`; this let the
-whole `var` block pass and the `{$I CALC.INC}` expand, which is why the ladder
-moved from `9202` to **`10350` - inside the include**.
-
-Not yet, and loud (unknown identifier): `Assign`, `Reset`, `Rewrite`, `Close`,
-`EndOfFile`, `IoResult`, and `read`/`write` with a leading file argument
-(`writeln (MCFile, Line)`, `Readln (H, Buffer^[count])`).  These need runtime
-entries doing `INT 21h` open/create/read/write/close, with text-mode newline
-and `^Z` semantics (`RESUME-TP3.md` §2).  `{$I-}`/`{$I+}` (Stage 2) already
-gate the checking that `IoResult` reports.
+variable is our own control block (`FileVarSize = 132`: the DOS handle word, a
+mode word, and a 128-byte NUL-terminated filename), because this runtime does
+unbuffered `INT 21h` I/O where TP3 keeps a 128-byte buffer inside the variable
+- a recorded divergence.  `file of T` parses its element type so the RECORD
+size a Read/Write must move is known.  Fixture `t66_files`.
+
+Control-block OPERATIONS are done (`t88_fileops`): `Assign` copies the counted
+filename, `Reset`/`Rewrite`/`Close` issue DOS `INT 21h` open/create/close, and
+`IoResult` reads and clears the status.  The boot harnesses have no filesystem,
+so both answer `AH=3Dh` with `CF=1, AX=2`; the fixture checks that error path
+under qemu and Exec86, and `rt_exec` pins the name copy.
+
+Not yet: `EndOfFile`, and `read`/`write` with a leading file argument
+(`writeln (MCFile, Line)`, `Readln (H, Buffer^[count])`).  These need
+text-mode newline/`^Z` semantics and typed-file block I/O (`RESUME-TP3.md`
+§2).  The next ladder blocker is not file data I/O, though: it is `var Line :
+AnyString`, a by-reference string parameter.
 
 **Stage 14 — the CRT identifiers.** `GotoXY` (25), `LowVideo`/`NormVideo`
 (16 + 16), `ClrEol` (10), `ClrScr` (6), `KeyPressed` (3), `Delay` (1),
@@ -1147,7 +1157,7 @@ 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 + 3,667 B runtime = **20,227 B** | **OK, 3,773 B spare** (§3.2) |
+| `MaxCode` | 24,000 | 16,560 B program + 3,907 B runtime = **20,467 B** | **OK, 3,533 B spare** (§3.2) |
 | code window | `CodeMax = 6F00h`, capped by `MaxCode` | 20,227 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) |
 | stack | 16,384 min / 655,360 max | 16,384 min | matches the original's `minstk`/`maxstk` |

+ 11 - 9
SUMMARY.md

@@ -2041,9 +2041,10 @@ census of what `CALC.PAS` actually needs and the reference measurements from
 the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
 verifiable on small fixtures) and the layout (§3, the only thing that stops
 the compiler *emitting* a 16,560-byte program). The ratchet and front-end
-features through **run-time set membership** are landed: the ladder
-(`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
-CALC.PAS now stops at `ERR 102` at pos 18267, `Write(Value:FW:DEC)` in `GotoCell`), the `{$I}` include,
+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
+`ERR 102` at pos 22706, `var Line : AnyString` in `GetFileName`), 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`/
@@ -2055,12 +2056,13 @@ WITH and n-dimensional subscripts (`t77_with`), set-field assignment and
 run-time `in` (`t78_setfield`), string fields through DI (`t79_strfield`),
 the measured 6-byte REAL constants (`t80_realconst`, with a byte-level check),
 `UpCase` and run-time set membership (`t81_upcase`, `t82_setin`), and
-`INT 10h`+`C6`+`D0-D3` in Exec86 (fixtures through `t82_setin`). The next rung
-is REAL output/arithmetic (the MBF core has started: `real_from_int`, entry
-21, is in the runtime and checked by 13 rt_exec cases); then the remaining string
-builtins, file operations, variable-index subscripts + `with`, pointers, `var`
-params, reals, recursion, and the layout change. Items 1-4 below are
-independent of both.
+real output (`t84_realwrite`), run-time set algebra and `Succ`/`Pred`
+(`t85`/`t86`), the original code-only `.COM` layout (§3.2), the untyped `file`
+type (`t87`), and file control-block operations (`t88`: `Assign`/`Reset`/
+`Rewrite`/`Close`/`IoResult`; data I/O is still refused). The next rung is a
+`var` string parameter. The runtime is 3,907 bytes / 39 entries and its probe
+scaffold is nearly full, so the next runtime addition must move HDR. 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)`

+ 104 - 4
shell/Compiler.mod

@@ -94,10 +94,13 @@ CONST
                                   field is scoped to its record, not global *)
    MaxArrs   = 1000 ;          (* array SHAPES, likewise: a symbol points at
                                   one, and 2-D indexing needs both ranges *)
-   FileVarSize = 4 ;           (* a file variable is our control block: the DOS
-                                  handle (word) and a mode word (0 closed, 1
-                                  read, 2 write).  TP3 keeps a buffer here too;
-                                  we do unbuffered INT 21h I/O. *)
+   FileVarSize = 132 ;         (* a file variable is our control block: the DOS
+                                  handle (word), a mode word (0 closed, 1 read,
+                                  2 write), then a 128-byte NUL-terminated
+                                  filename.  TP3 keeps its own name buffer and
+                                  a 128-byte data buffer here; we keep the name
+                                  and do unbuffered INT 21h I/O, with the data
+                                  buffer supplied by the caller. *)
 
    (* type codes (TP3 vartp) *)
    TNone    = 0 ;  TArray  = 1 ;  TRecord = 2 ;  TSet  = 3 ;
@@ -132,6 +135,12 @@ CONST
    BI_ClrScr  = 6 ;  BI_GotoXY = 7 ;  BI_LowVideo = 8 ;
    BI_NormVideo = 9 ; BI_ClrEol = 10 ; BI_Delay = 11 ;
    BI_KeyPressed = 12 ; BI_UpCase = 13 ; BI_Succ = 14 ; BI_Pred = 15 ;
+   (* File operations.  TP3 has these as standard procedures/functions; our
+      control block is Compiler.FileVarSize, so Assign stores the name for
+      the later Reset/Rewrite.  IOResult is an expression; EndOfFile is the
+      next file increment. *)
+   BI_Assign = 16 ; BI_Reset = 17 ; BI_Rewrite = 18 ;
+   BI_Close = 19 ; BI_IoResult = 20 ;
    (* the sub-codes EmCrtSeq takes *)
    CrtClr = 0 ; CrtDl = 1 ; CrtDh = 2 ; CrtCaret = 3 ; CrtEol = 4 ;
    CrtLow = 5 ; CrtNorm = 6 ; CrtDelay = 7 ; CrtKey = 8 ;
@@ -390,6 +399,11 @@ VAR
    TU_SetOp    : CARDINAL ;  (* runtime set operations, entries 31..33 *)
    TU_SetCmp   : CARDINAL ;
    TU_SetCopy  : CARDINAL ;
+   TU_FileAssign  : CARDINAL ;  (* file control block operations, entries 34..38 *)
+   TU_FileReset   : CARDINAL ;
+   TU_FileRewrite : CARDINAL ;
+   TU_FileClose   : CARDINAL ;
+   TU_IoResult    : CARDINAL ;
    TU_CmpStr   : CARDINAL ;  (* string comparison; two addresses on the stack,
                                 signed -1/0/+1 in AX *)
    TU_StrCat   : CARDINAL ;  (* string concatenation; dest and source addresses
@@ -3905,6 +3919,17 @@ BEGIN
          fixture had ever exercised the fold. *)
       ParseVar (r) ;
       RETURN
+   ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_IoResult) THEN
+      (* IOResult: the last file operation's DOS status; reading it clears the
+         slot in the runtime.  It is a no-argument function. *)
+      IF MatchDelim ('(') THEN
+         ExpectDelim (')', ENoSemi)
+      END ;
+      DropC (EmCall (TU_IoResult)) ;
+      r.kind := 2 ;
+      r.cls := TScalar ;
+      r.chr := FALSE ;
+      RETURN
    ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_UpCase) THEN
       (* UpCase(c): a CHAR function.  A constant folds; otherwise AL is
          mapped 'a'..'z' -> 'A'..'Z' and left alone outside that range.  The
@@ -4795,6 +4820,64 @@ BEGIN
       END ;
       RETURN
    END ;
+   IF which = BI_Assign THEN
+      (* Assign(F, Name): the file variable's address and the counted
+         string's address.  The runtime copies the characters into the file
+         control block and marks it closed; Reset/Rewrite do the DOS open.
+         Name is pushed first, so the runtime sees F at [BP+4] and Name at
+         [BP+6]. *)
+      IF MatchDelim ('(') THEN
+         ParseExpr (arg) ;
+         IF OK () AND (arg.kind = 1) AND
+            ((arg.cls = TFile) OR (arg.cls = TText) OR (arg.cls = TUntyp)) THEN
+            ExpectDelim (',', ENoSemi) ;
+            ParseExpr (dummy) ;
+            IF OK () THEN
+               IF dummy.cls = TString THEN
+                  EmPushStrAddr (dummy) ;
+                  EmPushVarAddr (symtab [arg.idx].local,
+                                 (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+                  DropC (EmCall (TU_FileAssign)) ;
+                  EmAddSp (4)
+               ELSE
+                  Err (ETypeErr)
+               END
+            END
+         ELSIF OK () THEN
+            Err (ETypeErr)
+         END ;
+         ExpectDelim (')', ENoSemi)
+      ELSE
+         Err (ENoSemi)
+      END ;
+      RETURN
+   END ;
+   IF (which = BI_Reset) OR (which = BI_Rewrite) OR (which = BI_Close) THEN
+      IF MatchDelim ('(') THEN
+         ParseExpr (arg) ;
+         IF OK () THEN
+            IF (arg.kind = 1) AND
+               ((arg.cls = TFile) OR (arg.cls = TText) OR (arg.cls = TUntyp)) THEN
+               EmPushVarAddr (symtab [arg.idx].local,
+                              (symtab [arg.idx].off + arg.boff) MOD 10000H) ;
+               IF which = BI_Reset THEN
+                  DropC (EmCall (TU_FileReset))
+               ELSIF which = BI_Rewrite THEN
+                  DropC (EmCall (TU_FileRewrite))
+               ELSE
+                  DropC (EmCall (TU_FileClose))
+               END ;
+               EmAddSp (2)
+            ELSE
+               Err (ETypeErr)
+            END
+         END ;
+         ExpectDelim (')', ENoSemi)
+      ELSE
+         Err (ENoSemi)
+      END ;
+      RETURN
+   END ;
    reading := (which = BI_Read) OR (which = BI_ReadLn) ;
    nArgs := 0 ;
    IF MatchDelim ('(') THEN
@@ -6633,6 +6716,18 @@ BEGIN
    i := NewSym ("SUCC", KBuiltin, BI_Succ, 0, 0, 0, 0, FALSE) ;
    i := NewSym ("PRED", KBuiltin, BI_Pred, 0, 0, 0, 0, FALSE) ;
    symtab [i].defnd := TRUE ;
+   (* File operations.  These are the names CALC actually uses; Eof, not
+      EndOfFile, because CALC declares a variable called EndOfFile. *)
+   i := NewSym ("ASSIGN", KBuiltin, BI_Assign, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("RESET", KBuiltin, BI_Reset, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("REWRITE", KBuiltin, BI_Rewrite, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("CLOSE", KBuiltin, BI_Close, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   i := NewSym ("IORESULT", KBuiltin, BI_IoResult, 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
    (* KBD: the console keyboard as a TEXT file.  This runtime's read entries
       already read the console, so `Read(Kbd, Ch)' drops the file argument and
       reads a character exactly as `Read(Ch)' does. *)
@@ -6789,6 +6884,11 @@ BEGIN
    TU_SetOp    := RT_Entry (31) ;
    TU_SetCmp   := RT_Entry (32) ;
    TU_SetCopy  := RT_Entry (33) ;
+   TU_FileAssign  := RT_Entry (34) ;
+   TU_FileReset   := RT_Entry (35) ;
+   TU_FileRewrite := RT_Entry (36) ;
+   TU_FileClose   := RT_Entry (37) ;
+   TU_IoResult    := RT_Entry (38) ;
    TU_WrStr    := RT_Entry (15) ;      (* a string variable is written through
                                           the address on the stack *)
    TU_RdStr    := RT_Entry (16) ;      (* readln(s)/read(s): TPSRC8 prdstr,

+ 11 - 2
shell/Exec86.mod

@@ -596,8 +596,10 @@ PROCEDURE DoInt21 ;
 (* The five services bootcom.s provides and the compiler emits, and nothing
    else.  Anything else faults, because the alternative is inventing DOS.
 
-   No flag is touched anywhere in here: IRET restores the caller's FLAGS, so
-   neither this code nor bootcom's CLC/STC is visible to the guest. *)
+   The console services leave the flags alone, matching an IRET that restores
+   the caller's FLAGS.  AH=3Dh is different: the runtime reads CF as the DOS
+   error flag, so the file-service path sets (or clears) it explicitly.  This
+   interpreter executes INT inline rather than modelling the pushed FLAGS. *)
 VAR ah, b, a, n, lim, stop : CARDINAL ;
     c : CHAR ;
     running : BOOLEAN ;
@@ -662,6 +664,13 @@ BEGIN
       END ;
       AX := (AX DIV 256) * 256 + b
 
+   | 3DH :                                   (* open file: no filesystem *)
+      (* The runtime uses real DOS AH=3Dh.  This interpreter has no
+         filesystem either; answer "File not found" the way DOS does:
+         CF=1 and AX=2.  The file-operation fixture checks that path. *)
+      AX := 2 ;
+      CF := TRUE
+
    | 04CH :                                  (* terminate *)
       halted := TRUE ;
       haltCode := AX MOD 256

+ 10 - 0
shell/Runtime.def

@@ -71,6 +71,16 @@ CONST
                                bit2 superset *)
    E_SetCopy = 33 ;         (* set copy: [BP+4]=size, [BP+6]=src,
                                [BP+8]=dest *)
+   E_FileAssign = 34 ;      (* [BP+4]=file control block, [BP+6]=
+                               <length><filename>; copy the name and close *)
+   E_FileReset = 35 ;       (* [BP+4]=file; DOS open read-only *)
+   E_FileRewrite = 36 ;     (* [BP+4]=file; DOS create/truncate *)
+   E_FileClose = 37 ;       (* [BP+4]=file; DOS close if open *)
+   E_IoResult = 38 ;        (* AX = last DOS status, then clear it *)
+
+   (* File control block, mirrored by Compiler.FileVarSize. *)
+   F_NameOff = 4 ;           (* handle word, mode word, then the name *)
+   F_NameMax = 127 ;
 
    (* Where a .COM's first byte ends up.  DOS loads a .COM at CS:0100 (the
       100 bytes below are the PSP), and CS = DS, so an image offset K is at

+ 132 - 3
shell/Runtime.mod

@@ -30,6 +30,11 @@ IMPLEMENTATION MODULE Runtime ;
      WrLn/RdLn/StackChk          nothing
      InitMem                     AX = offset of the program header word block
      ProgEnd/Halt                nothing; exits with code 0
+     FileAssign                  [BP+4]=file block, [BP+6]=<len><name>
+     FileReset/Rewrite/Close     [BP+4]=file block
+     IoResult                    no argument; AX = last DOS status, cleared
+   The file operations use real DOS INT 21h calls and leave their status in
+   the runtime's D_IORES word.
    All entries preserve BP and SP, and every register except the documented
    result, so they can be called from the middle of an expression. *)
 
@@ -92,7 +97,8 @@ CONST
    D_FMTPAD = 189 ;   (* 2 bytes: left padding *)
    D_FMTTOT = 191 ;   (* 2 bytes: pad + length *)
    D_ASCSRC = 193 ;   (* 2 bytes: ascreal's source pointer *)
-   D_END   = 195 ;    (* the block is padded to here *)
+   D_IORES = 195 ;    (* 2 bytes: the last file-operation DOS status *)
+   D_END   = 197 ;    (* the block is padded to here *)
 
    (* THE LOAD BIAS.  Everything above is an offset into the image as this
       compiler numbers it: the entry JMP is at image 0.  But a DOS .COM is
@@ -149,7 +155,7 @@ VAR
    dataAt : CARDINAL ;
    rtBase : CARDINAL ;
    built  : BOOLEAN ;
-   entNm  : ARRAY [0..33] OF ARRAY [0..MaxNm] OF CHAR ;
+   entNm  : ARRAY [0..38] OF ARRAY [0..MaxNm] OF CHAR ;
 
 (* ---------------------------------------------------------------- *)
 (*  name helpers                                                     *)
@@ -2902,6 +2908,119 @@ BEGIN
 END EmitSetOps ;
 
 
+PROCEDURE EmitFileOps ;
+(* File control-block operations.  The block is Compiler.FileVarSize:
+      +0  DOS handle (FFFFh when closed)
+      +2  mode (0 closed, 1 read, 2 write)
+      +4  128-byte NUL-terminated filename
+   Assign copies the counted string into +4; Reset/Rewrite/Close issue the DOS
+   calls and leave their status in D_IORES.  IOResult reads and clears that
+   word.  Eof is a later increment: it needs position/size arithmetic, and the
+   compile ladder has not reached it yet.
+
+   The caller pushes arguments left-to-right before the CALL, so [BP+4] is the
+   last push.  assign takes the filename first and the file block last:
+   [BP+4]=file, [BP+6]=<length><filename>.  The one-argument entries take the
+   file block at [BP+4]. *)
+BEGIN
+   (* Assign(F, Name) *)
+   M ("file_assign") ;
+   PushBp ; MovBpSp ;
+   PushBx ; PushCx ; PushDx ; PushSi ; PushDi ;
+   MovDiBp4 ;                         (* DI = file block *)
+   MovAxImm (0FFFFH) ; StDiAx ;       (* handle := invalid *)
+   XorAxAx ;
+   B (89H) ; B (45H) ; B (2) ;        (* MOV [DI+2],AX: mode := closed *)
+   MovSiBp6 ;                         (* SI = counted filename *)
+   MovAlSi ;                          (* AL := length byte *)
+   IncSi ;
+   B (3CH) ; B (7FH) ;                (* CMP AL,127 *)
+   Jbe8 ("fa_len") ;
+   MovAlD (127) ;                     (* clamp to the name buffer *)
+   M ("fa_len") ;
+   MovClAl ;                          (* CL := length *)
+   B (30H) ; B (0EDH) ;               (* XOR CH,CH: CX := length *)
+   B (83H) ; B (0C7H) ; B (4) ;       (* ADD DI,4: name buffer *)
+   Jcxz8 ("fa_nul") ;
+   M ("fa_copy") ;
+   LdAlSi ; StDiAl ;                  (* copy one byte, no string instr *)
+   IncSi ;
+   B (47H) ;                          (* INC DI *)
+   Loop8 ("fa_copy") ;
+   M ("fa_nul") ;
+   B (0C6H) ; B (05H) ; B (0) ;       (* MOV byte [DI],0: NUL *)
+   PopDi ; PopSi ; PopDx ; PopCx ; PopBx ;
+   MovSpBp ; PopBp ; RetR ;
+
+   (* Reset(F): AX=3D00h, DS:DX=name *)
+   M ("file_reset") ;
+   PushBp ; MovBpSp ;
+   PushBx ; PushCx ; PushDx ; PushDi ;
+   MovDiBp4 ;
+   B (8DH) ; B (55H) ; B (4) ;        (* LEA DX,[DI+4] *)
+   MovAxImm (3D00H) ; Int21 ;
+   Jb8 ("fr_err") ;
+   StDiAx ;                           (* handle := AX *)
+   MovAxImm (1) ; B (89H) ; B (45H) ; B (2) ;   (* mode := read *)
+   XorAxAx ; StDataAx (D_IORES) ;
+   Jmp8 ("fr_out") ;
+   M ("fr_err") ;
+   B (89H) ; B (0C3H) ;               (* MOV BX,AX: keep the DOS status *)
+   MovAxImm (0FFFFH) ; StDiAx ;
+   XorAxAx ; B (89H) ; B (45H) ; B (2) ;        (* mode := closed *)
+   StDataBx (D_IORES) ;
+   M ("fr_out") ;
+   PopDi ; PopDx ; PopCx ; PopBx ;
+   MovSpBp ; PopBp ; RetR ;
+
+   (* Rewrite(F): AX=3C00h, CX=0, DS:DX=name *)
+   M ("file_rewrite") ;
+   PushBp ; MovBpSp ;
+   PushBx ; PushCx ; PushDx ; PushDi ;
+   MovDiBp4 ;
+   B (8DH) ; B (55H) ; B (4) ;        (* LEA DX,[DI+4] *)
+   XorCxCx ;
+   MovAxImm (3C00H) ; Int21 ;
+   Jb8 ("fw_err") ;
+   StDiAx ;
+   MovAxImm (2) ; B (89H) ; B (45H) ; B (2) ;   (* mode := write *)
+   XorAxAx ; StDataAx (D_IORES) ;
+   Jmp8 ("fw_out") ;
+   M ("fw_err") ;
+   B (89H) ; B (0C3H) ;
+   MovAxImm (0FFFFH) ; StDiAx ;
+   XorAxAx ; B (89H) ; B (45H) ; B (2) ;
+   StDataBx (D_IORES) ;
+   M ("fw_out") ;
+   PopDi ; PopDx ; PopCx ; PopBx ;
+   MovSpBp ; PopBp ; RetR ;
+
+   (* Close(F): AX=3E00h, BX=handle; a closed block needs no DOS call *)
+   M ("file_close") ;
+   PushBp ; MovBpSp ;
+   PushBx ; PushDx ; PushDi ;
+   MovDiBp4 ;
+   LdAxDi ;
+   CmpAxImm (0FFFFH) ;
+   Je8 ("fc_out") ;
+   B (89H) ; B (0C3H) ;               (* BX := handle *)
+   MovAh (3EH) ; Int21 ;
+   B (89H) ; B (0C3H) ;               (* BX := status *)
+   MovAxImm (0FFFFH) ; StDiAx ;
+   XorAxAx ; B (89H) ; B (45H) ; B (2) ;
+   StDataBx (D_IORES) ;
+   M ("fc_out") ;
+   PopDi ; PopDx ; PopBx ;
+   MovSpBp ; PopBp ; RetR ;
+
+   (* IOResult: AX := D_IORES; D_IORES := 0 *)
+   M ("ioresult") ;
+   MovAxData (D_IORES) ;
+   StDataImm (D_IORES, 0) ;
+   RetR ;
+
+END EmitFileOps ;
+
 PROCEDURE EmitData ;
 (* The runtime's data block.  The D_ constants name the offsets in it and are
    ASSERTED against what is emitted here, three ways: the starting offset of
@@ -3035,6 +3154,10 @@ BEGIN
       HALT
    END ;
    B (0) ; B (0) ;
+   IF dataAt + D_IORES # rpos THEN
+      HALT
+   END ;
+   B (0) ; B (0) ;                 (* the last file-operation status *)
    WHILE rpos < dataAt + D_END DO
       B (0)
    END ;
@@ -3105,6 +3228,7 @@ BEGIN
    EmitRdStr ; EmitStrStore ; EmitCmpStr ; EmitStrCat ;
    EmitRealFromInt ; EmitRealAddSub ; EmitRealMul ;
    EmitRealCmp ; EmitRealInt ; EmitRealConv ; EmitSetOps ;
+   EmitFileOps ;
    EmitData ;
    FixUp ;
    SetStr (entNm [0], "initmem") ;
@@ -3141,6 +3265,11 @@ BEGIN
    SetStr (entNm [31], "set_op") ;
    SetStr (entNm [32], "set_cmp") ;
    SetStr (entNm [33], "set_copy") ;
+   SetStr (entNm [34], "file_assign") ;
+   SetStr (entNm [35], "file_reset") ;
+   SetStr (entNm [36], "file_rewrite") ;
+   SetStr (entNm [37], "file_close") ;
+   SetStr (entNm [38], "ioresult") ;
    built := TRUE
 END RT_Build ;
 
@@ -3168,7 +3297,7 @@ BEGIN
    IF NOT built THEN
       RT_Build (0)
    END ;
-   IF i > 33 THEN
+   IF i > 38 THEN
       RETURN 0
    END ;
    RETURN rtBase + LblOff (entNm [i])

+ 8 - 3
shell/tests/RtProbe.mod

@@ -108,7 +108,7 @@ END PENT ;
 
 PROCEDURE Main ;
 VAR i, n, base : CARDINAL ;
-    names : ARRAY [0..33] OF ARRAY [0..15] OF CHAR ;
+    names : ARRAY [0..38] OF ARRAY [0..15] OF CHAR ;
 BEGIN
    base := GetBase () ;
    RT_Build (base) ;
@@ -121,7 +121,7 @@ BEGIN
    PCARD (RT_Size ()) ; PS (" bytes") ; NL ;
    PS ("code ends at ") ; PCARD (RT_CodeEnd ()) ; NL ;
    i := 0 ;
-   WHILE i <= 33 DO
+   WHILE i <= 38 DO
       names [i] := "" ;
       INC (i)
    END ;
@@ -146,8 +146,13 @@ BEGIN
    names [31] := "set_op" ;
    names [32] := "set_cmp" ;
    names [33] := "set_copy" ;
+   names [34] := "file_assign" ;
+   names [35] := "file_reset" ;
+   names [36] := "file_rewrite" ;
+   names [37] := "file_close" ;
+   names [38] := "ioresult" ;
    i := 0 ;
-   WHILE i <= 33 DO
+   WHILE i <= 38 DO
       PENT (i, names [i]) ;
       INC (i)
    END ;

+ 7 - 2
shell/tests/audit_helpers.py

@@ -151,6 +151,11 @@ NON_ONE_LINE = {
         "SubAl": PARAM, "CmpAl": PARAM, "AddDl": PARAM, "J8": COMP,
         "Jcc": COMP, "CmpCxV": PARAM,
         "B": DATA, "C8": DATA,
+        # File control-block operations: a multi-instruction routine with
+        # labels and raw DOS sequences, not a one-line ModRM helper.  Its
+        # bytes are pinned by rt_exec's file_assign case and the runtime
+        # golden; the name grammar cannot read a whole routine.
+        "EmitFileOps": COMP,
     },
     "Compiler.mod": {
         "AddSp": PARAM, "SubSp": PARAM, "Setcc": PARAM,
@@ -751,7 +756,7 @@ def sweep(src, mod, verbose):
     # Its D_ offsets are asserted against this block by check_runtime.py's
     # golden, so "excluded from the name audit" is not "unchecked": the bytes
     # are pinned, only the name-to-opcode correspondence does not apply.
-    excluded = {"EmitData"}
+    excluded = {"EmitData", "EmitFileOps"}
     if verbose:
         print("-- %s" % mod)
         print("name                 bytes            FCML decode")
@@ -960,7 +965,7 @@ def sweep(src, mod, verbose):
     # Its D_ offsets are asserted against this block by check_runtime.py's
     # golden, so "excluded from the name audit" is not "unchecked": the bytes
     # are pinned, only the name-to-opcode correspondence does not apply.
-    excluded = {"EmitData"}
+    excluded = {"EmitData", "EmitFileOps"}
     strip = (lambda s: s[2:]) if mod != "Runtime.mod" else (lambda s: s)
     # The table is written in the names the audit uses (Runtime.mod's own
     # names, Compiler.mod's with the Em prefix already removed), so it is NOT

+ 40 - 0
shell/tests/exec/bootcom.s

@@ -211,6 +211,8 @@ handler:
 	je	.Lh08
 	cmpb	$0x0b, %ah
 	je	.Lh0b
+	cmpb	$0x3d, %ah
+	je	.Lh3d
 	cmpb	$0x4c, %ah
 	je	.Lh4c
 	stc				# unknown function
@@ -277,6 +279,16 @@ handler:
 	clc
 	jmp	.Ldone8
 
+.Lh3d:					# open file: no filesystem in this harness
+	# The runtime uses real DOS AH=3Dh.  A boot floppy has no DOS and no
+	# filesystem driver, so answer the one question the file-operation
+	# fixture asks - "the file is absent" - the way DOS does: CF=1,
+	# AX=2 (File not found).  The result must survive the epilogue, and
+	# CF must be visible to the guest, so this uses .LdoneFile.
+	movw	$2, %ax
+	stc
+	jmp	.LdoneFile
+
 .Lh4c:					# terminate: exit with AL as the code
 	movw	$0x0501, %dx		# isa-debug-exit
 	outb	%al, %dx
@@ -311,6 +323,34 @@ handler:
 #
 # AX is therefore popped only on the paths where it is not a result, and
 # AH=02h/09h (which also leave AL alone in DOS) keep using .Ldone.
+.LdoneFile:
+	# AX is the result and CF is the DOS error flag.  POP does not change
+	# CF, so restore the saved registers first, then copy CF into the saved
+	# FLAGS word before dropping the saved AX and IRETing.  The 16-bit ADD
+	# used to discard AX would clobber CF, so set FLAGS before it.
+	popw	%es
+	popw	%ds
+	popw	%di
+	popw	%si
+	popw	%dx
+	popw	%cx
+	popw	%bx
+	# 16-bit addressing has no [SP] form.  Borrow BX as the frame pointer
+	# for two instructions; POP does not affect CF, so the branch still sees
+	# the handler's result.  After the push, SP points at the saved BX, so
+	# saved AX is +2 and saved FLAGS are +8.
+	pushw	%bx
+	movw	%sp, %bx
+	jnc	.Ldf_ok
+	orw	$1, 8(%bx)		# saved FLAGS: set CF
+	jmp	.Ldf_drop
+.Ldf_ok:
+	andw	$0xfffe, 8(%bx)		# saved FLAGS: clear CF
+.Ldf_drop:
+	popw	%bx
+	addw	$2, %sp		# drop the saved AX, keep the result
+	iret
+
 .Ldone8:
 	popw	%es
 	popw	%ds

+ 5 - 0
shell/tests/exec/rtdrv.s

@@ -151,6 +151,7 @@
 	.set	R_BLK34, 0x1520
 	.set	R_BLK35, 0x1530
 	.set	R_BLK36, 0x1540
+	.set	FILEBLK, 0x1A00	# the 132-byte file control block
 	.set	SETA, 0x1580
 	.set	SETB, 0x1590
 	.set	SETC, 0x15A0
@@ -617,3 +618,7 @@ HDR:
 	.byte	0x18, 0x03
 	.org	SET2D
 	.byte	0x00, 0x00
+
+# File control block for the file_assign case.
+	.org	FILEBLK
+	.zero	132

+ 9 - 9
shell/tests/fixtures/calc_ladder.tsv

@@ -25,12 +25,12 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 #
-# Current rung (2026-10-22): the untyped `file' type in CALC.INC's Exist
-# (`var F : file;').  UpCase, run-time set membership, string fields through
-# DI, the original memory layout and now untyped-file declarations moved the
-# ladder 15911 -> 18267 -> 19847 -> 20912 -> 22253 -> 22280.  The function
-# declaration itself was never the blocker: the parser required `of T' after
-# FILE and reported ENoSemi at the semicolon inside Exist's var block.  The
-# next blocker is the file-operation call `assign(F, FileN)', which is not a
-# builtin yet (TP3 error 41).
-CALC.PAS	ERR	41	22280
+# Current rung (2026-10-22): `var Line : AnyString' on CALC.INC's GetFileName.
+# UpCase, run-time set membership, string fields through DI, the original
+# memory layout, untyped-file declarations and the file control-block
+# operations moved the ladder 15911 -> 18267 -> 19847 -> 20912 -> 22253 ->
+# 22280 -> 22706.  The function declaration itself was never the blocker;
+# `file' required an OF, and then Assign/Reset/IOResult/Close were missing.
+# The next blocker is a VAR parameter of string type, which ProcFunc refuses
+# with ENoLib (TP3 error 102).
+CALC.PAS	ERR	102	22706

+ 10 - 2
shell/tests/fixtures/expected.tsv

@@ -519,7 +519,7 @@ t65_array_access	OK	106	17
 #                   element type for the record size a Read/Write moves.
 #                   data=12 = two file variables (4 each) + the two @@T
 #                   temporaries.  File OPERATIONS are the next increment.
-t66_files	OK	55	13
+t66_files	OK	55	269
 #   t67_strparam           a STRING VALUE parameter - CALC.INC:17.  TP3 puts a
 #                   value parameter's whole size on the stack, so a string[20]
 #                   parameter is 21 bytes and the caller COPIES <len><chars>
@@ -628,4 +628,12 @@ t86_succ	OK	115	8
 #                   variable is the same four-byte control block as a typed
 #                   file but carries no element size; data Read/Write through
 #                   it remains refused.
-t87_untypedfile	OK	62	9
+t87_untypedfile	OK	62	137
+
+#   t88_fileops          file control-block operations: assign copies the
+#                   counted name, reset calls DOS AH=3Dh, IOResult reads and
+#                   clears the status, close releases the handle.  The
+#                   boot harnesses have no filesystem, so the fixture exercises
+#                   the missing-file path (CF=1, AX=2) in both oracles; the
+#                   success path needs real DOS.
+t88_fileops	OK	109	137

+ 1 - 0
shell/tests/fixtures/t88_fileops.out

@@ -0,0 +1 @@
+TRUE

+ 26 - 0
shell/tests/fixtures/t88_fileops.pas

@@ -0,0 +1,26 @@
+program t88_fileops;
+
+{ t88 -- the file control-block operations CALC.INC's Exist uses:
+
+      assign(F, 'T88.DAT');
+      reset(F);       (with I/O checking disabled)
+      if IOResult <> 0 then ...
+      close(F)
+
+  Assign copies the counted filename into the file variable, Reset asks DOS
+  to open it, IOResult returns the DOS status and clears it, and Close closes
+  the handle.  The boot harnesses have no filesystem, so this fixture checks
+  the missing-file path: both oracles answer AH=3Dh with CF set and AX=2,
+  exactly as DOS does when the file is absent.  The success path needs a real
+  filesystem and is not executed here. }
+
+var
+  F : file;
+begin
+  assign (F, 'T88.DAT') ;
+  {$I-}
+  reset (F) ;
+  {$I+}
+  writeln (IOResult <> 0) ;
+  close (F)
+end.

+ 4 - 4
shell/tests/nonvacuity.sh

@@ -1804,20 +1804,20 @@ cp "$SAVED_C" Compiler.mod
 
 # --- the file variable's size ---------------------------------------------
 # Our file control block is FileVarSize bytes; shrink it and every file
-# variable's slot moves, so t66's `data=12' becomes `data=8'.  Visible at
+# variable's slot moves, so t66's `data=269' becomes `data=265'.  Visible at
 # compile time, so the matrix carries it.
 cp "$SAVED_C" Compiler.mod
 if python3 - <<'PYX'
 p = 'Compiler.mod'
 s = open(p).read()
-old = 'FileVarSize = 4 ;'
+old = 'FileVarSize = 132 ;'
 assert s.count(old) == 1, 'the file variable size found %d times -- update this mutation' % s.count(old)
-open(p, 'w').write(s.replace(old, 'FileVarSize = 2 ;'))
+open(p, 'w').write(s.replace(old, 'FileVarSize = 130 ;'))
 PYX
 then
     if rebuild_compiler; then
         expect_red "t66 red when the file variable is the wrong size" \
-            "expected: OK 55 13" tests/run_compile_tests.sh
+            "expected: OK 55 269" tests/run_compile_tests.sh
     else
         echo "  FAIL: the compiler would not rebuild with the file size changed"
         fail=$((fail + 1))

+ 10 - 1
shell/tests/rt_exec.py

@@ -82,6 +82,7 @@ SST_WORLD = 0x1220
 AXOUT = 0x11A6          # the driver stores the last call's AX here
 FMTDEST = 0x1430        # a 64-byte string for the real_fmt cases
 ASCSRC = 0x1470          # the ASCIIZ strings for real_val
+FILEBLK = 0x1A00         # the 132-byte file control block
 REALDST = 0x0FA8
 
 # A .COM is loaded at CS:0100, and every address in the image is an image
@@ -110,7 +111,9 @@ ENTS = ["initmem", "progend", "stackchk", "wrint", "wrchar", "wrbool",
         "wrtinl", "rdbyte", "wrstr", "rdstr", "strstore", "cmpstr", "strcat",
         "wrintw", "real_from_int", "real_add", "real_sub", "real_mul",
         "real_div", "real_cmp", "real_int", "real_fmt", "real_val",
-        "real_write", "set_op", "set_cmp", "set_copy"]
+        "real_write", "set_op", "set_cmp", "set_copy",
+        "file_assign", "file_reset", "file_rewrite", "file_close",
+        "ioresult"]
 
 # A call slot's flags, as rtdrv.s reads them.
 F_STACK = 1     # the argument goes on the stack, popped by the caller
@@ -482,6 +485,12 @@ def build_cases():
         dumpbytes=b"\x00" * DLEN,
         note="the data area is poisoned to AA first, so a zero is a result")
 
+    add("file_assign copies the name",
+        [s2("file_assign", FILEBLK + LOAD_BIAS, SST_HELLO + LOAD_BIAS)],
+        dump=10, dumpbytes=b"\xff\xff\x00\x00hello\x00",
+        dumpadr=FILEBLK,
+        note="handle=FFFF, mode=0, then the NUL-terminated name")
+
     for v, why in [(0, "the one value where a signed/unsigned slip is invisible"),
                    (1, ""), (-1, ""), (7, ""), (10, ""), (42, ""),
                    (100, ""), (12345, ""), (-32768, "the sign bit"), (32767, "")]:

+ 218 - 94
shell/tests/runtime.golden

@@ -41,7 +41,7 @@
 0036  58          pop ax
 0037  F7 D8       neg ax
 0039  B9 0A 00    mov cx,0ah
-003C  BB 98 0E    mov bx,0e98h
+003C  BB 86 0F    mov bx,0f86h
 003F  89 DE       mov si,bx
 0041  31 D2       xor dx,dx
 0043  F7 F1       div ax,cx
@@ -93,7 +93,7 @@
 00A2  58          pop ax
 00A3  F7 D8       neg ax
 00A5  B9 0A 00    mov cx,0ah
-00A8  BB 98 0E    mov bx,0e98h
+00A8  BB 86 0F    mov bx,0f86h
 00AB  89 DE       mov si,bx
 00AD  31 D2       xor dx,dx
 00AF  F7 F1       div ax,cx
@@ -125,18 +125,18 @@
 00DE  83 7E 04 00 cmp word ptr [bp+4h],0h
 00E2  89 EC       mov sp,bp
 00E4  75 05       jne 0ebh
-00E6  BA 9D 0E    mov dx,0e9dh
+00E6  BA 8B 0F    mov dx,0f8bh
 00E9  EB 03       jmp 0eeh
-00EB  BA 98 0E    mov dx,0e98h
+00EB  BA 86 0F    mov dx,0f86h
 00EE  B4 09       mov ah,9h
 00F0  CD 21       int 21h
 00F2  5D          pop bp
 00F3  C3          ret
-00F4  BA A6 0E    mov dx,0ea6h
+00F4  BA 94 0F    mov dx,0f94h
 00F7  B4 09       mov ah,9h
 00F9  CD 21       int 21h
 00FB  C3          ret
-00FC  BA A3 0E    mov dx,0ea3h
+00FC  BA 91 0F    mov dx,0f91h
 00FF  B4 09       mov ah,9h
 0101  CD 21       int 21h
 0103  C3          ret
@@ -303,12 +303,12 @@
 0217  EB EF       jmp 208h
 0219  58          pop ax
 021A  C3          ret
-021B  8B 1E AC 0E mov bx,word ptr [0each]
+021B  8B 1E 9A 0F mov bx,word ptr [0f9ah]
 021F  83 FB 00    cmp bx,0h
 0222  74 09       je 22dh
 0224  8A C3       mov al,bl
 0226  31 DB       xor bx,bx
-0228  89 1E AC 0E mov word ptr [0each],bx
+0228  89 1E 9A 0F mov word ptr [0f9ah],bx
 022C  C3          ret
 022D  B4 08       mov ah,8h
 022F  CD 21       int 21h
@@ -316,7 +316,7 @@
 0232  88 C2       mov dl,al
 0234  BB 01 00    mov bx,1h
 0237  8A DA       mov bl,dl
-0239  89 1E AC 0E mov word ptr [0each],bx
+0239  89 1E 9A 0F mov word ptr [0f9ah],bx
 023D  C3          ret
 023E  55          push bp
 023F  8B EC       mov bp,sp
@@ -543,7 +543,7 @@
 03C2  8B 4D 06    mov cx,word ptr [di+6h]
 03C5  8B 75 08    mov si,word ptr [di+8h]
 03C8  8B 7D 0A    mov di,word ptr [di+0ah]
-03CB  C7 06 AE 0E 00 00 mov word ptr [0eaeh],0h
+03CB  C7 06 9C 0F 00 00 mov word ptr [0f9ch],0h
 03D1  E8 4D 00    call 421h
 03D4  5F          pop di
 03D5  89 05       mov word ptr [di],ax
@@ -572,7 +572,7 @@
 03FE  8B 4D 06    mov cx,word ptr [di+6h]
 0401  8B 75 08    mov si,word ptr [di+8h]
 0404  8B 7D 0A    mov di,word ptr [di+0ah]
-0407  C7 06 AE 0E 00 80 mov word ptr [0eaeh],8000h
+0407  C7 06 9C 0F 00 80 mov word ptr [0f9ch],8000h
 040D  E8 11 00    call 421h
 0410  5F          pop di
 0411  89 05       mov word ptr [di],ax
@@ -587,7 +587,7 @@
 0420  C3          ret
 0421  80 F9 00    cmp cl,0h
 0424  74 0E       je 434h
-0426  33 3E AE 0E xor di,word ptr [0eaeh]
+0426  33 3E 9C 0F xor di,word ptr [0f9ch]
 042A  3C 00       cmp al,0h
 042C  75 07       jne 435h
 042E  89 C8       mov ax,cx
@@ -599,16 +599,16 @@
 0439  91          xchg ax,cx
 043A  87 F3       xchg bx,si
 043C  87 FA       xchg dx,di
-043E  88 0E B2 0E mov byte ptr [0eb2h],cl
+043E  88 0E A0 0F mov byte ptr [0fa0h],cl
 0442  28 C1       sub cl,al
 0444  80 F9 28    cmp cl,28h
 0447  72 06       jb 44fh
-0449  8A 0E B2 0E mov cl,byte ptr [0eb2h]
+0449  8A 0E A0 0F mov cl,byte ptr [0fa0h]
 044D  EB DF       jmp 42eh
-044F  89 3E AE 0E mov word ptr [0eaeh],di
-0453  80 26 AF 0E 80 and byte ptr [0eafh],80h
-0458  89 3E B0 0E mov word ptr [0eb0h],di
-045C  30 36 B1 0E xor byte ptr [0eb1h],dh
+044F  89 3E 9C 0F mov word ptr [0f9ch],di
+0453  80 26 9D 0F 80 and byte ptr [0f9dh],80h
+0458  89 3E 9E 0F mov word ptr [0f9eh],di
+045C  30 36 9F 0F xor byte ptr [0f9fh],dh
 0460  81 CF 00 80 or di,8000h
 0464  80 CE 80    or dh,80h
 0467  80 F9 10    cmp cl,10h
@@ -633,8 +633,8 @@
 0492  D0 DC       rcr ah,1h
 0494  FE C9       dec cl
 0496  75 F6       jne 48eh
-0498  A0 B2 0E    mov al,byte ptr [0eb2h]
-049B  F6 06 B1 0E 80 test byte ptr [0eb1h],80h
+0498  A0 A0 0F    mov al,byte ptr [0fa0h]
+049B  F6 06 9F 0F 80 test byte ptr [0f9fh],80h
 04A0  75 14       jne 4b6h
 04A2  00 EC       add ah,ch
 04A4  11 F3       adc bx,si
@@ -654,7 +654,7 @@
 04BE  19 F3       sbb bx,si
 04C0  19 FA       sbb dx,di
 04C2  73 14       jnb 4d8h
-04C4  80 36 AF 0E 80 xor byte ptr [0eafh],80h
+04C4  80 36 9D 0F 80 xor byte ptr [0f9dh],80h
 04C9  F6 D4       not ah
 04CB  F7 D3       not bx
 04CD  F7 D2       not dx
@@ -686,7 +686,7 @@
 0506  31 D2       xor dx,dx
 0508  C3          ret
 0509  80 E6 7F    and dh,7fh
-050C  32 36 AF 0E xor dh,byte ptr [0eafh]
+050C  32 36 9D 0F xor dh,byte ptr [0f9dh]
 0510  C3          ret
 0511  55          push bp
 0512  8B EC       mov bp,sp
@@ -732,23 +732,23 @@
 0565  B0 01       mov al,1h
 0567  B4 4C       mov ah,4ch
 0569  CD 21       int 21h
-056B  89 0E B9 0E mov word ptr [0eb9h],cx
+056B  89 0E A7 0F mov word ptr [0fa7h],cx
 056F  8B CA       mov cx,dx
 0571  31 F9       xor cx,di
 0573  F6 D5       not ch
 0575  80 E5 80    and ch,80h
-0578  88 2E AF 0E mov byte ptr [0eafh],ch
+0578  88 2E 9D 0F mov byte ptr [0f9dh],ch
 057C  80 CE 80    or dh,80h
 057F  81 CF 00 80 or di,8000h
-0583  89 36 BB 0E mov word ptr [0ebbh],si
-0587  89 3E BD 0E mov word ptr [0ebdh],di
-058B  A3 B3 0E    mov word ptr [0eb3h],ax
-058E  89 1E B5 0E mov word ptr [0eb5h],bx
-0592  89 16 B7 0E mov word ptr [0eb7h],dx
+0583  89 36 A9 0F mov word ptr [0fa9h],si
+0587  89 3E AB 0F mov word ptr [0fabh],di
+058B  A3 A1 0F    mov word ptr [0fa1h],ax
+058E  89 1E A3 0F mov word ptr [0fa3h],bx
+0592  89 16 A5 0F mov word ptr [0fa5h],dx
 0596  30 E4       xor ah,ah
 0598  31 DB       xor bx,bx
 059A  31 D2       xor dx,dx
-059C  BF B9 0E    mov di,0eb9h
+059C  BF A7 0F    mov di,0fa7h
 059F  B1 05       mov cl,5h
 05A1  47          inc di
 05A2  8A 2D       mov ch,byte ptr [di]
@@ -763,9 +763,9 @@
 05B5  BE 08 00    mov si,8h
 05B8  D0 DD       rcr ch,1h
 05BA  73 0C       jnb 5c8h
-05BC  02 26 B4 0E add ah,byte ptr [0eb4h]
-05C0  13 1E B5 0E adc bx,word ptr [0eb5h]
-05C4  13 16 B7 0E adc dx,word ptr [0eb7h]
+05BC  02 26 A2 0F add ah,byte ptr [0fa2h]
+05C0  13 1E A3 0F adc bx,word ptr [0fa3h]
+05C4  13 16 A5 0F adc dx,word ptr [0fa5h]
 05C8  D1 DA       rcr dx,1h
 05CA  D1 DB       rcr bx,1h
 05CC  D0 DC       rcr ah,1h
@@ -785,7 +785,7 @@
 05E6  74 02       je 5eah
 05E8  FE C9       dec cl
 05EA  91          xchg ax,cx
-05EB  32 36 AF 0E xor dh,byte ptr [0eafh]
+05EB  32 36 9D 0F xor dh,byte ptr [0f9dh]
 05EF  3C 00       cmp al,0h
 05F1  75 06       jne 5f9h
 05F3  31 C0       xor ax,ax
@@ -834,29 +834,29 @@
 0647  B0 01       mov al,1h
 0649  B4 4C       mov ah,4ch
 064B  CD 21       int 21h
-064D  89 0E B9 0E mov word ptr [0eb9h],cx
+064D  89 0E A7 0F mov word ptr [0fa7h],cx
 0651  8B CA       mov cx,dx
 0653  31 F9       xor cx,di
 0655  F6 D5       not ch
 0657  80 E5 80    and ch,80h
-065A  88 2E AF 0E mov byte ptr [0eafh],ch
+065A  88 2E 9D 0F mov byte ptr [0f9dh],ch
 065E  80 CE 80    or dh,80h
 0661  81 CF 00 80 or di,8000h
-0665  89 36 BB 0E mov word ptr [0ebbh],si
-0669  89 3E BD 0E mov word ptr [0ebdh],di
-066D  A2 B3 0E    mov byte ptr [0eb3h],al
-0670  BF B8 0E    mov di,0eb8h
+0665  89 36 A9 0F mov word ptr [0fa9h],si
+0669  89 3E AB 0F mov word ptr [0fabh],di
+066D  A2 A1 0F    mov byte ptr [0fa1h],al
+0670  BF A6 0F    mov di,0fa6h
 0673  B1 05       mov cl,5h
 0675  BE 08 00    mov si,8h
-0678  3B 16 BD 0E cmp dx,word ptr [0ebdh]
+0678  3B 16 AB 0F cmp dx,word ptr [0fabh]
 067C  75 0A       jne 688h
-067E  3B 1E BB 0E cmp bx,word ptr [0ebbh]
+067E  3B 1E A9 0F cmp bx,word ptr [0fa9h]
 0682  75 04       jne 688h
-0684  3A 26 BA 0E cmp ah,byte ptr [0ebah]
+0684  3A 26 A8 0F cmp ah,byte ptr [0fa8h]
 0688  72 0C       jb 696h
-068A  2A 26 BA 0E sub ah,byte ptr [0ebah]
-068E  1B 1E BB 0E sbb bx,word ptr [0ebbh]
-0692  1B 16 BD 0E sbb dx,word ptr [0ebdh]
+068A  2A 26 A8 0F sub ah,byte ptr [0fa8h]
+068E  1B 1E A9 0F sbb bx,word ptr [0fa9h]
+0692  1B 16 AB 0F sbb dx,word ptr [0fabh]
 0696  F5          cmc
 0697  D0 D5       rcl ch,1h
 0699  4E          dec si
@@ -870,24 +870,24 @@
 06A8  D1 D3       rcl bx,1h
 06AA  D1 D2       rcl dx,1h
 06AC  73 CA       jnb 678h
-06AE  2A 26 BA 0E sub ah,byte ptr [0ebah]
-06B2  1B 1E BB 0E sbb bx,word ptr [0ebbh]
-06B6  1B 16 BD 0E sbb dx,word ptr [0ebdh]
+06AE  2A 26 A8 0F sub ah,byte ptr [0fa8h]
+06B2  1B 1E A9 0F sbb bx,word ptr [0fa9h]
+06B6  1B 16 AB 0F sbb dx,word ptr [0fabh]
 06BA  F8          clc
 06BB  EB D9       jmp 696h
 06BD  D0 E4       shl ah,1h
 06BF  D1 D3       rcl bx,1h
 06C1  D1 D2       rcl dx,1h
 06C3  72 11       jb 6d6h
-06C5  3B 16 BD 0E cmp dx,word ptr [0ebdh]
+06C5  3B 16 AB 0F cmp dx,word ptr [0fabh]
 06C9  75 0A       jne 6d5h
-06CB  3B 1E BB 0E cmp bx,word ptr [0ebbh]
+06CB  3B 1E A9 0F cmp bx,word ptr [0fa9h]
 06CF  75 04       jne 6d5h
-06D1  3A 26 BA 0E cmp ah,byte ptr [0ebah]
+06D1  3A 26 A8 0F cmp ah,byte ptr [0fa8h]
 06D5  F5          cmc
-06D6  8B 0E B3 0E mov cx,word ptr [0eb3h]
-06DA  8B 1E B5 0E mov bx,word ptr [0eb5h]
-06DE  8B 16 B7 0E mov dx,word ptr [0eb7h]
+06D6  8B 0E A1 0F mov cx,word ptr [0fa1h]
+06DA  8B 1E A3 0F mov bx,word ptr [0fa3h]
+06DE  8B 16 A5 0F mov dx,word ptr [0fa5h]
 06E2  9F          lahf
 06E3  F6 C6 80    test dh,80h
 06E6  75 09       jne 6f1h
@@ -1030,47 +1030,47 @@
 07EC  B5 00       mov ch,0h
 07EE  B6 00       mov dh,0h
 07F0  51          push cx
-07F1  BB 09 0F    mov bx,0f09h
+07F1  BB F7 0F    mov bx,0ff7h
 07F4  E8 41 02    call 0a38h
 07F7  59          pop cx
 07F8  89 D8       mov ax,bx
-07FA  BB 09 0F    mov bx,0f09h
+07FA  BB F7 0F    mov bx,0ff7h
 07FD  2B C3       sub ax,bx
-07FF  A3 49 0F    mov word ptr [0f49h],ax
-0802  89 0E 4B 0F mov word ptr [0f4bh],cx
-0806  8B 06 4B 0F mov ax,word ptr [0f4bh]
-080A  8B 16 49 0F mov dx,word ptr [0f49h]
+07FF  A3 37 10    mov word ptr [1037h],ax
+0802  89 0E 39 10 mov word ptr [1039h],cx
+0806  8B 06 39 10 mov ax,word ptr [1039h]
+080A  8B 16 37 10 mov dx,word ptr [1037h]
 080E  2B C2       sub ax,dx
 0810  83 F8 00    cmp ax,0h
 0813  7D 02       jnl 817h
 0815  31 C0       xor ax,ax
-0817  A3 4D 0F    mov word ptr [0f4dh],ax
-081A  8B 16 49 0F mov dx,word ptr [0f49h]
+0817  A3 3B 10    mov word ptr [103bh],ax
+081A  8B 16 37 10 mov dx,word ptr [1037h]
 081E  03 C2       add ax,dx
 0820  3D FF 00    cmp ax,0ffh
 0823  77 03       jnbe 828h
 0825  E9 03 00    jmp 82bh
 0828  B8 FF 00    mov ax,0ffh
-082B  A3 4F 0F    mov word ptr [0f4fh],ax
+082B  A3 3D 10    mov word ptr [103dh],ax
 082E  8B 56 06    mov dx,word ptr [bp+6h]
 0831  89 D7       mov di,dx
 0833  88 05       mov byte ptr [di],al
 0835  47          inc di
-0836  8B 0E 4D 0F mov cx,word ptr [0f4dh]
+0836  8B 0E 3B 10 mov cx,word ptr [103bh]
 083A  E3 07       jcxz 843h
 083C  B0 20       mov al,20h
 083E  88 05       mov byte ptr [di],al
 0840  47          inc di
 0841  E2 F9       loop 83ch
-0843  BE 09 0F    mov si,0f09h
-0846  8B 0E 49 0F mov cx,word ptr [0f49h]
+0843  BE F7 0F    mov si,0ff7h
+0846  8B 0E 37 10 mov cx,word ptr [1037h]
 084A  E3 08       jcxz 854h
 084C  8A 04       mov al,byte ptr [si]
 084E  88 05       mov byte ptr [di],al
 0850  46          inc si
 0851  47          inc di
 0852  E2 F8       loop 84ch
-0854  8B 06 4F 0F mov ax,word ptr [0f4fh]
+0854  8B 06 3D 10 mov ax,word ptr [103dh]
 0858  5F          pop di
 0859  5E          pop si
 085A  5A          pop dx
@@ -1086,12 +1086,12 @@
 0866  57          push di
 0867  8B 5E 04    mov bx,word ptr [bp+4h]
 086A  8B 7E 06    mov di,word ptr [bp+6h]
-086D  89 1E 51 0F mov word ptr [0f51h],bx
+086D  89 1E 3F 10 mov word ptr [103fh],bx
 0871  E8 CF 02    call 0b43h
 0874  73 03       jnb 879h
 0876  E9 0B 00    jmp 884h
 0879  89 D8       mov ax,bx
-087B  8B 16 51 0F mov dx,word ptr [0f51h]
+087B  8B 16 3F 10 mov dx,word ptr [103fh]
 087F  2B C2       sub ax,dx
 0881  E9 03 00    jmp 887h
 0884  B8 FF FF    mov ax,0ffffh
@@ -1140,7 +1140,7 @@
 08CE  52          push dx
 08CF  53          push bx
 08D0  50          push ax
-08D1  88 0E CB 0E mov byte ptr [0ecbh],cl
+08D1  88 0E B9 0F mov byte ptr [0fb9h],cl
 08D5  80 F9 00    cmp cl,0h
 08D8  78 03       js 8ddh
 08DA  E9 02 00    jmp 8dfh
@@ -1151,7 +1151,7 @@
 08E6  D0 EB       shr bl,1h
 08E8  00 FB       add bl,bh
 08EA  30 FF       xor bh,bh
-08EC  8D BF CD 0E lea di,[bx+0ecdh]
+08EC  8D BF BB 0F lea di,[bx+0fbbh]
 08F0  8B 05       mov ax,word ptr [di]
 08F2  8B 5D 02    mov bx,word ptr [di+2h]
 08F5  8B 55 04    mov dx,word ptr [di+4h]
@@ -1168,7 +1168,7 @@
 0910  58          pop ax
 0911  5B          pop bx
 0912  5A          pop dx
-0913  F6 06 CB 0E 80 test byte ptr [0ecbh],80h
+0913  F6 06 B9 0F 80 test byte ptr [0fb9h],80h
 0918  74 03       je 91dh
 091A  E9 03 00    jmp 920h
 091D  E9 27 FC    jmp 547h
@@ -1195,11 +1195,11 @@
 0947  3C 00       cmp al,0h
 0949  74 03       je 94eh
 094B  E9 16 00    jmp 964h
-094E  BE BF 0E    mov si,0ebfh
+094E  BE AD 0F    mov si,0fadh
 0951  C7 04 30 30 mov word ptr [si],3030h
 0955  46          inc si
 0956  46          inc si
-0957  81 FE CB 0E cmp si,0ecbh
+0957  81 FE B9 0F cmp si,0fb9h
 095B  74 03       je 960h
 095D  E9 F1 FF    jmp 951h
 0960  B9 00 00    mov cx,0h
@@ -1243,7 +1243,7 @@
 09AF  FE C9       dec cl
 09B1  74 03       je 9b6h
 09B3  E9 F3 FF    jmp 9a9h
-09B6  BE BF 0E    mov si,0ebfh
+09B6  BE AD 0F    mov si,0fadh
 09B9  88 F5       mov ch,dh
 09BB  B1 04       mov cl,4h
 09BD  D2 ED       shr ch,cl
@@ -1269,13 +1269,13 @@
 09E1  D1 D3       rcl bx,1h
 09E3  D1 D2       rcl dx,1h
 09E5  46          inc si
-09E6  81 FE CB 0E cmp si,0ecbh
+09E6  81 FE B9 0F cmp si,0fb9h
 09EA  74 03       je 9efh
 09EC  E9 CA FF    jmp 9b9h
 09EF  59          pop cx
 09F0  C3          ret
 09F1  30 E4       xor ah,ah
-09F3  BB BF 0E    mov bx,0ebfh
+09F3  BB AD 0F    mov bx,0fadh
 09F6  03 D8       add bx,ax
 09F8  80 3F 35    cmp byte ptr [bx],35h
 09FB  C6 07 00    mov byte ptr [bx],0h
@@ -1346,7 +1346,7 @@
 0A92  00 C8       add al,cl
 0A94  78 03       js 0a99h
 0A96  E9 08 00    jmp 0aa1h
-0A99  C6 06 BF 0E 00 mov byte ptr [0ebfh],0h
+0A99  C6 06 AD 0F 00 mov byte ptr [0fadh],0h
 0A9E  E9 0C 00    jmp 0aadh
 0AA1  3C 0C       cmp al,0ch
 0AA3  73 03       jnb 0aa8h
@@ -1354,7 +1354,7 @@
 0AA8  B0 0B       mov al,0bh
 0AAA  E8 44 FF    call 9f1h
 0AAD  5B          pop bx
-0AAE  BE BF 0E    mov si,0ebfh
+0AAE  BE AD 0F    mov si,0fadh
 0AB1  F6 C5 80    test ch,80h
 0AB4  75 03       jne 0ab9h
 0AB6  E9 05 00    jmp 0abeh
@@ -1440,7 +1440,7 @@
 0B75  31 DB       xor bx,bx
 0B77  31 D2       xor dx,dx
 0B79  31 C9       xor cx,cx
-0B7B  C6 06 CC 0E 00 mov byte ptr [0ecch],0h
+0B7B  C6 06 BA 0F 00 mov byte ptr [0fbah],0h
 0B80  8A 0C       mov cl,byte ptr [si]
 0B82  80 F9 61    cmp cl,61h
 0B85  73 03       jnb 0b8ah
@@ -1469,7 +1469,7 @@
 0BB5  58          pop ax
 0BB6  5B          pop bx
 0BB7  5A          pop dx
-0BB8  C7 06 AE 0E 00 00 mov word ptr [0eaeh],0h
+0BB8  C7 06 9C 0F 00 00 mov word ptr [0f9ch],0h
 0BBE  E8 60 F8    call 421h
 0BC1  59          pop cx
 0BC2  5E          pop si
@@ -1477,7 +1477,7 @@
 0BC4  F6 C5 40    test ch,40h
 0BC7  75 03       jne 0bcch
 0BC9  E9 04 00    jmp 0bd0h
-0BCC  FE 0E CC 0E dec byte ptr [0ecch]
+0BCC  FE 0E BA 0F dec byte ptr [0fbah]
 0BD0  46          inc si
 0BD1  E9 AC FF    jmp 0b80h
 0BD4  80 F9 2E    cmp cl,2eh
@@ -1489,7 +1489,7 @@
 0BE4  80 CD 40    or ch,40h
 0BE7  E9 E6 FF    jmp 0bd0h
 0BEA  80 F9 45    cmp cl,45h
-0BED  8A 0E CC 0E mov cl,byte ptr [0ecch]
+0BED  8A 0E BA 0F mov cl,byte ptr [0fbah]
 0BF1  74 03       je 0bf6h
 0BF3  E9 4D 00    jmp 0c43h
 0BF6  E8 2A FD    call 923h
@@ -1555,31 +1555,31 @@
 0C77  56          push si
 0C78  57          push di
 0C79  8B 46 06    mov ax,word ptr [bp+6h]
-0C7C  A3 4B 0F    mov word ptr [0f4bh],ax
+0C7C  A3 39 10    mov word ptr [1039h],ax
 0C7F  8B 56 04    mov dx,word ptr [bp+4h]
 0C82  8B 4E 06    mov cx,word ptr [bp+6h]
 0C85  8B 7E 08    mov di,word ptr [bp+8h]
-0C88  BB 09 0F    mov bx,0f09h
+0C88  BB F7 0F    mov bx,0ff7h
 0C8B  E8 AA FD    call 0a38h
 0C8E  89 D8       mov ax,bx
-0C90  BB 09 0F    mov bx,0f09h
+0C90  BB F7 0F    mov bx,0ff7h
 0C93  2B C3       sub ax,bx
-0C95  A3 49 0F    mov word ptr [0f49h],ax
-0C98  8B 06 4B 0F mov ax,word ptr [0f4bh]
-0C9C  8B 16 49 0F mov dx,word ptr [0f49h]
+0C95  A3 37 10    mov word ptr [1037h],ax
+0C98  8B 06 39 10 mov ax,word ptr [1039h]
+0C9C  8B 16 37 10 mov dx,word ptr [1037h]
 0CA0  2B C2       sub ax,dx
 0CA2  83 F8 00    cmp ax,0h
 0CA5  7D 02       jnl 0ca9h
 0CA7  31 C0       xor ax,ax
-0CA9  A3 4D 0F    mov word ptr [0f4dh],ax
-0CAC  8B 0E 4D 0F mov cx,word ptr [0f4dh]
+0CA9  A3 3B 10    mov word ptr [103bh],ax
+0CAC  8B 0E 3B 10 mov cx,word ptr [103bh]
 0CB0  E3 08       jcxz 0cbah
 0CB2  B0 20       mov al,20h
 0CB4  B4 02       mov ah,2h
 0CB6  CD 21       int 21h
 0CB8  E2 F8       loop 0cb2h
-0CBA  BE 09 0F    mov si,0f09h
-0CBD  8B 0E 49 0F mov cx,word ptr [0f49h]
+0CBA  BE F7 0F    mov si,0ff7h
+0CBD  8B 0E 37 10 mov cx,word ptr [1037h]
 0CC1  E3 09       jcxz 0ccch
 0CC3  8A 04       mov al,byte ptr [si]
 0CC5  B4 02       mov ah,2h
@@ -1699,3 +1699,127 @@
 0D8C  89 EC       mov sp,bp
 0D8E  5D          pop bp
 0D8F  C3          ret
+0D90  55          push bp
+0D91  8B EC       mov bp,sp
+0D93  53          push bx
+0D94  51          push cx
+0D95  52          push dx
+0D96  56          push si
+0D97  57          push di
+0D98  8B 7E 04    mov di,word ptr [bp+4h]
+0D9B  B8 FF FF    mov ax,0ffffh
+0D9E  89 05       mov word ptr [di],ax
+0DA0  31 C0       xor ax,ax
+0DA2  89 45 02    mov word ptr [di+2h],ax
+0DA5  8B 76 06    mov si,word ptr [bp+6h]
+0DA8  8A 04       mov al,byte ptr [si]
+0DAA  46          inc si
+0DAB  3C 7F       cmp al,7fh
+0DAD  76 02       jbe 0db1h
+0DAF  B0 7F       mov al,7fh
+0DB1  88 C1       mov cl,al
+0DB3  30 ED       xor ch,ch
+0DB5  83 C7 04    add di,4h
+0DB8  E3 08       jcxz 0dc2h
+0DBA  8A 04       mov al,byte ptr [si]
+0DBC  88 05       mov byte ptr [di],al
+0DBE  46          inc si
+0DBF  47          inc di
+0DC0  E2 F8       loop 0dbah
+0DC2  C6 05 00    mov byte ptr [di],0h
+0DC5  5F          pop di
+0DC6  5E          pop si
+0DC7  5A          pop dx
+0DC8  59          pop cx
+0DC9  5B          pop bx
+0DCA  89 EC       mov sp,bp
+0DCC  5D          pop bp
+0DCD  C3          ret
+0DCE  55          push bp
+0DCF  8B EC       mov bp,sp
+0DD1  53          push bx
+0DD2  51          push cx
+0DD3  52          push dx
+0DD4  57          push di
+0DD5  8B 7E 04    mov di,word ptr [bp+4h]
+0DD8  8D 55 04    lea dx,[di+4h]
+0DDB  B8 00 3D    mov ax,3d00h
+0DDE  CD 21       int 21h
+0DE0  72 0F       jb 0df1h
+0DE2  89 05       mov word ptr [di],ax
+0DE4  B8 01 00    mov ax,1h
+0DE7  89 45 02    mov word ptr [di+2h],ax
+0DEA  31 C0       xor ax,ax
+0DEC  A3 41 10    mov word ptr [1041h],ax
+0DEF  EB 10       jmp 0e01h
+0DF1  89 C3       mov bx,ax
+0DF3  B8 FF FF    mov ax,0ffffh
+0DF6  89 05       mov word ptr [di],ax
+0DF8  31 C0       xor ax,ax
+0DFA  89 45 02    mov word ptr [di+2h],ax
+0DFD  89 1E 41 10 mov word ptr [1041h],bx
+0E01  5F          pop di
+0E02  5A          pop dx
+0E03  59          pop cx
+0E04  5B          pop bx
+0E05  89 EC       mov sp,bp
+0E07  5D          pop bp
+0E08  C3          ret
+0E09  55          push bp
+0E0A  8B EC       mov bp,sp
+0E0C  53          push bx
+0E0D  51          push cx
+0E0E  52          push dx
+0E0F  57          push di
+0E10  8B 7E 04    mov di,word ptr [bp+4h]
+0E13  8D 55 04    lea dx,[di+4h]
+0E16  31 C9       xor cx,cx
+0E18  B8 00 3C    mov ax,3c00h
+0E1B  CD 21       int 21h
+0E1D  72 0F       jb 0e2eh
+0E1F  89 05       mov word ptr [di],ax
+0E21  B8 02 00    mov ax,2h
+0E24  89 45 02    mov word ptr [di+2h],ax
+0E27  31 C0       xor ax,ax
+0E29  A3 41 10    mov word ptr [1041h],ax
+0E2C  EB 10       jmp 0e3eh
+0E2E  89 C3       mov bx,ax
+0E30  B8 FF FF    mov ax,0ffffh
+0E33  89 05       mov word ptr [di],ax
+0E35  31 C0       xor ax,ax
+0E37  89 45 02    mov word ptr [di+2h],ax
+0E3A  89 1E 41 10 mov word ptr [1041h],bx
+0E3E  5F          pop di
+0E3F  5A          pop dx
+0E40  59          pop cx
+0E41  5B          pop bx
+0E42  89 EC       mov sp,bp
+0E44  5D          pop bp
+0E45  C3          ret
+0E46  55          push bp
+0E47  8B EC       mov bp,sp
+0E49  53          push bx
+0E4A  52          push dx
+0E4B  57          push di
+0E4C  8B 7E 04    mov di,word ptr [bp+4h]
+0E4F  8B 05       mov ax,word ptr [di]
+0E51  3D FF FF    cmp ax,0ffffh
+0E54  74 16       je 0e6ch
+0E56  89 C3       mov bx,ax
+0E58  B4 3E       mov ah,3eh
+0E5A  CD 21       int 21h
+0E5C  89 C3       mov bx,ax
+0E5E  B8 FF FF    mov ax,0ffffh
+0E61  89 05       mov word ptr [di],ax
+0E63  31 C0       xor ax,ax
+0E65  89 45 02    mov word ptr [di+2h],ax
+0E68  89 1E 41 10 mov word ptr [1041h],bx
+0E6C  5F          pop di
+0E6D  5A          pop dx
+0E6E  5B          pop bx
+0E6F  89 EC       mov sp,bp
+0E71  5D          pop bp
+0E72  C3          ret
+0E73  8B 06 41 10 mov ax,word ptr [1041h]
+0E77  C7 06 41 10 00 00 mov word ptr [1041h],0h
+0E7D  C3          ret