#pragma mode( separator(.,;) integer(h32) )
// ============================================================================
//  CASProbe beta 1d  --  how does a PPL program talk to the HP Prime's CAS?
//  Roger Metcalf, 2026.  Written with AI assistance (Anthropic Claude).
//
//  Free for educational and personal use.  Keep this header with the file,
//  credit the author.  Not for sale or commercial redistribution.  Provided
//  as is, with no warranty.
//
//  Firmware tested on: HP Prime Virtual Calculator 2.4.2 (build 2026-09-09),
//  17 Oct 2026: beta 1a, then beta 1b (CAS available, call form 2, 42 pass,
//  8 fail, 19 values, 0 errors), then beta 1c (44 pass, 9 fail, 19 values,
//  0 errors: T43b's own test was wrong, see WHAT CHANGED IN BETA 1d).
//  Beta 1d is not yet run anywhere but the PC harness (ppl_run_v3e).  Not
//  yet run on a G1 or G2.
//  Target: HP Prime G2 and G1, firmware 2026-09-09.
//
//  WHY THIS EXISTS
//  On 16 Oct 2026 Roger allowed the CAS into fleet programs, behind a fence
//  (BRIEF_CAS.md, CAS_Python_Ideas_2026-10-16_v1.md).  Every fleet program
//  will call the CAS through ONE routine, xxCas(s), whose key line is
//      cR := STRING(CAS(xxS));
//  Nothing about the CAS has run on our calculators yet.  This probe asks
//  the calculator, test by test, and writes what it saw to CX_LOG:
//    - which call form works when the CAS text is in a VARIABLE (the way
//      every program builds it), and whether all 30 digits of C(100, 50)
//      come back as text;
//    - whether fractions stay fractions, whether a CAS string is a PPL
//      string, and what exact / approximate mode does to the text;
//    - why Home's STRING() of a number must never go into CAS text (digit
//      grouping marks, a special minus sign), and whether our own digit
//      routine, xxIStr, is the cure;
//    - whether IFERR catches a CAS error, and what undef looks like;
//    - whether a CAS variable survives from one call to the next (so a
//      student's x could leak into our text) and whether purge clears it;
//    - limits, solve, fsolve, diff and int on teaching examples, with the
//      TIME of every call -- the G1 timings matter most;
//    - a 300-digit integer and a degree-36 generating polynomial;
//    - from MicroPython: are integers exact at any size, and does
//      cas.caseval work (one author reported it crashing the calculator).
//
//  HOW TO RUN
//  Paste into the Connectivity Kit, sync, run CASProbe from the Program
//  Catalog with the Terminal closed.  Choose "Run every test".  Most tests
//  run by themselves; a progress page names the test being run.  Before the
//  last test a warning page asks whether to run cas.caseval (+ = run it,
//  - or Esc = skip, or tap a button).  At the end: a SUMMARY page --
//  photograph it -- then the log pager (Up/Down = a line, Left/Right or a
//  tap = a page, Esc = done).  Run it on the G2 first, then on the G1.
//  CX_LOG, CX_SUM and CX_LAST stay in Home afterwards.  If the program ever
//  stops silently, type CX_LAST in Home: it names the test that was running.
//  If the calculator resets during the cas.caseval test, rerun CASProbe and
//  choose "Skip Python CAS".
//
//  WHAT CHANGED IN BETA 1d (17 Oct 2026, after beta 1c ran on the emulator)
//  Beta 1c ran on the HP Prime Virtual Calculator 2.4.2 (build 2026-09-09).
//  Its summary said "pass 44, fail 9, value 19, error 0, skip 0", one FAIL
//  more than the 45 / 8 expected.  The extra FAIL was T43b:
//      T43b FAIL STRING(0.096, 2, 1) is not 9.6E-2
//        = 9.6E-2, codes 57 46 54 7431 8722 50
//  and the summary line read "STRING(0.096, 2, 1): NOT scientific".  The
//  probe was wrong, not the Prime.  The Prime DOES print the scientific
//  form (7431 is the small capital E), but the exponent's minus is
//  CHAR(8722), Home's minus sign (U+2212, the one T01 finds at the start of
//  STRING(-2.5)), not the ASCII hyphen, 45, that T43b compared against.
//  (The log shows both characters as they are; this ASCII file writes them
//  E and -.)  So beta 1d:
//    - T43b PASSes when the text holds CHAR(7431), the scientific form,
//      whatever the sign character; a note line under it says which minus
//      the exponent used: CHAR(8722), ASCII 45 or none.  Its FAIL line now
//      reads "STRING(0.096, 2, 1) is not scientific".
//    - cxSci, and so the summary line, follows the same test: on the
//      emulator it reads "STRING(0.096, 2, 1): scientific".
//  Expected on the emulator: 45 pass, 8 fail, 19 values, 0 errors.  Every
//  other test keeps its number and its code.  The beta 1c paragraph below
//  wrote the exponent's minus as a hyphen; on the Prime it is CHAR(8722).
//
//  WHAT CHANGED IN BETA 1c (17 Oct 2026, after emulator round 2)
//  Typed by hand in Home on the emulator: INSTRING("", "a") is "Error:
//  Invalid input" (the program stops), LEFT("abc", 0) is "abc" -- the WHOLE
//  string -- and STRING(0.096, 2, 1) is "9.6E-2", scientific (the E is the
//  small capital, CHAR(7431)).  The probe tested a CAS answer with INSTRING
//  even when the call had failed and the answer was "", so a failed call
//  stopped its test with ERROR instead of logging a FAIL:
//    - cxCas, the kit (T07, T32): the undef / "rror" test runs only on a
//      non-empty answer, as in every fleet kit now;
//    - cxFlat, which every result line passes through (cxShow): it looked
//      for a line break in "" after a failed call; it now looks only while
//      text is left, and a break at the very start no longer takes
//      LEFT(s, 0), which would have looped for ever;
//    - cxSplit (T33, T34) and cxPyType (T34): an empty answer, a ; or a
//      quote at the very end, or one at the very start, the same way;
//    - T25, T39 and T34b: an answer that may be "" is tested with DIM first;
//    - cxLineCol (the pager): an empty line, should CX_LOG ever hold one.
//  NEW TEST T43 (no CAS; runs after T42): INSTRING("", "a") inside IFERR
//  (PASS = an error), STRING(0.096, 2, 1) (PASS = "9.6E-2" with CHAR(7431))
//  and the fleet's cure STRING(ROUND(0.096, 1), 2, 1) (PASS = "0.1"), so a
//  G1 / G2 run confirms both rules.  Two summary lines in the log say what
//  T43 found.  Every other test keeps its number and its code.  The two
//  #PYTHON blocks are unchanged (their comments still say beta 1b).
//
//  WHAT CHANGED IN BETA 1b (17 Oct 2026, after beta 1a ran on the emulator)
//  Beta 1a ran on the HP Prime Virtual Calculator 2.4.2 (build 2026-09-09).
//  Its call-form tests asked for comb(100,50), all 30 digits.  Two things
//  stopped every form, so T11-T41 were skipped:
//    - CAS(s) with s a string VARIABLE hands the text back unevaluated
//      (T06, T07).  The fleet's xxCas as written does not work.
//    - EXPR("CAS(" + q + s + q + ")") does evaluate (T08), but a CAS integer
//      too big for Home crosses back as a 12-digit real, 1.00891344546E29.
//  By hand in Home, EXPR("CAS(\"string(comb(100,50))\")") gave all 30 digits,
//  and STRING(EXPR("CAS(\"comb(8,4)/2^8\")")) gave 35/128.  So beta 1b:
//    - T06-T09 ask for string(comb(100,50)): the CAS makes the text, and a
//      string crosses whole;
//    - forms 2 and 3 of cxCall wrap every CAS text in string(...), so the
//      tests T11-T41 now run through the form the fleet would use.
//  Nothing else changes; every test keeps its number.
//
//  WHAT CHANGED IN BETA 1a (16 Oct 2026, for the programs being built on
//  the CAS: ErrorRate Lab, RankLab, the regression and Fisher pages)
//  Eight tests added, T35-T42.  They take new numbers so that every beta 1
//  test keeps its number (other programs' notes cite them); they run after
//  T32 and before the Python tests, which stay last.  Nothing else changed
//  except the version strings, the pass-flag lists (40 -> 50 entries) and
//  three new lines in the log's summary.
//    T35 one CAS() text of about 1,000, 2,000, 3,000 and 6,000 characters
//        (a sum of exact fractions, answer N/(N+1)): works?  how long?
//        which length breaks it?
//    T36 solve of two equations: the exact text shape of the answer, and
//        the least-squares normal equations with fractional answers
//    T37 normal() and factor() on expressions in tagged symbols: the shape
//    T38 does a product print with * (or a dot)?  does a list print with
//        [ ] or { }?  (a list, two roots, a matrix)
//    T39 series(..., cxt=0, 3) and convert(..., polynom): the shape
//    T40 the binomial-to-Poisson limits ErrorRate Lab sends, timed
//    T41 how the answer of an infinite limit prints: inf, infinity, a glyph
//    T42 (no CAS) BINOMIAL_CDF(30, 0.2, 2) = 0.044179: the (n, p, k) order
//
//  WHAT IT CHANGES ON THE CALCULATOR
//  Only its own Home variables (CX_LOG and the five below).  It READS
//  HSeparator and HFormat and never changes them; it never changes the CAS
//  settings.  T25 assigns the CAS variable cxq and purges it again, and only
//  if cxq was unassigned to begin with.  T25 also READS (never assigns) the
//  CAS variables x, n, p, k and t, to see whether any is assigned now.
//
//  THE TESTS (riskiest last; each runs inside IFERR, so one failure cannot
//  stop the rest; the PASS line for each is what desktop Giac 1.9 gives)
//    T01 Home STRING(123456789012) and STRING(-2.5): plain ASCII?
//    T02 cxIStr, the fleet's digit routine (BRIEF_CAS xxIStr)
//    T03 HSeparator and HFormat, read only
//    T04 STRING(CAS("comb(100,50)")) with a literal string: 30 digits?
//    T05 a CAS answer kept in a LOCAL: its TYPE, STRING and "" + answer
//    T06 CAS(s) with s a LOCAL string variable
//    T07 cxCas(s): the BRIEF_CAS call routine exactly as written
//    T08 EXPR("CAS(" + q + s + q + ")"), the literal form built at run time
//    T09 EXPR("CAS.eval(" + q + s + q + ")")   (PyReversi, EE-CpE applet)
//    T10 EXPR("CAS.comb(100,50)")              (HP Commands.pdf, CAS.function)
//        -> the first of T07, T08, T09 that passes is the call form used
//           for T11-T32 and the one the summary reports
//    T11 comb(8,4)/2^8 -> 35/128
//    T12 string(comb(8,4)/2^8): is the answer a PPL string?
//    T13 exact(0.4857142857142857) -> 17/35
//    T14 exact or approximate mode: 1/3, sqrt(2), evalf, cas_setup()
//    T15 cxIStr text fed to the CAS
//    T16 limit(n(1-(1/20)^(1/n)), n, inf) -> ln(20)     (rule of three)
//    T17 limit(cosh(t/sqrt(n))^n, n, inf) -> exp(t^2/2) (CLT for coins)
//    T18 solve PPV = 1/2 for the prevalence -> 1/20
//    T19 fsolve((1-p)^30 = 5/100, p = 0..1) -> 0.0950339
//    T20 diff(ln(p/(1-p)), p), simplify, value at p = 1/4 -> 16/3
//    T21 int of the normal pdf from -inf to 196/100 -> 0.975002
//    T22 comb(1000,500): all 300 digits?
//    T23 expand the Mann-Whitney 6 vs 6 generating polynomial (degree 36)
//    T24 its 37 coefficients read back as a PPL list (sum 924)
//    T25 does a CAS variable persist between calls?  does purge clear it?
//    T26 a CAS syntax error: caught by IFERR?
//    T27 1/0 as text           T28 0/0 (undef) as text
//    T29 a misspelt command    T30 fsolve with no root
//    T31 Home STRING() of a number fed to the CAS
//    T32 the BRIEF_CAS guard rejects undef and errors?  (T32b: are the CAS
//        settings, cas_setup(), the same as at T14?)
//    T35-T42, added in beta 1a, run here (see WHAT CHANGED IN BETA 1a)
//    T43 (beta 1c, no CAS) INSTRING("", "a") and STRING(0.096, 2, 1), after T42
//    T33 MicroPython: 2**100, comb(100,50), CAS text read from PPL
//    T34 MicroPython: cas.caseval  (warning page first; can be skipped)
//
//  Names: every routine and file-level LOCAL carries the tag cx (no other
//  fleet program uses it); CAS symbols are cxn, cxt, cxp, cxz, cxq, cxj --
//  never x, n, p, k or t, which a student may own in the CAS view.  Beta 1a
//  adds cxa, cxb and cxl.
//  House rules (Lessons Learned v7f): LOCAL lines of six or fewer; frames
//  composed in G1 and blitted once; capped finger-lift drain; drain-wait-
//  drain round every wait; RETURN, not PRINT, at the end.
// ============================================================================

EXPORT CX_LOG := {};     // one line per finding (46 characters at most) -- read it in Home
EXPORT CX_LAST := "";    // the test that was running last (crash marker); "done" at the end
EXPORT CX_SUM := "";     // the one-line verdict, also returned to Home
EXPORT CX_CS := "";      // T33: the CAS's 30-digit text, handed to Python to read
EXPORT CX_PYS := "";     // T33/T34: Python's answer, fields separated by ;
EXPORT CX_PYM := "";     // T33/T34: Python's last progress mark (survives a crash)

// ---------------------------------------------------------------- palette --
// The house colours (Lessons Learned section 13), set once in cxInit().
LOCAL cxBLUE, cxMAG, cxRED, cxGRN, cxORNG, cxFOOT;
LOCAL cxGRAY, cxLBLU, cxLGRN, cxLRED, cxLORN;

