expressir 2.4.0 → 2.4.2
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/TODO.bugs/01-stale-transformer-autoload.md +39 -0
- data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
- data/TODO.bugs/03-builder-mutable-state.md +43 -0
- data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
- data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
- data/TODO.bugs/06-collection-registry-single-source.md +53 -0
- data/TODO.bugs/07-require-relative-cleanup.md +42 -0
- data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
- data/TODO.bugs/09-parser-split.md +53 -0
- data/TODO.bugs/10-to-s-override.md +42 -0
- data/TODO.bugs/11-parser-class-variables.md +39 -0
- data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
- data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
- data/TODO.bugs/14-model-formatting-leak.md +30 -0
- data/TODO.bugs/15-expression-children-macro.md +27 -0
- data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
- data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
- data/TODO.bugs/18-const-get-private-constants.md +30 -0
- data/TODO.bugs/19-format-methods-public.md +22 -0
- data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
- data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
- data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
- data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
- data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
- data/TODO.bugs/25-debug-puts-in-production.md +21 -0
- data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
- data/TODO.bugs/27-package-build-god-method.md +19 -0
- data/TODO.bugs/28-package-god-class.md +30 -0
- data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
- data/TODO.bugs/30-unicode-map-extraction.md +19 -0
- data/TODO.bugs/README.md +43 -0
- data/TODO.max-perf/01-restore-ci-green.md +29 -0
- data/TODO.max-perf/02-streaming-parse-path.md +31 -0
- data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
- data/TODO.max-perf/04-benchmark-harness.md +28 -0
- data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
- data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
- data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
- data/TODO.max-perf/08-builder-build-perf.md +45 -0
- data/TODO.max-perf/09-grammar-cold-start.md +25 -0
- data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
- data/TODO.max-perf/11-ci-green-closeout.md +25 -0
- data/TODO.max-perf/12-require-boot-profile.md +25 -0
- data/TODO.max-perf/13-key-conversion-specs.md +26 -0
- data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
- data/benchmark/srl_benchmark.rb +76 -17
- data/docs/_tutorials/formatting-schemas.adoc +89 -0
- data/docs/_tutorials/index.adoc +10 -0
- data/docs/lychee.toml +3 -0
- data/expressir.gemspec +4 -2
- data/lib/expressir/cli.rb +3 -0
- data/lib/expressir/commands/changes_import_eengine.rb +0 -6
- data/lib/expressir/commands/changes_validate.rb +0 -2
- data/lib/expressir/commands/clean.rb +1 -1
- data/lib/expressir/commands/coverage.rb +6 -2
- data/lib/expressir/commands/file_violations.rb +70 -0
- data/lib/expressir/commands/format.rb +1 -1
- data/lib/expressir/commands/non_ascii_character.rb +49 -0
- data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
- data/lib/expressir/commands/package.rb +143 -187
- data/lib/expressir/commands/validate_ascii.rb +0 -510
- data/lib/expressir/commands/version.rb +1 -1
- data/lib/expressir/commands.rb +3 -0
- data/lib/expressir/coverage.rb +49 -117
- data/lib/expressir/express/ast_key_converter.rb +114 -0
- data/lib/expressir/express/builder.rb +71 -194
- data/lib/expressir/express/builder_context.rb +22 -0
- data/lib/expressir/express/builders/expression_builder.rb +16 -16
- data/lib/expressir/express/cache.rb +35 -8
- data/lib/expressir/express/error.rb +24 -0
- data/lib/expressir/express/formatter.rb +33 -18
- data/lib/expressir/express/formatters/declarations_formatter.rb +24 -2
- data/lib/expressir/express/formatters/remark_formatter.rb +75 -2
- data/lib/expressir/express/formatters/statements_formatter.rb +20 -3
- data/lib/expressir/express/grammar/parser.rb +705 -0
- data/lib/expressir/express/grammar.rb +11 -0
- data/lib/expressir/express/node_position_index.rb +215 -0
- data/lib/expressir/express/parallel_files.rb +229 -0
- data/lib/expressir/express/parser.rb +138 -950
- data/lib/expressir/express/pretty_formatter.rb +23 -5
- data/lib/expressir/express/remark_attacher.rb +497 -581
- data/lib/expressir/express/remark_scanner.rb +84 -19
- data/lib/expressir/express/schema_block_scanner.rb +137 -0
- data/lib/expressir/express/schema_source_formatter.rb +15 -0
- data/lib/expressir/express/scope_resolver.rb +223 -0
- data/lib/expressir/express/source_formatter.rb +15 -0
- data/lib/expressir/express/streaming_builder.rb +147 -176
- data/lib/expressir/express.rb +10 -6
- data/lib/expressir/model/concerns.rb +3 -0
- data/lib/expressir/model/data_types/generic_entity.rb +6 -6
- data/lib/expressir/model/declarations/entity.rb +5 -0
- data/lib/expressir/model/declarations/function.rb +5 -0
- data/lib/expressir/model/declarations/procedure.rb +5 -0
- data/lib/expressir/model/declarations/rule.rb +6 -0
- data/lib/expressir/model/declarations/schema.rb +21 -8
- data/lib/expressir/model/declarations/type.rb +3 -0
- data/lib/expressir/model/exp_file.rb +2 -0
- data/lib/expressir/model/expressions/aggregate_initializer.rb +1 -0
- data/lib/expressir/model/expressions/aggregate_initializer_item.rb +1 -0
- data/lib/expressir/model/expressions/binary_expression.rb +1 -0
- data/lib/expressir/model/expressions/entity_constructor.rb +1 -0
- data/lib/expressir/model/expressions/function_call.rb +1 -0
- data/lib/expressir/model/expressions/interval.rb +1 -0
- data/lib/expressir/model/expressions/query_expression.rb +2 -0
- data/lib/expressir/model/expressions/unary_expression.rb +1 -0
- data/lib/expressir/model/model_element.rb +133 -14
- data/lib/expressir/model/remark_format.rb +17 -0
- data/lib/expressir/model/remark_info.rb +31 -3
- data/lib/expressir/model/remark_placement.rb +29 -0
- data/lib/expressir/model/repository.rb +18 -5
- data/lib/expressir/model/statements/alias.rb +2 -0
- data/lib/expressir/model/statements/assignment.rb +4 -0
- data/lib/expressir/model/statements/case.rb +21 -0
- data/lib/expressir/model/statements/case_action.rb +1 -0
- data/lib/expressir/model/statements/compound.rb +4 -0
- data/lib/expressir/model/statements/escape.rb +3 -0
- data/lib/expressir/model/statements/if.rb +5 -0
- data/lib/expressir/model/statements/null.rb +3 -0
- data/lib/expressir/model/statements/procedure_call.rb +3 -0
- data/lib/expressir/model/statements/repeat.rb +3 -0
- data/lib/expressir/model/statements/return.rb +3 -0
- data/lib/expressir/model/statements/skip.rb +3 -0
- data/lib/expressir/model.rb +12 -3
- data/lib/expressir/package/builder.rb +2 -2
- data/lib/expressir/version.rb +6 -1
- data/lib/expressir.rb +56 -3
- metadata +81 -7
- data/lib/expressir/express/transformer/remark_handling.rb +0 -194
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# TODO.max-perf/08 — Instrument and optimize Builder.build internals
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
TODO.max-perf/06 established the split (CPU-time, parsanol 1.3.30, 4 mid-size
|
|
6
|
+
SRL schemas): native parse 50%, `Builder.build_with_remarks` 48%, references
|
|
7
|
+
2%; within the builder, model build is 1.12s vs remark attachment 0.06s.
|
|
8
|
+
`fast_convert_keys`/`cached_snake_case` are already single-pass and
|
|
9
|
+
allocation-avoiding. The unmeasured stages inside `Builder.build`:
|
|
10
|
+
|
|
11
|
+
1. `builder.call(snake_data)` — registered handler → lutaml-model
|
|
12
|
+
instantiation (thousands of attribute setters per schema)
|
|
13
|
+
2. `attach_source_info` → `extract_source_info` → `find_slice` — a
|
|
14
|
+
depth-capped recursive scan of each node's data subtree; nested data can
|
|
15
|
+
be re-scanned by ancestors, which is quadratic-ish on deep nesting
|
|
16
|
+
3. The `when Array` recursion
|
|
17
|
+
|
|
18
|
+
## Work
|
|
19
|
+
|
|
20
|
+
Findings: fast_convert_keys ran 172k–266k times per schema (~60 calls per
|
|
21
|
+
model node): build() descends into subtrees the parent's deep conversion
|
|
22
|
+
already scanned, so every subtree was re-scanned once per ancestor level.
|
|
23
|
+
attach_source_info is negligible (0.02–0.07s instrumented upper bound);
|
|
24
|
+
the remaining unoptimized share is lutaml-model instantiation inside
|
|
25
|
+
builder.call (upstream territory).
|
|
26
|
+
|
|
27
|
+
- [x] Instrumented (counters + timers) over topology / presentation /
|
|
28
|
+
measure / action schemas; split recorded above
|
|
29
|
+
- [x] Implemented the evidenced win: fast_convert_keys marks scanned or
|
|
30
|
+
converted Hash/Array containers with an invisible ivar and skips
|
|
31
|
+
them on re-visits
|
|
32
|
+
- [x] Before/after (CPU-time, warm): topology 0.42→0.25s (−40%),
|
|
33
|
+
presentation 0.27→0.22s (−19%), measure 0.43→0.30s (−30%),
|
|
34
|
+
action 0.11→0.06s (−48%)
|
|
35
|
+
- [x] Full suite green (1551 examples) including golden-file parser specs;
|
|
36
|
+
to_hash equality verified against pre-change output on all four
|
|
37
|
+
schemas
|
|
38
|
+
|
|
39
|
+
- [x] Handler-level audit (TODO 14 overlap): `expression` 25.8% / `syntax` 15.4% of builder self-CPU — structural dispatch cost (per-level key cascades + build_node wrapper-hash allocations) spread across ~20k nodes; no single fixable waste. Architectural cure = skip the intermediate Hash AST via parsanol's stable parse_with_builder (blocked upstream, TODO 02 / parsanol#59).
|
|
40
|
+
|
|
41
|
+
## Acceptance
|
|
42
|
+
|
|
43
|
+
Numbers table for the three stages; either a measured improvement merged or
|
|
44
|
+
the cost definitively attributed to lutaml-model instantiation (which moves
|
|
45
|
+
the next step upstream to lutaml-model).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TODO.max-perf/09 — Grammar cold-start cost and disk cache
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Every process boot builds the parsanol grammar and serializes ~2,300 atoms to
|
|
6
|
+
JSON (`Grammar::Parser.cached_parser` / `cached_grammar_json`) before the
|
|
7
|
+
first parse. Short-lived processes (CLI invocations, tests) pay this on every
|
|
8
|
+
run. parsanol-ruby#59 tracks upstream grammar-to-code compilation; an
|
|
9
|
+
expressir-side JSON disk cache would help regardless.
|
|
10
|
+
|
|
11
|
+
## Work
|
|
12
|
+
|
|
13
|
+
Measured (fresh process, 3 runs): boot (require expressir) 161–181ms,
|
|
14
|
+
grammar build ~28ms, JSON serialization ~30ms (83KB), first parse ~25ms.
|
|
15
|
+
Grammar-related cold cost is ~58ms — below the 100ms bar; a disk cache
|
|
16
|
+
would save ~50ms per process at the cost of temp-file staleness machinery.
|
|
17
|
+
|
|
18
|
+
- [x] Cold-start measured and recorded (above)
|
|
19
|
+
- [x] Documented as negligible — no cache implemented (the larger cold
|
|
20
|
+
cost is the 161ms require boot, a separate lazy-loading concern)
|
|
21
|
+
|
|
22
|
+
## Acceptance
|
|
23
|
+
|
|
24
|
+
Cold-start number recorded; cache implemented with specs if the number
|
|
25
|
+
justifies it, otherwise documented as negligible.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# TODO.max-perf/10 — Parser facade API hygiene
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The `Parser` facade accumulated internal helpers with public visibility
|
|
6
|
+
during the parallel work: `parse_files_sequentially` and `build_repository`
|
|
7
|
+
are implementation details of `from_files`. `Commands::Coverage` defines a
|
|
8
|
+
`DEFAULT_MAX_PROCESSES` constant that duplicates
|
|
9
|
+
`ParallelFiles::DEFAULT_MAX_PROCESSES` (the CLI option default already
|
|
10
|
+
references the latter directly) — a DRY violation with a dead constant.
|
|
11
|
+
|
|
12
|
+
## Work
|
|
13
|
+
|
|
14
|
+
- [x] `parse_files_sequentially` and `build_repository` are now
|
|
15
|
+
`private_class_method` (no external callers existed)
|
|
16
|
+
- [x] Dead `Coverage::DEFAULT_MAX_PROCESSES` removed; the CLI option
|
|
17
|
+
default references `ParallelFiles::DEFAULT_MAX_PROCESSES` directly
|
|
18
|
+
- [x] Rubocop clean; full suite green (1551 examples, 40s)
|
|
19
|
+
|
|
20
|
+
## Acceptance
|
|
21
|
+
|
|
22
|
+
Facade exposes only the public contract; single source for the worker-cap
|
|
23
|
+
constant.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TODO.max-perf/11 — CI green close-out
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
TODO 01's final checkbox. Main carries the Windows fork guard (#353), the
|
|
6
|
+
engine-selection guard pinning :standard adapters on `Gem.win_platform?`
|
|
7
|
+
(#354), and the builder memoization (#355). yeptris mingw remains broken
|
|
8
|
+
upstream (leptris/yeptris#318); the guard must keep it unloaded.
|
|
9
|
+
|
|
10
|
+
## Work
|
|
11
|
+
|
|
12
|
+
- [x] Confirmed locally via full `bundle exec rake` (CI-equivalent):
|
|
13
|
+
1558 examples, 0 failures, rubocop clean across 325 files
|
|
14
|
+
- [x] If red: diagnose the failing job, fix in expressir if the defect is
|
|
15
|
+
here, escalate upstream otherwise
|
|
16
|
+
|
|
17
|
+
- [x] Root causes of the red run on 35c1f62 found and fixed: (1) rubocop offenses in the committed benchmark harness + a directive typo in another session's remark perf spec — fixed; (2) sequential-path nil-pad parity bug in from_files — fixed with regression spec; (3) strict-mode spec unguarded on fork-less platforms — guarded; (4) yeptris still loading on Windows: root cause is lutaml-model#798 (configured adapter fell through to detection, loading yeptris before any expressir pin could apply) — fixed upstream + expressir spec_helper now pins json too, with a temporary require-spy proving zero yeptris loads in the suite
|
|
18
|
+
|
|
19
|
+
- [x] Landed on main via #357 (the #356 merge had gone to its stale base
|
|
20
|
+
branch `feat/max-perf-todos`; cherry-picked 17aa264 onto main and
|
|
21
|
+
re-landed as 0576a08)
|
|
22
|
+
|
|
23
|
+
## Acceptance
|
|
24
|
+
|
|
25
|
+
rake green on main; TODO 01's checkbox ticked.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TODO.max-perf/12 — Require-boot profile and deferrable loads
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
TODO 09 measured the cold start: grammar work is only ~58ms; the dominant
|
|
6
|
+
cold cost is `require "expressir"` itself at 161–181ms. Short-lived
|
|
7
|
+
processes (CLI, CI) pay it per invocation. expressir.rb eagerly requires
|
|
8
|
+
`lutaml/model` and `liquid` (for Lutaml::Model::Liquefiable); the gemspec
|
|
9
|
+
also pulls csv, benchmark-ips, ruby-progressbar, nokogiri-adjacent moxml,
|
|
10
|
+
thor — some needed only by CLI commands, not by library consumers.
|
|
11
|
+
|
|
12
|
+
## Work
|
|
13
|
+
|
|
14
|
+
- [x] Profiled: per-gem load times via a require-timing probe in a fresh
|
|
15
|
+
process (results below)
|
|
16
|
+
- [x] Evaluated deferrals: nothing safely deferrable (results below)
|
|
17
|
+
- [x] Full suite green (no deferred loads introduced)
|
|
18
|
+
|
|
19
|
+
- [x] Per-gem boot profile (fresh process, 2 runs): liquid 172-207ms, lutaml/model 327-559ms (includes moxml chain), thor 19-40ms, expressir own lib ~1ms (autoloads already optimal)
|
|
20
|
+
- [x] Documented as not safely deferrable: liquid backs Lutaml::Model::Liquefiable used by expressir models in metanorma rendering; lutaml-model's boot is upstream (its lazy-loading is tracked there). expressir's own load adds ~1ms — nothing to defer.
|
|
21
|
+
|
|
22
|
+
## Acceptance
|
|
23
|
+
|
|
24
|
+
Per-gem boot profile recorded; measured reduction if safely deferrable,
|
|
25
|
+
otherwise documented why not.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# TODO.max-perf/13 — Unit specs for the key-conversion memoization
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
PR #355 optimized `Builder.fast_convert_keys` with an invisible-ivar marker
|
|
6
|
+
so re-visited containers are skipped. The optimization is currently covered
|
|
7
|
+
only indirectly by golden-file parser specs. The memoization's own contract
|
|
8
|
+
needs pinning so future edits cannot silently break it.
|
|
9
|
+
|
|
10
|
+
## Work
|
|
11
|
+
|
|
12
|
+
- [x] Unit specs for the converter (renamed `AstKeyConverter.convert`):
|
|
13
|
+
- converts CamelCase keys at every depth; untouched data keeps object
|
|
14
|
+
identity
|
|
15
|
+
- idempotence: converted results convert to themselves
|
|
16
|
+
- marked containers are skipped on re-visits (marker on both paths)
|
|
17
|
+
- frozen input hashes do not raise
|
|
18
|
+
- [x] Marker invisibility: marked hashes remain == to unmarked copies and
|
|
19
|
+
survive Marshal round-trip without affecting content
|
|
20
|
+
|
|
21
|
+
- [x] All contract specs written against the extracted public Expressir::Express::AstKeyConverter (key conversion was promoted from a private Builder helper to its own class — MECE: converting AST keys is not building models)
|
|
22
|
+
- [x] Depth conversion, identity preservation, idempotence, marker set on both paths, frozen-hash safety, marker invisibility (== and Marshal), snake_case behavior — spec/expressir/express/ast_key_converter_spec.rb
|
|
23
|
+
|
|
24
|
+
## Acceptance
|
|
25
|
+
|
|
26
|
+
Dedicated spec file green; the optimization's behavior is pinned.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# TODO.max-perf/14 — Per-handler builder.call audit
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
TODO 08 attributed the remaining (post-memoization) builder cost to model
|
|
6
|
+
instantiation inside `builder.call` — but that was inferred, not measured
|
|
7
|
+
per handler. ~200 registered handlers exist (builders/*.rb); expressir-side
|
|
8
|
+
waste in the hottest handlers (e.g., redundant intermediate arrays, repeated
|
|
9
|
+
`build()` calls, hash re-shaping) would be ours to fix.
|
|
10
|
+
|
|
11
|
+
## Work
|
|
12
|
+
|
|
13
|
+
- [x] Instrumented per handler via a self-time build wrapper (child CPU
|
|
14
|
+
subtracted) over representative schemas; top handlers recorded below
|
|
15
|
+
- [x] Inspected the top handlers' source (expression_builder.rb et al.)
|
|
16
|
+
- [x] No evident expressir-side waste to fix (see verdict below); the
|
|
17
|
+
optimization taken instead was the fast_convert_keys memoization
|
|
18
|
+
(merged in #355) after TODO 08's stage instrumentation
|
|
19
|
+
- [x] Full suite green
|
|
20
|
+
|
|
21
|
+
- [x] Handler-level instrumentation via a self-time build wrapper (child-time subtracted): 20,270 nodes, 1.58s self CPU over 3 schemas
|
|
22
|
+
- [x] Top handlers: expression 25.8% (3,184 calls), syntax 15.4%, entity_ref 7.7%, simple_expression 6.0%, entity_decl 5.1% — long tail of leaf handlers below 4% each
|
|
23
|
+
- [x] Verdict: no single expressir-side waste; `expression`'s share is structural dispatch (if/elsif key cascades + per-call build_node wrapper-hash allocations). The architectural fix is bypassing the intermediate Hash AST entirely — blocked on parsanol's stable parse_with_builder (TODO 02, parsanol#59). Recorded; not refactored blindly.
|
|
24
|
+
|
|
25
|
+
## Acceptance
|
|
26
|
+
|
|
27
|
+
Handler-level numbers table; either a merged improvement or the cost
|
|
28
|
+
definitively attributed to lutaml-model constructors (upstream follow-up).
|
data/benchmark/srl_benchmark.rb
CHANGED
|
@@ -17,7 +17,8 @@ require "parsanol/native"
|
|
|
17
17
|
require "expressir"
|
|
18
18
|
|
|
19
19
|
# Configuration
|
|
20
|
-
SRL_PATH = "
|
|
20
|
+
SRL_PATH = ENV["SRL_PATH"] ||
|
|
21
|
+
"/Users/mulgogi/src/mn/iso-10303/schemas/resources"
|
|
21
22
|
ITERATIONS = (ENV["ITERATIONS"] || 1).to_i
|
|
22
23
|
TIMEOUT_SECONDS = (ENV["TIMEOUT"] || 30).to_i # Timeout per file
|
|
23
24
|
|
|
@@ -104,6 +105,69 @@ def find_exp_files
|
|
|
104
105
|
end
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
# Parse one file in a forked child with a hard wall-clock kill.
|
|
109
|
+
# Ruby's Timeout cannot interrupt the native parser (the GVL is held
|
|
110
|
+
# for the whole FFI call), so process isolation is the only reliable
|
|
111
|
+
# guard against pathological backtracking hangs.
|
|
112
|
+
def parse_file_isolated(file, use_native:, timeout: TIMEOUT_SECONDS)
|
|
113
|
+
rd, wr = IO.pipe
|
|
114
|
+
pid = fork do
|
|
115
|
+
rd.close
|
|
116
|
+
t = Time.now
|
|
117
|
+
result = { status: "ok", elapsed: 0.0, error: nil }
|
|
118
|
+
begin
|
|
119
|
+
if use_native
|
|
120
|
+
content = File.read(file)
|
|
121
|
+
Expressir::Express::Parser.from_exp(content, skip_references: true,
|
|
122
|
+
use_native: true)
|
|
123
|
+
else
|
|
124
|
+
Expressir::Express::Parser.from_file(file, skip_references: true,
|
|
125
|
+
use_native: false)
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
result[:status] = "err"
|
|
129
|
+
result[:error] = "#{e.class}: #{e.message[0..60]}"
|
|
130
|
+
end
|
|
131
|
+
result[:elapsed] = Time.now - t
|
|
132
|
+
data = Marshal.dump(result)
|
|
133
|
+
wr.write([data.bytesize].pack("N"))
|
|
134
|
+
wr.write(data)
|
|
135
|
+
wr.close
|
|
136
|
+
exit!(0)
|
|
137
|
+
end
|
|
138
|
+
wr.close
|
|
139
|
+
|
|
140
|
+
deadline = Time.now + timeout
|
|
141
|
+
timed_out = false
|
|
142
|
+
loop do
|
|
143
|
+
done = Process.waitpid(pid, Process::WNOHANG)
|
|
144
|
+
break if done
|
|
145
|
+
|
|
146
|
+
if Time.now > deadline
|
|
147
|
+
Process.kill("TERM", pid)
|
|
148
|
+
sleep 1
|
|
149
|
+
begin
|
|
150
|
+
Process.kill("KILL", pid) if Process.waitpid(pid, Process::WNOHANG).nil?
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
timed_out = true
|
|
155
|
+
break
|
|
156
|
+
end
|
|
157
|
+
sleep 0.1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if timed_out
|
|
161
|
+
rd.close
|
|
162
|
+
{ status: "timeout", elapsed: timeout.to_f, error: "Timeout after #{timeout}s" }
|
|
163
|
+
else
|
|
164
|
+
header = rd.read(4)
|
|
165
|
+
payload = header && Marshal.load(rd.read(header.unpack1("N"))) # rubocop:disable Security/MarshalLoad
|
|
166
|
+
rd.close
|
|
167
|
+
payload
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
107
171
|
def count_lines(files)
|
|
108
172
|
files.sum { |f| File.read(f).lines.count }
|
|
109
173
|
end
|
|
@@ -176,28 +240,22 @@ class ParserBenchmark
|
|
|
176
240
|
file_start = Time.now
|
|
177
241
|
schema_lines = File.read(file).lines.count
|
|
178
242
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Expressir::Express::Parser.from_exp(content, skip_references: true,
|
|
185
|
-
use_native: true)
|
|
186
|
-
else
|
|
187
|
-
Expressir::Express::Parser.from_file(file, skip_references: true)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
243
|
+
result = parse_file_isolated(file, use_native: @use_native)
|
|
244
|
+
result[:elapsed]
|
|
245
|
+
|
|
246
|
+
case result[:status]
|
|
247
|
+
when "ok"
|
|
190
248
|
iteration_results[:success] += 1
|
|
191
249
|
status = "#{BRIGHT_GREEN}✓#{RESET}"
|
|
192
|
-
|
|
250
|
+
when "timeout"
|
|
193
251
|
iteration_results[:failed] += 1
|
|
194
252
|
iteration_results[:errors] << { file: File.basename(file),
|
|
195
|
-
error:
|
|
253
|
+
error: result[:error] }
|
|
196
254
|
status = "#{BRIGHT_YELLOW}⏱#{RESET}"
|
|
197
|
-
|
|
255
|
+
else
|
|
198
256
|
iteration_results[:failed] += 1
|
|
199
257
|
iteration_results[:errors] << { file: File.basename(file),
|
|
200
|
-
error:
|
|
258
|
+
error: result[:error] }
|
|
201
259
|
status = "#{BRIGHT_RED}✗#{RESET}"
|
|
202
260
|
end
|
|
203
261
|
|
|
@@ -346,7 +404,8 @@ print_warmup_start
|
|
|
346
404
|
warmup_file = files.first
|
|
347
405
|
|
|
348
406
|
begin
|
|
349
|
-
Expressir::Express::Parser.from_file(warmup_file, skip_references: true
|
|
407
|
+
Expressir::Express::Parser.from_file(warmup_file, skip_references: true,
|
|
408
|
+
use_native: false)
|
|
350
409
|
rescue StandardError => e
|
|
351
410
|
puts "#{BRIGHT_YELLOW}⚠️ Ruby warmup warning: #{e.message[0..40]}#{RESET}"
|
|
352
411
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Formatting Schemas
|
|
3
|
+
nav_order: 7
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
== Formatting Schemas
|
|
7
|
+
|
|
8
|
+
Expressir renders parsed EXPRESS schemas back to source text via the `Formatter`. This is what powers the `expressir format` CLI command, the `Schema#full_source` method, and the Liquid drop exposed to documentation templates.
|
|
9
|
+
|
|
10
|
+
=== The default formatter
|
|
11
|
+
|
|
12
|
+
The default `Expressir::Express::Formatter` produces a normalized, consistently-indented version of the schema — comments preserved, original line breaks normalized:
|
|
13
|
+
|
|
14
|
+
[source,ruby]
|
|
15
|
+
----
|
|
16
|
+
require "expressir"
|
|
17
|
+
|
|
18
|
+
exp_file = Expressir::Express::Parser.from_file("my_schema.exp")
|
|
19
|
+
schema = exp_file.schemas.first
|
|
20
|
+
|
|
21
|
+
puts Expressir::Express::Formatter.format(schema)
|
|
22
|
+
----
|
|
23
|
+
|
|
24
|
+
To format every schema in an ExpFile (or Repository), pass the container:
|
|
25
|
+
|
|
26
|
+
[source,ruby]
|
|
27
|
+
----
|
|
28
|
+
puts Expressir::Express::Formatter.format(exp_file)
|
|
29
|
+
----
|
|
30
|
+
|
|
31
|
+
=== Stripping remarks (`no_remarks:`)
|
|
32
|
+
|
|
33
|
+
Pass `no_remarks: true` to produce a listing without tail (`--`) or embedded (`(* ... *)`) remarks. Useful for diffs, license headers, and "code-only" outputs.
|
|
34
|
+
|
|
35
|
+
[source,ruby]
|
|
36
|
+
----
|
|
37
|
+
formatter = Class.new(Expressir::Express::Formatter) do
|
|
38
|
+
def initialize
|
|
39
|
+
super(no_remarks: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts formatter.format(schema)
|
|
44
|
+
----
|
|
45
|
+
|
|
46
|
+
=== CLI: `expressir format`
|
|
47
|
+
|
|
48
|
+
The CLI wraps the same formatter:
|
|
49
|
+
|
|
50
|
+
[source,sh]
|
|
51
|
+
----
|
|
52
|
+
expressir format my_schema.exp # prints formatted output
|
|
53
|
+
expressir format my_schema.exp -o out.exp # writes to a file
|
|
54
|
+
expressir format --no-remarks my_schema.exp
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
=== Schema head and hyperlink formatters
|
|
58
|
+
|
|
59
|
+
Two optional mixins layer on top of the base formatter:
|
|
60
|
+
|
|
61
|
+
* `Expressir::Express::SchemaHeadFormatter` — emits a file-level header banner before each schema (schema name, version, source file).
|
|
62
|
+
* `Expressir::Express::HyperlinkFormatter` — inserts cross-reference hyperlinks (`<<express:...>>` anchors) into the formatted text for use by the Metanorma documentation pipeline.
|
|
63
|
+
|
|
64
|
+
Combine them by defining a subclass that includes both:
|
|
65
|
+
|
|
66
|
+
[source,ruby]
|
|
67
|
+
----
|
|
68
|
+
formatter = Class.new(Expressir::Express::Formatter) do
|
|
69
|
+
include Expressir::Express::SchemaHeadFormatter
|
|
70
|
+
include Expressir::Express::HyperlinkFormatter
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts formatter.format(exp_file)
|
|
74
|
+
----
|
|
75
|
+
|
|
76
|
+
=== `Schema#full_source`
|
|
77
|
+
|
|
78
|
+
For convenience, `Schema#full_source` returns the default-formatted text (memoized). This is what templates should use when they want the canonical rendered schema body:
|
|
79
|
+
|
|
80
|
+
[source,ruby]
|
|
81
|
+
----
|
|
82
|
+
schema = exp_file.schemas.first
|
|
83
|
+
puts schema.full_source
|
|
84
|
+
----
|
|
85
|
+
|
|
86
|
+
=== See also
|
|
87
|
+
|
|
88
|
+
* link:querying-schemas.html[Querying Schemas] — find entities, types, and remarks programmatically.
|
|
89
|
+
* link:liquid-templates.html[Liquid Templates] — use the formatted output inside documentation templates.
|
data/docs/_tutorials/index.adoc
CHANGED
|
@@ -58,6 +58,11 @@ Duration: 45-60 minutes
|
|
|
58
58
|
+
|
|
59
59
|
Analyze and improve schema documentation quality with coverage tools and metrics.
|
|
60
60
|
|
|
61
|
+
**link:formatting-schemas.html[Formatting Schemas]**::
|
|
62
|
+
Duration: 15-20 minutes
|
|
63
|
+
+
|
|
64
|
+
Render parsed schemas back to EXPRESS source text — default formatting, remark stripping, schema-head and hyperlink mixins.
|
|
65
|
+
|
|
61
66
|
=== Tutorial Features
|
|
62
67
|
|
|
63
68
|
Each tutorial includes:
|
|
@@ -121,6 +126,11 @@ Where to go after completing the tutorial
|
|
|
121
126
|
| Advanced
|
|
122
127
|
| Coverage analysis
|
|
123
128
|
| 45-60 min
|
|
129
|
+
|
|
130
|
+
| link:formatting-schemas.html[Formatting Schemas]
|
|
131
|
+
| Beginner
|
|
132
|
+
| Rendering schemas to source text
|
|
133
|
+
| 15-20 min
|
|
124
134
|
|===
|
|
125
135
|
|
|
126
136
|
=== Additional Resources
|
data/docs/lychee.toml
CHANGED
|
@@ -59,6 +59,9 @@ exclude = [
|
|
|
59
59
|
"https://www\\.nist\\.gov/.*",
|
|
60
60
|
"https://www\\.steptools\\.com/.*",
|
|
61
61
|
|
|
62
|
+
# iso.org returns 403 to bots — the ISO store page is reachable in a browser
|
|
63
|
+
"https://www\\.iso\\.org/.*",
|
|
64
|
+
|
|
62
65
|
# GitHub URLs that require authentication
|
|
63
66
|
"https://github\\.com/.*/(issues|pull)/[0-9]+/files.*",
|
|
64
67
|
|
data/expressir.gemspec
CHANGED
|
@@ -4,7 +4,7 @@ require "expressir/version"
|
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "expressir"
|
|
7
|
-
spec.version = Expressir::VERSION
|
|
7
|
+
spec.version = Expressir::Version::VERSION
|
|
8
8
|
spec.authors = ["Ribose Inc."]
|
|
9
9
|
spec.email = ["open.source@ribose.com"]
|
|
10
10
|
|
|
@@ -42,7 +42,9 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
spec.add_dependency "paint"
|
|
43
43
|
spec.add_dependency "parsanol", "~> 1.3.9", ">= 1.3.9"
|
|
44
44
|
spec.add_dependency "ruby-progressbar", "~> 1.11"
|
|
45
|
-
spec.add_dependency "rubyzip"
|
|
45
|
+
spec.add_dependency "rubyzip"
|
|
46
46
|
spec.add_dependency "table_tennis"
|
|
47
47
|
spec.add_dependency "thor", "~> 1.0"
|
|
48
|
+
# Native YAML/JSON engine (lutaml-model opt-in by bundle contents)
|
|
49
|
+
spec.add_dependency "yeptris", "~> 0.6.1"
|
|
48
50
|
end
|
data/lib/expressir/cli.rb
CHANGED
|
@@ -67,6 +67,9 @@ module Expressir
|
|
|
67
67
|
desc: "Output file path for JSON/YAML formats (defaults to coverage_report.json/yaml)"
|
|
68
68
|
method_option :ignore_files, type: :string,
|
|
69
69
|
desc: "Path to YAML file containing array of files to ignore from overall coverage calculation"
|
|
70
|
+
method_option :max_processes, type: :numeric,
|
|
71
|
+
default: Expressir::Express::ParallelFiles::DEFAULT_MAX_PROCESSES,
|
|
72
|
+
desc: "Parallel parse workers (1 = sequential; falls back to sequential where fork is unavailable)"
|
|
70
73
|
def coverage(*paths)
|
|
71
74
|
Expressir::Commands::Coverage.new(options).run(paths)
|
|
72
75
|
end
|
|
@@ -12,8 +12,6 @@ module Expressir
|
|
|
12
12
|
# @param options [Hash] Additional options
|
|
13
13
|
# @return [Expressir::Changes::SchemaChange]
|
|
14
14
|
def self.from_xml(xml_content, schema_name, version, **)
|
|
15
|
-
require "expressir/changes"
|
|
16
|
-
|
|
17
15
|
# Parse into CompareReport using Lutaml::Model
|
|
18
16
|
compare_report = Expressir::Eengine::CompareReport.from_xml(xml_content)
|
|
19
17
|
|
|
@@ -24,8 +22,6 @@ module Expressir
|
|
|
24
22
|
|
|
25
23
|
# File-based workflow (backward compatible)
|
|
26
24
|
def self.call(input_file, output_file, schema_name, version, **options)
|
|
27
|
-
require "expressir/changes"
|
|
28
|
-
|
|
29
25
|
xml_content = File.read(input_file)
|
|
30
26
|
|
|
31
27
|
# Load existing schema if output file exists
|
|
@@ -51,8 +47,6 @@ module Expressir
|
|
|
51
47
|
|
|
52
48
|
def convert_to_schema_change(compare_report, schema_name, version,
|
|
53
49
|
**options)
|
|
54
|
-
require "expressir/changes"
|
|
55
|
-
|
|
56
50
|
# Extract changes from CompareReport
|
|
57
51
|
changes = {
|
|
58
52
|
additions: extract_items(compare_report.additions,
|
|
@@ -5,7 +5,7 @@ module Expressir
|
|
|
5
5
|
repository = Expressir::Express::Parser.from_file(path)
|
|
6
6
|
formatted_schemas = repository.schemas.map do |schema|
|
|
7
7
|
# Format schema without remarks
|
|
8
|
-
schema.
|
|
8
|
+
schema.format(no_remarks: true)
|
|
9
9
|
end.join("\n\n")
|
|
10
10
|
|
|
11
11
|
if options[:output]
|
|
@@ -73,7 +73,9 @@ module Expressir
|
|
|
73
73
|
|
|
74
74
|
# Parse all files and create a repository with progress tracking
|
|
75
75
|
begin
|
|
76
|
-
repository = Expressir::Express::Parser.from_files(
|
|
76
|
+
repository = Expressir::Express::Parser.from_files(
|
|
77
|
+
exp_files, max_processes: options[:max_processes]
|
|
78
|
+
) do |filename, _schemas, error|
|
|
77
79
|
if error
|
|
78
80
|
say " Error processing #{File.basename(filename)}: #{error.message}"
|
|
79
81
|
end
|
|
@@ -125,7 +127,9 @@ module Expressir
|
|
|
125
127
|
)
|
|
126
128
|
|
|
127
129
|
# Process files with progress tracking
|
|
128
|
-
repository = Expressir::Express::Parser.from_files(
|
|
130
|
+
repository = Expressir::Express::Parser.from_files(
|
|
131
|
+
schema_files, max_processes: options[:max_processes]
|
|
132
|
+
) do |filename, _schemas, error|
|
|
129
133
|
if error
|
|
130
134
|
say " Error processing #{File.basename(filename)}: #{error.message}"
|
|
131
135
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Commands
|
|
5
|
+
class FileViolations
|
|
6
|
+
attr_reader :path, :filename, :directory, :violations
|
|
7
|
+
|
|
8
|
+
def initialize(file_path)
|
|
9
|
+
@path = file_path
|
|
10
|
+
@filename = File.basename(file_path)
|
|
11
|
+
@directory = File.dirname(file_path)
|
|
12
|
+
@characters = {} # Map of characters to NonAsciiCharacter objects
|
|
13
|
+
@violations = [] # List of violations (line, column, etc.)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_violation(line_number, column, match, char_details, line)
|
|
17
|
+
violation = {
|
|
18
|
+
line_number: line_number,
|
|
19
|
+
column: column,
|
|
20
|
+
match: match,
|
|
21
|
+
char_details: char_details,
|
|
22
|
+
line: line,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@violations << violation
|
|
26
|
+
|
|
27
|
+
# Register each character
|
|
28
|
+
char_details.each do |detail|
|
|
29
|
+
char = detail[:char]
|
|
30
|
+
unless @characters[char]
|
|
31
|
+
@characters[char] = NonAsciiCharacter.new(
|
|
32
|
+
char,
|
|
33
|
+
detail[:hex],
|
|
34
|
+
detail[:utf8],
|
|
35
|
+
detail[:is_math],
|
|
36
|
+
detail[:replacement],
|
|
37
|
+
detail[:replacement_type],
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@characters[char].add_occurrence(line_number, column, line)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def violation_count
|
|
46
|
+
@violations.size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unique_characters
|
|
50
|
+
@characters.values
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def display_path
|
|
54
|
+
"#{File.basename(@directory)}/#{@filename}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def full_path
|
|
58
|
+
File.expand_path(@path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{
|
|
63
|
+
file: display_path,
|
|
64
|
+
count: violation_count,
|
|
65
|
+
non_ascii_characters: unique_characters.map(&:to_h),
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|