parsanol 1.3.56-arm-linux → 1.3.57-arm-linux

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/exe/parsanol +7 -0
  3. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  4. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  5. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  6. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  7. data/lib/parsanol/native/libparsanol.so +0 -0
  8. data/lib/parsanol/native/serializer.rb +5 -2
  9. data/lib/parsanol/parg/artifact.rb +213 -0
  10. data/lib/parsanol/parg/authoring.rb +110 -0
  11. data/lib/parsanol/parg/bindings.rb +129 -0
  12. data/lib/parsanol/parg/cli.rb +251 -0
  13. data/lib/parsanol/parg/compiler.rb +465 -0
  14. data/lib/parsanol/parg/derive.rb +28 -0
  15. data/lib/parsanol/parg/document.rb +69 -0
  16. data/lib/parsanol/parg/error.rb +18 -0
  17. data/lib/parsanol/parg/frontend.rb +173 -0
  18. data/lib/parsanol/parg/import.rb +34 -0
  19. data/lib/parsanol/parg/importers/abnf.rb +299 -0
  20. data/lib/parsanol/parg/importers/ebnf.rb +201 -0
  21. data/lib/parsanol/parg/importers/pest.rb +314 -0
  22. data/lib/parsanol/parg/imports.rb +87 -0
  23. data/lib/parsanol/parg/lexer.rb +72 -0
  24. data/lib/parsanol/parg/lints.rb +171 -0
  25. data/lib/parsanol/parg/lsp.rb +208 -0
  26. data/lib/parsanol/parg/lutaml.rb +67 -0
  27. data/lib/parsanol/parg/node.rb +20 -0
  28. data/lib/parsanol/parg/parser.rb +458 -0
  29. data/lib/parsanol/parg/preprocess.rb +36 -0
  30. data/lib/parsanol/parg/render.rb +44 -0
  31. data/lib/parsanol/parg/selfhost.rb +49 -0
  32. data/lib/parsanol/parg/visitor.rb +64 -0
  33. data/lib/parsanol/parg.rb +38 -0
  34. data/lib/parsanol/version.rb +1 -1
  35. data/lib/parsanol/vm.rb +7 -5
  36. data/lib/parsanol.rb +3 -0
  37. data/parsanol.gemspec +3 -1
  38. metadata +31 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aeca0c2fc9dc07051ae8d1aaf97c301b5e4a15e50db4dddfecbc74ff56ffcb4
4
- data.tar.gz: 40753b75f019ceab0e9b4d9f1e7b5d4b7daf140256afa1305725d7547ba9da26
3
+ metadata.gz: b95e9035301a942d6d8f3acea734d6a4b0f6c5716dee43bc003cbb0be22060d1
4
+ data.tar.gz: c473d80ba7ebcf0ad407c5b1d120641bfc914808d90d7f3cbe45af296f391c69
5
5
  SHA512:
6
- metadata.gz: db1a264aa20d104ebfb43810ad68821b602d5d117972acc1d4827c8d959e068b4772037a28a2d952c450861a5c64e913917eaba291805a38ead18915dd2f7e9c
7
- data.tar.gz: 7e9a5ea62f34ae8b25ad159fce73f128fefe7ad32bf81f8809261e443ed18ad2d15561747a971fa74ac04f46817912923ab4b1c3e547946bb64ee45f93cfc602
6
+ metadata.gz: f998f1206ae7342ff33b5ed1f8225e0a05b9e6f92af7602d7c7741763ca621ee22c50d95ec303a5f44e2ec2f373ee714e20aeb4a57ccc8da108da1b290895820
7
+ data.tar.gz: 0a607fc6dda81038d2181e7226afe026a025564deed09b12caab1c938afd25f086096bd3bf6210c0a1b4bb38dfe8445fef032654832dfebd2e24f24b4df2becc
data/exe/parsanol ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "parsanol"
5
+ require "parsanol/parg/cli"
6
+ argv = ARGV[0] == "parg" ? ARGV[1..] : ARGV
7
+ exit Parsanol::PARG::CLI.run(argv)
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -95,9 +95,12 @@ module Parsanol
95
95
 
96
96
  def serialize_re(atom)
97
97
  # Ruby's Regexp#to_s produces "(?-mix:pattern)" format
