f8-engine 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {f8_engine-0.1.0 → f8_engine-0.2.0}/Cargo.lock +2 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/Cargo.toml +1 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/PKG-INFO +18 -16
- {f8_engine-0.1.0 → f8_engine-0.2.0}/README.md +17 -15
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/compiled/mod.rs +12 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/compiled/model.rs +10 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/emission/format.rs +45 -11
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/emission/workbook.rs +43 -47
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/cell.rs +11 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/error.rs +12 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/expand.rs +38 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/layout/mod.rs +22 -8
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/error.rs +3 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/format.rs +33 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/mod.rs +1 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/table.rs +114 -8
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/theme.rs +9 -3
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-python/src/error.rs +3 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-python/src/lib.rs +8 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/__init__.py +2 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/_core.pyi +3 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/audit.py +10 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/builder.py +49 -1
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/cli.py +54 -4
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/skills/f8-modeling/SKILL.md +2 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/skills/f8-modeling/references/auditing-and-repair.md +4 -3
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/skills/f8-modeling/references/syntax-and-grid.md +70 -7
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/types.py +13 -2
- {f8_engine-0.1.0 → f8_engine-0.2.0}/LICENSE +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/Cargo.toml +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/compiled/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/emission/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/emission/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/eval/context.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/eval/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/eval/expr.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/eval/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/eval/model.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/graph/cycle.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/graph/dependency.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/graph/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/graph/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/graph/resolve.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/id.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/ir/symbol.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/layout/a1.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/layout/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/lowering/address.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/lowering/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/lowering/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/compiler/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/expr/ast.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/expr/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/expr/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/expr/parser.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/common.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/date/constructors.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/date/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/date/parts.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/date/serial.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/date/yearfrac.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/financial/annuity.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/financial/discounting.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/financial/irr.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/financial/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/logical/boolean.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/logical/branching.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/logical/error_handling.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/logical/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/lookup/index.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/lookup/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/lookup/search.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/lookup/xlookup.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/lookup/xmatch.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/aggregation.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/basic.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/conditional.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/criteria.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/product.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/math/rounding.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/registry.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/functions/traits.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/lib.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/id.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/model/manifest.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/value/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/value/format.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-core/src/value/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-python/Cargo.toml +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/crates/f8-python/src/model.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/pyproject.toml +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/__main__.py +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/py.typed +0 -0
- {f8_engine-0.1.0 → f8_engine-0.2.0}/python/f8/skills/f8-modeling/references/functions.md +0 -0
|
@@ -37,7 +37,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "f8-core"
|
|
40
|
-
version = "0.
|
|
40
|
+
version = "0.2.0"
|
|
41
41
|
dependencies = [
|
|
42
42
|
"indexmap",
|
|
43
43
|
"petgraph",
|
|
@@ -50,7 +50,7 @@ dependencies = [
|
|
|
50
50
|
|
|
51
51
|
[[package]]
|
|
52
52
|
name = "f8-python"
|
|
53
|
-
version = "0.
|
|
53
|
+
version = "0.2.0"
|
|
54
54
|
dependencies = [
|
|
55
55
|
"f8-core",
|
|
56
56
|
"pyo3",
|
|
@@ -3,7 +3,7 @@ resolver = "3"
|
|
|
3
3
|
members = ["crates/f8-core", "crates/f8-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.2.0"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
description = "Financial modeling engine with compiler verification and live Excel formulas"
|
|
9
9
|
authors = ["Michael (Chencheng) Hu, CFA"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: f8-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
@@ -28,25 +28,25 @@ Project-URL: Repository, https://github.com/mhcfa/f8
|
|
|
28
28
|
|
|
29
29
|
Financial modeling engine with compiler verification and live Excel formulas.
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Who is f8 for
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
1. **People who want AI to reliably build complex Excel models (financial and beyond)**: Give AI agents a semantic 2D DSL and compile-time verification to produce clean and error-free `.xlsx` workbooks with live formulas.
|
|
34
|
+
2. **People doing financial analysis in pure code (no Excel required)**: Run row-by-row financial statements, corkscrew schedules, and cross-table dependency graphs in Python (where columnar DataFrames fall short), with `compiled.save("model.xlsx")` ready whenever a stakeholder asks for the spreadsheet.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
## Why f8
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
I am a CFA charterholder working in private M&A and project finance, where complex Excel models remain the non-negotiable deliverable despite fragile manual work, silent copy-paste errors, and zero version control.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
- No verification loop: Standard spreadsheet libraries do not evaluate formulas, so an agent has to inspect the `.xlsx` file just to get feedback.
|
|
40
|
+
AI agents struggle to manipulate raw spreadsheets (`openpyxl`, Office.js, or GUI) for two reasons:
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
- Brittle coordinates: Agents must reason in spatial cell addresses (`C42:G42`) instead of domain semantics; inserting one row shifts every downstream reference.
|
|
43
|
+
- No verification loop: Spreadsheet libraries do not evaluate formulas, leaving agents blind until the `.xlsx` file is opened and inspected.
|
|
43
44
|
|
|
44
|
-
`f8`
|
|
45
|
+
`f8` turns the `.xlsx` workbook into a compiled artifact rather than a hand-edited canvas:
|
|
45
46
|
|
|
46
|
-
- Semantic 2D modeling: Author models
|
|
47
|
-
- Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or
|
|
48
|
-
- Closed-loop
|
|
49
|
-
- End-to-end fundamental workflow in code: Keep data ingestion, domain modeling, automated auditing, and live `.xlsx` emission in version-controlled code with full AI agent assistance.
|
|
47
|
+
- Semantic 2D modeling: Author models with readable table, row, and column identifiers; `f8` handles spatial layout and lowers logic into native Excel formulas.
|
|
48
|
+
- Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or formula overlaps) and enforces a strict DAG via Tarjan cycle detection.
|
|
49
|
+
- Closed-loop verification: Evaluates formulas in memory with 100% Excel parity (including the 1900 leap day bug) so AI agents and `pytest` suites can audit tables, trace dependencies, and verify accounting checks in milliseconds.
|
|
50
50
|
|
|
51
51
|
## Getting started
|
|
52
52
|
|
|
@@ -62,6 +62,7 @@ If you are working with an AI coding agent, `f8` ships with a bundled modeling s
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
f8 agent init # scaffolds .agents/skills/f8-modeling/
|
|
65
|
+
f8 theme init # generates a customizable theme.json in the workspace root
|
|
65
66
|
f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
66
67
|
```
|
|
67
68
|
|
|
@@ -100,7 +101,7 @@ compiled = model.compile()
|
|
|
100
101
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
101
102
|
print(compiled.table("PnL").to_markdown())
|
|
102
103
|
|
|
103
|
-
# Emit a live .xlsx workbook with native Excel formulas and styling
|
|
104
|
+
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
104
105
|
compiled.save("forecast.xlsx")
|
|
105
106
|
```
|
|
106
107
|
|
|
@@ -150,9 +151,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
|
|
|
150
151
|
`Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
|
|
151
152
|
|
|
152
153
|
- Architecture overview: `docs/architecture.md`
|
|
153
|
-
- 2D matrix DSL reference: `docs/dsl/spec-format.md`
|
|
154
|
-
- Standard function catalog: `docs/functions/catalog.md`
|
|
155
154
|
- Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
|
|
155
|
+
- Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
|
|
156
|
+
- Function reference: `python/f8/skills/f8-modeling/references/functions.md`
|
|
157
|
+
- Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
|
|
156
158
|
|
|
157
159
|
## License
|
|
158
160
|
|
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Financial modeling engine with compiler verification and live Excel formulas.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Who is f8 for
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. **People who want AI to reliably build complex Excel models (financial and beyond)**: Give AI agents a semantic 2D DSL and compile-time verification to produce clean and error-free `.xlsx` workbooks with live formulas.
|
|
8
|
+
2. **People doing financial analysis in pure code (no Excel required)**: Run row-by-row financial statements, corkscrew schedules, and cross-table dependency graphs in Python (where columnar DataFrames fall short), with `compiled.save("model.xlsx")` ready whenever a stakeholder asks for the spreadsheet.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
## Why f8
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
I am a CFA charterholder working in private M&A and project finance, where complex Excel models remain the non-negotiable deliverable despite fragile manual work, silent copy-paste errors, and zero version control.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- No verification loop: Standard spreadsheet libraries do not evaluate formulas, so an agent has to inspect the `.xlsx` file just to get feedback.
|
|
14
|
+
AI agents struggle to manipulate raw spreadsheets (`openpyxl`, Office.js, or GUI) for two reasons:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- Brittle coordinates: Agents must reason in spatial cell addresses (`C42:G42`) instead of domain semantics; inserting one row shifts every downstream reference.
|
|
17
|
+
- No verification loop: Spreadsheet libraries do not evaluate formulas, leaving agents blind until the `.xlsx` file is opened and inspected.
|
|
17
18
|
|
|
18
|
-
`f8`
|
|
19
|
+
`f8` turns the `.xlsx` workbook into a compiled artifact rather than a hand-edited canvas:
|
|
19
20
|
|
|
20
|
-
- Semantic 2D modeling: Author models
|
|
21
|
-
- Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or
|
|
22
|
-
- Closed-loop
|
|
23
|
-
- End-to-end fundamental workflow in code: Keep data ingestion, domain modeling, automated auditing, and live `.xlsx` emission in version-controlled code with full AI agent assistance.
|
|
21
|
+
- Semantic 2D modeling: Author models with readable table, row, and column identifiers; `f8` handles spatial layout and lowers logic into native Excel formulas.
|
|
22
|
+
- Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or formula overlaps) and enforces a strict DAG via Tarjan cycle detection.
|
|
23
|
+
- Closed-loop verification: Evaluates formulas in memory with 100% Excel parity (including the 1900 leap day bug) so AI agents and `pytest` suites can audit tables, trace dependencies, and verify accounting checks in milliseconds.
|
|
24
24
|
|
|
25
25
|
## Getting started
|
|
26
26
|
|
|
@@ -36,6 +36,7 @@ If you are working with an AI coding agent, `f8` ships with a bundled modeling s
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
f8 agent init # scaffolds .agents/skills/f8-modeling/
|
|
39
|
+
f8 theme init # generates a customizable theme.json in the workspace root
|
|
39
40
|
f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -74,7 +75,7 @@ compiled = model.compile()
|
|
|
74
75
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
75
76
|
print(compiled.table("PnL").to_markdown())
|
|
76
77
|
|
|
77
|
-
# Emit a live .xlsx workbook with native Excel formulas and styling
|
|
78
|
+
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
78
79
|
compiled.save("forecast.xlsx")
|
|
79
80
|
```
|
|
80
81
|
|
|
@@ -124,9 +125,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
|
|
|
124
125
|
`Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
|
|
125
126
|
|
|
126
127
|
- Architecture overview: `docs/architecture.md`
|
|
127
|
-
- 2D matrix DSL reference: `docs/dsl/spec-format.md`
|
|
128
|
-
- Standard function catalog: `docs/functions/catalog.md`
|
|
129
128
|
- Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
|
|
129
|
+
- Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
|
|
130
|
+
- Function reference: `python/f8/skills/f8-modeling/references/functions.md`
|
|
131
|
+
- Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
|
|
130
132
|
|
|
131
133
|
## License
|
|
132
134
|
|
|
@@ -277,6 +277,7 @@ impl CompiledModel {
|
|
|
277
277
|
decimals: cell_node.decimals,
|
|
278
278
|
currency: cell_node.currency,
|
|
279
279
|
is_highlight: cell_node.is_highlight,
|
|
280
|
+
shade: cell_node.shade,
|
|
280
281
|
indent: cell_node.indent,
|
|
281
282
|
is_formula,
|
|
282
283
|
formula_expr,
|
|
@@ -309,17 +310,21 @@ impl CompiledModel {
|
|
|
309
310
|
.map(|col| match col {
|
|
310
311
|
crate::model::table::ColumnSpec::Data(d) => ColumnInspection {
|
|
311
312
|
id: Some(d.id.clone()),
|
|
313
|
+
group: d.group.clone(),
|
|
312
314
|
headers: d.headers.clone(),
|
|
313
315
|
is_gutter: false,
|
|
314
316
|
is_descriptor: d.role.is_descriptor(),
|
|
315
317
|
style: Some(d.style.unwrap_or(SemanticStyle::Data)),
|
|
318
|
+
shade: d.shade.filter(|s| !s.is_none()),
|
|
316
319
|
},
|
|
317
320
|
crate::model::table::ColumnSpec::Gutter => ColumnInspection {
|
|
318
321
|
id: None,
|
|
322
|
+
group: None,
|
|
319
323
|
headers: Vec::new(),
|
|
320
324
|
is_gutter: true,
|
|
321
325
|
is_descriptor: false,
|
|
322
326
|
style: None,
|
|
327
|
+
shade: None,
|
|
323
328
|
},
|
|
324
329
|
})
|
|
325
330
|
.collect();
|
|
@@ -346,6 +351,7 @@ impl CompiledModel {
|
|
|
346
351
|
is_gutter: false,
|
|
347
352
|
indent,
|
|
348
353
|
style: Some(d.style.unwrap_or(SemanticStyle::Data)),
|
|
354
|
+
shade: d.shade.filter(|s| !s.is_none()),
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
357
|
crate::model::table::RowSpec::SectionHeader(s) => RowInspection {
|
|
@@ -355,6 +361,7 @@ impl CompiledModel {
|
|
|
355
361
|
is_gutter: false,
|
|
356
362
|
indent: s.indent.unwrap_or(0),
|
|
357
363
|
style: None,
|
|
364
|
+
shade: None,
|
|
358
365
|
},
|
|
359
366
|
crate::model::table::RowSpec::Gutter => RowInspection {
|
|
360
367
|
id: None,
|
|
@@ -363,6 +370,7 @@ impl CompiledModel {
|
|
|
363
370
|
is_gutter: true,
|
|
364
371
|
indent: 0,
|
|
365
372
|
style: None,
|
|
373
|
+
shade: None,
|
|
366
374
|
},
|
|
367
375
|
})
|
|
368
376
|
.collect();
|
|
@@ -641,8 +649,10 @@ impl CompiledModel {
|
|
|
641
649
|
}
|
|
642
650
|
ColumnSpec::Data(d) => {
|
|
643
651
|
let k_len = d.headers.len();
|
|
644
|
-
let
|
|
645
|
-
|
|
652
|
+
let first_hdr_tier = total_tiers.saturating_sub(k_len);
|
|
653
|
+
let mut header_text = if t == 0 && d.group.is_some() {
|
|
654
|
+
d.group.clone()
|
|
655
|
+
} else if let Some(k) = t.checked_sub(first_hdr_tier) {
|
|
646
656
|
d.headers.get(k).cloned()
|
|
647
657
|
} else {
|
|
648
658
|
None
|
|
@@ -4,7 +4,7 @@ use indexmap::IndexMap;
|
|
|
4
4
|
use serde::{Deserialize, Serialize};
|
|
5
5
|
|
|
6
6
|
use crate::compiler::ir::id::CellId;
|
|
7
|
-
use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle};
|
|
7
|
+
use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle, Shade};
|
|
8
8
|
use crate::model::id::{ColId, RowId, SheetId, TableId};
|
|
9
9
|
use crate::value::CellValue;
|
|
10
10
|
|
|
@@ -81,6 +81,8 @@ pub struct CellInspection {
|
|
|
81
81
|
pub currency: Option<CurrencyCode>,
|
|
82
82
|
/// Visual highlight emphasis flag.
|
|
83
83
|
pub is_highlight: bool,
|
|
84
|
+
/// Semantic background shade if assigned.
|
|
85
|
+
pub shade: Option<Shade>,
|
|
84
86
|
/// Descriptor text indentation level.
|
|
85
87
|
pub indent: u8,
|
|
86
88
|
/// `true` if this cell is calculated by formula, `false` if constant literal.
|
|
@@ -177,7 +179,9 @@ where
|
|
|
177
179
|
pub struct ColumnInspection {
|
|
178
180
|
/// Column identifier if this is a data column.
|
|
179
181
|
pub id: Option<ColId>,
|
|
180
|
-
///
|
|
182
|
+
/// Horizontal spanning group banner label if declared.
|
|
183
|
+
pub group: Option<String>,
|
|
184
|
+
/// Per-column stacked header labels.
|
|
181
185
|
pub headers: Vec<String>,
|
|
182
186
|
/// `true` if this column is a blank spacing gutter.
|
|
183
187
|
pub is_gutter: bool,
|
|
@@ -185,6 +189,8 @@ pub struct ColumnInspection {
|
|
|
185
189
|
pub is_descriptor: bool,
|
|
186
190
|
/// Structural presentation style if this is a data column.
|
|
187
191
|
pub style: Option<SemanticStyle>,
|
|
192
|
+
/// Semantic background shade if this is a data column.
|
|
193
|
+
pub shade: Option<Shade>,
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
/// Row inspection metadata.
|
|
@@ -202,6 +208,8 @@ pub struct RowInspection {
|
|
|
202
208
|
pub indent: u8,
|
|
203
209
|
/// Structural presentation style if this is a calculable data row.
|
|
204
210
|
pub style: Option<SemanticStyle>,
|
|
211
|
+
/// Semantic background shade if this is a calculable data row.
|
|
212
|
+
pub shade: Option<Shade>,
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
/// Cell-level audit information within a table inspection view.
|
|
@@ -5,7 +5,7 @@ use rust_xlsxwriter::{Color, Format, FormatAlign, FormatBorder, FormatUnderline}
|
|
|
5
5
|
|
|
6
6
|
use super::error::EmissionError;
|
|
7
7
|
use crate::compiler::layout::StyleRole;
|
|
8
|
-
use crate::model::format::{CurrencyCode, FormatPreset};
|
|
8
|
+
use crate::model::format::{CurrencyCode, FormatPreset, Shade};
|
|
9
9
|
use crate::model::theme::{BorderStyle, PaletteConfig, Theme};
|
|
10
10
|
|
|
11
11
|
/// Parses a hexadecimal color string (`#RGB`, `#RRGGBB`, or `#RRGGBBAA`) into a `rust_xlsxwriter::Color`.
|
|
@@ -94,7 +94,9 @@ struct ParsedPalette {
|
|
|
94
94
|
total_text: Color,
|
|
95
95
|
input_text: Color,
|
|
96
96
|
formula_text: Color,
|
|
97
|
-
|
|
97
|
+
shade_muted_fill: Option<Color>,
|
|
98
|
+
shade_accent_fill: Option<Color>,
|
|
99
|
+
shade_alt_fill: Option<Color>,
|
|
98
100
|
highlight_fill: Option<Color>,
|
|
99
101
|
highlight_text: Color,
|
|
100
102
|
gridline_color: Color,
|
|
@@ -122,7 +124,9 @@ impl ParsedPalette {
|
|
|
122
124
|
total_text: parse_hex_color("total_text", &p.total_text)?,
|
|
123
125
|
input_text: parse_hex_color("input_text", &p.input_text)?,
|
|
124
126
|
formula_text: parse_hex_color("formula_text", &p.formula_text)?,
|
|
125
|
-
|
|
127
|
+
shade_muted_fill: parse_fill_color("shade_muted_fill", &p.shade_muted_fill)?,
|
|
128
|
+
shade_accent_fill: parse_fill_color("shade_accent_fill", &p.shade_accent_fill)?,
|
|
129
|
+
shade_alt_fill: parse_fill_color("shade_alt_fill", &p.shade_alt_fill)?,
|
|
126
130
|
highlight_fill: parse_fill_color("highlight_fill", &p.highlight_fill)?,
|
|
127
131
|
highlight_text: parse_hex_color("highlight_text", &p.highlight_text)?,
|
|
128
132
|
gridline_color: parse_hex_color("gridline_color", &p.gridline_color)?,
|
|
@@ -184,6 +188,8 @@ pub struct FormatCache {
|
|
|
184
188
|
title_format: Format,
|
|
185
189
|
subtitle_format: Format,
|
|
186
190
|
super_header_format: Format,
|
|
191
|
+
col_header_upper_descriptor_format: Format,
|
|
192
|
+
col_header_upper_measure_format: Format,
|
|
187
193
|
col_header_descriptor_format: Format,
|
|
188
194
|
col_header_measure_format: Format,
|
|
189
195
|
section_header_format: Format,
|
|
@@ -239,15 +245,17 @@ impl FormatCache {
|
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
// 4. Column Header (Descriptor - Left Aligned)
|
|
242
|
-
let mut
|
|
248
|
+
let mut col_header_upper_descriptor_format = Format::new()
|
|
243
249
|
.set_font_name(&theme.typography.font_family)
|
|
244
250
|
.set_font_size(theme.typography.col_header_size)
|
|
245
251
|
.set_bold()
|
|
246
252
|
.set_font_color(palette.col_header_text)
|
|
247
253
|
.set_align(FormatAlign::Left);
|
|
248
254
|
if let Some(fill) = palette.col_header_fill {
|
|
249
|
-
|
|
255
|
+
col_header_upper_descriptor_format =
|
|
256
|
+
col_header_upper_descriptor_format.set_background_color(fill);
|
|
250
257
|
}
|
|
258
|
+
let mut col_header_descriptor_format = col_header_upper_descriptor_format.clone();
|
|
251
259
|
if theme.borders.header_border_bottom != BorderStyle::None {
|
|
252
260
|
col_header_descriptor_format = col_header_descriptor_format
|
|
253
261
|
.set_border_bottom(translate_border_style(theme.borders.header_border_bottom))
|
|
@@ -255,15 +263,17 @@ impl FormatCache {
|
|
|
255
263
|
}
|
|
256
264
|
|
|
257
265
|
// 5. Column Header (Measure - Right Aligned)
|
|
258
|
-
let mut
|
|
266
|
+
let mut col_header_upper_measure_format = Format::new()
|
|
259
267
|
.set_font_name(&theme.typography.font_family)
|
|
260
268
|
.set_font_size(theme.typography.col_header_size)
|
|
261
269
|
.set_bold()
|
|
262
270
|
.set_font_color(palette.col_header_text)
|
|
263
271
|
.set_align(FormatAlign::Right);
|
|
264
272
|
if let Some(fill) = palette.col_header_fill {
|
|
265
|
-
|
|
273
|
+
col_header_upper_measure_format =
|
|
274
|
+
col_header_upper_measure_format.set_background_color(fill);
|
|
266
275
|
}
|
|
276
|
+
let mut col_header_measure_format = col_header_upper_measure_format.clone();
|
|
267
277
|
if theme.borders.header_border_bottom != BorderStyle::None {
|
|
268
278
|
col_header_measure_format = col_header_measure_format
|
|
269
279
|
.set_border_bottom(translate_border_style(theme.borders.header_border_bottom))
|
|
@@ -290,6 +300,8 @@ impl FormatCache {
|
|
|
290
300
|
title_format,
|
|
291
301
|
subtitle_format,
|
|
292
302
|
super_header_format,
|
|
303
|
+
col_header_upper_descriptor_format,
|
|
304
|
+
col_header_upper_measure_format,
|
|
293
305
|
col_header_descriptor_format,
|
|
294
306
|
col_header_measure_format,
|
|
295
307
|
section_header_format,
|
|
@@ -328,15 +340,21 @@ impl FormatCache {
|
|
|
328
340
|
};
|
|
329
341
|
|
|
330
342
|
// 3. Background Fill Precedence:
|
|
331
|
-
// is_highlight > total > subtotal > data
|
|
343
|
+
// is_highlight > total (or shade) > subtotal (or shade) > shade > data
|
|
344
|
+
let shade_fill = match key.role.shade {
|
|
345
|
+
Some(Shade::Muted) => self.palette.shade_muted_fill,
|
|
346
|
+
Some(Shade::Accent) => self.palette.shade_accent_fill,
|
|
347
|
+
Some(Shade::Alt) => self.palette.shade_alt_fill,
|
|
348
|
+
Some(Shade::None) | None => None,
|
|
349
|
+
};
|
|
332
350
|
let fill = if key.role.is_highlight {
|
|
333
351
|
self.palette.highlight_fill
|
|
334
352
|
} else if key.role.horizontal.is_total() || key.role.vertical.is_total() {
|
|
335
|
-
self.palette.total_fill
|
|
353
|
+
self.palette.total_fill.or(shade_fill)
|
|
336
354
|
} else if key.role.horizontal.is_subtotal() || key.role.vertical.is_subtotal() {
|
|
337
|
-
self.palette.subtotal_fill
|
|
355
|
+
self.palette.subtotal_fill.or(shade_fill)
|
|
338
356
|
} else {
|
|
339
|
-
self.palette.data_fill
|
|
357
|
+
shade_fill.or(self.palette.data_fill)
|
|
340
358
|
};
|
|
341
359
|
if let Some(fill_color) = fill {
|
|
342
360
|
format = format.set_background_color(fill_color);
|
|
@@ -453,6 +471,16 @@ impl FormatCache {
|
|
|
453
471
|
self.super_header_format.clone()
|
|
454
472
|
}
|
|
455
473
|
|
|
474
|
+
/// Returns the upper-tier stacked column header format (descriptor or measure, without bottom border).
|
|
475
|
+
#[must_use]
|
|
476
|
+
pub fn col_header_upper_format(&self, is_descriptor: bool) -> Format {
|
|
477
|
+
if is_descriptor {
|
|
478
|
+
self.col_header_upper_descriptor_format.clone()
|
|
479
|
+
} else {
|
|
480
|
+
self.col_header_upper_measure_format.clone()
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
456
484
|
/// Returns the column header format (descriptor or measure).
|
|
457
485
|
#[must_use]
|
|
458
486
|
pub fn col_header_format(&self, is_descriptor: bool) -> Format {
|
|
@@ -463,6 +491,12 @@ impl FormatCache {
|
|
|
463
491
|
}
|
|
464
492
|
}
|
|
465
493
|
|
|
494
|
+
/// Returns true if the theme defines a background fill for column headers.
|
|
495
|
+
#[must_use]
|
|
496
|
+
pub fn has_col_header_fill(&self) -> bool {
|
|
497
|
+
self.palette.col_header_fill.is_some()
|
|
498
|
+
}
|
|
499
|
+
|
|
466
500
|
/// Returns the section header format with optional indentation level.
|
|
467
501
|
#[must_use]
|
|
468
502
|
pub fn section_header_format(&self, indent: Option<u8>) -> Format {
|
|
@@ -221,61 +221,24 @@ impl WorkbookEmitter {
|
|
|
221
221
|
let tier_row = header_start_row + t as u32;
|
|
222
222
|
let is_bottom_tier = t == total_tiers - 1;
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
for (c_idx, col_spec) in table.columns.iter().enumerate() {
|
|
227
|
-
let col_idx = (placement.start_col + c_idx as u32) as u16;
|
|
228
|
-
match col_spec {
|
|
229
|
-
ColumnSpec::Gutter => {
|
|
230
|
-
// Gutter column: blank unstyled cell across all header tiers
|
|
231
|
-
}
|
|
232
|
-
ColumnSpec::Data(d) => {
|
|
233
|
-
if let Some(label) = d.bottom_header() {
|
|
234
|
-
let is_desc = d.role.is_descriptor();
|
|
235
|
-
let fmt = format_cache.col_header_format(is_desc);
|
|
236
|
-
worksheet.write_string_with_format(
|
|
237
|
-
tier_row, col_idx, label, &fmt,
|
|
238
|
-
)?;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
// Super-header tier: CenterAcross contiguous identical runs
|
|
224
|
+
// Step A (t == 0 Group Banners): CenterAcross contiguous runs with identical group label
|
|
225
|
+
if t == 0 {
|
|
245
226
|
let mut col_idx = 0;
|
|
246
|
-
while
|
|
247
|
-
let
|
|
248
|
-
|
|
227
|
+
while let Some(col_spec) = table.columns.get(col_idx) {
|
|
228
|
+
let group_opt = match col_spec {
|
|
229
|
+
ColumnSpec::Data(d) => d.group.as_deref(),
|
|
249
230
|
ColumnSpec::Gutter => None,
|
|
250
|
-
ColumnSpec::Data(d) => {
|
|
251
|
-
let k_len = d.headers.len();
|
|
252
|
-
if t >= total_tiers - k_len {
|
|
253
|
-
let k = t - (total_tiers - k_len);
|
|
254
|
-
Some(&d.headers[k])
|
|
255
|
-
} else {
|
|
256
|
-
None
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
231
|
};
|
|
260
232
|
|
|
261
|
-
if let Some(label) =
|
|
233
|
+
if let Some(label) = group_opt {
|
|
262
234
|
let start_c = col_idx;
|
|
263
235
|
let mut end_c = col_idx;
|
|
264
|
-
while
|
|
265
|
-
let
|
|
266
|
-
|
|
236
|
+
while let Some(next_spec) = table.columns.get(end_c + 1) {
|
|
237
|
+
let next_group = match next_spec {
|
|
238
|
+
ColumnSpec::Data(d) => d.group.as_deref(),
|
|
267
239
|
ColumnSpec::Gutter => None,
|
|
268
|
-
ColumnSpec::Data(d) => {
|
|
269
|
-
let k_len = d.headers.len();
|
|
270
|
-
if t >= total_tiers - k_len {
|
|
271
|
-
let k = t - (total_tiers - k_len);
|
|
272
|
-
Some(&d.headers[k])
|
|
273
|
-
} else {
|
|
274
|
-
None
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
240
|
};
|
|
278
|
-
if
|
|
241
|
+
if next_group == Some(label) {
|
|
279
242
|
end_c += 1;
|
|
280
243
|
} else {
|
|
281
244
|
break;
|
|
@@ -305,6 +268,39 @@ impl WorkbookEmitter {
|
|
|
305
268
|
}
|
|
306
269
|
}
|
|
307
270
|
}
|
|
271
|
+
|
|
272
|
+
// Step B (Per-column stacked headers across all t in 0..total_tiers)
|
|
273
|
+
for (c_idx, col_spec) in table.columns.iter().enumerate() {
|
|
274
|
+
let col_idx = (placement.start_col + c_idx as u32) as u16;
|
|
275
|
+
match col_spec {
|
|
276
|
+
ColumnSpec::Gutter => {
|
|
277
|
+
// Gutter column: blank unstyled cell across all header tiers
|
|
278
|
+
}
|
|
279
|
+
ColumnSpec::Data(d) => {
|
|
280
|
+
if t == 0 && d.group.is_some() {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
let k_len = d.headers.len();
|
|
284
|
+
let first_hdr_tier = total_tiers.saturating_sub(k_len);
|
|
285
|
+
let is_desc = d.role.is_descriptor();
|
|
286
|
+
let fmt = if is_bottom_tier {
|
|
287
|
+
format_cache.col_header_format(is_desc)
|
|
288
|
+
} else {
|
|
289
|
+
format_cache.col_header_upper_format(is_desc)
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if let Some(label) =
|
|
293
|
+
t.checked_sub(first_hdr_tier).and_then(|k| d.headers.get(k))
|
|
294
|
+
{
|
|
295
|
+
worksheet.write_string_with_format(
|
|
296
|
+
tier_row, col_idx, label, &fmt,
|
|
297
|
+
)?;
|
|
298
|
+
} else if is_bottom_tier || format_cache.has_col_header_fill() {
|
|
299
|
+
worksheet.write_blank(tier_row, col_idx, &fmt)?;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
308
304
|
}
|
|
309
305
|
}
|
|
310
306
|
|
|
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|
|
4
4
|
use super::error::IrError;
|
|
5
5
|
use super::id::CellId;
|
|
6
6
|
use crate::expr::ast::Expr;
|
|
7
|
-
use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle};
|
|
7
|
+
use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle, Shade};
|
|
8
8
|
use crate::model::id::{ColId, RowId, SheetId, TableId};
|
|
9
9
|
use crate::model::table::ValueLiteral;
|
|
10
10
|
|
|
@@ -64,6 +64,9 @@ pub struct CellNode {
|
|
|
64
64
|
pub vertical: SemanticStyle,
|
|
65
65
|
/// Visual highlight emphasis flag.
|
|
66
66
|
pub is_highlight: bool,
|
|
67
|
+
/// Semantic background shade if assigned.
|
|
68
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
69
|
+
pub shade: Option<Shade>,
|
|
67
70
|
/// Descriptor text indentation level.
|
|
68
71
|
#[serde(default)]
|
|
69
72
|
pub indent: u8,
|
|
@@ -85,6 +88,7 @@ impl CellNode {
|
|
|
85
88
|
horizontal: SemanticStyle::Data,
|
|
86
89
|
vertical: SemanticStyle::Data,
|
|
87
90
|
is_highlight: false,
|
|
91
|
+
shade: None,
|
|
88
92
|
indent: 0,
|
|
89
93
|
payload,
|
|
90
94
|
note: None,
|
|
@@ -122,6 +126,12 @@ impl CellNode {
|
|
|
122
126
|
self
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
/// Builder method attaching a semantic background shade.
|
|
130
|
+
pub fn with_shade(mut self, shade: Option<Shade>) -> Self {
|
|
131
|
+
self.shade = shade;
|
|
132
|
+
self
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
/// Builder method attaching typographic indentation.
|
|
126
136
|
pub fn with_indent(mut self, indent: u8) -> Self {
|
|
127
137
|
self.indent = indent;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use thiserror::Error;
|
|
2
2
|
|
|
3
3
|
use super::id::CellId;
|
|
4
|
-
use crate::model::format::{CurrencyCode, FormatPreset};
|
|
4
|
+
use crate::model::format::{CurrencyCode, FormatPreset, Shade};
|
|
5
5
|
use crate::model::id::{ColId, RowId, SheetId, TableId};
|
|
6
6
|
|
|
7
7
|
/// Errors encountered during intermediate representation construction and symbol registration.
|
|
@@ -102,6 +102,17 @@ pub enum IrError {
|
|
|
102
102
|
col_currency: CurrencyCode,
|
|
103
103
|
},
|
|
104
104
|
|
|
105
|
+
#[error(
|
|
106
|
+
"Table '{table}': shade conflict at row '{row}' ({row_shade}) and column '{column}' ({col_shade}) (hint: remove 'shade' from either row or column, or specify an explicit cell override)"
|
|
107
|
+
)]
|
|
108
|
+
ShadeConflict {
|
|
109
|
+
table: TableId,
|
|
110
|
+
row: RowId,
|
|
111
|
+
column: ColId,
|
|
112
|
+
row_shade: Shade,
|
|
113
|
+
col_shade: Shade,
|
|
114
|
+
},
|
|
115
|
+
|
|
105
116
|
#[error("model validation error: {0}")]
|
|
106
117
|
Model(String),
|
|
107
118
|
}
|