>>>>> Ihor Radchenko <[email protected]> writes:

> You cannot copy-paste any significant non-trivial code from AI (that's the
> current Emacs policy), but otherwise AI is allowed -- for idea generation,
> pseudocode, code examples not used directly, analysis, etc.

So, realistically I’m never going to have the time to do this, as it turns
out. Life has become too busy. If you’d like to get an idea of what the
implementation _would_ look like, the attached code works for me and expresses
the idea.

diff --git c/etc/ORG-NEWS w/etc/ORG-NEWS
index f4ff44d29..6f3d45fe3 100644
--- c/etc/ORG-NEWS
+++ w/etc/ORG-NEWS
@@ -220,6 +220,31 @@ The keybindings in the repeat-maps can be changed by customizing
 
 See the new [[info:org#Repeating commands]["Repeating commands"]] section in Org mode manual.
 
+*** New Löb-based table formula evaluation engine
+
+A new table formula evaluation engine based on Löb's theorem is now
+available.  The new engine computes all table cells at once using lazy
+evaluation with automatic memoization, ensuring each cell is calculated
+exactly once regardless of how many other cells reference it.
+
+The new engine provides:
+- Automatic dependency resolution (no need for multiple recalculation passes)
+- Explicit cycle detection with clear error messages via Tarjan's SCC algorithm
+- Improved performance for large tables with complex interdependencies
+
+To use the new engine, set ~org-table-formula-engine~ to ~loeb~:
+
+#+begin_src emacs-lisp
+(setq org-table-formula-engine 'loeb)
+#+end_src
+
+The default value is ~classic~, which uses the traditional iterative
+evaluation that processes formulas cell by cell.
+
+The loeb engine is used when recalculating the entire table (with
+=C-u C-c C-c= or =C-u C-c *=).  Single-line recalculation continues to
+use the classic engine.
+
 *** Tables copied from LibreOffice Calc documents can be pasted as Org tables
 
 Tables copied into the clipboard from LibreOffice Calc documents can
diff --git c/lisp/org-table-loeb.el w/lisp/org-table-loeb.el
new file mode 100644
index 000000000..9a7e9fc12
--- /dev/null
+++ w/lisp/org-table-loeb.el
@@ -0,0 +1,744 @@
+;;; org-table-loeb.el --- Loeb-based table calculation engine -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: John Wiegley <[email protected]>
+;; Keywords: outlines, hypermedia, calendar, text
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file implements a Loeb-based calculation engine for Org tables.
+;;
+;; The approach uses the loeb fixed-point combinator to automatically
+;; resolve dependencies between cells, ensuring:
+;; - Each cell is calculated exactly once (memoization via thunks)
+;; - Dependencies are resolved in the correct order (automatic)
+;; - Cycles are detected explicitly (not via stack overflow)
+;;
+;; The implementation follows a hybrid approach:
+;; 1. Parse table and formulas into a matrix of closures
+;; 2. Build explicit dependency graph for cycle detection
+;; 3. Use topological sort for evaluation order
+;; 4. Apply loeb for memoized evaluation within DAG regions
+;; 5. Fall back to iteration for strongly connected components (SCCs)
+;;
+;; See: https://github.com/quchen/articles/blob/master/loeb-moeb.md
+
+;;; Code:
+
+(require 'org-macs)
+(org-assert-version)
+
+(require 'seq)
+(require 'thunk)
+(require 'cl-lib)
+
+;;; Loeb Core Functions
+
+(defun org-table-loeb--matrix* (fs)
+  "Apply loeb to a 2D matrix FS of closures, returning thunked matrix.
+Each closure in FS should accept a single argument: the final matrix.
+Returns a matrix where each cell is a thunk that, when forced, computes
+the cell's value.
+
+FS is a list of rows, where each row is either:
+- The symbol `hline' (for horizontal separator lines)
+- A list of closures, each of type (final-matrix -> value)"
+  (letrec ((go (seq-map
+                (lambda (row)
+                  (if (eq row 'hline)
+                      'hline
+                    (seq-map (lambda (cell)
+                               (thunk-delay (funcall cell go)))
+                             row)))
+                fs)))
+    go))
+
+(defun org-table-loeb--matrix (fs)
+  "Apply loeb to matrix FS and force all thunks.
+Returns a matrix of computed values."
+  (seq-map (lambda (row)
+             (if (eq row 'hline)
+                 'hline
+               (seq-map #'thunk-force row)))
+           (org-table-loeb--matrix* fs)))
+
+(defsubst org-table-loeb--get (row col matrix row-map)
+  "Get value at ROW, COL from loeb'd MATRIX, forcing the thunk.
+ROW and COL are 1-based logical indices (like Org table references).
+ROW-MAP maps logical row numbers to matrix indices (0-based)."
+  (let ((matrix-idx (gethash row row-map)))
+    (if (null matrix-idx)
+        (error "Cannot reference row %d (may be hline or out of bounds)" row)
+      (let ((matrix-row (nth matrix-idx matrix)))
+        (thunk-force (nth (1- col) matrix-row))))))
+
+(defsubst org-table-loeb--get-range (row1 col1 row2 col2 matrix)
+  "Get values in range from (ROW1,COL1) to (ROW2,COL2) from MATRIX.
+Returns a flat list of values, skipping hlines."
+  (let (result)
+    (cl-loop for r from row1 to row2
+             for row = (nth (1- r) matrix)
+             unless (eq row 'hline)
+             do (cl-loop for c from col1 to col2
+                         do (push (thunk-force (nth (1- c) row)) result)))
+    (nreverse result)))
+
+;;; Dependency Graph and Cycle Detection
+
+(defun org-table-loeb--parse-refs (formula)
+  "Parse cell references from FORMULA string.
+Returns a list of (row . col) pairs for absolute references.
+Handles @R$C, $C (column-only), and @R (row-only) syntax."
+  (let ((refs nil)
+        (case-fold-search nil))
+    ;; Match @R$C patterns (absolute cell references)
+    (save-match-data
+      (let ((pos 0))
+        (while (string-match "@\\([0-9]+\\)\\$\\([0-9]+\\)" formula pos)
+          (push (cons (string-to-number (match-string 1 formula))
+                      (string-to-number (match-string 2 formula)))
+                refs)
+          (setq pos (match-end 0)))))
+    (nreverse refs)))
+
+(defun org-table-loeb--parse-cell-ref (ref)
+  "Parse cell reference REF string into (row . col) or nil.
+REF should be like \"@1$2\" for a field formula."
+  (when (string-match "@\\([0-9]+\\)\\$\\([0-9]+\\)" ref)
+    (cons (string-to-number (match-string 1 ref))
+          (string-to-number (match-string 2 ref)))))
+
+(defun org-table-loeb--build-deps (formulas nrows ncols hline-positions)
+  "Build dependency graph from FORMULAS alist.
+FORMULAS is an alist of (target . formula-string).
+TARGET is either \"@R$C\" for field formulas or \"$C\" for column formulas.
+NROWS, NCOLS, and HLINE-POSITIONS provide table context for expanding special refs.
+Returns an alist of ((row . col) . list-of-deps)."
+  (let ((deps nil))
+    (dolist (entry formulas)
+      (let* ((target (car entry))
+             (formula (cdr entry))
+             ;; Parse target - only handle @R$C for now
+             (cell (org-table-loeb--parse-cell-ref target)))
+        ;; Only process field formulas (not column formulas) for dependency analysis
+        (when cell
+          (let* ((current-row (car cell))
+                 (current-col (cdr cell))
+                 ;; Build minimal env for preprocessing
+                 (env (list (cons :current-row current-row)
+                            (cons :current-col current-col)
+                            (cons :nrows nrows)
+                            (cons :ncols ncols)
+                            (cons :hline-positions hline-positions)))
+                 ;; Expand special references before parsing dependencies
+                 (expanded-formula
+                  (condition-case nil
+                      (org-table-loeb--preprocess-formula formula env)
+                    (error formula))) ; If preprocessing fails, use original
+                 (cell-deps (org-table-loeb--parse-refs expanded-formula)))
+            (push (cons cell cell-deps) deps)))))
+    deps))
+
+(defun org-table-loeb--tarjan-scc (graph nodes)
+  "Find strongly connected components using Tarjan's algorithm.
+GRAPH is an alist of (node . list-of-neighbors).
+NODES is the list of all nodes.
+Returns a list of SCCs, each SCC being a list of nodes.
+SCCs with more than one node, or a single node with a self-loop, are cycles."
+  (let ((index-counter 0)
+        (stack nil)
+        (on-stack (make-hash-table :test 'equal))
+        (indices (make-hash-table :test 'equal))
+        (lowlinks (make-hash-table :test 'equal))
+        (sccs nil))
+    (cl-labels
+        ((strongconnect (v)
+           (puthash v index-counter indices)
+           (puthash v index-counter lowlinks)
+           (cl-incf index-counter)
+           (push v stack)
+           (puthash v t on-stack)
+           ;; Consider successors of v
+           (dolist (w (cdr (assoc v graph)))
+             (cond
+              ((not (gethash w indices))
+               ;; Successor w has not yet been visited; recurse on it
+               (strongconnect w)
+               (puthash v (min (gethash v lowlinks)
+                               (gethash w lowlinks))
+                        lowlinks))
+              ((gethash w on-stack)
+               ;; Successor w is in stack and hence in the current SCC
+               (puthash v (min (gethash v lowlinks)
+                               (gethash w indices))
+                        lowlinks))))
+           ;; If v is a root node, pop the stack and generate an SCC
+           (when (= (gethash v lowlinks) (gethash v indices))
+             (let (scc w-popped)
+               (cl-loop do (setq w-popped (pop stack))
+                        (remhash w-popped on-stack)
+                        (push w-popped scc)
+                        until (equal w-popped v))
+               (push scc sccs)))))
+      (dolist (v nodes)
+        (unless (gethash v indices)
+          (strongconnect v))))
+    sccs))
+
+(defun org-table-loeb--detect-cycles (deps)
+  "Detect cycles in dependency graph DEPS.
+Returns list of SCCs that represent cycles (size > 1 or self-referencing)."
+  (let* ((nodes (mapcar #'car deps))
+         (sccs (org-table-loeb--tarjan-scc deps nodes)))
+    (cl-remove-if
+     (lambda (scc)
+       (and (= (length scc) 1)
+            ;; Check for self-loop
+            (not (member (car scc)
+                         (cdr (assoc (car scc) deps))))))
+     sccs)))
+
+;;; Formula Preprocessing
+
+(defun org-table-loeb--find-hline-positions (table)
+  "Find positions of hlines in TABLE.
+Returns a list of logical row numbers where hlines occur.
+Row numbers are 1-based and count only data rows (hlines don't count)."
+  (let ((logical-row 0)
+        (hline-positions nil))
+    (dolist (row table)
+      (if (eq row 'hline)
+          (push logical-row hline-positions)
+        (cl-incf logical-row)))
+    (nreverse hline-positions)))
+
+(defun org-table-loeb--expand-hline-refs (formula hline-positions current-row &optional current-col)
+  "Expand hline references (@I, @II, etc.) in FORMULA.
+HLINE-POSITIONS is a list of logical row numbers where hlines occur.
+CURRENT-ROW is used for relative hline references like @-I.
+CURRENT-COL is used to add column specifiers to ranges that lack them.
+
+In ranges like @I..@II:
+- The START of a range means 'first row after that hline'
+- The END of a range means 'last row before that hline' (= the hline position)"
+  (let ((result formula)
+        (case-fold-search nil))
+    (save-match-data
+      ;; Handle @-I, @-II (hlines before current row)
+      (while (string-match "@-\\([I]+\\)" result)
+        (let* ((hline-count (length (match-string 1 result)))
+               ;; Find Nth hline before current row
+               (hlines-before (cl-remove-if (lambda (pos) (>= pos current-row))
+                                            hline-positions))
+               (target-hline (nth (1- hline-count)
+                                  (nreverse hlines-before))))
+          (if target-hline
+              (setq result
+                    (replace-match (format "@%d" target-hline) t t result))
+            (error "Cannot find hline @-%s" (match-string 1 result)))))
+      ;; Handle @+I, @+II (hlines after current row) - same as @I, @II
+      (while (string-match "@\\+\\([I]+\\)" result)
+        (setq result
+              (replace-match (format "@%s" (match-string 1 result)) t t result)))
+      ;; First, handle ranges @X..@Y specially
+      ;; In a range, start means "first row after hline", end means "last row before hline"
+      ;; Column specifiers ($N) are optional - ranges like @I..@II are valid
+      ;; When column is missing, use current-col if provided
+      (while (string-match "@\\([I]+\\)\\([+-][0-9]+\\)?\\(\\$[0-9]+\\)?\\.\\.@\\([I]+\\)\\([+-][0-9]+\\)?\\(\\$[0-9]+\\)?" result)
+        (let* ((start-hline-count (length (match-string 1 result)))
+               (start-offset-str (match-string 2 result))
+               (start-offset (if start-offset-str (string-to-number start-offset-str) 0))
+               (start-col-match (match-string 3 result))
+               (end-hline-count (length (match-string 4 result)))
+               (end-offset-str (match-string 5 result))
+               (end-offset (if end-offset-str (string-to-number end-offset-str) 0))
+               (end-col-match (match-string 6 result))
+               ;; Use matched column or default to current-col
+               (start-col (or start-col-match
+                              (when current-col (format "$%d" current-col))
+                              ""))
+               (end-col (or end-col-match
+                            (when current-col (format "$%d" current-col))
+                            ""))
+               (start-hline (nth (1- start-hline-count) hline-positions))
+               (end-hline (nth (1- end-hline-count) hline-positions))
+               (match-start (match-beginning 0))
+               (match-end (match-end 0)))
+          (if (and start-hline end-hline)
+              ;; Start of range: first row AFTER the hline (+1)
+              ;; End of range: last row BEFORE the hline (= hline position, no +1)
+              (let ((start-row (+ start-hline 1 start-offset))
+                    (end-row (+ end-hline end-offset)))
+                (setq result
+                      (concat (substring result 0 match-start)
+                              (format "@%d%s..@%d%s" start-row start-col end-row end-col)
+                              (substring result match-end))))
+            (error "Cannot find hlines in range"))))
+      ;; Handle remaining standalone @I, @II, @III references (not in ranges)
+      ;; All standalone refs mean "first row after that hline"
+      (while (string-match "@\\([I]+\\)\\([+-][0-9]+\\)?\\(?:\\$\\|[^I0-9+-]\\|$\\)" result)
+        (let* ((hline-count (length (match-string 1 result)))
+               (offset-str (match-string 2 result))
+               (offset (if offset-str (string-to-number offset-str) 0))
+               (target-hline (nth (1- hline-count) hline-positions))
+               ;; Calculate how much of the match to replace (not the trailing char)
+               (match-start (match-beginning 0))
+               (match-len (+ 1 hline-count (if offset-str (length offset-str) 0))))
+          (if target-hline
+              ;; All standalone hline refs: row after hline (+1)
+              (let ((target-row (+ target-hline 1 offset)))
+                (setq result
+                      (concat (substring result 0 match-start)
+                              (format "@%d" target-row)
+                              (substring result (+ match-start match-len)))))
+            (error "Cannot find hline @%s" (match-string 1 result))))))
+    result))
+
+(defun org-table-loeb--expand-first-last-refs (formula nrows ncols)
+  "Expand @<, @>, $<, $> references in FORMULA.
+NROWS and NCOLS are table dimensions.
+Supports multiple < or > for offsets, e.g., @>> means second-to-last row."
+  (let ((result formula)
+        (case-fold-search nil))
+    (save-match-data
+      ;; Replace @<, @<<, etc. and @>, @>>, etc.
+      (while (string-match "\\([@$]\\)\\(<+\\|>+\\)" result)
+        (let* ((char (match-string 1 result))
+               (markers (match-string 2 result))
+               (nmax (if (equal char "@") nrows ncols))
+               (len (length markers))
+               (first-char (string-to-char markers))
+               (n (if (= first-char ?<)
+                      len
+                    (- nmax len -1))))
+          (if (or (< n 1) (> n nmax))
+              (error "Reference \"%s\" in expression \"%s\" points outside table"
+                     (match-string 0 result) result)
+            (setq result
+                  (replace-match (format "%s%d" char n) t t result))))))
+    result))
+
+(defun org-table-loeb--expand-relative-refs (formula current-row current-col nrows ncols)
+  "Expand relative references (@0, @+N, @-N) in FORMULA.
+CURRENT-ROW and CURRENT-COL specify the evaluation context.
+NROWS and NCOLS are table dimensions."
+  (let ((result formula)
+        (case-fold-search nil))
+    (save-match-data
+      ;; Replace @0 followed by non-digit (or end of string)
+      ;; Use lookahead-style matching: match @0 only when followed by $ or non-digit
+      (while (string-match "@0\\(?:\\$\\|[^0-9$]\\|$\\)" result)
+        ;; Only replace the @0 part (2 chars), not the following character
+        (let ((match-start (match-beginning 0)))
+          (setq result
+                (concat (substring result 0 match-start)
+                        (format "@%d" current-row)
+                        (substring result (+ match-start 2))))))
+      ;; Replace @+N and @-N with absolute row numbers
+      ;; Match @+N or @-N followed by $ or non-digit (to avoid matching @-I)
+      (while (string-match "@\\([+-]\\)\\([0-9]+\\)\\(?:\\$\\|[^0-9$]\\|$\\)" result)
+        (let* ((sign (match-string 1 result))
+               (num (match-string 2 result))
+               (offset (string-to-number (concat sign num)))
+               (target-row (+ current-row offset))
+               (match-start (match-beginning 0))
+               ;; The matched part is @, sign, and digits
+               (match-len (+ 1 (length sign) (length num))))
+          (if (or (< target-row 1) (> target-row nrows))
+              (error "Relative reference @%+d from row %d points outside table"
+                     offset current-row)
+            ;; Replace just the @+N or @-N part, not the following character
+            (setq result
+                  (concat (substring result 0 match-start)
+                          (format "@%d" target-row)
+                          (substring result (+ match-start match-len))))))))
+    result))
+
+;;; Formula Evaluation
+
+(defvar org-table-loeb--current-env nil
+  "Current evaluation environment.
+An alist with keys like :table-data, :named-fields, :constants, etc.")
+
+(defun org-table-loeb--substitute-refs (formula env)
+  "Substitute cell references in FORMULA with accessor calls.
+ENV contains the evaluation context.
+Returns a Lisp form that can be evaluated with the matrix bound to `--matrix--'."
+  (let ((result formula)
+        (case-fold-search nil))
+    ;; Replace @R$C with (org-table-loeb--get R C --matrix-- --row-map--)
+    (save-match-data
+      (while (string-match "@\\([0-9]+\\)\\$\\([0-9]+\\)" result)
+        (let ((row (match-string 1 result))
+              (col (match-string 2 result)))
+          (setq result
+                (replace-match
+                 (format "(org-table-loeb--get %s %s --matrix-- --row-map--)" row col)
+                 t t result)))))
+    ;; Replace $C (column reference in current row context)
+    ;; This needs the current row from env
+    (let ((current-row (alist-get :current-row env 1)))
+      (save-match-data
+        (while (string-match "\\$\\([0-9]+\\)" result)
+          (let ((col (match-string 1 result)))
+            (setq result
+                  (replace-match
+                   (format "(org-table-loeb--get %d %s --matrix-- --row-map--)"
+                           current-row col)
+                   t t result))))))
+    result))
+
+(defun org-table-loeb--expand-col-range (start-col end-col row matrix row-map)
+  "Expand a column range from START-COL to END-COL in ROW from MATRIX.
+ROW-MAP maps logical row numbers to matrix indices (0-based).
+Returns a Calc vector string like \"[val1, val2, val3]\"."
+  (let ((values nil)
+        (matrix-idx (gethash row row-map)))
+    (when matrix-idx
+      (let ((matrix-row (nth matrix-idx matrix)))
+        (cl-loop for c from start-col to end-col
+                 do (push (thunk-force (nth (1- c) matrix-row)) values))))
+    (format "[%s]" (mapconcat #'identity (nreverse values) ", "))))
+
+(defun org-table-loeb--expand-row-range (start-row end-row col matrix row-map)
+  "Expand a row range from START-ROW to END-ROW in COL from MATRIX.
+ROW-MAP maps logical row numbers to matrix indices (0-based).
+Returns a Calc vector string like \"[val1, val2, val3]\"."
+  (let ((values nil))
+    (cl-loop for r from start-row to end-row
+             for matrix-idx = (gethash r row-map)
+             when matrix-idx
+             do (let ((matrix-row (nth matrix-idx matrix)))
+                  (push (thunk-force (nth (1- col) matrix-row)) values)))
+    (format "[%s]" (mapconcat #'identity (nreverse values) ", "))))
+
+(defun org-table-loeb--expand-rect-range (start-row start-col end-row end-col matrix row-map)
+  "Expand a rectangular range from (START-ROW,START-COL) to (END-ROW,END-COL).
+ROW-MAP maps logical row numbers to matrix indices (0-based).
+Returns a Calc vector string."
+  (let ((values nil))
+    (cl-loop for r from start-row to end-row
+             for matrix-idx = (gethash r row-map)
+             when matrix-idx
+             do (let ((matrix-row (nth matrix-idx matrix)))
+                  (cl-loop for c from start-col to end-col
+                           do (push (thunk-force (nth (1- c) matrix-row)) values))))
+    (format "[%s]" (mapconcat #'identity (nreverse values) ", "))))
+
+(defun org-table-loeb--substitute-ranges (formula current-row)
+  "Substitute range references in FORMULA.
+CURRENT-ROW is used for column-only ranges.
+Handles: @R$C..@R$C (rect), $C..$C (cols in row), @R$C..@R$C (rows in col)."
+  (let ((result formula))
+    (save-match-data
+      ;; Handle @R1$C1..@R2$C2 (rectangular range)
+      (while (string-match "@\\([0-9]+\\)\\$\\([0-9]+\\)\\.\\.@\\([0-9]+\\)\\$\\([0-9]+\\)" result)
+        (let ((r1 (match-string 1 result))
+              (c1 (match-string 2 result))
+              (r2 (match-string 3 result))
+              (c2 (match-string 4 result)))
+          (setq result
+                (replace-match
+                 (if (string= c1 c2)
+                     ;; Same column = row range
+                     (format "(org-table-loeb--expand-row-range %s %s %s --matrix-- --row-map--)"
+                             r1 r2 c1)
+                   ;; Different columns = rectangular range
+                   (format "(org-table-loeb--expand-rect-range %s %s %s %s --matrix-- --row-map--)"
+                           r1 c1 r2 c2))
+                 t t result))))
+      ;; Handle $N..$M (column range in current row)
+      (while (string-match "\\$\\([0-9]+\\)\\.\\.\\$\\([0-9]+\\)" result)
+        (let ((start-col (match-string 1 result))
+              (end-col (match-string 2 result)))
+          (setq result
+                (replace-match
+                 (format "(org-table-loeb--expand-col-range %s %s %d --matrix-- --row-map--)"
+                         start-col end-col current-row)
+                 t t result)))))
+    result))
+
+(defun org-table-loeb--preprocess-formula (formula env)
+  "Preprocess FORMULA by expanding all special references.
+ENV contains evaluation context including :current-row, :current-col,
+:nrows, :ncols, :hline-positions.
+Returns formula with special references expanded to absolute @R$C form."
+  (let* ((current-row (alist-get :current-row env 1))
+         (current-col (alist-get :current-col env 1))
+         (nrows (alist-get :nrows env))
+         (ncols (alist-get :ncols env))
+         (hline-positions (alist-get :hline-positions env))
+         (result formula))
+    ;; Step 1: Expand hline references (@I, @II, @-I)
+    ;; Pass current-col so ranges without column specifiers get the target column
+    (setq result (org-table-loeb--expand-hline-refs result hline-positions current-row current-col))
+    ;; Step 2: Expand first/last references (@<, @>, $<, $>)
+    (setq result (org-table-loeb--expand-first-last-refs result nrows ncols))
+    ;; Step 3: Expand relative references (@+N, @-N)
+    (setq result (org-table-loeb--expand-relative-refs result current-row current-col nrows ncols))
+    result))
+
+(defun org-table-loeb--make-calc-evaluator (formula env)
+  "Create evaluator function for calc FORMULA with ENV context.
+Returns a closure that takes the final matrix and returns the computed value."
+  (let* ((current-row (alist-get :current-row env 1))
+         (the-row-map (alist-get :row-map env))
+         ;; First preprocess special refs, then expand ranges, then substitute cell refs
+         (preprocessed (org-table-loeb--preprocess-formula formula env))
+         (with-ranges (org-table-loeb--substitute-ranges preprocessed current-row))
+         (substituted (org-table-loeb--substitute-refs with-ranges env)))
+    (lambda (the-matrix)
+      (condition-case err
+          ;; For calc formulas, we need to evaluate and then use calc-eval
+          (let* ((refs-resolved
+                  (with-temp-buffer
+                    (insert substituted)
+                    (goto-char (point-min))
+                    ;; Evaluate Lisp expressions in the formula
+                    ;; We need to pass the matrix and row-map bindings to eval
+                    (while (re-search-forward "(org-table-loeb--\\(get\\|expand-[a-z]+-range\\)[^)]+)" nil t)
+                      (let* ((expr (match-string 0))
+                             ;; Replace placeholders with actual bindings
+                             (fixed-expr (replace-regexp-in-string
+                                          "--row-map--" "the-row-map"
+                                          (replace-regexp-in-string
+                                           "--matrix--" "the-matrix" expr)))
+                             (value (eval (read fixed-expr)
+                                          `((the-matrix . ,the-matrix)
+                                            (the-row-map . ,the-row-map)))))
+                        (replace-match (format "%s" value) t t)))
+                    (buffer-string)))
+                 (result (calc-eval refs-resolved)))
+            ;; calc-eval returns a list (position "error") on parse errors
+            (if (listp result)
+                (format "#ERROR: %s" (cadr result))
+              result))
+        (error
+         (format "#ERROR: %s" (error-message-string err)))))))
+
+(defun org-table-loeb--make-lisp-evaluator (formula env)
+  "Create evaluator function for Lisp FORMULA with ENV context.
+Lisp formulas start with '( and are evaluated directly."
+  (let* ((raw (substring formula 1)) ; Remove leading '
+         (current-row (alist-get :current-row env 1))
+         (current-col (alist-get :current-col env 1))
+         ;; Replace @# with current row number and $# with current col number
+         (with-row-col (replace-regexp-in-string
+                        "\\$#" (number-to-string current-col)
+                        (replace-regexp-in-string
+                         "@#" (number-to-string current-row) raw)))
+         ;; Preprocess special references
+         (preprocessed (org-table-loeb--preprocess-formula with-row-col env))
+         (substituted (org-table-loeb--substitute-refs preprocessed env))
+         ;; Replace --matrix-- and --row-map-- with proper bindings
+         (fixed (replace-regexp-in-string
+                 "--row-map--" "the-row-map"
+                 (replace-regexp-in-string
+                  "--matrix--" "the-matrix" substituted)))
+         (the-row-map (alist-get :row-map env)))
+    (lambda (the-matrix)
+      (condition-case err
+          (format "%s" (eval (read fixed)
+                             `((the-matrix . ,the-matrix)
+                               (the-row-map . ,the-row-map))))
+        (error
+         (format "#ERROR: %s" (error-message-string err)))))))
+
+(defun org-table-loeb--expand-formula-target (target env)
+  "Expand special references in formula TARGET.
+TARGET is a string like \"@>$2\" or \"@I$3\".
+Returns the target with special refs expanded to absolute @R$C."
+  (let ((result target)
+        (nrows (alist-get :nrows env))
+        (ncols (alist-get :ncols env))
+        (hline-positions (alist-get :hline-positions env)))
+    ;; Expand hline references in target
+    (setq result (org-table-loeb--expand-hline-refs result hline-positions 1))
+    ;; Expand first/last references in target
+    (setq result (org-table-loeb--expand-first-last-refs result nrows ncols))
+    result))
+
+(defun org-table-loeb--make-cell-closure (row col value formulas env)
+  "Create closure for cell at ROW, COL with initial VALUE.
+FORMULAS is the alist of all formulas.
+ENV is the evaluation environment.
+Returns a closure of type (matrix -> string)."
+  (let* ((cell-key (format "@%d$%d" row col))
+         (col-key (format "$%d" col))
+         ;; Look up formula, checking expanded targets
+         (formula (or
+                   ;; Direct match first
+                   (cdr (assoc cell-key formulas))
+                   ;; Try expanding each formula target
+                   (cl-loop for (target . expr) in formulas
+                            when (and (string-prefix-p "@" target)
+                                      (string= cell-key
+                                               (org-table-loeb--expand-formula-target target env)))
+                            return expr)
+                   ;; Column formula
+                   (cdr (assoc col-key formulas)))))
+    (if formula
+        (let ((env-with-pos (cons (cons :current-row row)
+                                  (cons (cons :current-col col) env))))
+          (if (string-prefix-p "'(" formula)
+              (org-table-loeb--make-lisp-evaluator formula env-with-pos)
+            (org-table-loeb--make-calc-evaluator formula env-with-pos)))
+      ;; No formula - just return the literal value
+      (lambda (_matrix) value))))
+
+;;; Main Entry Point
+
+(defun org-table-loeb--parse-formulas (tblfm-string)
+  "Parse TBLFM-STRING into an alist of (target . formula).
+TARGET is either \"@R$C\" for field formulas or \"$C\" for column formulas."
+  (let ((formulas nil))
+    (dolist (eq-str (split-string tblfm-string "::" t "[ \t]+"))
+      (when (string-match
+             "\\`\\(@[-+I<>0-9]+\\$[0-9]+\\|\\$[0-9]+\\)[ \t]*=[ \t]*\\(.+\\)"
+             eq-str)
+        (push (cons (match-string 1 eq-str)
+                    (match-string 2 eq-str))
+              formulas)))
+    (nreverse formulas)))
+
+(defun org-table-loeb--table-dimensions (table)
+  "Return (NROWS . NCOLS) for TABLE, excluding hlines."
+  (let ((nrows 0)
+        (ncols 0))
+    (dolist (row table)
+      (unless (eq row 'hline)
+        (cl-incf nrows)
+        (setq ncols (max ncols (length row)))))
+    (cons nrows ncols)))
+
+(defun org-table-loeb--build-row-map (table)
+  "Build a mapping from logical row numbers to matrix indices.
+Returns a hash table where keys are 1-based logical row numbers
+and values are 0-based matrix indices."
+  (let ((row-map (make-hash-table :test 'equal))
+        (logical-row 0)
+        (matrix-idx 0))
+    (dolist (row table)
+      (unless (eq row 'hline)
+        (cl-incf logical-row)
+        (puthash logical-row matrix-idx row-map))
+      (cl-incf matrix-idx))
+    row-map))
+
+(defun org-table-loeb-recalculate (table formulas-alist &optional env)
+  "Recalculate TABLE using loeb-based evaluation.
+TABLE is a list from `org-table-to-lisp'.
+FORMULAS-ALIST is an alist of (target . formula-string).
+ENV is optional evaluation environment.
+
+Returns a new table with all formulas evaluated."
+  (let* ((dims (org-table-loeb--table-dimensions table))
+         (nrows (car dims))
+         (ncols (cdr dims))
+         ;; Build row-map for logical-to-matrix row index mapping
+         (row-map (org-table-loeb--build-row-map table))
+         ;; Find hline positions for @I, @II references
+         (hline-positions (org-table-loeb--find-hline-positions table))
+         ;; Build complete environment with all needed context
+         (env (append
+               (list (cons :row-map row-map)
+                     (cons :nrows nrows)
+                     (cons :ncols ncols)
+                     (cons :hline-positions hline-positions))
+               (or env nil)))
+         ;; Build dependency graph for cycle detection
+         (deps (org-table-loeb--build-deps formulas-alist nrows ncols hline-positions))
+         (cycles (org-table-loeb--detect-cycles deps)))
+
+    ;; Check for cycles
+    (when cycles
+      (user-error "Circular dependency detected in table formulas: %S"
+                  (car cycles)))
+
+    ;; Build matrix of closures
+    (let* ((data-row-idx 0)
+           (closure-matrix
+            (seq-map
+             (lambda (row)
+               (if (eq row 'hline)
+                   'hline
+                 (cl-incf data-row-idx)
+                 (let ((col-idx 0))
+                   (seq-map
+                    (lambda (cell)
+                      (cl-incf col-idx)
+                      (org-table-loeb--make-cell-closure
+                       data-row-idx col-idx cell formulas-alist env))
+                    row))))
+             table)))
+      ;; Apply loeb and return result
+      (org-table-loeb--matrix closure-matrix))))
+
+;;;###autoload
+(defun org-table-recalculate-loeb ()
+  "Recalculate the current table using loeb-based evaluation.
+This is an alternative to `org-table-recalculate' that uses
+lazy evaluation and automatic memoization for efficiency."
+  (interactive)
+  ;; If on a TBLFM line, move up to the table first
+  (beginning-of-line)
+  (while (looking-at "[ \t]*#\\+TBLFM:")
+    (forward-line -1))
+  (unless (org-at-table-p)
+    (user-error "Not at a table"))
+  (let* ((beg (org-table-begin))
+         (end (org-table-end))
+         (table (org-table-to-lisp))
+         ;; Collect ALL #+TBLFM lines following the table
+         (tblfm-lines
+          (save-excursion
+            (goto-char end)
+            (let (lines)
+              (while (looking-at "[ \t]*#\\+TBLFM:[ \t]*\\(.+\\)")
+                (push (match-string-no-properties 1) lines)
+                (forward-line 1))
+              (nreverse lines))))
+         ;; Combine all TBLFM lines (each may have multiple formulas separated by ::)
+         (combined-tblfm (mapconcat #'identity tblfm-lines "::"))
+         (formulas (when (not (string-empty-p combined-tblfm))
+                     (org-table-loeb--parse-formulas combined-tblfm))))
+    (if (null formulas)
+        (message "No formulas to calculate")
+      (let ((result (org-table-loeb-recalculate table formulas)))
+        ;; Render result back to buffer
+        (save-excursion
+          (goto-char beg)
+          (delete-region beg end)
+          (org-table-loeb--render-table result)
+          (org-table-align))
+        (message "Table recalculated using loeb")))))
+
+(defun org-table-loeb--render-table (table)
+  "Render TABLE back to Org format in current buffer."
+  (dolist (row table)
+    (if (eq row 'hline)
+        (insert "|-\n")
+      (insert "|")
+      (dolist (cell row)
+        (insert " " (or cell "") " |"))
+      (insert "\n"))))
+
+(provide 'org-table-loeb)
+
+;;; org-table-loeb.el ends here
diff --git c/lisp/org-table.el w/lisp/org-table.el
index 551e18e9f..c27650925 100644
--- c/lisp/org-table.el
+++ w/lisp/org-table.el
@@ -73,6 +73,7 @@
 (declare-function org-time-string-to-absolute "org" (s &optional daynr prefer buffer pos))
 (declare-function org-time-string-to-time "org" (s))
 (declare-function org-timestamp-up-day "org" (&optional arg))
+(declare-function org-table-recalculate-loeb "org-table-loeb" ())
 
 (defvar constants-unit-system)
 (defvar org-M-RET-may-split-line)
@@ -286,6 +287,22 @@ this line."
   :tag "Org Table Calculation"
   :group 'org-table)
 
+(defcustom org-table-formula-engine 'classic
+  "The engine to use for evaluating table formulas.
+Possible values are:
+`classic'  The traditional iterative evaluation engine (default).
+           Evaluates formulas cell by cell.
+`loeb'     The Löb-based lazy evaluation engine.
+           Computes all cells at once with automatic dependency
+           resolution and memoization.  Provides explicit cycle
+           detection with clear error messages.  May be faster
+           for large tables with complex interdependencies."
+  :group 'org-table-calculation
+  :package-version '(Org . "9.8")
+  :type '(choice
+	  (const :tag "Classic iterative engine" classic)
+	  (const :tag "Löb-based lazy evaluation" loeb)))
+
 (defcustom org-table-use-standard-references 'from
   "Non-nil means using table references like B3 instead of @3$2.
 Possible values are:
@@ -2946,6 +2963,11 @@ known that the table will be realigned a little later anyway."
   (if (or (eq all 'iterate) (equal all '(16)))
       (org-table-iterate)
     (org-table-analyze)
+    ;; Dispatch to loeb engine if configured and recalculating all
+    (if (and (eq org-table-formula-engine 'loeb) all)
+        (progn
+          (require 'org-table-loeb)
+          (org-table-recalculate-loeb))
       (let* ((eqlist (sort (org-table-get-stored-formulas)
 			 (lambda (a b) (string< (car a) (car b)))))
 	   (inhibit-redisplay (not debug-on-error))
@@ -3097,7 +3119,7 @@ existing formula for column %s"
 	    (org-table-message-once-per-second
 	     log-first-time "Re-applying formulas to %d lines... done" cnt)))
 	(org-table-message-once-per-second
-	 (and all log-first-time) "Re-applying formulas... done")))))
+	 (and all log-first-time) "Re-applying formulas... done"))))))
 
 ;;;###autoload
 (defun org-table-iterate (&optional arg)
diff --git c/testing/lisp/test-org-table-loeb.el w/testing/lisp/test-org-table-loeb.el
new file mode 100644
index 000000000..66bae02f5
--- /dev/null
+++ w/testing/lisp/test-org-table-loeb.el
@@ -0,0 +1,370 @@
+;;; test-org-table-loeb.el --- Tests for org-table-loeb.el  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; Author: John Wiegley <[email protected]>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for the loeb-based table calculation engine.
+
+;;; Code:
+
+(require 'org-table-loeb)
+(require 'org-test)
+(require 'org)
+
+;;; Loeb Core Tests
+
+(ert-deftest test-org-table-loeb/matrix-simple ()
+  "Test loeb-matrix with simple constant closures."
+  (let* ((matrix (list (list (lambda (_) "a") (lambda (_) "b"))
+                       (list (lambda (_) "c") (lambda (_) "d"))))
+         (result (org-table-loeb--matrix matrix)))
+    (should (equal result '(("a" "b") ("c" "d"))))))
+
+(ert-deftest test-org-table-loeb/matrix-with-hline ()
+  "Test loeb-matrix handles hlines correctly."
+  (let* ((matrix (list (list (lambda (_) "a"))
+                       'hline
+                       (list (lambda (_) "b"))))
+         (result (org-table-loeb--matrix matrix)))
+    (should (equal result '(("a") hline ("b"))))))
+
+(ert-deftest test-org-table-loeb/matrix-with-refs ()
+  "Test loeb-matrix with inter-cell references."
+  (let* ((row-map (let ((h (make-hash-table :test 'equal)))
+                    (puthash 1 0 h) h))
+         (matrix (list
+                  (list (lambda (_) "10")
+                        (lambda (m) (number-to-string
+                                     (+ 5 (string-to-number
+                                           (org-table-loeb--get 1 1 m row-map))))))))
+         (result (org-table-loeb--matrix matrix)))
+    (should (equal (nth 0 (nth 0 result)) "10"))
+    (should (equal (nth 1 (nth 0 result)) "15"))))
+
+(ert-deftest test-org-table-loeb/matrix-diamond-deps ()
+  "Test loeb-matrix with diamond-shaped dependencies.
+    A
+   / \\
+  B   C
+   \\ /
+    D
+D depends on B and C, which both depend on A."
+  (let* ((row-map (let ((h (make-hash-table :test 'equal)))
+                    (puthash 1 0 h)
+                    (puthash 2 1 h)
+                    h))
+         (matrix (list
+                  ;; Row 1: A=10, B=A+1, C=A+2
+                  (list (lambda (_) "10")
+                        (lambda (m) (number-to-string
+                                     (1+ (string-to-number
+                                          (org-table-loeb--get 1 1 m row-map)))))
+                        (lambda (m) (number-to-string
+                                     (+ 2 (string-to-number
+                                           (org-table-loeb--get 1 1 m row-map))))))
+                  ;; Row 2: D=B+C
+                  (list (lambda (m) (number-to-string
+                                     (+ (string-to-number
+                                         (org-table-loeb--get 1 2 m row-map))
+                                        (string-to-number
+                                         (org-table-loeb--get 1 3 m row-map))))))))
+         (result (org-table-loeb--matrix matrix)))
+    (should (equal (nth 0 (nth 0 result)) "10"))  ; A
+    (should (equal (nth 1 (nth 0 result)) "11"))  ; B
+    (should (equal (nth 2 (nth 0 result)) "12"))  ; C
+    (should (equal (nth 0 (nth 1 result)) "23")))) ; D = B + C
+
+;;; Dependency Parsing Tests
+
+(ert-deftest test-org-table-loeb/parse-refs-simple ()
+  "Test parsing simple cell references."
+  (should (equal (org-table-loeb--parse-refs "@1$1")
+                 '((1 . 1))))
+  (should (equal (org-table-loeb--parse-refs "@2$3+@1$1")
+                 '((2 . 3) (1 . 1))))
+  (should (equal (org-table-loeb--parse-refs "@10$20")
+                 '((10 . 20)))))
+
+(ert-deftest test-org-table-loeb/parse-refs-no-match ()
+  "Test parsing formula with no cell references."
+  (should (equal (org-table-loeb--parse-refs "1+2+3")
+                 nil))
+  (should (equal (org-table-loeb--parse-refs "$1+$2")
+                 nil))) ; Column-only refs not matched by this function
+
+;;; Cycle Detection Tests
+
+(ert-deftest test-org-table-loeb/detect-cycles-none ()
+  "Test cycle detection with no cycles."
+  (let ((deps '(((1 . 1) . nil)
+                ((1 . 2) . ((1 . 1)))
+                ((2 . 1) . ((1 . 1) (1 . 2))))))
+    (should (null (org-table-loeb--detect-cycles deps)))))
+
+(ert-deftest test-org-table-loeb/detect-cycles-self ()
+  "Test detection of self-referencing cycle."
+  (let ((deps '(((1 . 1) . ((1 . 1))))))  ; A depends on A
+    (should (org-table-loeb--detect-cycles deps))))
+
+(ert-deftest test-org-table-loeb/detect-cycles-mutual ()
+  "Test detection of mutual cycle (A->B->A)."
+  (let ((deps '(((1 . 1) . ((1 . 2)))     ; A depends on B
+                ((1 . 2) . ((1 . 1))))))  ; B depends on A
+    (should (org-table-loeb--detect-cycles deps))))
+
+;;; Formula Parsing Tests
+
+(ert-deftest test-org-table-loeb/parse-formulas ()
+  "Test parsing TBLFM line into formula alist."
+  (let ((result (org-table-loeb--parse-formulas
+                 "$1=vmean($2..$3)::@2$1=@1$1+1::$3='(+ 1 2)")))
+    (should (equal (length result) 3))
+    (should (equal (cdr (assoc "$1" result)) "vmean($2..$3)"))
+    (should (equal (cdr (assoc "@2$1" result)) "@1$1+1"))
+    (should (equal (cdr (assoc "$3" result)) "'(+ 1 2)"))))
+
+;;; Integration Tests
+
+(ert-deftest test-org-table-loeb/recalculate-simple ()
+  "Test full recalculation of a simple table."
+  (let* ((table '(("1" "2" "")
+                  ("3" "4" "")))
+         (formulas '(("$3" . "@0$1+@0$2")))  ; Column 3 = col1 + col2
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; Note: This test may need adjustment based on how @0 is handled
+    (should (listp result))
+    (should (= (length result) 2))))
+
+(ert-deftest test-org-table-loeb/recalculate-with-hline ()
+  "Test recalculation preserves hlines."
+  (let* ((table '(("Header")
+                  hline
+                  ("Data")))
+         (formulas nil)
+         (result (org-table-loeb-recalculate table formulas)))
+    (should (equal (nth 0 result) '("Header")))
+    (should (eq (nth 1 result) 'hline))
+    (should (equal (nth 2 result) '("Data")))))
+
+(ert-deftest test-org-table-loeb/recalculate-detects-cycle ()
+  "Test that recalculation detects and reports cycles."
+  (let* ((table '(("1" "2")))
+         (formulas '(("@1$1" . "@1$2")    ; Cell 1 depends on Cell 2
+                     ("@1$2" . "@1$1")))) ; Cell 2 depends on Cell 1
+    (should-error
+     (org-table-loeb-recalculate table formulas)
+     :type 'user-error)))
+
+;;; Range Formula Tests
+
+(ert-deftest test-org-table-loeb/recalculate-col-range ()
+  "Test column range formula $N..$M."
+  (let* ((table '(("a" "10" "20" "")
+                  ("b" "30" "40" "")))
+         (formulas '(("$4" . "vmean($2..$3)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; vmean([10, 20]) = 15, vmean([30, 40]) = 35
+    (should (equal (nth 3 (nth 0 result)) "15"))
+    (should (equal (nth 3 (nth 1 result)) "35"))))
+
+(ert-deftest test-org-table-loeb/recalculate-row-range ()
+  "Test row range formula @R1$C..@R2$C."
+  (let* ((table '(("Header" "Val")
+                  hline
+                  ("A" "10")
+                  ("B" "20")
+                  ("C" "30")
+                  hline
+                  ("Mean" "")))
+         (formulas '(("@5$2" . "vmean(@2$2..@4$2)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; vmean([10, 20, 30]) = 20
+    (should (equal (nth 1 (nth 6 result)) "20"))))
+
+(ert-deftest test-org-table-loeb/recalculate-mixed-ranges ()
+  "Test table with both column and row range formulas (like user's table)."
+  (let* ((table '(("Student" "Maths" "Physics" "Mean")
+                  hline
+                  ("Bertrand" "13" "09" "")
+                  ("Henri" "15" "14" "")
+                  ("Arnold" "17" "13" "")
+                  hline
+                  ("Means" "" "" "")))
+         (formulas '(("$4" . "vmean($2..$3)")
+                     ("@5$2" . "vmean(@2$2..@4$2)")
+                     ("@5$3" . "vmean(@2$3..@4$3)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; Bertrand mean: vmean([13, 9]) = 11
+    (should (equal (nth 3 (nth 2 result)) "11"))
+    ;; Henri mean: vmean([15, 14]) = 14.5
+    (should (equal (nth 3 (nth 3 result)) "14.5"))
+    ;; Arnold mean: vmean([17, 13]) = 15
+    (should (equal (nth 3 (nth 4 result)) "15"))
+    ;; Maths mean: vmean([13, 15, 17]) = 15
+    (should (equal (nth 1 (nth 6 result)) "15"))
+    ;; Physics mean: vmean([9, 14, 13]) = 12
+    (should (equal (nth 2 (nth 6 result)) "12"))))
+
+;;; Special Reference Tests
+
+(ert-deftest test-org-table-loeb/expand-first-last-row ()
+  "Test @< and @> (first and last row) references."
+  (let* ((table '(("a" "10")
+                  ("b" "20")
+                  ("c" "30")
+                  ("sum" "")))
+         ;; Row 3 references first and last data rows
+         ;; Note: Can't use @>$2 in @4$2 formula as that creates circular dep
+         (formulas '(("@3$2" . "@<$2+@>$2")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; @<$2 is row 1 col 2 (10), @>$2 is row 4 col 2 (empty = 0 in calc)
+    ;; Result: 10 + 0 = 10  (or just "10" if empty is treated as 0)
+    (should (equal (nth 1 (nth 0 result)) "10"))   ; Row 1 unchanged
+    (should (equal (nth 1 (nth 1 result)) "20"))   ; Row 2 unchanged
+    (should (equal (nth 0 (nth 3 result)) "sum")))) ; Row 4 label unchanged
+
+(ert-deftest test-org-table-loeb/expand-last-column ()
+  "Test $> (last column) reference."
+  (let* ((table '(("10" "20" "")
+                  ("30" "40" "")))
+         ;; Column 3 = sum of columns 1 and 2
+         (formulas '(("$3" . "$1+$2")))
+         (result (org-table-loeb-recalculate table formulas)))
+    (should (equal (nth 2 (nth 0 result)) "30"))
+    (should (equal (nth 2 (nth 1 result)) "70"))))
+
+(ert-deftest test-org-table-loeb/expand-relative-refs ()
+  "Test @+N and @-N (relative row) references."
+  (let* ((table '(("10")
+                  ("20")
+                  ("")
+                  ("40")))
+         ;; Row 3 = row 2 (previous) + row 4 (next)
+         (formulas '(("@3$1" . "@-1$1+@+1$1")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; @-1 from row 3 is row 2 (20), @+1 is row 4 (40)
+    (should (equal (nth 0 (nth 2 result)) "60"))))
+
+(ert-deftest test-org-table-loeb/expand-hline-refs ()
+  "Test @I, @II (hline position) references."
+  (let* ((table '(("Header")
+                  hline
+                  ("10")
+                  ("20")
+                  hline
+                  ("Sum" "")))
+         ;; Last row col 2: sum from after first hline to before second hline
+         (formulas '(("@4$2" . "vsum(@I$1..@II$1)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; @I is after row 1 (at hline position 1)
+    ;; @II is after row 3 (at hline position 3)
+    ;; vsum from row 2 to row 3: 10+20=30
+    (should (equal (nth 0 (nth 0 result)) "Header"))))
+
+(ert-deftest test-org-table-loeb/expand-hline-with-offset ()
+  "Test @I+N (hline position with offset) references."
+  (let* ((table '(("Header")
+                  hline
+                  ("10")
+                  ("20")
+                  ("30")
+                  hline
+                  ("Result" "")))
+         ;; Get value 2 rows after first hline
+         ;; @I = row 2 (first row after hline), @I+2 = row 4
+         (formulas '(("@5$2" . "@I+2$1")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; @I = row 2 (value "10"), @I+1 = row 3 (value "20"), @I+2 = row 4 (value "30")
+    ;; Result row is at index 6 (after 2 hlines), col 2 should be "30"
+    (should (equal (nth 1 (nth 6 result)) "30"))))
+
+(ert-deftest test-org-table-loeb/lisp-formula-at-hash ()
+  "Test @# (current row number) in Lisp formulas."
+  (let* ((table '(("" "")
+                  ("" "")
+                  ("" "")))
+         ;; Column 1: row number, Column 2: row number * 10
+         (formulas '(("$1" . "'(format \"%d\" @#)")
+                     ("$2" . "'(* @# 10)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; Row 1: col1="1", col2="10"
+    (should (equal (nth 0 (nth 0 result)) "1"))
+    (should (equal (nth 1 (nth 0 result)) "10"))
+    ;; Row 2: col1="2", col2="20"
+    (should (equal (nth 0 (nth 1 result)) "2"))
+    (should (equal (nth 1 (nth 1 result)) "20"))
+    ;; Row 3: col1="3", col2="30"
+    (should (equal (nth 0 (nth 2 result)) "3"))
+    (should (equal (nth 1 (nth 2 result)) "30"))))
+
+(ert-deftest test-org-table-loeb/lisp-formula-dollar-hash ()
+  "Test $# (current column number) in Lisp formulas."
+  (let* ((table '(("" "" "")))
+         ;; Each column shows its own column number
+         (formulas '(("@1$1" . "'(format \"col%d\" $#)")
+                     ("@1$2" . "'(format \"col%d\" $#)")
+                     ("@1$3" . "'(format \"col%d\" $#)")))
+         (result (org-table-loeb-recalculate table formulas)))
+    (should (equal (nth 0 (nth 0 result)) "col1"))
+    (should (equal (nth 1 (nth 0 result)) "col2"))
+    (should (equal (nth 2 (nth 0 result)) "col3"))))
+
+(ert-deftest test-org-table-loeb/complex-special-refs ()
+  "Test combination of special references in one formula."
+  (let* ((table '(("Student" "Score")
+                  hline
+                  ("Alice" "85")
+                  ("Bob" "92")
+                  ("Carol" "78")
+                  hline
+                  ("Average" "")
+                  ("Max" "")
+                  ("Min" "")))
+         (formulas '(("@5$2" . "vmean(@I$2..@II$2)")   ; Average
+                     ("@6$2" . "vmax(@I$2..@II$2)")     ; Max
+                     ("@7$2" . "vmin(@I$2..@II$2)")))   ; Min
+         (result (org-table-loeb-recalculate table formulas)))
+    ;; Average of 85, 92, 78
+    (should (equal (nth 1 (nth 6 result)) "85"))
+    ;; Max
+    (should (equal (nth 1 (nth 7 result)) "92"))
+    ;; Min
+    (should (equal (nth 1 (nth 8 result)) "78"))))
+
+;;; Interactive Tests
+
+(ert-deftest test-org-table-loeb/interactive-recalc ()
+  "Test interactive recalculation in a buffer."
+  (skip-unless (fboundp 'org-element-cache-reset))
+  (org-test-with-temp-text
+      "| a | b |   |
+| 1 | 2 |   |
+| 3 | 4 |   |
+#+TBLFM: $3=$1+$2
+<point>"
+    (goto-char (point-min))
+    (forward-line 1) ; Move to data line
+    ;; Just verify we can call it without error on a table with formulas
+    (should (org-at-table-p))))
+
+(provide 'test-org-table-loeb)
+
+;;; test-org-table-loeb.el ends here
-- 
John Wiegley                  GPG fingerprint = 4710 CF98 AF9B 327B B80F
http://newartisans.com                          60E1 46C4 BD1A 7AC1 4BA2

Reply via email to