Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/babl/builder/template_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'babl/utils'
require 'babl/builder'
require 'babl/rendering'
require 'benchmark'

module Babl
module Builder
Expand Down Expand Up @@ -30,10 +31,14 @@ def compile(preloader: Rendering::NoopPreloader, pretty: true, optimize: true)
schema = tree.schema
end

data = Codegen::Variable.new
uncompiled_renderer = tree.renderer(Codegen::Context.new(data))
renderer = Codegen::Generator.new(uncompiled_renderer, data).compile

Rendering::CompiledTemplate.with(
preloader: preloader,
pretty: pretty,
node: tree,
renderer: renderer,
dependencies: dependencies,
json_schema: schema.json
)
Expand Down
8 changes: 8 additions & 0 deletions lib/babl/codegen.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true
require 'babl/codegen/context'
require 'babl/codegen/expression'
require 'babl/codegen/resource'
require 'babl/codegen/generator'
require 'babl/codegen/linked_expression'
require 'babl/codegen/variable'
require 'babl/codegen/local'
50 changes: 50 additions & 0 deletions lib/babl/codegen/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'babl/errors'

module Babl
module Codegen
class Context
attr_reader :key, :object, :parent, :pins

def initialize(object, key = nil, parent = nil, pins = nil)
@key = key
@object = object
@parent = parent
@pins = pins
end

# Standard navigation (enter into property)
def move_forward(new_object, key)
Context.new(new_object, key, self, pins)
end

# Go back to parent
def move_backward
raise Errors::InvalidTemplate, 'There is no parent element' unless parent
Context.new(parent.object, parent.key, parent.parent, pins)
end

# Go to a pinned context
def goto_pin(ref)
pin = pins&.[](ref)
raise Errors::InvalidTemplate, 'Pin reference cannot be used here' unless pin
Context.new(pin.object, pin.key, pin.parent, (pin.pins || {}).merge(pins))
end

# Associate a pin to current context
def create_pin(ref)
Context.new(object, key, parent, (pins || {}).merge(ref => self))
end

def formatted_stack
stack_trace = ([:__root__] + stack).join('.')
"BABL @ #{stack_trace}"
end

# Return an array containing the navigation history
def stack
(parent ? parent.stack : []) + [key].compact
end
end
end
end
12 changes: 12 additions & 0 deletions lib/babl/codegen/expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'babl/utils'

module Babl
module Codegen
class Expression < Utils::Value.new(:code)
def initialize(&block)
super(block)
end
end
end
end
180 changes: 180 additions & 0 deletions lib/babl/codegen/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# frozen_string_literal: true
require 'babl/utils/value'

module Babl
module Codegen
class Generator
class InlineResolver
attr_reader :assigned_vars, :resolver

def initialize(resolver, assigned_vars)
@resolver = resolver
@assigned_vars = assigned_vars
end

def resolve(val, vars = {})
case val
when Expression then resolver.resolve(val, assigned_vars.merge(vars))
when Variable then (assigned_vars[val] && ('(' + assigned_vars[val] + ')')) || resolver.resolve(val)
else resolver.resolve(val)
end
end
end

class Resolver
attr_reader :generator, :argument_names, :called_linked_expressions, :expr, :local_names

def initialize(generator, expr)
@expr = expr
@generator = generator
@argument_names = {}
@called_linked_expressions = []
@local_names = {}
end

def resolve(*args)
case args.first
when Variable then variable(*args)
when Resource then resource(*args)
# TODO : do not inline if resolved more than once + ensure we are always re-resolving to
# account for all cases
when Expression then expression(*args)
when Local then local(*args)
end
end

def local(var)
local_names[var] ||= "l#{local_names.size}"
end

def expression(other_expr, assigned_vars = {})
linked_other = generator.link(other_expr)

if linked_other.expression && generator.allowed_inlining.include?(linked_other)
inline_resolver = InlineResolver.new(self, assigned_vars)
'(' + linked_other.expression.code.call(inline_resolver) + ')'
else
called_linked_expressions << linked_other
params = linked_other.inputs.map { |rv|
(assigned_vars[rv] && ('(' + assigned_vars[rv] + ')')) || variable(rv)
}.join(',')
linked_other.name + (params.empty? ? '' : '(' + params + ')')
end
end

def variable(var)
argument_names[var] ||= "v#{argument_names.size}"
end

def resource(res)
generator.resource_name(res)
end
end

attr_reader :linked_expressions, :root_expression, :method_names, :evaluator_inputs,
:resources, :allowed_inlining, :linked_root_expression

def initialize(root_expression, *evaluator_inputs)
@evaluator_inputs = evaluator_inputs
@root_expression = root_expression
@allowed_inlining = Set.new
@method_names = {}
@resources = {}
@linked_expressions = {}

