Rufineer v0.43.0
Account
Browse the docs

What you get

Compiler

What each construct becomes in Excel, and what the compiler says when something is wrong.

What the compiler builds today

The compiler is written in Go and runs in your browser, compiled to WebAssembly, in the app; it builds everything on the Language page; the Rust and Ruby compilers that came before it are kept as the references it is tested against, model by model and value by value. The Planned language page is the design it is growing towards.

BuiltPlanned
model; timeline with a step per phase, a fiscal year end, and phase lengths by scenario
inputs with @source, @note and | grow; table and the data workbook
lines, | during a phase, window or condition, units and the time rule; several currencies with their exchange rates
functions: prev next cumulative ahead trailing lag grow decline ramp spread window after before sum avg min max clamp abs irr npv mirr multiple effective dscr llcr
components: loan (equal, annuity, bullet, sculpted, sweep; arrears, capitalised interest, refinancing), depreciation, tax_losses, base_case
conventions: revenue, cost, capex, loans and reserves, and the statements, waterfall, returns and checks they write
balance, solve (with the Solve macro), funding
statement, check, alert, dashboard, sensitivities (with the Run sensitivities macro)
scenarios: a drop-down switch per set at the top of the Inputs sheet, the inputs that differ a column per case under it, and sensitivity cases that pick a case
rulers, a change of step inside a phase
debt sized on loan to value and debt yield, balloon repayments, the PLCR, sizing on several lender cases
lock-ups and partnership promotes as waterfall tiers
cases that shift dates, seek
groups of items and tables indexed by row, portfolios
series inputs, events and bands, statements translated into another currency

The worked examples use the planned language, so most of them don't compile yet; the first model, the three tutorials and the hotel do, and each is checked on every change by recalculating it in LibreOffice.

How it works, stage by stage (in Go): compiler/ reads the files (the lexer and parser), resolves names and units into a blueprint of the workbook, finds the circular references (circularity.go) and lays the sheets out (layout.go); eval/ calculates it, the way Excel will, for the preview; view/ is what the app's preview shows (the sheets, the audit, the problems), all of it running in the page; the lender-grade workbook and its macros are written by Rufineer's service, for the export. Every file is commented for reading.

Lender-grade models

The hotel example (the Hotel template in New model) is built to the standard a lender's model reviewer expects: a 120-room hotel, two years of construction, twenty years of operation. It has a cash waterfall with a debt service reserve account (DSRA) and a lock-up, an income statement, a cash flow statement and a balance sheet that balance every quarter, returns and cover ratios, a dashboard and seven sensitivities. Everything on this page compiles today, and the test suites recalculate the workbook in LibreOffice with every check passing.

Looking ahead: next

A DSRA usually holds the next two quarters of debt service. next(x) is the mirror of prev(x): the value in the next column, and 0 after the last period.

Rufineer
service_next: USD = next(debt.service)                 "Debt service next quarter"
dsra_target:  USD = service_next + next(service_next)  "DSRA target"

Looking ahead is safe on its own, and so is looking back, but not both in one loop: if a reads next(b) and b reads prev(a), Excel's column H needs column I, which needs column H. The compiler refuses such a loop.

Totals over a window

A DSCR is zero outside the repayment window, so min(dscr) would be 0. Give the window:

Rufineer
min_dscr: x = min(dscr, during: repaying)     "Minimum DSCR"
avg_dscr: x = avg(dscr, during: repaying)     "Average DSCR"

These become MINIFS, AVERAGEIFS, SUMIFS and MAXIFS over the row and the window's flag row. (MINIFS and MAXIFS need Excel 2019 or Microsoft 365.)

Returns: irr and npv

Rufineer
equity_irr: %/y = irr(equity_flow)                                   "Equity IRR"
equity_npv: USD = npv(inputs.cost_of_equity, equity_flow)            "Equity NPV"
llcr:       x   = npv(inputs.interest_rate, cfads_in_tenor, at: operations.start) / capex.total_debt

irr is Excel's XIRR, with each flow dated at the end of its period, so a quarterly model gives an annual rate with no conversion. npv is XNPV, valued at the model's start unless at: names another date: the compiler writes XNPV(…) * (1 + rate) ^ ((at - first date) / 365). A payback period needs nothing new: count the time before the cumulative cash turns positive.

