expressir 2.4.64 → 2.4.66
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 +4 -4
- data/.gitattributes +4 -0
- data/.github/workflows/engine-compat.yml +30 -0
- data/.github/workflows/fuzz.yml +49 -0
- data/.github/workflows/lane-parity.yml +38 -0
- data/ENGINE-COMPAT.json +10 -0
- data/ext/expressir_core/Cargo.toml +1 -1
- data/lib/expressir/express/builder.rb +2 -2
- data/lib/expressir/express/core.rb +7 -1
- data/lib/expressir/express/formatters/remark_item_formatter.rb +20 -13
- data/lib/expressir/express/parser.rb +2 -1
- data/lib/expressir/p21/iso-10303-21.artifact.json +1 -0
- data/lib/expressir/p21/parser.rb +27 -1
- data/lib/expressir/tools/check_engine_compat.rb +89 -0
- data/lib/expressir/version.rb +1 -1
- data/rakelib/contract_matrix.rake +60 -0
- data/rakelib/engine_compat.rake +17 -0
- data/rakelib/fuzz.rake +99 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 181fe5fd66409895fe025e9c6ac010ce82af48f8ba9f51a697ab8c8910e8834f
|
|
4
|
+
data.tar.gz: 987e054bdf5ee0c6f9f2571286f518babb40ee01aa96d1c30450cf42b27e7ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9a09e37cf413fadbe54a5194cbfb902eb8e8ef5c60ece71edd7f4c1cf782b3eb7e2ebee86d3b64b7593d2629c5a8fc23734d886c62f48ffc5e49e52f1a4851e
|
|
7
|
+
data.tar.gz: bb9027b0e4b5bc568e00edcdbfd4a5f2f91d176ac32ddf5d43122b23601a2929ecfaf163dd27a80f9133f07ec9c15d0d503b85c601d2e106c02ca9c7af201af7
|
data/.gitattributes
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: engine-compat
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
engine-compat:
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
ruby-version: "3.4"
|
|
22
|
+
|
|
23
|
+
# The checker is stdlib-only (json/open3/rubygems) — no bundler.
|
|
24
|
+
- name: Verify ENGINE-COMPAT.json against the ext pin and rubygems
|
|
25
|
+
run: |
|
|
26
|
+
ruby -Ilib -rexpressir/tools/check_engine_compat -e '
|
|
27
|
+
failures = Expressir::Tools::CheckEngineCompat.failures(root: Dir.pwd)
|
|
28
|
+
abort(failures.join("\n")) unless failures.empty?
|
|
29
|
+
puts "engine-compat OK"
|
|
30
|
+
'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# The differential fuzz (issue #624, mirroring parsanol's fuzz.yml):
|
|
2
|
+
# date-seeded mutations over the corpus, parsed + attached + formatted
|
|
3
|
+
# on both lanes (native extension, pure-Ruby fallback) with byte-exact
|
|
4
|
+
# comparison. Drift is a release blocker.
|
|
5
|
+
name: differential-fuzz
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
schedule:
|
|
12
|
+
- cron: "0 5 * * *"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
seed:
|
|
16
|
+
description: "Generator seed (default: today, YYYYMMDD)"
|
|
17
|
+
required: false
|
|
18
|
+
cases:
|
|
19
|
+
description: "Number of mutated cases"
|
|
20
|
+
required: false
|
|
21
|
+
default: "120"
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
fuzz:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
env:
|
|
27
|
+
FUZZ_SEED: ${{ github.event.inputs.seed }}
|
|
28
|
+
FUZZ_CASES: ${{ github.event.inputs.cases }}
|
|
29
|
+
FUZZ_EXPORT_DIR: ${{ github.workspace }}/fuzz-artifacts
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: ruby/setup-ruby@v1
|
|
33
|
+
with:
|
|
34
|
+
ruby-version: "3.4"
|
|
35
|
+
bundler-cache: true
|
|
36
|
+
- name: Default the seed to today
|
|
37
|
+
run: |
|
|
38
|
+
if [ -z "$FUZZ_SEED" ]; then
|
|
39
|
+
echo "FUZZ_SEED=$(date +%Y%m%d)" >> "$GITHUB_ENV"
|
|
40
|
+
fi
|
|
41
|
+
- name: Differential fuzz across the lanes
|
|
42
|
+
run: |
|
|
43
|
+
bundle exec rake fuzz:run 2>&1 | tee fuzz.log
|
|
44
|
+
{
|
|
45
|
+
echo '### Differential fuzz'
|
|
46
|
+
echo '```'
|
|
47
|
+
grep 'fuzz stats' fuzz.log || true
|
|
48
|
+
echo '```'
|
|
49
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# The lane-parity gate (issue #623): the pure-Ruby fallback is a
|
|
2
|
+
# supported configuration, not a degraded one — the suite runs on
|
|
3
|
+
# both lanes and the fallback lane must stay green. Drift between
|
|
4
|
+
# the native lane and the fallback lane is a release blocker.
|
|
5
|
+
name: lane-parity
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [master, main]
|
|
13
|
+
pull_request:
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
native-lane:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
ruby-version: "3.4"
|
|
23
|
+
bundler-cache: true
|
|
24
|
+
- name: Suite on the native lane
|
|
25
|
+
run: bundle exec rspec spec/expressir/express spec/expressir/p21 -f progress
|
|
26
|
+
|
|
27
|
+
fallback-lane:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: ruby/setup-ruby@v1
|
|
32
|
+
with:
|
|
33
|
+
ruby-version: "3.4"
|
|
34
|
+
bundler-cache: true
|
|
35
|
+
- name: Suite on the pure-Ruby fallback lane
|
|
36
|
+
run: bundle exec rspec spec/expressir/express spec/expressir/p21 -f progress
|
|
37
|
+
env:
|
|
38
|
+
EXPRESSIR_DISABLE_NATIVE: "1"
|
data/ENGINE-COMPAT.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"engine_commit": "7a3fb54319a47b96ec07b9324066f98099bb2f8a",
|
|
3
|
+
"rubygems": "2.4.65",
|
|
4
|
+
"ext": "v0.1.35",
|
|
5
|
+
"crates": null,
|
|
6
|
+
"npm": null,
|
|
7
|
+
"pypi": null,
|
|
8
|
+
"released_at": "2026-11-11T02:02:03Z",
|
|
9
|
+
"_comment": "expressir#618 — the engine-compat manifest, mirroring parsanol-rs#423. Maps the expressir gem train to the expressir-rs ext tag and its engine commit; the null fields fill as the crates/npm/pypi tiers land (expressir#619/#622/#621). tools/check_engine_compat.rb verifies the pairings in CI; every release train updates this file."
|
|
10
|
+
}
|
|
@@ -13,7 +13,7 @@ magnus = { version = "0.9" }
|
|
|
13
13
|
# Pinned core release (M1, TODO.parity-ee/19): the ext never floats
|
|
14
14
|
# main — bump the tag per expressir release, alongside the validation
|
|
15
15
|
# run that pins it.
|
|
16
|
-
expressir-rs = { git = "https://github.com/lutaml/expressir-rs", tag = "v0.1.
|
|
16
|
+
expressir-rs = { git = "https://github.com/lutaml/expressir-rs", tag = "v0.1.35" }
|
|
17
17
|
serde_json = "1.0"
|
|
18
18
|
|
|
19
19
|
|
|
@@ -106,7 +106,7 @@ module Expressir
|
|
|
106
106
|
# build so recursive build calls can read source/include_source via
|
|
107
107
|
# `Builder.source` / `Builder.include_source`. The previous context
|
|
108
108
|
# (if any) is restored on exit so nested builds are reentrant.
|
|
109
|
-
def build_with_remarks(ast, source: nil, include_source: nil)
|
|
109
|
+
def build_with_remarks(ast, source: nil, include_source: nil, annotations: false)
|
|
110
110
|
# Trigger BuilderRegistry autoload so all AST node type handlers
|
|
111
111
|
# are registered before build() runs. The reference to
|
|
112
112
|
# BuilderRegistry resolves the autoload defined in express.rb.
|
|
@@ -123,7 +123,7 @@ module Expressir
|
|
|
123
123
|
# Only attach remarks if include_source is explicitly true
|
|
124
124
|
# (nil means use default behavior - attach remarks)
|
|
125
125
|
if source && result && include_source != false
|
|
126
|
-
attacher = RemarkAttacher.new(source)
|
|
126
|
+
attacher = RemarkAttacher.new(source, annotations: annotations)
|
|
127
127
|
attacher.attach(result)
|
|
128
128
|
end
|
|
129
129
|
|
|
@@ -9,7 +9,11 @@ module Expressir
|
|
|
9
9
|
# callers fall back to the pure-Ruby parser otherwise.
|
|
10
10
|
#
|
|
11
11
|
module Core
|
|
12
|
-
|
|
12
|
+
# EXPRESSIR_DISABLE_NATIVE=1 forces the pure-Ruby fallback lanes
|
|
13
|
+
# even when the extension is present: the no-native lane is a
|
|
14
|
+
# supported configuration (issue #623) — CI runs the suite on
|
|
15
|
+
# both lanes so fallback drift cannot land.
|
|
16
|
+
NATIVE_AVAILABLE = ENV["EXPRESSIR_DISABLE_NATIVE"] != "1" && begin
|
|
13
17
|
require "expressir/expressir_core"
|
|
14
18
|
true
|
|
15
19
|
rescue LoadError
|
|
@@ -68,6 +72,8 @@ module Expressir
|
|
|
68
72
|
# RemarkScanner::Remark's fields (position, end_position, line,
|
|
69
73
|
# text, tag, format), straight from the Rust scanner.
|
|
70
74
|
def self.scan_remarks(source, annotations: false)
|
|
75
|
+
raise NotImplementedError, "the native extension is unavailable" unless NATIVE_AVAILABLE
|
|
76
|
+
|
|
71
77
|
::Expressir::Core.scan_remarks(source, annotations)
|
|
72
78
|
end
|
|
73
79
|
|
|
@@ -13,27 +13,23 @@ module Expressir
|
|
|
13
13
|
# The tag form follows the owner: items in rule contexts
|
|
14
14
|
# (WHERE/UNIQUE/IP) historically carry bare tags
|
|
15
15
|
# (`--"unusual_placement" text`), WG12-style items carry the
|
|
16
|
-
# schema-relative path.
|
|
17
|
-
#
|
|
18
|
-
# (
|
|
16
|
+
# schema-relative path. A referable `__note:id` has no rule to
|
|
17
|
+
# anchor it in a bare tail tag (it would reattach to the
|
|
18
|
+
# schema), so it carries the path too (annotated-express#103).
|
|
19
19
|
rule_context = node.parent.is_a?(Model::Declarations::WhereRule) ||
|
|
20
20
|
node.parent.is_a?(Model::Declarations::UniqueRule) ||
|
|
21
21
|
node.parent.is_a?(Model::Declarations::InformalPropositionRule)
|
|
22
|
-
if rule_context
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
root = root.parent while root.respond_to?(:parent) && root.parent
|
|
28
|
-
prefix = "#{root.id}." if root.respond_to?(:id) && root.id
|
|
29
|
-
tag = tag.delete_prefix(prefix) if prefix && tag.start_with?(prefix)
|
|
30
|
-
end
|
|
22
|
+
tag = if rule_context && !node.id.to_s.start_with?("__")
|
|
23
|
+
node.id.to_s
|
|
24
|
+
else
|
|
25
|
+
schema_relative_path(node)
|
|
26
|
+
end
|
|
31
27
|
|
|
32
28
|
# Remark items are WG12 annotation blocks: the embedded form,
|
|
33
29
|
# a block per remark, matching the source convention of the
|
|
34
30
|
# annotated corpora (the conservation pairs are embedded, so
|
|
35
31
|
# tail-form emission reads as phantom gains). Rule-context
|
|
36
|
-
# owners keep the historic
|
|
32
|
+
# owners keep the historic tail form.
|
|
37
33
|
if rule_context
|
|
38
34
|
return node.remarks
|
|
39
35
|
.map { |remark| "--\"#{tag}\" #{remark}" }
|
|
@@ -48,6 +44,17 @@ module Expressir
|
|
|
48
44
|
end
|
|
49
45
|
end.join("\n")
|
|
50
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# node.path is rooted at the ExpFile, whose id is the file path
|
|
51
|
+
# or nil; drop that leading segment (expressir#603).
|
|
52
|
+
def schema_relative_path(node)
|
|
53
|
+
root = node
|
|
54
|
+
root = root.parent while root.respond_to?(:parent) && root.parent
|
|
55
|
+
prefix = root.respond_to?(:id) ? "#{root.id}." : ""
|
|
56
|
+
node.path.to_s.delete_prefix(prefix)
|
|
57
|
+
end
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
end
|
|
@@ -94,7 +94,8 @@ module Expressir
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
exp_file = ::Expressir::Express::Builder.build_with_remarks(ast, source: source,
|
|
97
|
-
include_source: include_source
|
|
97
|
+
include_source: include_source,
|
|
98
|
+
annotations: annotations)
|
|
98
99
|
|
|
99
100
|
# issue #537 part 2: a remark-only fragment (no SCHEMA
|
|
100
101
|
# wrapper — e.g. conversion artifacts) parses to an empty
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"bindings":null,"version":"1.0.0","grammar":"Iso1030321","shape":"parsanol-tree/v2","binding_version":1,"entries":{"exchange_structure":{"root":"exchange_structure","grammar":{"atoms":[{"Entity":{"atom":15}},{"Re":{"pattern":"[ ]"}},{"Re":{"pattern":"[\\x09]"}},{"Re":{"pattern":"[\\x0d]"}},{"Re":{"pattern":"[\\x0a]"}},{"Entity":{"atom":13}},{"Str":{"pattern":"/*"}},{"Str":{"pattern":"*/"}},{"Lookahead":{"atom":7,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[8,9]}},{"Repetition":{"atom":10,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"*/"}},{"Sequence":{"atoms":[6,11,12]}},{"Alternative":{"atoms":[1,2,3,4,5]}},{"Repetition":{"atom":14,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"ISO-10303-21;"}},{"Entity":{"atom":15}},{"Entity":{"atom":183}},{"Str":{"pattern":"HEADER;"}},{"Entity":{"atom":15}},{"Entity":{"atom":25}},{"Str":{"pattern":".."}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[22,23]}},{"Repetition":{"atom":24,"min":0,"max":null,"tag":"Repetition"}},{"Entity":{"atom":178}},{"Entity":{"atom":174}},{"Entity":{"atom":37}},{"Str":{"pattern":"!"}},{"Repetition":{"atom":29,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[31,32,33,34]}},{"Repetition":{"atom":35,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[30,36]}},{"Named":{"name":"keyword","atom":28}},{"Entity":{"atom":15}},{"Entity":{"atom":172}},{"Str":{"pattern":"("}},{"Entity":{"atom":15}},{"Entity":{"atom":168}},{"Entity":{"atom":166}},{"Entity":{"atom":37}},{"Named":{"name":"keyword","atom":45}},{"Entity":{"atom":15}},{"Entity":{"atom":164}},{"Named":{"name":"parameter","atom":43}},{"Str":{"pattern":","}},{"Entity":{"atom":15}},{"Entity":{"atom":158}},{"Named":{"name":"typed_parameter","atom":44}},{"Entity":{"atom":62}},{"Named":{"name":"parameter","atom":52}},{"Sequence":{"atoms":[50,51,55]}},{"Repetition":{"atom":56,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[49,57]}},{"Repetition":{"atom":58,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[41,42,59,60,61]}},{"Named":{"name":"parameter_list","atom":54}},{"Entity":{"atom":68}},{"Str":{"pattern":"#"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":66,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[65,67]}},{"Named":{"name":"entity_instance_name","atom":64}},{"Entity":{"atom":82}},{"Str":{"pattern":"#"}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[72,73,74]}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[76,77,78,79]}},{"Repetition":{"atom":80,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[71,75,81]}},{"Named":{"name":"constant_reference","atom":70}},{"Entity":{"atom":92}},{"Str":{"pattern":"<"}},{"Str":{"pattern":">"}},{"Lookahead":{"atom":86,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[87,88]}},{"Repetition":{"atom":89,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":">"}},{"Sequence":{"atoms":[85,90,91]}},{"Named":{"name":"external_anchor","atom":84}},{"Entity":{"atom":117}},{"Re":{"pattern":"[+]"}},{"Re":{"pattern":"[\\-]"}},{"Alternative":{"atoms":[95,96]}},{"Repetition":{"atom":97,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":99,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"."}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":102,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[101,103]}},{"Repetition":{"atom":104,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[E]"}},{"Re":{"pattern":"[e]"}},{"Alternative":{"atoms":[106,107]}},{"Re":{"pattern":"[+]"}},{"Re":{"pattern":"[\\-]"}},{"Alternative":{"atoms":[109,110]}},{"Repetition":{"atom":111,"min":0,"max":1,"tag":"Maybe"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":113,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[108,112,114]}},{"Repetition":{"atom":115,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[98,100,105,116]}},{"Named":{"name":"number_literal","atom":94}},{"Entity":{"atom":128}},{"Str":{"pattern":"."}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[121,122,123,124]}},{"Repetition":{"atom":125,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"."}},{"Sequence":{"atoms":[120,126,127]}},{"Named":{"name":"enumeration","atom":119}},{"Entity":{"atom":138}},{"Re":{"pattern":"[\"]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[\\x41-\\x46]"}},{"Re":{"pattern":"[\\x61-\\x66]"}},{"Alternative":{"atoms":[132,133,134]}},{"Repetition":{"atom":135,"min":0,"max":null,"tag":"Repetition"}},{"Re":{"pattern":"[\"]"}},{"Sequence":{"atoms":[131,136,137]}},{"Named":{"name":"binary_literal","atom":130}},{"Entity":{"atom":150}},{"Str":{"pattern":"'"}},{"Str":{"pattern":"'"}},{"Lookahead":{"atom":142,"positive":false}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[143,144]}},{"Str":{"pattern":"''"}},{"Alternative":{"atoms":[145,146]}},{"Repetition":{"atom":147,"min":0,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"'"}},{"Sequence":{"atoms":[141,148,149]}},{"Named":{"name":"string_literal","atom":140}},{"Entity":{"atom":153}},{"Str":{"pattern":"$"}},{"Named":{"name":"unset","atom":152}},{"Entity":{"atom":156}},{"Str":{"pattern":"*"}},{"Named":{"name":"derived","atom":155}},{"Alternative":{"atoms":[53,63,69,83,93,118,129,139,151,154,157]}},{"Named":{"name":"parameter","atom":52}},{"Sequence":{"atoms":[50,51,159]}},{"Repetition":{"atom":160,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[49,161]}},{"Repetition":{"atom":162,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[41,42,163,60,61]}},{"Named":{"name":"parameter_list","atom":48}},{"Sequence":{"atoms":[46,47,165]}},{"Named":{"name":"typed_parameter","atom":44}},{"Alternative":{"atoms":[167,63,69,83,93,118,129,139,151,154,157]}},{"Named":{"name":"parameter","atom":43}},{"Sequence":{"atoms":[169,161]}},{"Repetition":{"atom":170,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[41,42,171,60,61]}},{"Named":{"name":"parameter_list","atom":40}},{"Sequence":{"atoms":[38,39,173]}},{"Named":{"name":"simple_record","atom":27}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[175,176,177]}},{"Repetition":{"atom":26,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"header_entity","atom":179}},{"Str":{"pattern":"ENDSEC;"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[19,20,21,180,181,182]}},{"Named":{"name":"header_section","atom":18}},{"Entity":{"atom":225}},{"Str":{"pattern":"ANCHOR;"}},{"Entity":{"atom":15}},{"Entity":{"atom":219}},{"Str":{"pattern":"<"}},{"Str":{"pattern":">"}},{"Lookahead":{"atom":190,"positive":false}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Re":{"pattern":"[.]"}},{"Re":{"pattern":"[/]"}},{"Re":{"pattern":"[:]"}},{"Re":{"pattern":"[?]"}},{"Re":{"pattern":"[=]"}},{"Re":{"pattern":"[&]"}},{"Re":{"pattern":"[%]"}},{"Re":{"pattern":"[\\-]"}},{"Alternative":{"atoms":[192,193,194,195,196,197,198,199,200,201,202,203]}},{"Repetition":{"atom":204,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"anchor_label","atom":205}},{"Str":{"pattern":">"}},{"Entity":{"atom":15}},{"Str":{"pattern":"="}},{"Entity":{"atom":15}},{"Entity":{"atom":68}},{"Named":{"name":"entity_instance_name","atom":211}},{"Entity":{"atom":153}},{"Named":{"name":"unset","atom":213}},{"Alternative":{"atoms":[212,214]}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[189,191,206,207,208,209,210,215,216,217]}},{"Named":{"name":"anchor_entry","atom":218}},{"Repetition":{"atom":188,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"anchor_entry","atom":220}},{"Str":{"pattern":"ENDSEC;"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[186,187,221,222,223]}},{"Named":{"name":"anchor_section","atom":224}},{"Repetition":{"atom":185,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":281}},{"Str":{"pattern":"REFERENCE;"}},{"Entity":{"atom":15}},{"Entity":{"atom":275}},{"Entity":{"atom":250}},{"Entity":{"atom":68}},{"Named":{"name":"entity_instance_name","atom":232}},{"Entity":{"atom":15}},{"Str":{"pattern":"="}},{"Entity":{"atom":15}},{"Entity":{"atom":245}},{"Str":{"pattern":"("}},{"Entity":{"atom":15}},{"Entity":{"atom":174}},{"Named":{"name":"simple_record","atom":240}},{"Repetition":{"atom":241,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":")"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[238,239,242,243,244]}},{"Named":{"name":"subsuper_record","atom":237}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[233,234,235,236,246,247,248]}},{"Named":{"name":"complex_entity_instance","atom":249}},{"Entity":{"atom":262}},{"Entity":{"atom":68}},{"Named":{"name":"entity_instance_name","atom":252}},{"Entity":{"atom":15}},{"Str":{"pattern":"="}},{"Entity":{"atom":15}},{"Entity":{"atom":174}},{"Named":{"name":"simple_record","atom":257}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[253,254,255,256,258,259,260]}},{"Named":{"name":"simple_entity_instance","atom":261}},{"Entity":{"atom":274}},{"Entity":{"atom":68}},{"Named":{"name":"entity_instance_name","atom":264}},{"Entity":{"atom":15}},{"Str":{"pattern":"="}},{"Entity":{"atom":15}},{"Entity":{"atom":92}},{"Named":{"name":"external_anchor","atom":269}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[265,266,267,268,270,271,272]}},{"Named":{"name":"external_reference_instance","atom":273}},{"Alternative":{"atoms":[231,251,263]}},{"Repetition":{"atom":230,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"instance_list","atom":276}},{"Str":{"pattern":"ENDSEC;"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[228,229,277,278,279]}},{"Named":{"name":"reference_section","atom":280}},{"Repetition":{"atom":227,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":297}},{"Str":{"pattern":"DATA"}},{"Entity":{"atom":15}},{"Entity":{"atom":172}},{"Named":{"name":"section_tag","atom":286}},{"Repetition":{"atom":287,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":";"}},{"Entity":{"atom":15}},{"Entity":{"atom":275}},{"Repetition":{"atom":291,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"instance_list","atom":292}},{"Str":{"pattern":"ENDSEC;"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[284,285,288,289,290,293,294,295]}},{"Named":{"name":"data_section","atom":296}},{"Repetition":{"atom":283,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"data_section","atom":298}},{"Str":{"pattern":"END-ISO-10303-21;"}},{"Entity":{"atom":15}},{"Sequence":{"atoms":[0,16,17,184,226,282,299,300,301]}}],"root":302},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule parameter: branches 1 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule parameter: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule entity_instance: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule entity_instance: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule entity_instance: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"exchange_structure","render":{},"derive":{},"tests":[{"entry":"exchange_structure","kind":"accept","input":"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('a'),'b');\nFILE_NAME('n');\nFILE_SCHEMA(('S'));\nENDSEC;\nDATA;\n#1=E('v');\n#2=(A(1)B(2));\nENDSEC;\nEND-ISO-10303-21;\n","expect":{}},{"entry":"exchange_structure","kind":"accept","input":"ISO-10303-21;\nHEADER;\nFILE_NAME('n');\nENDSEC;\nANCHOR;\n<lbl>=#1;\nENDSEC;\nDATA;\n#1=E('v');\nENDSEC;\nEND-ISO-10303-21;\n","expect":{}},{"entry":"exchange_structure","kind":"reject","input":"ISO-10303-21;\nHEADER;\nFILE_NAME('n');\nENDSEC;\nDATA;\n#1=E('v');\n","expect":{}}],"docs":{"ws":"Whitespace and /* */ comments (clause 5.2).","string_literal":"ISO 10303-21: doubled quotes are escaped quotes.","binary_literal":"The double-quote terminal is %x22: a bare \"'\" breaks the lexer's\nquote pairing.","signature_lines":"ISO 10303-21:2016 clause 14: two signature lines open the header\nof a signed exchange structure.","anchor_entry":"ISO 10303-21:2016 clause 9: the anchor section binds labels to\nlocal instances (a promissory anchor binds the unset value).","reference_section":"ISO 10303-21:2016 clause 10: the reference section binds a local\ninstance name to an external anchor.","exchange_structure":"The section rules carry their own capture (mirroring the DSL),\nso the call sites stay bare — an extra `as` here would\ndouble-wrap the subtree."},"source":"## ISO 10303-21 exchange structure (expressir#625).\n##\n## Single source of truth for P21 parsing across engines: the rust\n## core loads the compiled artifact (assets/p21/iso-10303-21.artifact.json),\n## and the ruby `Expressir::P21` pipeline consumes the same parse tree\n## in place of its DSL grammar (which this file mirrors 1:1 — the\n## capture names are the Builder's contract).\n##\n## Reference: ISO 10303-21 (the exchange structure; clauses 8-14,\n## annexes E/H/I/J). Supported subset: the header FILE_* entities,\n## ANCHOR and REFERENCE sections, named data sections, and\n## simple/complex/external-reference instances with lists, typed\n## parameters, references, enumerations, binaries, strings, unset ($)\n## and derived (*).\n\ngrammar Iso1030321 version \"1.0.0\" {\n\n ## Whitespace and /* */ comments (clause 5.2).\n ws = *( %x20 / %x09 / %x0D / %x0A / comment )\n\n comment = {\n \"/*\" *( !\"*/\" ANY ) \"*/\"\n }\n\n keyword = {\n [ \"!\" ] 1*( %x41-5A / %x61-7A / %x30-39 / %x5F )\n }\n\n entity_instance_name = {\n \"#\" 1*%x30-39\n }\n\n constant_reference = {\n \"#\" ( %x41-5A / %x61-7A / %x5F ) *( %x41-5A / %x61-7A / %x30-39 / %x5F )\n }\n\n external_anchor = {\n \"<\" *( !\">\" ANY ) \">\"\n }\n\n ## ISO 10303-21: doubled quotes are escaped quotes.\n string_literal = {\n \"'\" *( ( !\"'\" ANY ) / \"''\" ) \"'\"\n }\n\n ## The double-quote terminal is %x22: a bare \"'\" breaks the lexer's\n ## quote pairing.\n binary_literal = {\n %x22 *( %x30-39 / %x41-46 / %x61-66 ) %x22\n }\n\n enumeration = {\n \".\" 1*( %x41-5A / %x61-7A / %x30-39 / %x5F ) \".\"\n }\n\n unset = {\n \"$\"\n }\n\n derived = {\n \"*\"\n }\n\n number_literal = {\n [ ( %x2B / %x2D ) ] 1*%x30-39\n [ \".\" 1*%x30-39 ]\n [ ( %x45 / %x65 ) [ ( %x2B / %x2D ) ] 1*%x30-39 ]\n }\n\n parameter = {\n ( typed_parameter as typed_parameter ) /\n ( parameter_list as parameter_list ) /\n ( entity_instance_name as entity_instance_name ) /\n ( constant_reference as constant_reference ) /\n ( external_anchor as external_anchor ) /\n ( number_literal as number_literal ) /\n ( enumeration as enumeration ) /\n ( binary_literal as binary_literal ) /\n ( string_literal as string_literal ) /\n ( unset as unset ) /\n ( derived as derived )\n }\n\n parameter_list = {\n \"(\" ws\n [ parameter as parameter\n *( \",\" ws parameter as parameter )\n ]\n \")\" ws\n }\n\n typed_parameter = {\n keyword as keyword ws parameter_list as parameter_list\n }\n\n simple_record = {\n keyword as keyword ws parameter_list as parameter_list\n }\n\n subsuper_record = {\n \"(\" ws 1*( simple_record as simple_record ) \")\" ws\n }\n\n simple_entity_instance = {\n ( entity_instance_name as entity_instance_name ws\n \"=\" ws simple_record as simple_record\n \";\" ws ) as simple_entity_instance\n }\n\n complex_entity_instance = {\n ( entity_instance_name as entity_instance_name ws\n \"=\" ws subsuper_record as subsuper_record\n \";\" ws ) as complex_entity_instance\n }\n\n external_reference_instance = {\n ( entity_instance_name as entity_instance_name ws\n \"=\" ws external_anchor as external_anchor\n \";\" ws ) as external_reference_instance\n }\n\n entity_instance = {\n complex_entity_instance / simple_entity_instance / external_reference_instance\n }\n\n header_entity = {\n simple_record as simple_record \";\" ws\n }\n\n ## ISO 10303-21:2016 clause 14: two signature lines open the header\n ## of a signed exchange structure.\n signature_lines = *( \"..\" ws )\n\n header_section = {\n \"HEADER;\" ws signature_lines\n 1*( header_entity ) as header_entity\n \"ENDSEC;\" ws\n }\n\n data_section = {\n ( \"DATA\" ws\n [ parameter_list as section_tag ] \";\" ws\n *( entity_instance ) as instance_list\n \"ENDSEC;\" ws ) as data_section\n }\n\n ## ISO 10303-21:2016 clause 9: the anchor section binds labels to\n ## local instances (a promissory anchor binds the unset value).\n anchor_entry = {\n ( \"<\" !\">\" *( %x41-5A / %x61-7A / %x30-39 / %x5F / %x2E / %x2F / %x3A / %x3F / %x3D / %x26 / %x25 / %x2D ) as anchor_label\n \">\" ws \"=\" ws\n ( ( entity_instance_name as entity_instance_name ) / ( unset as unset ) )\n \";\" ws ) as anchor_entry\n }\n\n anchor_section = {\n ( \"ANCHOR;\" ws *( anchor_entry ) as anchor_entry \"ENDSEC;\" ws ) as anchor_section\n }\n\n ## ISO 10303-21:2016 clause 10: the reference section binds a local\n ## instance name to an external anchor.\n reference_section = {\n ( \"REFERENCE;\" ws *( entity_instance ) as instance_list \"ENDSEC;\" ws ) as reference_section\n }\n\n entry exchange_structure: exchange_structure\n\n ## The section rules carry their own capture (mirroring the DSL),\n ## so the call sites stay bare — an extra `as` here would\n ## double-wrap the subtree.\n exchange_structure = {\n ws \"ISO-10303-21;\" ws\n header_section as header_section\n [ anchor_section ]\n [ reference_section ]\n 1*( data_section ) as data_section\n \"END-ISO-10303-21;\" ws\n }\n\n test exchange_structure {\n accept \"ISO-10303-21;\\nHEADER;\\nFILE_DESCRIPTION(('a'),'b');\\nFILE_NAME('n');\\nFILE_SCHEMA(('S'));\\nENDSEC;\\nDATA;\\n#1=E('v');\\n#2=(A(1)B(2));\\nENDSEC;\\nEND-ISO-10303-21;\\n\"\n accept \"ISO-10303-21;\\nHEADER;\\nFILE_NAME('n');\\nENDSEC;\\nANCHOR;\\n<lbl>=#1;\\nENDSEC;\\nDATA;\\n#1=E('v');\\nENDSEC;\\nEND-ISO-10303-21;\\n\"\n reject \"ISO-10303-21;\\nHEADER;\\nFILE_NAME('n');\\nENDSEC;\\nDATA;\\n#1=E('v');\\n\"\n }\n}\n","source_hash":"70b966720a8c5918e203d607f35cb699068ffb95ab14e7323b3d40b76db3dc01","unicode_tables":"onigmo (Ruby 3.4.8)","compiler_version":"1.3.130","edition":2,"semantics_hash":"sha256:98a96f64d8b9013f71ec9dcb852a2739fdc77cd1d073dcb4db0c95b18a8e1063","checksum":"sha256:8c748f7a0d992894441f995cd5f9e2db821aab0ed5a6f29ef40ac9fa89b28245"}
|
data/lib/expressir/p21/parser.rb
CHANGED
|
@@ -4,13 +4,39 @@ module Expressir
|
|
|
4
4
|
module P21
|
|
5
5
|
# Facade: parse an ISO 10303-21 exchange structure into a
|
|
6
6
|
# {Population} (TODO.eengine/06 step 1).
|
|
7
|
+
#
|
|
8
|
+
# The grammar ships as a compiled PARG artifact
|
|
9
|
+
# (+iso-10303-21.artifact.json+, the checksummed single source of
|
|
10
|
+
# truth shared with expressir-rs; issue #625): parsing runs through
|
|
11
|
+
# the artifact when it loads, with the DSL grammar as the fallback
|
|
12
|
+
# lane. The parity spec pins the two lanes byte-identically on the
|
|
13
|
+
# annex corpus.
|
|
7
14
|
module Parser
|
|
8
15
|
module_function
|
|
9
16
|
|
|
10
17
|
def parse(source)
|
|
11
|
-
tree = Grammar.new.parse(source)
|
|
18
|
+
tree = artifact_tree(source) || Grammar.new.parse(source)
|
|
12
19
|
Builder.build(tree)
|
|
13
20
|
end
|
|
21
|
+
|
|
22
|
+
def artifact_tree(source)
|
|
23
|
+
artifact.parse("exchange_structure", source)
|
|
24
|
+
rescue Parsanol::PARG::ArtifactError
|
|
25
|
+
# The artifact itself is unusable (shape/edition/binding drift):
|
|
26
|
+
# fall back to the DSL grammar lane. A genuine ParseFailed from
|
|
27
|
+
# a valid artifact propagates — malformed input fails loudly on
|
|
28
|
+
# both lanes.
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def artifact
|
|
33
|
+
@artifact ||= begin
|
|
34
|
+
require "parsanol"
|
|
35
|
+
Parsanol::PARG::Artifact.load(
|
|
36
|
+
Expressir.root_path.join("lib/expressir/p21/iso-10303-21.artifact.json").to_s,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
14
40
|
end
|
|
15
41
|
end
|
|
16
42
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "rubygems/version"
|
|
6
|
+
|
|
7
|
+
module Expressir
|
|
8
|
+
module Tools
|
|
9
|
+
# The engine-compat manifest checker (expressir#618, mirroring
|
|
10
|
+
# parsanol-rs#423's tools/check_engine_compat.py): ENGINE-COMPAT.json
|
|
11
|
+
# maps the gem train to the expressir-rs ext tag and its engine
|
|
12
|
+
# commit; this verifies the pairings so the train cannot ship with a
|
|
13
|
+
# stale manifest.
|
|
14
|
+
#
|
|
15
|
+
# Checks:
|
|
16
|
+
# 1. The ext pin in ext/expressir_core/Cargo.toml names
|
|
17
|
+
# manifest["ext"].
|
|
18
|
+
# 2. The tag exists on origin and points at manifest["engine_commit"].
|
|
19
|
+
# 3. rubygems.org serves a version >= manifest["rubygems"] (>= because
|
|
20
|
+
# CI runs on the bump commit before the tag publish catches up).
|
|
21
|
+
module CheckEngineCompat
|
|
22
|
+
MANIFEST = "ENGINE-COMPAT.json"
|
|
23
|
+
PIN_RE = /expressir-rs = \{ git = "([^"]+)", tag = "([^"]+)" \}/
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def failures(root: Dir.pwd)
|
|
28
|
+
manifest = JSON.parse(File.read(File.join(root, MANIFEST)))
|
|
29
|
+
failures = []
|
|
30
|
+
|
|
31
|
+
failures.concat(pin_failures(root, manifest))
|
|
32
|
+
failures.concat(tag_failures(root, manifest))
|
|
33
|
+
failures.concat(registry_failures(manifest)) unless manifest["rubygems"].nil?
|
|
34
|
+
failures
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
["#{MANIFEST}: #{e.message}"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# [git url, tag] straight from the ext pin — the tag lives on the
|
|
40
|
+
# expressir-rs repository, not on expressir's origin.
|
|
41
|
+
def ext_pin(root)
|
|
42
|
+
m = File.read(File.join(root, "ext/expressir_core/Cargo.toml")).match(PIN_RE)
|
|
43
|
+
return unless m
|
|
44
|
+
|
|
45
|
+
# ssh and https forms both resolve via ls-remote; normalize to https.
|
|
46
|
+
[m[1].sub(%r{\Agit@github\.com:}, "https://github.com/"), m[2]]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pin_failures(root, manifest)
|
|
50
|
+
pin = ext_pin(root)
|
|
51
|
+
return ["ext pin missing from ext/expressir_core/Cargo.toml"] unless pin
|
|
52
|
+
return [] if pin[1] == manifest["ext"]
|
|
53
|
+
|
|
54
|
+
["ext pin #{pin[1].inspect} != manifest ext #{manifest['ext'].inspect}"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tag_failures(root, manifest)
|
|
58
|
+
pin = ext_pin(root)
|
|
59
|
+
return ["ext pin missing from ext/expressir_core/Cargo.toml"] unless pin
|
|
60
|
+
|
|
61
|
+
url, tag = pin
|
|
62
|
+
out, status = Open3.capture2("git", "ls-remote", url, "refs/tags/#{tag}")
|
|
63
|
+
return ["git ls-remote failed for #{url}"] unless status.success?
|
|
64
|
+
|
|
65
|
+
sha = out[/^([0-9a-f]{40})/, 1]
|
|
66
|
+
return ["tag #{tag.inspect} not found on #{url}"] unless sha
|
|
67
|
+
|
|
68
|
+
# annotated (release) tags carry a ^{} peeled entry; either line
|
|
69
|
+
# matching the engine commit passes.
|
|
70
|
+
return [] if out.include?(manifest["engine_commit"])
|
|
71
|
+
|
|
72
|
+
["tag #{tag.inspect} points at #{sha[0, 7]}, " \
|
|
73
|
+
"not engine_commit #{manifest['engine_commit'][0, 7]}"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def registry_failures(manifest)
|
|
77
|
+
url = "https://rubygems.org/api/v1/versions/expressir/latest.json"
|
|
78
|
+
out, status = Open3.capture2("curl", "-sf", "--retry", "3", url)
|
|
79
|
+
return ["rubygems query failed"] unless status.success?
|
|
80
|
+
|
|
81
|
+
served = JSON.parse(out)["version"]
|
|
82
|
+
return [] if Gem::Version.new(served) >= Gem::Version.new(manifest["rubygems"])
|
|
83
|
+
|
|
84
|
+
["rubygems serves #{served.inspect}, manifest wants >= " \
|
|
85
|
+
"#{manifest['rubygems'].inspect}"]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/expressir/version.rb
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The contract matrix (issue #624): the canonical lane seals one
|
|
4
|
+
# artifact per wire shape; every lane re-derives the shape from the
|
|
5
|
+
# same source and must be byte-identical. Drift here is a release
|
|
6
|
+
# blocker — canonicalization changes are invisible to parse-level
|
|
7
|
+
# differentials (the parsanol rs#397 lesson).
|
|
8
|
+
#
|
|
9
|
+
# No file-level constants (rake double-loads rakelib); helpers stay
|
|
10
|
+
# inside the task body.
|
|
11
|
+
namespace :contract_matrix do
|
|
12
|
+
require "json"
|
|
13
|
+
require "digest"
|
|
14
|
+
require "tmpdir"
|
|
15
|
+
|
|
16
|
+
desc "Seal the contract-matrix artifacts from the canonical lane"
|
|
17
|
+
task :seal do
|
|
18
|
+
require "expressir"
|
|
19
|
+
matrix = "spec/fixtures/contract-matrix"
|
|
20
|
+
source_path = File.join(matrix, "source.exp")
|
|
21
|
+
|
|
22
|
+
seal = lambda do |shape, text|
|
|
23
|
+
File.write(File.join(matrix, shape), text)
|
|
24
|
+
Digest::SHA256.hexdigest(text)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Dir.mktmpdir do |dir|
|
|
28
|
+
file = File.join(dir, "source.exp")
|
|
29
|
+
FileUtils.cp(source_path, file)
|
|
30
|
+
|
|
31
|
+
source = File.read(file)
|
|
32
|
+
wire = Expressir::Express::Core.parse_to_model_hash(source, "source.exp")
|
|
33
|
+
attached_model = Expressir::Express::Core.parse_to_model_attached_with_wire(
|
|
34
|
+
source, "source.exp"
|
|
35
|
+
)
|
|
36
|
+
attached = JSON.parse(attached_model.core_wire)
|
|
37
|
+
formatted = Expressir::Express::Core.format_model_hash(attached)
|
|
38
|
+
stripped = Expressir::Express::Core.format_model_hash(attached, no_remarks: true)
|
|
39
|
+
|
|
40
|
+
checksums = {
|
|
41
|
+
"model-hash wire" => seal.call("wire.json", "#{JSON.pretty_generate(wire)}\n"),
|
|
42
|
+
"attached wire" => seal.call("attached.json", "#{JSON.pretty_generate(attached)}\n"),
|
|
43
|
+
"formatted text" => seal.call("formatted.exp", formatted),
|
|
44
|
+
"stripped text" => seal.call("stripped.exp", stripped),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
manifest = {
|
|
48
|
+
"contract" => "what the canonical lane seals, every lane re-derives byte-identically",
|
|
49
|
+
"source" => "source.exp",
|
|
50
|
+
"canonical_lane" => "Expressir::Express::Core (the native extension)",
|
|
51
|
+
"artifacts" => checksums.transform_values { |digest| "sha256:#{digest}" },
|
|
52
|
+
"shapes" => %w[
|
|
53
|
+
model-hash wire attached wire formatted text stripped text
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
File.write(File.join(matrix, "manifest.yml"), YAML.dump(manifest))
|
|
57
|
+
puts "sealed #{checksums.size} artifacts + manifest.yml"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
|
|
5
|
+
namespace :lint do
|
|
6
|
+
desc "Verify ENGINE-COMPAT.json against the ext pin and rubygems (#618)"
|
|
7
|
+
task :engine_compat do
|
|
8
|
+
require_relative "../lib/expressir/tools/check_engine_compat"
|
|
9
|
+
failures = Expressir::Tools::CheckEngineCompat.failures(root: Dir.pwd)
|
|
10
|
+
if failures.empty?
|
|
11
|
+
puts "lint:engine_compat OK"
|
|
12
|
+
else
|
|
13
|
+
warn failures.join("\n")
|
|
14
|
+
raise "lint:engine_compat found #{failures.length} problem(s)"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/rakelib/fuzz.rake
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The differential fuzz (issue #624, mirroring parsanol's fuzz.yml):
|
|
4
|
+
# date-seeded mutations over the corpus, each source parsed + attached
|
|
5
|
+
# + formatted on BOTH lanes — the native extension and the pure-Ruby
|
|
6
|
+
# fallback (use_core: false, use_native: false) — with byte-exact
|
|
7
|
+
# comparison. Fallback drift cannot land.
|
|
8
|
+
#
|
|
9
|
+
# rake fuzz # today's seed, 40 cases
|
|
10
|
+
# rake fuzz SEED=20261107 CASES=200
|
|
11
|
+
#
|
|
12
|
+
# No file-level constants (rake double-loads rakelib); helpers stay
|
|
13
|
+
# inside the task body.
|
|
14
|
+
namespace :fuzz do
|
|
15
|
+
require "tmpdir"
|
|
16
|
+
require "fileutils"
|
|
17
|
+
|
|
18
|
+
desc "Date-seeded differential fuzz across the native and fallback lanes"
|
|
19
|
+
task :run do
|
|
20
|
+
require "expressir"
|
|
21
|
+
|
|
22
|
+
mutators = %i[
|
|
23
|
+
squeeze_whitespace reword_remark rename_identifier
|
|
24
|
+
splice_comment duplicate_line
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
corpus_sources = lambda do
|
|
28
|
+
glob = ENV.fetch("FUZZ_CORPUS", "spec/fixtures/contract-matrix/source.exp")
|
|
29
|
+
files = Dir[glob]
|
|
30
|
+
files.empty? ? ["spec/fixtures/contract-matrix/source.exp"] : files
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
mutate = lambda do |source, rng|
|
|
34
|
+
lines = source.lines
|
|
35
|
+
rng.rand(1..3).times do
|
|
36
|
+
case mutators[rng.rand(mutators.size)]
|
|
37
|
+
when :squeeze_whitespace then lines.each { |l| l.gsub!(/[ \t]+/, " ") }
|
|
38
|
+
when :reword_remark
|
|
39
|
+
lines.each_with_index do |l, i|
|
|
40
|
+
lines[i] = l.sub(/\(\*"(.*?)"\s*(.*?)\s*\*\)/) do
|
|
41
|
+
"(\"#{Regexp.last_match(1)}\" reworded fuzz note #{i} *)"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
when :rename_identifier
|
|
45
|
+
lines.map! { |l| l.sub(/\b(part|compatible)\b/) { "#{Regexp.last_match(0)}_#{rng.rand(100)}" } }
|
|
46
|
+
when :splice_comment then lines.insert(rng.rand(lines.size + 1), "(* a spliced fuzz comment *)\n")
|
|
47
|
+
when :duplicate_line then lines.insert(rng.rand(lines.size + 1), lines[rng.rand(lines.size)].to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
lines.join
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
lane_outputs = lambda do |source, lane_options|
|
|
54
|
+
Dir.mktmpdir do |dir|
|
|
55
|
+
file = File.join(dir, "fuzz.exp")
|
|
56
|
+
File.write(file, source)
|
|
57
|
+
model = Expressir::Express::Parser.from_file(file, **lane_options)
|
|
58
|
+
{
|
|
59
|
+
"format" => Expressir::Express::Formatter.new.format(model),
|
|
60
|
+
"stripped" => Expressir::Express::Formatter.new(no_remarks: true).format(model),
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
seed = Integer(ENV.fetch("FUZZ_SEED", Time.now.strftime("%Y%m%d")))
|
|
66
|
+
cases = Integer(ENV.fetch("FUZZ_CASES", 40))
|
|
67
|
+
rng = Random.new(seed)
|
|
68
|
+
sources = corpus_sources.call
|
|
69
|
+
export = ENV.fetch("FUZZ_EXPORT_DIR", "tmp/fuzz")
|
|
70
|
+
|
|
71
|
+
failures = 0
|
|
72
|
+
compared = 0
|
|
73
|
+
|
|
74
|
+
cases.times do |i|
|
|
75
|
+
base = sources[rng.rand(sources.size)]
|
|
76
|
+
mutated = mutate.call(File.read(base), rng)
|
|
77
|
+
begin
|
|
78
|
+
native_outputs = lane_outputs.call(mutated, {})
|
|
79
|
+
fallback_outputs = lane_outputs.call(mutated, { use_core: false, use_native: false })
|
|
80
|
+
compared += 1
|
|
81
|
+
native_outputs.each do |key, native_text|
|
|
82
|
+
next if native_text == fallback_outputs[key]
|
|
83
|
+
|
|
84
|
+
failures += 1
|
|
85
|
+
FileUtils.mkdir_p(export)
|
|
86
|
+
File.write(File.join(export, "case-#{i}-native.#{key}"), native_text)
|
|
87
|
+
File.write(File.join(export, "case-#{i}-fallback.#{key}"), fallback_outputs[key])
|
|
88
|
+
puts "DIVERGENT case #{i} (#{File.basename(base)}, seed #{seed}): #{key} — artifacts in #{export}"
|
|
89
|
+
end
|
|
90
|
+
rescue Expressir::Express::Error::SchemaParseFailure, Parsanol::ParseFailed
|
|
91
|
+
# A mutation neither lane accepts is fine; a lane DIVERGENCE
|
|
92
|
+
# (one parses, the other refuses) is what this must catch.
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "fuzz stats: seed=#{seed} cases=#{cases} compared=#{compared} divergent=#{failures}"
|
|
97
|
+
abort "differential fuzz found #{failures} divergences" if failures.positive?
|
|
98
|
+
end
|
|
99
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expressir
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.66
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -232,11 +232,15 @@ extensions:
|
|
|
232
232
|
extra_rdoc_files: []
|
|
233
233
|
files:
|
|
234
234
|
- ".cargo/config.toml"
|
|
235
|
+
- ".gitattributes"
|
|
235
236
|
- ".github/scripts/test_installed_gem.rb"
|
|
236
237
|
- ".github/workflows/codeql.yml"
|
|
237
238
|
- ".github/workflows/conformance.yml"
|
|
238
239
|
- ".github/workflows/docs.yml"
|
|
240
|
+
- ".github/workflows/engine-compat.yml"
|
|
241
|
+
- ".github/workflows/fuzz.yml"
|
|
239
242
|
- ".github/workflows/gemspec-floor.yml"
|
|
243
|
+
- ".github/workflows/lane-parity.yml"
|
|
240
244
|
- ".github/workflows/links.yml"
|
|
241
245
|
- ".github/workflows/native-gems.yml"
|
|
242
246
|
- ".github/workflows/performance-release.yml"
|
|
@@ -253,6 +257,7 @@ files:
|
|
|
253
257
|
- ".rubocop_todo.yml"
|
|
254
258
|
- CHANGELOG.md
|
|
255
259
|
- Cargo.toml
|
|
260
|
+
- ENGINE-COMPAT.json
|
|
256
261
|
- Gemfile
|
|
257
262
|
- README.adoc
|
|
258
263
|
- Rakefile
|
|
@@ -765,6 +770,7 @@ files:
|
|
|
765
770
|
- lib/expressir/p21/dataset_manifest.rb
|
|
766
771
|
- lib/expressir/p21/diff.rb
|
|
767
772
|
- lib/expressir/p21/grammar.rb
|
|
773
|
+
- lib/expressir/p21/iso-10303-21.artifact.json
|
|
768
774
|
- lib/expressir/p21/json.rb
|
|
769
775
|
- lib/expressir/p21/parser.rb
|
|
770
776
|
- lib/expressir/p21/population.rb
|
|
@@ -789,12 +795,16 @@ files:
|
|
|
789
795
|
- lib/expressir/schema_manifest.rb
|
|
790
796
|
- lib/expressir/schema_manifest_entry.rb
|
|
791
797
|
- lib/expressir/scoreboard.rb
|
|
798
|
+
- lib/expressir/tools/check_engine_compat.rb
|
|
792
799
|
- lib/expressir/version.rb
|
|
793
800
|
- lib/tasks/conformance.rake
|
|
794
801
|
- lib/tasks/instance.rake
|
|
795
802
|
- lib/tasks/reference.rake
|
|
796
803
|
- lib/tasks/scoreboard.rake
|
|
797
804
|
- lib/tasks/verify_remarks.rake
|
|
805
|
+
- rakelib/contract_matrix.rake
|
|
806
|
+
- rakelib/engine_compat.rake
|
|
807
|
+
- rakelib/fuzz.rake
|
|
798
808
|
- rakelib/native.rake
|
|
799
809
|
- rakelib/oracle.rake
|
|
800
810
|
- rakelib/verify_remarks.rake
|