98
- # We need to extract just the pattern for the Rust parser
98
+ # The pattern must reach the Rust regex engine verbatim, including
99
+ # inline flag groups: stripping (?i:...) silently turned every
100
+ # case-insensitive regex into a case-sensitive one on the native
101
+ # path (the Ruby atom still compiled it with the flag, so the two
102
+ # engines disagreed). Rust regex understands the same inline groups.
99
103
  pattern = atom.match
100
- pattern = ::Regexp.last_match(1) if pattern =~ /^\(\?[-mix]*:(.+)\)$/
101
104
  {
102
105
  "Re" => {
103
106
  "pattern" => pattern,
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Parsanol
7
+ module PARG
8
+ # A compiled artifact envelope: load, verify checksum, parse inputs.
9
+ #
10
+ # The envelope's grammar section is the portable JSON the native engines
11
+ # consume; the embedded PARG source lets a Ruby runtime recompile the atom
12
+ # tree locally without any other dependency.
13
+ class Artifact
14
+ attr_reader :envelope, :path, :tables_dir
15
+
16
+ def self.load(path, tables_dir: nil)
17
+ envelope = JSON.parse(File.read(path))
18
+ new(envelope, path, tables_dir || File.dirname(path))
19
+ end
20
+
21
+ def self.from_json(text, tables_dir: nil)
22
+ new(JSON.parse(text), nil, tables_dir)
23
+ end
24
+
25
+ SUPPORTED_SHAPE = "parsanol-tree/v2"
26
+
27
+ def initialize(envelope, path, tables_dir)
28
+ unless envelope["shape"] == SUPPORTED_SHAPE
29
+ raise ArtifactError,
30
+ "unsupported artifact shape #{envelope['shape'].inspect} " \
31
+ "(this runtime implements #{SUPPORTED_SHAPE.inspect})"
32
+ end
33
+ @envelope = envelope
34
+ @path = path
35
+ @tables_dir = tables_dir
36
+ verify_checksum!
37
+ @compiler = nil
38
+ end
39
+
40
+ def version = envelope["version"]
41
+
42
+ def entries = envelope["entries"].keys
43
+
44
+ def entry(name)
45
+ envelope["entries"].fetch(name) do
46
+ raise ArtifactError, "unknown entry #{name.inspect}"
47
+ end
48
+ end
49
+
50
+ # Parse via the native engine: the envelope's grammar section is the
51
+ # exact portable JSON the Rust side registers, so the artifact parse
52
+ # path is a straight register-and-run — no Ruby recompilation. The
53
+ # Ruby atom runtime (mode: :ruby) remains available explicitly, and
54
+ # serves as the fallback on platforms without the extension.
55
+ def parse(entry_name, input, mode: :native)
56
+ case mode
57
+ when :native
58
+ if Native.available?
59
+ return Native.parse(JSON.generate(entry(entry_name).fetch("grammar")), input)
60
+ end
61
+
62
+ root_atom(entry_name).parse(input)
63
+ when :ruby
64
+ root_atom(entry_name).parse(input)
65
+ else
66
+ raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
67
+ end
68
+ end
69
+
70
+ def apply_bindings(entry_name, shape)
71
+ Bindings.apply(self, entry(entry_name), shape)
72
+ end
73
+
74
+ # Render the identifier string from a bound attribute map (F6).
75
+ def render(_entry_name, bound, variant: "default")
76
+ Render.apply(envelope["render"] || {}, variant, bound)
77
+ end
78
+
79
+ def render_string(entry_name, input, variant: "default")
80
+ render(entry_name, parse_and_bind(entry_name, input), variant: variant)
81
+ end
82
+
83
+ # Evaluate a named derive spec against a bound map (F6).
84
+ def derive(name, bound)
85
+ Derive.apply(envelope["derive"] || {}, name, bound)
86
+ end
87
+
88
+ def derive_string(entry_name, input, name)
89
+ derive(name, parse_and_bind(entry_name, input))
90
+ end
91
+
92
+ def parse_and_bind(entry_name, input, mode: :native)
93
+ apply_bindings(entry_name, parse(entry_name, input, mode: mode))
94
+ end
95
+
96
+ # Re-run the grammar's in-file tests against this artifact. Returns
97
+ # the failure descriptions; empty means every test passes.
98
+ def run_tests(mode: :native)
99
+ run_test_list(envelope.fetch("tests", []), mode: mode)
100
+ end
101
+
102
+ # Run an external test list (suite files yield Document::Test
103
+ # structs; embedded tests are artifact-shaped hashes).
104
+ def run_test_list(tests, mode: :native)
105
+ tests.filter_map do |test|
106
+ hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
107
+ entry_name = hash["entry"] || entries.first
108
+ begin
109
+ bound = parse_and_bind(entry_name, hash["input"], mode: mode)
110
+ if hash["kind"] == "reject"
111
+ "test #{hash['input'].inspect}: expected the input to be rejected"
112
+ elsif hash["kind"] == "example"
113
+ expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
114
+ mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
115
+ next if mismatched.empty?
116
+
117
+ "test #{hash['input'].inspect}: expected captures " \
118
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
119
+ end
120
+ rescue Parsanol::ParseFailed
121
+ unless hash["kind"] == "reject"
122
+ "test #{hash['input'].inspect}: expected the input to parse"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def test_to_hash(test)
129
+ {
130
+ "entry" => test.entry,
131
+ "kind" => test.kind.to_s,
132
+ "input" => test.input,
133
+ "expect" => test.expect,
134
+ }
135
+ end
136
+
137
+ # Rule documentation embedded from ## doc comments.
138
+ def rule_docs = envelope.fetch("docs", {})
139
+
140
+ # Structured parse diagnostics (PN 2): {offset, message} for the
141
+ # deepest failure of the most recent parse attempt on this entry.
142
+ def parse_with_diagnostics(entry_name, input, mode: :native)
143
+ shape = parse(entry_name, input, mode: mode)
144
+ { "ok" => true, "offset" => nil, "message" => nil, "shape" => shape }
145
+ rescue Parsanol::ParseFailed => e
146
+ cause = deepest_cause(e.parse_failure_cause)
147
+ { "ok" => false, "offset" => cause&.position,
148
+ "message" => cause&.message || e.message, "shape" => nil }
149
+ end
150
+
151
+ def deepest_cause(cause)
152
+ return cause if cause.nil? || cause.children.empty?
153
+
154
+ deepest = cause.children.filter_map { |child| deepest_cause(child) }
155
+ .max_by { |node| node.position.to_i }
156
+ (deepest&.position.to_i >= cause.position.to_i ? deepest : cause)
157
+ end
158
+
159
+ def table_rows(name)
160
+ @table_cache ||= {}
161
+ return @table_cache[name] if @table_cache.key?(name)
162
+
163
+ declared = envelope["tables"].fetch(name) do
164
+ raise ArtifactError, "artifact does not declare table #{name.inspect}"
165
+ end
166
+ if declared.is_a?(Hash) && declared.key?("rows")
167
+ return @table_cache[name] = declared["rows"]
168
+ end
169
+
170
+ file = declared
171
+ path = File.join(@tables_dir.to_s, file)
172
+ raw = path.end_with?(".json") ? JSON.parse(File.read(path)) : YAML.safe_load_file(path, aliases: true)
173
+ rows = case raw
174
+ when Hash
175
+ raw.map { |key, value| { "name" => key.to_s }.merge(value.to_h.transform_keys(&:to_s)) }
176
+ when Array then raw.map { |row| row.to_h.transform_keys(&:to_s) }
177
+ else raise ArtifactError, "table #{name.inspect} must be a map or array"
178
+ end
179
+ @table_cache[name] = rows
180
+ end
181
+
182
+ def compiler
183
+ @compiler ||= begin
184
+ document = Parser.new(envelope.fetch("source")).parse
185
+ # `use`-importing grammars carry dotted cross-grammar rule
186
+ # references that only resolve once the imported sources merge;
187
+ # without this the pure-Ruby path fails on artifacts whose
188
+ # native atom graphs inline everything. The imported sources
189
+ # sit beside the artifact (vendors ship them together).
190
+ imports_dir = path && File.directory?(File.dirname(path)) ? File.dirname(path) : nil
191
+ Imports.merge!(document, [imports_dir].compact) if imports_dir
192
+ Compiler.new(document, @tables_dir)
193
+ end
194
+ end
195
+
196
+ def root_atom(entry_name)
197
+ rule = entry(entry_name).fetch("root")
198
+ @compiled ||= {}
199
+ @compiled[rule] ||= compiler.atom_for(rule)
200
+ end
201
+
202
+ def verify_checksum!
203
+ stored = envelope["checksum"]
204
+ computed = Compiler.checksum(envelope)
205
+ return if stored == computed
206
+
207
+ raise ArtifactError,
208
+ "artifact checksum mismatch: stored #{stored.inspect}, " \
209
+ "computed #{computed.inspect}"
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Parsanol
6
+ module PARG
7
+ # External test suites: *.pgtest files carrying the same test syntax
8
+ # as the inline test section (accept / reject / example). Suites are
9
+ # development-time corpora — they iterate a grammar against its
10
+ # binding requirements without being embedded in the artifact.
11
+ #
12
+ # suite smoke for entry identifier {
13
+ # accept "ISO 8601-1:2019"
14
+ # example "ISO/CD 12345" { stage: "draft20" }
15
+ # }
16
+ module Suite
17
+ module_function
18
+
19
+ # Load every *.pgtest file in a directory.
20
+ # Returns { suite_name => [Document::Test, ...] }.
21
+ def load(dir)
22
+ Dir.glob(File.join(dir, "*.pgtest")).to_h do |file|
23
+ [File.basename(file, ".pgtest"), read_file(file)]
24
+ end
25
+ end
26
+
27
+ def read_file(file)
28
+ content = File.read(file).gsub(/^#[^\n]*\n/, "")
29
+ header = content.match(/suite\s+\w+(?:\s+for\s+entry\s+(?<entry>\w+))?\s*\{/)
30
+ unless header
31
+ raise ParseError,
32
+ "#{file}: expected 'suite <name> [for <entry>] { ... }'"
33
+ end
34
+
35
+ body = content[header.end(0)..].sub(/\}\s*\z/, "")
36
+ document = Parser.new("grammar Suite version \"0.0.0\" {\n}\ntest {\n#{body}\n}\n").parse
37
+ document.tests.map do |test|
38
+ Document::Test.new(header[:entry] || test.entry, test.kind, test.input, test.expect)
39
+ end
40
+ end
41
+ end
42
+
43
+ # The binding-requirements schema: what a language-specific data model
44
+ # must implement for an entry. Derived from the declared bindings plus
45
+ # the observed example values.
46
+ module Schema
47
+ module_function
48
+
49
+ def from_artifact(artifact)
50
+ artifact.entries.to_h do |entry|
51
+ entry_data = artifact.entry(entry)
52
+ fields = entry_data["bindings"].to_h do |binding|
53
+ requirements = {
54
+ "type" => binding["type"],
55
+ "card" => binding["card"] || "1",
56
+ }
57
+ requirements["preprocess"] = binding["preprocess"] if binding["preprocess"]
58
+ [binding["path"], requirements]
59
+ end
60
+ examples = artifact.envelope.fetch("tests", [])
61
+ .select { |test| (test["kind"] == "example" && test["entry"].nil?) || test["entry"] == entry }
62
+ .map { |test| { "input" => test["input"], "captures" => test["expect"] } }
63
+ [entry, { "root" => entry_data["root"], "fields" => fields, "examples" => examples }]
64
+ end
65
+ end
66
+
67
+ # TypeScript materialization types: one interface per entry, one
68
+ # property per bound field. Cards ending in * become arrays;
69
+ # 0..1 becomes nullable.
70
+ def to_typescript(schema)
71
+ lines = ["// Generated by parsanol parg schema --ts — binding-requirements types."]
72
+ schema.each do |entry, data|
73
+ lines << ""
74
+ lines << "export interface #{ts_name(entry)} {"
75
+ data["fields"].each do |path, requirements|
76
+ lines << " #{ts_prop(path)}: #{ts_type(requirements)};"
77
+ end
78
+ lines << "}"
79
+ end
80
+ "#{lines.join("\n")}\n"
81
+ end
82
+
83
+ def ts_name(entry)
84
+ entry.split(/[_.]/).map(&:capitalize).join
85
+ end
86
+
87
+ def ts_prop(path)
88
+ leaf = path.include?("[]") ? path.split("].").last : path
89
+ leaf.gsub(/([A-Za-z0-9])_([a-z])/) do
90
+ "#{Regexp.last_match(1)}#{Regexp.last_match(2).upcase}"
91
+ end
92
+ end
93
+
94
+ def ts_type(requirements)
95
+ base = case requirements["type"]
96
+ when "integer", "float" then "number"
97
+ when "boolean" then "boolean"
98
+ else "string"
99
+ end
100
+ if requirements["card"].to_s.end_with?("*")
101
+ "#{base}[]"
102
+ elsif requirements["card"] == "0..1"
103
+ "#{base} | null"
104
+ else
105
+ base
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ module PARG
5
+ # Applies an entry's artifact bindings to a parsanol-shape parse tree,
6
+ # producing the attribute hash that fills a lutaml-model class:
7
+ # capture collection -> preprocessing -> type cast -> path assignment.
8
+ module Bindings
9
+ module_function
10
+
11
+ def apply(artifact, entry, shape)
12
+ bindings = entry["bindings"] || []
13
+ captures = collect_captures(shape)
14
+ out = {}
15
+ scalar, arrays = bindings.partition { |binding| !binding["path"].to_s.include?("[]") }
16
+ scalar.each do |binding|
17
+ validate_path!(binding)
18
+ assign_scalar(out, artifact, binding, captures)
19
+ end
20
+ arrays.group_by { |binding| binding["path"].split("[]").first }.each do |prefix, group|
21
+ assign_array(out, artifact, prefix, group, captures)
22
+ end
23
+ out
24
+ end
25
+
26
+ def collect_captures(shape)
27
+ found = Hash.new { |hash, key| hash[key] = [] }
28
+ queue = [shape]
29
+ until queue.empty?
30
+ node = queue.shift
31
+ case node
32
+ when Hash
33
+ node.each do |key, value|
34
+ found[key.to_sym] << value
35
+ queue << value
36
+ end
37
+ when Array then queue.concat(node)
38
+ end
39
+ end
40
+ found
41
+ end
42
+
43
+ def assign_scalar(out, artifact, binding, captures)
44
+ value = captures[binding["capture"].to_sym].first
45
+ return if value.nil?
46
+
47
+ out[leaf_key(binding)] = finalize(artifact, binding, value)
48
+ end
49
+
50
+ def assign_array(out, artifact, prefix, group, captures)
51
+ count = group.map { |binding| captures[binding["capture"].to_sym].length }.max || 0
52
+ return if count.zero?
53
+
54
+ list = (out[prefix] ||= [])
55
+ count.times do |index|
56
+ element = {}
57
+ group.each do |binding|
58
+ value = captures[binding["capture"].to_sym][index]
59
+ next if value.nil?
60
+
61
+ element[leaf_key(binding)] = finalize(artifact, binding, value)
62
+ end
63
+ list << element
64
+ end
65
+ end
66
+
67
+ def validate_path!(binding)
68
+ path = binding["path"].to_s
69
+ return unless path.include?(".") || path.include?("[")
70
+
71
+ raise ArtifactError,
72
+ "binding #{binding['capture'].inspect}: nested path " \
73
+ "#{path.inspect} is not supported; bind the components instead"
74
+ end
75
+
76
+ def leaf_key(binding)
77
+ path = binding["path"].to_s
78
+ leaf = path.include?("[]") ? path.split("].").last : path
79
+ leaf.to_sym
80
+ end
81
+
82
+ def finalize(artifact, binding, value)
83
+ cast(preprocess(artifact, binding, value), binding["type"])
84
+ end
85
+
86
+ def preprocess(artifact, binding, value)
87
+ return value if binding["preprocess"].nil?
88
+
89
+ steps = artifact.envelope["preprocess"].fetch(binding["preprocess"]) do
90
+ raise ArtifactError,
91
+ "preprocess step #{binding['preprocess'].inspect} not declared"
92
+ end
93
+ steps.each do |step|
94
+ case step["op"]
95
+ when "table_lookup"
96
+ map = artifact.table_rows(step["table"]).to_h do |row|
97
+ [row[step["from"]].to_s, row[step["to"]]]
98
+ end
99
+ value = map[value.to_s] || value
100
+ else
101
+ raise ArtifactError, "unknown preprocess op #{step['op'].inspect}"
102
+ end
103
+ end
104
+ value
105
+ end
106
+
107
+ def cast(value, type)
108
+ case type
109
+ when "integer" then Integer(value)
110
+ when "float" then Float(value)
111
+ when "string"
112
+ # Parsanol::Slice (Ruby atom runtime) and native leaf objects are
113
+ # the shape contract's capture leaves; their to_s is the text.
114
+ scalar = value.nil? || value.is_a?(String) || value.is_a?(Numeric) ||
115
+ value == true || value == false || value.is_a?(Parsanol::Slice)
116
+ unless scalar
117
+ raise ArtifactError,
118
+ "cannot bind a #{value.class} capture to (string); " \
119
+ "captures must be scalar"
120
+ end
121
+
122
+ value.to_s
123
+ when "boolean" then value == true || value.to_s.casecmp("true").zero?
124
+ else value
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end