Rufineer
still_to_recover: y = if equity_cumulative < 0 then year_frac else 0 | during operations
equity_payback:   y = sum(still_to_recover)          "Equity payback (years from opening)"

Balance sheets and cash flow statements

A statement's rows were always summed over each financial year. Balances need the value at the year's end instead:

Rufineer
statement balance "Balance sheet" | at year end {
  section "Assets"
  + accounts.building          "Building"
  + accounts.ffe               "Furniture, fixtures and equipment"
  + cash.dsra                  "Debt service reserve account"
  + cash.locked                "Locked-up cash"
  = total_assets               "Total assets"

  section "Liabilities and equity"
  + debt.loan                  "Senior debt"
  + accounts.share_capital     "Share capital"
  + accounts.retained_earnings "Retained earnings"
  = total_funding              "Total liabilities and equity"

  section "Check"
  + total_assets
  - total_funding
  = difference                 "Difference (0 when it balances)"
}

section starts a new block, and subtotals start again below it. A row can name an earlier row of the same statement (+ total_assets), and a row can have its own basis: the cash flow statement sums flows by year but takes opening cash | at year start. The balance sheet check itself runs every quarter, in the model, so an error shows in the quarter it happens:

Rufineer
check "Balance sheet balances"  accounts.total_assets == accounts.total_funding  tolerance 1 USD

The cash waterfall

A waterfall is ordinary lines: CFADS, less debt service, the DSRA topped up, drawn or released, a lock-up while the DSCR is below 1.15x, and what is left distributed. See the hotel example's cash module, and the annual waterfall statement that shows it year by year.

The dashboard

Rufineer
dashboard {
  section "Returns"
  returns.project_irr     "Project IRR (post-tax)"
  returns.equity_irr      "Equity IRR"

  section "Debt"
  debt.min_dscr           "Minimum DSCR"
  debt.llcr               "LLCR at opening"

  chart column "Revenue, EBITDA and net profit" {
    income.total_revenue  "Total revenue"
    income.ebitda         "EBITDA"
  }
  chart line "DSCR" { debt.dscr }
}

Each result is read live from its sheet (=Returns!$D$20); the Dashboard shows, it never calculates. Results must be single values; charts take lines that change over time: statement rows (one column per year) or module lines (one per period).

Sensitivities

Rufineer
sensitivities {
  results {
    returns.equity_irr   "Equity IRR"
    debt.min_dscr        "Min DSCR"
  }
  case "ADR −10%"            { inputs.adr * 90 % }
  case "Interest rate +1%"   { inputs.interest_rate + 1 %/y }
  case "Combined downside" {
    inputs.adr                  * 95 %
    inputs.stabilised_occupancy - 3 %
  }
}

How the workbook does it, without breaking a house rule:

  • Every change is an input on the Inputs sheet ("ADR: multiply by 90%"), and a sensitivity case switch at the top of the sheet picks the active one (the base case, or a case by its title).
  • A Scenarios sheet works out each changed input's value in the active case. The model's name for the input (inputs.adr) now means that value, so switching the case flows through everything.
  • The RunSensitivities macro (in the workbook with Solve, behind a button) makes each case active, runs Solve, and pastes the results into a table on the Sensitivities sheet, next to each case's change from the base case. Every case is solved in full, so interest during construction and anything else in a solve loop follow the case. Excel data tables can't do that.
  • An alert shows while a case other than the base case is active.

The hotel's results

Base case
Project IRR (post-tax, unlevered) / equity IRR13.4% / 15.5%
Minimum / average DSCR1.30x / 1.35x
LLCR / PLCR at opening1.74x / 2.06x
Equity payback10.5 years from opening
ADR −10%: equity IRR, minimum DSCR13.3%, 1.18x

A second example: a toll road

examples/express_fiji is a 40-year highway concession: four years of construction, 36 years of tolls, 65% senior debt at a fixed rate. Traffic is in vehicles per year and tolls in pounds per vehicle, so the compiler checks veh/y × GBP/veh. The arrangement fee, the commitment fee and interest during construction all depend on the size of the loan they help fund: three solve lines. The loan is repaid in level annual instalments, written as the annuity formula:

