Matrices and lists
TI-84 Plus OS 2.55MP stores lists and matrices as VAT objects and evaluates
their element, aggregate, and linear-algebra operations through page-02
routines. This page covers layout, indexing, arithmetic, sorting, determinant,
inverse, multiplication, and row reduction. Variables and the
VAT, Floating-point engine, and
Variables, archive and unarchive describe the shared
storage and arithmetic layers.
Raw disassembly supplies the banked-page operations that the decompiler does not reduce reliably.
Data model
-
A list is
word count(2 bytes) followed bycount× 9-byteTIFloatelements (18-byte complex elements if the list is complex, flagged0x0C). Element $i$ (1-based) lives at $\mathrm{addr}(L_i)=\mathrm{data}+2+(i-1)\cdot 9$. -
A matrix is
byte columnsbyte rowsfollowed bycolumns*rows× 9-byteTIFloat, stored row-major. The element offset from the start of the data area, after the two dimension bytes, is$$\mathrm{offset}=\big((\mathit{row}-1)\cdot \mathit{columns}+(\mathit{column}-1)\big)\times 9$$
-
Every element read/write routes one
TIFloatthroughOP1/OP2and the FP engine — there is no “vector unit”; matrix multiply is a triple loop of_FPMult+_FPAdd. -
The data area is found through the VAT (
_FindSym, Variables & the VAT): the VAT entry’s data pointer + page byte locate thecount/dimheader, after which all indexing is pointer arithmetic computed by_AdrLEle/_AdrMEle. -
One shared Gauss-Jordan engine (
02:42A6) implements matrix inverse[A]⁻¹(flag0x00) anddet((flag0x40) with partial pivoting.rref(/ref(are the same elimination family.
Data layouts and creator routines [confirmed]
List — _CreateRList (00:10C4), _CreateCList (00:1109)
_CreateRList(count, dataPtrOut):
reject unless OP1 name token (8478.exp) ∈ {0x5D, 0x24, 0x3A, 0x72} # list-name classes
var_alloc(1) # carve count*9 + 2 bytes via _InsertMem
store count word at data[0..1]
if list is complex (8499.type & 8): data[2] = 0x0C # element-size flag
Layout: [countLo countHi] [TIFloat e1] [TIFloat e2] …. A complex list keeps a 0x0C
flag and 18-byte elements.
Matrix — _CreateRMat (00:1115)
_CreateRMat(H=rows, L=columns, dataPtrOut):
_HTimesL() # element count = H * L
var_alloc(2) # carve H*L*9 + 2 bytes
LD (HL),C # write columns
INC HL
LD (HL),B # write rows
_HTimesL(00:1EF6) computesresult = H * L(B=HHL=Σ L, aDJNZadd loop) — it computes the element count from the two dimension bytes. [confirmed]- The header stores
columns,rows; the payload containscolumns*rowsfloats row-major.
Dimension naming.
_CreateRMatreceivesH=rowsandL=columns. The common header writer atram:10E0recovers those bytes asBandC, then storesCbeforeBatram:10EE–ram:10F1._AdrMElereads the first byte as the stride, adds itB-1times, and addsC-1. Its public register convention is thereforeB=row,C=column, with the offset(row-1)*columns+(column-1). [confirmed]
Element access and index-to-offset conversion [confirmed]
Two address-calculators turn a 1-based index into a byte
pointer, then a 9-byte move shuttles the TIFloat to/from OP1.
List element address — _AdrLEle (02:47C5)
_AdrLEle(index, listDataPtr): ; HL=index, DE=listDataPtr
INC DE
INC DE ; skip the 2-byte count header
A = (DE) & 0x1F ; element type (low 5 bits); 0x0C ⇒ complex
CALL 21C4 ; classify real vs complex element width
HL = (index − 1) ; _HLTimes9(index-1)
CALL 1930 (_HLTimes9) ; HL = (index-1) * 9
HL += DE ; final element pointer
So list element i is at data + 2 + (i−1)*9 (×18 path for complex). _HLTimes9
(00:1930) is the universal “multiply by 9” (real TIFloat size). chk_type_lt_1a (ram:21C4)
masks the type to ≤0x19 and sets carry for the complex case (drives the 18-byte width). [confirmed]
Convenience wrappers (all = _AdrLEle then a 9-byte move through OP1, complex-aware): [confirmed]
_GetLToOP1(02:47EA) — list[i] → OP1 (real or complex via two_Mov9B).rcl_list_elem_to_op1(02:47FB),rcl_list_elem_b(02:47FE) — recall to OP1 with the index pre-loaded in RAM (84AF/84D3)._PutToL(02:4829) — OP1 → list[i];_CkValidNumvalidates the float first, then copies, honoring the complex (& 0xC) element width.rcl_c_list_elem(02:49A7),rcl_c_list_elem_b(02:49B5) — complex-list element viacplx_op_arrange(splits real/imag into OP1/OP2).get_pos_list_elem(02:5BBB) — fetch by a positive-integer index with_CkOP1Posbounds (loadsA=0x15=E_Statand jumps to the error vectorram:2741on a bad index).
Matrix element address — _AdrMEle (02:4002) [confirmed]
_AdrMEle: ; B=row, C=column, DE=matrixDataPtr
if B==0 or C==0 -> LD A,0x78
JP 0x2793 ; 0-index rejected (error vector)
A = (DE) ; A = columns ; first header byte
HL = 0
repeat (B − 1) times: HL += columns ; (row-1) * columns
HL += (C − 1) ; + (column-1)
DE += 2 ; skip both dim bytes
CALL 1930 (_HLTimes9) ; HL *= 9
HL += DE ; final element pointer
The row-major address is data + 2 + ((row-1)*columns + (column-1)) * 9.
Each (B-1) addition skips one complete row, and C-1 selects an element
within that row. The 8-bit additions propagate carry into H, so the result
is a 16-bit offset for matrices up to 99×99. [confirmed]
Matrix element wrappers: [confirmed]
_AdrMRow(02:4000) — address of the start of rowB; it setsC=1and enters_AdrMEle._GetMToOP1(02:4044) —[M](r,c)→ OP1 (_AdrMElethenRST4= load 9 bytes)._PutToMat(02:406C) =mele_store_ckvalid(02:4068):_AdrMEle_CkValidNum_MovFrOP1— OP1 →[M](r,c)with validation._StMatEl(38:6C8F) — high-level “store into[M](r,c)” used by the parser: resolves the matrix name (5F45), bounds-checks indices against the dims (r≤rows && c≤cols, else_JError 0x8C=E_Dimension), unarchives if needed, then_PutToMat. [standard]
Internal index helpers reused by the algorithms [confirmed]
mele_adr_af_jp(02:403C) =_AdrMEle(currentIJ)RST4— “load[M](i,j)to OP1” (the elimination inner-loop read). Indices come from the loop state at84AF/84B3/84B4.mele_adr_to8483(02:4051) =_AdrMEle_Mov9B(→OP2@8483)— load element to OP2.mele_put_af(02:405A) /mele_put_d3(02:405E) =_AdrMEle_CkValidNum_MovFrOP1— store OP1 back to[M](i,j).list_idx_times9(35:79E9) =_HLTimes9(idx)then a small dispatch (RST4) — the list analogue used in a few list-builder paths.
List operations [standard]
Creation, resizing, insertion, and deletion
| Routine | addr | Role |
|---|---|---|
_CreateRList | 00:10C4 | new real list: count*9+2 bytes; see Data layouts and creator routines [confirmed] |
_CreateCList | 00:1109 | new complex list: count*18+2 [confirmed] |
_IncLstSize | 07:4EF4 | grow a list in place via _InsertMem; caps length at 999 (0x3E7), else E_Dimension 0x8C (07:4F00 JP Z,0x2719 → LD A,0x8C). _InsertList is the distinct sibling at 07:4F07. [confirmed] |
_DelListEl | 07:4F43 | delete element(s): _HLTimes9(index) to size the gap (×2 if complex, & 0x1F == 0x0D), then _DelMem via a cross-page jump [confirmed] |
_RedimMat/_ConvDim | 07:4D3B / 38:741F | re-dimension (shared with matrices); _ConvDim/_ConvDim00 (38:741F/7422) coerce OP1 to a real index first [confirmed] |
dim(, dim(L)→n, list↔value
dim( reads the count word straight from the list header; assigning n→dim(L) calls the
resize path (_IncLstSize/_DelListEl) to grow/shrink, zero-filling new cells. List→matrix
and matrix→list (List►matr(, Matr►list() reshape via _DataSize and a linear payload copy
(mele_copy9_d3 (02:4539)/mele_copy9_loop (02:453F), a _DataSize-counted byte copy of the float payload). [standard]
List arithmetic L1+L2, scalar broadcast
Binary list ops are element-wise folds: the parser walks both lists by index, loads
L1[i]→OP1, L2[i]→OP2, applies the FP RST shortcut (RST 30h _FPAdd, _FPSub, _FPMult,
_FPDiv), stores into a freshly _CreateRList’d result. Length mismatch ⇒ E_DimMismatch
(_ErrDimMismatch 00:2715, 0x8B); a list⊕scalar broadcasts the scalar across every element. [standard]
sum(, prod( — higher-order folds over a list [confirmed]
Tokens 0xB6=sum(, 0xB7=prod( load a combiner function pointer and fold the
list (dispatcher 02:6104):
sum( : HL = 0x3A83 (cross-page → FP add-accumulate), seed via _OP1Set0
prod( : HL = 0x49B9 (seed accumulator = 1.0, _PushOP1), combine with _FPMult
CALL 0x64B7
...
JP (HL) # apply the combiner across e1..eN
The fold seeds the accumulator (0 for sum, 1 for prod), then for each element does
acc = combine(acc, L[i]) through OP1/OP2. Works on real and complex lists (type 1/0xD
both route to 02:6140). [confirmed]
Sequence, cumulative, sorting, and statistics operations
seq(expr,var,lo,hi[,step])evaluatesexprforvar = lo..hi, pushing each result and finally_CreateRList-ing the collected floats;_SetSeqM 36:7D1Fis the sequence-graph variant. A trace ofseq(X²,X,1,5,1)produced{1 4 9 16 25}and mapped the collection path (tools/macros/list-seq-eval.macro). Each element enters throughcross_page_jumpat37:6E87. The parser setup at38:5B3Cevaluates the expression, with34:5AA1and34:5BD8computing X². The append path runs through02:69BCand37:4260–37:4285; it addresses list elements through00:150Fand00:154Fand compares them at00:198D. Page07VAT routines at07:565F,07:5662, and07:5683grow the storage. After the last element,37:70DCcalls_CreateRListat00:10C4. The trace contains one collection cycle per element, with a period of roughly 2,000 instructions repeated five times. [confirmed] The02:5E14–02:5F5Dspan is the command-executor dispatch shared by every evaluated command; it is not theseq(collection loop.cumSum(is a running_FPAddwriting back each partial sum (the sum-fold with the accumulator stored every step). [hypothesis]SortA(/SortD(— list sort in place (SortA(co-sorts dependent lists); the comparator and per-element sort key are detailed in the next subsection. [confirmed]- Stats (
mean/median/sum/stdDev/variance) are list folds layered onsum(/sort. [hypothesis]
SortA( and SortD( list sorting [confirmed]
SortA( (tSortA 0xE3) and SortD( (tSortD 0xE4) sort a list in place — ascending and
descending respectively; SortA(L1,L2,…) co-sorts the trailing lists by the same permutation. This
is the command sort, distinct from the stat-internal stat_sort (3A:7935) that backs median/
quartile/Med-Med (see Statistics).
The command dispatch is byte-pinned in list_fold_dispatch on page 02.
CP 0xE3 at 02:6529 (SortA() and CP 0xE4 at 02:657A (SortD() converge
on the shared setup at 02:652F. Register A carries the direction: 0x0E for
ascending and 0x10 for descending. The executor chain checks arguments at
ram:38BB, registers the list through 02:5DFB, and saves the element pointer
from 0x84AF to 0x84B1. It then resets the pointer to 1 and enters the
compare/store loop through 02:6A12. The engine at 02:5939 compares each
element with _CpOP1OP2 (00:198D).
_CpOP1OP2 compares two TIFloats as real numbers [confirmed]: it tests the
sign (type byte bit 7), then the exponent, then the mantissa digits, and returns the
ordering. It does not compute a magnitude and does not read an imaginary part. Each comparison
therefore orders elements by the single 9-byte TIFloat the sort holds in OP1/OP2:
| List element | Sort key |
|---|---|
| real | the value (sign → magnitude) |
| complex | the real part only; the imaginary part is not read, and elements with equal real parts keep their input order |
No element type is ordered by magnitude/modulus (_CAbs is never on this path). [comparator and
its real-number semantics confirmed; the per-element sort key follows from them — the unanalyzed
sort body’s element-load is not byte-traced]
Traceable list sample
The tools/tibasic-samples/data.* fixture drives the list paths above with a
small end-to-end TI-BASIC program:
{3,1,4,1,5}->L1
SortA(L1)
cumSum(L1)->L2
sum(L1)->S
Disp L1
Disp L2
Disp S
It exercises list literal creation, list variable tokens (5D 00/5D 01),
in-place sorting, a running cumulative sum, a folded sum, and list display. The
generated DATA.8xp was run under headless TilEm: the screen showed sorted
L1={1 1 3 4 5}, cumulative L2={1 2 5 9 14}, and sum 14; the trace hit
list_fold_dispatch (02:6104) plus the page-38 list parse/store helpers. [confirmed]
Matrix operations [confirmed]
dim(, redim, identity, copy
dim([M])reads the two header bytes → a 2-element list{rows,cols};{r,c}→dim([M])reallocates via_RedimMat(07:4D3B), preserving overlapping cells and zero-filling new ones. [standard]identity(n)(token0xB4→identity_build(02:4108)) [confirmed]: allocaten×n, then walk every cell writing1.0whenrow==col(theexp==typetest) and0otherwise:_OP1Set1 ; for each (i,j): if i==j -> store 1.0 (mantissa[0]=0x10) else 0Fill(value,[M])/randM(stamp a constant / random values across all cells via a per-cell loop over the whole matrix. The02:62D4branch (CP 0xB5) isdim((0xB5=tDim), which creates ther×cresult (5DBB→_CreateRMat 110F) and stores the dims (631B/631C/4825) but performs no fill. For the decodedrandM(fill see TherandM(cell fill.- Matrix copy/reshape =
_DataSize-counted byte copy of the float payload (mele_copy9_d3(02:4539)/mele_copy9_loop(02:453F)). [confirmed]
The randM( cell fill [confirmed]
randM(rows,cols) builds its r×c result through _CreateRMat (00:110F). It fills
each cell with $\operatorname{int}(19\cdot\operatorname{rand})-9$, matching the
documented integer range $[-9,9]$. The loop is byte-pinned at
02:5CC1–02:5CE6. A headless TilEm trace of randM(3,3) executes this path
(tools/macros/matrix-randm.macro):
02:5CC1 loop:
PUSH BC / PUSH DE ; save cell counter and element pointer
5CC3: CALL ram:392D ; banked-call stub -> _Random (36:7DC9); OP1 = uniform [0,1)
LD A,0x13 ; 19 decimal
CALL ram:389D ; banked-call stub -> 33:5F83; load small int A as FP operand
CALL _FPMult (238B) ; OP1 = 19·rand
CALL _Intgr (2263) ; truncate -> {0..18}
LD A,0x09 ; 9 decimal
CALL ram:389D ; second operand = 9
CALL _FPSub (2297) ; OP1 = int(19·rand) - 9 in [-9, 9]
POP DE ; advance element pointer by one float cell
CALL 1B0C ; store OP1 into the matrix element
LD HL,-18 / ADD HL,DE ; step to the next 9-byte cell
POP BC / DEC BC ; cells remaining--
JR NZ,loop
The loop reaches _Random (0x4B79 → 36:7DC9) through a page 0 banked-call
stub table. It does not use an RST 28h bcall site, so a ROM-wide scan for
RST 28h.dw 0x4B79 finds no match. The stub at ram:392D contains CALL 2B09
followed by the inline descriptor .dw 0x7DC9.db 0x76. The trampoline writes
the descriptor’s page byte to port 6. Bit 7 clear selects flash, and the low six
bits select the page, so 0x76 selects page 36. Static descriptor scans must
mask the page byte with 0x3F. The small-integer loader stub at ram:389D
targets 33:5F83 through the same mechanism. [confirmed]
[A] + [B], [A] - [B], scalar·[A] — element-wise [standard]
Binary matrix add/sub apply the FP operation through a nested walk:
for each column:
for each row:
load [M](r,c) -> OP1
apply the FP operation
store the result
The operation requires equal dimensions (_ErrDimMismatch 0x8B). The nested two-counter cell
walk at 02:412A is the transpose copy (§ transpose); the add/sub element-loop driver is a
sibling in the same 412A–414E family and is inferred here. [standard]
[A] * [B] — matrix multiply [confirmed]
The multiply body is at 02:40BA. It is not a defined function in the disassembly (so the
decompiler/MCP can’t reach it), so this was decoded from rom.bin directly with z80dasm,
cross-checked against a routine Ghidra does define. The body is called from 02:5FFF (the
* operator handler, in the 02:5FE6 region) and reused from 02:4605 and 02:5B39. (0x40BA
is also the _SinCosRad bcall ID in ti83plus.inc — a hex coincidence, unrelated to this page-02
address.)
40BA is a classic O(n³) triple loop with an FP accumulator:
for each result cell (i,j): # counters at 84B7, 84B4
for k = 1 .. inner: # inner counter at 84AF
load [A](i,k) (403C mele_adr_af_jp)
multiply by [B](k,j) (47B9 / 0166F FP multiply)
accumulate (479F)
store acc -> [C](i,j) (4064 / 405A)
The three dec (hl) counters (84AF inner, 84B4, 84B7) each have a jr nz back-edge
(40E5, 40F9, 4100); an inner-dim mismatch (A.cols ≠ B.rows) raises _ErrDimMismatch. An
n×n product is n³ TIFloat multiply+add steps. [confirmed] The body comes
from direct rom.bin decoding; callers 02:5FFF, 02:4605, and 02:5B39
are byte-verified.
Transpose [A]ᵀ — 02:412A, dispatched from the ᵀ token 0x0E [confirmed]
The transpose operator ᵀ is the postfix token tTrnspos = 0x0E. The page-02 command
dispatcher handles it at 02:60E9 (CP 0x0E). It requires one matrix operand by testing
CP 0x02 followed by JR NZ. At 02:60F5, it swaps the two dimension bytes for the result
header with LD A,H, LD H,L, and LD L,A, then
allocates the transposed-shape matrix (5DBB/5DE0), runs the per-cell copy body at 02:412A,
then stores via JP 0x5F89. 02:412A has exactly one caller, 02:60FE (byte-verified CD 2A 41).
02:412A is the transpose copy [confirmed]. It walks every source cell and writes the value into the
destination whose _AdrMEle stride is the swapped dimension, so dst(c,r) = src(r,c):
412A: LD HL,(84AF) ; loop counters = dims
412E: CALL 403C ; load src [M] (B=row,C=column) from (84D3) → OP1
4131: LD HL,(84AF)
LD B,L
LD C,H
4136: CALL 4068 ; store OP1 → dst [M] via dest ptr (84D7)
4139: DEC (84AF)
JR NZ,412E ; inner counter
4141: LD (HL),C
INC HL
DEC (HL)
JR NZ,412E ; outer counter
4146: POP HL
LD B,L
LD C,H
RET
403C reads from the source data pointer (84D3); 4068 writes to the destination pointer
(84D7). The destination header carries the dimensions swapped by 02:60F5.
The row-major _AdrMEle calls therefore place src(r,c) at dst(c,r). [confirmed]
02:4178 (mat_fill_type1) is a separate single-counter fill/apply in the 414A–4178 block,
not the transpose body. [confirmed]
augment(, dim(, List►matr(, Matr►list( — per-function drivers [standard]
These are dispatched from the page-02 function-token evaluator (list_fold_dispatch, the
CP immJR/JP chain that runs 5E46/60C8–63xx, keyed on the token byte). Each command’s
body and its single caller are byte-verified below.
| Command | dispatch site | body | what the disassembly shows |
|---|---|---|---|
Matr►list( | 0x8D @ 6388 | 02:4773 (2-arg), 02:49E3 (1-arg list copy) | [confirmed] The 0x8D branch splits on argument count (638D: CP 0x02). The column-extract engine is 02:4773 (2-arg path: 639D: CALL 5DD8CALL 4773; only caller 63A0, byte-verified CD 73 47). It holds one column in C while B walks the rows, reading through the _GetMToOP1 setup at 02:4040 and writing through mele_store_ckvalid at 02:4068. It then copies the completed column into the destination list through 02:4051/02:479F. The 1-arg/list path uses 02:49E3 (6397: CALL 0x49E3), a list-element copy-until-length-match (47E6 recall, 4825 store, 21BB compare vs (84AF), RET Z). |
transpose ᵀ | 0x0E @ 60E9 | 02:412A | [confirmed] Swaps the dim header (60F5), allocates the transposed shape, then 412A copies dst(c,r)=src(r,c) over every cell (403C read from (84D3), 4068 write to (84D7)); only caller 60FE. See the transpose subsection above. |
augment( | 0x91 @ 02:635B | 02:6238 copy [confirmed]; 02:4663 engine entered but carry-gated [confirmed] | The branch requires two operands, reads the dimensions at 02:5D98, and compares the row counts with LD A,HCP L. Equal rows fall through; H>L raises E_Dimension. 02:6238 allocates the result and copies the row-major float payload through 02:4539. The branch then calls 02:4663. Carry is set at 02:6361 and restored at 02:6378; JR C,46EF at 02:46DC skips elimination. The statistics regression path enters the same dispatcher through 3A:6398 with carry clear. The augment(L1,L2) sibling at 02:637F also shares the setup at 02:6362 with carry clear. [confirmed] |
dim( (matrix create/set-dims) | 0xB5 @ 62D4 | create + dim setup (5DBB/5DEB) [confirmed] | The compare at 62D4 is CP 0xB5, and 0xB5 = tDim (dim(), not randM( — so this is the →dim( matrix create/resize handler. It splits on argument count (62D9: CP 0x02): a 2-arg path (62DD) and a 1-arg path (630A). Both create the result and set its dims through 02:5DBB (CALL 5CEB registers the variable by name, stores the data pointer to 84D3, reads and zero-rejects the dim bytes OR LJP Z,2719, stores dims to 84AF) and 02:5DEB/02:631E. There is no per-cell fill loop here — consistent with dim(, which only sets dimensions. 02:5264 (cplx_swap_dispatch) is reached only from the 0xBD complex-operand branch (62D0), not here. randM( is a separate two-byte token (tRandM = 0xBB20) whose decoded fill loop is documented under The randM( cell fill. [confirmed] |
List►matr( | 0x8E @ 61C1 | 02:7D19 + copy | reshapes the argument lists into a matrix (_DataSize-counted float copy 4539/453F). [standard] |
The matrix-element kernels these drivers share are _AdrMEle/_AdrMRow (4002/4000) for indexing,
4068 (mele_store_ckvalid) for validated stores, and 4539 (mele_copy9_d3) for the bulk
row-major payload copy. Each command’s dispatch site and body is
[confirmed]. The randM( cell fill and the carry-gated role of 02:4663
inside augment( are also [confirmed].
Determinant, inverse, and row reduction [confirmed]
det( and [A]⁻¹ share the Gauss-Jordan elimination engine with partial pivoting —
matrix_gauss_engine @ 02:42A6 — the entry flag in A selecting behaviour; only two
direct call sites exist (byte-verified — CD A6 42 appears exactly twice). rref(/ref( are a
separate driver and do not call 42A6 (see below):
| Token / op | site | flag A | meaning |
|---|---|---|---|
[A]⁻¹ (^ token 0x0C, operand = matrix) | 02:5F80 | 0x00 | inverse; singular ⇒ error |
det( (token 0xB3) | 02:5FC0 | 0x40 | determinant; bit6 set ⇒ singular tolerated (returns 0) |
det(’s handler at 02:5FA3 (not a defined function in the disassembly; address
unverified) first type-checks the operand is a matrix (chk_op_is_matrix (02:69B7):
type==2 else E_DataType 0x89), then LD A,0x40CALL 0x42A6.
The engine (42A6) [confirmed]
matrix_gauss_engine(A = mode flags):
HL = dims (84AF)
if H != L -> _JError(0x8C) # must be square for det/inverse
if 1x1: handle scalar directly (inverse = _FPRecip)
461C: scan |all elements| -> max magnitude (pivot-tolerance baseline)
init permutation/pivot vector at (84D5): perm[k] = k # identity permutation
for each pivot column 'col' (84AF loop):
41D0/41C1: PARTIAL PIVOT — scan the column for the largest |element|,
compare |OP1| vs |best| via _AbsO1O2Cp
remember the row
43B9 -> 414E: SWAP the pivot row into place (full physical row swap)
4259 swaps the matching entries in the permutation vector,
and (for det) toggles the running sign
normalize pivot row: load pivot, _FPRecip / _FPDiv so pivot -> 1
4473 / 426D: ELIMINATE — for every other row, row_r -= factor * pivot_row
(4473 = load-load-_FPSub element step, 426D/426F = dot-product /
back-substitution accumulate with _FPMult + RST6 _FPAdd)
accumulate determinant = product of pivots (× sign from swaps)
SINGULAR handling (43A5): if a pivot is ~0:
BIT 6,A
JP Z, 0x26F0 (_ErrSingularMat, E_SingularMat 0x83)
-> inverse (flag 0, bit6=0) ERRORS
-> det (flag 0x40, bit6=1) returns 0
Key sub-routines (all page_02; names are the live Ghidra DB labels): [confirmed]
461Cmat_max_abs— compute the matrix’s max-abs element (numeric scale for the near-zero pivot test).41C1abs_cmp_op1op2—|OP1|vs|pivot|compare (1A0F/1987abs+compare);41D0— scan a column for the largest-magnitude pivot (partial pivoting), calling43B9to swap rows as it goes.43B9/414Emrow_swap_loop/_AdrMRow— physical row swap / row scale (whole-row moves;414Eloads the column-count stride and swaps two complete rows via_AdrMRow×2 +1DDA).4259— swap two entries in the permutation vector at84D5.4473ele_sub_ref— the elimination element step ([M](i,k) − factor*[M](pivot,k):RST8CALL 403CJP 2297= load +_FPSub).426Dcol_dot_accum/426Fcol_dot_accum_from— column dot-product / back- substitution accumulate (_FPMult+RST6).- Pivot normalize uses
_FPRecip/_FPDiv; sign/inverse use_InvOP1S.
det( therefore = forward elimination with partial pivoting, return the signed product
of the pivots (each row swap flips the sign); a zero pivot ⇒ det = 0 (no error).
[A]⁻¹ = full Gauss-Jordan (reduce to identity, the augmented identity becomes the
inverse); a zero pivot ⇒ ERR:SINGULAR MAT.
Determinant sign and pivot-product bytes (02:43D8–02:4470) [confirmed]
The determinant sign comes from the permutation parity, not a separate sign cell. Each
physical row swap (43B9) calls 4259 to swap the matching pair in the permutation
vector at 84D5; the determinant magnitude is the running product of the diagonal pivots
formed during back-elimination. The tail that closes the det/inverse pass:
43D8 (det branch, bit6 = det):
43D9: BIT 6,A ; det mode?
43DE: CALL 151B ; pop pivot
43E3..43F6: PUSH AF ; (RST 8 _CpyToOP2)
CALL 403c (load [M](i,j)) ;
CALL 238b (_FPMult)
DEC pivot/row counters (84B0) ; loop
→ multiply the running determinant by each pivot
43F8: POP AF
AND 1
JP NZ,24bd ; *** DET SIGN *** low bit of the
; permutation-swap count → conditional _InvOP1S (negate)
43FF (inverse branch): re-walk for the augmented-identity columns,
4410..446F: per-column back-substitution (4428/445B = _FPMult-accumulate,
442B/24bd = _InvOP1S sign flips), then JP 0x420F to undo the
column permutation (4259-pairs) so the inverse comes out in the
original row/col order.
So the sign byte is the LSB of the swap-count applied via _InvOP1S (00:24BD) at
43FB/442B; the pivot product is the 238B/RST 30h accumulate over the diagonal in
43E3-43F6. The permutation undo (420F/4259) restores element order for the inverse. [confirmed]
Separate rref( and ref( driver [standard]
rref(/ref( do not re-enter the 42A6 Gauss-Jordan engine. A function-xref shows
matrix_gauss_engine (02:42A6) has exactly two callers — mat_inverse_entry (02:5F80,
flag 0) and det_entry (02:5FC0, flag 0x40); there is no third call site (byte-confirmed
above: CD A6 42 appears exactly twice). So det(/[A]⁻¹ are the only consumers of that
square-only, partial-pivoting driver. [confirmed]
rref( (BBh,A6h) and ref( (BBh,A5h) are 2-byte 0xBB-lead function tokens. On the
page-38 statement/expression evaluator (eval_expr_inner 38:59A4), token 0xBB is detected
and parse_advance consumes the prefix; the second byte is then dispatched through the
evaluator’s six-entry leaf_production_handler_table at 38:7175.
The selector at 38:701A–7026 chooses grammar_handler_table, the
38:478C code family, or this leaf table; 703A: CALL 0x0033 = _LdHLind
jumps to the resolved handler. Their reduced-row-echelon elimination is therefore a distinct,
non-square-tolerant driver reached through that table — a separate routine from 42A6, using
the same per-element FP primitives (_FPDiv/_FPMult/_FPSub) but with its own pivot loop
that tolerates rectangular matrices and rank deficiency (zero rows left in place, no
SINGULAR MAT). The concrete rref/ref body sits behind the two-byte entries in
leaf_production_handler_table; the table is now named and typed in the rebuilt
database, but the two tokens’ exact handler selection has not yet been isolated.
The two-caller xref establishes that it is a
separate driver from 02:42A6 [confirmed]. Its exact body address remains
[standard].
Floating-point and VAT integration [confirmed]
- Every element is a
TIFloat(Floating-point). Indexing produces a pointer; the value is then moved intoOP1/OP2(RST4= load-9,_Mov9B,_MovFrOP1) and all arithmetic is the FP engine’sRST 30h(_FPAdd)/_FPMult/_FPDiv/_FPSub/_FPRecip. There is no SIMD; a matrix multiply makes thousands of these calls. Complex elements (lists/[i]) carry a0x0Cflag and use 18-byte (two-float) elements, split viacplx_op_arrange. - Where the data lives: the parser resolves the list/matrix name through
OP1→_FindSym/_ChkFindSym(Variables & the VAT/sub-vat) → VAT entry → data pointer (+ flash page if archived). Thecount/dimheader is read first; then_AdrLEle/_AdrMEledo pointer math. A store into an archived matrix/list unarchives to RAM first (_Arc_Unarc; Flash cannot be written in place). - Scratch RAM used by the algorithms (verified operands):
84AF(current dims / i,j loop state),84B0/84B3/84B4(pivot, k, row counters),84B7(dims copy),84D3/84D5/84D7(data pointers + the permutation vector base),8478=OP1,8483=OP2,8499=OP4,84AF=OP6 region = the matrix-op loop frame.
Errors [confirmed]
The list/matrix routines raise these _JError codes; each row gives the code, its name,
and the routine and condition that triggers it.
_JError code | name | raised by |
|---|---|---|
0x78 | 0-index reject (via ram:2793) | _AdrMEle/_AdrMRow on a 0 row/col index |
0x83 | E_SingularMat (ERR:SINGULAR MAT) | 42A6 inverse on a zero pivot (_ErrSingularMat 00:26F0) |
0x85 | E_Increment | _ErrIncrement 00:26F8 (bad seq/loop step) |
0x89 | E_DataType | det(/matrix ops on a non-matrix operand (chk_op_is_matrix (02:69B7)) |
0x8B | E_DimMismatch (ERR:DIM MISMATCH) | add/sub/multiply with incompatible dims (_ErrDimMismatch 00:2715) |
0x8C | E_Dimension (ERR:INVALID DIM) | non-square det/inverse, out-of-range element store (_ErrDimension 00:2719, _StMatEl) |
0x15 | E_Stat (via ram:2741) | get_pos_list_elem bad index (_CkOP1Pos) |
Routine index
| space:addr | name | what |
|---|---|---|
00:10C4 | _CreateRList | new real list (count*9+2) [confirmed] |
00:1109 | _CreateCList | new complex list (count*18+2) [confirmed] |
00:1115 | _CreateRMat | new matrix (H*L*9+2, header columns,rows) [confirmed] |
00:1EF6 | _HTimesL | element count = H*L (dims multiplied) [confirmed] |
00:1930 | _HLTimes9 | ×9 (real TIFloat stride) [confirmed] |
02:4000 | _AdrMRow | address of matrix row start [confirmed] |
02:4002 | _AdrMEle | matrix element address: ((row-1)*columns+(column-1))*9 [confirmed] |
02:4044 | _GetMToOP1 | [M](i,j) → OP1 [confirmed] |
02:406C | _PutToMat | OP1 → [M](i,j) (validated) [confirmed] |
02:40BA | matrix-multiply body | O(n³) triple loop, decoded from rom.bin (not a defined function in the disassembly); called from 02:5FFF/4605/5B39. 0x40BA in ti83plus.inc is the unrelated _SinCosRad bcall ID. [confirmed] |
02:4108 | identity_build | identity(n): diagonal-1 fill (token 0xB4) [confirmed] |
02:412A | mat_transpose | transpose [A]ᵀ body (token 0x0E, dispatched 60E9/called 60FE): per-cell copy dst(c,r)=src(r,c) via the swapped dest header [confirmed] |
02:414E | mrow_swap_loop | row swap/scale (elimination) [confirmed] |
02:4178 | mat_fill_type1 | live DB name; single-counter per-cell fill/apply loop in the 414A–4178 block — not transpose [confirmed] |
02:4539 | mele_copy9_d3 | bulk row-major float-payload copy (skip 2 dim bytes, LDIR); used by augment(/reshape [confirmed] |
02:4663 | mat_gauss_engine | live DB name; min(H,L) partial-pivoting elimination engine; only caller is the augment( 0x91 branch (6379). Its role inside plain augment( is the one open item [standard] |
02:4773 | mat_to_list_cols | Matr►list( 2-arg column-extract engine (only caller 63A0): nested col×row walk copying matrix columns into list element(s) [confirmed] |
02:5264 | cplx_swap_dispatch | live DB name; complex OP-pair arrange/swap (5344/52D3) reached only from the 0xBD branch (62D0) — not the 0xB5/dim( matrix-create branch [confirmed] |
02:6238 | mat_augment_copy | augment( column-concat: allocate result (5DE0) + 4539 payload copy + re-point 84D3 [confirmed] |
02:49E3 | lele_copy_until_eq | live DB name; list-element copy-until-length-match (21BB, RET Z); inner copy of the Matr►list( 1-arg/list path (6397) [confirmed] |
02:41C1 | abs_cmp_op1op2 | absolute-value compare: OP1 vs pivot [confirmed] |
02:41D0 | pivot_col_scan | partial-pivot: find largest absolute value in column [confirmed] |
02:4259 | perm_swap | swap two entries of the permutation vector (84D5) [confirmed] |
02:426D/426F | col_dot_accum/col_dot_accum_from | column dot-product / back-substitution accumulate [confirmed] |
02:42A6 | matrix_gauss_engine | inverse(flag 0)/det(flag 0x40) Gauss-Jordan + partial pivot; square-only (H==L guard) [confirmed] |
02:4473 | ele_sub_ref | [M] − factor*pivot element step (_FPSub) [confirmed] |
02:461C | mat_max_abs | maximum absolute element (pivot tolerance) [confirmed] |
02:47C5 | _AdrLEle | list element address: data+2+(i-1)*9 [confirmed] |
02:47EA | _GetLToOP1 | list[i] → OP1 (complex-aware) [confirmed] |
02:47FB | rcl_list_elem_to_op1 | recall list elem to OP1 [confirmed] |
02:47FE | rcl_list_elem_b | recall list elem (B-indexed) [confirmed] |
02:4829 | _PutToL | OP1 → list[i] (validated, complex-aware) [confirmed] |
02:49A7 | rcl_c_list_elem | complex-list element → OP1/OP2 [confirmed] |
02:49B5 | rcl_c_list_elem_b | complex-list element (B-indexed) [confirmed] |
02:5BBB | get_pos_list_elem | list element by positive index (bounds) [confirmed] |
02:5E46 | func_eval_dispatch | single-byte function-token evaluator (0xB0–0xCD) [confirmed] |
02:5F80 | mat_inverse_entry | [A]⁻¹: flag 0 → matrix_gauss_engine [confirmed] |
02:5FC0 | det_entry | det(: flag 0x40 → matrix_gauss_engine [confirmed] |
02:6104 | list_fold_dispatch | sum(/prod( higher-order list fold [confirmed] |
02:69B7 | chk_op_is_matrix | require operand type==2 else E_DataType [confirmed] |
ram:21C4 | chk_type_lt_1a | classify element type width: AND 0x1FCP 0x1ACP 0x18CCF — real-vs-complex (0x0C) element width [confirmed] |
35:79E9 | list_idx_times9 | list index ×9 + dispatch [confirmed] |
07:4D3B | _RedimMat | re-dimension matrix/list [confirmed] |
07:4F07 | _InsertList/_IncLstSize | grow a list in place [confirmed] |
07:4F43 | _DelListEl | delete list element(s) [confirmed] |
38:6C8F | _StMatEl | parser store into [M](r,c) (bounds-checked) [confirmed] |
38:741F/7422 | _ConvDim/_ConvDim00 | coerce a dim/index to real [confirmed] |
00:26F0 | _ErrSingularMat | E_SingularMat 0x83 [confirmed] |
00:26F8 | _ErrIncrement | E_Increment 0x85 [confirmed] |
00:2715 | _ErrDimMismatch | E_DimMismatch 0x8B [confirmed] |
00:2719 | _ErrDimension | E_Dimension 0x8C [confirmed] |
Resolved behavior and remaining questions
rref(/ref(use a separate driver, not42A6. Xref proves42A6has exactly two callers (inverse5F80, det5FC0); rref/ref are 2-byte0xBB-lead function tokens dispatched via the page-38 evaluator’sleaf_production_handler_table(38:7175). Theref(execution dispatch is byte-pinned in the page02command chain. It comparesCP 0x2Dat02:609Aand, with arguments present, executesRST 28h.dw 0x4B85. Bcall ID4B85hresolves through the page3Btable to35:7995; its port-encoded page byte0x75selects page35.35:7995is an iterative FP reduction loop (_Minus1/_FPMult/OP-exchange primitives, back edge at35:79C4) consistent with the row-reduction driver. [confirmed] Therref(execution dispatch lives on page38, where two entry stubs (38:514Fwith carry set andB=1;38:5157with carry clear andB=0) converge onRST 28h.dw 0x4B88at38:515D. The ID resolves through the page3Btable to02:7C23, a per-element driver that walks the pushed matrix data from the FPS pointer (LD HL,(9824)then aDJNZloop), validates dimensions against the header bytes (8479/847Aexponent checks raising through26F4on failure), and stores results back per cell. NoCP 0x2Esite exists on page02, so the parser normalizes therref(token before this dispatcher. The role ofBand carry in distinguishingrref(from related calls remains [hypothesis]. The parse-side signature descriptors remain distinct (38:431E/0x5108forref(vs38:4323/0x510Cforrref().- det sign / pivot-product (
42A6tail43D8-4470) and dimension labeling. The det sign = LSB of the permutation-swap count applied via_InvOP1S(24BD) at43FB/442B; the magnitude is the238B/RST 30hdiagonal-pivot accumulate (43E3-43F6);420F/4259undo the column permutation for the inverse. Matrix storage is [confirmed]: the first header byte is the column count, the second is the row count, and_AdrMEletakesB=row,C=column. See Data layouts and Element access. - transpose,
Matr►list(, and theaugment(column-concat bodies. Each command’s page-02dispatch site and body are byte-confirmed, every body having exactly one caller:- transpose
[A]ᵀ(token0x0E@60E9) →02:412A(only caller60FE): the dim header is swapped (60F5) and412Acopiesdst(c,r)=src(r,c)over every cell.02:4178is a separate single-counter fill/apply, not transpose. [confirmed] Matr►list((0x8D@6388) →02:4773(2-arg column-extract engine, only caller63A0) with02:49E3as the 1-arg/list inner copy. [confirmed]augment((0x91@635B) → equal-rows guard (CP LJP NC,2719) + column-concat copy at02:6238(5DE0allocate +02:4539LDIRpayload copy). [confirmed]dim((0xB5@62D4;0xB5=tDim, notrandM() → creates the result and sets its dims (5DBB/5DEB).02:5264(cplx_swap_dispatch, only caller62D0in the0xBDbranch) is reached only from that complex branch, not here. [confirmed]List►matr(0x8Ebranch (61C1) →02:7D19+_DataSizecopy (4539/453F) is unchanged [standard].
- transpose
- The
augment(call to02:4663performs pivot-column setup but skips elimination because the engine tests the carry set by02:6361. The statistics regression path enters the same dispatcher with carry clear. [confirmed] - The
randM(fill loop at02:5CC1–02:5CE6computes $\operatorname{int}(19 \cdot \operatorname{rand}) - 9$ per cell. It calls_Random(36:7DC9) through the page 0 banked-call stub atram:392D; noRST 28hbcall site is involved. See TherandM(cell fill. [confirmed] seq(/SortA(/SortD(/stats list-builders: confirm the collect-then-_CreateRListloop and the in-place float sort/compare. (Residual — comparator_CpOP1OP2confirmed; the unanalyzed page-02 sort body’s element-load is still not byte-traced.)