Просмотр исходного кода

TP3-STRVAR: string variables -- string[n] declarations, literal assignment, wrstr entry

s : string [n] is no longer an ENoLib refusal but a real type, laid out as
TPSRC8 lays it out: a length byte followed by the characters, n+1 bytes
for string [n].  Three things make it useful, each mirroring the original:

- DefVar: the size > 2 gate that refuses every declared type wider than a
  word now exempts TString (arrays and records still ERR; t14 intent kept).
- Assignment: the literal is known at compile time, so its bytes become
  immediate per-byte stores -- length byte, then each character -- and a
  source longer than varsize-1 is TRUNCATED by rewriting the length byte,
  exactly as TPSRC2 strstore (MOV.B [SI],AL / CMP.B CL,AL / JBE) truncates.
  Assignment from a string variable or a single-char literal is still ENoLib,
  and the concat-only error (concerr/16) stays concat-only.
- writeln(s)/write(s): IoCall pushes the variable's address -- the absolute
  form for a global, EmBpDisp [BP+off] for a local -- and calls the new
  wrstr runtime entry (selector 15, wedged into the write group after
  EmitWrInl).  wrstr reads the length byte and writes that many characters,
  which is TPSRC4 xwrtstr minus its field-width formatting.  Runtime grows
  465 -> 497 bytes, code region ends 434 -> 466.

t39_strvar pins it all through generated code: a global string [10] and a
local string [5] inside a procedure, an empty string that prints nothing,
and a bare writeln -- exercised under qemu and under Exec86, byte for byte
against its hand-derived .out (36/36 fixtures run).

The golden had to be re-blessed, and the review of that blessing is the
point of this writeup: the only changes to the disassembly are the
21-instruction wrstr block and the uniform +0x20 shift of what follows it.
check_runtime.py, RtProbe.mod and rt_exec.py all grew their entry inventory
to 16; rt_exec gains two seeded wrstr cases (five characters from driver
memory, and a zero length byte); and nonvacuity.sh gains three cases -- the
assignment arm, the write arm and wrstr's length load, one per place the
feature lives.

Checks: run_all 15/15; nonvacuity 70 ok, 0 failed; compile matrix 40/40;
qemu 36/36; exec86 36/36; rt_exec 43/43; audit 102/102.
Eric Streit 2 часов назад
Родитель
Сommit
8639bdd4ba

+ 64 - 46
SUMMARY.md

@@ -29,6 +29,7 @@ manual, not guessed.
 | Arguments pushed as they are parsed; the parameter frame read from BP+4 backwards | `v-TP3-ARGORDER` | done, **`t36_argclobber` runs: a computed argument survives the parse of the next one, and the callee's slots match the order they were pushed in** |
 | A `byte` reader of its own, and bug 36's four sites proved re-derivable | `v-TP3-NONVACUITY-RDBYTE` | done, **`readln (b)` of a `byte` prints the byte and leaves the variable beside it alone; reverting any one of the argument-order fix's four sites turns `t36_argclobber` red** |
 | A `BOOLEAN` read refused with TP3's own error 66 | `v-TP3-BOOLREAD` | done, **`readln (b)` of a boolean is an error, not a word store into a one-byte variable — exactly TPSRC8 `prderrtp`; the compile matrix demands `ERR 66` at the character after the variable, and the accepting dispatch is one of the in-suite reverts** |
+| String *variables* — `s : string[10]`, `s := 'hi'`, `writeln(s)` | `v-TP3-STRVAR` | done, **the first declared type bigger than a word that is not an error. `string [n]` is n+1 bytes — a length byte and characters, the layout TPSRC8's `string` type gives a variable (`INC BX / MOV varsize,BX`) — so `DefVar`'s `size > 2` catch-all (still the answer for an `array` at its point of use) exempts `TString`. `s := 'hi'` copies the literal byte by byte and truncates a longer one exactly as TPSRC2 `strstore` does; `writeln(s)` pushes the variable's address and calls a new `wrstr` entry (TPSRC4 `xwrtstr` minus its field-width formatting), which prints the length byte's worth of characters and nothing for `''`. Both a global and a local string are exercised — the absolute and the `[BP+off]` address paths — and the `.out` is compared byte for byte under qemu and Exec86 (36/36)** |
 
 Every row that names a tag has one, and every tag points at a commit on
 `master`; verified by diffing the rows against `git tag -l`, which is how
@@ -122,11 +123,11 @@ needing a squint. It was checked for vacuousness by reverting the string
 scanner fix: the suite went red with exit 1, and came back green only when the
 fix was restored.
 
-**35 of 39 fixtures compile clean**, up from 1 (the empty program) when the
+**36 of 40 fixtures compile clean**, up from 1 (the empty program) when the
 direct harness was first built.
 
 ```
-compile matrix: 39 passed, 0 failed (of 39)
+compile matrix: 40 passed, 0 failed (of 40)
 ```
 
 Compiling: `t01` minimal · `t04` var+assign+`writeln` · `t06` two args ·
@@ -143,18 +144,21 @@ expression · `t31` a value parameter *and* a call across statements ·
 operators nothing else uses (`div mod and or`, unary `-`, a variable `*`) ·
 `t35` `not`, both halves of TPSRC9's `neglevel` split · `t36` a call whose
 arguments are computed in various positions, each one keeping its own value. ·
-`t37` `readln (b)` of a `byte` sitting next to an `integer`
+`t37` `readln (b)` of a `byte` sitting next to an `integer` ·
+`t39` `s : string [10]`, assigned a literal and written — globally and from
+inside a procedure, so both address paths run
 
 Failing, all deliberately: `t14` `array [1..5] of integer` at its point of use
-and `t25` a string literal used as a *value* (`s := 'hi'`) both → `ENoLib`
+and `t25` a string literal assigned to an *integer* (`i := 'ab'`, the reverse
+direction is still unimplemented) both → `ENoLib`
 (102), the original's "not implemented" path; `t38` `readln (b)` of a
 `boolean` → error 66, TP3's "I/O not allowed here" (TPSRC8 `prderrtp`), which
 refuses the whole class because the runtime has no boolean reader at all; and
 `uierror` is a deliberate syntax error (41) used by the UI test.
 
-`run_com_tests.sh` additionally links **all 35 that compile** to a real `.COM`
+`run_com_tests.sh` additionally links **all 36 that compile** to a real `.COM`
 and re-verifies the bytes with an independent Python checker that *measures*
