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

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