Module: Lutaml::Xml::Leptris::PlanCompiler
- Defined in:
- lib/lutaml/xml/leptris/plan_compiler.rb
Overview
Phase 5 slice: compile a model's XML mapping into a Leptris::XML::Descriptor plan and materialize whole documents in one native pass (leptris_plan_walk) — no moxml wrapper tree, no per-element Ruby dispatch. Measured on the 200-item probe: walk+hydrate 8.7x faster, 82% fewer allocations than the interpretive path, hydration-equal output.
A model compiles when EVERY rule is plan-shaped; richer rows defer their subtree verbatim and interpret post-walk rather than opting the whole model out:
- map_attribute / map_element / content / raw rows
- Value-scalar types or nested Serializables
- custom methods, polymorphism, unions, and ordered/mixed
children capture :raw and interpret post-walk
- ordered/mixed root mappings compile; the entry points
reconstruct element_order from the node surface
- attributes not derived/union/polymorphic
The fast path is opt-in (Config.xml_plan_fast_path) while the full semantics audit (parent links, consolidation, ordering metadata) completes.
Constant Summary collapse
- PLAN_CACHE =
Isolated holder: suites freeze model classes; a cache on a frozen constant would be immutable (TypeProbeCache precedent).
::Class.new do class << self def cache @cache ||= {} end end end.cache
Class Method Summary collapse
- .compile(model_class, register) ⇒ Object
- .opt_out!(clause) ⇒ Object
-
.stats ⇒ Object
TODO.perf/15: env-gated histogram of why models opt out of the plan path.
Class Method Details
.compile(model_class, register) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/lutaml/xml/leptris/plan_compiler.rb', line 52 def compile(model_class, register) # Version gate, not a capability probe: the plan path needs # leptris >= 1.9.273.0 (the child-handles snapshot). Older # gems take the interpretive path via the nil opt-out. return nil unless Leptris.plan_path_compatible? # A child model's declared lutaml_default_register takes # precedence over the ambient (parent) register — the same # contract the interpretive path applies through # Register.resolve_for_child. Without this, plan compilation # resolves the child's symbol attribute types in the parent # context and raises UnknownTypeError for ids registered only # in the child's own register (#876). register = Lutaml::Model::Register.resolve_for_child( model_class, register ) key = [model_class, register] return PLAN_CACHE[key] if PLAN_CACHE.key?(key) # Cycle guard: a self-referential model (JATS sec-in-sec) must # resolve to nil — the interpretive pipeline owns it. The guard # is THREAD-LOCAL (a shared in-progress set races: a concurrent # same-key compile would cache false permanently — the #828 # lesson); the nil at the cycle point is NOT cached — the # outermost build completes and caches the real verdict. stack = (Thread.current[:plan_compiler_stack] ||= []) return nil if stack.include?(key) stack.push(key) begin PLAN_CACHE[key] = build(model_class, register) ensure stack.pop end end |
.opt_out!(clause) ⇒ Object
35 36 37 38 39 |
# File 'lib/lutaml/xml/leptris/plan_compiler.rb', line 35 def self.opt_out!(clause) @plan_stats ||= Hash.new(0) @plan_stats[clause] += 1 if ENV["PLAN_COMPILE_STATS"] nil end |
.stats ⇒ Object
TODO.perf/15: env-gated histogram of why models opt out of the plan path. Set PLAN_COMPILE_STATS=1 and read PlanCompiler.stats after a parse. Zero cost when disabled.
31 32 33 |
# File 'lib/lutaml/xml/leptris/plan_compiler.rb', line 31 def self.stats @stats ||= Hash.new(0) end |