Skip to content

Commit

Permalink
Codegen draft
Browse files Browse the repository at this point in the history
  • Loading branch information
fterrazzoni committed Jul 23, 2017
1 parent ddafb1e commit a125289
Show file tree
Hide file tree
Showing 31 changed files with 531 additions and 86 deletions.
6 changes: 5 additions & 1 deletion lib/babl/builder/template_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ def initialize(builder = ChainBuilder.new(&:itself))
def compile(preloader: Rendering::NoopPreloader, pretty: true)
node = precompile

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

Rendering::CompiledTemplate.with(
preloader: preloader,
pretty: pretty,
node: node,
renderer: renderer,
dependencies: node.dependencies,
json_schema: node.schema.json
)
Expand Down
6 changes: 6 additions & 0 deletions lib/babl/codegen.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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'
49 changes: 49 additions & 0 deletions lib/babl/codegen/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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
11 changes: 11 additions & 0 deletions lib/babl/codegen/expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'babl/utils'

module Babl
module Codegen
class Expression < Utils::Value.new(:inline, :inline_outside, :code)
def initialize(inline: false, inline_outside: false, &block)
super(inline, inline_outside, block)
end
end
end
end
160 changes: 160 additions & 0 deletions lib/babl/codegen/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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

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

def resolve(*args)
case args.first
when Variable then variable(*args)
when Resource then resource(*args)
when Expression then expression(*args)
end
end

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

if linked_other.expression.inline && generator.allowed_inlining.include?(linked_other) && expr.inline_outside
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
prev_inline_size = allowed_inlining.size
compute_allowed_inlining
@linked_expressions = {}
@linked_root_expression = link(root_expression)
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(&:first)
.map { |le, called_by_les| [le.code, le, called_by_les.map(&:last).size] }
.group_by(&:first)
.each { |_code, stats|
total = stats.sum { |_code, _le, times| times }
next if total > 1
stats.each { |_code, le, _times| allowed_inlining << 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) }
# linked_expressions.values.flat_map(&:called_linked_expressions).uniq + [linked_root_expression]
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) }
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)

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
7 changes: 7 additions & 0 deletions lib/babl/codegen/linked_expression.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'babl/utils'

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

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

module Babl
module Codegen
class Variable
end
end
end
2 changes: 1 addition & 1 deletion lib/babl/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
require 'babl/nodes/nav'
require 'babl/nodes/object'
require 'babl/nodes/parent'
require 'babl/nodes/static'
require 'babl/nodes/primitive'
require 'babl/nodes/switch'
require 'babl/nodes/terminal_value'
require 'babl/nodes/typed'
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 @@ -3,8 +3,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 @@ -7,8 +7,8 @@ def initialize(node, path)
super(node, canonicalize(path))
end

def render(ctx)
node.render(ctx)
def renderer(ctx)
node.renderer(ctx)
end

def schema
Expand Down
29 changes: 23 additions & 6 deletions lib/babl/nodes/each.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,29 @@ def pinned_dependencies
node.pinned_dependencies
end

def render(ctx)
collection = ctx.object
unless Enumerable === collection
raise Errors::RenderingError, "Not enumerable : #{collection}\n#{ctx.formatted_stack}"
end
collection.each_with_index.map { |value, idx| node.render(ctx.move_forward(value, idx)) }
def renderer(ctx)
it_var = Codegen::Variable.new
formatted_stack_var = Codegen::Variable.new
formatted_stack_res = Codegen::Resource.new(ctx.formatted_stack)
inner_expression = node.renderer(ctx.move_forward(it_var, :'#'))

ensure_enumerable = Codegen::Expression.new(inline_outside: true) { |resolver|
val = resolver.resolve(ctx.object)
stack = resolver.resolve(formatted_stack_var)
<<~RUBY
unless ::Enumerable === #{val}
raise Errors::RenderingError, "Not enumerable : " + #{val}.inspect + "\n" + #{stack}
end
#{val}
RUBY
}

Codegen::Expression.new(inline: true) { |resolver|
array = resolver.resolve(ensure_enumerable, formatted_stack_var => resolver.resolve(formatted_stack_res))
inner = resolver.resolve(inner_expression, it_var => 'val')

"#{array}.map { |val| #{inner} }"
}
end
end
end
Expand Down
7 changes: 5 additions & 2 deletions lib/babl/nodes/fixed_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ def pinned_dependencies
nodes.map(&:pinned_dependencies).reduce({}) { |a, b| Babl::Utils::Hash.deep_merge(a, b) }
end

def render(ctx)
nodes.map { |node| node.render(ctx) }
def renderer(ctx)
renderers = nodes.map { |node| node.renderer(ctx) }
Codegen::Expression.new(inline: true, inline_outside: true) { |resolver|
'[' + renderers.map { |expr| resolver.resolve(expr) }.join(',') + ']'
}
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/babl/nodes/goto_pin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def schema
node.schema
end

def render(ctx)
node.render(ctx.goto_pin(ref))
def renderer(ctx)
node.renderer(ctx.goto_pin(ref))
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/babl/nodes/internal_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ def pinned_dependencies
{}
end

def render(ctx)
ctx.object
def renderer(ctx)
Codegen::Expression.new(inline: true) { |resolver| resolver.resolve(ctx.object) }
end
end
end
Expand Down
Loading

0 comments on commit a125289

Please sign in to comment.