diff options
Diffstat (limited to 'spec/scripts/gen_ebnf.py')
| -rw-r--r-- | spec/scripts/gen_ebnf.py | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/spec/scripts/gen_ebnf.py b/spec/scripts/gen_ebnf.py new file mode 100644 index 0000000..65824bd --- /dev/null +++ b/spec/scripts/gen_ebnf.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Generate UGI_GRAMMAR_v0.ebnf from ugi_registry_v0.json.""" + +import json +import os +import sys + + +def load_registry(path): + with open(path, "r") as f: + return json.load(f) + + +def main(): + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} <registry.json> <output.ebnf>", file=sys.stderr) + sys.exit(1) + + registry_path, output_path = sys.argv[1], sys.argv[2] + reg = load_registry(registry_path) + fields = reg["fields"] + + lines = [] + lines.append("/* UGI Grammar v0 — W3C EBNF */") + lines.append("/* Generated from ugi_registry_v0.json */") + lines.append("/* Compatible with bottlecaps.de/rr/ui */") + lines.append("") + + # ── Top-level ── + lines.append("/* === Top-level === */") + lines.append("") + lines.append("ugi_string ::= 'ugi:' version '@' handle ':' field_list") + lines.append("") + lines.append("version ::= digit+ ( '/' digit+ )?") + lines.append("") + lines.append("/* handle: alphanumeric, hyphens, underscores, dots */") + lines.append("handle ::= ( alpha | digit | '-' | '_' | '.' )+") + lines.append("") + lines.append("field_list ::= field ( ',' field )*") + lines.append("") + + # ── Field dispatch ── + lines.append("/* === Field dispatch === */") + lines.append("") + + field_alts = [f"field_{code}" for code in sorted(fields.keys())] + # Break into multiple lines for readability + lines.append("field ::= " + "\n | ".join(field_alts)) + lines.append("") + + # ── Common rules ── + lines.append("/* === Common rules === */") + lines.append("") + lines.append("odigit ::= [0-7]") + lines.append("") + lines.append("paid ::= '$'") + lines.append("") + lines.append("aspire ::= '+' odigit") + lines.append("") + lines.append("paid_aspire ::= paid aspire?") + lines.append("") + lines.append("modifier ::= paid_aspire | aspire") + lines.append("") + lines.append("alternative ::= '/' odigit") + lines.append("") + lines.append("/* custom sub-ID: ~ followed by 2+ alpha chars */") + lines.append("custom_sub ::= '~' alpha alpha alpha*") + lines.append("") + lines.append("/* sub-ID: 1-4 alpha chars */") + lines.append("sub_id ::= alpha alpha? alpha? alpha?") + lines.append("") + + # ── Per-field rules ── + lines.append("/* === Per-field rules === */") + lines.append("") + + for code in sorted(fields.keys()): + f = fields[code] + ftype = f["type"] + mods = f.get("modifiers", {}) + has_paid = mods.get("paid", False) + has_aspire = mods.get("aspire", False) + has_alt = mods.get("alternative", False) + + lines.append(f"/* {code} — {f['name']} ({ftype}) */") + + if ftype == "direct": + mod_parts = [] + if has_alt: + mod_parts.append("alternative") + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + lines.append(f"field_{code} ::= '{code}' odigit{mod_str}") + + elif ftype == "multi": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + if code == "l": + # l uses 2-char ISO 639-1 codes instead of generic sub-ids + lines.append( + f"field_{code}_entry ::= alpha alpha odigit{mod_str}" + ) + else: + lines.append( + f"field_{code}_entry ::= ( sub_id | custom_sub ) odigit{mod_str}" + ) + lines.append( + f"field_{code} ::= '{code}' field_{code}_entry+" + ) + + elif ftype == "single": + mod_parts = [] + if has_paid and has_aspire: + mod_parts.append("modifier") + elif has_paid: + mod_parts.append("paid") + elif has_aspire: + mod_parts.append("aspire") + + if mod_parts: + mod_str = " ( " + " | ".join(mod_parts) + " )?" + else: + mod_str = "" + + lines.append( + f"field_{code} ::= '{code}' ( sub_id | custom_sub ) odigit{mod_str}" + ) + + elif ftype == "special": + if code == "g": + lines.append("g_domain ::= alpha alpha alpha? alpha? paid?") + lines.append("g_custom ::= '~' alpha alpha alpha*") + lines.append( + "field_g ::= 'g' ( g_domain | g_custom ) ( '/' ( g_domain | g_custom ) )*" + ) + elif code == "b": + aspire_str = " aspire?" if has_aspire else "" + lines.append( + f"field_b ::= 'b' odigit{aspire_str} '/' odigit{aspire_str}" + ) + elif code == "v": + lines.append("field_v ::= 'v' odigit '/' odigit") + else: + lines.append( + f"field_{code} ::= '{code}' ( alpha | digit | '$' | '+' | '/' | '~' )+" + ) + + lines.append("") + + # ── Block format ── + lines.append("/* === Block format === */") + lines.append("") + lines.append( + "ugi_block ::= block_header newline block_first_line newline " + "block_field_line+ block_footer" + ) + lines.append("") + lines.append("block_header ::= '------- BEGIN UGI BLOCK -------'") + lines.append("block_footer ::= '-------- END UGI BLOCK --------'") + lines.append("") + lines.append( + "block_first_line ::= 'v:' version ' @' handle ( ' ' block_geek_field )?" + ) + lines.append("") + lines.append( + "block_geek_field ::= 'G' ( g_domain | g_custom ) ( '/' ( g_domain | g_custom ) )*" + ) + lines.append("") + lines.append("block_field_line ::= block_field ( ' ' block_field )* newline") + lines.append("") + lines.append( + "block_field ::= alpha ( alpha | digit | '$' | '+' | '/' | '~' )+" + ) + lines.append("") + + # ── Terminals ── + lines.append("/* === Terminals === */") + lines.append("") + lines.append("alpha ::= [a-zA-Z]") + lines.append("digit ::= [0-9]") + lines.append("newline ::= #xD #xA | #xA") + lines.append("") + + output = "\n".join(lines) + + os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) + with open(output_path, "w") as f: + f.write(output) + + print(f"Generated {output_path}") + + +if __name__ == "__main__": + main() |
