Bläddra i källkod

compiler: LENGTH, the first string builtin (CALC string builtins)

The ladder moves: CALC.PAS went from ERR 41 at 10405 to ERR 41 at 10753.
`Length(S)' inside CALC.INC's Center compiles, and the next blocker is `ClrScr'
in a nested procedure - a CRT identifier.

  - BI_Length, registered as a KBuiltin (the id lives in `cls', as the other
    builtins' do).  It is dispatched in ParseAtom, because LENGTH is a
    FUNCTION used in an expression, not a statement.
  - A string's length is the byte at [address], so the call is loaded INLINE:
    for a variable, EmLoadVar of one byte at its off; for a literal, EmStrBlob
    emits it inline and the byte is read from that image offset.  No runtime
    entry, no address pushed, and EmMovAh0 clears the high byte.
  - A non-string argument is ETypeErr; a computed string is ENoLib.

Fixture t68_length: Length of a variable, of a literal and of the empty string
- .out 5/3/0 - executed under qemu AND Exec86 (58/58).

nonvacuity.sh gains a case that removes the LENGTH registration: t68 then fails
to compile (ERR 41) against an expected OK 122 25.  88 ok, 0 failed.

run_all.sh: ALL PASS - matrix 69/69, ladder 1/1 at ERR 41 10753, 60 .COM
linked, 58 executed under qemu and Exec86; helper audit PASS.
Eric Streit 2 dagar sedan
förälder
incheckning
45baf26579

+ 11 - 9
CALC-COMPILE.md

@@ -97,7 +97,7 @@ Current rung (measured today, normalised position):
 
 | position | blocker |
 |---|---|
-| **10405** | `Length(S)` inside `CALC.INC`'s `Center` — a **string builtin** (`ERR 41`), the first rung that is a standard FUNCTION |
+| **10753** | `ClrScr` in `CALC.INC`'s nested `Welcome` — a **CRT identifier** (`ERR 41`) |
 
 Rungs behind it (now passed):
 
@@ -111,12 +111,13 @@ Rungs behind it (now passed):
 | 8705 | `Cells = array['A'..'G',1..FYMax]`, `Sheet : Cells`, `XPOS` | Stage 8 (declarations) |
 | 9202 | `MCFile : file of CellRec` — file type, whole `var` block, `{$I}` expansion | Stage 13 (file types) |
 | 10350 | `procedure Center(S : AnyString)` — a **string value parameter** | calling convention, steps 1-3 |
+| 10405 | `Length(S)` — the first string builtin | `LENGTH`, inline |
 
 Rungs ahead:
 
 | position | blocker |
 |---|---|
-| 10405 | the string builtins (`Length`, `Copy`, `Delete`, `Insert`, `Str`, `Val`), then the rest of `CALC.INC` |
+| 10753 | the CRT identifiers (`ClrScr`, `GotoXY`, `LowVideo`/`NormVideo`, `ClrEol`, `KeyPressed`, `Delay`, `Kbd`), then the remaining string builtins and `CALC.INC` |
 
 Corrections the measurements force, worth stating plainly because the stage list
 below was written before them:
@@ -183,7 +184,7 @@ tables, so this list is also the `KProc`/`KFunc` work list.
 | category | builtins (count in CALC) |
 |---|---|
 | **present today** | `write` 41, `writeln` 5, `read` 15, `readln` 1, `halt` 1 |
-| strings | `length` 19, `copy` 5, `delete` 3, `insert` 1, `str` 3, `val` 5 |
+| strings | `length` 19 (**done**, `t68_length`), `copy` 5, `delete` 3, `insert` 1, `str` 3, `val` 5 |
 | ordinal | `chr` 5, `upcase` 12, `succ` 5, `pred` 1 |
 | reals | `ln` 5, `exp` 3, `abs` 3, `sqrt` 2, `sqr` 1, `sin` 1, `cos` 1, `arctan` 1, `int` 1, `str`/`val` above |
 | heap | `new` 1, `dispose` 1 |
@@ -463,10 +464,11 @@ Exec86):
   (`v-TP3-CALC-STRPARAM`).  Fixture `t67_strparam` proves value semantics: the
   callee mutates `S[1]`, the caller's string is unchanged.  This moved the rung
   from 10350 to **10405**.
-- **The ladder now stops at `ERR 41` at `10405`** — `Length(S)`, inside
-  `Center`'s body.  The string builtins (`Length`, `Copy`, `Delete`, `Insert`,
-  `Str`, `Val`) are the next increment, and the first rung that is a standard
-  FUNCTION rather than a type.
+- **`LENGTH` is landed** — the first string builtin FUNCTION, loaded inline
+  from the string's length byte (no runtime entry).  Fixture `t68_length`
+  (5/3/0).  The rung moved 10405 → **10753**.
+- **The ladder now stops at `ERR 41` at `10753`** — `ClrScr` in `CALC.INC`'s
+  nested `Welcome`.  The **CRT identifiers** are the next group.
 - **One surprise on resuming:** the working tree was found holding the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
@@ -476,8 +478,8 @@ Exec86):
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   been green and only `t40_readln` (and later CALC) would have noticed.
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
-  **OVERALL: ALL PASS**, compile matrix **68/68**, CALC ladder **1/1** at
-  `ERR 41 10405`, 59 `.COM` linked and 57 executed under both qemu and Exec86
+  **OVERALL: ALL PASS**, compile matrix **69/69**, CALC ladder **1/1** at
+  `ERR 41 10753`, 60 `.COM` linked and 58 executed under both qemu and Exec86
   (`t64_arrays` is compile-only: its image is past the 8 KB boot-sector read).
 - **`tests/nonvacuity.sh`:** re-run after this commit's cases (the set
   membership bit among them); see the commit message for the count. It is not in

+ 8 - 9
SUMMARY.md

@@ -2037,16 +2037,15 @@ 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 **string value parameters** are landed: the ladder
+features through **the first string builtin** are landed: the ladder
 (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
-CALC.PAS now stops at `ERR 41` at pos 10405, the `Length` builtin inside
-`CALC.INC`), the `{$I}` include, `^X`/`#nn`, scalar typed constants, sets,
-enums, records (CellRec = 80), subranges, arrays, element/field access, file
-types, and string value parameters (`t67_strparam`; fixtures through `t67`).
-The next rungs are the string builtins (`Length`/`Copy`/`Delete`/`Insert`/
-`Str`/`Val`), file operations, variable-index subscripts + `with`, pointers,
-`var` params, reals, CRT, recursion, and the layout change. Items 1-4 below are
-independent of both.
+CALC.PAS now stops at `ERR 41` at pos 10753, `ClrScr` inside `CALC.INC`), the
+`{$I}` include, `^X`/`#nn`, scalar typed constants, sets, enums, records
+(CellRec = 80), subranges, arrays, element/field access, file types, string
+value parameters, and `Length` (`t68_length`; fixtures through `t68`). The next
+rungs are the CRT identifiers, 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.
 
 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)`

+ 37 - 2
shell/Compiler.mod

@@ -114,7 +114,7 @@ CONST
 
    (* which standard procedure a KBuiltin symbol denotes *)
    BI_Write   = 0 ;  BI_WriteLn = 1 ;  BI_Read  = 2 ;
-   BI_ReadLn  = 3 ;  BI_Halt    = 4 ;
+   BI_ReadLn  = 3 ;  BI_Halt    = 4 ;  BI_Length = 5 ;
 
    (* keyword tokens *)
    TkNone = 0 ;   TkProgram = 1 ;  TkBegin  = 2 ;  TkEnd = 3 ;
@@ -3240,6 +3240,38 @@ BEGIN
          fixture had ever exercised the fold. *)
       ParseVar (r) ;
       RETURN
+   ELSIF (symtab [idx].tag = KBuiltin) AND (symtab [idx].cls = BI_Length) THEN
+      (* LENGTH (s): the string's length byte, loaded inline.  No runtime
+         entry, and no address is pushed - a string's value already carries its
+         length at [addr].  A literal is emitted inline (EmStrBlob) and the
+         byte is read from that image offset. *)
+      IF NOT MatchDelim ('(') THEN
+         Err (ENoSemi) ;
+         RETURN
+      END ;
+      ParseExpr (r) ;
+      ExpectDelim (')', ENoSemi) ;
+      IF NOT OK () THEN
+         RETURN
+      END ;
+      IF r.cls # TString THEN
+         Err (ETypeErr) ;
+         RETURN
+      END ;
+      IF r.kind = 1 THEN
+         EmLoadVar (symtab [r.idx].local,
+                    (symtab [r.idx].off + r.boff) MOD 10000H, 1)
+      ELSIF r.kind = 3 THEN
+         EmLoadVar (FALSE, EmStrBlob (r), 1)
+      ELSE
+         Err (ENoLib) ;
+         RETURN
+      END ;
+      EmMovAh0 () ;
+      r.kind := 2 ;
+      r.cls := TScalar ;
+      r.chr := FALSE ;
+      RETURN
    ELSE
       Err (EUnknown)
    END
@@ -5080,7 +5112,10 @@ BEGIN
    symtab [i].goPos := TU_RdInt ;
    i := NewSym ("HALT"   , KBuiltin, BI_Halt   , 0, 0, 0, 0, FALSE) ;
    symtab [i].defnd := TRUE ;
-   symtab [i].goPos := TU_Halt
+   symtab [i].goPos := TU_Halt ;
+   i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;
+   symtab [i].defnd := TRUE ;
+   symtab [i].goPos := 0            (* inline: the length byte, no entry *)
 END DefBuiltins ;
 
 PROCEDURE Inittur () ;

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

@@ -25,9 +25,9 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 #
-# Current rung (2026-10-20): `Length(S)' inside CALC.INC's Center - a string
-# BUILTIN, not a declaration.  The rung moved from 10350 (`procedure Center(S :
-# AnyString)') when STRING VALUE PARAMETERS landed: the whole slot is copied
-# onto the stack, length byte at BP+4, and the callee mutates only its copy.
-# This is the first rung that is a standard FUNCTION rather than a type.
-CALC.PAS	ERR	41	10405
+# Current rung (2026-10-20): `ClrScr' in CALC.INC's nested `Welcome' - a CRT
+# identifier (`ERR 41').  The rung moved from 10405 (`Length(S)') when LENGTH
+# landed (loaded inline from the string's length byte).  The CRT identifiers
+# are the next group: GotoXY, ClrScr, LowVideo/NormVideo, ClrEol, KeyPressed,
+# Delay, Kbd.
+CALC.PAS	ERR	41	10753

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

