Skip to content

Commit eaafb86

Browse files
authored
Merge pull request #1 from regtab/feature/embedded-rtl-dsl
Embedded RTL DSL (pyregtab.dsl), release 0.2.0
2 parents 45015ed + 61e8cf0 commit eaafb86

13 files changed

Lines changed: 1943 additions & 14 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyregtab"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "Native core of pyRegTab: RTL compiler, ATP matcher and table interpreter"
66
license = "MIT"

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ TableSyntax → RtlCompiler/TablePattern → AtpMatcher → TableInterpreter →
1212
```
1313

1414
**pyRegTab 0.1.x ≙ jRegTab 0.4.0** (same API, same semantics, same test
15-
corpus).
15+
corpus). **0.2.0** adds the embedded RTL DSL (`pyregtab.dsl`), mirroring
16+
jRegTab's `ru.icc.regtab.dsl`.
1617

1718
## Installation
1819

@@ -52,6 +53,9 @@ Patterns can also be built without RTL, via the fluent spec API
5253
snake_case method names), and serialized back to RTL with
5354
`AtpToRtlSerializer.serialize(pattern)`.
5455

56+
For a terser, RTL-like way to build patterns in code, use the **embedded RTL**
57+
DSL (`pyregtab.dsl`) — see [Embedded RTL](#embedded-rtl) below.
58+
5559
Named Python predicates are attached to RTL via `EXT('name')`:
5660

5761
```python
@@ -63,6 +67,32 @@ p = RtlCompiler.compile(
6367
)
6468
```
6569

70+
## Embedded RTL
71+
72+
The `pyregtab.dsl` module is a fluent DSL that reads almost like RTL but is
73+
ordinary Python — with IDE completion, structural typing, pattern composition
74+
via plain variables, and Python callables as escape-hatch constraints. It builds
75+
the **same `TablePattern`** objects as the compiler (verified byte-for-byte
76+
against `RtlCompiler.compile` for a representative set of tasks in
77+
`tests/test_dsl.py`).
78+
79+
```python
80+
from pyregtab.dsl import *
81+
82+
# RTL: { [ [VAL : ST*->REC] [VAL]{2} []+ ]
83+
# [ [] [VAL]{4} []+ ] }+
84+
p = table(
85+
subtable(
86+
row(cell(VAL, rec(ST.unbounded())), cell(VAL).exactly(2), skip().one_or_more()),
87+
row(skip(), cell(VAL).exactly(4), skip().one_or_more()),
88+
).one_or_more())
89+
```
90+
91+
Method names are snake_case (`.one_or_more()`, `.and_()`, `.split_by()`); the
92+
vocabulary constants (`VAL`, `ST`, `COL`, `C(n)`, …) match RTL. See the
93+
[Embedded RTL guide](docs/embedded-rtl.md) for the full mapping and the
94+
`where(...)` escape hatch.
95+
6696
## API mapping (Java → Python)
6797

6898
| Java | Python |
@@ -104,14 +134,17 @@ Rust (`pyregtab._core`, built with [PyO3](https://pyo3.rs) and
104134

105135
## Testing
106136

107-
`pytest tests` runs (1 878 tests):
137+
`pytest tests` runs (1 904 tests):
108138

109139
- the full benchmark suite — tasks 001–150 (Foofah, RegTab, Baikal),
110140
every fixture variant, **both** via RTL patterns and via ATP patterns
111141
built with the Python spec API (1 500 task variants in total; fixtures
112142
are copied verbatim from jRegTab into `tests/fixtures/tasks`, ATP
113143
builders are mechanically translated from the Java tests by
114144
`tools/translate_atp.py`);
145+
- embedded RTL DSL parity — 26 representative tasks/constructs built with
146+
`pyregtab.dsl` produce byte-identical ATP to `RtlCompiler.compile`
147+
(`tests/test_dsl.py`);
115148
- the RTL conformance corpus (positive canonical forms, fixed points,
116149
negative rejections);
117150
- RTL↔ATP round-trip for tasks 001–050;

docs/api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ cell predicates receive a `Cell`, item filters receive
101101
predicates cannot be serialized back to RTL (as in Java) and disable the
102102
GIL-release fast path.
103103

104+
## Embedded RTL DSL
105+
106+
`pyregtab.dsl` is a fluent, RTL-like layer over the ATP factories above:
107+
`table/subtable/row/subrow/cell`, atoms `val/attr/aux`, actions `rec/avp/join/
108+
fill/prefix/suffix`, provider constants (`ST`, `COL`, `C(n)`, …) with
109+
`.and_()/.or_()/.card()/.unbounded()` and traversal methods, and `where(...)`
110+
Python-callable escape hatches. It builds ordinary `TablePattern` objects,
111+
byte-identical to `RtlCompiler.compile` for lambda-free patterns. See the
112+
[Embedded RTL guide](embedded-rtl.md).
113+
104114
## Recordset transformations
105115

106116
`WhitespaceNormalization()`, `AnchorAttributeAtPosition(pos)`,

docs/embedded-rtl.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Embedded RTL
2+
3+
**Embedded RTL** is a Python DSL (module `pyregtab.dsl`) that mirrors RTL syntax while
4+
remaining ordinary Python code. It combines the brevity of RTL with full Python integration:
5+
callables as constraints, IDE completion, and pattern composition with plain variables and
6+
functions.
7+
8+
Patterns built with embedded RTL produce **exactly the same `TablePattern` objects** as
9+
`RtlCompiler.compile(...)` — this equivalence is verified task-by-task for a representative
10+
subset of the benchmark corpus (`tests/test_dsl.py`): for every lambda-free pattern the DSL
11+
builds a structurally identical ATP and serializes to the same canonical RTL.
12+
13+
```python
14+
from pyregtab.dsl import *
15+
```
16+
17+
## Quick example
18+
19+
RTL (task 001):
20+
21+
```
22+
{ [ [VAL : ST*->REC] [VAL]{2} []+ ]
23+
[ [] [VAL]{4} []+ ] }+
24+
```
25+
26+
Embedded RTL:
27+
28+
```python
29+
p = table(
30+
subtable(
31+
row(cell(VAL, rec(ST.unbounded())), cell(VAL).exactly(2), skip().one_or_more()),
32+
row(skip(), cell(VAL).exactly(4), skip().one_or_more()),
33+
).one_or_more())
34+
```
35+
36+
The result is a plain `pyregtab.TablePattern` — match and interpret it exactly like any other:
37+
38+
```python
39+
from pyregtab import AtpMatcher, TableInterpreter
40+
41+
itm = AtpMatcher.match(p, syntax)
42+
rs = TableInterpreter().interpret(itm)
43+
```
44+
45+
---
46+
47+
## Vocabulary
48+
49+
### Pattern levels and quantifiers
50+
51+
| RTL | Embedded RTL |
52+
|---|---|
53+
| table pattern | `table(subtable…)` |
54+
| `{ rows }` subtable | `subtable(row…)` |
55+
| `[ cells ]` row | `row(cell…)` |
56+
| `{ cells }` explicit subrow | `subrow(cell…)`, mixed with implicit runs: `row(subrow(…), subrow(…))` |
57+
| `+` `*` `?` `{n}` | postfix `.one_or_more()` `.zero_or_more()` `.zero_or_one()` `.exactly(n)` |
58+
59+
A row with plain cells (`row(cell…)`) wraps them into one implicit subrow, exactly like the
60+
compiler.
61+
62+
### Cells and guards
63+
64+
| RTL | Embedded RTL |
65+
|---|---|
66+
| `[]` | `skip()` |
67+
| `[VAL : acts]` | `cell(VAL, acts…)` |
68+
| `[!BLANK ? VAL : acts]` | `cell(not_blank(), VAL, acts…)` |
69+
| `[BLANK]` (condition-only) | `cell(blank())` |
70+
| guards `BLANK` `"re"` `~"s"` (+ `!`) | `blank()` `re("…")` `contains("…")` / `not_blank()` `not_re("…")` `not_contains("…")` |
71+
| — (escape hatch) | `cell(where("desc", lambda c: …), VAL)` |
72+
73+
### Content specifications
74+
75+
| RTL | Embedded RTL |
76+
|---|---|
77+
| `VAL` / `ATTR` / `AUX` / `_` | constants `VAL ATTR AUX SKIP`; atoms `val(acts…)` `attr(…)` `aux(…)` |
78+
| `VAL #'tag'` | `val(…).tagged("tag")` |
79+
| `VAL = NORM` (also `TRIM UC LC REPL SUBSTR`, chains) | `val(…).extract(NORM)`, `repl("rx","rep")`, `substr(b,e)`, `chain(…)` |
80+
| compound `A "d" B` | `val(…).then(" ", val(…))` (chainable) |
81+
| delimited `(VAL : …){","}` | `val(…).split_by(",")` |
82+
| conditional `BLANK ? _ \| VAL` | `when(blank(), SKIP, VAL)` (directives and specs mix freely) |
83+
84+
### Providers and constraints
85+
86+
| RTL | Embedded RTL |
87+
|---|---|
88+
| `LT RT AV BW ROW COL SR SC ST NCL CL STR` | constants with the same names (type `Prov`) |
89+
| `Cn` / `Ca..b` / `C+n`, `C-n` / `C+a..b` / `C+a..*` | `C(n)` / `C(lo,hi)` / `Crel(±n)` / `Crel(lo,hi)` / `CrelFrom(lo)` |
90+
| `Rn Ra..b R±n`, `Pn Pa..b P±n` | `R(…)`, `Rrel(…)`, `P(…)`, `Prel(…)` |
91+
| `#'t'` / `!#'t'` | `tag("t")` / `not_tag("t")` |
92+
| item `"re"`, `~"s"`, `BLANK` (+ `!`) | `item_re item_contains item_blank` / `item_not_re item_not_contains item_not_blank` |
93+
| `&` conjunction | `.and_(…)` |
94+
| `\|` disjunction | `.or_(…)`; distribution matches the compiler: `A.and_(B.or_(C))``(A&B)\|(A&C)` |
95+
| `{n}` / `*` cardinality | `.card(n)` / `.unbounded()` |
96+
| `-` / `^` / `-^` traversal | `.reversed()` / `.col_major()` / `.reversed_col_major()` |
97+
| — (escape hatch) | `ROW.where("desc", lambda anchor, candidate: …)` |
98+
99+
!!! note "Why `C(n)` and `Crel(n)` are separate"
100+
`C(n)` and `C(lo, hi)` are absolute column constraints (one column, or a range); `Crel`
101+
takes a signed anchor-relative delta: `Crel(1)``C+1`, `Crel(-1)``C-1`, and
102+
`Crel(lo, hi)` / `CrelFrom(lo)` give relative and open-ended relative ranges. The naming
103+
mirrors jRegTab's, where absolute and relative forms must be distinct factories.
104+
105+
### Actions and context providers
106+
107+
| RTL | Embedded RTL |
108+
|---|---|
109+
| `(…)->REC` / `REC(n)` / `REC('s')` | `rec(…)` / `rec(n, …)` / `rec_split("s", …)` |
110+
| `prov->AVP` / `'NAME'->AVP` | `avp(prov)` / `avp("NAME")` |
111+
| `(…)->JOIN` / `JOIN(k)` | `join(…)` / `join(k, …)` |
112+
| `(…)->FILL('d')`, `PREFIX`, `SUFFIX` | `fill("d", …)`, `prefix(…)`, `suffix(…)` (delimiter optional) |
113+
| `'EUR'` context literal | `lit("EUR")` (VALUE under REC/JOIN, ATTRIBUTE otherwise — as in the compiler) |
114+
| `@'K'='V'` | `ctx_avp("K", "V")` |
115+
116+
Provider kinds (VAL/ATTR/UNRESTRICTED) are inferred from the action, exactly as in the RTL
117+
compiler — `rec(ST)` builds a VAL provider, `avp(SC)` an ATTR provider.
118+
119+
### Level-scoped (inherited) actions and conditions
120+
121+
RTL allows action specs and a condition at the table, subtable, row, and subrow level; they are
122+
merged down into every atom below. Embedded RTL mirrors this with `acts(…)` and an optional
123+
leading `CellPredicate`:
124+
125+
```python
126+
# RTL: [ BW*->REC { [ATTR] [VAL] }* ]
127+
row(acts(rec(BW.unbounded())),
128+
subrow(cell(ATTR), cell(VAL)).zero_or_more())
129+
130+
# RTL: !BLANK ? BW*->REC [ [VAL] ]+
131+
table(not_blank(), acts(rec(BW.unbounded())), subtable(row(cell(VAL)).one_or_more()))
132+
```
133+
134+
### Settings
135+
136+
The RTL settings prefix maps to recordset transformations:
137+
138+
```python
139+
# RTL: <NORM,ANCH(1),SPLIT(",")> …
140+
table(…).with_transformations(norm(), anch(1), split(","))
141+
```
142+
143+
### Fragments are just Python
144+
145+
RTL named fragments `$name=[…]` become plain variables — and gain parameterisation for free:
146+
147+
```python
148+
# RTL: $V=['\d+' ? VAL: (COL&#'H'*,ROW&#'S'*)->REC] … [$V]+
149+
v = cell(re(r"\d+"), VAL, rec(COL.and_(tag("H")).unbounded(),
150+
ROW.and_(tag("S")).unbounded()))
151+
row(cell(blank()).one_or_more(), v.one_or_more())
152+
```
153+
154+
---
155+
156+
## Escape hatches into Python
157+
158+
The reason embedded RTL exists: anywhere the model accepts a predicate, a plain Python callable
159+
works.
160+
161+
```python
162+
p = table(subtable(row(
163+
cell(where("is_total", lambda c: c.text.startswith("Total")), VAL,
164+
rec(ROW.where("is_num", lambda a, c: c.str.isdigit()).unbounded())),
165+
cell(VAL).one_or_more())))
166+
```
167+
168+
Patterns containing `where(...)` **cannot be serialized back to RTL** (an opaque Python callable
169+
has no RTL analog) and two patterns with distinct callables never compare equal. The serializable
170+
alternative is a named `EXT('name')` binding in the string compiler, which round-trips:
171+
172+
```python
173+
from pyregtab import RtlCompiler, Bindings
174+
175+
p = RtlCompiler.compile(
176+
"{ [ [EXT('is_total') ? VAL : ST*->REC] []+ ] }+",
177+
Bindings.of().cell("is_total", lambda cell: cell.text.startswith("Total")),
178+
)
179+
```
180+
181+
---
182+
183+
## Relation to the other two APIs
184+
185+
| | RTL string | Embedded RTL | ATP API |
186+
|---|---|---|---|
187+
| Brevity | ✅✅ |||
188+
| Python callables | via `EXT` + `Bindings` | ✅ direct | ✅ direct |
189+
| IDE completion / structural typing ||||
190+
| Layer | compiles to ATP | thin sugar over ATP | the model itself |
191+
192+
Embedded RTL is a construction layer only — it adds no expressive power beyond ATP, and every
193+
pattern it builds is an ordinary `TablePattern`. The [ATP API](model/atp.md) remains the
194+
documented low-level layer.

docs/getting-started.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ for record in rs.records:
150150
## What's next
151151

152152
- [RTL reference](rtl-reference.md) — complete syntax for the RTL DSL.
153+
- [Embedded RTL](embedded-rtl.md) — build the same patterns fluently in Python (`pyregtab.dsl`).
153154
- [ITM](model/itm.md) — syntactic and semantic layers, working state, table interpretation.
154155
- [ATP](model/atp.md) — pattern hierarchy, content specs, matching algorithm.
155156
- [API reference](api.md) — public classes and methods.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ nav:
3939
- Home: index.md
4040
- Getting started: getting-started.md
4141
- RTL reference: rtl-reference.md
42+
- Embedded RTL: embedded-rtl.md
4243
- IDE support: ide-support.md
4344
- Formal model:
4445
- ITM: model/itm.md

0 commit comments

Comments
 (0)