Skip to content

Commit 82a5a47

Browse files
authored
Merge pull request #790 from interscript/feat/parg-artifact-runtime
feat: parse ISC through the shipped .parg artifact
2 parents 54e414f + 2b7ed75 commit 82a5a47

5 files changed

Lines changed: 128 additions & 8 deletions

File tree

‎lib/interscript/isc/grammar/isc.artifact.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎lib/interscript/isc/normalizer.rb‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ def normalize_block(block)
6262
end
6363
}
6464
elsif block.key?(:tests)
65-
{tests: merge_tests(Array(block[:tests]))}
65+
# A comment-only or empty tests block captures as its raw
66+
# text through the artifact; the DSL grammar folds it. Both
67+
# normalize to no tests.
68+
items = block[:tests].is_a?(Array) ? block[:tests] : []
69+
{tests: merge_tests(items)}
6670
elsif block.key?(:stage)
6771
{stage_name: scalar(block[:stage_name]), stage: normalize_stage_items(block[:stage])}
6872
elsif block.key?(:target)

‎lib/interscript/isc/parser.rb‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,50 @@ class Parser < Parsanol::Parser
1616

1717
root :isc_source
1818

19-
def self.parse(source, filename: nil)
20-
new.parse_with_callbacks(source, filename: filename)
21-
end
19+
# The compiled grammar, shipped alongside the .parg source; the
20+
# runtime parses through it when parsanol carries PARG (>= 1.3.61)
21+
# and falls back to the Ruby-DSL grammar otherwise. Trees are
22+
# byte-identical by the corpus gate in the parg_diff spec.
23+
ARTIFACT_PATH = File.expand_path("grammar/isc.artifact.json", __dir__)
24+
25+
class ArtifactUnavailable < StandardError; end
26+
27+
class << self
28+
def parse(source, filename: nil)
29+
parse_with_callbacks(source, filename: filename)
30+
end
31+
32+
def parse_with_callbacks(source, filename: nil)
33+
tree = begin
34+
parse_with_artifact(source)
35+
rescue ArtifactUnavailable
36+
parse_with_dsl(source)
37+
end
38+
Normalizer.normalize(tree)
39+
rescue Parsanol::ParseFailed => e
40+
raise ParseError.new(e.message, filename: filename, source: source, cause: e)
41+
end
42+
43+
def parse_with_artifact(source)
44+
artifact.parse("isc", source)
45+
end
46+
47+
def parse_with_dsl(source)
48+
new.parse(source)
49+
end
50+
51+
def artifact
52+
@artifact ||= begin
53+
require "parsanol/parg"
54+
Parsanol::PARG::Artifact.load(ARTIFACT_PATH)
55+
rescue LoadError, StandardError => e
56+
raise ArtifactUnavailable, "parg artifact unavailable: #{e.class}: #{e.message}"
57+
end
58+
end
2259

23-
def parse_with_callbacks(source, filename: nil)
24-
Normalizer.normalize(parse(source))
25-
rescue Parsanol::ParseFailed => e
26-
raise ParseError.new(e.message, filename: filename, source: source, cause: e)
60+
def reset_artifact
61+
@artifact = nil
62+
end
2763
end
2864
end
2965

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Differential gate: every corpus map parses byte-identically through
4+
# the shipped .parg artifact and the Ruby-DSL grammar. The runtime
5+
# prefers the artifact; this spec is the proof the two agree.
6+
require "support/parg_diff"
7+
8+
RSpec.describe Interscript::Isc::Parser, :aggregate_failures do
9+
maps_dir = ENV.fetch("INTERSCRIPT_MAPS_PATH", "../maps/maps")
10+
maps = Dir[File.expand_path("*.isc", maps_dir)].sort
11+
12+
skip "maps checkout not present" if maps.empty?
13+
skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact)
14+
15+
it "parses every corpus map identically via artifact and DSL" do
16+
failures = []
17+
18+
maps.each do |path|
19+
source = File.read(path, encoding: "utf-8")
20+
artifact_tree = PargDiff.normalize(
21+
Interscript::Isc::Normalizer.normalize(described_class.parse_with_artifact(source))
22+
)
23+
dsl_tree = PargDiff.normalize(
24+
Interscript::Isc::Normalizer.normalize(described_class.parse_with_dsl(source))
25+
)
26+
diffs = PargDiff.diff(dsl_tree, artifact_tree)
27+
failures << "#{File.basename(path)}: #{diffs.first(3).join("; ")}" unless diffs.empty?
28+
rescue Parsanol::ParseFailed => e
29+
failures << "#{File.basename(path)}: parse failed: #{e.message[0, 120]}"
30+
end
31+
32+
expect(failures).to be_empty,
33+
"#{failures.size}/#{maps.size} maps diverge; first: #{failures.first(5).join(" | ")}"
34+
end
35+
end

‎spec/interscript/isc/parser_spec.rb‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,48 @@
4848
expect { described_class.parse(src, filename: "test.isc") }.not_to raise_error
4949
end
5050
end
51+
52+
describe "artifact runtime" do
53+
let(:src) do
54+
<<~ISC
55+
system "s" {
56+
metadata { authority_id t }
57+
stage main {
58+
parallel {
59+
sub "a" "b"
60+
}
61+
}
62+
}
63+
ISC
64+
end
65+
66+
it "parses through the shipped .parg artifact when parsanol has PARG" do
67+
skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact)
68+
69+
tree = described_class.parse(src)
70+
doc = Interscript::Isc::DocumentBuilder.build(tree, filename: "test.isc")
71+
expect(doc[:systemCode]).to eq("s")
72+
expect(doc[:tests]).to be_empty
73+
end
74+
75+
it "produces byte-identical trees via artifact and DSL paths" do
76+
skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact)
77+
78+
artifact_tree = described_class.parse_with_artifact(src)
79+
dsl_tree = described_class.parse_with_dsl(src)
80+
expect(artifact_tree).to eq(dsl_tree)
81+
end
82+
83+
it "falls back to the DSL grammar when the artifact cannot load" do
84+
skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact)
85+
86+
allow(File).to receive(:read).and_call_original
87+
allow(File).to receive(:read).with(%r{isc\.artifact\.json}).and_raise(Errno::ENOENT)
88+
described_class.reset_artifact
89+
tree = described_class.parse(src)
90+
doc = Interscript::Isc::DocumentBuilder.build(tree, filename: "test.isc")
91+
expect(doc[:systemCode]).to eq("s")
92+
described_class.reset_artifact
93+
end
94+
end
5195
end

0 commit comments

Comments
 (0)