소스 검색

compiler: 6-byte REAL constants and copies; Value := 0 compiles

CALC.INC:163 is `Value := 0'; Value is a 6-byte Real.  The format is the
one CALC-PORT.md measured from CALCDEMO.MCS against 1..5:

    value = (1 + N/2^39) * 2^(E-129)

byte 0 = E biased by 129, bytes 1..5 = N little-endian, sign in bit 7 of
byte 5, all-zero = 0.0.

IntToMbf converts an integer constant at COMPILE time (the bytes are
known), and Assignment stores the six bytes at symbol+boff or through
DI; a real field copies to another real field byte for byte (DI
destination included, with a byte store at [DI+off]).  A real RHS that
is neither a constant nor a copy - real arithmetic, a real function
result - is still ENoLib, loudly: this is representation, not algebra.

t80_realconst stores 0,1,2,3,5 both through a symbol and through DI, and
copies one real field to a DI-addressed one, with integer fields on
either side of V to pin the store's WIDTH.  No oracle can read a Real
back yet, so tests/check_realconst.py is the value check: it compiles
with @image and asserts the measured operand bytes in the emitted code
(the six B8 vv 00 A2 runs with consecutive addresses, the C6 45 form,
and the 88 45 0..5 copy).  Non-vacuity changes the exponent bias to 128
and drops the implicit 1 from the fraction; both fail only that check.

The ladder moves 15272 -> 15911, past `Value := 0', `DEC := 2' and
`FW := 10' to `Upcase(Ch)' - an unknown builtin.
Eric Streit 1 일 전
부모
커밋
0946e8033f

+ 128 - 0
shell/Compiler.mod

@@ -1396,6 +1396,12 @@ BEGIN
    Ebyte (0C6H) ; Ebyte (45H) ; Ebyte (VAL (BYTE, off)) ; Ebyte (VAL (BYTE, v))
 END EmStDiImm ;
 