Rufineer
instalment: GBP = drawn * period_rate / (1 - (1 + period_rate) ^ -repayments)
                                                 | during in_repayment

Depreciation runs ahead of principal repayments, so cash builds up in the company that can't be paid as dividends. The model pays it back to the shareholders when the concession ends, in the equity IRR. With the heavy-vehicle toll at £19.81 (2023 prices) the equity IRR is 23%.

Editor support

The app's editor and preview are built on the same compiler as the export, so what they show is exactly what the workbook will contain.

FeatureWhat you see
Errors as you typeThe compiler's messages, with the fix it suggests: underlined in the editor, counted in the status and on each file in the file tree, and listed in the preview's Problems tab. Click one to go to it.
HoverThe line's unit, whether it has a value per period, its formula written with the model's names (energy * inputs.tariff * 1000) and what it reads. No Excel cells: you work with names, and the compiler decides where things go.
Go to definition⌘-click (Ctrl-click) or F12 on a name, across files; Shift+F12 finds all references.
Completiondebt. lists debt's lines; units after a number; modifiers after |; functions with their arguments and the values they take.
OutlineThe modules and lines of the open file, in the Outline under the file tree. Click one to go to it.
The audit panelClick any value in the preview: the formula in names and with values, the Excel formula, what it reads and what reads it, period by period. Click to follow the chain.
FormattingFormat Document (⇧⌥F / Shift+Alt+F) and format on save, the same as rfn fmt: two spaces per level; names, units and = lined up in columns, then modifiers, labels, @source notes and comments; formulas that go on over several lines with their operators under the =; no line over 100 characters where it can break: a […] list of names (by [pv, cables, …], sheets […]) filled over lines; a formula broken before a | during, then/else, and/or, +/− or *//, its pieces under the =, else after a comma inside brackets; a label that alone makes a line too long on the next line; a check going on under its condition. Lines you break yourself stay broken. It only changes spaces, blank lines and where lines break, keeps a break only if the file reads as the same model, and leaves a file with syntax errors alone.
CommandsThe Console (⌘K / Ctrl+K), with or without rfn in front: every command in The console. + Generate adds a module or a line; Export to Excel (with Pro) is at the top right.

What the compiler writes

RufineerExcel
modulea sheet with a header, an Imports block and a Calculations block
inputsInputs sheet, one section per block, with Base and Live columns
tablevalues copied from its sheet of the data workbook into an input block, with the source noted
timelineTiming sheet: dates, year fractions, phase flags, counters
time-series lineone row with one formula copied across every period
constant lineone cell in the constants column
reference to another modulean Imports row at the top of the sheet (a table: a row per column, its values across). The Imports are an Excel group: click − by Imports to fold them away
| during p× flag, with the flag imported
balancea corkscrew of four or more rows
waterfallavailable, paid and after rows per tier
solvecalculated, pasted and difference rows, plus the Solve macro
monthly module read quarterlya Timeline aggregation row
scenariosa drop-down switch at the top of the Inputs sheet and its case number (MATCH), and under it each scenario input with a column per case and CHOOSE on the case number in column D
resultsa pasted results table and its macro
check, alertrows on the Checks sheet feeding the master flag

Formulas use a fixed set of functions. OFFSET, INDIRECT, LAMBDA and dynamic arrays are not used, so the output stays auditable in any Excel version.

Error messages

Errors say what is wrong and how to fix it, in plain terms.

Problems
error[unit]: can't add USD and MWh
  --> revenue.rfn:4:18
   |
 4 | total: USD = energy.net + tariff
   |              ^^^^^^^^^^   ^^^^^^ USD/kWh
   |              MWh
   = help: did you mean `energy.net * tariff`?  (MWh × USD/kWh → USD)

error[circular]: these lines depend on each other
   funding.uses → debt.senior.draws → debt.senior.interest → debt.idc → funding.uses
   = help: break the loop with `solve`, for example
           solve uses: USD = construction.total + debt.idc | during construction

warning[time]: `inputs.om_fixed` is USD/y but `ops.fixed_opex` is declared USD
   → multiplying by year fraction (0.083 in monthly columns, 0.25 in quarterly)