// ------------------------------------------------------------- the state --
// cxForm: which call form cxCall uses (1 STRING(CAS(s)), 2 the EXPR form,
// 3 the CAS.eval form).  cxAvail: 1 when a form works from a variable.
// cxMs / cxErr: time and error flag of the last cxCall.  cxOK(n): 1 when
// test n passed.  cxNP ... cxNS: how many PASS, FAIL, VALUE, ERROR, SKIP.
LOCAL cxForm, cxAvail, cxMs, cxErr, cxSlowMs, cxSlowId;
LOCAL cxNP, cxNF, cxNV, cxNE, cxNS, cxOK;
LOCAL cxCurN, cxCur, cxWhat, cxTX, cxTY, cxC50;
LOCAL cxQB, cxFormTxt, cxCasTxt, cxPyMode, cxPyA, cxPyB;
LOCAL cxHomeOK, cxPersist, cxQuo, cxCatOK, cxSet1;
LOCAL cxLong, cxInfTxt, cxBcdf;          // beta 1a: T35, T41, T42
LOCAL cxIns0, cxSci;                     // beta 1c: T43

cxInit()
BEGIN
  LOCAL iI;
  cxBLUE := RGB(0,60,200);       // results that are values
  cxMAG  := RGB(200,0,200);      // derived: the call form to use
  cxRED  := RGB(255,0,0);        // FAIL, ERROR, the warning
  cxGRN  := RGB(0,140,60);       // PASS
  cxORNG := RGB(255,69,0);       // frames, title bands
  cxFOOT := RGB(30,144,255);     // footers (dodger blue)
  cxGRAY := RGB(90,90,90);       // notes
  cxLBLU := RGB(235,242,255);    // light fills
  cxLGRN := RGB(232,246,235);
  cxLRED := RGB(255,235,235);
  cxLORN := RGB(255,240,220);
  // C(100, 50), the 30-digit integer every call-form test asks for
  cxC50 := "100891344545564193334812497256";
  // The Mann-Whitney 6 vs 6 generating function: the Gaussian binomial
  // [12 choose 6] in cxq, written out (no symbolic product index).
  cxQB := "(1-cxq^7)*(1-cxq^8)*(1-cxq^9)*(1-cxq^10)*(1-cxq^11)*(1-cxq^12)/((1-cxq)*(1-cxq^2)*(1-cxq^3)*(1-cxq^4)*(1-cxq^5)*(1-cxq^6))";
  cxForm := 1;  cxAvail := 0;  cxMs := 0;  cxErr := 0;
  cxSlowMs := -1;  cxSlowId := "(none)";
  cxNP := 0;  cxNF := 0;  cxNV := 0;  cxNE := 0;  cxNS := 0;
  cxCurN := 0;  cxCur := "";  cxWhat := "";  cxTX := -1;  cxTY := -1;
  cxFormTxt := "(not decided)";  cxCasTxt := "";  cxPyMode := 2;
  cxPyA := "not run";  cxPyB := "not run";  cxHomeOK := -1;  cxPersist := -1;
  cxCatOK := -1;  cxSet1 := "";
  cxLong := -1;  cxInfTxt := "not tested";  cxBcdf := -1;
  cxIns0 := -1;  cxSci := -1;
  // one pass flag and one "came back in quotes" flag per test, built with a
  // plain loop (Lessons Learned s5)
  cxOK := {};
  cxQuo := {};
  FOR iI FROM 1 TO 50 DO
    cxOK := CONCAT(cxOK, {0});
    cxQuo := CONCAT(cxQuo, {0});
  END;
END;

// ------------------------------------------------------- text helpers --

// A whole number as plain digits: no digit-grouping marks, no decimal point,
// no exponent.  Copied from BRIEF_CAS.md (xxIStr) with the tag cx; T02 tests
// it.  Home's STRING() follows the Home number format (PyProbe saw grouping
// marks and a special minus sign in argv), which the CAS cannot read, so CAS
// text is built only from this routine.
cxIStr(cxN)
BEGIN
  LOCAL dS, dV, dD;
  dV := ROUND(ABS(cxN), 0);
  IF dV == 0 THEN RETURN "0"; END;
  dS := "";
  WHILE dV > 0 DO
    dD := dV MOD 10;
    dS := CHAR(48 + dD) + dS;
    dV := (dV - dD) / 10;
  END;
  IF cxN < 0 THEN dS := "-" + dS; END;
  RETURN dS;
END;

// A number for the log: whole numbers through cxIStr, others as Home shows them.
cxNum(nV)
BEGIN
  IF nV == IP(nV) THEN RETURN cxIStr(nV); END;
  RETURN STRING(nV);
END;

// "T05" from 5.
cxId(nN)
BEGIN
  IF nN < 10 THEN RETURN "T0" + cxIStr(nN); END;
  RETURN "T" + cxIStr(nN);
END;

// The text from character k to the end; "" when k is past the end (so no
// MID ever starts beyond a string -- what the device does then is untested).
cxRest(rS, rK)
BEGIN
  IF rK > DIM(rS) THEN RETURN ""; END;
  RETURN MID(rS, rK);
END;

// Add one finding to CX_LOG.  Long text is cut into lines of at most 46
// characters (font 1 fits about 48 across the screen), at the last space
// that keeps a line under the limit; text with no space there (a long run
// of digits) is cut at 46.  Continuation lines start with two spaces, so
// the log reads the same in Home as on the pager.
cxNote(nS)
BEGIN
  LOCAL nT, nL, nB, nK;
  nT := nS;
  nL := 46;
  WHILE DIM(nT) > nL DO
    // look back from character 47 for a space (but not into the first 24),
    // skipping the space inside "(12 ms)" so a time stays on one line
    nB := nL + 1;
    nK := 0;
    WHILE nB > 24 AND nK == 0 DO
      IF MID(nT, nB, 1) == " " THEN
        nK := 1;
        IF nB + 3 <= DIM(nT) THEN
          IF MID(nT, nB + 1, 3) == "ms)" THEN nK := 0; END;
        END;
      END;
      IF nK == 0 THEN nB := nB - 1; END;
    END;
    IF nK == 0 THEN
      CX_LOG := CONCAT(CX_LOG, {LEFT(nT, nL)});
      nT := "  " + cxRest(nT, nL + 1);
    ELSE
      CX_LOG := CONCAT(CX_LOG, {LEFT(nT, nB - 1)});
      nT := "  " + cxRest(nT, nB + 1);
    END;
  END;
  CX_LOG := CONCAT(CX_LOG, {nT});
END;

// A CAS answer can hold line breaks (desktop Giac prints warnings): show
// each break as " / " so the log stays one string per line.
// Beta 1c: INSTRING("", ...) is "Invalid input" on the Prime and LEFT(s, 0)
// is the WHOLE string (emulator, 17 Oct 2026), so a failed call's "" stopped
// the test here, and a break at the very start would have looped for
// ever.  Now: look only while text is left; take LEFT only past 1.
cxFlat(fS)
BEGIN
  LOCAL fPos, fT, fR;
  fR := "";
  fT := fS;
  fPos := 0; IF DIM(fT) > 0 THEN fPos := INSTRING(fT, CHAR(10)); END;
  WHILE fPos > 0 DO
    IF fPos > 1 THEN fR := fR + LEFT(fT, fPos - 1); END;
    fR := fR + " / ";
    fT := cxRest(fT, fPos + 1);
    fPos := 0; IF DIM(fT) > 0 THEN fPos := INSTRING(fT, CHAR(10)); END;
  END;
  RETURN fR + fT;
END;

// Long text shortened to its head and tail, so a 300-digit answer does not
// fill the log; the length is always logged beside it.
cxShort(sT, sN)
BEGIN
  LOCAL sD;
  sD := DIM(sT);
  IF sD <= sN THEN RETURN sT; END;
  IF sN < 24 THEN RETURN LEFT(sT, sN - 3) + "..."; END;
  RETURN LEFT(sT, sN - 14) + " ... " + MID(sT, sD - 8, 9);
END;

// Drop one pair of surrounding double quotes, if the text has them (a PPL
// string given to STRING() comes back quoted).
cxStrip(tS)
BEGIN
  LOCAL tD;
  tD := DIM(tS);
  IF tD >= 2 THEN
    IF LEFT(tS, 1) == CHAR(34) AND MID(tS, tD, 1) == CHAR(34) THEN
      RETURN MID(tS, 2, tD - 2);
    END;
  END;
  RETURN tS;
END;

// The character codes outside plain ASCII (32-126) in a string, at most
// six of them, as " 8722 65104"; "" when the text is plain ASCII.
// ASC returns a list of codes (corpus: Molar_Mass, Fingerspelling).
cxCodes(cS)
BEGIN
  LOCAL cL, cJ, cT, cN;
  cT := "";
  cN := 0;
  IF DIM(cS) == 0 THEN RETURN ""; END;
  cL := ASC(cS);
  IF TYPE(cL) <> 6 THEN cL := {cL}; END;
  FOR cJ FROM 1 TO SIZE(cL) DO
    IF (cL(cJ) < 32 OR cL(cJ) > 126) AND cN < 6 THEN
      cT := cT + " " + cxIStr(cL(cJ));
      cN := cN + 1;
    END;
  END;
  RETURN cT;
END;

// Every character code of a short string (the first twelve).
cxAllCodes(aS)
BEGIN
  LOCAL aL, aI, aT;
  aT := "";
  IF DIM(aS) == 0 THEN RETURN "(empty)"; END;
  aL := ASC(aS);
  IF TYPE(aL) <> 6 THEN aL := {aL}; END;
  FOR aI FROM 1 TO MIN(SIZE(aL), 12) DO
    aT := aT + cxIStr(aL(aI)) + " ";
  END;
  RETURN aT;
END;

// " none" for an empty code list, the list itself otherwise.
cxNone(oS)
BEGIN
  IF oS == "" THEN RETURN " none"; END;
  RETURN oS;
END;

// Split "a;b;c" at each pC into {"a","b","c"} (Python's answers).
// Beta 1c: an empty CX_PYS, or a ; at the very end, left "" for INSTRING,
// "Invalid input" on the Prime; a ; at the very start took LEFT(s, 0), the
// WHOLE string.  Now: look only while text is left; LEFT only past 1.
cxSplit(pS, pC)
BEGIN
  LOCAL pL, pP, pT;
  pL := {};
  pT := pS;
  pP := 0; IF DIM(pT) > 0 AND DIM(pC) > 0 THEN pP := INSTRING(pT, pC); END;
  WHILE pP > 0 DO
    IF pP > 1 THEN pL := CONCAT(pL, {LEFT(pT, pP - 1)}); ELSE pL := CONCAT(pL, {""}); END;
    pT := cxRest(pT, pP + 1);
    pP := 0; IF DIM(pT) > 0 THEN pP := INSTRING(pT, pC); END;
  END;
  pL := CONCAT(pL, {pT});
  RETURN pL;
END;

// A CAS list as text ("[1,1,2]", "poly1[1,1,2]", "list[1/20]", "{1,1,2}")
// turned into a PPL list: take what lies between the first opening bracket
// and the last closing one, wrap it in braces, EXPR it.  {} if that fails.
// (A number the program DRAWS with may come from the CAS this way --
// BRIEF_CAS fence rule 3 -- inside IFERR, with a PPL fallback.)
cxToList(tS)
BEGIN
  LOCAL tA, tB, tI, tC, tL;
  tA := 0;
  tB := 0;
  FOR tI FROM 1 TO DIM(tS) DO
    tC := MID(tS, tI, 1);
    IF tA == 0 AND (tC == "[" OR tC == "{") THEN tA := tI; END;
    IF tC == "]" OR tC == "}" THEN tB := tI; END;
  END;
  tL := {};
  IF tA > 0 AND tB > tA + 1 THEN
    IFERR
      tL := EXPR("{" + MID(tS, tA + 1, tB - tA - 1) + "}");
    THEN
      tL := {};
    END;
  END;
  RETURN tL;
END;

// ------------------------------------------------------ the touch kit --
// (house shapes from BRIEF_TOUCH.md, device-verified in ChiTable)

// Wait for the finger to lift, but never for more than a second.
cxDrain()
BEGIN
  LOCAL drT;
  drT := TICKS;
  REPEAT UNTIL MOUSE(1) == -1 OR TICKS - drT > 1000;
END;

// Drain, wait for a key OR a tap, drain.  Returns the key code, or -1 for a
// tap, whose x and y are left in cxTX and cxTY.
cxWaitAny()
BEGIN
  LOCAL wk, wm, wm1;
  REPEAT wk := GETKEY; UNTIL wk == -1;
  cxDrain();
  REPEAT
    wk := GETKEY;
    wm := MOUSE;
    wm1 := wm(1);
  UNTIL wk > -1 OR SIZE(wm1) > 0;
  cxTX := -1; cxTY := -1;
  IF SIZE(wm1) > 0 THEN cxTX := wm1(1); cxTY := wm1(2); wk := -1; END;
  cxDrain();
  REPEAT UNTIL GETKEY == -1;
  RETURN wk;
END;

// ------------------------------------------------------ the recording --

// Remember the slowest CAS call of the run (and which test made it).
cxSlowUp(uMs)
BEGIN
  IF uMs > cxSlowMs THEN
    cxSlowMs := uMs;
    cxSlowId := cxCur;
  END;
END;

