interscript 0.1.0

Rust runtime for Interscript ISC maps, executing the parsanol .parg grammar
Documentation
# Interscript ISC grammar — portable .parg (parsanol grammar language).
#
# A 1:1 port of the canonical Ruby-DSL grammar in
# lib/interscript/isc/grammar/concerns/*.rb (+ core.rb). Every rule in
# this file carries a comment naming its concern rule of origin. The
# port is semantic, not cosmetic — the parsanol .parg surface has no
# `.maybe`/`.repeat`/`.absent?` combinators, so they map as:
#
#   Ruby                     .parg
#   -----------------------  --------------------------------
#   x.repeat(0) / repeat     *x
#   x.repeat(1)              1*x
#   x.maybe                  [x]
#   x.absent?                !x
#   x.present?               &x
#   a | b                    a / b            (ordered choice, PEG)
#   a >> b                   a b
#   expr.as(:name)           (expr) as name
#
# Two semantics worth calling out because the corpus depends on them:
#
# * The Ruby DSL's `any` matches ANY character including raw newlines
#   (multiline `description { ... }` bodies parse). `chr` here is that
#   true any-character matcher. Wherever the concerns stop at a newline
#   they do it with an EXPLICIT newline.absent? — mirrored verbatim
#   below (line_comment, field_raw, comment_item).
# * Choice is ordered (PEG), exactly like the Ruby alternation — the
#   alternative order below is copied verbatim from the concerns and is
#   load-bearing (item_atom, metadata_field, stage_item, ...).
#
# Consumed by tools/isc-parg-check (parsanol-rs validation harness).