+PROCEDURE EmStDiAlOff (off : CARDINAL) ;
+(* MOV [DI+off],AL: one byte of a real copied through a DI-addressed field. *)
+BEGIN
+   Ebyte (88H) ; Ebyte (45H) ; Ebyte (VAL (BYTE, off))
+END EmStDiAlOff ;
+
 PROCEDURE EmLdAlDiOff (off : CARDINAL) ;
 (* MOV AL,[DI+off]: the byte of a set variable that holds an element's bit. *)
 BEGIN
@@ -3872,16 +3878,138 @@ BEGIN
    END
 END ParseLabelStmt ;
 
+PROCEDURE IntToMbf (v : LONGINT ; VAR b : ARRAY OF CHAR) ;
+(* TP3's 6-byte Real, as measured in CALC-PORT.md against the cells holding
+   1, 2, 3, 4 and 5:
+
+      value = (1 + N / 2^39) * 2^(E - 129)
+
+   byte 0 is E (biased by 129); bytes 1..5 are N, little-endian; the SIGN is
+   bit 7 of byte 5 (the book's `sign + 39-bit mantissa').  An all-zero value
+   is 0.0.  1 -> 81 00 00 00 00 00, 3 -> 82 00 00 00 00 40,
+   5 -> 83 00 00 00 00 20 - all cross-checked against CALCDEMO.MCS.
+
+   Only INTEGER values are converted; real arithmetic and formatting are a
+   later stage, and a real RHS that is not a constant or a copy is ENoLib. *)
+VAR a, p, n : LONGINT ;
+    e, k, i : CARDINAL ;
+    neg : BOOLEAN ;
+BEGIN
+   i := 0 ;
+   WHILE i < 6 DO
+      b [i] := CHR (0) ;
+      INC (i)
+   END ;
+   IF v = 0 THEN
+      RETURN
+   END ;
+   neg := v < 0 ;
+   IF neg THEN
+      a := -v
+   ELSE
+      a := v
+   END ;
+   (* k = floor(log2(a)); p = 2^k *)
+   p := 1 ;
+   k := 0 ;
+   WHILE (k < 39) AND (p * 2 <= a) DO
+      p := p * 2 ;
+      INC (k)
+   END ;
+   IF k >= 39 THEN
+      Err (EConstRange) ;
+      RETURN
+   END ;
+   e := 129 + k ;
+   b [0] := CHR (e) ;
+   n := a - p ;
+   i := 39 - k ;
+   WHILE i > 0 DO
+      n := n * 2 ;
+      DEC (i)
+   END ;
+   i := 1 ;
+   WHILE i <= 5 DO
+      b [i] := CHR (VAL (CARDINAL, n MOD 256)) ;
+      n := n DIV 256 ;
+      INC (i)
+   END ;
+   IF neg THEN
+      b [5] := CHR (ORD (b [5]) + 128)
+   END
+END IntToMbf ;
+
 PROCEDURE Assignment (r : ERes) ;
 (* ':=' already consumed by the caller; store expression into r *)
 VAR src : ERes ;
     k, n : CARDINAL ;
     sbuf : ARRAY [0..31] OF CHAR ;
+    rbuf : ARRAY [0..5] OF CHAR ;
 BEGIN
    IF (r.kind # 1) AND (r.kind # 5) THEN
       Err (EUnknown) ;
       RETURN
    END ;
+   IF r.cls = TReal THEN
+      (* A 6-byte REAL.  A compile-time ordinal constant is converted to the
+         measured MBF layout; another REAL field is copied byte for byte.
+         Real arithmetic and real-valued expressions are a later stage and
+         are refused, not guessed. *)
+      ParseExpr (src) ;
+      IF NOT OK () THEN
+         RETURN
+      END ;
+      IF (src.kind = 0) AND (src.cls # TString) AND (src.cls # TSet) THEN
+         IntToMbf (src.imm, rbuf) ;
+         IF r.kind = 5 THEN
+            k := 0 ;
+            WHILE k < 6 DO
+               EmStDiImm (k, ORD (rbuf [k])) ;
+               INC (k)
+            END
+         ELSIF r.kind = 1 THEN
+            k := 0 ;
+            WHILE k < 6 DO
+               EmMovAxi (ORD (rbuf [k])) ;
+               EmStoreVar (symtab [r.idx].local,
+                           (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+               INC (k)
+            END
+         ELSE
+            Err (ENoLib)
+         END ;
+         RETURN
+      END ;
+      IF (src.kind = 1) AND (src.cls = TReal) THEN
+         IF r.kind = 5 THEN
+            IF src.kind = 5 THEN
+               Err (ENoLib) ;
+               RETURN
+            END ;
+            k := 0 ;
+            WHILE k < 6 DO
+               EmLoadVar (symtab [src.idx].local,
+                          (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
+               EmStDiAlOff (k) ;
+               INC (k)
+            END
+         ELSIF r.kind = 1 THEN
+            k := 0 ;
+            WHILE k < 6 DO
+               EmLoadVar (symtab [src.idx].local,
+                          (symtab [src.idx].off + src.boff + k) MOD 10000H, 1) ;
+               EmStoreVar (symtab [r.idx].local,
+                           (symtab [r.idx].off + r.boff + k) MOD 10000H, 1) ;
+               INC (k)
+            END
+         ELSE
+            Err (ENoLib)
+         END ;
+         RETURN
+      END ;
+      Err (ENoLib) ;
+      RETURN
+   END ;
    IF r.cls = TSet THEN
       (* `field := [elements]'.  The constructor's TYPE comes from the target:
          CALC's CellStatus is `set of Attributes' (one byte), not the default

+ 2 - 0
shell/tests/audit_helpers.py

@@ -168,6 +168,8 @@ NON_ONE_LINE = {
         "StDiImm": COMP,
         "LdAlDiOff": COMP,
         "AndAlImm": COMP,
+        # A real copy through DI: one byte of the six.
+        "StDiAlOff": COMP,
     },
 }
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,

+ 102 - 0
shell/tests/check_realconst.py

@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""check_realconst.py -- the six bytes of TP3's Real, against CALC-PORT.md.
+
+CALC-PORT.md decoded the format from CALCDEMO.MCS, cross-checked against the
+cells holding 1, 2, 3, 4 and 5:
+
+    value = (1 + N/2^39) * 2^(E-129)
+
+byte 0 = E biased by 129, bytes 1..5 = N little-endian, the sign is bit 7 of
+byte 5, and all-zero is 0.0.  No execution oracle can read a Real back yet, so
+this check asserts the OPERAND BYTES in the emitted code instead: integer
+constants stored through a symbol (B8 vv 00 A2 addr), through DI (C6 45 off
+vv), and the six-byte copy (A0 src / 88 45 off).
+
+The fixture is t80_realconst; its execution still checks the integer fields on
+either side of the Real, which is what pins the store's WIDTH.
+"""
+import os
+import re
+import subprocess
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SHELL = os.path.dirname(HERE)
+FIX = os.path.join(HERE, "fixtures", "t80_realconst.pas")
+
+# From the measured table (CALC-PORT.md).
+MBF = {
+    "0": bytes.fromhex("000000000000"),
+    "1": bytes.fromhex("810000000000"),
+    "2": bytes.fromhex("820000000000"),
+    "3": bytes.fromhex("820000000040"),
+    "5": bytes.fromhex("830000000020"),
+}
+
+
+def dump():
+    r = subprocess.run([os.path.join(SHELL, "compiletest")],
+                       input=("@image\n" + FIX + "\n").encode(),
+                       capture_output=True, cwd=SHELL)
+    text = r.stdout.decode("latin-1")
+    if "  OK " not in text:
+        sys.exit("the fixture did not compile:\n" + text)
+    data = bytearray()
+    for line in text.splitlines():
+        m = re.match(r"^\s+([0-9A-Fa-f]{4}):\s+((?:[0-9A-Fa-f]{2}\s*)+)$", line)
+        if m:
+            data.extend(int(t, 16) for t in m.group(2).split())
+    return bytes(data)
+
+
+def symbol_stores(data):
+    """The 6-byte runs of B8 vv 00 A2 lo hi with consecutive addresses."""
+    runs = []
+    i = 0
+    while i + 6 <= len(data):
+        if data[i] == 0xB8 and data[i + 2] == 0x00 and data[i + 3] == 0xA2:
+            runs.append((data[i + 1], data[i + 4] | (data[i + 5] << 8)))
+            i += 6
+        else:
+            i += 1
+    found = {}
+    for s in range(len(runs) - 5):
+        seg = runs[s:s + 6]
+        if all(seg[t + 1][1] == seg[t][1] + 1 for t in range(5)):
+            found[bytes(v for v, _ in seg)] = seg[0][1]
+    return found
+
+
+def main():
+    data = dump()
+    problems = []
+
+    found = symbol_stores(data)
+    for name, want in MBF.items():
+        if want not in found:
+            problems.append("no symbol store of %s as %s" % (name, want.hex(" ")))
+        else:
+            print("  %s -> %s at %04xh" % (name, want.hex(" "), found[want]))
+
+    # The DI form of `V := 1'.
+    di1 = bytes.fromhex("C6450081C6450100C6450200C6450300C6450400C6450500")
+    if di1 not in data:
+        problems.append("the DI store of 1 is not C6 45 00 81 ... 05 00")
+
+    # The six-byte copy through DI.
+    for k in range(6):
+        if bytes([0x88, 0x45, k]) not in data:
+            problems.append("the copy never stores byte %d through DI" % k)
+
+    if problems:
+        print("FAIL:")
+        for p in problems:
+            print("  - " + p)
+        return 1
+    print("PASS: the measured MBF bytes for 0,1,2,3,5 are emitted, the DI "
+          "store holds them,\n      and the six-byte copy stores every byte")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 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-21): `Value := 0' in CALC.INC's Init - a REAL field
-# assignment.  String-field assignment through DI landed (t79_strfield),
-# moving 15201 -> 15272.  Value is a 6-byte Real; the RHS is the integer 0, so
-# this needs the real representation and an integer-to-real conversion before
-# any real arithmetic can follow.
-CALC.PAS	ERR	102	15272
+# Current rung (2026-10-21): `Upcase(Ch)' in CALC.INC's Clear - an unknown
+# builtin.  The REAL field assignment landed (t80_realconst: the measured
+# 6-byte MBF layout, integer constants and 6-byte copies), moving 15272 ->
+# 15911 past Init's `Value := 0', `DEC := 2' and `FW := 10'.  UpCase is the
+# next rung; the big real-arithmetic stage waits further ahead.
+CALC.PAS	ERR	41	15911

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

@@ -599,3 +599,8 @@ t78_setfield	OK	279	23
 #                   source via strstore, and a short literal length (not the
 #                   capacity).  Also pins the field offset in writeln.
 t79_strfield	OK	307	78
+#   t80_realconst          a REAL field assigned integer constants: the
+#                   measured 6-byte MBF layout, symbol and DI stores, a
+#                   6-byte copy; the integer fields on either side pin the
+#                   width.  tests/check_realconst.py asserts the bytes.
+t80_realconst	OK	462	73

+ 2 - 0
shell/tests/fixtures/t80_realconst.out

@@ -0,0 +1,2 @@
+23
+0

+ 56 - 0
shell/tests/fixtures/t80_realconst.pas

@@ -0,0 +1,56 @@
+program t80_realconst;
+
+{ t80 -- a REAL record field assigned from integer constants.
+
+  CALC.INC:163 is `Value := 0'; Value is a 6-byte Real.  TP3's layout is
+  measured in CALC-PORT.md against the cells holding 1, 2, 3, 4, 5:
+
+      value = (1 + N/2^39) * 2^(E-129)
+
+  byte 0 = E biased by 129, bytes 1..5 = N little-endian, sign in bit 7 of
+  byte 5, all-zero = 0.0.
+
+  Only compile-time ordinal constants and 6-byte copies are accepted; real
+  arithmetic/formatting is a later stage and is refused loudly.  The .out
+  checks the fields A and B on either side of V (the store is six bytes, not
+  more), and tests/check_realconst.py asserts the six bytes themselves
+  against the measured table. }
+
+type
+   Cell = record
+      V : real;
+      A, B : integer;
+   end;
+
+var
+   g : array [1..2, 1..3] of Cell;
+   i, j : integer;
+
+begin
+   with g[2,3] do
+   begin
+      V := 0;
+      A := 11;
+      B := 12
+   end;
+   with g[2,3] do
+      V := 1;
+   i := 2;
+   j := 3;
+   with g[i,j] do
+      V := 1;          (* the DI form: C6 45 <off> <byte> *)
+   with g[2,3] do
+      V := 2;
+   with g[2,3] do
+      V := 3;
+   with g[2,3] do
+      V := 5;
+   with g[1,1] do
+      V := 3;
+   with g[i,j] do
+      V := g[1,1].V;   (* 6-byte copy, destination through DI *)
+   with g[2,3] do
+      A := A + B;
+   writeln (g[2,3].A);
+   writeln (g[1,1].A)
+end.

+ 44 - 0
shell/tests/nonvacuity.sh

@@ -1995,6 +1995,50 @@ else
 fi
 cp "$SAVED_C" Compiler.mod
 
+# --- the Real exponent bias is 129, not 128 -------------------------------
+# byte 0 is E + 129 in CALC-PORT.md's measured table.  One less and every
+# constant's exponent is wrong; the byte check reads the emitted code.
+cp "$SAVED_C" Compiler.mod
+if mutate_compiler 's|   e := 129 + k ;|   e := 128 + k ;|'; then
+    if rebuild_compiler; then
+        if ./tests/run_compile_tests.sh --build-only >/dev/null 2>&1; then
+            expect_red "the Real bytes red when the exponent bias is 128"                 "FAIL" python3 tests/check_realconst.py
+        else
+            echo "  FAIL: compiletest would not rebuild with the wrong bias"
+            fail=$((fail + 1))
+        fi
+    else
+        echo "  FAIL: the compiler would not rebuild with the wrong bias"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the Real-bias mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
+# --- the fraction is (a - 2^k), not a ------------------------------------
+# Dropping the implicit 1 from the mantissa: 1.0 becomes 2.0, 3.0's bytes are
+# wrong, and the measured table no longer appears.
+cp "$SAVED_C" Compiler.mod
+if mutate_compiler 's|   n := a - p ;|   n := a ;|'; then
+    if rebuild_compiler; then
+        if ./tests/run_compile_tests.sh --build-only >/dev/null 2>&1; then
+            expect_red "the Real bytes red when the implicit 1 is missing"                 "FAIL" python3 tests/check_realconst.py
+        else
+            echo "  FAIL: compiletest would not rebuild without the implicit 1"
+            fail=$((fail + 1))
+        fi
+    else
+        echo "  FAIL: the compiler would not rebuild without the implicit 1"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the implicit-1 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"

+ 3 - 0
shell/tests/run_all.sh

@@ -209,6 +209,9 @@ run "frame displ"     python3 tests/check_framedisp.py
 # This one asks "does the target have this opcode at all", and asserts the
 # lowering that replaced the two illegal ones is the original compiler's shape.
 run "8086 opcodes"   python3 tests/check_8086.py
+# The 6-byte Real's operand bytes, against CALC-PORT.md's measured table:
+# no oracle can read a Real back yet, so this reads the emitted code.
+run "real constants" python3 tests/check_realconst.py
 run "UI error path"   python3 tests/uitest.py
 run "UI success path" python3 tests/comtest.py
 # And the path that produces NO artifact: R compiles, pokes the image into the