// Start a test: name it in CX_LAST (the crash marker) and draw a progress
// page, so a CAS call that takes seconds on a G1 does not look like a hang.
cxBegin(bN, bW)
BEGIN
  cxCurN := bN;
  cxWhat := bW;
  cxCur := cxId(bN) + " " + bW;
  CX_LAST := cxCur;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 0, 0, 319, 18, cxORNG);
  TEXTOUT_P("CASProbe beta 1d: running", G1, 4, 2, 2, #FFFFFFh);
  TEXTOUT_P(cxShort(cxCur, 38), G1, 10, 60, 2, cxBLUE);
  TEXTOUT_P("so far: " + cxIStr(cxNP) + " pass, " + cxIStr(cxNF) + " fail, " + cxIStr(cxNV) + " value, " + cxIStr(cxNE) + " error", G1, 10, 90, 1, #000000h);
  TEXTOUT_P("A CAS call can take seconds on a G1.", G1, 10, 120, 1, cxGRAY);
  TEXTOUT_P("If it stops here for good, type CX_LAST in Home.", G1, 10, 136, 1, cxGRAY);
  BLIT_P(G0, 0, 0, G1);
END;

// One result line: "T05 PASS what (12 ms)".  rV is PASS, FAIL, VALUE,
// ERROR or SKIP; rMs < 0 means no time to report.
cxRec(rId, rV, rW, rMs)
BEGIN
  LOCAL rS;
  rS := rId + " " + rV + " " + rW;
  IF rMs >= 0 THEN rS := rS + " (" + cxIStr(rMs) + " ms)"; END;
  IF rV == "PASS" THEN
    cxNP := cxNP + 1;
    IF cxCurN >= 1 AND cxCurN <= 50 THEN cxOK(cxCurN) := 1; END;
  END;
  IF rV == "FAIL" THEN cxNF := cxNF + 1; END;
  IF rV == "VALUE" THEN cxNV := cxNV + 1; END;
  IF rV == "ERROR" THEN cxNE := cxNE + 1; END;
  IF rV == "SKIP" THEN cxNS := cxNS + 1; END;
  cxNote(rS);
END;

// The text the CAS gave, on the line(s) under the result, with any
// characters outside plain ASCII listed by code (radical, infinity ...).
cxShow(wT)
BEGIN
  LOCAL wC;
  cxNote("  = " + cxShort(cxFlat(wT), 130));
  wC := cxCodes(wT);
  IF wC <> "" THEN cxNote("  codes over 126:" + wC); END;
END;

// ---------------------------------------------------- calling the CAS --

// The ONE place this program calls the CAS, exactly as BRIEF_CAS.md gives
// it to every fleet program (xxCas, tag cx).  It returns the CAS answer as
// text, or "" if the CAS failed, gave undef, or reported an error.  T07
// tests it as written; T32 tests its guard.  CASProbe's verdict on the call
// form is a change to the one IFERR line below.
cxCas(cxS)
BEGIN
  LOCAL cR;
  cR := "";
  IFERR
    cR := STRING(CAS(cxS));
  THEN
    cR := "";
  END;
  // beta 1c: an empty answer is already a failure; INSTRING("", ...) is
  // "Invalid input" on the Prime (emulator, 17 Oct 2026), so the checks run
  // only on a non-empty answer -- the round-2 fix every fleet kit got
  IF DIM(cR) > 0 THEN
    IF INSTRING(cR, "undef") > 0 OR INSTRING(cR, "rror") > 0 THEN cR := ""; END;
  END;
  RETURN cR;
END;

// The probe's own instrumented call: the same IFERR shape, with the call
// form chosen by cxForm, the time in cxMs and the error flag in cxErr.  It
// does NOT reject undef or error text, because the probe wants to see it.
//   form 1  STRING(CAS(s))                         (BRIEF_CAS as written)
//   form 2  STRING(EXPR("CAS(" + q + "string(" + s + ")" + q + ")"))
//   form 3  STRING(EXPR("CAS.eval(" + q + "string(" + s + ")" + q + ")"))
// Beta 1b: forms 2 and 3 ask the CAS for string(answer).  On the emulator
// (17 Oct 2026) a CAS integer too big for Home came back as a 12-digit
// real; a string crosses into Home whole.
// q is CHAR(34), the double quote.  Forms 2 and 3 are read by EXPR at run
// time, so if the calculator does not know them it is a run-time error that
// IFERR catches -- not a paste-time syntax error.
cxCall(cxS)
BEGIN
  LOCAL kR, kT, kQ;
  kR := "";
  cxErr := 0;
  kQ := CHAR(34);
  kT := TICKS;
  IFERR
    IF cxForm == 1 THEN kR := STRING(CAS(cxS)); END;
    IF cxForm == 2 THEN kR := STRING(EXPR("CAS(" + kQ + "string(" + cxS + ")" + kQ + ")")); END;
    IF cxForm == 3 THEN kR := STRING(EXPR("CAS.eval(" + kQ + "string(" + cxS + ")" + kQ + ")")); END;
  THEN
    kR := "";
    cxErr := 1;
  END;
  cxMs := TICKS - kT;
  cxSlowUp(cxMs);
  RETURN kR;
END;

// The same, but handing back the CAS answer itself (not STRING of it), so
// T12 can ask its TYPE.
cxObj(cxS)
BEGIN
  LOCAL oRes, oT, oQ;
  oRes := "";
  cxErr := 0;
  oQ := CHAR(34);
  oT := TICKS;
  IFERR
    IF cxForm == 1 THEN oRes := CAS(cxS); END;
    IF cxForm == 2 THEN oRes := EXPR("CAS(" + oQ + cxS + oQ + ")"); END;
    IF cxForm == 3 THEN oRes := EXPR("CAS.eval(" + oQ + cxS + oQ + ")"); END;
  THEN
    oRes := "";
    cxErr := 1;
  END;
  cxMs := TICKS - oT;
  cxSlowUp(cxMs);
  RETURN oRes;
END;

// Call, and PASS when the answer is exactly eWant (quotes dropped).
cxEq(eId, eW, eX, eWant)
BEGIN
  LOCAL eR;
  eR := cxCall(eX);
  IF cxErr == 1 THEN
    cxRec(eId, "FAIL", eW + ": CAS error", cxMs);
  ELSE
    IF cxStrip(eR) == eWant THEN
      cxRec(eId, "PASS", eW, cxMs);
    ELSE
      cxRec(eId, "FAIL", eW, cxMs);
    END;
  END;
  cxShow(eR);
  RETURN eR;
END;

// Call, and PASS when the answer CONTAINS hWant (so 0.0950338528553 and
// 9.50338528553E-2 both pass on "950338").
cxHas(hId, hW, hX, hWant)
BEGIN
  LOCAL hR;
  hR := cxCall(hX);
  IF cxErr == 1 THEN
    cxRec(hId, "FAIL", hW + ": CAS error", cxMs);
  ELSE
    IF INSTRING(hR, hWant) > 0 THEN
      cxRec(hId, "PASS", hW, cxMs);
    ELSE
      cxRec(hId, "FAIL", hW, cxMs);
    END;
  END;
  cxShow(hR);
  RETURN hR;
END;

// Call and record the answer as a VALUE (no right answer to compare with).
cxVal(vId, vW, vX)
BEGIN
  LOCAL vR;
  vR := cxCall(vX);
  IF cxErr == 1 THEN
    cxRec(vId, "VALUE", vW + ": CAS error", cxMs);
  ELSE
    cxRec(vId, "VALUE", vW, cxMs);
  END;
  cxShow(vR);
  RETURN vR;
END;

// The verdict for the call-form tests T04-T10, which all ask for C(100,50).
// It names the way a failure looked, because that says what to change.
cxVerdict50(fId, fT, fMs)
BEGIN
  LOCAL fS;
  fS := cxStrip(fT);
  IF fS == cxC50 THEN
    cxRec(fId, "PASS", cxWhat + ": 30 digits", fMs);
    cxCasTxt := fS;
    IF fT <> fS THEN cxQuo(cxCurN) := 1; END;
  ELSE
    IF fT == "(error)" THEN
      cxRec(fId, "FAIL", cxWhat + ": run-time error", fMs);
    ELSE
      IF fT == "" THEN
        cxRec(fId, "FAIL", cxWhat + ": empty answer", fMs);
      ELSE
        IF INSTRING(fT, "comb") > 0 THEN
          cxRec(fId, "FAIL", cxWhat + ": text came back unevaluated", fMs);
        ELSE
          IF INSTRING(fT, "E") > 0 OR INSTRING(fT, ".") > 0 THEN
            cxRec(fId, "FAIL", cxWhat + ": became a 12-digit real", fMs);
          ELSE
            cxRec(fId, "FAIL", cxWhat + ": other text", fMs);
          END;
        END;
      END;
    END;
  END;
  cxShow(fT);
  IF fT <> fS THEN cxNote("  (the text came back inside quotes)"); END;
END;

// ================================================================ TESTS ==

// T01  Home's STRING() of a real.  PyProbe saw U+FE50 digit-group marks and
//      a U+2212 minus when numbers crossed to Python as argv; does STRING()
//      do the same?  If it does, STRING(real) can never go into CAS text.
cxT01()
BEGIN
  LOCAL hA, hB, hC, hKa;
  cxBegin(1, "Home STRING() of a real");
  hA := STRING(123456789012);
  hB := STRING(-2.5);
  hC := "" + 123456789012;
  hKa := cxCodes(hA);
  cxHomeOK := 0;
  IF hA == "123456789012" AND hB == "-2.5" THEN cxHomeOK := 1; END;
  IF cxHomeOK == 1 THEN
    cxRec("T01", "PASS", "STRING(real) is plain ASCII", -1);
  ELSE
    cxRec("T01", "FAIL", "STRING(real) is not plain ASCII", -1);
  END;
  cxNote("  STRING(123456789012) = " + hA);
  cxNote("  codes over 126:" + cxNone(hKa));
  cxNote("  STRING(-2.5) = " + hB + ", codes " + cxAllCodes(hB));
  cxNote("  " + CHAR(34) + CHAR(34) + " + 123456789012 = " + hC);
  cxNote("  codes over 126:" + cxNone(cxCodes(hC)));
END;

// T02  cxIStr, the fleet's own digit routine.  No CAS.
cxT02()
BEGIN
  LOCAL iA, iB, iC, iG, iOK;
  cxBegin(2, "cxIStr digit routine");
  iA := cxIStr(123456789012);
  iB := cxIStr(-25);
  iC := cxIStr(0);
  iG := cxIStr(COMB(30, 15));
  iOK := 0;
  IF iA == "123456789012" AND iB == "-25" THEN
    IF iC == "0" AND iG == "155117520" THEN iOK := 1; END;
  END;
  IF iOK == 1 THEN
    cxRec("T02", "PASS", "cxIStr gives plain digits", -1);
  ELSE
    cxRec("T02", "FAIL", "cxIStr gives the wrong text", -1);
  END;
  cxNote("  123456789012 -> " + iA + ", -25 -> " + iB);
  cxNote("  0 -> " + iC + ", COMB(30,15) -> " + iG);
  cxNote("  codes over 126:" + cxNone(cxCodes(iA + iB + iG)));
END;

// T03  HSeparator (decimal mark and digit grouping) and HFormat, READ only.
//      Read through EXPR, so a name the firmware does not know is a run-time
//      error IFERR catches, not a paste-time one.  The decimal mark follows
//      the corpus rule "HSeparator MOD 9 < 4 means a dot" (repeating.zip;
//      PrimeControls uses the same split).
cxT03()
BEGIN
  LOCAL sV, fV, sM;
  cxBegin(3, "HSeparator, read only");
  sV := -1;
  fV := -1;
  IFERR sV := EXPR("HSeparator"); THEN sV := -1; END;
  IFERR fV := EXPR("HFormat"); THEN fV := -1; END;
  IF sV < 0 THEN
    cxRec("T03", "FAIL", "HSeparator could not be read", -1);
  ELSE
    sM := "dot";
    IF (sV MOD 9) >= 4 THEN sM := "comma"; END;
    cxRec("T03", "VALUE", "HSeparator = " + cxIStr(sV) + ", decimal " + sM, -1);
  END;
  cxNote("  HFormat = " + cxIStr(fV) + " (0 = Standard, -1 = unread)");
  cxNote("  (left unchanged; a comma decimal would turn");
  cxNote("  a decimal point in CAS text into trouble)");
END;

// T04  The plainest call: a literal string straight into CAS().  The kit
//      measured CAS("1/2+1/3") = 5/6 on the emulator; does the device keep
//      all 30 digits of C(100, 50) when STRING() turns the answer into text?
cxT04()
BEGIN
  LOCAL lA, lT;
  cxBegin(4, "CAS literal comb(100,50)");
  lA := "";
  lT := TICKS;
  IFERR
    lA := STRING(CAS("comb(100,50)"));
  THEN
    lA := "(error)";
  END;
  lT := TICKS - lT;
  cxSlowUp(lT);
  cxVerdict50("T04", lA, lT);
END;

// T05  The answer kept in a LOCAL first.  The kit measured TYPE 8 for a CAS
//      answer (HP's help says 14.x).  Does the LOCAL keep the exact integer,
//      or turn it into a 12-digit real?  And does "" + answer give the same
//      text as STRING(answer)?
cxT05()
BEGIN
  LOCAL kV, kT, kS, kC, kTy, kU;
  cxBegin(5, "CAS answer kept in a LOCAL");
  kTy := -1;  kS := "(error)";  kC := "(error)";  kU := -1;
  kV := 0;
  kT := TICKS;
  IFERR
    kV := CAS("comb(100,50)");
  THEN
    kTy := -2;
  END;
  kT := TICKS - kT;
  cxSlowUp(kT);
  IF kTy == -2 THEN
    cxRec("T05", "FAIL", "CAS literal: run-time error", kT);
    RETURN;
  END;
  IFERR kTy := TYPE(kV); THEN kTy := -1; END;
  IFERR kS := STRING(kV); THEN kS := "(error)"; END;
  IFERR kC := "" + kV; THEN kC := "(error)"; END;
  IFERR kU := TYPE(CAS("comb(8,4)/2^8")); THEN kU := -1; END;
  cxCatOK := 0;
  IF kC == cxC50 THEN cxCatOK := 1; END;      // "" + answer, with no quotes
  IF cxStrip(kS) == cxC50 AND cxStrip(kC) == cxC50 THEN
    cxRec("T05", "PASS", "the LOCAL keeps all 30 digits", kT);
  ELSE
    cxRec("T05", "FAIL", "the LOCAL lost the exact digits", kT);
  END;
  cxNote("  TYPE = " + cxNum(kTy) + " (0 real, 2 string, 8 CAS?)");
  cxNote("  STRING(v) = " + cxShort(kS, 40));
  cxNote("  " + CHAR(34) + CHAR(34) + " + v = " + cxShort(kC, 40));
  cxNote("  TYPE of CAS(comb(8,4)/2^8) = " + cxNum(kU));
END;

// T06  CAS(s) with s a LOCAL string -- how every program builds CAS text.
//      A real risk: the CAS may simply hand the STRING back (in Giac a
//      string evaluates to itself), and then the text comes back unevaluated.
cxT06()
BEGIN
  LOCAL sS, sA, sT;
  cxBegin(6, "CAS(s), s a LOCAL string");
  sS := "string(comb(100,50))";
  sA := "";
  sT := TICKS;
  IFERR
    sA := STRING(CAS(sS));
  THEN
    sA := "(error)";
  END;
  sT := TICKS - sT;
  cxSlowUp(sT);
  cxVerdict50("T06", sA, sT);
END;

// T07  The BRIEF_CAS call routine exactly as written (the text arrives as a
//      routine PARAMETER).  If this passes, the fleet's xxCas needs no change.
cxT07()
BEGIN
  LOCAL bA, bT;
  cxBegin(7, "xxCas as written");
  bT := TICKS;
  bA := cxCas("string(comb(100,50))");
  bT := TICKS - bT;
  cxSlowUp(bT);
  cxVerdict50("T07", bA, bT);
END;

// T08  The literal form built at run time: EXPR reads the text
//      CAS("comb(100,50)") as if it had been typed.  (EXPR: HP User Guide;
//      CAS("..."): kit CAS.md, spect.hpprgm.)
cxT08()
BEGIN
  LOCAL eS, eA, eT, eDq;
  cxBegin(8, "EXPR CAS(q s q) form");
  eDq := CHAR(34);
  eS := "string(comb(100,50))";
  eA := "";
  eT := TICKS;
  IFERR
    eA := STRING(EXPR("CAS(" + eDq + eS + eDq + ")"));
  THEN
    eA := "(error)";
  END;
  eT := TICKS - eT;
  cxSlowUp(eT);
  cxVerdict50("T08", eA, eT);
END;

// T09  CAS.eval("...") inside text that PPL evaluates at run time -- the
//      form PyReversi (hpcalc games, PyReversi_Python.txt line 246) and the
//      EE-CpE applet (ReavesX, MENU.py line 57) use to store CAS results.
cxT09()
BEGIN
  LOCAL eS, eA, eT, eDq;
  cxBegin(9, "EXPR CAS.eval(q s q) form");
  eDq := CHAR(34);
  eS := "string(comb(100,50))";
  eA := "";
  eT := TICKS;
  IFERR
    eA := STRING(EXPR("CAS.eval(" + eDq + eS + eDq + ")"));
  THEN
    eA := "(error)";
  END;
  eT := TICKS - eT;
  cxSlowUp(eT);
  cxVerdict50("T09", eA, eT);
END;

// T10  A dotted CAS command, CAS.comb(100,50).  HP's Commands.pdf: "CAS(
//      expression) or CAS.function(...) ... Note that outputs in numerical
//      mode are transformed into strings"; the User Guide's example is
//      CAS.irem(45,7); fseries.hpprgm uses CAS.round and CAS.int.  Inside
//      EXPR, so an unknown form cannot stop the paste.  Information only:
//      the fence keeps dotted calls out of fleet programs.
cxT10()
BEGIN
  LOCAL eA, eT;
  cxBegin(10, "EXPR CAS.comb(100,50)");
  eA := "";
  eT := TICKS;
  IFERR
    eA := STRING(EXPR("CAS.comb(100,50)"));
  THEN
    eA := "(error)";
  END;
  eT := TICKS - eT;
  cxSlowUp(eT);
  cxVerdict50("T10", eA, eT);
END;

// After T10: the first form that works from a variable becomes cxForm, and
// the line to put in every program's xxCas goes into the log.
cxPickForm()
BEGIN
  LOCAL pQ;
  pQ := CHAR(34);
  cxForm := 0;
  IF cxOK(7) == 1 THEN cxForm := 1; END;
  IF cxForm == 0 AND cxOK(8) == 1 THEN cxForm := 2; END;
  IF cxForm == 0 AND cxOK(9) == 1 THEN cxForm := 3; END;
  cxAvail := 0;
  IF cxForm > 0 THEN cxAvail := 1; END;
  cxNote("-- call form chosen after T04-T10 --");
  // HP's Commands.pdf says CAS "outputs in numerical mode are transformed
  // into strings"; if so, STRING() of the answer comes back inside quotes
  // (cxQuo), and the advice changes with it.
  IF cxForm == 1 THEN
    IF cxQuo(7) == 0 THEN
      cxFormTxt := "STRING(CAS(s)), xxCas as written";
      cxNote("form 1: keep xxCas as written:");
      cxNote("  cR := STRING(CAS(xxS));");
    ELSE
      IF cxCatOK == 1 THEN
        cxFormTxt := pQ + pQ + " + CAS(s), not STRING";
        cxNote("form 1, but STRING() adds quotes; in xxCas use");
        cxNote("  cR := " + pQ + pQ + " + CAS(xxS);  (T05: no quotes)");
      ELSE
        cxFormTxt := "STRING(CAS(s)), drop the quotes";
        cxNote("form 1, but STRING() adds quotes; in xxCas");
        cxNote("  keep STRING(CAS(xxS)) and drop the quotes");
      END;
    END;
  END;
  IF cxForm == 2 THEN
    cxFormTxt := "EXPR(" + pQ + "CAS(" + pQ + "+q+s+q+" + pQ + ")" + pQ + ")";
    cxNote("form 2: in xxCas use (q = CHAR(34)):");
    cxNote("  cR := STRING(EXPR(" + pQ + "CAS(" + pQ + " + CHAR(34)");
    cxNote("    + " + pQ + "string(" + pQ + " + xxS + " + pQ + ")" + pQ + " + CHAR(34) + " + pQ + ")" + pQ + "));");
  END;
  IF cxForm == 3 THEN
    cxFormTxt := "EXPR(" + pQ + "CAS.eval(" + pQ + "+q+s+q+" + pQ + ")" + pQ + ")";
    cxNote("form 3: in xxCas use (q = CHAR(34)):");
    cxNote("  cR := STRING(EXPR(" + pQ + "CAS.eval(" + pQ + " + CHAR(34)");
    cxNote("    + " + pQ + "string(" + pQ + " + xxS + " + pQ + ")" + pQ + " + CHAR(34) + " + pQ + ")" + pQ + "));");
  END;
  IF cxForm >= 2 THEN
    IF cxQuo(6 + cxForm) == 1 THEN
      cxFormTxt := cxFormTxt + ", drop quotes";
      cxNote("  the text comes in quotes: drop them (cxStrip)");
    END;
  END;
  IF cxForm == 0 THEN
    cxForm := 1;
    IF cxOK(4) == 1 THEN
      cxFormTxt := "none from a variable (literal only)";
    ELSE
      cxFormTxt := "none: the CAS gave no 30 digits";
    END;
    cxNote("no form works from a variable: " + cxFormTxt);
    cxNote("  every CAS feature must use its decimal fallback");
  END;
END;

// T11  An exact fraction: C(8,4)/2^8, Galton's bin 4 of 8 rows.
cxT11()
BEGIN
  cxBegin(11, "fraction comb(8,4)/2^8");
  cxEq("T11", "comb(8,4)/2^8 = 35/128", "comb(8,4)/2^8", "35/128");
END;

// T12  string(...) inside the CAS: does a PPL string come back (TYPE 2)?
//      And does STRING() of it add quotes, where "" + it does not?
cxT12()
BEGIN
  LOCAL gV, gTy, gS, gC;
  cxBegin(12, "CAS string() gives a PPL string?");
  gTy := -1;  gS := "(error)";  gC := "(error)";
  gV := cxObj("string(comb(8,4)/2^8)");
  IF cxErr == 1 THEN
    cxRec("T12", "FAIL", "string(...): CAS error", cxMs);
    RETURN;
  END;
  IFERR gTy := TYPE(gV); THEN gTy := -1; END;
  IFERR gS := STRING(gV); THEN gS := "(error)"; END;
  IFERR gC := "" + gV; THEN gC := "(error)"; END;
  IF gTy == 2 AND cxStrip(gC) == "35/128" THEN
    cxRec("T12", "PASS", "string(...) is a PPL string", cxMs);
  ELSE
    cxRec("T12", "VALUE", "string(...) is not TYPE 2", cxMs);
  END;
  cxNote("  TYPE = " + cxNum(gTy) + " (2 = a PPL string)");
  cxNote("  STRING(it) = " + gS + ", DIM " + cxIStr(DIM(gS)));
  cxNote("  " + CHAR(34) + CHAR(34) + " + it = " + gC + ", DIM " + cxIStr(DIM(gC)));
END;

// T13  exact() of a 16-digit decimal: the lady tasting tea's p = 17/35.
//      (Also the one test with a decimal point inside CAS text.)
cxT13()
BEGIN
  cxBegin(13, "exact(0.4857...) = 17/35");
  cxEq("T13", "exact(0.4857142857142857) = 17/35", "exact(0.4857142857142857)", "17/35");
END;

// T14  Exact or approximate mode.  Roger's CAS settings are not known and
//      are NOT changed: this only looks at what comes back.
cxT14()
BEGIN
  LOCAL aA, aD;
  cxBegin(14, "exact or approximate mode");
  aA := cxCall("1/3");
  IF cxErr == 1 THEN
    cxRec("T14a", "FAIL", "1/3: CAS error", cxMs);
  ELSE
    IF cxStrip(aA) == "1/3" THEN
      cxRec("T14a", "VALUE", "1/3 stays 1/3: CAS exact", cxMs);
    ELSE
      cxRec("T14a", "VALUE", "1/3 became decimal: approx?", cxMs);
    END;
  END;
  cxShow(aA);
  cxVal("T14b", "sqrt(2) as text", "sqrt(2)");
  cxHas("T14c", "evalf(17/35) = 0.485714...", "evalf(17/35)", "485714");
  aD := cxVal("T14d", "cas_setup(), item 1: 1 = approx", "cas_setup()");
  IF cxErr == 0 THEN cxSet1 := cxStrip(aD); END;   // compared again in T32b
END;

// T15  CAS text built from cxIStr: what every program will do with the
//      student's numbers.
cxT15()
BEGIN
  cxBegin(15, "cxIStr text fed to the CAS");
  cxEq("T15a", "comb(cxIStr(100),cxIStr(50))", "comb(" + cxIStr(100) + "," + cxIStr(50) + ")", cxC50);
  cxEq("T15b", "cxIStr(123456789012)+1", cxIStr(123456789012) + "+1", "123456789013");
END;

// T16  The rule of three as a limit: n(1 - 0.05^(1/n)) -> -ln(0.05) = ln 20.
//      If "inf" is not understood, try "infinity".
cxT16()
BEGIN
  cxBegin(16, "limit, rule of three");
  cxHas("T16", "limit = ln(20)", "limit(cxn*(1-(1/20)^(1/cxn)),cxn,inf)", "ln(20)");
  IF cxErr == 1 THEN
    cxHas("T16b", "limit with infinity = ln(20)", "limit(cxn*(1-(1/20)^(1/cxn)),cxn,infinity)", "ln(20)");
  END;
END;

// T17  The CLT for coin flips: the MGF of a standardised sum of n fair +-1
//      coins, cosh(t/sqrt(n))^n, tends to exp(t^2/2).
cxT17()
BEGIN
  cxBegin(17, "limit, CLT for coins");
  cxHas("T17", "limit = exp(cxt^2/2)", "limit(cosh(cxt/sqrt(cxn))^cxn,cxn,inf)", "cxt^2/2");
END;

// T18  BayesTree's break-even prevalence: PPV = 1/2 for a 95 %/95 % test.
cxT18()
BEGIN
  cxBegin(18, "solve PPV = 1/2");
  cxHas("T18", "solve gives 1/20", "solve((95/100)*cxp/((95/100)*cxp+(5/100)*(1-cxp))=1/2,cxp)", "1/20");
END;

// T19  ErrorRate Lab's bound: 0 errors in 30 tests, upper 95 % bound.
cxT19()
BEGIN
  cxBegin(19, "fsolve (1-p)^30 = 5/100");
  cxHas("T19", "fsolve = 0.0950339", "fsolve((1-cxp)^30=5/100,cxp=0..1)", "950338");
END;

// T20  The delta method's derivative: d/dp ln(p/(1-p)) = 1/(p(1-p)).  The
//      printed forms vary, so the PASS is its value at p = 1/4: 16/3.
cxT20()
BEGIN
  cxBegin(20, "diff of the logit");
  cxVal("T20a", "diff(ln(p/(1-p)),p)", "diff(ln(cxp/(1-cxp)),cxp)");
  cxVal("T20b", "simplify of it", "simplify(diff(ln(cxp/(1-cxp)),cxp))");
  cxEq("T20c", "its value at p = 1/4 is 16/3", "subst(diff(ln(cxp/(1-cxp)),cxp),cxp=1/4)", "16/3");
END;

// T21  The normal area below z = 1.96 (written 196/100: no decimal point in
//      CAS text), exactly and as a decimal.
cxT21()
BEGIN
  cxBegin(21, "int of the normal pdf");
  cxVal("T21a", "int, exact form", "int(exp(-cxz^2/2)/sqrt(2*pi),cxz,-inf,196/100)");
  cxHas("T21b", "evalf of it = 0.975002", "evalf(int(exp(-cxz^2/2)/sqrt(2*pi),cxz,-inf,196/100))", "975002");
END;

// T22  A 300-digit integer: C(1000, 500).  Does every digit come back?
cxT22()
BEGIN
  LOCAL gA, gD, gH, gT;
  cxBegin(22, "comb(1000,500), 300 digits");
  gA := cxStrip(cxCall("comb(1000,500)"));
  IF cxErr == 1 THEN
    cxRec("T22", "FAIL", "comb(1000,500): CAS error", cxMs);
    RETURN;
  END;
  gD := DIM(gA);
  gH := gA;
  gT := gA;
  IF gD >= 6 THEN
    gH := LEFT(gA, 6);
    gT := MID(gA, gD - 5, 6);
  END;
  IF gD == 300 AND gH == "270288" AND gT == "216320" THEN
    cxRec("T22", "PASS", "comb(1000,500): all 300 digits", cxMs);
  ELSE
    cxRec("T22", "FAIL", "comb(1000,500): digits lost", cxMs);
  END;
  cxNote("  " + cxIStr(gD) + " characters: " + gH + " ... " + gT);
END;

// T23  RankLab's generating function: the Mann-Whitney U counts for 6 vs 6
//      are the coefficients of the Gaussian binomial [12 choose 6] in q, a
//      polynomial of degree 36 whose middle coefficient is 58.
cxT23()
BEGIN
  LOCAL qA;
  cxBegin(23, "expand the 6 vs 6 polynomial");
  qA := cxCall("expand(normal(" + cxQB + "))");
  IF cxErr == 1 THEN
    cxRec("T23", "FAIL", "expand: CAS error", cxMs);
    RETURN;
  END;
  IF INSTRING(qA, "58*cxq^18") > 0 THEN
    cxRec("T23", "PASS", "degree 36, middle term 58*q^18", cxMs);
  ELSE
    cxRec("T23", "FAIL", "no 58*cxq^18 term", cxMs);
  END;
  cxNote("  " + cxIStr(DIM(qA)) + " characters: " + cxShort(qA, 60));
END;

// T24  Reading the 37 coefficients back into a PPL list -- the way a
//      program would get CAS numbers to DRAW bars with.  seq(coeff(...))
//      first; symb2poly as the second try.  Checks: 37 items, sum
//      C(12,6) = 924, item 19 (the q^18 count) = 58.
cxT24()
BEGIN
  LOCAL lA, lL, lS, lI, lT, lM;
  cxBegin(24, "coefficients back as a list");
  lA := cxCall("seq(coeff(normal(" + cxQB + "),cxq,cxj),cxj,0,36)");
  lM := cxMs;
  IF cxErr == 1 THEN
    cxNote("  seq(coeff(...)) failed; trying symb2poly");
    lA := cxCall("symb2poly(normal(" + cxQB + "),cxq)");
    lM := cxMs;
  END;
  IF cxErr == 1 THEN
    cxRec("T24", "FAIL", "coefficients: CAS error", lM);
    RETURN;
  END;
  lT := TICKS;
  lL := cxToList(lA);
  lT := TICKS - lT;
  lS := 0;
  IFERR
    FOR lI FROM 1 TO SIZE(lL) DO lS := lS + lL(lI); END;
  THEN
    lS := -1;
  END;
  IF SIZE(lL) == 37 AND lS == 924 THEN
    IF lL(19) == 58 THEN
      cxRec("T24", "PASS", "37 numbers, sum 924, middle 58", lM);
    ELSE
      cxRec("T24", "FAIL", "37 numbers but middle not 58", lM);
    END;
  ELSE
    cxRec("T24", "FAIL", "list has " + cxIStr(SIZE(lL)) + " items, sum " + cxNum(lS), lM);
  END;
  cxNote("  CAS text: " + cxShort(lA, 50));
  cxNote("  text to PPL list (EXPR): " + cxIStr(lT) + " ms");
END;

// T25  Does a CAS variable survive from one CAS() call to the next?  If it
//      does, a student's x (or n, p, k, t) leaks into any program text that
//      uses that name -- which is why the fleet uses tagged names.  Only the
//      tagged cxq is assigned, and only if it is unassigned to begin with;
//      it is purged again.  x, n, p, k and t are only READ.
cxT25()
BEGIN
  LOCAL vA, vB, vC, vD, vP, vI;
  LOCAL vN, vR, vS, vK;
  cxBegin(25, "CAS variables persist? purge?");
  vA := cxStrip(cxCall("cxq+1"));
  IF cxErr == 1 THEN
    cxRec("T25", "FAIL", "cxq+1: CAS error", cxMs);
    RETURN;
  END;
  // unassigned when the answer still names cxq ("cxq+1", or "1+cxq")
  // beta 1c: an empty answer would stop the test at INSTRING ("Invalid
  // input" on the Prime); it is read as "no cxq in it"
  vK := 0; IF DIM(vA) > 0 THEN vK := INSTRING(vA, "cxq"); END;
  IF vK == 0 THEN
    cxRec("T25", "SKIP", "cxq is already assigned: kept", -1);
    cxNote("  cxq+1 = " + vA);
  ELSE
    vB := cxStrip(cxCall("cxq:=7"));
    vC := cxStrip(cxCall("cxq+1"));
    vP := "(error)";
    IFERR vP := cxStrip(cxCall("purge(cxq)")); THEN vP := "(error)"; END;
    vD := cxStrip(cxCall("cxq+1"));
    IF vC == "8" THEN
      cxPersist := 1;
      cxRec("T25a", "VALUE", "CAS variables persist", -1);
    ELSE
      cxPersist := 0;
      cxRec("T25a", "VALUE", "each CAS() call starts afresh", -1);
    END;
    // beta 1c: vD is "" when that call failed (cxErr is not checked here)
    vK := 0; IF DIM(vD) > 0 THEN vK := INSTRING(vD, "cxq"); END;
    IF vK > 0 THEN
      cxRec("T25b", "PASS", "purge(cxq) clears it again", -1);
    ELSE
      cxRec("T25b", "FAIL", "cxq still set after purge", -1);
      IFERR cxCall("purge(cxq)"); THEN vP := "(error)"; END;
    END;
    cxNote("  cxq:=7 gives " + vB + ", then cxq+1 = " + vC);
    cxNote("  purge(cxq) gives " + cxShort(vP, 20) + ", then " + vD);
  END;
  // read-only look at the names a student may own
  vN := {"x", "n", "p", "k", "t"};
  vS := "";
  FOR vI FROM 1 TO 5 DO
    vR := cxStrip(cxCall(vN(vI)));
    IF cxErr == 1 THEN vR := "(error)"; END;
    vS := vS + vN(vI) + " = " + cxShort(vR, 12);
    IF vI < 5 THEN vS := vS + ", "; END;
  END;
  cxRec("T25c", "VALUE", "student names now (read only):", -1);
  cxNote("  " + vS);
END;

// T26  A syntax error inside the CAS text.  Caught by IFERR (the call
//      routine's assumption), or handed back as text?
cxT26()
BEGIN
  LOCAL rA;
  cxBegin(26, "CAS syntax error");
  rA := cxCall("comb(100,");
  IF cxErr == 1 THEN
    cxRec("T26", "PASS", "syntax error caught by IFERR", cxMs);
  ELSE
    IF INSTRING(rA, "rror") > 0 THEN
      cxRec("T26", "VALUE", "syntax error came back as text", cxMs);
    ELSE
      cxRec("T26", "VALUE", "no error was raised", cxMs);
    END;
    cxShow(rA);
  END;
END;

// T27  1/0: desktop Giac says "infinity"; the Prime may print a glyph.
cxT27()
BEGIN
  cxBegin(27, "1/0 as text");
  cxVal("T27", "1/0 as text", "1/0");
END;

// T28  0/0: undef.  The call routine rejects any answer containing "undef",
//      so the word itself must be what comes back.
cxT28()
BEGIN
  cxBegin(28, "0/0 (undef) as text");
  cxHas("T28", "0/0 contains undef", "0/0", "undef");
END;

// T29  A misspelt command comes back as itself (no error): the call
//      routine's guard cannot catch a typo, so every template is tested.
cxT29()
BEGIN
  cxBegin(29, "a misspelt command");
  cxVal("T29", "cxnosuch(3), unknown name", "cxnosuch(3)");
END;

// T30  fsolve with no root in the interval: desktop Giac gives [].
cxT30()
BEGIN
  cxBegin(30, "fsolve with no root");
  cxVal("T30", "fsolve(p^2+1=0, p=0..1)", "fsolve(cxp^2+1=0,cxp=0..1)");
END;

// T31  Home's STRING() of a number fed to the CAS.  If T01 found grouping
//      marks or a special minus, these break -- the reason for cxIStr.
cxT31()
BEGIN
  cxBegin(31, "STRING(real) fed to the CAS");
  cxEq("T31a", "STRING(123456789012)+1", STRING(123456789012) + "+1", "123456789013");
  cxVal("T31b", "STRING(-2.5)+1/2", STRING(-2.5) + "+1/2");
END;

// T32  The BRIEF_CAS guard: cxCas must give "" for undef and for an error,
//      and the probe notes that a misspelt command still gets through.
cxT32()
BEGIN
  LOCAL uA, uB, uC, uS;
  cxBegin(32, "the xxCas guard");
  uA := cxCas("0/0");
  uB := cxCas("comb(100,");
  uC := cxCas("cxnosuch(3)");
  IF uA == "" AND uB == "" THEN
    cxRec("T32", "PASS", "xxCas gives " + CHAR(34) + CHAR(34) + " for undef, error", -1);
  ELSE
    cxRec("T32", "FAIL", "xxCas let undef or an error through", -1);
  END;
  cxNote("  0/0 -> " + CHAR(34) + uA + CHAR(34) + ", comb(100, -> " + CHAR(34) + cxShort(uB, 20) + CHAR(34));
  cxNote("  cxnosuch(3) -> " + CHAR(34) + uC + CHAR(34) + " (a typo passes)");
  IF cxForm <> 1 THEN cxNote("  (xxCas uses form 1, which failed in T07)"); END;
  // T32b: did twenty-odd CAS calls change the CAS settings?  (read only)
  IF cxSet1 == "" THEN
    cxNote("  (cas_setup() gave nothing in T14: not compared)");
  ELSE
    uS := cxStrip(cxCall("cas_setup()"));
    IF uS == cxSet1 THEN
      cxRec("T32b", "PASS", "CAS settings as they were at T14", cxMs);
    ELSE
      cxRec("T32b", "FAIL", "CAS settings changed since T14", cxMs);
      cxShow(uS);
    END;
  END;
END;

// ============================================================ BETA 1a ==
// T35-T42, added in beta 1a for the programs being built on the CAS.  They
// keep new numbers so every beta 1 test keeps its own; they run after T32
// and before the Python tests.

// The bracket skeleton of a CAS answer, so a program knows how to take it
// apart: what comes before the first bracket (Giac 1.9 writes "list[...]"
// and "poly1[...]"), the brackets in order (at most 10), and whether it
// uses commas or semicolons.  "[[2,1]]" gives "[[]], commas".
cxShape(kS)
BEGIN
  LOCAL kB, kI, kC, kN, kP;
  kB := "";
  kN := 0;
  kP := 0;
  FOR kI FROM 1 TO DIM(kS) DO
    kC := MID(kS, kI, 1);
    IF kC == "[" OR kC == "]" OR kC == "{" OR kC == "}" THEN
      IF kP == 0 THEN kP := kI; END;
      IF kN < 10 THEN
        kB := kB + kC;
        kN := kN + 1;
      END;
    END;
  END;
  IF kB == "" THEN RETURN "no brackets"; END;
  IF kP > 1 THEN kB := "prefix " + LEFT(kS, kP - 1) + ", " + kB; END;
  IF INSTRING(kS, ",") > 0 THEN kB := kB + ", commas"; END;
  IF INSTRING(kS, ";") > 0 THEN kB := kB + ", semicolons"; END;
  RETURN kB;
END;

// Which multiplication sign a CAS answer uses: "*", or a dot or a times
// sign by its character code (the display rule "show * as x" assumes *).
cxMulSign(mS)
BEGIN
  IF INSTRING(mS, "*") > 0 THEN RETURN "*"; END;
  IF INSTRING(mS, CHAR(183)) > 0 THEN RETURN "middle dot (183)"; END;
  IF INSTRING(mS, CHAR(8901)) > 0 THEN RETURN "dot operator (8901)"; END;
  IF INSTRING(mS, CHAR(8729)) > 0 THEN RETURN "bullet (8729)"; END;
  IF INSTRING(mS, CHAR(215)) > 0 THEN RETURN "times sign (215)"; END;
  RETURN "none found";
END;

// Call, and PASS when the answer contains BOTH pieces (printed forms vary:
// exp(-l) or e^(-l), so the pieces are chosen to be in every likely form).
cxHas2(jId, jW, jX, jA, jB)
BEGIN
  LOCAL jR;
  jR := cxCall(jX);
  IF cxErr == 1 THEN
    cxRec(jId, "FAIL", jW + ": CAS error", cxMs);
  ELSE
    IF INSTRING(jR, jA) > 0 AND INSTRING(jR, jB) > 0 THEN
      cxRec(jId, "PASS", jW, cxMs);
    ELSE
      cxRec(jId, "FAIL", jW, cxMs);
    END;
  END;
  cxShow(jR);
  RETURN jR;
END;

// T35  Long CAS text.  A program that writes a whole formula with the
//      student's numbers (a Fisher sum, a likelihood) can send thousands
//      of characters.  The text is a sum of exact fractions,
//      1/2 + 1/6 + 1/12 + ... = sum of 1/(k(k+1)) for k = 1..N, whose
//      answer is N/(N+1) (it telescopes), built with cxIStr and grown to
//      about 1,000, 2,000, 3,000 and 6,000 characters.  It stops at the
//      first length that fails: that is the break.
cxT35()
BEGIN
  LOCAL lS, lK, lI, lR, lW, lOK;
  LOCAL lG, lL, lMax;
  cxBegin(35, "long CAS text, 1000 to 6000");
  lG := {1000, 2000, 3000, 6000};
  lL := {"T35a", "T35b", "T35c", "T35d"};
  lS := "";
  lK := 0;
  lOK := 1;
  lMax := 0;
  cxNote("  1/2+1/6+1/12+... = N/(N+1), sent whole");
  FOR lI FROM 1 TO 4 DO
    IF lOK == 1 THEN
      // grow the same text until it reaches this length
      WHILE DIM(lS) < lG(lI) DO
        lK := lK + 1;
        IF lK > 1 THEN lS := lS + "+"; END;
        lS := lS + "1/" + cxIStr(lK * (lK + 1));
      END;
      lW := cxIStr(lK) + "/" + cxIStr(lK + 1);
      lR := cxStrip(cxCall(lS));
      IF cxErr == 0 AND lR == lW THEN
        cxRec(lL(lI), "PASS", cxIStr(DIM(lS)) + " chars, N = " + cxIStr(lK) + ": " + lW, cxMs);
        lMax := DIM(lS);
      ELSE
        lOK := 0;
        IF cxErr == 1 THEN
          cxRec(lL(lI), "FAIL", cxIStr(DIM(lS)) + " chars: CAS error", cxMs);
        ELSE
          cxRec(lL(lI), "FAIL", cxIStr(DIM(lS)) + " chars: not " + lW, cxMs);
          cxShow(lR);
        END;
      END;
    END;
  END;
  IF lOK == 1 THEN
    cxNote("  no break up to " + cxIStr(lMax) + " characters");
  ELSE
    cxNote("  works to " + cxIStr(lMax) + " characters; the FAIL is the break");
  END;
  cxLong := lMax;
END;

// T36  Two equations at once.  What does the answer LOOK like -- a list of
//      lists, braces, semicolons, a "list" prefix?  A program that solves
//      the normal equations must take it apart.  Then least squares
//      through (0,2), (1,3), (2,5), (3,4): the normal equations are built
//      from the data with cxIStr, as a program would, and give the
//      intercept a = 23/10 and the slope b = 4/5 (numpy.polyfit agrees).
cxT36()
BEGIN
  LOCAL qA, qX, qY, qI, qN, qSx;
  LOCAL qSy, qSxx, qSxy, qT;
  cxBegin(36, "solve two equations");
  qA := cxCall("solve([cxa+cxb=3,cxa-cxb=1],[cxa,cxb])");
  IF cxErr == 1 THEN
    cxRec("T36a", "FAIL", "a+b = 3, a-b = 1: CAS error", cxMs);
  ELSE
    IF INSTRING(qA, "2,1") > 0 THEN
      cxRec("T36a", "PASS", "a+b = 3, a-b = 1: a = 2, b = 1", cxMs);
    ELSE
      cxRec("T36a", "VALUE", "a+b = 3, a-b = 1: see the text", cxMs);
    END;
    cxShow(qA);
    cxNote("  shape: " + cxShape(cxStrip(qA)));
  END;
  qX := {0, 1, 2, 3};
  qY := {2, 3, 5, 4};
  qN := 4;
  qSx := 0;  qSy := 0;  qSxx := 0;  qSxy := 0;
  FOR qI FROM 1 TO 4 DO
    qSx := qSx + qX(qI);
    qSy := qSy + qY(qI);
    qSxx := qSxx + qX(qI) * qX(qI);
    qSxy := qSxy + qX(qI) * qY(qI);
  END;
  qT := "solve([" + cxIStr(qN) + "*cxa+" + cxIStr(qSx) + "*cxb=" + cxIStr(qSy) + "," + cxIStr(qSx) + "*cxa+" + cxIStr(qSxx) + "*cxb=" + cxIStr(qSxy) + "],[cxa,cxb])";
  cxNote("  " + qT);
  qA := cxHas2("T36b", "normal equations: 23/10 and 4/5", qT, "23/10", "4/5");
  IF cxErr == 0 THEN cxNote("  shape: " + cxShape(cxStrip(qA))); END;
END;

// T37  normal() and factor() on expressions in tagged symbols: the shape
//      of the text a "where the formula comes from" page will show.  The
//      printed forms vary, so the PASS for normal() is its value at
//      t = 1, p = 3: (4)(5)/(2(3-1-3)) = -10.
cxT37()
BEGIN
  LOCAL rEx;
  cxBegin(37, "normal and factor, the shape");
  rEx := "(3*cxt+1)*(4*cxt+1)/(2*(cxt*cxp-cxt-cxp))";
  cxVal("T37a", "normal of a rational expression", "normal(" + rEx + ")");
  cxEq("T37b", "its value at t = 1, p = 3 is -10", "subst(subst(normal(" + rEx + "),cxt=1),cxp=3)", "-10");
  cxHas2("T37c", "factor(6t^2+7t+2) = (2t+1)(3t+2)", "factor(6*cxt^2+7*cxt+2)", "2*cxt+1", "3*cxt+2");
END;

// T38  The text of products and of lists.  The fence's display rule
//      ("show * as x") assumes products print with *; a program that
//      reads a CAS list must know its brackets.
cxT38()
BEGIN
  LOCAL mA, mB;
  cxBegin(38, "the times sign, list brackets");
  mA := cxCall("expand((cxt+2)*(cxp+3))");
  IF cxErr == 1 THEN
    cxRec("T38a", "FAIL", "expand((t+2)(p+3)): CAS error", cxMs);
  ELSE
    IF cxMulSign(mA) == "*" THEN
      cxRec("T38a", "PASS", "products print with *", cxMs);
    ELSE
      cxRec("T38a", "VALUE", "products print with " + cxMulSign(mA), cxMs);
    END;
    cxShow(mA);
  END;
  mB := cxVal("T38b", "seq(j^2, j, 1, 4): a list", "seq(cxj^2,cxj,1,4)");
  cxNote("  shape: " + cxShape(cxStrip(mB)));
  mB := cxVal("T38c", "solve(t^2 = 4): two roots", "solve(cxt^2=4,cxt)");
  cxNote("  shape: " + cxShape(cxStrip(mB)));
  mB := cxVal("T38d", "a 2 x 2 matrix times 2", "[[1,2],[3,4]]*2");
  cxNote("  shape: " + cxShape(cxStrip(mB)));
END;

// T39  series and convert(..., polynom), as ErrorRate Lab's trawl page
//      sends them: the series of 1 - exp(-t) to order 3 carries a remainder
//      term (desktop Giac: cxt^4*order_size(cxt)); convert(..., polynom)
//      should drop it, leaving t - t^2/2 + t^3/6.
cxT39()
BEGIN
  LOCAL sA;
  cxBegin(39, "series and convert(polynom)");
  sA := cxVal("T39a", "series(1-exp(-t), t=0, 3)", "series(1-exp(-cxt),cxt=0,3)");
  // beta 1c: cxVal hands back "" when the call failed; INSTRING("") would stop
  IF DIM(sA) > 0 THEN
    IF INSTRING(sA, "order") > 0 THEN cxNote("  (the remainder is written order_size)"); END;
    IF INSTRING(sA, "O(") > 0 THEN cxNote("  (the remainder is written O(...))"); END;
  END;
  sA := cxCall("convert(series(1-exp(-cxt),cxt=0,3),polynom)");
  IF cxErr == 1 THEN
    cxRec("T39b", "FAIL", "convert(..., polynom): CAS error", cxMs);
  ELSE
    IF INSTRING(sA, "cxt^3") > 0 AND INSTRING(sA, "order") == 0 AND INSTRING(sA, "O(") == 0 THEN
      cxRec("T39b", "PASS", "convert(polynom) drops the remainder", cxMs);
    ELSE
      cxRec("T39b", "FAIL", "convert(polynom): not a plain cubic", cxMs);
    END;
  END;
  cxShow(sA);
END;

// T40  The binomial-to-Poisson limits ErrorRate Lab sends (its
//      erlPoiTerms), each timed: the k = 2 term written with comb(n,2) and
//      with ErrorRate's falling factorial n(n-1)/2 (both -> l^2 e^(-l)/2),
//      then ErrorRate's whole k <= 2 sum inside factor(limit(...)), which
//      desktop Giac gives as (l^2+2l+2) e^(-l)/2.
cxT40()
BEGIN
  LOCAL fTerms;
  cxBegin(40, "binomial-to-Poisson limits");
  cxHas2("T40a", "comb(n,2) form -> l^2 e^(-l)/2", "limit(comb(cxn,2)*(cxl/cxn)^2*(1-cxl/cxn)^(cxn-2),cxn,inf)", "cxl^2", "-cxl");
  cxHas2("T40b", "n(n-1)/2 form -> l^2 e^(-l)/2", "limit(cxn*(cxn-1)/2*(cxl/cxn)^2*(1-cxl/cxn)^(cxn-2),cxn,inf)", "cxl^2", "-cxl");
  fTerms := "(1-cxl/cxn)^cxn+cxn/1*(cxl/cxn)^1*(1-cxl/cxn)^(cxn-1)+cxn*(cxn-1)/2*(cxl/cxn)^2*(1-cxl/cxn)^(cxn-2)";
  cxHas2("T40c", "factor(limit(k <= 2 sum))", "factor(limit(" + fTerms + ",cxn,inf))", "cxl^2", "-cxl");
END;

// T41  How the ANSWER of an infinite limit prints.  T16 settles how the
//      INPUT must be spelt (inf, or infinity in T16b); this is the output,
//      which a program may test for or show.  Desktop Giac: +infinity.
//      Then ErrorRate's trawl limit 1 - (1 - l/n)^n -> 1 - exp(-l).
cxT41()
BEGIN
  LOCAL iA, iW;
  cxBegin(41, "how an infinite limit prints");
  iA := cxCall("limit(cxn^2,cxn,inf)");
  IF cxErr == 1 THEN
    cxRec("T41a", "FAIL", "limit(n^2, n, inf): CAS error", cxMs);
  ELSE
    iW := "other text";
    IF INSTRING(iA, "inf") > 0 THEN iW := "inf"; END;
    IF INSTRING(iA, "infinity") > 0 THEN iW := "infinity"; END;
    IF INSTRING(iA, CHAR(8734)) > 0 THEN iW := "the glyph (8734)"; END;
    cxInfTxt := iW;
    cxRec("T41a", "VALUE", "limit(n^2, n, inf) prints as " + iW, cxMs);
    cxShow(iA);
  END;
  cxHas("T41b", "limit 1-(1-l/n)^n = 1-exp(-l)", "limit(1-(1-cxl/cxn)^cxn,cxn,inf)", "-cxl");
END;

// T42  No CAS: BINOMIAL_CDF's argument order.  GfxProbe T20 checks
//      BINOMIAL's; the fleet (ErrorRate's bisection, StatDist) calls
//      BINOMIAL_CDF(n, p, k).  SciPy: binom.cdf(2, 30, 0.2) = 0.044179.
cxT42()
BEGIN
  LOCAL bV;
  cxBegin(42, "BINOMIAL_CDF(30, 0.2, 2) order");
  bV := -1;
  IFERR
    bV := BINOMIAL_CDF(30, 0.2, 2);
  THEN
    bV := -1;
  END;
  cxBcdf := 0;
  IF bV < 0 THEN
    cxRec("T42", "FAIL", "BINOMIAL_CDF(30,0.2,2): run-time error", -1);
  ELSE
    IF ABS(bV - 0.044179) < 0.000001 THEN
      cxBcdf := 1;
      cxRec("T42", "PASS", "BINOMIAL_CDF takes (n, p, k)", -1);
    ELSE
      cxRec("T42", "FAIL", "BINOMIAL_CDF(30,0.2,2) is not 0.044179", -1);
    END;
    cxNote("  = " + STRING(bV, 2, 6) + " (SciPy 0.044179)");
  END;
END;

// T43  (beta 1c, no CAS)  Two string rules emulator round 2 found by hand in
//      Home (17 Oct 2026), recorded here so a G1 / G2 run confirms them:
//      a  INSTRING("", "a"), inside IFERR: the emulator said "Error: Invalid
//         input".  PASS = an error, as there.  (The fleet now tests DIM(s) > 0
//         before every INSTRING whose text can be empty.)
//      b  STRING(0.096, 2, 1): the emulator prints it in SCIENTIFIC form,
//         codes 57 46 54 7431 8722 50 (beta 1c's run, 17 Oct 2026): the
//         small capital E, CHAR(7431), and Home's minus, CHAR(8722), before
//         the exponent.  PASS = CHAR(7431) is in the text, whatever the sign
//         character; a note line says which minus the exponent used
//         (CHAR(8722), ASCII 45 or none).  (Beta 1c wanted exactly
//         "9.6" + CHAR(7431) + "-2", with an ASCII hyphen, and so FAILed on
//         the emulator: fixed in beta 1d.)  Fixed decimals turn scientific
//         whenever 0 < |x| < 10^-d.
//      c  STRING(ROUND(0.096, 1), 2, 1), the fleet's cure: PASS = "0.1".
cxT43()
BEGIN
  LOCAL sxA, sxB, sxC, sxE, sxM;
  cxBegin(43, "empty INSTRING, small STRING");
  sxA := -1;
  sxE := 0;
  IFERR
    sxA := INSTRING("", "a");
  THEN
    sxE := 1;
  END;
  cxIns0 := sxE;
  IF sxE == 1 THEN
    cxRec("T43a", "PASS", "INSTRING of an empty string is an error", -1);
  ELSE
    cxRec("T43a", "FAIL", "INSTRING of an empty string gave " + cxNum(sxA), -1);
  END;
  sxB := STRING(0.096, 2, 1);
  // beta 1d: scientific = the small capital E is there, whatever minus
  // sign the exponent carries (DIM first: INSTRING("") is an error)
  cxSci := 0;
  IF DIM(sxB) > 0 THEN
    IF INSTRING(sxB, CHAR(7431)) > 0 THEN cxSci := 1; END;
  END;
  IF cxSci == 1 THEN
    cxRec("T43b", "PASS", "STRING(0.096, 2, 1) is scientific", -1);
  ELSE
    cxRec("T43b", "FAIL", "STRING(0.096, 2, 1) is not scientific", -1);
  END;
  cxNote("  = " + sxB + ", codes " + cxAllCodes(sxB));
  // beta 1d: which minus the exponent used (the emulator: CHAR(8722))
  IF cxSci == 1 THEN
    sxM := "none";
    IF INSTRING(sxB, CHAR(7431) + "-") > 0 THEN sxM := "ASCII 45, a hyphen"; END;
    IF INSTRING(sxB, CHAR(7431) + CHAR(8722)) > 0 THEN sxM := "CHAR(8722), Home's minus"; END;
    cxNote("  exponent minus: " + sxM);
  END;
  sxC := STRING(ROUND(0.096, 1), 2, 1);
  IF sxC == "0.1" THEN
    cxRec("T43c", "PASS", "STRING(ROUND(0.096, 1), 2, 1) = 0.1", -1);
  ELSE
    cxRec("T43c", "FAIL", "STRING(ROUND(0.096, 1), 2, 1) is not 0.1", -1);
  END;
  cxNote("  = " + sxC);
END;

// ============================================================== PYTHON ==
// Route A (Lessons Learned s8): the Python lives in this file and runs with
// PYTHON(name).  The bridge (s9): PPL writes CX_CS; Python reads it with
// hpprime.eval and writes its answer into CX_PYS as text, fields separated
// by ";".  Python leaves progress marks in CX_PYM -- a PPL global survives
// even when the Python side dies silently.  The PPL side keeps the three
// guards: (1) clear CX_PYS and CX_PYM first; (2) validate by USING the
// answer (split it, compare every field); (3) when no answer comes, log
// "no answer" and the last mark, and carry on.
// Everything imported inside main() or a try, never a risky top-level
// import (s9: a failed top-level import closes the app with no message).

#PYTHON cxPyBig
# CASProbe beta 1b, test T33.  Ran on the emulator (beta 1a): PASS.
# Are MicroPython's integers exact at any size?  (CPython's are; a
# MicroPython build may use 64-bit "long long" integers instead, and then
# 2**100 raises OverflowError.)  And can the CAS text that PPL got be read
# here and turned into an exact integer?
# Answer, written to CX_PYS:   2**100 ; comb(100,50) ; same|differ|noread
from hpprime import eval as ev


def mark(t):
    # Leave a progress note in the PPL global CX_PYM.  It survives a crash.
    try:
        ev('CX_PYM:="' + str(t).replace('"', "'") + '"')
    except Exception:
        pass


def clean(x):
    # No double quotes or backslashes inside a PPL string; keep it short.
    return str(x).replace('"', "'").replace(chr(92), "/").replace(chr(10), " ")[:120]


def main():
    mark("T33 started")
    try:
        s1 = str(2 ** 100)
    except Exception as e:
        s1 = "ERR " + clean(e)
    mark("T33 2**100 done")
    # C(100,50) by exact integer steps: C(50+i, i) = C(49+i, i-1)(50+i)/i
    try:
        c = 1
        for i in range(1, 51):
            c = c * (50 + i) // i
        s2 = str(c)
    except Exception as e:
        c = -1
        s2 = "ERR " + clean(e)
    mark("T33 comb done")
    # the CAS's own text, handed over by PPL in CX_CS
    v = "noread"
    try:
        t = str(ev("CX_CS")).replace('"', "").strip()
        if int(t) == c:
            v = "same"
        else:
            v = "differ"
    except Exception:
        v = "noread"
    mark("T33 CX_CS read: " + v)
    ev('CX_PYS:="' + s1 + ';' + s2 + ';' + v + '"')
    mark("T33 done")


main()
#END

#PYTHON cxPyCas
# CASProbe beta 1b, test T34.  Ran on the emulator (beta 1a) without a crash.  RISKY: Tetravex's
# author reports cas.caseval crashing the calculator; the PPL side shows a
# warning page first and can skip this test.
# Markowski's page (Community_Library/docs/markowski_udel) gives the form
#     import cas
#     cas.caseval(text)
# Answer, written to CX_PYS:  type ; value ; ms ; type ; value ; ms
# for caseval("comb(100,50)") and caseval("comb(8,4)/2^8").
from hpprime import eval as ev


def mark(t):
    # Leave a progress note in the PPL global CX_PYM.  It survives a crash.
    try:
        ev('CX_PYM:="' + str(t).replace('"', "'") + '"')
    except Exception:
        pass


def clean(x):
    # No double quotes or backslashes inside a PPL string; keep it short.
    return str(x).replace('"', "'").replace(chr(92), "/").replace(chr(10), " ")[:120]


def now():
    # Milliseconds from hpprime's ticks, which worked in PyProbe beta 1;
    # PPL's TICKS through the bridge if that import fails.
    try:
        from hpprime import ticks
        tk = ticks()
        return int(tk)
    except Exception:
        try:
            return int(float(ev("TICKS")))
        except Exception:
            return 0


def main():
    mark("T34 started")
    try:
        import cas
    except Exception as e:
        ev('CX_PYS:="noimport;' + clean(e) + '"')
        mark("T34 import cas failed")
        return
    mark("T34 import ok, calling caseval")
    t0 = now()
    r = cas.caseval("comb(100,50)")
    t1 = now()
    mark("T34 first caseval returned")
    out = clean(type(r)) + ";" + clean(r) + ";" + str(t1 - t0)
    ev('CX_PYS:="' + out + '"')
    t0 = now()
    r2 = cas.caseval("comb(8,4)/2^8")
    t1 = now()
    mark("T34 second caseval returned")
    out = out + ";" + clean(type(r2)) + ";" + clean(r2) + ";" + str(t1 - t0)
    ev('CX_PYS:="' + out + '"')
    mark("T34 done")


main()
#END

// The class name out of Python's "<class 'str'>": the text between the
// first two single quotes, or the whole text.
// Beta 1c: an empty field, or a quote at the very end, left "" for INSTRING
// ("Invalid input" on the Prime); two quotes in a row took LEFT(s, 0), the
// WHOLE string.
cxPyType(yS)
BEGIN
  LOCAL yA, yR;
  IF DIM(yS) == 0 THEN RETURN yS; END;
  yA := INSTRING(yS, "'");
  IF yA == 0 THEN RETURN yS; END;
  yR := cxRest(yS, yA + 1);
  IF DIM(yR) == 0 THEN RETURN yR; END;
  yA := INSTRING(yR, "'");
  IF yA == 0 THEN RETURN yR; END;
  IF yA == 1 THEN RETURN ""; END;
  RETURN LEFT(yR, yA - 1);
END;

// T33  MicroPython's integers and the bridge (lower risk: hpprime.eval is
//      the fleet's bridge; only big integers are new).
cxT33()
BEGIN
  LOCAL pT, pL, pE, pA, pB, pC;
  cxBegin(33, "Python: 2**100 and CAS text");
  // guard 1: clear the answer and the mark, so nothing stale survives
  CX_PYS := "";
  CX_PYM := "";
  CX_CS := cxCasTxt;
  IF CX_CS == "" THEN
    CX_CS := cxC50;
    cxNote("  (CX_CS is the constant: no CAS text)");
  END;
  pE := 0;
  pT := TICKS;
  IFERR
    PYTHON(cxPyBig);
  THEN
    pE := 1;          // (an error INSIDE Python never reaches here)
  END;
  pT := TICKS - pT;
  // guard 2: validate by USING the answer
  pL := {};
  IFERR pL := cxSplit(CX_PYS, ";"); THEN pL := {}; END;
  IF SIZE(pL) < 3 THEN
    // guard 3: no answer -- say so, with the last mark, and carry on
    cxRec("T33", "FAIL", "Python gave no answer", pT);
    cxNote("  last mark: " + cxNone(CX_PYM));
    cxPyA := "no answer";
    RETURN;
  END;
  pA := pL(1);  pB := pL(2);  pC := pL(3);
  IF pA == "1267650600228229401496703205376" THEN
    cxRec("T33a", "PASS", "2**100 exact, 31 digits", pT);
    cxPyA := "exact (2**100 ok)";
  ELSE
    cxRec("T33a", "FAIL", "2**100 is not exact", pT);
    cxPyA := "not exact";
  END;
  cxNote("  2**100 = " + pA);
  IF pB == cxC50 THEN
    cxRec("T33b", "PASS", "Python's comb(100,50) exact", -1);
  ELSE
    cxRec("T33b", "FAIL", "Python's comb(100,50) wrong", -1);
    cxNote("  = " + pB);
  END;
  IF pC == "same" THEN
    cxRec("T33c", "PASS", "CAS text read by Python: same", -1);
  ELSE
    cxRec("T33c", "FAIL", "CAS text read by Python: " + pC, -1);
  END;
  cxNote("  last mark: " + cxNone(CX_PYM));
END;

// The warning before T34, with two buttons: returns 1 = run it, 0 = skip.
cxWarnPy()
BEGIN
  LOCAL wK, wGo;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 0, 0, 319, 18, cxORNG);
  TEXTOUT_P("T34 next: cas.caseval from Python", G1, 4, 2, 2, #FFFFFFh);
  RECT_P(G1, 6, 26, 313, 126, cxRED, cxLRED);
  TEXTOUT_P("Next test calls cas.caseval from", G1, 14, 32, 2, cxRED);
  TEXTOUT_P("Python; one author reported a crash.", G1, 14, 50, 2, cxRED);
  TEXTOUT_P("If the calculator resets, rerun", G1, 14, 68, 2, #000000h);
  TEXTOUT_P("CASProbe and choose 'Skip Python CAS'.", G1, 14, 86, 2, #000000h);
  TEXTOUT_P("(After a reset, type CX_LAST in Home first.)", G1, 14, 108, 1, cxGRAY);
  RECT_P(G1, 20, 140, 150, 190, cxGRN, cxLGRN);
  TEXTOUT_P("Run it", G1, 56, 156, 3, cxGRN);
  RECT_P(G1, 170, 140, 300, 190, cxRED, cxLRED);
  TEXTOUT_P("Skip it", G1, 203, 156, 3, cxRED);
  RECT_P(G1, 0, 206, 319, 239, cxLBLU);
  TEXTOUT_P("+ = run it, - = skip, Esc = skip, or tap", G1, 4, 216, 1, cxFOOT);
  BLIT_P(G0, 0, 0, G1);
  wGo := -1;
  REPEAT
    wK := cxWaitAny();
    IF wK == 50 THEN wGo := 1; END;
    IF wK == 45 OR wK == 4 THEN wGo := 0; END;
    IF wK == -1 THEN
      IF cxTY >= 140 AND cxTY <= 190 THEN
        IF cxTX >= 20 AND cxTX <= 150 THEN wGo := 1; END;
        IF cxTX >= 170 AND cxTX <= 300 THEN wGo := 0; END;
      END;
    END;
  UNTIL wGo >= 0;
  RETURN wGo;
END;

// T34  cas.caseval from MicroPython -- the riskiest test, so the last one.
cxT34()
BEGIN
  LOCAL pT, pL, pE, pV, pW, pK;
  cxBegin(34, "Python cas.caseval");
  IF cxWarnPy() == 0 THEN
    cxRec("T34", "SKIP", "cas.caseval skipped at the warning", -1);
    cxPyB := "skipped";
    RETURN;
  END;
  cxBegin(34, "Python cas.caseval");
  CX_LAST := "T34 Python cas.caseval: if you read this after a reset, caseval crashed";
  CX_PYS := "";
  CX_PYM := "";
  pE := 0;
  pT := TICKS;
  IFERR
    PYTHON(cxPyCas);
  THEN
    pE := 1;          // (an error INSIDE Python never reaches here)
  END;
  pT := TICKS - pT;
  CX_LAST := "T34 returned";
  pL := {};
  IFERR pL := cxSplit(CX_PYS, ";"); THEN pL := {}; END;
  IF SIZE(pL) >= 1 THEN
    IF pL(1) == "noimport" THEN
      cxRec("T34", "FAIL", "import cas failed", pT);
      IF SIZE(pL) >= 2 THEN cxNote("  " + pL(2)); END;
      cxPyB := "import cas failed";
      RETURN;
    END;
  END;
  IF SIZE(pL) < 3 THEN
    cxRec("T34", "FAIL", "caseval gave no answer", pT);
    cxNote("  last mark: " + cxNone(CX_PYM));
    cxPyB := "no answer (crash?)";
    RETURN;
  END;
  pV := cxStrip(pL(2));
  pW := cxPyType(pL(1));
  IF pV == cxC50 THEN
    cxRec("T34a", "PASS", "caseval comb(100,50): 30 digits", pT);
    cxPyB := pW + ", 30 digits, " + pL(3) + " ms";
  ELSE
    cxRec("T34a", "FAIL", "caseval comb(100,50): not 30 digits", pT);
    cxPyB := pW + ", wrong text";
  END;
  cxNote("  type " + pL(1) + ", " + pL(3) + " ms in Python");
  cxNote("  = " + cxShort(pL(2), 40));
  IF SIZE(pL) >= 6 THEN
    // beta 1c: an empty field would stop the test at INSTRING ("Invalid input")
    pK := 0; IF DIM(pL(5)) > 0 THEN pK := INSTRING(pL(5), "35/128"); END;
    IF pK > 0 THEN
      cxRec("T34b", "PASS", "caseval comb(8,4)/2^8 = 35/128", -1);
    ELSE
      cxRec("T34b", "FAIL", "caseval comb(8,4)/2^8", -1);
    END;
    cxNote("  type " + pL(4) + ", = " + cxShort(pL(5), 20) + ", " + pL(6) + " ms");
  ELSE
    cxRec("T34b", "FAIL", "second caseval gave no answer", -1);
  END;
  cxNote("  last mark: " + cxNone(CX_PYM));
END;

// ============================================================ RUNNING ==

// Run test n (T01-T32, T35-T42).  The caller wraps this in IFERR.
cxRun(rN)
BEGIN
  IF rN == 1 THEN cxT01(); END;
  IF rN == 2 THEN cxT02(); END;
  IF rN == 3 THEN cxT03(); END;
  IF rN == 4 THEN cxT04(); END;
  IF rN == 5 THEN cxT05(); END;
  IF rN == 6 THEN cxT06(); END;
  IF rN == 7 THEN cxT07(); END;
  IF rN == 8 THEN cxT08(); END;
  IF rN == 9 THEN cxT09(); END;
  IF rN == 10 THEN cxT10(); END;
  IF rN == 11 THEN cxT11(); END;
  IF rN == 12 THEN cxT12(); END;
  IF rN == 13 THEN cxT13(); END;
  IF rN == 14 THEN cxT14(); END;
  IF rN == 15 THEN cxT15(); END;
  IF rN == 16 THEN cxT16(); END;
  IF rN == 17 THEN cxT17(); END;
  IF rN == 18 THEN cxT18(); END;
  IF rN == 19 THEN cxT19(); END;
  IF rN == 20 THEN cxT20(); END;
  IF rN == 21 THEN cxT21(); END;
  IF rN == 22 THEN cxT22(); END;
  IF rN == 23 THEN cxT23(); END;
  IF rN == 24 THEN cxT24(); END;
  IF rN == 25 THEN cxT25(); END;
  IF rN == 26 THEN cxT26(); END;
  IF rN == 27 THEN cxT27(); END;
  IF rN == 28 THEN cxT28(); END;
  IF rN == 29 THEN cxT29(); END;
  IF rN == 30 THEN cxT30(); END;
  IF rN == 31 THEN cxT31(); END;
  IF rN == 32 THEN cxT32(); END;
  IF rN == 35 THEN cxT35(); END;
  IF rN == 36 THEN cxT36(); END;
  IF rN == 37 THEN cxT37(); END;
  IF rN == 38 THEN cxT38(); END;
  IF rN == 39 THEN cxT39(); END;
  IF rN == 40 THEN cxT40(); END;
  IF rN == 41 THEN cxT41(); END;
  IF rN == 42 THEN cxT42(); END;
  IF rN == 43 THEN cxT43(); END;
END;

// ------------------------------------------------------ summary page --
// What to photograph: the call form, whether the CAS is usable, the
// slowest call, the tallies, and the four yes/no answers.
cxSummary()
BEGIN
  LOCAL mK, mS;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 0, 0, 319, 18, cxORNG);
  TEXTOUT_P("CASProbe beta 1d: summary", G1, 4, 2, 2, #FFFFFFh);
  IF cxAvail == 1 THEN
    TEXTOUT_P("CAS available: yes", G1, 8, 24, 2, cxGRN);
  ELSE
    TEXTOUT_P("CAS available: no", G1, 8, 24, 2, cxRED);
  END;
  TEXTOUT_P("Call form to use:", G1, 8, 42, 2, #000000h);
  TEXTOUT_P(cxFormTxt, G1, 16, 60, 2, cxMAG);
  TEXTOUT_P("slowest call: " + cxIStr(cxSlowMs) + " ms", G1, 8, 80, 2, cxORNG);
  TEXTOUT_P("in " + cxShort(cxSlowId, 44), G1, 16, 98, 1, cxGRAY);
  mS := "pass " + cxIStr(cxNP) + ", fail " + cxIStr(cxNF) + ", value " + cxIStr(cxNV);
  mS := mS + ", error " + cxIStr(cxNE) + ", skip " + cxIStr(cxNS);
  TEXTOUT_P(mS, G1, 8, 114, 1, cxBLUE);
  IF cxHomeOK == 1 THEN
    TEXTOUT_P("Home STRING(real): plain ASCII", G1, 8, 130, 2, #000000h);
  ELSE
    TEXTOUT_P("Home STRING(real): NOT plain", G1, 8, 130, 2, cxRED);
  END;
  IF cxPersist == 1 THEN TEXTOUT_P("CAS variables persist: yes", G1, 8, 148, 2, #000000h); END;
  IF cxPersist == 0 THEN TEXTOUT_P("CAS variables persist: no", G1, 8, 148, 2, #000000h); END;
  IF cxPersist < 0 THEN TEXTOUT_P("CAS variables persist: not tested", G1, 8, 148, 2, cxGRAY); END;
  TEXTOUT_P("Python ints: " + cxShort(cxPyA, 26), G1, 8, 166, 2, #000000h);
  TEXTOUT_P("caseval: " + cxShort(cxPyB, 30), G1, 8, 184, 2, #000000h);
  RECT_P(G1, 0, 206, 319, 239, cxLBLU);
  TEXTOUT_P("Photograph this page.  CX_SUM, CX_LOG in Home.", G1, 4, 209, 1, cxGRAY);
  TEXTOUT_P("any key or tap = the log pages", G1, 4, 224, 1, cxFOOT);
  BLIT_P(G0, 0, 0, G1);
  mK := cxWaitAny();
END;

// The colour of a log line on the pager: by its verdict.
cxLineCol(lS)
BEGIN
  LOCAL lP;
  // beta 1c: an empty line (CX_LOG is a Home variable) would stop the pager
  // at INSTRING ("Invalid input" on the Prime)
  IF DIM(lS) == 0 THEN RETURN #000000h; END;
  lP := INSTRING(lS, " PASS ");
  IF lP >= 4 AND lP <= 5 THEN RETURN cxGRN; END;
  lP := INSTRING(lS, " FAIL ");
  IF lP >= 4 AND lP <= 5 THEN RETURN cxRED; END;
  lP := INSTRING(lS, " ERROR ");
  IF lP >= 4 AND lP <= 5 THEN RETURN cxRED; END;
  lP := INSTRING(lS, " VALUE ");
  IF lP >= 4 AND lP <= 5 THEN RETURN cxBLUE; END;
  lP := INSTRING(lS, " SKIP ");
  IF lP >= 4 AND lP <= 5 THEN RETURN cxGRAY; END;
  IF LEFT(lS, 1) == " " THEN RETURN #000000h; END;
  RETURN cxMAG;
END;

// ------------------------------------------------------- results pager --
// CX_LOG, 13 lines a page, coloured by verdict.  Up/Down = a line,
// Left/Right = a page, a tap on the top half = back a page, the bottom half
// = on a page, Esc or Enter = done.  Every wait goes through cxWaitAny.
cxView()
BEGIN
  LOCAL vN, vTop, vPg, vK, vI, vRun;
  LOCAL vLast, vL;
  vN := SIZE(CX_LOG);
  vPg := 13;
  vTop := 1;
  vRun := 1;
  WHILE vRun == 1 DO
    DIMGROB_P(G1, 320, 240, #FFFFFFh);
    RECT_P(G1, 0, 0, 319, 18, cxORNG);
    TEXTOUT_P("CASProbe log (CX_LOG in Home)", G1, 4, 2, 2, #FFFFFFh);
    FOR vI FROM 0 TO vPg - 1 DO
      IF vTop + vI <= vN THEN
        vL := CX_LOG(vTop + vI);
        TEXTOUT_P(vL, G1, 4, 22 + 14*vI, 1, cxLineCol(vL));
      END;
    END;
    RECT_P(G1, 0, 206, 319, 239, cxLBLU);
    vLast := MIN(vTop + vPg - 1, vN);
    TEXTOUT_P("lines " + cxIStr(vTop) + "-" + cxIStr(vLast) + " of " + cxIStr(vN), G1, 4, 209, 1, cxGRAY);
    TEXTOUT_P("Up/Dn = line, L/R = page, tap = page, Esc = done", G1, 4, 224, 1, cxFOOT);
    BLIT_P(G0, 0, 0, G1);
    vK := cxWaitAny();
    IF vK == -1 THEN
      IF cxTY < 120 THEN vTop := vTop - vPg; ELSE vTop := vTop + vPg; END;
    END;
    IF vK == 2 THEN vTop := vTop - 1; END;
    IF vK == 12 THEN vTop := vTop + 1; END;
    IF vK == 7 THEN vTop := vTop - vPg; END;
    IF vK == 8 THEN vTop := vTop + vPg; END;
    IF vK == 4 OR vK == 30 THEN vRun := 0; END;
    vTop := MIN(vTop, MAX(1, vN - vPg + 1));
    vTop := MAX(vTop, 1);
  END;
END;

// --------------------------------------------------------------- the run --
EXPORT CASProbe()
BEGIN
  LOCAL mSel, mPrev, mN;
  cxInit();
  // the crash marker from the last run, read before it is overwritten
  mPrev := "";
  IFERR
    IF TYPE(CX_LAST) == 2 THEN mPrev := CX_LAST; END;
  THEN
    mPrev := "";
  END;
  mSel := 0;
  CHOOSE(mSel, "CASProbe beta 1d", {"Run every test", "Skip Python CAS", "Skip all Python", "Show last log"});
  IF mSel == 0 THEN RETURN "CASProbe: cancelled"; END;
  IF mSel == 4 THEN
    IF SIZE(CX_LOG) > 0 THEN cxView(); END;
    RETURN "CASProbe: " + cxIStr(SIZE(CX_LOG)) + " lines in CX_LOG";
  END;
  cxPyMode := 2;                          // 2 = both Python tests
  IF mSel == 2 THEN cxPyMode := 1; END;   // 1 = T33 only
  IF mSel == 3 THEN cxPyMode := 0; END;   // 0 = no Python
  CX_LOG := {};
  cxNote("CASProbe beta 1d, 17 Oct 2026 build");
  IF mSel == 1 THEN cxNote("menu: Run every test"); END;
  IF mSel == 2 THEN cxNote("menu: Skip Python CAS"); END;
  IF mSel == 3 THEN cxNote("menu: Skip all Python"); END;
  IF mPrev <> "" AND mPrev <> "done" THEN
    cxNote("previous run stopped in: " + mPrev);
    MSGBOX("The last run stopped inside:" + CHAR(10) + cxShort(mPrev, 40) + CHAR(10) + "That is a finding: it is in the new log.");
  END;
  // T01-T10: Home text, the digit routine, and the call forms
  FOR mN FROM 1 TO 10 DO
    IFERR cxRun(mN); THEN cxRec(cxId(mN), "ERROR", cxWhat + ": test stopped", -1); END;
  END;
  cxPickForm();
  // T11-T32 through the chosen form; skipped if no form works
  IF cxAvail == 1 THEN
    FOR mN FROM 11 TO 32 DO
      IFERR cxRun(mN); THEN cxRec(cxId(mN), "ERROR", cxWhat + ": test stopped", -1); END;
    END;
  ELSE
    cxCurN := 11;
    cxRec("T11", "SKIP", "T11-T32, T35-T41: no call form works", -1);
  END;
  // T35-T42, added in beta 1a: T35-T41 through the chosen form (skipped
  // with the rest if none works); T42 has no CAS, so it always runs
  cxNote("-- T35-T42 (added in beta 1a) --");
  IF cxAvail == 1 THEN
    FOR mN FROM 35 TO 41 DO
      IFERR cxRun(mN); THEN cxRec(cxId(mN), "ERROR", cxWhat + ": test stopped", -1); END;
    END;
  END;
  IFERR cxRun(42); THEN cxRec("T42", "ERROR", cxWhat + ": test stopped", -1); END;
  // T43, added in beta 1c: no CAS, so it always runs
  cxNote("-- T43 (added in beta 1c) --");
  IFERR cxRun(43); THEN cxRec("T43", "ERROR", cxWhat + ": test stopped", -1); END;
  // the Python tests, riskiest last
  IF cxPyMode >= 1 THEN
    IFERR cxT33(); THEN cxRec("T33", "ERROR", "test stopped", -1); END;
  ELSE
    cxCurN := 33;
    cxRec("T33", "SKIP", "Python skipped at the menu", -1);
    cxPyA := "skipped";
  END;
  IF cxPyMode == 2 THEN
    IFERR cxT34(); THEN cxRec("T34", "ERROR", "test stopped", -1); END;
  ELSE
    cxCurN := 34;
    cxRec("T34", "SKIP", "cas.caseval skipped at the menu", -1);
    cxPyB := "skipped";
  END;
  CX_LAST := "done";
  // the verdict, in the log, in CX_SUM and on the summary page
  CX_SUM := "form " + cxIStr(cxForm * cxAvail) + ", CAS " + IFTE(cxAvail == 1, "yes", "no");
  CX_SUM := CX_SUM + ", slowest " + cxIStr(cxSlowMs) + " ms, " + cxIStr(cxNP) + " pass, " + cxIStr(cxNF) + " fail";
  cxNote("-- summary --");
  cxNote("CAS available: " + IFTE(cxAvail == 1, "yes", "no"));
  cxNote("call form: " + cxFormTxt);
  cxNote("slowest call: " + cxIStr(cxSlowMs) + " ms, " + cxSlowId);
  cxNote("Python ints: " + cxPyA);
  cxNote("caseval: " + cxPyB);
  IF cxLong < 0 THEN cxNote("long CAS text: not tested"); END;
  IF cxLong == 0 THEN cxNote("long CAS text: fails at 1000 characters"); END;
  IF cxLong > 0 THEN cxNote("long CAS text: works to " + cxIStr(cxLong) + " characters"); END;
  cxNote("an infinite limit prints: " + cxInfTxt);
  IF cxBcdf == 1 THEN cxNote("BINOMIAL_CDF order (n, p, k): yes"); END;
  IF cxBcdf == 0 THEN cxNote("BINOMIAL_CDF order (n, p, k): NO"); END;
  IF cxIns0 == 1 THEN cxNote("INSTRING of an empty string: an error"); END;
  IF cxIns0 == 0 THEN cxNote("INSTRING of an empty string: NO error"); END;
  IF cxSci == 1 THEN cxNote("STRING(0.096, 2, 1): scientific"); END;
  IF cxSci == 0 THEN cxNote("STRING(0.096, 2, 1): NOT scientific"); END;
  cxNote(CX_SUM);
  cxSummary();
  cxView();
  RETURN "CASProbe: " + CX_SUM;
END;