# First pass: we link all expressions together without inlining.
@linked_root_expression = link(root_expression)

# Second pass: we have collected data about how much time each expression is used
# so we can selectivety enable inlining when appropriate.
loop do
# break
prev_inline_size = allowed_inlining.size
compute_allowed_inlining
# puts allowed_inlining.size
@linked_expressions = {}
@linked_root_expression = link(root_expression)
# break
break if prev_inline_size == allowed_inlining.size
end
end

def compute_allowed_inlining
# Inline expressions which are only called once
linked_expressions.values
.flat_map { |le| le.called_linked_expressions.map { |called_le| [called_le, le] } }
.group_by { |called_le, _| called_le.name }
.each { |_, group|
next if group.size > 1
group.each { |called_le, _|
allowed_inlining << called_le
}
}

# Inline expressions taking no parameter
linked_expressions.values
.select { |le| le.inputs.empty? }
.each { |le| allowed_inlining << le }
end

def called_linked_expressions(root)
[root] + root.called_linked_expressions.flat_map { |le| called_linked_expressions(le) }
end

def compile
body = called_linked_expressions(linked_root_expression).map(&:code).uniq.join("\n")
linked_root_expr = linked_expressions[root_expression]

ordered_variables = linked_root_expr.inputs.map { |rv| "v#{evaluator_inputs.index(rv)}" }
raise Errors::InvalidTemplate, 'Codegen failed' if ordered_variables.include?('v')

body << <<~RUBY
def evaluate(#{Array.new(evaluator_inputs.size) { |i| "v#{i}" }.join(',')})
#{linked_root_expr.name}(#{ordered_variables.join(',')})
end
RUBY

# puts body

Class.new.tap { |clazz|
resources.each { |k, v|
clazz.const_set(v, k.value)
# puts "#{v} = #{k.value.inspect}"
}

clazz.class_eval(body)
}.new
end

def link(expr)
return linked_expressions[expr] if linked_expressions[expr]

resolver = Resolver.new(self, expr)
body = expr.code.call(resolver)
args = resolver.argument_names.values
name = method_name(body, args)

fle = linked_expressions.find { |_xp, le| le.code == body }&.last
if fle
fle.called_linked_expressions += resolver.called_linked_expressions
return fle
end

linked_expressions[expr] = LinkedExpression.new(
name, resolver.argument_names.keys, expr, resolver.called_linked_expressions, <<~RUBY)
def #{name}(#{args.join(',')})
#{body}
end
RUBY
end

def resource_name(resource)
@resources[resource] ||= "R#{@resources.size}"
end

def method_name(body, args)
@method_names[[body, args]] ||= "x#{@method_names.size}"
end
end
end
end
8 changes: 8 additions & 0 deletions lib/babl/codegen/linked_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true
require 'babl/utils'

module Babl
module Codegen
LinkedExpression = Utils::Value.new(:name, :inputs, :expression, :called_linked_expressions, :code)
end
end
9 changes: 9 additions & 0 deletions lib/babl/codegen/local.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'babl/utils/value'

module Babl
module Codegen
class Local
end
end
end
8 changes: 8 additions & 0 deletions lib/babl/codegen/resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true
require 'babl/utils'

module Babl
module Codegen
Resource = Utils::Value.new(:value)
end
end
9 changes: 9 additions & 0 deletions lib/babl/codegen/variable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'babl/utils/value'

module Babl
module Codegen
class Variable
end
end
end
1 change: 1 addition & 0 deletions lib/babl/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
require 'babl/nodes/terminal_value'
require 'babl/nodes/typed'
require 'babl/nodes/with'
require 'babl/nodes/shared/error_handling'
9 changes: 5 additions & 4 deletions lib/babl/nodes/constant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
module Babl
module Nodes
class Constant < Utils::Value.new(:value, :schema)
def render(_ctx)
value
end

def dependencies
Utils::Hash::EMPTY
end
Expand All @@ -17,6 +13,11 @@ def pinned_dependencies
Utils::Hash::EMPTY
end

def renderer(_ctx)
res = Codegen::Resource.new(value)
Codegen::Expression.new { |resolver| resolver.resolve(res) }
end

def optimize
self
end
Expand Down
4 changes: 2 additions & 2 deletions lib/babl/nodes/create_pin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
module Babl
module Nodes
class CreatePin < Utils::Value.new(:node, :ref)
def render(ctx)
node.render(ctx.create_pin(ref))
def renderer(ctx)
node.renderer(ctx.create_pin(ref))
end

def schema
Expand Down
4 changes: 2 additions & 2 deletions lib/babl/nodes/dep.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
module Babl
module Nodes
class Dep < Utils::Value.new(:node, :path)
def render(ctx)
node.render(ctx)
def renderer(ctx)
node.renderer(ctx)
end

def schema
Expand Down
Loading