-the layout instead of restating it: `independent .COM check: 35 checked, 0
+the layout instead of restating it: `independent .COM check: 36 checked, 0
 failed`. That last part was itself a bug fix — see "the two restated
 constants" below.
 
@@ -193,13 +197,13 @@ proves each one can go red.
 |---|---|---|
 | `probe/run_modrm19.py` | the mod=00/01/10 effective addresses, by **executing** 23 cases on a real 8086 under qemu and scanning for where the marker landed | mod=11 — see below |
 | `probe/modrm11.py` | the mod=11 register identities, by **encoding** with GNU `as` and decoding with FCML, against hard-coded bytes | the table agreeing with itself |
-| `audit_helpers.py` | every one-line emitter in **both** `Runtime.mod` and `Compiler.mod` decodes to what its *name* says — **101/101**, from an inventory scanned independently of the parser | anything longer than one instruction |
+| `audit_helpers.py` | every one-line emitter in **both** `Runtime.mod` and `Compiler.mod` decodes to what its *name* says — **102/102**, from an inventory scanned independently of the parser | anything longer than one instruction |
 | `check_comimage.py` | a linked image's layout is described in **exactly one file**: `comimage.py` defines each part once, every reader imports it, no second `find_header` exists anywhere in `tests/` | a check that *hand-rolls* the layout from literals (`hdr = 3 + rt_size`), which defines none of those names — the readers catch that a different way: each states the layout twice, from two independent sources, and requires the two to agree |
-| `check_runtime.py` + `runtime.golden` | the built runtime's code region (465 bytes, code ends at 434) sweeps cleanly through FCML, every entry and all branch targets land on an instruction boundary, and the whole disassembly is byte-for-byte the committed golden | whether the golden is *right* |
+| `check_runtime.py` + `runtime.golden` | the built runtime's code region (497 bytes, code ends at 466) sweeps cleanly through FCML, every entry and all branch targets land on an instruction boundary, and the whole disassembly is byte-for-byte the committed golden | whether the golden is *right* |
 | `check_framedisp.py` | `[BP+off]` uses disp8 iff `off <= 127`, for locals (negative) and far parameters (>127) | which of the two encodings was chosen, if the other also works |
 | `run_com_exec.py` | the **emitted image executes** and prints exactly the expected bytes | semantics the fixture never exercises |
-| `run_exec86.py` | the same image runs in a **second, independent 8086** and agrees with qemu byte for byte, 33/33 | the `.out` file it shares with `run_com_exec.py` — a wrong expectation fails both at once |
-| `rt_exec.py` | the **runtime entries themselves** are called directly, one qemu boot per call, 40 cases plus a pre-flight, 41/41 — plus a `check_bp_contract` pre-flight over the built blob: an entry that borrows BP must open with `55 8B EC` (exact) and contain a `5D` (a screen, because `5D` is also a displacement byte) | whether the *caller's* half of a contract is right, where the only witness is a case that happens to make two calls in a row |
+| `run_exec86.py` | the same image runs in a **second, independent 8086** and agrees with qemu byte for byte, 36/36 | the `.out` file it shares with `run_com_exec.py` — a wrong expectation fails both at once |
+| `rt_exec.py` | the **runtime entries themselves** are called directly, one qemu boot per call, 42 cases plus a pre-flight, 43/43 — plus a `check_bp_contract` pre-flight over the built blob: an entry that borrows BP must open with `55 8B EC` (exact) and contain a `5D` (a screen, because `5D` is also a displacement byte) | whether the *caller's* half of a contract is right, where the only witness is a case that happens to make two calls in a row |
 
 Two of these deserve the detail, because the reason they exist is the reason
 they are hard.
@@ -303,7 +307,7 @@ signature (`hdrFlag = 1`, `hdrDS == hdrOff + 1000h + bias`, `hdrHeap > hdrDS`,
 `prologAt` and `dataBase` per file:
 
 ```
-measured runtime size: 465 bytes (header at image offset 468)
+measured runtime size: 497 bytes (header at image offset 500)
 ```
 
 A restated constant that has drifted is worse than a derived one, and the two
@@ -330,8 +334,8 @@ compiler: **does the 8086 have this instruction at all?**
 8086 check: 38 comparison sites, 34 lowered to a Boolean value, 13 lowered to a branch
             value conditions  : = x6  <> x2  < x5  >= x3  <= x2  > x16
             branch conditions : IF / REPEAT x9  CASE x2  FOR downto x1  FOR to x3
-            runtime: 465 bytes, 235 swept, 0 0F-prefixed
-            program code: 32 of 35 fixtures swept end to end, 3496 bytes
+            runtime: 497 bytes, 256 swept, 0 0F-prefixed
+            program code: 34 of 36 fixtures swept end to end, 3720 bytes
             t33_cmpops: 13 comparisons matched against their source operators, in order
             clause H: 8 of 8 fixtures matched the branch conditions read off their source
 ```
@@ -557,7 +561,7 @@ checker.
 compile to `.COM` images, are booted on a floppy by a 512-byte hand-assembled
 boot sector, and are compared **byte for byte** against the exact output the
 fixture demands — `tests/run_com_exec.py`, wired into `run_all.sh`, 21/21 at
-that tag and 33/33 now.
+that tag and 36/36 now.
 
 Everything below is about establishing what *can* be believed, because the
 first attempt at this used an emulator that was wrong, and a wrong oracle is
@@ -670,13 +674,19 @@ told apart from a broken harness without re-deriving anything.
 never calls `rdint` without a `readln` behind it, never calls `rdbool` twice in
 a row, and never exercises the `INT16` extremes individually.
 
-40 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
+42 cases, one fresh qemu boot each, plus a pre-flight that runs before any of
 them: `initmem` (data poisoned to `AA` first, so
 a zero is a *result* and not a leftover), `wrint` ×10 including both `INT16`
 extremes, `wrchar` ×2, `wrbool` ×3, `wrln`, `stackchk`, a composed
 `writeln(42) writeln TRUE` sequence, `rdint` ×6 against supplied input,
 `rdchar` ×2, `rdbool` ×6, `rdbyte` ×4 against supplied input with a poison
-byte sitting past the one-byte variable, `rdint`/`rdbyte` at EOF, and `wrtinl`.
+byte sitting past the one-byte variable, `rdint`/`rdbyte` at EOF, `wrtinl`,
+and `wrstr` ×2 — a five-character string and an empty one, seeded in the
+driver's own memory so the entry dereferences a real pointer rather than the
+call site's return address. The two `wrstr` cases exist because the entry has
+the *opposite* argument contract from `wrtinl`: its stack word is an
+address, and the only way to test "prints the length byte's worth and stops"
+twice is two lengths.
 
 Three properties are load-bearing, and each of them was chosen against the
 easier alternative:
@@ -923,22 +933,24 @@ initialised past it (`MOV pc,#$2D7C`). Same shape here, and now actually done:
 image-absolute. **No relocation pass is needed** — worth having paid for, since
 a linker that has to walk fixups is a linker that can get them wrong.
 
-Current blob: **465 bytes**, 15 entries, 41 branch targets, offsets read back
+Current blob: **497 bytes**, 16 entries, 42 branch targets, offsets read back
 out of the assembled bytes by `tests/check_runtime.py` rather than asserted by
 hand:
 
 | entry | offset | entry | offset | entry | offset |
 |---|---|---|---|---|---|
-| `initmem` | 0 | `wrint` | 36 | `rdint` | 171 |
-| `progend` | 28 | `wrchar` | 97 | `rdbyte` | 278 |
-| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 308 |
-| `halt` | 28 | `wrreal` | 136 | `rdbool` | 331 |
-| | | `wrln` | 144 | `rdln` | 379 |
+| `initmem` | 0 | `wrint` | 36 | `rdint` | 203 |
+| `progend` | 28 | `wrchar` | 97 | `rdbyte` | 310 |
+| `stackchk` | 35 | `wrbool` | 111 | `rdchar` | 340 |
+| `halt` | 28 | `wrreal` | 136 | `rdbool` | 363 |
+| | | `wrln` | 144 | `rdln` | 411 |
 | | | `wrtinl` | 152 | | |
+| | | `wrstr` | 171 | | |
 
 Every number in that table is measured, and that is the point: it was last
 correct when the blob was 391 bytes, and the entries quietly moved as it grew
-past 436 to 465 — six of the offsets in an earlier version of this table were
+past 436 to 465 and again to 497 (`wrstr` joined, `v-TP3-STRVAR`) — six of
+the offsets in an earlier version of this table were
 stale, and the size was stale too. A restated constant that has drifted is
 worse than a derived one, which is why the compiler asks
 `Runtime.RT_Entry` instead (below) and why the checkers locate the runtime by
