Changelog¶
All notable changes to this project are documented here. The project follows semantic versioning.
1.2.1 - 2026-10-25¶
Fixed¶
date()with a day, month or year that does not exist raisesinvalid date: 2026-02-30on every supported Python version; the message used to repeat Python's own wording, which differs between versions.
1.2.0 - 2026-10-25¶
Added¶
- The language reference documents every construct with its syntax, types,
null and interval behaviour, errors and examples whose results the tests
compute, and a new quick reference has the language and its grammar on
one page. The tests check that the reference covers
dpmxl.grammar. Recordset.default: the record of a key a record set lacks, from a selection'sdefault.Recordset.mapcarries it andRecordset.lookupreturns it for a missing key.Frame.has_default: whether a columnar frame comes from a selection with adefault.
Fixed¶
- An operand with a
defaultstands for its default where another operand has a record it lacks, as a fact variable's fallback value does, instead of dropping that record from the join.with {default: null}: if not isnull({tA, c0010}) then not isnull({tB, c0010}) endifover two parts of an open table now fails for a key filed only inA; it was not evaluated. Operands without a default still decide the keys and join on the keys they share. Anifcondition with a default also covers keys only a branch has.where,sub,filter, awithcontext'swhere,rename, atime_shiftof a date key andifresults keep the default, so a filtered operand falls back to it for keys the other operands bring;get, aggregates and analytic functions do not. The columnar evaluator leaves these cases to the reference evaluator. [rename a to b]on a record set that already has a keybis an error; it produced two keys of the same name.- A
withcontext'swhereapplies only to the selections that have the keys it names. A body selection without them, key-less ones included, was an error ("unknown key component" or "where needs a keyed operand").
1.1.0 - 2026-10-25¶
Added¶
diagnose()finds problems without report data, as positionedDiagnostics with a stablecode, thesubjectconcerned and asuggestionto replace the text with: every syntax error (parsing resumes after one), keywords run together (ornot), unknown functions, wrong operand counts and selections without a table. Given table layouts it also reports unknown tables, rows, columns and sheets, empty ranges, clause names that are not keys and, with cell data types, operands of the wrong type; given the report parameters, names that resolve to nothing. An error means the rule fails, is inapplicable or cannot be evaluated as written; a code on an open axis or one the table lacks, whose outcome is up to the provider, is a warning.Diagnostic.pointer()renders one;DIAGNOSTIC_CODESlists the codes.dpmxl.grammar.SIGNATURES: each function'sSignature(operand counts,group by, key and period operands), which both evaluators anddiagnose()check calls against.TableLayout.open_axesandTableLayout.data_typesdescribe open axes and cell data types (dpmxl.provider.CELL_TYPES; it may be partial); data files accept them.- Parsed selections keep source positions:
AxisSelector.spanandspans, andSelectionSpec.source_span. dpm-xl checkreports every problemdiagnose()finds, also asdiagnosticsin--json.--layouts DATAchecks against a data file's tables and parameters;--strictfails on any error, not only syntax errors.
Changed¶
- Calls with the wrong operands fail with one wording in both evaluators,
such as
abs takes 1 operand, not 2, before their operands are evaluated. dpm-xl check --jsonlists every syntax error, soerrorsmay hold several entries for one expression: count failing expressions withtotal - parsed, notlen(errors).- The errors for
max_aggrandmin_aggrwith several operands and forrankwithoutoverpoint at the function name; an unknown selection component's error spans the whole component.
Fixed¶
- A range that repeats its axis letter, such as
r0440-r0480, selected every coordinate from its lower bound up; it now selects whatr0440-0480does. The letter is dropped only when the lower bound does not start with it, sorr1-rr10reads as written (seeAxisSelector).
1.0.0 - 2026-10-24¶
Added¶
- Python 3.15 support, tested in CI on 3.11 to 3.15 with and without extras.
- Documentation site at https://luukhopman.github.io/dpm-xl/, with
llms.txtandllms-full.txt; every example in it is tested. examples/rules.txtandexamples/xbrl-report/, a small xBRL-CSV report for the end-to-end guide.dpm-xl evaltakes a rule file as well as one expression and summarizes every rule;checkandevalaccept--json;--versionand examples in--help.- Runnable examples in
examples/, run by the tests. dpmxl.__version__;--jsonoutput carriesschema_version.- The public API is documented: every name in a module's
__all__(including thedpmxl.asttree andVALUE_COLUMNS), the columnar frame schema and which caches are thread-safe. tokenize()andToken(kind, text, start, end)are public, with kinds for syntax highlighting (keyword, function, operator, brace, table and axis references, literal kinds, item, identifier, punctuation and more); every AST node's sourcespanis documented.DpmXlSyntaxError.end, andpointer()underlines the offending text.- Text concatenation
&,substr,log, the date functionsyear,semester,quarter,month,week,dayanddate(y, m, d), and date literals with a time part (#2026-12-31T10:00:00#). - Sets: literals such as
{1, 2}and{},set_of,union,intersect,setdiffandsymdiff,inwith any set operand anddefault: {…}; theValueSetvalue type. - Typed parameters
{p_Name, type[, default: v]}, operation references{o_Code, …}and table groups{g_Group, …}, resolved through optionaloperation(code)andtable_group(code)provider methods (andMemoryProvider'soperationsandtable_groups, also in the CLI data file);Reference.kindandReference.code, andtables(node, provider)expands table groups. - Statements separated by
;with temporary (name := e) and persistent ({target} <- e) assignments:parse_program, theProgram,AssignmentandPersistentAssignmentnodes, andRuleResult.assignments.evaluate_rule, the columnar fallback anddpm-xl checkaccept programs;parsestill returns one expression and allows a trailing;. time_shiftevaluates its operand on another period's report from the optionalearlier_report(period, count)provider method (orMemoryProvider(earlier_reports=...), orearlierin the CLI data file), or moves a date key; its points carryprovider.EarlierRefrefs, and explanations show the earlier report's values;annualise(x, fiscalYearEnd, key).- Analytic functions:
rankand aggregates withover (partition by … order by … window), parsed to theAnalyticnode withWindowandWindowBound. tests/test_grammar_coverage.py: an example for every alternative of the DPM-XL grammar, each parsed and, where it yields a value, evaluated.dpmxl.grammar: keywords, operators, function names and parameter types as frozensets (AXIS_LETTERSforr,c,s),canonical_functionandCALL_NAMESfor theCall.nameof each spelling, and a TextMate grammar ineditors/dpm-xl.tmLanguage.json.
Changed¶
- Breaking:
dpm-xlexits with status 3, not 2, for an evaluation error or unreadable input, so 2 means a usage error only. - The tokenizer splits selections into tokens;
Token.textis the exact source text (quotes, brackets and backticks included) andTokenis a named tuple. Error positions inside selections point at the component. {}is the empty set rather than an empty selection, andIn.valuesmay be any set-valued node, not only aSetLiteral;ValueincludesValueSet.time_shiftis no longer alwaysunsupported: with a provider that hasearlier_reportit is evaluated, andMemoryProviderwithout the requested report reports it with a new message. The columnar evaluator hands everytime_shiftrule to the reference evaluator (raisingColumnarUnsupported) instead of reportingunsupporteditself.- A
_after thet,g,oandpprefixes is cosmetic, as the DPM-XL grammar says:{t_T1}selects tableT1, not_T1.{v_X}stays a filing-indicator test.SelectionSpechastable_groupandoperationfields. children()andwalk()reach thedefaultof a selection, awithcontext and a typed parameter.with,to,where,get,sub,renameandoverare reserved words; escape a key of that name as`to`.a<-1is now a persistent assignment; writea < -1for the comparison.Number.ofraisesValueErrorfordecimalsbeyond the decimal128 range (about -6,100 to 6,100, such as -7000), which 0.1.1 accepted; callers that pass report-level decimals should catch it. The CLI reports it.- Syntax errors read in plain words, such as "expression ends where an operand is expected" and "expected a name, found ')'".
- A
whereorsubon a selection inside awithbody overrides only the context'swhereterms on the same key; the other terms still apply. - Expressions nested more than 50 deep (chained clauses included), and
numbers outside the decimal128 range, are syntax errors;
4.5%%is no longer read as 0.00045. - Explanations round the remaining gap down, so "at least" never overstates it, and keep only the parentheses the rule writes.
Fixed¶
dpm-xl checkandevalreport a rule file path that does not exist instead of reading it as an expression;check --jsonerrors carryend.- A division by zero or type error fails only its own record: a rule is an
error only if that record reaches the result, so guards such as
if {x} != 0 then {y} / {x} > 1 endifwork. evaluate_rulereports deep recursion, decimal overflow and type errors in[sub]as statuserrorinstead of raising.- Arithmetic no longer depends on the caller's decimal context.
- Explanations show the right values after
[rename]. - Explanations of values around 1e33 and larger with
interval: trueno longer raise. ifwithoutelsetreats filing indicators ({v_…}) and Boolean facts as Boolean, as for other conditions.- Items built with an
eba_category match parsed items. - The columnar evaluator hands every case it cannot match exactly to the
reference evaluator: any polars failure, values outside
Decimal(38, 12), numeric open keys, ordering items and text that may be an item. - The CLI accepts
"decimals": "INF"and reports malformed data without a traceback. - The columnar evaluator hands record-level type errors (comparing text with
numbers,
and/not/abson the wrong type, mixedmax/mingroups) to the reference evaluator, which fails only the records concerned. - A non-Boolean condition of a keyed
iffails only its records. - The columnar evaluator no longer orders items without a category as text;
item literals in
<,<=,>,>=and such items in the data go to the reference evaluator, which reports the error.
Performance¶
ifconditions whose branches add keys look records up by key instead of scanning every branch record; repeatedwhereverdicts are reused.
0.1.1 - 2026-10-23¶
Performance¶
- Faster explanations and selections when many rules run against large reports.
0.1.0 - 2026-10-23¶
First release on PyPI.