Scope
Ship the minimum useful formula engine end-to-end. Goal: a user can write a TBLFM comment under a table, hit <localleader>tF=, and see values populate correctly. Opt-in via table.formulas.enabled = true.
This scope was tightened after a design-review pass that surfaced a latent parser bug, ambiguity in the formula clause separator, and unsound fixed-point semantics. See "Design review revisions" below.
In scope
- Parser fix (latent bug): tighten
lua/markdown-plus/table/parser.lua is_table_row to exclude HTML comments. Without this, the new TBLFM comment line would be slurped into the table and corrupted by format.format_table().
- TBLFM annotation parser/writer: read
<!-- TBLFM: ... --> immediately after a table (first non-empty line at matching indentation; multi-line comments rejected in Phase 1). Smart clause splitter on top-level | only (respects parens, strings if we ever add them).
- Lexer + Pratt parser for expression grammar.
- Reference resolver:
$N positional (1-indexed)
$Name header-name, normalized via lower() + non-alphanumerics collapsed to _. Duplicate normalized headers produce #REF! for that reference with a warning listing the colliding headers. Empty/punctuation-only headers are positionally addressable only.
@N row (org-mode: @1 header, @2 first data row; separator unaddressable)
@N$M cell
- Vertical ranges
@a$c..@b$c and horizontal ranges $colA..$colB. Ranges that include @1 return #RANGE!. Reversed ranges normalized when axis is obvious, else #RANGE!. Single-cell ranges valid. Empty ranges: vsum/vcount/vcounta return 0; vmean/vmin/vmax return #ERROR.
- Conservative numeric coercion: trim whitespace; accept
123, 123.45, .5, 1.2e5; accept comma thousands when grouped correctly (1,200, 12,345); reject malformed (12,34). No currency, no percent. Document the rule.
- Function library (numeric-only MVP):
vsum, vmean, vmin, vmax, vcount (numeric only), vcounta (non-empty), round, floor, ceil, abs, if(cond, a, b). Constants pi, e.
- Operators: arithmetic
+ - * / % ^, comparison < <= > >= == != (return 1/0).
- Target-cell precomputation: before evaluation, compile annotations into a
target_cell -> formula map. Column formulas expand to per-row targets; specific-cell formulas overwrite that map. Duplicate ownerships emit a warning.
- Dependency-graph evaluation: build dep graph from compiled targets, topologically sort, evaluate in order. Strongly-connected components (true cycles) produce
#CYCLE! on participating cells. No max_iterations knob.
- Error sentinels (in-cell, round-trip safe):
#REF! (unknown ref / header collision), #DIV0!, #CYCLE!, #RANGE! (range crosses header / reversed / invalid), #ERROR (generic).
- Operations guards (deferred-to-Phase-2 work pulled forward): when the table has a TBLFM annotation, sort/transpose/insert/delete row/insert/delete column/move row/move column prompt the user (default No) before proceeding. Without this, those ops would silently corrupt formula references.
- On-demand recalc command +
<Plug>(MarkdownPlusTableFormulaRecalc) + default <localleader>tF=. All formula features sit under the <localleader>tF (capital F) sub-prefix to avoid colliding with the existing <localleader>tf (TableFormat) default.
- Config wiring:
table.formulas.{enabled}. Default enabled = false. Update types, validator, README, vimdoc, tests (the six-file rule).
- Tests: separate spec files per concern. Mirror the
footnotes/ test split:
table_formula_lexer_spec.lua
table_formula_parser_spec.lua
table_formula_refs_spec.lua
table_formula_functions_spec.lua
table_formula_evaluator_spec.lua
table_formula_annotation_spec.lua
table_formula_coercion_spec.lua
table_formula_dependency_spec.lua
table_formula_operations_guard_spec.lua
table_formula_spec.lua — end-to-end integration
- Also extend
table_spec.lua to verify the new is_table_row HTML-comment exclusion doesn't regress existing tables.
Out of scope (deferred to Phase 2+)
- String values + string comparison (
== on text cells)
- Logical operators
&&, ||, ! (would conflict with | clause separator)
nan constant
- Format directives (
;%.2f)
- Extmark indicators on formula cells
auto_recalc = "save"
- Numeric-ref auto-rewrite on row/column ops (Phase 1 only guards, doesn't rewrite)
- Quickfix list population
- Float-window formula editor
- Interactive formula authoring command
- Currency / percent / locale-aware coercion
Design review revisions (locked)
These resolve specific risks identified in the design-review pass. They supersede the looser language in the original design plan.
- Parser fix is a prerequisite, not optional. Without HTML-comment exclusion in
is_table_row, TBLFM comments would be parsed as table data.
- Clause separator is
|. Logical OR/AND/NOT are deferred to avoid a smart-splitter requirement in Phase 1.
- Evaluation is dependency-graph with topo sort + SCC detection, not fixed-point iteration. No
max_iterations.
- Target-cell precomputation before evaluation. Specific-cell formulas don't compete with column formulas at evaluation time.
- Duplicate normalized headers →
#REF! with warning, not last-wins.
- Annotation locality: first non-empty line after the table at matching indentation. Single occurrence. Multi-line rejected.
- Existing destructive table operations must guard when TBLFM is present. Pulled into Phase 1 from Phase 2.
Acceptance criteria
- Existing table behavior unchanged when
table.formulas.enabled = false (the default).
- HTML comments inside or immediately after tables no longer corrupt parsing (verified by added test).
- Scenarios 1, 2, 3, 5, 6 from the design doc work end-to-end (invoice, sprint, grades, A/B matrix, habit tracker).
- Sorting/transposing/inserting/deleting on a formula-bearing table prompts before proceeding.
- All listed spec files exist and pass.
make check passes.
- README and vimdoc include a "Table Formulas" section with at least one worked example and the locked semantics (numeric-only, header normalization rule, error sentinels).
Scope
Ship the minimum useful formula engine end-to-end. Goal: a user can write a TBLFM comment under a table, hit
<localleader>tF=, and see values populate correctly. Opt-in viatable.formulas.enabled = true.This scope was tightened after a design-review pass that surfaced a latent parser bug, ambiguity in the formula clause separator, and unsound fixed-point semantics. See "Design review revisions" below.
In scope
lua/markdown-plus/table/parser.luais_table_rowto exclude HTML comments. Without this, the new TBLFM comment line would be slurped into the table and corrupted byformat.format_table().<!-- TBLFM: ... -->immediately after a table (first non-empty line at matching indentation; multi-line comments rejected in Phase 1). Smart clause splitter on top-level|only (respects parens, strings if we ever add them).$Npositional (1-indexed)$Nameheader-name, normalized vialower()+ non-alphanumerics collapsed to_. Duplicate normalized headers produce#REF!for that reference with a warning listing the colliding headers. Empty/punctuation-only headers are positionally addressable only.@Nrow (org-mode:@1header,@2first data row; separator unaddressable)@N$Mcell@a$c..@b$cand horizontal ranges$colA..$colB. Ranges that include@1return#RANGE!. Reversed ranges normalized when axis is obvious, else#RANGE!. Single-cell ranges valid. Empty ranges:vsum/vcount/vcountareturn0;vmean/vmin/vmaxreturn#ERROR.123,123.45,.5,1.2e5; accept comma thousands when grouped correctly (1,200,12,345); reject malformed (12,34). No currency, no percent. Document the rule.vsum,vmean,vmin,vmax,vcount(numeric only),vcounta(non-empty),round,floor,ceil,abs,if(cond, a, b). Constantspi,e.+ - * / % ^, comparison< <= > >= == !=(return1/0).target_cell -> formulamap. Column formulas expand to per-row targets; specific-cell formulas overwrite that map. Duplicate ownerships emit a warning.#CYCLE!on participating cells. Nomax_iterationsknob.#REF!(unknown ref / header collision),#DIV0!,#CYCLE!,#RANGE!(range crosses header / reversed / invalid),#ERROR(generic).<Plug>(MarkdownPlusTableFormulaRecalc)+ default<localleader>tF=. All formula features sit under the<localleader>tF(capital F) sub-prefix to avoid colliding with the existing<localleader>tf(TableFormat) default.table.formulas.{enabled}. Defaultenabled = false. Update types, validator, README, vimdoc, tests (the six-file rule).footnotes/test split:table_formula_lexer_spec.luatable_formula_parser_spec.luatable_formula_refs_spec.luatable_formula_functions_spec.luatable_formula_evaluator_spec.luatable_formula_annotation_spec.luatable_formula_coercion_spec.luatable_formula_dependency_spec.luatable_formula_operations_guard_spec.luatable_formula_spec.lua— end-to-end integrationtable_spec.luato verify the newis_table_rowHTML-comment exclusion doesn't regress existing tables.Out of scope (deferred to Phase 2+)
==on text cells)&&,||,!(would conflict with|clause separator)nanconstant;%.2f)auto_recalc = "save"Design review revisions (locked)
These resolve specific risks identified in the design-review pass. They supersede the looser language in the original design plan.
is_table_row, TBLFM comments would be parsed as table data.|. Logical OR/AND/NOT are deferred to avoid a smart-splitter requirement in Phase 1.max_iterations.#REF!with warning, not last-wins.Acceptance criteria
table.formulas.enabled = false(the default).make checkpasses.