Module: Lutaml::Xml::PlanCompiler

Defined in:
lib/lutaml/xml/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



51
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
# File 'lib/lutaml/xml/plan_compiler.rb', line 51

def compile(model_class, register)
  # 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



34
35
36
37
38
# File 'lib/lutaml/xml/plan_compiler.rb', line 34

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.



30
31
32
# File 'lib/lutaml/xml/plan_compiler.rb', line 30

def self.stats
  @stats ||= Hash.new(0)
end