f8-engine 0.1.0__tar.gz → 0.3.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.3.0}/Cargo.lock +2 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/Cargo.toml +1 -1
- {f8_engine-0.1.0 → f8_engine-0.3.0}/PKG-INFO +44 -40
- {f8_engine-0.1.0 → f8_engine-0.3.0}/README.md +43 -39
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/mod.rs +12 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/model.rs +10 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/format.rs +45 -11
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/workbook.rs +43 -47
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/model.rs +1 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/resolve.rs +199 -181
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/cell.rs +11 -1
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/error.rs +12 -1
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/expand.rs +47 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/symbol.rs +1 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/error.rs +19 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/mod.rs +267 -71
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/address.rs +39 -7
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/mod.rs +30 -34
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/ast.rs +170 -14
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/mod.rs +2 -1
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/parser.rs +114 -187
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/lib.rs +1 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/error.rs +3 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/format.rs +33 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/mod.rs +4 -3
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/table.rs +227 -8
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/theme.rs +27 -10
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/error.rs +14 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/lib.rs +8 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/pyproject.toml +4 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/__init__.py +4 -1
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/_core.pyi +3 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/audit.py +10 -2
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/builder.py +253 -10
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/cli.py +54 -4
- f8_engine-0.3.0/python/f8/skills/f8-modeling/SKILL.md +45 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +170 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/functions.md +128 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +324 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/types.py +49 -6
- f8_engine-0.1.0/python/f8/skills/f8-modeling/SKILL.md +0 -43
- f8_engine-0.1.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +0 -366
- f8_engine-0.1.0/python/f8/skills/f8-modeling/references/functions.md +0 -172
- f8_engine-0.1.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +0 -342
- {f8_engine-0.1.0 → f8_engine-0.3.0}/LICENSE +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/Cargo.toml +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/context.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/expr.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/cycle.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/dependency.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/id.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/a1.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/common.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/constructors.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/parts.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/serial.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/yearfrac.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/annuity.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/discounting.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/irr.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/boolean.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/branching.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/error_handling.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/index.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/search.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xlookup.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xmatch.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/aggregation.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/basic.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/conditional.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/criteria.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/product.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/rounding.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/registry.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/traits.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/id.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/manifest.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/error.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/format.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/mod.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/Cargo.toml +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/model.rs +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/__main__.py +0 -0
- {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/py.typed +0 -0
|
@@ -37,7 +37,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "f8-core"
|
|
40
|
-
version = "0.
|
|
40
|
+
version = "0.3.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.3.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.3.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.3.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
@@ -26,27 +26,27 @@ Project-URL: Repository, https://github.com/mhcfa/f8
|
|
|
26
26
|
|
|
27
27
|
# f8
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
|
|
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
|
|
|
@@ -70,48 +71,50 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
|
70
71
|
```python
|
|
71
72
|
from f8 import Model
|
|
72
73
|
|
|
74
|
+
years = ["2025A", "2026E", "2027E"]
|
|
75
|
+
capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
|
|
76
|
+
depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
|
|
77
|
+
|
|
73
78
|
model = Model("Operating Forecast")
|
|
74
79
|
sheet = model.add_sheet("Summary")
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for year in
|
|
79
|
-
|
|
81
|
+
ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
|
|
82
|
+
ppe.add_column("item", role="descriptor", header="Line Item")
|
|
83
|
+
for year in years:
|
|
84
|
+
ppe.add_column(year, header=year, format="currency", decimals=1)
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
cells={"item": "
|
|
84
|
-
expr='
|
|
85
|
-
)
|
|
86
|
-
pnl.add_row(
|
|
87
|
-
"cogs",
|
|
88
|
-
cells={"item": "Cost of Revenue"},
|
|
89
|
-
expr='PnL["revenue", @col] * 0.30',
|
|
86
|
+
ppe.add_row(
|
|
87
|
+
"beg_balance",
|
|
88
|
+
cells={"item": "Beginning Balance", "2025A": 100.0},
|
|
89
|
+
expr='PPE["end_balance", col.prev]',
|
|
90
90
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
|
|
92
|
+
ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
|
|
93
|
+
ppe.add_row(
|
|
94
|
+
"end_balance",
|
|
95
|
+
cells={"item": "Ending Balance"},
|
|
96
|
+
expr='SUM(PPE["beg_balance".."depreciation", @col])',
|
|
95
97
|
style="total",
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
compiled = model.compile()
|
|
99
101
|
|
|
100
102
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
101
|
-
print(compiled.table("
|
|
103
|
+
print(compiled.table("PPE").to_markdown())
|
|
102
104
|
|
|
103
|
-
# Emit a live .xlsx workbook with native Excel formulas and styling
|
|
105
|
+
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
104
106
|
compiled.save("forecast.xlsx")
|
|
105
107
|
```
|
|
106
108
|
|
|
107
109
|
Output:
|
|
108
110
|
|
|
109
111
|
```markdown
|
|
110
|
-
| Line Item
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
112
|
+
| Line Item | 2025A | 2026E | 2027E |
|
|
113
|
+
| :------------------- | ---------: | ---------: | ---------: |
|
|
114
|
+
| Beginning Balance | $100.0 | $110.0 | $122.0 |
|
|
115
|
+
| Capital Expenditures | $25.0 | $30.0 | $35.0 |
|
|
116
|
+
| Depreciation | ($15.0) | ($18.0) | ($22.0) |
|
|
117
|
+
| **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
## Sample deliverables
|
|
@@ -150,9 +153,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
|
|
|
150
153
|
`Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
|
|
151
154
|
|
|
152
155
|
- Architecture overview: `docs/architecture.md`
|
|
153
|
-
- 2D matrix DSL reference: `docs/dsl/spec-format.md`
|
|
154
|
-
- Standard function catalog: `docs/functions/catalog.md`
|
|
155
156
|
- Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
|
|
157
|
+
- Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
|
|
158
|
+
- Function reference: `python/f8/skills/f8-modeling/references/functions.md`
|
|
159
|
+
- Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
|
|
156
160
|
|
|
157
161
|
## License
|
|
158
162
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# f8
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
|
|
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
|
|
|
@@ -44,48 +45,50 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
|
44
45
|
```python
|
|
45
46
|
from f8 import Model
|
|
46
47
|
|
|
48
|
+
years = ["2025A", "2026E", "2027E"]
|
|
49
|
+
capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
|
|
50
|
+
depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
|
|
51
|
+
|
|
47
52
|
model = Model("Operating Forecast")
|
|
48
53
|
sheet = model.add_sheet("Summary")
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for year in
|
|
53
|
-
|
|
55
|
+
ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
|
|
56
|
+
ppe.add_column("item", role="descriptor", header="Line Item")
|
|
57
|
+
for year in years:
|
|
58
|
+
ppe.add_column(year, header=year, format="currency", decimals=1)
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
cells={"item": "
|
|
58
|
-
expr='
|
|
59
|
-
)
|
|
60
|
-
pnl.add_row(
|
|
61
|
-
"cogs",
|
|
62
|
-
cells={"item": "Cost of Revenue"},
|
|
63
|
-
expr='PnL["revenue", @col] * 0.30',
|
|
60
|
+
ppe.add_row(
|
|
61
|
+
"beg_balance",
|
|
62
|
+
cells={"item": "Beginning Balance", "2025A": 100.0},
|
|
63
|
+
expr='PPE["end_balance", col.prev]',
|
|
64
64
|
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
|
|
66
|
+
ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
|
|
67
|
+
ppe.add_row(
|
|
68
|
+
"end_balance",
|
|
69
|
+
cells={"item": "Ending Balance"},
|
|
70
|
+
expr='SUM(PPE["beg_balance".."depreciation", @col])',
|
|
69
71
|
style="total",
|
|
70
72
|
)
|
|
71
73
|
|
|
72
74
|
compiled = model.compile()
|
|
73
75
|
|
|
74
76
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
75
|
-
print(compiled.table("
|
|
77
|
+
print(compiled.table("PPE").to_markdown())
|
|
76
78
|
|
|
77
|
-
# Emit a live .xlsx workbook with native Excel formulas and styling
|
|
79
|
+
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
78
80
|
compiled.save("forecast.xlsx")
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
Output:
|
|
82
84
|
|
|
83
85
|
```markdown
|
|
84
|
-
| Line Item
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
86
|
+
| Line Item | 2025A | 2026E | 2027E |
|
|
87
|
+
| :------------------- | ---------: | ---------: | ---------: |
|
|
88
|
+
| Beginning Balance | $100.0 | $110.0 | $122.0 |
|
|
89
|
+
| Capital Expenditures | $25.0 | $30.0 | $35.0 |
|
|
90
|
+
| Depreciation | ($15.0) | ($18.0) | ($22.0) |
|
|
91
|
+
| **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
## Sample deliverables
|
|
@@ -124,9 +127,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
|
|
|
124
127
|
`Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
|
|
125
128
|
|
|
126
129
|
- Architecture overview: `docs/architecture.md`
|
|
127
|
-
- 2D matrix DSL reference: `docs/dsl/spec-format.md`
|
|
128
|
-
- Standard function catalog: `docs/functions/catalog.md`
|
|
129
130
|
- Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
|
|
131
|
+
- Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
|
|
132
|
+
- Function reference: `python/f8/skills/f8-modeling/references/functions.md`
|
|
133
|
+
- Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
|
|
130
134
|
|
|
131
135
|
## License
|
|
132
136
|
|
|
@@ -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 {
|