|
| 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. |
0 commit comments