asciichem 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f8a0ef9952b9507c35ded04295298a3a62e86a7efce1f92d258a1aaf6d6125c
4
- data.tar.gz: 0d11ccfb16383194e8a1fd3bf0816ce8b8465b548bc6dac43535d2633a0cf2ba
3
+ metadata.gz: 6c1ac898f589ce81ddaa9d9b9a06f4540603b81bf1196f0a29b5b65468c0e4b0
4
+ data.tar.gz: 436d78daf7a738ea821ae9d936096abfbb01a4de07d51fb83152aa43998e6e7f
5
5
  SHA512:
6
- metadata.gz: 270015bf0f88f0bc2f06452a472759970b39ab75ae9aef233ea3c9f94f7b3abc2d20752098151aedd7ab51251aaad9414cfdb349167c059ac036e8bbdaff04d5
7
- data.tar.gz: a4211637fc504c7db50994d14b151bb5e45357b48d870b4f4aff7eb49c79c269ba7014a755fd00c6e8dd22678e1f044011b9b4123ecc24530b9d693464a11357
6
+ metadata.gz: 8af34246000adfc3d4c370095d81f613eb28eae24ef48071043e50fd626cbfe97824def5d31e0c8cef46629c977d22beec4bb93e8bae2b2f188126c7712a2f03
7
+ data.tar.gz: '090b048ff7fe04c62602985dccf2d35adbaf22f9033e9604009a2e4c1f012e105f1cedfadd0c4b3be684c906c0f3eebf649ce76cfd5e2f2d34942c86f3ef1fef'
data/CHANGELOG.md CHANGED
@@ -3,6 +3,37 @@
3
3
  All notable changes to AsciiChem are documented here.
4
4
  This project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.31.0] - 2026-10-19
7
+
8
+ ### Changed
9
+ - elkrb floor raised to `~> 2.0` (from `~> 1.0`): the layout stack
10
+ migrates to elkrb 2.0's option-based API — the graph-level
11
+ `LayoutOptions` object is gone and the algorithm flows through
12
+ `Elkrb.layout(graph, algorithm:)` (the call site already passed
13
+ it). Full suite (2024 examples: structural-SVG goldens included)
14
+ green under both parse engines on elkrb 2.0.0; layered remains
15
+ the default algorithm and output is unchanged byte-for-byte.
16
+
17
+ ### Added
18
+ - The `:parg` engine (opt-in, third): parses through the compiled
19
+ PARG artifact of the AsciiChem grammar — vendored from
20
+ asciichem-parg (`lib/asciichem/engine/parg/asciichem.json`,
21
+ version-pinned, drift fails loudly) — and adapts its parse trees
22
+ to the same shapes the shared `TransformRules` consume. The full
23
+ suite (2024 examples: round-trip conformance, L2 MathML goldens,
24
+ CML three-way, edge cases) passes byte-identically under
25
+ `ASCIICHEM_ENGINE=parg`, the shared corpus gates 221/221 with
26
+ model equality against the reference engine, and the workload
27
+ benchmark runs ~2.7x parslet. `AsciiChem::Engine.use(:parg)` or
28
+ `ASCIICHEM_ENGINE=parg`; requires parsanol ≥ 1.3.94 (PARG),
29
+ still a soft dev dependency. Repetition flattening follows
30
+ parslet's own hash-only semantics (parsanol-ruby#231/#232), so
31
+ the grammar captures every structural rule and the engine's
32
+ tree adapter unwraps the wrappers into the DSL shapes; a
33
+ capture-completeness gate fails any grammar edit that leaves a
34
+ trailing literal uncaptured (`benchmarks/parg_recheck.rb` is
35
+ the standing gate).
36
+
6
37
  ## [0.30.1] - 2026-09-23
7
38
 
8
39
  ### Fixed
data/asciichem.gemspec CHANGED
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  # Floors are the versions the corpus suite validates against
37
37
  # (pessimistic ~>); raise them only with a full-suite run.
38
38
  spec.add_dependency "chemicalml", "~> 0.3.0"
39
- spec.add_dependency "elkrb", "~> 1.0"
39
+ spec.add_dependency "elkrb", "~> 2.0"
40
40
  spec.add_dependency "lutaml-model", "~> 0.8"
41
41
  spec.add_dependency "mml", "~> 2.3"
42
42
  spec.add_dependency "moxml", "~> 0.5"
data/benchmarks/README.md CHANGED
@@ -230,3 +230,24 @@ it. The version is fully transitive — lutaml-model constrains
230
230
  stubbing `leptris_preferred_available?` — lutaml's
231
231
  `detect_xml_adapter` calls `runtime_default_adapter`, which
232
232
  ignores `default_adapter=`.