@@ -958,6 +970,7 @@ Conventions, matching `Compiler.IoCall` exactly:
 |---|---|---|
 | `WrInt/WrChar/WrBool/WrReal` | one 16-bit **value** on the stack | caller pops |
 | `RdInt/RdByte/RdChar/RdBool` | one **address** on the stack | caller pops |
+| `WrStr` | one **address** on the stack — the string variable's length byte first | caller pops |
 | `WrLn/RdLn/StackChk` | nothing | |
 | `InitMem` | `AX` = offset of the program header | a *register*, not a stack word |
 | `ProgEnd/Halt` | nothing | exits, code 0 |
@@ -1000,7 +1013,7 @@ all patched after the blob is placed, so nothing depends on a hand-computed
 displacement.
 
 **It executes.** Every entry is called directly under qemu by
-`tests/rt_exec.py` — 41 cases, all passing — with the register contract each
+`tests/rt_exec.py` — 43 cases, all passing — with the register contract each
 entry must honour stated rather than assumed, which is how `wrchar` and
 `wrbool` were caught destroying the caller's BP. That milestone
 (`v-TP3-BP-CONTRACT`) is the reason this paragraph no longer says the runtime
@@ -1033,9 +1046,9 @@ assertion, rather than asking the compiler where it thinks the header is.
 last dot, padded with a zero gap to `max(pc, dc)`; its stack sits at the segment
 top (`SS = SP = CS:FFFE`), which is where DOS puts it.
 
-**Known limit:** the data area starts at a fixed `rtSz + 1000H` (465 + 4096 =
-4561), so a program whose code exceeds 4 KiB runs into its own data. Every
-fixture's image is between 4568 and 4576 bytes. Documented rather than fixed,
+**Known limit:** the data area starts at a fixed `rtSz + 1000H` (497 + 4096 =
+4593), so a program whose code exceeds 4 KiB runs into its own data. Every
+fixture's image is between 4600 and 4611 bytes. Documented rather than fixed,
 because the
 original has the same fixed-offset behaviour.
 
@@ -1092,10 +1105,10 @@ Details that are deliberate, not incidental:
   The length is one byte, so 300 characters would go out behind a length of
   44 and the runtime would print 44 of them and silently drop the rest. TP3
   strings are at most 255 characters, so refusing is the faithful answer.
-- **A string *variable* is `ENoLib`, not wrong code.** `IoCall` knows the
-  difference and refuses. `EmPushVarAddr`'s local form is fixed now (see bug 4
-  below), so the blocker is no longer the encoding — it is that there is no
-  `string` type, no length word, no assignment path and no `WrStr` entry.
+- **A string *variable* is a real store now, not a refusal.** `s := 'hi'` and
+  `writeln(s)` compile (see `v-TP3-STRVAR`); what is still refused is
+  `readln(s)`, `s := another_string`, `s [i]` indexing and a literal assigned
+  to a non-string — all `ENoLib`, because none of them has an encoding yet.
 
 
 ## Honest limitations
@@ -1106,7 +1119,7 @@ Details that are deliberate, not incidental:
   prove the *path* exists (compile → poke → run → report → no file written),
   which it does with eight assertions. The other 34 are covered by
   `run_exec86.py`, which calls the same interpreter directly.
-- **Every fixture that compiles is now also run.** 35 of 39 execute; the 4 that
+- **Every fixture that compiles is now also run.** 36 of 40 execute; the 4 that
   do not are `t14` and `t25` (`ENoLib`, by design), `t38` (a boolean read,
   refused with TP3's error 66, by design) and `uierror` (a deliberate syntax
   error). The gap this replaces was nine fixtures that compiled and were
@@ -1164,13 +1177,13 @@ Details that are deliberate, not incidental:
   (`7x 03 E9`, searching for the `E9`) and by `t22_case`'s execution, but the
   label immediate itself is unchecked.
 - **The runtime's entries are now each called directly, and two of its
-  invariants are stated rather than implied.** 465 bytes, 15 entries, 102
+  invariants are stated rather than implied.** 497 bytes, 16 entries, 102
   emitter helpers decoded against their own names across both modules, the
   whole code region golden-pinned, all branch targets on instruction
-  boundaries — and 40 cases that call the entries one at a time under qemu
-  (`rt_exec.py`, 41/41 with its BP pre-flight). What is *not* covered is what a direct call cannot
+  boundaries — and 42 cases that call the entries one at a time under qemu
+  (`rt_exec.py`, 43/43 with its BP pre-flight). What is *not* covered is what a direct call cannot
   see: `wrtinl` is checked, but only from the harness's side of the calling
