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



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/lutaml/xml/plan_compiler.rb', line 38

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