Goluca
Movement-based accounting format using arrow notation to show flows between accounts.
Goluca uses movements instead of traditional postings, inspired by Pacioli's
Credit/Debit notation. Each movement transfers an amount between two accounts
using arrow operators (->, //, >), with linked movements grouped via the
+ prefix.
See also: ABNF and Plain Text Accounting, ABNF syntax comparison, and ABNF Standards and Extensions for details on the non-standard constructs used in the grammars below.
Journals and Source Files
The books of account are the totality of an entity's accounting records. Within them, the journal is the chronological record of movements — it is the source of truth and, in a modern PTA system, has a plain text format (the source files). The ledger is the same data reorganised by account, holding only one side of each movement; each journal entry produces two ledger entries. The ledger is derived from the journal, not stored independently.
A journal covers a single accounting period (e.g. a financial year) and may be stored across one or more source files.
Period linkage
Successive journals need a mechanism to link together so the full history forms an auditable chain. Pacioli numbered his journals sequentially, marking the first with a cross (✠). Goluca could adopt a similar scheme — an explicit period declaration with a sequence number and a reference to the prior period.
Integrity via Merkle trees
To guarantee that a collection of journals has not been tampered with, each period's closing state could include a cryptographic hash of its contents. Chaining these hashes (as in a Merkle tree) would let anyone verify the integrity of the entire ledger history from a single root hash.
Open questions
- Syntax for declaring a period and its sequence number.
- Whether the hash covers the raw source text or a canonical serialisation.
- How opening balances reference the prior period's closing hash.
- Whether sub-files within a period (e.g. monthly splits) also participate in the hash chain.
Details to be worked out.
ABNF Grammar (proposed)
This extends the auto-generated grammar from grammar.js via
tree-sitter2abnf
with new directives for commodities, accounts, customers, configuration,
and full datetime support.
See Goluca Datetime Formats for details on
the datetime design and its relationship to ISO 8601 / RFC 3339.
; @grammar "goluca" ; @extras (@pattern("\\r")) source_file = *((directive / transaction / comment / %s"\n")) ; --- Directives --- directive = commodity_directive / open_directive / option_directive / alias_directive / customer_directive / data_point commodity_directive = [datetime _sp] %s"commodity" _sp commodity %s"\n" *metadata_line open_directive = datetime _sp %s"open" _sp account [_sp commodity_list] %s"\n" *metadata_line option_directive = %s"option" _sp option_key _sp option_value %s"\n" alias_directive = %s"alias" _sp alias_name _sp account %s"\n" customer_directive = %s"customer" _sp customer_name %s"\n" 1*customer_property ; --- Transactions --- transaction = header 1*movement header = datetime [knowledge_datetime] _sp flag [_sp payee] %s"\n" movement = _sp [linked_prefix] @field(from) account _sp arrow _sp @field(to) account [_sp description] _sp amount _sp commodity %s"\n" ; --- Metadata --- metadata_line = _indent metadata_key %s":" _sp metadata_value %s"\n" customer_property = _indent (customer_account / customer_constraint / metadata_line) customer_account = %s"account" _sp account %s"\n" customer_constraint = %s"max-aggregate-balance" _sp amount _sp commodity %s"\n" ; --- Data Points (see goluca-parameters.html) --- data_point = datetime [knowledge_datetime] %s"data" _sp param_name _sp param_value [_sp comment] %s"\n" param_name = name_part *(%s":" name_part) name_part = @pattern("[a-zA-Z][a-zA-Z0-9_-]*") param_value = @pattern("[^\\n;]+") ; --- Tokens --- comment = @token(@pattern("[#;]") @pattern("[^\\n]*")) _sp = @pattern(" +") _indent = @pattern(" ") datetime = date [%s"T" time timezone] date = @pattern("\\d{4}-\\d{2}-\\d{2}") time = @pattern("\\d{2}:\\d{2}:\\d{2}") [fractional] fractional = %s"." @pattern("\\d{1,6}") timezone = %s"Z" / tz_offset tz_offset = (%s"+" / %s"-") @pattern("\\d{2}:\\d{2}") knowledge_datetime = %s"%" datetime flag = (%s"*" / %s"!") payee = @pattern("[^\\n]+") linked_prefix = %s"+" account = @pattern("[A-Za-z0-9][a-zA-Z0-9]*(:[A-Za-z0-9][a-zA-Z0-9-]*)+") arrow = (%s"->" / %s"//" / %s"→" / %s">") description = @pattern("\"[^\"]*\"") amount = @pattern("-?[0-9][0-9,]*(\\.[0-9]+)?") commodity = @token(@prec(1) @pattern("[A-Z][A-Z]+")) commodity_list = commodity *(%s"," commodity) option_key = @pattern("[a-z][a-z-]*") option_value = @pattern("[^\\n]+") alias_name = @pattern("[A-Za-z][a-zA-Z0-9-]*") customer_name = @pattern("\"[^\"]*\"") metadata_key = @pattern("[a-z][a-z-]*") metadata_value = @pattern("[^\\n]+")
ABNF Grammar (current goluca)
Auto-generated from the current
tree-sitter-goluca
grammar.js via
tree-sitter2abnf.
; @grammar "goluca" ; @extras (@pattern("\\r")) source-file = *(directive / transaction / comment / %s"\n") directive = commodity-directive / open-directive / option-directive / alias-directive / customer-directive / data-point transaction = header 1*(movement / metadata-line) data-point = datetime [knowledge-datetime] _sp %s"data" _sp param-name _sp param-value %s"\n" header = datetime [knowledge-datetime] _sp flag [_sp payee] %s"\n" commodity-directive = [datetime _sp] %s"commodity" _sp commodity %s"\n" *metadata-line open-directive = datetime _sp %s"open" _sp account [_sp commodity-list] %s"\n" *metadata-line customer-directive = %s"customer" _sp customer-name %s"\n" 1*customer-property knowledge-datetime = %s"%" datetime customer-property = (customer-account / customer-constraint / metadata-line) datetime = date [%s"T" time [fractional] [timezone]] [period-anchor] option-directive = %s"option" _sp option-key _sp option-value %s"\n" alias-directive = %s"alias" _sp alias-name _sp account %s"\n" customer-account = _sp %s"account" _sp account %s"\n" customer-constraint = _sp %s"max-aggregate-balance" _sp amount _sp commodity %s"\n" metadata-line = _sp metadata-key %s":" _sp metadata-value %s"\n" timezone = (%s"Z" / tz-offset) movement = _sp [linked-prefix] @field(from) account _sp arrow _sp @field(to) account [_sp description] _sp amount _sp commodity %s"\n" commodity-list = commodity *(%s"," commodity) ; --- Tokens --- comment = @token(@pattern("[#;]") @pattern("[^\\n]*")) param-name = @pattern("[a-zA-Z][a-zA-Z0-9_-]*(:[a-zA-Z][a-zA-Z0-9_-]*)*") param-value = @token(@prec(-1) @pattern("[^\\n]+")) metadata-key = @pattern("[a-z][a-z0-9_-]*") metadata-value = @pattern("[^\\n]+") _sp = @pattern(" +") period-anchor = (%s"^" / %s"$") date = @pattern("\\d{4}(-\\d{2}(-\\d{2})?)?") time = @pattern("\\d{2}:\\d{2}:\\d{2}") fractional = @pattern("\\.\\d{3}(\\d{3}(\\d{3})?)?") tz-offset = @pattern("[+-]\\d{2}:\\d{2}") flag = (%s"*" / %s"!") payee = @pattern("[^\\n]+") linked-prefix = %s"+" account = @pattern("[A-Za-z0-9][a-zA-Z0-9]*(:[A-Za-z0-9][a-zA-Z0-9-]*)+") arrow = (%s"->" / %s"//" / %s"→" / %s">") description = @pattern("\"[^\"]*\"") amount = @pattern("-?[0-9][0-9,]*(\\.[0-9]+)?") commodity = @token(@prec(1) @pattern("[A-Z][A-Z]+")) option-key = (@pattern("\"[^\"]*\"") / @pattern("[a-z][a-z-]*")) option-value = (@pattern("\"[^\"]*\"") / @pattern("[^\\n]+")) alias-name = @pattern("[A-Za-z][a-zA-Z0-9-]*") customer-name = @pattern("\"[^\"]*\"")
ABNF → JSON round-trip
{
"name": "goluca",
"rules": {
"source_file": {
"type": "REPEAT",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "directive"
},
{
"type": "SYMBOL",
"name": "transaction"
},
{
"type": "SYMBOL",
"name": "comment"
},
{
"type": "STRING",
"value": "\n"
}
]
}
},
"directive": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "commodity_directive"
},
{
"type": "SYMBOL",
"name": "open_directive"
},
{
"type": "SYMBOL",
"name": "option_directive"
},
{
"type": "SYMBOL",
"name": "alias_directive"
},
{
"type": "SYMBOL",
"name": "customer_directive"
},
{
"type": "SYMBOL",
"name": "data_point"
}
]
},
"transaction": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "header"
},
{
"type": "REPEAT1",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "movement"
},
{
"type": "SYMBOL",
"name": "metadata_line"
}
]
}
}
]
},
"data_point": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "datetime"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "knowledge_datetime"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "STRING",
"value": "data"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "param_name"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "param_value"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"header": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "datetime"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "knowledge_datetime"
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "flag"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "payee"
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "\n"
}
]
},
"commodity_directive": {
"type": "SEQ",
"members": [
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "datetime"
},
{
"type": "SYMBOL",
"name": "_sp"
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "commodity"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "commodity"
},
{
"type": "STRING",
"value": "\n"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "metadata_line"
}
}
]
},
"open_directive": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "datetime"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "STRING",
"value": "open"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "account"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "commodity_list"
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "\n"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "metadata_line"
}
}
]
},
"customer_directive": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "customer"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "customer_name"
},
{
"type": "STRING",
"value": "\n"
},
{
"type": "REPEAT1",
"content": {
"type": "SYMBOL",
"name": "customer_property"
}
}
]
},
"knowledge_datetime": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "%"
},
{
"type": "SYMBOL",
"name": "datetime"
}
]
},
"customer_property": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "customer_account"
},
{
"type": "SYMBOL",
"name": "customer_constraint"
},
{
"type": "SYMBOL",
"name": "metadata_line"
}
]
},
"datetime": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "date"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "T"
},
{
"type": "SYMBOL",
"name": "time"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "fractional"
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "timezone"
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "period_anchor"
},
{
"type": "BLANK"
}
]
}
]
},
"option_directive": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "option"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "option_key"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "option_value"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"alias_directive": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "alias"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "alias_name"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "account"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"customer_account": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "STRING",
"value": "account"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "account"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"customer_constraint": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "STRING",
"value": "max-aggregate-balance"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "amount"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "commodity"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"metadata_line": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "metadata_key"
},
{
"type": "STRING",
"value": ":"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "metadata_value"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"timezone": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "Z"
},
{
"type": "SYMBOL",
"name": "tz_offset"
}
]
},
"movement": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "linked_prefix"
},
{
"type": "BLANK"
}
]
},
{
"type": "FIELD",
"content": {
"type": "SYMBOL",
"name": "account"
},
"name": "from"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "arrow"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "FIELD",
"content": {
"type": "SYMBOL",
"name": "account"
},
"name": "to"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "description"
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "amount"
},
{
"type": "SYMBOL",
"name": "_sp"
},
{
"type": "SYMBOL",
"name": "commodity"
},
{
"type": "STRING",
"value": "\n"
}
]
},
"commodity_list": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "commodity"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "SYMBOL",
"name": "commodity"
}
]
}
}
]
},
"comment": {
"type": "TOKEN",
"content": {
"type": "SEQ",
"members": [
{
"type": "PATTERN",
"value": "[#;]"
},
{
"type": "PATTERN",
"value": "[^\\n]*"
}
]
}
},
"param_name": {
"type": "PATTERN",
"value": "[a-zA-Z][a-zA-Z0-9_-]*(:[a-zA-Z][a-zA-Z0-9_-]*)*"
},
"param_value": {
"type": "TOKEN",
"content": {
"type": "PREC",
"content": {
"type": "PATTERN",
"value": "[^\\n]+"
},
"value": -1
}
},
"metadata_key": {
"type": "PATTERN",
"value": "[a-z][a-z0-9_-]*"
},
"metadata_value": {
"type": "PATTERN",
"value": "[^\\n]+"
},
"_sp": {
"type": "PATTERN",
"value": " +"
},
"period_anchor": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "^"
},
{
"type": "STRING",
"value": "$"
}
]
},
"date": {
"type": "PATTERN",
"value": "\\d{4}(-\\d{2}(-\\d{2})?)?"
},
"time": {
"type": "PATTERN",
"value": "\\d{2}:\\d{2}:\\d{2}"
},
"fractional": {
"type": "PATTERN",
"value": "\\.\\d{3}(\\d{3}(\\d{3})?)?"
},
"tz_offset": {
"type": "PATTERN",
"value": "[+-]\\d{2}:\\d{2}"
},
"flag": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "*"
},
{
"type": "STRING",
"value": "!"
}
]
},
"payee": {
"type": "PATTERN",
"value": "[^\\n]+"
},
"linked_prefix": {
"type": "STRING",
"value": "+"
},
"account": {
"type": "PATTERN",
"value": "[A-Za-z0-9][a-zA-Z0-9]*(:[A-Za-z0-9][a-zA-Z0-9-]*)+"
},
"arrow": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "-\u003e"
},
{
"type": "STRING",
"value": "//"
},
{
"type": "STRING",
"value": "→"
},
{
"type": "STRING",
"value": "\u003e"
}
]
},
"description": {
"type": "PATTERN",
"value": "\"[^\"]*\""
},
"amount": {
"type": "PATTERN",
"value": "-?[0-9][0-9,]*(\\.[0-9]+)?"
},
"commodity": {
"type": "TOKEN",
"content": {
"type": "PREC",
"content": {
"type": "PATTERN",
"value": "[A-Z][A-Z]+"
},
"value": 1
}
},
"option_key": {
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "\"[^\"]*\""
},
{
"type": "PATTERN",
"value": "[a-z][a-z-]*"
}
]
},
"option_value": {
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "\"[^\"]*\""
},
{
"type": "PATTERN",
"value": "[^\\n]+"
}
]
},
"alias_name": {
"type": "PATTERN",
"value": "[A-Za-z][a-zA-Z0-9-]*"
},
"customer_name": {
"type": "PATTERN",
"value": "\"[^\"]*\""
}
},
"extras": [
{
"type": "PATTERN",
"value": "\\r"
}
]
}
ABNF Grammar (bytestone.uk)
Hand-written ABNF from the bytestone.uk article. This predates the tree-sitter grammar and uses standard ABNF conventions (WSP, CRLF, DQUOTE).
journal = *(movement / linked-movement) movement = date WSP flag CRLF WSP WSP from-account WSP arrow WSP to-account [WSP description] WSP amount WSP commodity CRLF linked-movement = movement 1*("+" from-account WSP arrow WSP to-account [WSP description] WSP amount [WSP commodity] CRLF) date = 4DIGIT "-" 2DIGIT "-" 2DIGIT flag = "*" / "!" arrow = ">" / "->" / "//" from-account = account to-account = account account = account-type *(":" name) account-type = "Assets" / "Liabilities" / "Equity" / "Income" / "Expenses" name = 1*VCHAR description = DQUOTE *VCHAR DQUOTE amount = 1*DIGIT ["." 1*DIGIT] commodity = 1*ALPHA
Characteristics
| Feature | Detail |
|---|---|
| Direction | Explicit — every movement names from -> to |
| Balancing | No implicit balancing; each movement is self-contained |
| Linked movements | + prefix groups multiple movements under one header |
| Arrow operators | -> (transfer), // (split/fee), > (shorthand) |
| Accounts | Colon-separated hierarchy, any root name — see accounts |
| Commodities | Uppercase alpha, 2+ chars; must be declared with scaling — see accounts |
| Dates | ISO 8601 (YYYY-MM-DD) |
| Flags | * (cleared) or ! (pending) |
| Comments | Lines starting with # or ; |
| Description | Optional, double-quoted string on a movement line |
| Knowledge date | Optional %YYYY-MM-DD suffix on transaction date |
| Commodity declarations | commodity GBP or 2024-01-01 commodity GBP with optional metadata |
| Account open | 2024-01-01 open Account:Name GBP with optional metadata |
| Options | option key value for file-level configuration |
| Aliases | alias ShortName Full:Account:Path for shorthand references |
| Customers | customer "Name" with account links and constraints |
| Metadata | Indented key: value lines on directives and open statements |
| Data points | Time-stamped named parameters — see parameters |
| Hierarchical params | Metadata on parent open cascades to child accounts |
Example
2024-01-15 * Tesco Assets:Bank:Current -> Expenses:Groceries "Weekly groceries" 45.50 GBP
Linked Movement Example
2024-01-20 * Transfer with fee Assets:Bank:Current -> Assets:Savings "Monthly savings" 500.00 GBP +Assets:Bank:Current -> Expenses:BankCharges "Transfer fee" 1.50 GBP
Knowledge Date Example
; Transaction occurred Jan 15, booked on Jan 20 (e.g. credit card statement arrived late) 2024-01-15%2024-01-20 * Tesco Assets:CreditCard -> Expenses:Groceries "Weekly groceries" 45.50 GBP
Commodity Declaration Examples
commodity GBP ; undated, no metadata 2024-01-01 commodity GBP ; dated, no metadata 2024-01-01 commodity GBP ; dated, with metadata name: "British Pound Sterling" precision: 2 2024-01-01 commodity BTC name: "Bitcoin" precision: 8
Account Open Examples
2024-01-01 open Assets:Bank:Current GBP 2024-01-01 open Assets:Bank:Savings GBP,USD description: "ISA savings account" ; Hierarchical: parent sets defaults, children inherit 2024-01-01 open Assets:Bank currency: GBP institution: "Barclays" 2024-01-01 open Assets:Bank:Current ; inherits currency: GBP, institution: "Barclays" 2024-01-01 open Assets:Bank:Savings currency: GBP,USD ; overrides parent's currency
Option Directive Examples
option operating-currency GBP option require-accounts true option title "My Personal Ledger"
Alias Examples
alias Groceries Expenses:Food:Groceries alias Rent Expenses:Housing:Rent alias Current Assets:Bank:Current 2024-01-15 * Tesco Current -> Groceries "Weekly shop" 45.50 GBP
Customer Model Examples
customer "John Smith" account Assets:Receivables:JohnSmith max-aggregate-balance 10000 GBP email: "john@example.com" payment-terms: "net-30" customer "Acme Corp" account Assets:Receivables:AcmeCorp max-aggregate-balance 50000 GBP vat-number: "GB123456789" 2024-03-01 * Invoice 001 Income:Consulting -> Assets:Receivables:JohnSmith "March consulting" 2500.00 GBP
Hierarchical Parameter Inheritance
2024-01-01 open Assets:Bank currency: GBP institution: "Barclays" 2024-01-01 open Assets:Bank:Current ; inherits currency: GBP, institution: "Barclays" 2024-01-01 open Assets:Bank:Savings currency: GBP,USD ; inherits institution: "Barclays", overrides currency
Complete File Example
; --- Options --- option operating-currency GBP option require-accounts true option title "My Personal Ledger" ; --- Commodities --- 2024-01-01 commodity GBP name: "British Pound Sterling" precision: 2 2024-01-01 commodity USD name: "US Dollar" precision: 2 ; --- Accounts --- 2024-01-01 open Assets:Bank currency: GBP institution: "Barclays" 2024-01-01 open Assets:Bank:Current 2024-01-01 open Assets:Bank:Savings 2024-01-01 open Assets:CreditCard 2024-01-01 open Expenses:Groceries 2024-01-01 open Expenses:BankCharges 2024-01-01 open Expenses:Housing:Rent 2024-01-01 open Income:Consulting ; --- Aliases --- alias Groceries Expenses:Groceries alias Current Assets:Bank:Current ; --- Customers --- customer "John Smith" account Assets:Receivables:JohnSmith max-aggregate-balance 10000 GBP payment-terms: "net-30" ; --- Transactions --- 2024-01-15 * Tesco Current -> Groceries "Weekly groceries" 45.50 GBP ; Knowledge date: occurred Jan 18, booked Jan 22 2024-01-18%2024-01-22 * Online purchase Assets:CreditCard -> Expenses:Groceries "Delivery order" 32.00 GBP 2024-01-20 * Transfer with fee Assets:Bank:Current -> Assets:Bank:Savings "Monthly savings" 500.00 GBP +Assets:Bank:Current -> Expenses:BankCharges "Transfer fee" 1.50 GBP 2024-03-01 * Invoice 001 Income:Consulting -> Assets:Receivables:JohnSmith "March consulting" 2500.00 GBP
Semantic Rules
require-accounts
When option require-accounts true is set:
- Every account used in a movement must have a prior
opendirective. - Aliases are resolved before account validation — alias names themselves
do not need an
open, but their target accounts do. - Commodity references must have prior
commoditydirectives. - Violations produce errors at parse/check time.
Hierarchical parameter inheritance
- Metadata on an
opendirective cascades to all descendant accounts. - A child
opencan override any inherited key. - Only absent keys are inherited — from the nearest ancestor.
- Inheritance follows the colon-separated account hierarchy, not file order.
- The commodity list on the
openline constrains movement commodities;currencymetadata is informational only.
Customer constraints
max-aggregate-balanceis checked against the aggregate balance of the customer's linked account.- Implementations should report a warning (not error) when exceeded, since violations may be intentional during reconciliation.
Differences: tree-sitter vs bytestone ABNF
The two grammars describe the same format but differ in detail:
| Aspect | tree-sitter2abnf | bytestone.uk |
|---|---|---|
| Top-level rule | source_file with transactions, comments, newlines |
journal with movements only |
| Transaction structure | header + 1*movement |
Flat movement / linked-movement |
| Comments | Included as a grammar rule | Not defined |
| Arrow operators | ->, //, → (Unicode), > |
->, //, > |
| Account pattern | Regex-based (any uppercase start + colon-separated parts) | Enumerated top-level types (Assets, Liabilities, etc.) |
| Commodity | Uppercase, 2+ chars, with precedence | Any 1*ALPHA |
| Amounts | Allows commas and optional decimals | Digits with optional decimals only |
Note: The proposed grammar extends the tree-sitter2abnf grammar with directives (commodity, open, option, alias, customer), knowledge dates, and metadata. These extensions are not yet implemented in the tree-sitter parser.