grammar InterscriptIsc version "1.0.0" edition "v1" {

  ## Interscript ISC map language, ported from the Ruby-DSL concerns.

  # ---------------------------------------------------------------------
  # Concerns::Primitives — whitespace, comments, identifiers, strings
  # ---------------------------------------------------------------------

  # The DSL's `any` — every Unicode scalar value, raw newlines included.
  utf = %x00-10FFFF
  chr = utf

  # rule(:newline) — "\n" | "\r\n" | "\r"
  newline = %x0A / %x0D.0A / %x0D

  # rule(:line_comment) — "#" >> (newline.absent? >> any).repeat
  # (stops at CR too — the absent? is on the full newline rule)
  line_comment = "#" *(!newline chr)

  # rule(:space) — /\s/ is [ \t\r\n\f\v]
  space = 1*(%x20 / %x09 / %x0A / %x0B / %x0C / %x0D)

  # rule(:whitespace) / rule(:whitespace?)
  whitespace = 1*(space / line_comment)
  wsq = [whitespace]

  # rule(:inline_space) / rule(:inline_whitespace) / inline_whitespace?
  inline_space = 1*(%x20 / %x09)
  inline_whitespace = 1*(inline_space / line_comment)
  iwsq = [inline_whitespace]

  # rule(:comma) — "," with trailing whitespace allowed
  comma = "," [whitespace]

  # rule(:arrow) — whitespace? >> "->" >> whitespace?
  arrow = [whitespace] "->" [whitespace]

  # rule(:identifier)
  ident_first = %x41-5A / %x61-7A / %x5F
  ident_rest = %x41-5A / %x61-7A / %x30-39 / %x5F
  identifier = (ident_first 0*ident_rest) as identifier

  # rule(:escape_sequence) — the corpus rulings are in the concerns:
  # \uXXXX (exactly 4), \U with 4..8 hex (gki-bel "\U040E"), and a lone
  # \U (un-mar converter corruption).
  esc_nl = ("n") as newline
  esc_cr = ("r") as carriage_return
  esc_tab = ("t") as tab
  esc_dq = (%x22) as dquote
  esc_bs = (%x5C) as backslash
  esc_uni4 = "u" (4hexd) as unicode
  esc_uni = "U" (4*8hexd) as unicode
  esc_u_lone = ("U") as u_lone
  hexd = %x30-39 / %x41-46 / %x61-66
  escape_sequence = %x5C (esc_nl / esc_cr / esc_tab / esc_dq / esc_bs / esc_uni4 / esc_uni / esc_u_lone)

  # rule(:plain_run) — escape-free stretch of a double-quoted string
  plain_run = (!%x22 !%x5C chr)

  # rule(:double_quoted_string) — runs captured as slices
  dq_part = escape_sequence / (1*plain_run as run)
  double_quoted_string = %x22 (*(dq_part) as string) %x22

  # rule(:single_quoted_string) — no escape interpretation
  single_quoted_string = %x27 (*( !%x27 chr ) as string) %x27

  # rule(:quoted_string)
  quoted_string = double_quoted_string / single_quoted_string

  # braced(inner) is a Ruby method (scaffold), not a rule: every use
  # site below spells "{" [whitespace] inner [whitespace] "}" inline.

  # ---------------------------------------------------------------------
  # Concerns::Items — item expressions
  # ---------------------------------------------------------------------

  # rule(:keyword) — the concern's ordered list. One deviation, forced
  # by the parg compiler's shadow lint: "any_character" must precede
  # "any" (longest-first). Semantically identical — keyword is used
  # only under negative lookahead, where any matching branch suffices.
  keyword = ("parallel" / "sequence" / "stage" / "compose" / "separate" / "system" / "metadata" / "aliases" / "tests" / "notes" / "description" / "name" / "authority" / "dependency" / "run" / "sub" / "before" / "after" / "not_before" / "not_after" / "any_character" / "any" / "none" / "boundary" / "line_start" / "line_end" / "word_boundary" / "downcase" / "upcase" / "title_case" / "capture" / "maybe" / "some" / "ref")

  # rule(:zero_width_primitive)
  zw_primitive = (("boundary" / "line_start" / "line_end" / "word_boundary" / "space" / "non_boundary")) as primitive

  # rule(:function_call)
  fn_lit = (("upcase" / "downcase" / "title_case" / "reverse" / "strip" / "swapcase")) as function

  # rule(:any_character)
  any_char_lit = ("any_character") as any_char

  # str("none").as(:none)
  none_lit = ("none") as none

  # rule(:capture_reference) — ref(N), single digit
  digit = %x30-39
  capture_reference = (("ref" "(" [whitespace] (digit as digit) [whitespace] ")")) as ref

  # rule(:alias_reference) — identifier with optional .qualified part,
  # rejected when a keyword or followed by "{".
  alias_reference = ((!(keyword) identifier [("." (identifier as qualified_name))] !"{")) as alias

  # rule(:range_arg)
  range_arg = ((quoted_string) as lo) [whitespace] ".." [whitespace] ((quoted_string) as hi)

  # rule(:set_arg) — quoted single, or [ item, item, ... ]
  list_items = item 0*((comma / whitespace) item)
  set_arg = ((quoted_string) as single) / ("[" [whitespace] ((list_items) as list) [whitespace] "]")

  # rule(:alias_arg) — bare alias inside any(...)
  alias_arg = ((!(zw_primitive) !(keyword) identifier)) as alias_ref

  # rule(:any_constructor)
  any_ctor = "any" "(" [whitespace] ((range_arg / set_arg / alias_arg / (item as any_item)) as any) [whitespace] ")"

  # rule(:capture_constructor) / maybe / some
  capture_ctor = "capture" "(" [whitespace] ((item) as capture_inner) [whitespace] ")"
  maybe_ctor = "maybe" "(" [whitespace] ((item) as maybe_inner) [whitespace] ")"
  some_ctor = "some" "(" [whitespace] ((item) as some_inner) [whitespace] ")"

  # rule(:item_atom) — ordered exactly as the concern
  item_atom = quoted_string / none_lit / zw_primitive / any_char_lit / any_ctor / capture_ctor / maybe_ctor / some_ctor / fn_lit / capture_reference / alias_reference

  # rule(:item_continuation) — lookahead only: not a block-rule keyword,
  # not `identifier =` (an alias declaration), and starts like an atom.
  item_atom_start = %x22 / %x27 / ident_first
  item_continuation = !("to" / "before" / "after" / "not_before" / "not_after" / "}") (!(identifier [whitespace] "=") item_atom_start)

  # rule(:concat_sep) — "+" with optional ws, or required whitespace
  concat_sep = ([whitespace] "+" [whitespace]) / whitespace

  # rule(:item) — concatenation, one or more atoms
  item = ((item_atom 0*((concat_sep &(item_continuation)) item_atom))) as concatenation

  # ---------------------------------------------------------------------
  # Concerns::Items — constraints
  # ---------------------------------------------------------------------

  # rule(:constraint) — order verbatim: before | after | not_before | not_after
  constraint = ("before" whitespace ((item) as before)) / ("after" whitespace ((item) as after)) / ("not_before" whitespace ((item) as not_before)) / ("not_after" whitespace ((item) as not_after))

  # rule(:constraints) — (whitespace >> constraint).repeat
  constraints = (*(whitespace constraint)) as constraints

  # ---------------------------------------------------------------------
  # Concerns::Stages — rules, directives, stage bodies
  # ---------------------------------------------------------------------

  # rule(:compact_rule) — from/to are single atoms
  compact_rule = "sub" whitespace ((item_atom) as from) whitespace ((item_atom) as to) constraints

  # rule(:block_rule) — from/to are concatenations
  block_rule = "sub" [whitespace] "{" [whitespace] "from" whitespace ((item) as from) [whitespace] "to" whitespace ((item) as to) constraints [whitespace] "}"

  # rule(:rule) — block | compact, ordered
  rule = block_rule / compact_rule

  # rule(:comment_item) — strict parse: comments only, no stray
  # identifiers (WO87/A4 ruling).
  comment_item = ("#" *(!%x0A chr)) as comment

  # rule(:bare_rule) — legacy .imp compatibility
  bare_rule = ((rule)) as bare_rule

  # rule(:sequence_block) / rule(:parallel_block)
  rule_line = [whitespace] (rule / comment_item) [whitespace]
  rule_items = *(rule_line)
  sequence_block = "sequence" [whitespace] "{" [whitespace] ((rule_items) as sequence) [whitespace] "}"
  parallel_block = "parallel" [whitespace] "{" [whitespace] ((rule_items) as parallel) [whitespace] "}"

  # rule(:separate_directive)
  separate_directive = (("separate")) as separate [whitespace "separator" whitespace ((item_atom) as separator)]

  # rule(:string_case_directive) — title_case takes optional kwargs
  kwarg = ((identifier) as kwarg_name) [whitespace] ":" [whitespace] ((quoted_string) as kwarg_value)
  kwarg_list = ((kwarg) as kwarg) 0*(("," [whitespace]) ((kwarg) as kwarg))
  string_case_directive = (("downcase" / "upcase")) as case / (("title_case") as case [whitespace ((kwarg_list) as case_kwargs)])

  # rule(:compose_directive)
  compose_directive = (("compose" / "decompose")) as compose

  # rule(:rababa_directive)
  rababa_directive = (("rababa")) as funcall_name [whitespace ((kwarg_list) as funcall_kwargs)]

  # rule(:run_rule) — map.<dep>.stage.<stage> | stage.<stage>
  run_rule = "run" whitespace (("map." ((identifier) as dep) ".stage." ((identifier) as stage)) / (("stage." ((identifier) as stage))) as run_stage_only)

  # rule(:stage_item) — order verbatim
  stage_item = [whitespace] (sequence_block / parallel_block / run_rule / separate_directive / string_case_directive / compose_directive / rababa_directive / bare_rule / comment_item) [whitespace]
  stage_items = *(stage_item)

  # rule(:stage_block) — stage(name) | stage name
  stage_block = "stage" (( "(" ((identifier) as stage_name) ")" ) / (whitespace ((identifier) as stage_name))) [whitespace] "{" [whitespace] ((stage_items) as stage) [whitespace] "}"

  # ---------------------------------------------------------------------
  # Concerns::Tests
  # ---------------------------------------------------------------------

  # rule(:test_line)
  test_line = [whitespace] ((quoted_string) as input) arrow ((quoted_string) as expected) [(whitespace "note" whitespace ((quoted_string) as note))] [whitespace]
  test_items = *(test_line)
  tests_block = "tests" [whitespace] "{" [whitespace] ((test_items) as tests) [whitespace] "}"

  # ---------------------------------------------------------------------
  # Concerns::Metadata
  # ---------------------------------------------------------------------

  # rule(:raw_text)
  raw_text = *( %x5C "{" / %x5C "}" / (!"}" chr) )

  # rule(:description_field)
  description_field = "description" [whitespace] "{" [whitespace] ((raw_text) as description) [whitespace] "}"

  # rule(:relations_field) / relation_block / relation_type
  relation_type = "supersedes" / "superseded_by" / "based_on" / "basis_for" / "alias_of" / "adopted_from" / "related_to"
  relation_block = [whitespace] ((relation_type)) as type whitespace ((quoted_string)) as system [(whitespace "note" whitespace ((quoted_string) as note))] [whitespace]
  relations_block = "relations" [whitespace] "{" [whitespace] ((*(relation_block)) as relations) [whitespace] "}"

  # rule(:notes_field) / note_line
  note_line = [whitespace] "note" [whitespace] ((quoted_string) as note) [whitespace]
  notes_field = "notes" [whitespace] "{" [whitespace] ((*(note_line)) as notes) [whitespace] "}"

  # rule(:system_status_field) / code_status_field
  system_status_field = "system_status" whitespace (("current" / "former" / "inactive")) as system_status
  code_status_field = "code_status" whitespace (("preferred" / "proposed" / "deprecated")) as code_status

  # rule(:specification_field)
  spec_value = (quoted_string) as specification
  specification_field = "specification" whitespace ((spec_value 0*((comma) (spec_value))) as specification)

  # rule(:provenance_field)
  provenance_value = (quoted_string) as provenance
  provenance_field = "provenance" [whitespace] ((provenance_value 0*((comma) (provenance_value))) as provenance)

  # rule(:generic_field) / empty_field / field_value
  empty_field = (&newline / &"}")
  field_raw = 1*(!newline !"}" !"{" chr)
  field_value = ((quoted_string / field_raw)) as field_value
  field_block = "{" [whitespace] ((raw_text) as field_block) [whitespace] "}"
  generic_field = ((identifier) as field_name) iwsq (empty_field / field_value / field_block)

  # rule(:metadata_field) — order verbatim. The concern's dead rules
  # (authority_field, source/target_spelling_field, identifying_field,
  # name_field) are unreachable from metadata_field and are subsumed by
  # generic_field here; see grammar/ISC-PARG-GAPS.md.
  metadata_field = [whitespace] (description_field / relations_block / system_status_field / code_status_field / specification_field / notes_field / provenance_field / generic_field) [whitespace]
  metadata_items = *(metadata_field)
  metadata_block = "metadata" [whitespace] "{" [whitespace] ((metadata_items) as metadata) [whitespace] "}"

  # ---------------------------------------------------------------------
  # Concerns::Aliases
  # ---------------------------------------------------------------------

  # rule(:alias_decl)
  alias_decl = [whitespace] ((identifier) as name) [whitespace] "=" [whitespace] ((item) as value) [whitespace]
  alias_items = *(alias_decl)
  aliases_block = "aliases" [whitespace] "{" [whitespace] ((alias_items) as aliases) [whitespace] "}"

  # ---------------------------------------------------------------------
  # Concerns::Dependencies
  # ---------------------------------------------------------------------

  # rule(:dependency_decl)
  dependency_decl = "dependency" whitespace ((quoted_string) as target) [(whitespace "as" whitespace ((identifier) as alias))]

  # ---------------------------------------------------------------------
  # Concerns::System
  # ---------------------------------------------------------------------

  # rule(:block_item) — order verbatim
  block_item = [whitespace] (metadata_block / aliases_block / tests_block / stage_block / dependency_decl) [whitespace]

  # rule(:system_block)
  system_block = "system" whitespace ((quoted_string) as system_code) [whitespace] "{" [whitespace] ((*(block_item)) as body) [whitespace] "}" [whitespace]

  # rule(:isc_source) — root; !utf pins full-input consumption
  isc_source = [whitespace] ((system_block)) as system [whitespace] !utf

  entry isc: isc_source

  test isc {
    accept "system \"T:a-b:c-d:1\" { stage main { parallel { sub \"a\" \"b\" } } }"
    accept "system \"T:a-b:c-d:1\" { metadata { } tests { } }"
    accept "system \"T:a-b:c-d:1\" { stage main { sub { from \"x\" to \"y\" before boundary } } }"
    accept "system \"T:a-b:c-d:1\" { stage main { sub { from \"e\" to \"f\" after any(\"aeiou\") } } }"
    reject "system \"T:a-b:c-d:1\" {"
    reject "system \"T:a-b:c-d:1\" { stage main { sub \"a\" } }"
    reject "system \"T:a-b:c-d:1\" { stage main { oops } }"
    reject "system \"T:a-b:c-d:1\" }"
  }
}