@@ -528,3 +528,9 @@ t66_files	OK	50	12
 #                   address; a literal argument is copied too.  data=25 = t (21)
 #                   + the @@T temporaries; the slot lives on the stack.
 t67_strparam	OK	189	25
+#   t68_length             LENGTH(s) - the first string builtin FUNCTION.
+#                   CALC.INC:20 uses it in `(80-Length(S)) div 2'.  Inline:
+#                   the length byte at [addr], no runtime entry.  A literal
+#                   argument is emitted inline and its byte read there.  .out
+#                   is 5/3/0 (a variable, a literal, the empty string).
+t68_length	OK	122	25

+ 3 - 0
shell/tests/fixtures/t68_length.out

@@ -0,0 +1,3 @@
+5
+3
+0

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

@@ -0,0 +1,23 @@
+program t68_length;
+
+{ t68 -- LENGTH, the first string builtin FUNCTION.  CALC.INC:20 is
+
+      for I := 1 to ((80-Length(S)) div 2) do Write(' ');
+
+  A string's length is the byte at [address], so Length is loaded inline - no
+  runtime entry and no address pushed.  A literal is emitted inline
+  (EmStrBlob) and its length byte read from that image offset.
+
+  CALC also writes LENGTH(StandardfunctionNames[sf]) - a string ELEMENT of an
+  array with a variable index - which needs the variable-index work and is not
+  what this fixture tests; here the argument is a variable and a literal. }
+
+var
+   s : string[20];
+begin
+   s := 'hello';
+   writeln (Length (s));
+   writeln (Length ('abc'));
+   s := '';
+   writeln (Length (s))
+end.

+ 28 - 0
shell/tests/nonvacuity.sh

@@ -1670,6 +1670,34 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# --- a string builtin is registered ---------------------------------------
+# LENGTH has to be in the symbol table, or the name is simply unknown.  Remove
+# its registration and t68 stops compiling (ERR 41), so the compile matrix
+# carries this one.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = ('   i := NewSym ("LENGTH" , KBuiltin, BI_Length , 0, 0, 0, 0, FALSE) ;\n'
+       '   symtab [i].defnd := TRUE ;\n'
+       '   symtab [i].goPos := 0            (* inline: the length byte, no entry *)\n')
+assert s.count(old) == 1, 'the LENGTH registration found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, ''))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t68 red when LENGTH is not registered" \
+            "expected: OK 122 25" tests/run_compile_tests.sh
+    else
+        echo "  FAIL: the compiler would not rebuild without the LENGTH builtin"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the LENGTH-registration mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
 if rebuild_compiler; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"