-  contract. (The nine fixtures that compiled but were never executed used to sit
+  contract; `wrstr` only from its seeded-address cases. (The nine fixtures that compiled but were never executed used to sit
   here as an extra gap; they are now run, so every fixture that reaches the
   runtime at all also reaches it *through generated code*.)
 - **The pushback slot's address is a moving target.** It sits at
@@ -1205,11 +1218,13 @@ Details that are deliberate, not incidental:
   emitter has to be revisited. The audit handles them as a documented
   *sequence*; if another one appears, the sequence machinery is where it goes,
   not a name hack.
-- **String *literals* work; string *variables* do not.** A literal in a
-  `WRITE`/`WRITELN` argument list is emitted inline and needs no runtime
-  support beyond `wrtinl`. Declaring `s : string`, assigning to it and
-  printing it are all `ENoLib` — there is no `string` type, no length word, no
-  assignment path, no `WrStr` entry. The `chr` flag on `ERes` is what keeps
+- **String *literals* and string *variables* work; the rest of the string
+  language does not.** A literal in a `WRITE`/`WRITELN` argument list is
+  emitted inline and needs no runtime support beyond `wrtinl`; a variable
+  (`s := 'hi'`, `writeln(s)`) is the `v-TP3-STRVAR` milestone — byte-by-byte
+  literal stores and the `wrstr` entry. Still refused: `readln(s)`, assignment
+  from one string to another, indexing, and the concatenation/comparison
+  operators. The `chr` flag on `ERes` is what keeps
   `writeln('a')` calling the *character* writer instead of the integer writer;
   without it the compiler emitted the integer path and printed 97 while the test
   still said OK.
@@ -1227,7 +1242,9 @@ Details that are deliberate, not incidental:
   and **must not** use `EXIT`, which ICEs gm2 in pass 3 (see the pitfalls
   list). Not yet done.
 - **Not implemented** (all `ENoLib`): real, set, record, file, string
-  *variables*, and any type wider than 2 bytes. `with` is `ENoLib`. `case` *is*
+  *reading*, string-to-string assignment, indexing, the `+`/comparison string
+  operators, and any declared type wider than 2 bytes other than `TString`.
+  `with` is `ENoLib`. `case` *is*
   implemented (cascade `CMP`/`JNZ` per label, per RESUME-TP3.md §3.6) but only
   over scalar labels — subrange labels and label lists are untested.
 - gm2 string-literal → `ARRAY OF CHAR` assignment copies the literal *plus a
@@ -1877,10 +1894,11 @@ independently-scanned inventory at all.
 
 ## Next steps
 
-1. **String *variables*** — `s : string`, `s := 'hi'`, `writeln(s)`. The
-   encoding blocker is gone (`EmBpDisp`); what is left is a length word, an
-   assignment path, and a `WrStr` entry (TPSRC4 `xwrtstr`). `IoCall` currently
-   refuses with `ENoLib`.
+1. The rest of the string language, now that `s : string` exists (the
+   `v-TP3-STRVAR` milestone covers `s := 'literal'` and `writeln(s)`):
+   `readln(s)` (TP3's `xrdstr`), `s := t` between two string variables,
+   `s [i]` indexing, and the `+` / comparison operators. All currently
+   `ENoLib`.
 2. Nested procedures / recursion, `var` parameters (the `SEG:OFF` push from
    RESUME-TP3.md §3.11), range/index checks (`TU_RANGE_CHECK`,
    `TU_INDEX_CHECK`), typed constants (RESUME-TP3.md §3.14), `array` at its

+ 8 - 4
TP3-COMPILER.md

@@ -123,11 +123,15 @@ against: per fixture it pins the verdict plus the code size plus the data size
 re-baselining is written in the file rather than in a commit message.  Read that
 for what compiles; this document for how.
 
-The two fixtures that fail are `t14` (`array [1..5] of integer` at its point of
-use) and `t25` (a string literal used as a value, `s := 'hi'`), both
-`ENoLib` (102) -- the original's own "not implemented" path, and pinned on
+The fixtures that fail are `t14` (`array [1..5] of integer` at its point of
+use) and `t25` (a string literal assigned to an integer, `i := 'ab'`) → `ENoLib`
+(102) -- the original's own "not implemented" path, and pinned on
 purpose so that implementing them is a deliberate change to a test rather than
-an accident.  `uierror` is a deliberate syntax error used by `uitest.py`.
+an accident; `t38` (`readln (b)` of a `boolean`) → error 66, TP3's "I/O not
+allowed here", for the same reason; and `uierror` is a deliberate syntax error
+used by `uitest.py`.  (`t39_strvar` is the counter-example: a *string*
+variable, `s : string [10]`, assigned a literal and written -- the first
+declared type wider than a word that compiles, and run under both oracles.)
 
 ### `run_com_exec.py` -- the emitted image, on a CPU
 

+ 71 - 23
shell/Compiler.mod

@@ -268,6 +268,7 @@ VAR
    TU_RdLn     : CARDINAL ;  TU_Halt   : CARDINAL ;
    TU_RdByte   : CARDINAL ;  (* one-byte variable reads; see IoCall *)
    TU_WrInl    : CARDINAL ;  (* inline string literal; NO stack argument *)
+   TU_WrStr    : CARDINAL ;  (* string variable; address is the stack argument *)
 
    abortFac : BOOLEAN ;
    errNum   : CARDINAL ;        (* NOT "errNo": Compile's formal of that
@@ -2270,11 +2271,44 @@ END ParseLabelStmt ;
 PROCEDURE Assignment (r : ERes) ;
 (* ':=' already consumed by the caller; store expression into r *)
 VAR src : ERes ;
+    k, n : CARDINAL ;
 BEGIN
    IF r.kind # 1 THEN
       Err (EUnknown) ;
       RETURN
    END ;
+   IF symtab [r.idx].cls = TString THEN
+      (* s := <string literal>.  TPSRC8 asn2 sends a string through the scalar
+         store path, and estcpl's string arm emits `MOV CL,max_len' (max_len =
+         varsize-1) then `CALL strstore', which copies the source - length byte
+         and characters - into the destination and TRUNCATES a source longer
+         than max_len by rewriting the length byte (TPSRC2 strstore's
+         `CMP.B CL,AL / JBE stslenok / MOV.B [SI],AL').  Here the literal is
+         known at compile time, so its bytes become immediate stores instead of
+         a stack push and a block move.  The variable that results is the same,
+         truncation included, and the tail past the new length is left as it
+         was, exactly as strstore leaves it. *)
+      ParseExpr (src) ;
+      IF (src.cls # TString) OR (src.kind # 3) THEN
+         Err (ENoLib) ;              (* only literal assignment exists yet *)
+         RETURN
+      END ;
+      n := strLen [src.strx] ;
+      IF n > symtab [r.idx].size - 1 THEN
+         n := symtab [r.idx].size - 1
+      END ;
+      EmMovAxi (n) ;
+      EmStoreVar (symtab [r.idx].local,
+                  (symtab [r.idx].off + r.boff) MOD 10000H, 1) ;
+      k := 0 ;
+      WHILE k < n DO
+         EmMovAxi (ORD (strPool [strOff [src.strx] + k])) ;
+         EmStoreVar (symtab [r.idx].local,
+                     (symtab [r.idx].off + r.boff + 1 + k) MOD 10000H, 1) ;
+         INC (k)
+      END ;
+      RETURN
+   END ;
    IF symtab [r.idx].size > 2 THEN
       Err (ENoLib) ;
       RETURN
@@ -2429,28 +2463,35 @@ BEGIN
                   pushed := FALSE                (* nothing was pushed for this one *)
                ELSE
                   IF acls = TString THEN
-                     (* A string *variable*.  Not emitted rather than emitted wrongly:
-                        EmPushVarAddr's local form is still wrong (see the note on
-                        that procedure), and a wrong address here would print
-                        garbage instead of failing. *)
-                     Err (ENoLib) ;
-                     RETURN
-                  END ;
-                  LoadAtom (arg) ;
-                  EmPushAx () ;
-                  IF arg.chr THEN
-                     ent := TU_WrChar                (* 'a' - one char, not 97 *)
-                  ELSIF acls = TReal THEN
-                     ent := TU_WrReal
-                  ELSIF acls = TBool THEN
-                     ent := TU_WrBool
-                  ELSIF acls = TChar THEN
-                     ent := TU_WrChar                (* c : char - one char *)
-                  ELSIF acls = TScalar THEN
-                     ent := TU_WrInt
+                     (* A string *variable*.  Its value IS its address, so it is
+                        passed exactly the way READ passes an address: push it
+                        and call the owner of the layout - wrstr - which reads
+                        the length byte and prints that many characters.  TPSRC8
+                        pwrstr/pwrchk route a string argument to xwrtstr, and
+                        xwrtstr reaches its argument through BP just like WrChar
+                        does.  The only difference from the inline-literal arm
+                        above is where the bytes live: a literal's are in the
+                        code stream (wrtinl) and a variable's are in the data
+                        segment or the frame (wrstr). *)
+                     EmPushVarAddr (symtab [arg.idx].local, symtab [arg.idx].off) ;
+                     ent := TU_WrStr
                   ELSE
-                     Err (ETypeErr) ;
-                     RETURN
+                     LoadAtom (arg) ;
+                     EmPushAx () ;
+                     IF arg.chr THEN
+                        ent := TU_WrChar             (* 'a' - one char, not 97 *)
+                     ELSIF acls = TReal THEN
+                        ent := TU_WrReal
+                     ELSIF acls = TBool THEN
+                        ent := TU_WrBool
+                     ELSIF acls = TChar THEN
+                        ent := TU_WrChar             (* c : char - one char *)
+                     ELSIF acls = TScalar THEN
+                        ent := TU_WrInt
+                     ELSE
+                        Err (ETypeErr) ;
+                        RETURN
+                     END
                   END
                END
             END ;
@@ -2972,7 +3013,12 @@ BEGIN
          ParseType (cls, size, elem) ;
          off := 0 ;
          IF lexnest = 0 THEN
-            IF size > 2 THEN
+            IF (size > 2) AND (cls # TString) THEN
+               (* Still the catch-all for a type whose storage this compiler does
+                  not lay out yet (an ARRAY at its point of use, a RECORD, ...).
+                  A STRING is the exception now: its layout is length byte +
+                  characters, and the rest of the compiler knows it - see
+                  Assignment and IoCall. *)
                Err (ENoLib) ;
                RETURN
             END ;
@@ -2992,7 +3038,7 @@ BEGIN
             DropC (NewSym (nm, KVar, cls, size, elem, off, 0, FALSE)) ;
             varspc := varspc + size
          ELSE
-            IF size > 2 THEN
+            IF (size > 2) AND (cls # TString) THEN
                Err (ENoLib) ;
                RETURN
             END ;
@@ -3511,6 +3557,8 @@ BEGIN
    TU_Halt     := RT_Entry (12) ;
    TU_WrInl    := RT_Entry (13) ;      (* inline string literal *)
    TU_RdByte   := RT_Entry (14) ;      (* a BYTE read stores one byte *)
+   TU_WrStr    := RT_Entry (15) ;      (* a string variable is written through
+                                          the address on the stack *)
 END Inittur ;
 
 PROCEDURE HeadWord (VAR slot : CARDINAL) ;

+ 2 - 0
shell/Runtime.def

@@ -29,6 +29,8 @@ CONST
    E_RdLn    = 11 ; E_Halt    = 12 ; E_WrInl   = 13 ;
    E_RdByte  = 14 ;         (* one-byte store: TPSRC8 prdnum's `varsize <= 1'
                                arm, TPSRC4 xrdbyte *)
+   E_WrStr   = 15 ;         (* a string variable: address on the stack, TPSRC4
+                               xwrtstr minus its field-width formatting *)
 
    (* 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

+ 45 - 3
shell/Runtime.mod

@@ -117,7 +117,7 @@ VAR
    dataAt : CARDINAL ;
    rtBase : CARDINAL ;
    built  : BOOLEAN ;
-   entNm  : ARRAY [0..14] OF ARRAY [0..MaxNm] OF CHAR ;
+   entNm  : ARRAY [0..15] OF ARRAY [0..MaxNm] OF CHAR ;
 
 (* ---------------------------------------------------------------- *)
 (*  name helpers                                                     *)
@@ -717,6 +717,47 @@ BEGIN
                                        the return address is already gone *)
 END EmitWrInl ;
 
+PROCEDURE EmitWrStr ;
+(* Write a string VARIABLE - TP3 TPSRC4 "xwrtstr", minus the field-width
+   formatting this compiler does not compile yet.
+
+   The compiler emits
+
+       <PUSH the string's address>   from EmPushVarAddr
+       CALL wrstr
+
+   so the address is the one 16-bit argument on the stack, read through BP
+   exactly as WrChar reads its value (there is no [SP] form).  The first byte
+   at that address is the current LENGTH and the characters follow it, which
+   is the layout TPSRC8's `string' type gives a variable: TPSRC8:83 does
+   `INC BX / MOV varsize,BX' for `string [n]', so n+1 bytes, length byte
+   first.
+
+   The loop is wrtinl's, pointed at the caller's bytes instead of at the
+   bytes behind the call: CL := [BX] is the length, BX moves to the first
+   character, and one INT 21h/02h prints each one until CX runs out.  A
+   length of 0 prints nothing, so an empty string is silent. *)
+BEGIN
+   M ("wrstr") ;
+   PushBp ; MovBpSp ;
+   PushBx ; PushCx ; PushAx ;
+   MovBxBp4 ;                        (* BX := the caller's string address *)
+   XorCxCx ;
+   MovClBx ;                         (* CX := the length byte at [BX] *)
+   IncBx ;                           (* BX -> the first character *)
+   MovAh (02H) ;                     (* INT 21h/02h: put the character in AL *)
+   Jcxz8 ("ws_end") ;                (* empty string -> nothing to do *)
+   M ("ws_loop") ;
+   LdAlBx ;                          (* AL := next character *)
+   Int21 ;                           (* preserves every register but AL *)
+   IncBx ;
+   Loop8 ("ws_loop") ;
+   M ("ws_end") ;
+   PopAx ; PopCx ; PopBx ;
+   MovSpBp ; PopBp ;
+   RetR
+END EmitWrStr ;
+
 PROCEDURE EmitWrBool ;
 (* 4, not 2, for the reason given on CmpArg4W0: BP is pushed before it is
    borrowed, so the argument is four bytes up.  POP BP before the RET, and
@@ -1073,7 +1114,7 @@ BEGIN
    EmitEnd ;
    EmitStackChk ;
    EmitWrInt ; EmitWrChar ; EmitWrBool ; EmitWrReal ; EmitWrLn ;
-   EmitWrInl ;
+   EmitWrInl ; EmitWrStr ;
    EmitRdInt ; EmitRdScan ; EmitRdByte ;
    EmitRdChar ; EmitRdBool ; EmitRdLn ;
    EmitGetCh ; EmitUnGetCh ;
@@ -1094,6 +1135,7 @@ BEGIN
    SetStr (entNm [12], "halt") ;
    SetStr (entNm [13], "wrtinl") ;
    SetStr (entNm [14], "rdbyte") ;
+   SetStr (entNm [15], "wrstr") ;
    built := TRUE
 END RT_Build ;
 
@@ -1121,7 +1163,7 @@ BEGIN
    IF NOT built THEN
       RT_Build (0)
    END ;
-   IF i > 14 THEN
+   IF i > 15 THEN
       RETURN 0
    END ;
    RETURN rtBase + LblOff (entNm [i])

+ 4 - 3
shell/tests/RtProbe.mod

@@ -108,7 +108,7 @@ END PENT ;
 
 PROCEDURE Main ;
 VAR i, n, base : CARDINAL ;
-    names : ARRAY [0..14] OF ARRAY [0..15] OF CHAR ;
+    names : ARRAY [0..15] 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 <= 14 DO
+   WHILE i <= 15 DO
       names [i] := "" ;
       INC (i)
    END ;
@@ -130,8 +130,9 @@ BEGIN
    names [6] := "wrreal" ;  names [7] := "wrln" ;    names [8] := "rdint" ;
    names [9] := "rdchar" ;  names [10] := "rdbool" ; names [11] := "rdln" ;
    names [12] := "halt" ;   names [13] := "wrtinl" ;  names [14] := "rdbyte" ;
+   names [15] := "wrstr" ;
    i := 0 ;
-   WHILE i <= 14 DO
+   WHILE i <= 15 DO
       PENT (i, names [i]) ;
       INC (i)
    END ;

+ 7 - 0
shell/tests/check_runtime.py

@@ -178,6 +178,13 @@ GOLDEN = {
     # answer lives in CX, from "T/t/Y/y/1" test, and has to reach the
     # caller's address in DI.
     "rdbool": "55 8B EC 50 53 51 57",
+    # wrstr: PUSH BP; MOV BP,SP; PUSH BX; PUSH CX; PUSH AX; MOV BX,[BP+4].
+    # It borrows BP to reach its argument (there is no [SP] form), exactly as
+    # wrchar does, so it must hand BP back - rt_exec.py's BP-contract screen
+    # checks that from the other side.  The address it loads through BP is the
+    # caller's string; the print loop behind it is wrtinl's, so this prefix
+    # stops at the address load rather than asserting the whole entry.
+    "wrstr": "55 8B EC 53 51 50 8B 5E 04",
 }
 
 

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

@@ -94,6 +94,8 @@
 	.set	DATA, 0x0810
 	.set	DLEN, 32			# bytes of data area for initmem to clear
 	.set	STORE, 0x0840		# where a read entry's result is stored
+	.set	WSTR, 0x0850		# a string for the wrstr case: length + chars
+	.set	WSTR_EMPTY, 0x0858	# the same, with a length of 0
 	.set	CASE, 0x0860		# the case record
 	.set	D_DUMPLEN, 0x0890		# the driver's own three words
 	.set	D_DUMPADR, 0x0892
@@ -314,6 +316,17 @@ HDR:
 	.org	STORE
 	.zero	2			# poisoned to EEEH by the driver
 
+# A string for the wrstr case: a length byte and that many characters, which is
+# the layout a compiled `string [n]' variable has.  It sits above the poisoned
+# area so the poison never touches it, and rt_exec.py restates its address - the
+# two must agree or the case reads whatever bytes are there and prints them.
+	.org	WSTR
+	.byte	5
+	.byte	'h', 'e', 'l', 'l', 'o'
+
+	.org	WSTR_EMPTY
+	.byte	0			# a length of 0: an empty string
+
 	.org	CASE
 	.incbin	"rtcase.bin"
 

+ 20 - 0
shell/tests/fixtures/expected.tsv

@@ -210,6 +210,25 @@
 #                 whole argument, and the atom parser's Skip() has run, so the
 #                 parser stands on the delimiter when IoCall dispatches on
 #                 the variable's class.
+#
+#   t39_strvar     the first fixture with a STRING VARIABLE, and the first
+#                 declared type bigger than a word that is not an error: `var
+#                 s : string [10]' is 11 bytes - a length byte and ten
+#                 characters, the layout TPSRC8's `string' type gives a
+#                 variable (`INC BX / MOV varsize,BX', so n+1) - and DefVar's
+#                 `size > 2' catch-all (still the answer for an ARRAY at its
+#                 point of use) now exempts TString.  Assignment is a literal
+#                 copied byte by byte: TPSRC8 asn2 sends a string through
+#                 estcpl's string arm, `MOV CL,max_len ; CALL strstore', and
+#                 this reproduces its observable result, truncation included.
+#                 writeln(s)/write(s) route to a new runtime entry, wrstr
+#                 (TPSRC4 xwrtstr without its field-width formatting), which
+#                 prints the length byte's worth of characters and nothing for
+#                 an empty string.  A global AND a local string are exercised,
+#                 so the absolute and the [BP+off] address paths both have
+#                 coverage, and the .out is compared byte for byte under qemu
+#                 and Exec86.  Still refused: readln(s), s := another string,
+#                 and s [i] indexing - all ENoLib.
 
 t01_minimal	OK	29	4
 t02_writeln	OK	38	4
@@ -249,4 +268,5 @@ t35_not	OK	224	7
 t36_argclobber	OK	492	12
 t37_readbyte	OK	139	7
 t38_readbool	ERR	66	659
+t39_strvar	OK	215	15
 uierror	ERR	41	331

+ 4 - 0
shell/tests/fixtures/t39_strvar.out

@@ -0,0 +1,4 @@
+hello
+loc
+done
+world

+ 23 - 0
shell/tests/fixtures/t39_strvar.pas

@@ -0,0 +1,23 @@
+program t39;
+var
+  s : string [10];
+
+procedure p;
+var
+  t : string [5];
+begin
+  t := 'loc';
+  writeln (t)
+end;
+
+begin
+  s := 'hello';
+  writeln (s);
+  p;
+  s := '';
+  write (s);
+  writeln ('done');
+  s := 'world';
+  write (s);
+  writeln
+end.

+ 106 - 0
shell/tests/nonvacuity.sh

@@ -1009,6 +1009,112 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# The string variable (t39).  A STRING is the first declared type bigger than
+# a word that this compiler accepts, and it is accepted in two places: DefVar
+# exempts TString from its `size > 2' catch-all so `var s : string [10]' can
+# exist at all, and the two arms below USE it once it does.  The three
+# mutations are the three places the feature lives, each restored to the
+# exact pre-fix state (all three used to be ENoLib, or never existed):
+#
+#   1. the DEPARSE of assignment: before the fix, `s := 'hello'' fell out of
+#      Assignment's size check.  Removing the string arm puts it back there.
+#   2. the WRITE: before the fix, `writeln (s)' was refused by IoCall's
+#      type dispatch.  Restoring the refusal arm is removing the feature.
+#   3. the RUNTIME: wrstr's print loop is only as good as its length byte,
+#      and only rt_exec.py can see a wrstr that reads no length at all.
+
+# 1. Assignment's string arm is gone, so `s := 'hello'' trips the old
+#    `size > 2' refusal again.  The arm is spliced out by its boundaries, not
+#    by its text, so it cannot silently stop matching when the comment inside
+#    is touched.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+a = s.index('   IF symtab [r.idx].cls = TString THEN')
+m = '      RETURN\n   END ;\n   IF symtab [r.idx].size > 2 THEN\n'
+i = s.index(m, a)
+new = ('   IF symtab [r.idx].cls = TString THEN\n'
+       '      Err (ENoLib) ;              (* MUTATION: the arm is refused *)\n'
+       '      RETURN\n')
+open(p, 'w').write(s[:a] + new + s[i + len('      RETURN\n'):])
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t39 red when the string-assignment arm is refused" \
+            "t39_strvar" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild with the string assignment refused"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the string-assignment mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
+# 2. The write arm of IoCall, restored to its pre-fix refusal.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = """                     EmPushVarAddr (symtab [arg.idx].local, symtab [arg.idx].off) ;
+                     ent := TU_WrStr"""
+assert s.count(old) == 1, 'the string-write arm found %d times -- update this mutation' % s.count(old)
+rep = """                     Err (ENoLib) ;
+                     RETURN"""
+open(p, 'w').write(s.replace(old, rep))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t39 red when a string write is refused again (ENoLib)" \
+            "t39_strvar" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild with the string write refused"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the string-write mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
+# 3. The runtime half: wrstr never reads the length byte, so every string -
+#    "hello" and the empty one alike - prints nothing.  The compiler, the
+#    golden and the audit all stay green; only executing the entry finds it.
+cp "$SAVED" Runtime.mod
+if python3 - <<'PYX'
+p = 'Runtime.mod'
+s = open(p).read()
+old = """   MovBxBp4 ;                        (* BX := the caller's string address *)
+   XorCxCx ;
+   MovClBx ;                         (* CX := the length byte at [BX] *)"""
+assert s.count(old) == 1, 'the wrstr length load found %d times -- update this mutation' % s.count(old)
+rep = """   MovBxBp4 ;                        (* BX := the caller's string address *)
+   XorCxCx ;                         (* CX is never loaded: prints nothing *)"""
+open(p, 'w').write(s.replace(old, rep))
+PYX
+then
+    if rebuild; then
+        expect_red "the runtime cases catch a wrstr that reads no length" \
+            "wrstr" python3 tests/rt_exec.py
+    else
+        echo "  FAIL: the runtime would not rebuild with the length load dropped"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the wrstr length-load mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED" Runtime.mod
+# Re-emit the object: the `rebuild` above compiled Runtime.o from the MUTATED
+# source, and the restored-compiler check next links that object - without
+# this recompile it would report on the mutation, not on the restored code.
+"$GM2" -fiso -c Runtime.mod >/dev/null 2>&1 || {
+    echo "  FAIL: Runtime.mod would not recompile after the restore"
+    fail=$((fail + 1))
+}
+
 if rebuild_compiler; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
         # No count: the matrix grows every time a fixture is added, and a

+ 18 - 1
shell/tests/rt_exec.py

@@ -66,6 +66,8 @@ HDR = 0x0800
 DLEN = 32
 DATA = 0x0810
 STORE = 0x0840
+WSTR = 0x0850
+WSTR_EMPTY = 0x0858
 CASE = 0x0860
 CASE_SZ = 40
 
@@ -92,7 +94,7 @@ SOH, STX, ETX, EOT = 0x01, 0x02, 0x03, 0x04
 # the lookup fail loudly instead of silently testing offset 0.
 ENTS = ["initmem", "progend", "stackchk", "wrint", "wrchar", "wrbool",
         "wrreal", "wrln", "rdint", "rdchar", "rdbool", "rdln", "halt",
-        "wrtinl", "rdbyte"]
+        "wrtinl", "rdbyte", "wrstr"]
 
 # A call slot's flags, as rtdrv.s reads them.
 F_STACK = 1     # the argument goes on the stack, popped by the caller
@@ -493,6 +495,21 @@ def build_cases():
         [("wrtinl", 0, F_INL)], out=b"hello",
         note="the one entry whose argument is not a stack word")
 
+    # wrstr is the other way round from wrtinl: its argument IS a stack word,
+    # but a word that has to be a POINTER, so the string's bytes live in memory
+    # instead of after the call.  WSTR (seeded in rtdrv.s, restated here) is a
+    # length byte and five characters - the same bytes wrtinl reads from the
+    # code stream - so the two cases must print the same word.  The argument
+    # carries LOAD_BIAS because a runtime entry dereferences a MEMORY address,
+    # not an image offset.
+    add("wrstr prints the length byte's worth of characters",
+        [s("wrstr", WSTR + LOAD_BIAS)], out=b"hello",
+        note="the entry dereferences the stack word; the bytes are in memory")
+
+    add("wrstr prints nothing for an empty string",
+        [s("wrstr", WSTR_EMPTY + LOAD_BIAS)], out=b"",
+        note="a length byte of 0 must print no characters at all")
+
     return cs
 
 

+ 166 - 145
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 BA 02    mov bx,2bah
+003C  BB DA 02    mov bx,2dah
 003F  89 DE       mov si,bx
 0041  31 D2       xor dx,dx
 0043  F7 F1       div ax,cx
@@ -73,18 +73,18 @@
 0072  83 7E 04 00 cmp word ptr [bp+4h],0h
 0076  89 EC       mov sp,bp
 0078  75 05       jne 7fh
-007A  BA BF 02    mov dx,2bfh
+007A  BA DF 02    mov dx,2dfh
 007D  EB 03       jmp 82h
-007F  BA BA 02    mov dx,2bah
+007F  BA DA 02    mov dx,2dah
 0082  B4 09       mov ah,9h
 0084  CD 21       int 21h
 0086  5D          pop bp
 0087  C3          ret
-0088  BA C8 02    mov dx,2c8h
+0088  BA E8 02    mov dx,2e8h
 008B  B4 09       mov ah,9h
 008D  CD 21       int 21h
 008F  C3          ret
-0090  BA C5 02    mov dx,2c5h
+0090  BA E5 02    mov dx,2e5h
 0093  B4 09       mov ah,9h
 0095  CD 21       int 21h
 0097  C3          ret
@@ -101,147 +101,168 @@
 00A9  FF E3       jmp bx
 00AB  55          push bp
 00AC  8B EC       mov bp,sp
-00AE  50          push ax
-00AF  53          push bx
-00B0  51          push cx
-00B1  52          push dx
-00B2  57          push di
-00B3  E8 13 00    call 0c9h
-00B6  83 F9 02    cmp cx,2h
-00B9  74 05       je 0c0h
-00BB  8B 7E 04    mov di,word ptr [bp+4h]
-00BE  89 05       mov word ptr [di],ax
-00C0  5F          pop di
-00C1  5A          pop dx
-00C2  59          pop cx
-00C3  5B          pop bx
+00AE  53          push bx
+00AF  51          push cx
+00B0  50          push ax
+00B1  8B 5E 04    mov bx,word ptr [bp+4h]
+00B4  31 C9       xor cx,cx
+00B6  8A 0F       mov cl,byte ptr [bx]
+00B8  43          inc bx
+00B9  B4 02       mov ah,2h
+00BB  E3 07       jcxz 0c4h
+00BD  8A 07       mov al,byte ptr [bx]
+00BF  CD 21       int 21h
+00C1  43          inc bx
+00C2  E2 F9       loop 0bdh
 00C4  58          pop ax
-00C5  89 EC       mov sp,bp
-00C7  5D          pop bp
-00C8  C3          ret
-00C9  E8 C3 00    call 18fh
-00CC  3C 1A       cmp al,1ah
-00CE  74 42       je 112h
-00D0  3C 20       cmp al,20h
-00D2  76 F5       jbe 0c9h
-00D4  31 C9       xor cx,cx
-00D6  3C 2D       cmp al,2dh
-00D8  75 06       jne 0e0h
-00DA  41          inc cx
-00DB  E8 B1 00    call 18fh
-00DE  EB 07       jmp 0e7h
-00E0  3C 2B       cmp al,2bh
-00E2  75 03       jne 0e7h
-00E4  E8 A8 00    call 18fh
-00E7  31 FF       xor di,di
-00E9  3C 30       cmp al,30h
-00EB  72 18       jb 105h
-00ED  3C 39       cmp al,39h
-00EF  77 14       jnbe 105h
-00F1  2C 30       sub al,30h
-00F3  B4 00       mov ah,0h
-00F5  87 C7       xchg di,ax
-00F7  BB 0A 00    mov bx,0ah
-00FA  F7 E3       mul bx
-00FC  03 C7       add ax,di
-00FE  89 C7       mov di,ax
-0100  E8 8C 00    call 18fh
-0103  EB E4       jmp 0e9h
-0105  E8 9E 00    call 1a6h
-0108  83 F9 00    cmp cx,0h
-010B  74 02       je 10fh
-010D  F7 DF       neg di
-010F  8B C7       mov ax,di
-0111  C3          ret
-0112  B9 02 00    mov cx,2h
-0115  C3          ret
-0116  55          push bp
-0117  8B EC       mov bp,sp
-0119  50          push ax
-011A  53          push bx
-011B  51          push cx
-011C  52          push dx
-011D  57          push di
-011E  E8 A8 FF    call 0c9h
-0121  83 F9 02    cmp cx,2h
-0124  74 05       je 12bh
-0126  8B 7E 04    mov di,word ptr [bp+4h]
-0129  88 05       mov byte ptr [di],al
-012B  5F          pop di
-012C  5A          pop dx
-012D  59          pop cx
-012E  5B          pop bx
-012F  58          pop ax
-0130  89 EC       mov sp,bp
-0132  5D          pop bp
-0133  C3          ret
-0134  55          push bp
-0135  8B EC       mov bp,sp
-0137  50          push ax
-0138  53          push bx
-0139  57          push di
-013A  E8 52 00    call 18fh
-013D  88 C2       mov dl,al
-013F  8B 7E 04    mov di,word ptr [bp+4h]
-0142  88 15       mov byte ptr [di],dl
-0144  5F          pop di
-0145  5B          pop bx
-0146  58          pop ax
-0147  89 EC       mov sp,bp
-0149  5D          pop bp
-014A  C3          ret
-014B  55          push bp
-014C  8B EC       mov bp,sp
-014E  50          push ax
-014F  53          push bx
-0150  51          push cx
-0151  57          push di
-0152  E8 3A 00    call 18fh
-0155  31 C9       xor cx,cx
-0157  3C 54       cmp al,54h
-0159  74 12       je 16dh
-015B  3C 74       cmp al,74h
-015D  74 0E       je 16dh
-015F  3C 59       cmp al,59h
-0161  74 0A       je 16dh
-0163  3C 79       cmp al,79h
-0165  74 06       je 16dh
-0167  3C 31       cmp al,31h
-0169  74 02       je 16dh
-016B  EB 01       jmp 16eh
-016D  41          inc cx
-016E  8B 7E 04    mov di,word ptr [bp+4h]
-0171  89 0D       mov word ptr [di],cx
-0173  5F          pop di
-0174  59          pop cx
-0175  5B          pop bx
-0176  58          pop ax
-0177  89 EC       mov sp,bp
-0179  5D          pop bp
-017A  C3          ret
-017B  50          push ax
-017C  E8 10 00    call 18fh
-017F  3C 0D       cmp al,0dh
+00C5  59          pop cx
+00C6  5B          pop bx
+00C7  89 EC       mov sp,bp
+00C9  5D          pop bp
+00CA  C3          ret
+00CB  55          push bp
+00CC  8B EC       mov bp,sp
+00CE  50          push ax
+00CF  53          push bx
+00D0  51          push cx
+00D1  52          push dx
+00D2  57          push di
+00D3  E8 13 00    call 0e9h
+00D6  83 F9 02    cmp cx,2h
+00D9  74 05       je 0e0h
+00DB  8B 7E 04    mov di,word ptr [bp+4h]
+00DE  89 05       mov word ptr [di],ax
+00E0  5F          pop di
+00E1  5A          pop dx
+00E2  59          pop cx
+00E3  5B          pop bx
+00E4  58          pop ax
+00E5  89 EC       mov sp,bp
+00E7  5D          pop bp
+00E8  C3          ret
+00E9  E8 C3 00    call 1afh
+00EC  3C 1A       cmp al,1ah
+00EE  74 42       je 132h
+00F0  3C 20       cmp al,20h
+00F2  76 F5       jbe 0e9h
+00F4  31 C9       xor cx,cx
+00F6  3C 2D       cmp al,2dh
+00F8  75 06       jne 100h
+00FA  41          inc cx
+00FB  E8 B1 00    call 1afh
+00FE  EB 07       jmp 107h
+0100  3C 2B       cmp al,2bh
+0102  75 03       jne 107h
+0104  E8 A8 00    call 1afh
+0107  31 FF       xor di,di
+0109  3C 30       cmp al,30h
+010B  72 18       jb 125h
+010D  3C 39       cmp al,39h
+010F  77 14       jnbe 125h
+0111  2C 30       sub al,30h
+0113  B4 00       mov ah,0h
+0115  87 C7       xchg di,ax
+0117  BB 0A 00    mov bx,0ah
+011A  F7 E3       mul bx
+011C  03 C7       add ax,di
+011E  89 C7       mov di,ax
+0120  E8 8C 00    call 1afh
+0123  EB E4       jmp 109h
+0125  E8 9E 00    call 1c6h
+0128  83 F9 00    cmp cx,0h
+012B  74 02       je 12fh
+012D  F7 DF       neg di
+012F  8B C7       mov ax,di
+0131  C3          ret
+0132  B9 02 00    mov cx,2h
+0135  C3          ret
+0136  55          push bp
+0137  8B EC       mov bp,sp
+0139  50          push ax
+013A  53          push bx
+013B  51          push cx
+013C  52          push dx
+013D  57          push di
+013E  E8 A8 FF    call 0e9h
+0141  83 F9 02    cmp cx,2h
+0144  74 05       je 14bh
+0146  8B 7E 04    mov di,word ptr [bp+4h]
+0149  88 05       mov byte ptr [di],al
+014B  5F          pop di
+014C  5A          pop dx
+014D  59          pop cx
+014E  5B          pop bx
+014F  58          pop ax
+0150  89 EC       mov sp,bp
+0152  5D          pop bp
+0153  C3          ret
+0154  55          push bp
+0155  8B EC       mov bp,sp
+0157  50          push ax
+0158  53          push bx
+0159  57          push di
+015A  E8 52 00    call 1afh
+015D  88 C2       mov dl,al
+015F  8B 7E 04    mov di,word ptr [bp+4h]
+0162  88 15       mov byte ptr [di],dl
+0164  5F          pop di
+0165  5B          pop bx
+0166  58          pop ax
+0167  89 EC       mov sp,bp
+0169  5D          pop bp
+016A  C3          ret
+016B  55          push bp
+016C  8B EC       mov bp,sp
+016E  50          push ax
+016F  53          push bx
+0170  51          push cx
+0171  57          push di
+0172  E8 3A 00    call 1afh
+0175  31 C9       xor cx,cx
+0177  3C 54       cmp al,54h
+0179  74 12       je 18dh
+017B  3C 74       cmp al,74h
+017D  74 0E       je 18dh
+017F  3C 59       cmp al,59h
 0181  74 0A       je 18dh
-0183  3C 0A       cmp al,0ah
+0183  3C 79       cmp al,79h
 0185  74 06       je 18dh
-0187  3C 1A       cmp al,1ah
+0187  3C 31       cmp al,31h
 0189  74 02       je 18dh
-018B  EB EF       jmp 17ch
-018D  58          pop ax
-018E  C3          ret
-018F  8B 1E CE 02 mov bx,word ptr [2ceh]
-0193  83 FB 00    cmp bx,0h
-0196  74 09       je 1a1h
-0198  8A C3       mov al,bl
-019A  31 DB       xor bx,bx
-019C  89 1E CE 02 mov word ptr [2ceh],bx
-01A0  C3          ret
-01A1  B4 08       mov ah,8h
-01A3  CD 21       int 21h
-01A5  C3          ret
-01A6  88 C2       mov dl,al
-01A8  BB 01 00    mov bx,1h
-01AB  8A DA       mov bl,dl
-01AD  89 1E CE 02 mov word ptr [2ceh],bx
-01B1  C3          ret
+018B  EB 01       jmp 18eh
+018D  41          inc cx
+018E  8B 7E 04    mov di,word ptr [bp+4h]
+0191  89 0D       mov word ptr [di],cx
+0193  5F          pop di
+0194  59          pop cx
+0195  5B          pop bx
+0196  58          pop ax
+0197  89 EC       mov sp,bp
+0199  5D          pop bp
+019A  C3          ret
+019B  50          push ax
+019C  E8 10 00    call 1afh
+019F  3C 0D       cmp al,0dh
+01A1  74 0A       je 1adh
+01A3  3C 0A       cmp al,0ah
+01A5  74 06       je 1adh
+01A7  3C 1A       cmp al,1ah
+01A9  74 02       je 1adh
+01AB  EB EF       jmp 19ch
+01AD  58          pop ax
+01AE  C3          ret
+01AF  8B 1E EE 02 mov bx,word ptr [2eeh]
+01B3  83 FB 00    cmp bx,0h
+01B6  74 09       je 1c1h
+01B8  8A C3       mov al,bl
+01BA  31 DB       xor bx,bx
+01BC  89 1E EE 02 mov word ptr [2eeh],bx
+01C0  C3          ret
+01C1  B4 08       mov ah,8h
+01C3  CD 21       int 21h
+01C5  C3          ret
+01C6  88 C2       mov dl,al
+01C8  BB 01 00    mov bx,1h
+01CB  8A DA       mov bl,dl
+01CD  89 1E EE 02 mov word ptr [2eeh],bx
+01D1  C3          ret