233
+
234
+ ### Re-check 10 (2026-09-23, parsanol 1.3.52)
235
+
236
+ Three releases since re-check 9 — notable upstream: 1.3.50 pins
237
+ cross-engine consume-all/recursion semantics (closing the five
238
+ PR-#22 divergence skips), 1.3.51 consolidates on a single Symbol
239
+ tag form. Validation:
240
+
241
+ - Gate **221/221** through the shipped `ParsanolEngine` — every
242
+ accept/reject/round-trip case identical to parslet, including the
243
+ optimizer-acceptance semantics pinned upstream in 1.3.50.
244
+ - The compat surface our engine rides (`Parsanol::Parslet::Parser`)
245
+ now resolves through `Parsanol::Parser` — renamed/absorbed
246
+ upstream during the 1.3.3x-4x line; our subclassing works
247
+ unchanged (the gate is the proof) and needs no engine-side change.
248
+ - **Perf: ratio-only** (machine at load ~175 during measurement —
249
+ the parslet control measured 7-9 i/s against its quiet ~75):
250
+ parsanol **2.6-3.2x parslet** across two same-process runs
251
+ (22.8/7.2, 24.3/9.4), consistent with re-checks 8/9.
252
+ - The unpublished v1.0.1 draft noted in re-check 9 is still
253
+ untagged/unpublished; nothing to validate there yet.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PARG gate + benchmark: AsciiChem::Engine.use(:parg) parses through
4
+ # the compiled PARG artifact vendored from asciichem-parg, adapted to
5
+ # the shared TransformRules. (1) gates on the shared corpus, forked
6
+ # per case so a native crash aborts the child — reported, not fatal;
7
+ # (2) gates on capture completeness (a grammar edit leaving a
8
+ # trailing literal uncaptured must fail loudly — the runtime itself
9
+ # is parslet-strict about end of input); (3) measures against the
10
+ # parslet path. The model-equality gate against the reference engine lives in
11
+ # spec/asciichem/engine/parg_engine_spec.rb and the full suite green
12
+ # under ASCIICHEM_ENGINE=parg is the L2 contract.
13
+ #
14
+ # bundle exec ruby benchmarks/parg_recheck.rb
15
+ require "benchmark/ips"
16
+ require "asciichem"
17
+ require "asciichem/engine/parg_engine"
18
+
19
+ def run_in_child
20
+ reader, writer = IO.pipe
21
+ pid = fork do
22
+ reader.close
23
+ Marshal.dump(yield, writer)
24
+ rescue StandardError => e
25
+ Marshal.dump({ exception: e.class.name, message: e.message }, writer)
26
+ ensure
27
+ writer.close
28
+ end
29
+ writer.close
30
+ payload = Marshal.load(reader)
31
+ reader.close
32
+ _, status = Process.waitpid2(pid)
33
+ [payload, status]
34
+ end
35
+
36
+ # -- 1. Capture-completeness gate --------------------------------------
37
+ # Tails the grammar cannot capture must reject, as in the reference.
38
+ EOS_CASES = { "A(oh)GARBAGE" => false, "H_2O extra" => false,
39
+ "Ca(OH)_2 junk" => false, "H_2O" => true }.freeze
40
+
41
+ AsciiChem::Engine.use(:parg)
42
+ puts "parg engine | artifact #{AsciiChem::Engine::PargEngine.artifact.version} " \
43
+ "(parsanol #{Parsanol::VERSION})"
44
+
45
+ eos_fail = 0
46
+ EOS_CASES.each do |input, should_parse|
47
+ payload, status = run_in_child do
48
+ begin
49
+ { text: AsciiChem.parse(input).to_text }
50
+ rescue AsciiChem::ParseError => e
51
+ { error: e.message }
52
+ end
53
+ end
54
+ accepted = status.success? && payload.key?(:text)
55
+ next if accepted == should_parse
56
+
57
+ eos_fail += 1
58
+ puts " EOS FAIL: #{input.inspect} accepted=#{accepted} expected=#{should_parse}"
59
+ end
60
+ puts format("capture-completeness gate: %d/%d ok", EOS_CASES.length - eos_fail, EOS_CASES.length)
61
+
62
+ # -- 2. Shared-corpus gate ---------------------------------------------
63
+ corpus_dir = File.expand_path("../../asciichem-tests/corpus/fixtures", __dir__)
64
+ unless File.directory?(corpus_dir)
65
+ warn "corpus not found at #{corpus_dir} — clone asciichem-tests alongside"
66
+ exit 1
67
+ end
68
+ cases = Dir[File.join(corpus_dir, "*.json")].sort.flat_map { |p| JSON.parse(File.read(p)) }
69
+ parser_cases = cases.select { |c| c.key?("input") && !c.key?("lint") && !c.key?("convention") }
70
+
71
+ pass = fail_parse = fail_reject = fail_roundtrip = fatal = 0
72
+ parser_cases.each do |fixture|
73
+ input = fixture.fetch("input")
74
+ payload, status = run_in_child do
75
+ formula = AsciiChem.parse(input)
76
+ { text: (formula.to_text if fixture["roundTrip"]) }
77
+ end
78
+ if status.signaled? || !status.success?
79
+ fatal += 1
80
+ puts " FATAL (child #{status.exitstatus ? "exit #{status.exitstatus}" : "aborted"}): #{fixture["id"]} #{input.inspect}" if fatal <= 8
81
+ next
82
+ end
83
+ if payload.key?(:exception)
84
+ if fixture.fetch("parses")
85
+ fail_parse += 1
86
+ puts " PARSE FAIL: #{input.inspect} -> #{payload[:message][0, 90]}" if fail_parse <= 8
87
+ else
88
+ pass += 1
89
+ end
90
+ next
91
+ end
92
+ unless fixture.fetch("parses")
93
+ fail_reject += 1
94
+ puts " SHOULD REJECT: #{input.inspect}" if fail_reject <= 8
95
+ next
96
+ end
97
+ if fixture["roundTrip"] && payload[:text] != input
98
+ fail_roundtrip += 1
99
+ puts " ROUNDTRIP DIFF: #{input.inspect} -> #{payload[:text].inspect}" if fail_roundtrip <= 5
100
+ next
101
+ end
102
+ pass += 1
103
+ end
104
+ total = parser_cases.length
105
+ puts format("corpus gate: %d/%d ok (parse-fails %d, should-reject %d, roundtrip-diffs %d, fatal %d)",
106
+ pass, total, fail_parse, fail_reject, fail_roundtrip, fatal)
107
+
108
+ # -- 3. Performance ----------------------------------------------------
109
+ WORKLOAD = [
110
+ "H_2O", "Ca^2+", "SO_4^2-", "(R)-CH_3CH(OH)COOH",
111
+ "2H_2 + O_2 -> 2H_2O", "N_2 + 3H_2 <=>[Fe][400C] 2NH_3",
112
+ "C1-C-C-C-C-C1", "CH_3-CH_2-OH",
113
+ '^14C @name("carbon-14") @cas("14104-86-4")',
114
+ "A ->[heat] B ->[cool] C"
115
+ ].freeze
116
+
117
+ Benchmark.ips do |x|
118
+ x.report("parg parse x10") { WORKLOAD.each { |s| AsciiChem.parse(s) } }
119
+ x.compare!
120
+ end
@@ -0,0 +1 @@
1
+ {"bindings":null,"version":"0.3.1","grammar":"AsciiChemCore","shape":"parsanol-tree/v2","binding_version":1,"entries":{"formula":{"root":"formula","grammar":{"atoms":[{"Entity":{"atom":5}},{"Str":{"pattern":" "}},{"Re":{"pattern":"[\\x09]"}},{"Re":{"pattern":"[\\x0a]"}},{"Re":{"pattern":"[\\x0d]"}},{"Alternative":{"atoms":[1,2,3,4]}},{"Repetition":{"atom":0,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":6,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":640}},{"Entity":{"atom":426}},{"Entity":{"atom":412}},{"Entity":{"atom":409}},{"Entity":{"atom":408}},{"Entity":{"atom":30}},{"Str":{"pattern":"("}},{"Entity":{"atom":26}},{"Str":{"pattern":"alpha"}},{"Str":{"pattern":"beta"}},{"Str":{"pattern":"R"}},{"Str":{"pattern":"S"}},{"Str":{"pattern":"E"}},{"Str":{"pattern":"Z"}},{"Str":{"pattern":"α"}},{"Str":{"pattern":"β"}},{"Str":{"pattern":"a"}},{"Str":{"pattern":"b"}},{"Alternative":{"atoms":[16,17,18,19,20,21,22,23,24,25]}},{"Named":{"name":"stereo","atom":15}},{"Str":{"pattern":")"}},{"Str":{"pattern":"-"}},{"Sequence":{"atoms":[14,27,28,29]}},{"Repetition":{"atom":13,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":43}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":33,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"("}},{"Str":{"pattern":"["}},{"Str":{"pattern":"{"}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Alternative":{"atoms":[35,36,37,38]}},{"Lookahead":{"atom":39,"positive":true}},{"Sequence":{"atoms":[34,40]}},{"Named":{"name":"value","atom":41}},{"Named":{"name":"coefficient","atom":42}},{"Repetition":{"atom":32,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":403}},{"Entity":{"atom":209}},{"Entity":{"atom":50}},{"Str":{"pattern":":"}},{"Repetition":{"atom":48,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"lone_pairs","atom":49}},{"Repetition":{"atom":47,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"^"}},{"Str":{"pattern":"_"}},{"Alternative":{"atoms":[52,53]}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":55,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[54,56]}},{"Named":{"name":"isotope","atom":57}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Repetition":{"atom":60,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[59,61]}},{"Named":{"name":"element","atom":62}},{"Entity":{"atom":124}},{"Str":{"pattern":"_"}},{"Entity":{"atom":77}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":68,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[69,70]}},{"Repetition":{"atom":71,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[67,72,73]}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":75,"min":1,"max":null,"tag":"Repetition"}},{"Alternative":{"atoms":[74,76]}},{"Sequence":{"atoms":[65,66]}},{"Named":{"name":"subscript","atom":78}},{"Repetition":{"atom":79,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"^"}},{"Entity":{"atom":120}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":84,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[85,86]}},{"Repetition":{"atom":87,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[83,88,89]}},{"Entity":{"atom":102}},{"Str":{"pattern":"("}},{"Re":{"pattern":"[I]"}},{"Re":{"pattern":"[V]"}},{"Re":{"pattern":"[X]"}},{"Re":{"pattern":"[L]"}},{"Re":{"pattern":"[C]"}},{"Re":{"pattern":"[D]"}},{"Alternative":{"atoms":[93,94,95,96,97,98]}},{"Repetition":{"atom":99,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[92,100,101]}},{"Entity":{"atom":118}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":104,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"+"}},{"Str":{"pattern":"-"}},{"Alternative":{"atoms":[106,107]}},{"Repetition":{"atom":108,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[105,109]}},{"Str":{"pattern":"+"}},{"Str":{"pattern":"-"}},{"Alternative":{"atoms":[111,112]}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":114,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":115,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[113,116]}},{"Alternative":{"atoms":[110,117]}},{"Alternative":{"atoms":[91,103]}},{"Alternative":{"atoms":[90,119]}},{"Sequence":{"atoms":[81,82]}},{"Named":{"name":"superscript","atom":121}},{"Repetition":{"atom":122,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[80,123]}},{"Entity":{"atom":128}},{"Str":{"pattern":"."}},{"Repetition":{"atom":126,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"radical_electrons","atom":127}},{"Repetition":{"atom":125,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":130,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"ring_closures","atom":131}},{"Repetition":{"atom":132,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":207}},{"Entity":{"atom":159}},{"Str":{"pattern":"@("}},{"Entity":{"atom":147}},{"Str":{"pattern":"-"}},{"Repetition":{"atom":138,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":140,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"."}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":143,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[142,144]}},{"Repetition":{"atom":145,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[139,141,146]}},{"Named":{"name":"x2","atom":137}},{"Str":{"pattern":","}},{"Entity":{"atom":147}},{"Named":{"name":"y2","atom":150}},{"Str":{"pattern":","}},{"Entity":{"atom":147}},{"Named":{"name":"z2","atom":153}},{"Sequence":{"atoms":[152,154]}},{"Repetition":{"atom":155,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Named":{"name":"coord_close","atom":157}},{"Sequence":{"atoms":[136,148,149,151,156,158]}},{"Repetition":{"atom":135,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":167}},{"Str":{"pattern":"@"}},{"Str":{"pattern":"R"}},{"Str":{"pattern":"S"}},{"Alternative":{"atoms":[163,164]}},{"Named":{"name":"atom_parity","atom":165}},{"Sequence":{"atoms":[162,166]}},{"Repetition":{"atom":161,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":176}},{"Str":{"pattern":"@m("}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":171,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"spin_multiplicity","atom":172}},{"Str":{"pattern":")"}},{"Named":{"name":"spin_close","atom":174}},{"Sequence":{"atoms":[170,173,175]}},{"Repetition":{"atom":169,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":191}},{"Str":{"pattern":"@t("}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":181,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[182,183]}},{"Repetition":{"atom":184,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"atom_title","atom":185}},{"Str":{"pattern":"\""}},{"Named":{"name":"title_quote","atom":187}},{"Str":{"pattern":")"}},{"Named":{"name":"title_close","atom":189}},{"Sequence":{"atoms":[179,180,186,188,190]}},{"Repetition":{"atom":178,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":205}},{"Str":{"pattern":"@f("}},{"Entity":{"atom":147}},{"Named":{"name":"x_fract","atom":195}},{"Str":{"pattern":","}},{"Entity":{"atom":147}},{"Named":{"name":"y_fract","atom":198}},{"Str":{"pattern":","}},{"Entity":{"atom":147}},{"Named":{"name":"z_fract","atom":201}},{"Str":{"pattern":")"}},{"Named":{"name":"fract_close","atom":203}},{"Sequence":{"atoms":[194,196,197,199,200,202,204]}},{"Repetition":{"atom":193,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[160,168,177,192,206]}},{"Sequence":{"atoms":[51,58,63,64,129,133,134]}},{"Named":{"name":"atom","atom":208}},{"Entity":{"atom":359}},{"Entity":{"atom":216}},{"Str":{"pattern":"("}},{"Str":{"pattern":"["}},{"Str":{"pattern":"{"}},{"Alternative":{"atoms":[212,213,214]}},{"Named":{"name":"open_bracket","atom":215}},{"Entity":{"atom":355}},{"Entity":{"atom":292}},{"Named":{"name":"reactants","atom":11}},{"Entity":{"atom":249}},{"Entity":{"atom":5}},{"Repetition":{"atom":221,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":222,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":229}},{"Str":{"pattern":"<=>"}},{"Str":{"pattern":"<->"}},{"Str":{"pattern":"->"}},{"Str":{"pattern":"<-"}},{"Alternative":{"atoms":[225,226,227,228]}},{"Named":{"name":"kind","atom":224}},{"Entity":{"atom":242}},{"Str":{"pattern":"["}},{"Str":{"pattern":"]"}},{"Lookahead":{"atom":233,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[234,235]}},{"Repetition":{"atom":236,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"text","atom":237}},{"Str":{"pattern":"]"}},{"Named":{"name":"condition_close","atom":239}},{"Sequence":{"atoms":[232,238,240]}},{"Named":{"name":"condition","atom":241}},{"Named":{"name":"above","atom":231}},{"Repetition":{"atom":243,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":242}},{"Named":{"name":"below","atom":245}},{"Repetition":{"atom":246,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[223,230,244,247]}},{"Named":{"name":"arrow","atom":248}},{"Entity":{"atom":5}},{"Repetition":{"atom":250,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":251,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":289}},{"Entity":{"atom":287}},{"Entity":{"atom":5}},{"Repetition":{"atom":255,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":256,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"+"}},{"Entity":{"atom":5}},{"Repetition":{"atom":259,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":260,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":285}},{"Entity":{"atom":280}},{"Str":{"pattern":"##"}},{"Named":{"name":"quadruple","atom":264}},{"Str":{"pattern":">-"}},{"Named":{"name":"wedge","atom":266}},{"Str":{"pattern":"-<"}},{"Named":{"name":"hash","atom":268}},{"Str":{"pattern":"~>"}},{"Named":{"name":"dative","atom":270}},{"Str":{"pattern":"~~"}},{"Named":{"name":"wavy","atom":272}},{"Str":{"pattern":"#"}},{"Named":{"name":"triple","atom":274}},{"Str":{"pattern":"="}},{"Named":{"name":"double","atom":276}},{"Str":{"pattern":"-"}},{"Named":{"name":"single","atom":278}},{"Alternative":{"atoms":[265,267,269,271,273,275,277,279]}},{"Alternative":{"atoms":[45,263]}},{"Repetition":{"atom":281,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"units","atom":282}},{"Sequence":{"atoms":[31,44,283]}},{"Named":{"name":"molecule","atom":284}},{"Sequence":{"atoms":[257,258,261,262]}},{"Named":{"name":"plus_term","atom":286}},{"Repetition":{"atom":254,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[12,288]}},{"Named":{"name":"products","atom":253}},{"Sequence":{"atoms":[219,220,252,290]}},{"Named":{"name":"reaction","atom":291}},{"Entity":{"atom":315}},{"Entity":{"atom":305}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":295,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"s"}},{"Str":{"pattern":"p"}},{"Str":{"pattern":"d"}},{"Str":{"pattern":"f"}},{"Str":{"pattern":"g"}},{"Str":{"pattern":"h"}},{"Alternative":{"atoms":[297,298,299,300,301,302]}},{"Sequence":{"atoms":[296,303]}},{"Named":{"name":"orbital","atom":304}},{"Str":{"pattern":"^"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":307,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"occupancy","atom":308}},{"Entity":{"atom":5}},{"Repetition":{"atom":310,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":311,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[294,306,309,312]}},{"Repetition":{"atom":313,"min":2,"max":null,"tag":"Repetition"}},{"Named":{"name":"electron_config","atom":314}},{"Entity":{"atom":285}},{"Entity":{"atom":333}},{"Repetition":{"atom":217,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"group_nodes","atom":318}},{"Entity":{"atom":325}},{"Str":{"pattern":")"}},{"Str":{"pattern":"]"}},{"Str":{"pattern":"}"}},{"Alternative":{"atoms":[321,322,323]}},{"Named":{"name":"close_bracket","atom":324}},{"Str":{"pattern":"_"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":327,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[326,328]}},{"Named":{"name":"multiplicity","atom":329}},{"Repetition":{"atom":330,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[211,319,320,331]}},{"Named":{"name":"group","atom":332}},{"Entity":{"atom":345}},{"Str":{"pattern":"`"}},{"Str":{"pattern":"`"}},{"Lookahead":{"atom":336,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[337,338]}},{"Repetition":{"atom":339,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"math_source","atom":340}},{"Str":{"pattern":"`"}},{"Named":{"name":"math_close","atom":342}},{"Sequence":{"atoms":[335,341,343]}},{"Named":{"name":"embedded_math","atom":344}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":347,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[348,349]}},{"Repetition":{"atom":350,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"\""}},{"Sequence":{"atoms":[346,351,352]}},{"Named":{"name":"group_text_run","atom":353}},{"Alternative":{"atoms":[218,293,316,317,334,354]}},{"Repetition":{"atom":217,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"group_nodes","atom":356}},{"Sequence":{"atoms":[211,357,320,331]}},{"Named":{"name":"group","atom":358}},{"Entity":{"atom":384}},{"Entity":{"atom":50}},{"Repetition":{"atom":361,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"H"}},{"Named":{"name":"element","atom":363}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Lookahead":{"atom":365,"positive":false}},{"Str":{"pattern":"_"}},{"Entity":{"atom":77}},{"Sequence":{"atoms":[367,368]}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":370,"min":1,"max":null,"tag":"Repetition"}},{"Alternative":{"atoms":[369,371]}},{"Named":{"name":"subscript","atom":372}},{"Repetition":{"atom":373,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"^"}},{"Entity":{"atom":120}},{"Sequence":{"atoms":[375,376]}},{"Named":{"name":"superscript","atom":377}},{"Repetition":{"atom":378,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":128}},{"Repetition":{"atom":380,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":207}},{"Sequence":{"atoms":[362,364,366,374,379,381,382]}},{"Named":{"name":"atom","atom":383}},{"Entity":{"atom":402}},{"Entity":{"atom":50}},{"Repetition":{"atom":386,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Repetition":{"atom":389,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[388,390]}},{"Named":{"name":"element","atom":391}},{"Entity":{"atom":124}},{"Entity":{"atom":128}},{"Repetition":{"atom":394,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":396,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"ring_closures","atom":397}},{"Repetition":{"atom":398,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":207}},{"Sequence":{"atoms":[387,392,393,395,399,400]}},{"Named":{"name":"atom","atom":401}},{"Alternative":{"atoms":[46,210,360,385]}},{"Alternative":{"atoms":[45,263]}},{"Repetition":{"atom":404,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"units","atom":405}},{"Sequence":{"atoms":[31,44,406]}},{"Named":{"name":"molecule","atom":407}},{"Sequence":{"atoms":[12,288]}},{"Named":{"name":"reactants","atom":11}},{"Sequence":{"atoms":[410,220,252,290]}},{"Named":{"name":"reaction","atom":411}},{"Named":{"name":"first","atom":10}},{"Entity":{"atom":422}},{"Entity":{"atom":249}},{"Entity":{"atom":5}},{"Repetition":{"atom":416,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":417,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":409}},{"Named":{"name":"products","atom":419}},{"Sequence":{"atoms":[415,418,420]}},{"Named":{"name":"segment","atom":421}},{"Repetition":{"atom":414,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"segments","atom":423}},{"Sequence":{"atoms":[413,424]}},{"Named":{"name":"cascade","atom":425}},{"Entity":{"atom":412}},{"Entity":{"atom":315}},{"Entity":{"atom":465}},{"Str":{"pattern":"crystal"}},{"Str":{"pattern":"["}},{"Str":{"pattern":"]"}},{"Lookahead":{"atom":432,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[433,434]}},{"Repetition":{"atom":435,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"crystal_name","atom":436}},{"Str":{"pattern":"]"}},{"Named":{"name":"crystal_name_close","atom":438}},{"Sequence":{"atoms":[431,437,439]}},{"Repetition":{"atom":440,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"("}},{"Str":{"pattern":")"}},{"Lookahead":{"atom":443,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[444,445]}},{"Repetition":{"atom":446,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"crystal_params","atom":447}},{"Str":{"pattern":")"}},{"Named":{"name":"crystal_params_close","atom":449}},{"Sequence":{"atoms":[442,448,450]}},{"Repetition":{"atom":451,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":454,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[455,456]}},{"Repetition":{"atom":457,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"crystal_body","atom":458}},{"Str":{"pattern":"}"}},{"Named":{"name":"crystal_body_close","atom":460}},{"Sequence":{"atoms":[453,459,461]}},{"Repetition":{"atom":462,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[430,441,452,463]}},{"Named":{"name":"crystal_node","atom":464}},{"Entity":{"atom":502}},{"Str":{"pattern":"spectrum"}},{"Str":{"pattern":"["}},{"Str":{"pattern":"]"}},{"Lookahead":{"atom":469,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[470,471]}},{"Repetition":{"atom":472,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"spectrum_type","atom":473}},{"Str":{"pattern":"]"}},{"Named":{"name":"spectrum_type_close","atom":475}},{"Sequence":{"atoms":[468,474,476]}},{"Repetition":{"atom":477,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"("}},{"Str":{"pattern":")"}},{"Lookahead":{"atom":480,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[481,482]}},{"Repetition":{"atom":483,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"spectrum_params","atom":484}},{"Str":{"pattern":")"}},{"Named":{"name":"spectrum_params_close","atom":486}},{"Sequence":{"atoms":[479,485,487]}},{"Repetition":{"atom":488,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":491,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[492,493]}},{"Repetition":{"atom":494,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"spectrum_body","atom":495}},{"Str":{"pattern":"}"}},{"Named":{"name":"spectrum_body_close","atom":497}},{"Sequence":{"atoms":[490,496,498]}},{"Repetition":{"atom":499,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[467,478,489,500]}},{"Named":{"name":"spectrum_node","atom":501}},{"Entity":{"atom":528}},{"Str":{"pattern":"calc"}},{"Str":{"pattern":"("}},{"Str":{"pattern":")"}},{"Lookahead":{"atom":506,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[507,508]}},{"Repetition":{"atom":509,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"calc_params","atom":510}},{"Str":{"pattern":")"}},{"Named":{"name":"calc_params_close","atom":512}},{"Sequence":{"atoms":[505,511,513]}},{"Repetition":{"atom":514,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":517,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[518,519]}},{"Repetition":{"atom":520,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"calc_body","atom":521}},{"Str":{"pattern":"}"}},{"Named":{"name":"calc_body_close","atom":523}},{"Sequence":{"atoms":[516,522,524]}},{"Repetition":{"atom":525,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[504,515,526]}},{"Named":{"name":"calc_node","atom":527}},{"Entity":{"atom":543}},{"Str":{"pattern":"zmatrix"}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":532,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[533,534]}},{"Repetition":{"atom":535,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"zmatrix_body","atom":536}},{"Str":{"pattern":"}"}},{"Named":{"name":"zmatrix_body_close","atom":538}},{"Sequence":{"atoms":[531,537,539]}},{"Repetition":{"atom":540,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[530,541]}},{"Named":{"name":"zmatrix_node","atom":542}},{"Entity":{"atom":558}},{"Str":{"pattern":"mechanism"}},{"Str":{"pattern":"{"}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":547,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[548,549]}},{"Repetition":{"atom":550,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"mechanism_body","atom":551}},{"Str":{"pattern":"}"}},{"Named":{"name":"mechanism_body_close","atom":553}},{"Sequence":{"atoms":[546,552,554]}},{"Repetition":{"atom":555,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[545,556]}},{"Named":{"name":"mechanism_node","atom":557}},{"Entity":{"atom":628}},{"Entity":{"atom":408}},{"Named":{"name":"mol","atom":560}},{"Entity":{"atom":624}},{"Entity":{"atom":5}},{"Repetition":{"atom":563,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":564,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":588}},{"Str":{"pattern":"@meta("}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":569,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[570,571]}},{"Repetition":{"atom":572,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"meta_key","atom":573}},{"Str":{"pattern":"\""}},{"Str":{"pattern":","}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":578,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[579,580]}},{"Repetition":{"atom":581,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"meta_value","atom":582}},{"Str":{"pattern":"\""}},{"Named":{"name":"meta_value_close","atom":584}},{"Str":{"pattern":")"}},{"Named":{"name":"meta_close","atom":586}},{"Sequence":{"atoms":[567,568,574,575,576,577,583,585,587]}},{"Entity":{"atom":621}},{"Str":{"pattern":"@"}},{"Entity":{"atom":602}},{"Str":{"pattern":"name"}},{"Str":{"pattern":"title"}},{"Str":{"pattern":"formula"}},{"Str":{"pattern":"label"}},{"Str":{"pattern":"inchi"}},{"Str":{"pattern":"smiles"}},{"Str":{"pattern":"cas"}},{"Str":{"pattern":"iupac"}},{"Str":{"pattern":"cid"}},{"Str":{"pattern":"chebi"}},{"Alternative":{"atoms":[592,593,594,595,596,597,598,599,600,601]}},{"Entity":{"atom":605}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Repetition":{"atom":604,"min":1,"max":null,"tag":"Repetition"}},{"Alternative":{"atoms":[591,603]}},{"Named":{"name":"ann_type","atom":606}},{"Str":{"pattern":"("}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":610,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[611,612]}},{"Repetition":{"atom":613,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"ann_value","atom":614}},{"Str":{"pattern":"\""}},{"Named":{"name":"ann_value_close","atom":616}},{"Str":{"pattern":")"}},{"Named":{"name":"ann_close","atom":618}},{"Sequence":{"atoms":[607,608,609,615,617,619]}},{"Sequence":{"atoms":[590,620]}},{"Alternative":{"atoms":[566,589]}},{"Sequence":{"atoms":[565,622]}},{"Named":{"name":"annotation_item","atom":623}},{"Repetition":{"atom":562,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"annotations","atom":625}},{"Sequence":{"atoms":[561,626]}},{"Named":{"name":"annotated_molecule","atom":627}},{"Entity":{"atom":408}},{"Entity":{"atom":345}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"\""}},{"Lookahead":{"atom":632,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[633,634]}},{"Repetition":{"atom":635,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"\""}},{"Sequence":{"atoms":[631,636,637]}},{"Named":{"name":"text_run","atom":638}},{"Alternative":{"atoms":[9,427,428,429,466,503,529,544,559,629,630,639]}},{"Entity":{"atom":5}},{"Repetition":{"atom":641,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":642,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":640}},{"Sequence":{"atoms":[643,644]}},{"Repetition":{"atom":645,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[8,646]}},{"Named":{"name":"formula","atom":647}},{"Entity":{"atom":5}},{"Repetition":{"atom":649,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":650,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[7,648,651]}}],"root":652},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule formula_node: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 1 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 1 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 2 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 2 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 3 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 3 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 4 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule formula_node: branches 9 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule arrow_kind: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule arrow_kind: branches 1 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule arrow_kind: branches 2 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule stereo_letter: branches 1 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule stereo_letter: branches 2 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule stereo_letter: branches 7 and 8 are order-dependent (shared first bytes; ordered choice is decisive)","rule unit: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule unit: branches 1 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule unit: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule group_unit: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule group_unit: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule group_unit: branches 1 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule group_unit: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule group_unit: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule bond: branches 1 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule bond: branches 3 and 8 are order-dependent (shared first bytes; ordered choice is decisive)","rule bond: branches 4 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule known_annotation_type: branches 5 and 8 are order-dependent (shared first bytes; ordered choice is decisive)","rule known_annotation_type: branches 7 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule known_annotation_type: branches 7 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule known_annotation_type: branches 9 and 10 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"formula","render":{},"derive":{},"tests":[],"docs":{"formula":"spaces? >> nodes.as(:formula) >> spaces?","formula_node":"Top-level nodes, DSL ordered-choice order (cascade before plain\nreaction, annotations before bare molecule).","cascade":"Two or more chained reactions: the transform promotes the chain\nto a ReactionCascade (first leg + arrow/products segments).","reaction":"Reactants, arrow (with optional above/below conditions), products.","arrow":"arrow.as(:arrow) {kind, above?, below?} — leading spaces ride the\narrow, conditions attach with no gap.","molecule":"A molecule: stereochemistry prefix, optional coefficient, units.","stereo_prefix":"`(R)-` style prefixes; a/b are the alpha/beta shorthand.","coefficient":"Coefficient: digits only when an element or bracket follows (else\nthe digits are subscripts/rings). Captured as {value}.","unit":"Units: prefixed (isotope) atoms, bracket groups, hydrogen\n(bare-digit subscripts allowed), plain atoms.","prefixed_atom":"Prefix isotope: ^14C or _14C — the marker binds to the atom, the\ncore semantic fix over AsciiMath. Captured with the marker.","group":"Bracket group: ( ... ) [ ... ] { ... } with optional multiplicity.","group_unit":"Inside a group: reactions, molecules, electron configs, embedded\nmath and quoted text recurse (DSL group_node set).","hydrogen_atom":"Hydrogen: bare digits after H are subscripts (H cannot ring-\nclose), but H must not steal the H of He, Hf...","atom_suffix":"Suffix markers consume `_` / `^` but capture marker + value\n(AtomBuilder strips the marker).","charge":"Charges: 2+, +2, 2 (bare digits fall through to the same capture).","oxidation_state":"Oxidation state: (IV), (II)...","lewis_prefix":"Lewis markers: leading colons = lone pairs; trailing dots =\nradical electrons.","atom_annotation":"Atom annotations, DSL order: coordinates, parity, multiplicity,\ntitle, fractional coordinates.","bond":"Bonds capture under their kind name (transform contract), longest\nfirst so `##` beats `#`; `->` is reserved for reactions.","crystal":"crystal[Name](a=X,b=Y,sg=SG){atoms with @f(x,y,z)}","spectrum":"spectrum[type](params){peak data}","calculation":"calc(method/basis){key: value units}","zmatrix":"zmatrix{lines}","mechanism":"mechanism{steps}","electron_config":"`1s2 2s2 2p6` — at least two orbital terms (DSL repeat(2)).","embedded_math":"Backtick-delimited Plurimath/AsciiMath source (delimiters stripped\nby the transform).","annotated_molecule":"Free-form text uses \"...\" delimiters, matching AsciiMath; the\ncapture includes the quotes (TextNormaliser strips them).\n@name(\"aspirin\") @cas(\"50-78-2\") @meta(\"k\",\"v\") — the annotation\nvocabulary rides the molecule.","metadata_annotation":"Two quoted args, comma-separated; keys distinguish metadata from\nregular annotations.","simple_annotation":"Known types (name, inchi, ...) handled specially by the transform;\nany other lowercase word becomes a property."},"source":"grammar AsciiChemCore version \"0.3.1\" {\n ## The AsciiChem syntax — full grammar, DSL-shaped captures\n ## (P1b, asciichem-ruby#96). Capture names and nesting mirror\n ## GrammarRules exactly so the PARG tree adapts to the same shape\n ## TransformRules consumes: sequence captures merge into one hash\n ## (parens to attach `as`), repetitions stay arrays, unwrapped\n ## rules let their captures ride into the parent context. Absent\n ## optionals omit their key (the tree adapter reinstates the ones\n ## transform patterns require, e.g. group multiplicity). Structural\n ## rules (molecule/reaction/group/annotated_molecule) are captured\n ## and the adapter unwraps them: PARG merge semantics for captured\n ## sequences with embedded repetitions yield arrays, and uncaptured\n ## sequence rules drop captures non-deterministically in reps\n ## (parsanol findings).\n ##\n ## Language notes (P0 findings): entries reference rules; sequences\n ## take no trailing capture (wrap in parens); [ ] is the option\n ## syntax so literal brackets are strings; until is exclusive\n ## (restate the closer); strict PEG — no repetition backtracking.\n\n entry formula: formula\n\n ## spaces? >> nodes.as(:formula) >> spaces?\n formula = [ 1*( ws ) ] ( formula_node 0*( [ 1*( ws ) ] formula_node ) ) as formula [ 1*( ws ) ]\n\n ws = ( \" \" / %x09 / %x0A / %x0D )\n\n ## Top-level nodes, DSL ordered-choice order (cascade before plain\n ## reaction, annotations before bare molecule).\n formula_node = cascade / reaction / electron_config / crystal / spectrum / calculation / zmatrix / mechanism / annotated_molecule / molecule / embedded_math / ( \"\\\"\" until \"\\\"\" \"\\\"\" ) as text_run\n\n # -- reactions ---------------------------------------------------------\n\n ## Two or more chained reactions: the transform promotes the chain\n ## to a ReactionCascade (first leg + arrow/products segments).\n cascade = ( ( reaction ) as first ( 1*( cascade_segment ) ) as segments ) as cascade\n\n cascade_segment = ( arrow [ 1*( ws ) ] ( terms as products ) ) as segment\n\n ## Reactants, arrow (with optional above/below conditions), products.\n reaction = ( ( terms as reactants ) arrow [ 1*( ws ) ] ( terms as products ) ) as reaction\n\n terms = molecule 0*( plus_term )\n\n plus_term = ( [ 1*( ws ) ] \"+\" [ 1*( ws ) ] molecule ) as plus_term\n\n ## arrow.as(:arrow) {kind, above?, below?} — leading spaces ride the\n ## arrow, conditions attach with no gap.\n arrow = ( [ 1*( ws ) ] ( arrow_kind ) as kind [ condition as above ] [ condition as below ] ) as arrow\n\n arrow_kind = ( \"<=>\" / \"<->\" / \"->\" / \"<-\" )\n\n condition = ( \"[\" ( until \"]\" ) as text ( \"]\" ) as condition_close ) as condition\n\n ## A molecule: stereochemistry prefix, optional coefficient, units.\n molecule = ( [ stereo_prefix ] [ coefficient ] ( 1*( unit / bond ) ) as units ) as molecule\n\n ## `(R)-` style prefixes; a/b are the alpha/beta shorthand.\n stereo_prefix = \"(\" ( stereo_letter ) as stereo \")\" \"-\"\n\n stereo_letter = ( \"alpha\" / \"beta\" / \"R\" / \"S\" / \"E\" / \"Z\" / \"α\" / \"β\" / \"a\" / \"b\" )\n\n ## Coefficient: digits only when an element or bracket follows (else\n ## the digits are subscripts/rings). Captured as {value}.\n coefficient = ( ( 1*(%x30-39) &( \"(\" / \"[\" / \"{\" / %x41-5A ) ) as value ) as coefficient\n\n ## Units: prefixed (isotope) atoms, bracket groups, hydrogen\n ## (bare-digit subscripts allowed), plain atoms.\n unit = prefixed_atom / group / hydrogen_atom / plain_atom\n\n ## Prefix isotope: ^14C or _14C — the marker binds to the atom, the\n ## core semantic fix over AsciiMath. Captured with the marker.\n prefixed_atom = ( [ lewis_prefix ] ( ( \"^\" / \"_\" ) 1*(%x30-39) ) as isotope ( %x41-5A [ %x61-7A ] ) as element atom_suffix [ lewis_radicals ] [ ( 1*(%x30-39) ) as ring_closures ] atom_annotation ) as atom\n\n ## Bracket group: ( ... ) [ ... ] { ... } with optional multiplicity.\n group = ( group_open ( 1*( group_unit ) ) as group_nodes group_close [ ( \"_\" 1*(%x30-39) ) as multiplicity ] ) as group\n\n group_open = ( \"(\" / \"[\" / \"{\" ) as open_bracket\n\n group_close = ( \")\" / \"]\" / \"}\" ) as close_bracket\n\n ## Inside a group: reactions, molecules, electron configs, embedded\n ## math and quoted text recurse (DSL group_node set).\n group_unit = reaction / electron_config / molecule / group / embedded_math / ( \"\\\"\" until \"\\\"\" \"\\\"\" ) as group_text_run\n\n ## Hydrogen: bare digits after H are subscripts (H cannot ring-\n ## close), but H must not steal the H of He, Hf...\n hydrogen_atom = ( [ lewis_prefix ] ( \"H\" ) as element !( %x61-7A ) [ ( \"_\" subscript_value / 1*(%x30-39) ) as subscript ] [ ( \"^\" superscript_value ) as superscript ] [ lewis_radicals ] atom_annotation ) as atom\n\n plain_atom = ( [ lewis_prefix ] ( %x41-5A [ %x61-7A ] ) as element atom_suffix [ lewis_radicals ] [ ( 1*(%x30-39) ) as ring_closures ] atom_annotation ) as atom\n\n ## Suffix markers consume `_` / `^` but capture marker + value\n ## (AtomBuilder strips the marker).\n atom_suffix = [ ( \"_\" subscript_value ) as subscript ] [ ( \"^\" superscript_value ) as superscript ]\n\n subscript_value = ( \"{\" until \"}\" \"}\" ) / 1*(%x30-39)\n\n superscript_value = ( \"{\" until \"}\" \"}\" ) / ( oxidation_state / charge )\n\n ## Charges: 2+, +2, 2 (bare digits fall through to the same capture).\n charge = ( 1*(%x30-39) [ \"+\" / \"-\" ] ) / ( ( \"+\" / \"-\" ) [ 1*(%x30-39) ] )\n\n ## Oxidation state: (IV), (II)...\n oxidation_state = \"(\" 1*( %x49 / %x56 / %x58 / %x4C / %x43 / %x44 ) \")\"\n\n ## Lewis markers: leading colons = lone pairs; trailing dots =\n ## radical electrons.\n lewis_prefix = 1*( \":\" ) as lone_pairs\n\n lewis_radicals = 1*( \".\" ) as radical_electrons\n\n ## Atom annotations, DSL order: coordinates, parity, multiplicity,\n ## title, fractional coordinates.\n atom_annotation = [ coordinate_annotation ] [ parity_annotation ] [ multiplicity_annotation ] [ title_annotation ] [ fractional_annotation ]\n\n coordinate_annotation = \"@(\" ( float_number ) as x2 \",\" ( float_number ) as y2 [ \",\" ( float_number ) as z2 ] ( \")\" ) as coord_close\n\n parity_annotation = \"@\" ( \"R\" / \"S\" ) as atom_parity\n\n multiplicity_annotation = \"@m(\" ( 1*(%x30-39) ) as spin_multiplicity ( \")\" ) as spin_close\n\n title_annotation = \"@t(\" \"\\\"\" ( until \"\\\"\" ) as atom_title ( \"\\\"\" ) as title_quote ( \")\" ) as title_close\n\n fractional_annotation = \"@f(\" ( float_number ) as x_fract \",\" ( float_number ) as y_fract \",\" ( float_number ) as z_fract ( \")\" ) as fract_close\n\n float_number = [ \"-\" ] 1*(%x30-39) [ \".\" 0*(%x30-39) ]\n\n # -- bonds ---------------------------------------------------------------\n\n ## Bonds capture under their kind name (transform contract), longest\n ## first so `##` beats `#`; `->` is reserved for reactions.\n bond = ( \"##\" as quadruple ) / ( \">-\" as wedge ) / ( \"-<\" as hash ) / ( \"~>\" as dative ) / ( \"~~\" as wavy ) / ( \"#\" as triple ) / ( \"=\" as double ) / ( \"-\" as single )\n\n # -- beyond-formulas constructs -----------------------------------------\n\n ## crystal[Name](a=X,b=Y,sg=SG){atoms with @f(x,y,z)}\n crystal = ( \"crystal\" [ ( \"[\" ( until \"]\" ) as crystal_name ( \"]\" ) as crystal_name_close ) ] [ ( \"(\" ( until \")\" ) as crystal_params ( \")\" ) as crystal_params_close ) ] [ ( \"{\" ( until \"}\" ) as crystal_body ( \"}\" ) as crystal_body_close ) ] ) as crystal_node\n\n ## spectrum[type](params){peak data}\n spectrum = ( \"spectrum\" [ ( \"[\" ( until \"]\" ) as spectrum_type ( \"]\" ) as spectrum_type_close ) ] [ ( \"(\" ( until \")\" ) as spectrum_params ( \")\" ) as spectrum_params_close ) ] [ ( \"{\" ( until \"}\" ) as spectrum_body ( \"}\" ) as spectrum_body_close ) ] ) as spectrum_node\n\n ## calc(method/basis){key: value units}\n calculation = ( \"calc\" [ ( \"(\" ( until \")\" ) as calc_params ( \")\" ) as calc_params_close ) ] [ ( \"{\" ( until \"}\" ) as calc_body ( \"}\" ) as calc_body_close ) ] ) as calc_node\n\n ## zmatrix{lines}\n zmatrix = ( \"zmatrix\" [ ( \"{\" ( until \"}\" ) as zmatrix_body ( \"}\" ) as zmatrix_body_close ) ] ) as zmatrix_node\n\n ## mechanism{steps}\n mechanism = ( \"mechanism\" [ ( \"{\" ( until \"}\" ) as mechanism_body ( \"}\" ) as mechanism_body_close ) ] ) as mechanism_node\n\n # -- electron configuration ----------------------------------------------\n\n ## `1s2 2s2 2p6` — at least two orbital terms (DSL repeat(2)).\n electron_config = 2*( orbital \"^\" ( 1*(%x30-39) ) as occupancy [ 1*( ws ) ] ) as electron_config\n\n orbital = ( 1*(%x30-39) ( \"s\" / \"p\" / \"d\" / \"f\" / \"g\" / \"h\" ) ) as orbital\n\n # -- embedded math and text ------------------------------------------------\n\n ## Backtick-delimited Plurimath/AsciiMath source (delimiters stripped\n ## by the transform).\n embedded_math = ( \"`\" ( until \"`\" ) as math_source ( \"`\" ) as math_close ) as embedded_math\n\n ## Free-form text uses \"...\" delimiters, matching AsciiMath; the\n ## capture includes the quotes (TextNormaliser strips them).\n\n # -- molecule annotations ---------------------------------------------------\n\n ## @name(\"aspirin\") @cas(\"50-78-2\") @meta(\"k\",\"v\") — the annotation\n ## vocabulary rides the molecule.\n annotated_molecule = ( ( molecule ) as mol ( 1*( annotation_item ) ) as annotations ) as annotated_molecule\n\n annotation_item = ( [ 1*( ws ) ] ( metadata_annotation / simple_annotation ) ) as annotation_item\n\n ## Two quoted args, comma-separated; keys distinguish metadata from\n ## regular annotations.\n metadata_annotation = \"@meta(\" \"\\\"\" ( until \"\\\"\" ) as meta_key \"\\\"\" \",\" \"\\\"\" ( until \"\\\"\" ) as meta_value ( \"\\\"\" ) as meta_value_close ( \")\" ) as meta_close\n\n ## Known types (name, inchi, ...) handled specially by the transform;\n ## any other lowercase word becomes a property.\n simple_annotation = \"@\" ( ( known_annotation_type / property_name ) as ann_type \"(\" \"\\\"\" ( until \"\\\"\" ) as ann_value ( \"\\\"\" ) as ann_value_close ( \")\" ) as ann_close )\n\n known_annotation_type = ( \"name\" / \"title\" / \"formula\" / \"label\" / \"inchi\" / \"smiles\" / \"cas\" / \"iupac\" / \"cid\" / \"chebi\" )\n\n property_name = 1*( %x61-7A )\n\n test formula {\n accept \"H_2O\"\n accept \"H2O\"\n accept \"Ca^2+\"\n accept \"SO_4^2-\"\n accept \"C1-C-C-C-C-C1\"\n accept \"C12-C-C1-C2\"\n accept \"CH_3-CH_2-OH\"\n accept \"2H_2 + O_2 -> 2H_2O\"\n accept \"N_2 + 3H_2 <=>[Fe][400C] 2NH_3\"\n accept \"A ->[heat] B ->[cool] C\"\n accept \"A -> B -> C -> D\"\n accept \"Ca(OH)_2\"\n accept \"Fe(IV)\"\n accept \"_14C\"\n accept \"^14C\"\n accept \"CC(=O)OC1=CC=CC=C1C(=O)O\"\n accept \"Ca^2+ + CO_3^2- -> CaCO_3\"\n accept \"(R)-CH_3CH(OH)COOH\"\n accept \":O\"\n accept \"1s^2 2s^2 2p^6\"\n accept \"`x^2`\"\n accept \"\\\"nitrogen gas\\\"\"\n accept \"crystal[NaCl](a=5.64,sg=Fm-3m){Na@f(0,0,0)}\"\n accept \"spectrum[nmr](type=1H){1.2: 3H s \\\"CH3\\\"}\"\n accept \"calc(b3lyp){energy: -234.5}\"\n accept \"zmatrix{ C1 H2 C1 1.09 }\"\n accept \"mechanism{ step1: A -> B }\"\n accept \"H_2O @name(\\\"water\\\") @cas(\\\"7732-18-5\\\")\"\n accept \"H_2O @meta(\\\"source\\\",\\\"handbook\\\")\"\n accept \" H_2O \"\n accept \"C@(10,20)@R@m(2)@t(\\\"C1\\\")\"\n reject \"\"\n reject \"???\"\n reject \"H_\"\n reject \"C(=O\"\n reject \"+ H_2O\"\n reject \"crystal[NaCl\"\n reject \"zmatrix{ C1 \"\n reject \"@name(\\\"x\\\") H_2O\"\n }\n}\n","source_hash":"bb8a13d71e196f3294318305cc157f60781c14e212802d6afb27b91d20237d56","compiler_version":"1.3.94","checksum":"sha256:4b042741d854fdd52375ffbb1fc980159855716b3a88b4f4ec6431b546eff343"}
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in soft dependency: fail with guidance, never silently.
4
+ begin
5
+ require 'parsanol/parslet'
6
+ require 'parsanol/parg'
7
+ require 'parsanol/native/dynamic'
8
+ rescue LoadError => e
9
+ raise AsciiChem::Engine::NotAvailableError,
10
+ 'the parg engine requires the parsanol gem (>= 1.3.94 for PARG) — ' \
11
+ 'add gem "parsanol" to your Gemfile ' \
12
+ "(#{e.message})"
13
+ end
14
+
15
+ module AsciiChem
16
+ module Engine
17
+ # The PARG engine: parses through the compiled AsciiChem PARG
18
+ # artifact (grammar/asciichem.parg in asciichem-parg, vendored at
19
+ # lib/asciichem/engine/parg/asciichem.json) and adapts its parse
20
+ # tree to the DSL shapes TransformRules consumes. The shared
21
+ # conformance corpus passes model-identically (227/227) against
22
+ # the reference engine (benchmarks/parg_recheck.rb).
23
+ #
24
+ # Tree shape: repetition flattening follows parslet's own
25
+ # hash-only semantics (parsanol-ruby#231/#232), so the grammar
26
+ # captures every structural rule — keeping every iteration a
27
+ # hash — and TreeAdapter unwraps the wrappers into the DSL
28
+ # shapes. Grammar#parse additionally fails when the tree covers
29
+ # less input than was matched (a grammar edit leaving a trailing
30
+ # literal uncaptured), a capture-completeness invariant rather
31
+ # than end-of-input handling: the runtime itself is parslet-strict.
32
+ class PargEngine
33
+ # Version the vendored artifact was built from; the load fails
34
+ # loudly on drift so the engine never runs a stale grammar.
35
+ PARG_GRAMMAR_VERSION = '0.3.1'
36
+
37
+ ARTIFACT_PATH = File.expand_path('parg/asciichem.json', __dir__)
38
+
39
+ class Transform < ::Parsanol::Parslet::Transform
40
+ include AsciiChem::TransformRules
41
+ end
42
+
43
+ # Grammar seam for Parser: #parse returns the adapted tree the
44
+ # shared TransformRules consume, raising Parsanol::ParseFailed
45
+ # (with a char position) on rejection or short matches.
46
+ class Grammar
47
+ def parse(text)
48
+ source = text.strip
49
+ raw = PargEngine.artifact.parse('formula', source)
50
+ unless TreeAdapter.consumed_length(raw) >= source.length
51
+ raise ::Parsanol::ParseFailed,
52
+ "Failed to match sequence at char #{TreeAdapter.consumed_length(raw)}: " \
53
+ 'input not fully captured'
54
+ end
55
+
56
+ TreeAdapter.adapt(raw)
57
+ end
58
+ end
59
+
60
+ # Unwraps the artifact tree's captured structural rules into the
61
+ # DSL shapes TransformRules matches on.
62
+ module TreeAdapter
63
+ WRAPPERS = %i[
64
+ molecule reaction annotated_molecule segment plus_term
65
+ annotation_item formula_tail condition embedded_math
66
+ ].freeze
67
+
68
+ # Sequence captures whose merged array form collapses to the
69
+ # single hash the DSL shape carries.
70
+ ARRAY_MERGE_KEYS = %i[cascade molecule annotated_molecule].freeze
71
+
72
+ private_constant :WRAPPERS, :ARRAY_MERGE_KEYS
73
+
74
+ module_function
75
+
76
+ def consumed_length(node)
77
+ case node
78
+ when ::Parsanol::Slice then node.offset + node.content.length
79
+ when Hash then node.values.sum { |value| consumed_length(value) }
80
+ when Array then node.map { |member| consumed_length(member) }.max.to_i
81
+ else 0
82
+ end
83
+ end
84
+
85
+ def adapt(node)
86
+ case node
87
+ when ::Parsanol::Slice then node.to_s
88
+ when Hash then normalise(node.to_h { |key, value| [key.to_sym, adapt(value)] })
89
+ when Array then node.map { |member| adapt(member) }
90
+ else node
91
+ end
92
+ end
93
+
94
+ def normalise(hash)
95
+ return unwrap_single_key(hash) if hash.length == 1
96
+
97
+ ARRAY_MERGE_KEYS.each do |key|
98
+ hash[key] = merge_items(hash[key]) if array_value?(hash, key)
99
+ end
100
+ hash
101
+ end
102
+
103
+ def unwrap_single_key(hash)
104
+ key = hash.keys.first
105
+ return { math_source: math_source_of(hash[key]) } if key == :embedded_math
106
+
107
+ return hash[key] if WRAPPERS.include?(key)
108
+ return merge_multiplicity(merge_items(hash[key])) if key == :group
109
+
110
+ hash
111
+ end
112
+
113
+ def math_source_of(inner)
114
+ inner.is_a?(Hash) ? inner[:math_source] : inner
115
+ end
116
+
117
+ def array_value?(hash, key)
118
+ hash.key?(key) && hash[key].is_a?(Array)
119
+ end
120
+
121
+ def merge_multiplicity(merged)
122
+ merged[:multiplicity] ||= nil
123
+ merged
124
+ end
125
+
126
+ # Captured sequences with embedded repetitions yield arrays
127
+ # of per-child capture hashes; merge them into the single hash
128
+ # the DSL shape carries (later duplicate keys append, matching
129
+ # parslet's repeated-capture behavior).
130
+ def merge_items(items)
131
+ return items unless items.is_a?(Array)
132
+
133
+ items.each_with_object({}) do |item, merged|
134
+ merged.update(item) { |_key, old, new| old.is_a?(Array) ? old + Array(new) : new }
135
+ end
136
+ end
137
+ end
138
+
139
+ class << self
140
+ def artifact
141
+ @artifact ||= load_artifact
142
+ end
143
+
144
+ def grammar
145
+ Grammar
146
+ end
147
+
148
+ def transform
149
+ Transform
150
+ end
151
+
152
+ def parse_failed
153
+ ::Parsanol::ParseFailed
154
+ end
155
+
156
+ def grammar_instance
157
+ @grammar_instance ||= grammar.new
158
+ end
159
+
160
+ def transform_instance
161
+ @transform_instance ||= transform.new
162
+ end
163
+
164
+ private
165
+
166
+ def load_artifact
167
+ artifact = ::Parsanol::PARG::Artifact.load(ARTIFACT_PATH)
168
+ version = artifact.version
169
+ return artifact if version == PARG_GRAMMAR_VERSION
170
+
171
+ raise NotAvailableError,
172
+ "vendored PARG artifact version #{version.inspect} does not match " \
173
+ "the engine's pinned grammar #{PARG_GRAMMAR_VERSION.inspect} — " \
174
+ 're-vendor from asciichem-parg'
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -31,6 +31,7 @@ module AsciiChem
31
31
 
32
32
  autoload :ParsletEngine, 'asciichem/engine/parslet_engine'
33
33
  autoload :ParsanolEngine, 'asciichem/engine/parsanol_engine'
34
+ autoload :PargEngine, 'asciichem/engine/parg_engine'
34
35
 
35
36
  class << self
36
37
  # Selects the engine by name. Returns the engine class.
@@ -50,8 +51,9 @@ module AsciiChem
50
51
  case name.to_s
51
52
  when 'parslet' then ParsletEngine
52
53
  when 'parsanol' then ParsanolEngine
54
+ when 'parg' then PargEngine
53
55
  else
54
- raise Error, "unknown engine #{name.inspect} (available: parslet, parsanol)"
56
+ raise Error, "unknown engine #{name.inspect} (available: parslet, parsanol, parg)"
55
57
  end
56
58
  end
57
59
  end
@@ -63,7 +63,7 @@ module AsciiChem
63
63
  # round-tripping through CML.
64
64
  return pre_positioned_result(walk) if all_positioned?(walk)
65
65
 
66
- graph = GraphBuilder.new(walk).build(algorithm: algorithm)
66
+ graph = GraphBuilder.new(walk).build
67
67
  laid_out = Elkrb.layout(graph, algorithm: algorithm)
68
68
  ResultExtractor.new(laid_out, walk).extract
69
69
  end
@@ -201,14 +201,15 @@ module AsciiChem
201
201
  @walk = walk
202
202
  end
203
203
 
204
- def build(algorithm:)
204
+ def build
205
205
  children = @walk.atoms.map { |a| elkrb_node(a) }
206
206
  edges = @walk.bonds.map { |b| elkrb_edge(b) }
207
+ # elkrb 2.0 takes the algorithm as a layout call option, not a
208
+ # graph-level LayoutOptions object (Layout.layout passes it).
207
209
  Elkrb::Graph::Graph.new(
208
210
  id: 'root',
209
211
  children: children,
210
- edges: edges,
211
- layout_options: Elkrb::Graph::LayoutOptions.new(algorithm: algorithm)
212
+ edges: edges
212
213
  )
213
214
  end
214
215
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AsciiChem
4
- VERSION = "0.30.1"
4
+ VERSION = "0.31.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciichem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.1
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '1.0'
32
+ version: '2.0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '1.0'
39
+ version: '2.0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: lutaml-model
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +178,7 @@ files:
178
178
  - benchmarks/RESULTS.md
179
179
  - benchmarks/benchmark.rb
180
180
  - benchmarks/engines.rb
181
+ - benchmarks/parg_recheck.rb
181
182
  - benchmarks/parsanol_recheck.rb
182
183
  - exe/asciichem
183
184
  - lib/asciichem.rb
@@ -193,6 +194,8 @@ files:
193
194
  - lib/asciichem/cml/opaque_extensions.rb
194
195
  - lib/asciichem/cml/translator.rb
195
196
  - lib/asciichem/engine.rb
197
+ - lib/asciichem/engine/parg/asciichem.json
198
+ - lib/asciichem/engine/parg_engine.rb
196
199
  - lib/asciichem/engine/parsanol_engine.rb
197
200
  - lib/asciichem/engine/parslet_engine.rb
198
201
  - lib/asciichem/errors.rb