PlanScript
PlanScript: a DSL where BAREWORDS build the graph. Inside the block, `fetch` isn't a variable or a method you defined - it's method_missing, catching the name and declaring a step; `rank after: fetch` catches two. That's the strange half. The principled half is that the DSL is a real compiler, and real compilers should be BIDIRECTIONAL: the graph decompiles back to canonical PlanScript source, the source re-parses to the same graph, and emit(parse(emit(g))) == emit(g) - a …
Data & Pipelines
Round 19
Yehuda Katz
exit 0
bundle exec ruby examples/plan_script.rb
a real captured run
PLANSCRIPT (barewords in, graph out, source back - a compiler that round-trips)
the graph, decompiled to canonical PlanScript:
plan do
fetch_feed
dedupe after: :fetch_feed
rank after: :dedupe
summarize after: :rank
render after: [:rank, :summarize]
end
parse(emit(graph)) == graph: true
emit(parse(emit(g))) == emit(g): true (the decompiler reached its fixpoint)
compiled and executed the REPARSED graph: completed, 5 steps,
dependency order respected: true
two tricks, one principle. the trick you see: method_missing
turning barewords into declarations, so the script reads like a
napkin sketch (`rank after: dedupe`) with no def, no symbols, no
quotes - respond_to_missing? and a Struct, that's the whole
parser. the principle underneath: a DSL is a compiler, and
compilers you can trust are BIDIRECTIONAL - the live graph
decompiles to a canonical source that re-parses to the same
graph, byte-stable at one iteration. that's what makes the
emitted source safe to commit, diff, and regenerate: it isn't
documentation ABOUT the plan, it IS the plan, in its Sunday
clothes. one-way DSLs age into folklore; round-trip DSLs age
into file formats.
source
# frozen_string_literal: true # PlanScript: a DSL where BAREWORDS build the graph. Inside the # block, `fetch` isn't a variable or a method you defined - it's # method_missing, catching the name and declaring a step; `rank # after: fetch` catches two. That's the strange half. The principled # half is that the DSL is a real compiler, and real compilers should # be BIDIRECTIONAL: the graph decompiles back to canonical # PlanScript source, the source re-parses to the same graph, and # emit(parse(emit(g))) == emit(g) - a fixpoint. A config format you # can regenerate from the live object is documentation that cannot # lie. One you can't is a one-way door with a nice font. # # bundle exec ruby examples/plan_script.rb # # Runs offline; exits 1 unless the round-trip reaches its fixpoint # and the compiled plan actually executes. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal class PlanScript StepRef = Struct.new(class="y">:name) attr_reader class="y">:steps # {name => [dep names]} def initialize @steps = {} end # Barewords arrive here. First sighting declares; `after:` wires. def method_missing(name, after: nil) @steps[name] ||= [] @steps[name] |= Array(after).map { |d| d.is_a?(StepRef) ? d.name : d } Array(after).each { |d| @steps[d.is_a?(StepRef) ? d.name : d] ||= [] } StepRef.new(name) end def respond_to_missing?(*) = true def self.parse(source = nil, &block) script = new source ? script.instance_eval(source, class="s">"plan_script") : script.instance_eval(&block) script end # The decompiler: canonical source from the graph. Sorted deps, # declaration order preserved - a NORMAL FORM, so emit is stable. def emit lines = @steps.map do |name, deps| if deps.empty? name.to_s else class="s">"#{name} after: #{(deps.size == 1) ? deps.first.inspect : deps.sort.inspect}" end end class="s">"plan do\n" + lines.map { |l| class="s">" #{l}" }.join(class="s">"\n") + class="s">"\nend" end def compile orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4) tasks = @steps.keys.to_h { |name| [name, Agentic:class="y">:Task.new(description: name.to_s, agent_spec: {class="s">"name" => name.to_s, class="s">"instructions" => class="s">"w"})] } ran = [] @steps.each do |name, deps| orchestrator.add_task(tasks[name], deps.map { |d| tasks[d] }, agent: ->(_t) { ran << name class="s">"#{name}: done" }) end [orchestrator, ran] end # `plan do ... end` inside emitted source def plan(&block) = instance_eval(&block) end puts class="s">"PLANSCRIPT (barewords in, graph out, source back - a compiler that round-trips)" puts # --- the script: no symbols, no strings, no defs. just names. ----------------------- original = PlanScript.parse do fetch_feed dedupe after: fetch_feed rank after: dedupe summarize after: rank render after: [rank, summarize] end source = original.emit puts class="s">" the graph, decompiled to canonical PlanScript:" source.lines.each { |l| puts class="s">" #{l.rstrip}" } puts # --- the fixpoint: parse(emit(g)) == g, and emit stabilizes -------------------------- reparsed = PlanScript.parse(source) same_graph = reparsed.steps == original.steps fixpoint = reparsed.emit == source puts class="s">" parse(emit(graph)) == graph: #{same_graph}" puts class="s">" emit(parse(emit(g))) == emit(g): #{fixpoint} (the decompiler reached its fixpoint)" puts # --- and it's not just pretty: the reparsed graph RUNS ------------------------------- orchestrator, ran = reparsed.compile result = orchestrator.execute_plan order_ok = ran.index(class="y">:fetch_feed) < ran.index(class="y">:dedupe) && ran.index(class="y">:rank) < ran.index(class="y">:render) && ran.index(class="y">:summarize) < ran.index(class="y">:render) puts class="s">" compiled and executed the REPARSED graph: #{result.status}, #{ran.size} steps," puts class="s">" dependency order respected: #{order_ok}" puts failures = [] failures << class="s">"round-trip lost the graph" unless same_graph failures << class="s">"emit not a normal form" unless fixpoint failures << class="s">"reparsed plan broken" unless result.status == class="y">:completed && order_ok puts class="s">" two tricks, one principle. the trick you see: method_missing" puts class="s">" turning barewords into declarations, so the script reads like a" puts class="s">" napkin sketch (`rank after: dedupe`) with no def, no symbols, no" puts class="s">" quotes - respond_to_missing? and a Struct, that's the whole" puts class="s">" parser. the principle underneath: a DSL is a compiler, and" puts class="s">" compilers you can trust are BIDIRECTIONAL - the live graph" puts class="s">" decompiles to a canonical source that re-parses to the same" puts class="s">" graph, byte-stable at one iteration. that's what makes the" puts class="s">" emitted source safe to commit, diff, and regenerate: it isn't" puts class="s">" documentation ABOUT the plan, it IS the plan, in its Sunday" puts class="s">" clothes. one-way DSLs age into folklore; round-trip DSLs age" puts class="s">" into file formats." exit(failures.empty? ? 0 : 1)