|
| 1 | +# ISC .parg port — expressiveness notes and gaps |
| 2 | + |
| 3 | +Status of the port: `grammar/isc.parg` expresses **every rule of the |
| 4 | +canonical Ruby-DSL grammar** (`lib/interscript/isc/grammar/concerns/*.rb` |
| 5 | ++ `core.rb`, origin/main lineage). No construct of the concerns proved |
| 6 | +inexpressible in the parsanol .parg surface. What follows is the precise |
| 7 | +record of the four places the port could not be a verbatim transcription |
| 8 | +and why, plus the porting traps the corpus caught. Validation evidence: |
| 9 | +tools/isc-parg-check parses 319/319 corpus files (318 `maps/` + 1 |
| 10 | +`maps-staging/`) and rejects 4/4 negative fixtures, with verdict parity |
| 11 | +against the Ruby DSL (`Interscript::Isc::Parser.parse_with_dsl`) on every |
| 12 | +file, positive and negative. |
| 13 | + |
| 14 | +## 1. Keyword alternation: shadow-lint-forced reorder (deviation, safe) |
| 15 | + |
| 16 | +- Construct: `rule(:keyword)` in `concerns/items.rb` — the ordered |
| 17 | + literal list of reserved words, used ONLY under `.absent?` (negative |
| 18 | + lookahead) in `alias_reference` and `alias_arg`. |
| 19 | +- Ruby spelling: `... | str("any") | ... | str("any_character")` — |
| 20 | + `"any"` precedes `"any_character"`. |
| 21 | +- Why the port deviates: the parsanol .parg compiler rejects |
| 22 | + prefix-shadowed alternation branches at compile time ("branch 21 |
| 23 | + ("any") shadows branch 34 ("any_character")"), because in a plain |
| 24 | + choice the shorter literal would make the longer unreachable. |
| 25 | +- Resolution: `"any_character"` moved before `"any"` (longest-first), |
| 26 | + as the lint instructs. |
| 27 | +- Why it is safe: `keyword` appears only under `!`. For a negative |
| 28 | + lookahead, every branch ordering is equivalent — the predicate fails |
| 29 | + whenever ANY branch matches at the position, and the two branch sets |
| 30 | + are identical. No corpus input changes verdict under the reorder. |
| 31 | + |
| 32 | +## 2. Dead concern rules: omitted (not a .parg limitation) |
| 33 | + |
| 34 | +- Construct: `authority_field`, `source_spelling_field`, |
| 35 | + `target_spelling_field`, `identifying_field`, `name_field` in |
| 36 | + `concerns/metadata.rb`. |
| 37 | +- Why omitted: none of them is reachable from `metadata_field` (they |
| 38 | + are defined but never listed in its alternation). In the Ruby DSL |
| 39 | + they are dead code; an `authority "X"` line parses via |
| 40 | + `generic_field` (identifier + quoted value), same as here. |
| 41 | +- Consequence: none for acceptance — a bare `authority "..."` / |
| 42 | + `name "..."` metadata line parses identically through |
| 43 | + `generic_field` on both engines (verified on the corpus, where |
| 44 | + `authority_id`, `name`, `url`, ... all flow through the generic |
| 45 | + branch). |
| 46 | + |
| 47 | +## 3. Unreachable lexical helpers: not carried over |
| 48 | + |
| 49 | +- `space?`, `newlines`, `newlines?` (`concerns/primitives.rb`) and |
| 50 | + `list_item` (`concerns/items.rb`, an alias of `item`) are defined but |
| 51 | + never referenced by a reachable rule. They are folded into their use |
| 52 | + sites here (`wsq`, `list_items`). `braced(inner)` is a Ruby method, |
| 53 | + not a rule; every use site spells the scaffold inline. |
| 54 | + |
| 55 | +## 4. The `field_value` repeat-binding trap (expressed; first port got it wrong) |
| 56 | + |
| 57 | +- Construct: `rule(:field_value)` in `concerns/metadata.rb`: |
| 58 | + `(newline.absent? >> (str("}").absent? >> str("{").absent? >> any)).repeat(1)`. |
| 59 | +- Trap: the `.repeat(1)` binds over the WHOLE parenthesized group, so |
| 60 | + `newline.absent?` is re-checked per iteration — the raw value run |
| 61 | + stops AT the newline (`din-hye`: `authority_id din` captures `raw: |
| 62 | + "din"`, not the whole metadata block). |
| 63 | +- Port: `field_raw = 1*(!newline !"}" !"{" chr)`. The first draft wrote |
| 64 | + `!newline` outside the repetition; the run then swallowed fields up |
| 65 | + to the next `{` and `din-hye-Armn-Latn-32706-2010.isc` failed. The |
| 66 | + harness caught it; the per-iteration guard matches the Ruby |
| 67 | + semantics. |
| 68 | + |
| 69 | +## 5. Related semantics fixed while porting (recorded for reviewers) |
| 70 | + |
| 71 | +- The DSL's `any` matches ANY character including raw newlines — |
| 72 | + multiline `description { ... }` bodies (raw_text) and quoted strings |
| 73 | + cross line breaks. `chr = %x00-10FFFF` here; newline stops exist |
| 74 | + ONLY where the concerns put an explicit `newline.absent?` |
| 75 | + (`line_comment`, `field_raw`, stages' `comment_item`). |
| 76 | +- Escape rulings from the concerns are mirrored: `\uXXXX` exactly 4 |
| 77 | + hex; `\U` 4..8 hex (`gki-bel "\U040E"`); a lone `\U` accepted |
| 78 | + (un-mar converter corruption). Acceptance parity holds on all three |
| 79 | + corpus shapes. |
| 80 | +- The root rule pins full-input consumption with an explicit `!utf` |
| 81 | + (`isc_source`), so the harness rejects trailing garbage without |
| 82 | + relying on engine EOF behavior. |
| 83 | + |
| 84 | +## Not a gap, but out of harness scope |
| 85 | + |
| 86 | +The harness gates ACCEPTANCE (parse/reject verdicts). The capture-tree |
| 87 | +SHAPE produced by a .parg artifact is parg-shaped (`cap` nodes); the |
| 88 | +gem's `Isc::Normalizer` owns DSL-tree equivalence for the artifact |
| 89 | +runtime. Verifying shape parity byte-for-byte against |
| 90 | +`parse_with_dsl` trees is the gem's corpus gate's job, not this |
| 91 | +harness's. |
0 commit comments