diff --git a/lib/babl/builder/template_base.rb b/lib/babl/builder/template_base.rb index 2f23464..65c5129 100644 --- a/lib/babl/builder/template_base.rb +++ b/lib/babl/builder/template_base.rb @@ -3,6 +3,7 @@ require 'babl/utils' require 'babl/builder' require 'babl/rendering' +require 'benchmark' module Babl module Builder @@ -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 ) diff --git a/lib/babl/codegen.rb b/lib/babl/codegen.rb new file mode 100644 index 0000000..59ba609 --- /dev/null +++ b/lib/babl/codegen.rb @@ -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' diff --git a/lib/babl/codegen/context.rb b/lib/babl/codegen/context.rb new file mode 100644 index 0000000..f4296fa --- /dev/null +++ b/lib/babl/codegen/context.rb @@ -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 diff --git a/lib/babl/codegen/expression.rb b/lib/babl/codegen/expression.rb new file mode 100644 index 0000000..55881fc --- /dev/null +++ b/lib/babl/codegen/expression.rb @@ -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 diff --git a/lib/babl/codegen/generator.rb b/lib/babl/codegen/generator.rb new file mode 100644 index 0000000..1eccc7e --- /dev/null +++ b/lib/babl/codegen/generator.rb @@ -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 diff --git a/lib/babl/codegen/linked_expression.rb b/lib/babl/codegen/linked_expression.rb new file mode 100644 index 0000000..ff8abd1 --- /dev/null +++ b/lib/babl/codegen/linked_expression.rb @@ -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 diff --git a/lib/babl/codegen/local.rb b/lib/babl/codegen/local.rb new file mode 100644 index 0000000..a9a2870 --- /dev/null +++ b/lib/babl/codegen/local.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require 'babl/utils/value' + +module Babl + module Codegen + class Local + end + end +end diff --git a/lib/babl/codegen/resource.rb b/lib/babl/codegen/resource.rb new file mode 100644 index 0000000..4be0347 --- /dev/null +++ b/lib/babl/codegen/resource.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +require 'babl/utils' + +module Babl + module Codegen + Resource = Utils::Value.new(:value) + end +end diff --git a/lib/babl/codegen/variable.rb b/lib/babl/codegen/variable.rb new file mode 100644 index 0000000..c45228e --- /dev/null +++ b/lib/babl/codegen/variable.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require 'babl/utils/value' + +module Babl + module Codegen + class Variable + end + end +end diff --git a/lib/babl/nodes.rb b/lib/babl/nodes.rb index bd94ca8..f87b781 100644 --- a/lib/babl/nodes.rb +++ b/lib/babl/nodes.rb @@ -14,3 +14,4 @@ require 'babl/nodes/terminal_value' require 'babl/nodes/typed' require 'babl/nodes/with' +require 'babl/nodes/shared/error_handling' diff --git a/lib/babl/nodes/constant.rb b/lib/babl/nodes/constant.rb index c321b21..b312448 100644 --- a/lib/babl/nodes/constant.rb +++ b/lib/babl/nodes/constant.rb @@ -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 @@ -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 diff --git a/lib/babl/nodes/create_pin.rb b/lib/babl/nodes/create_pin.rb index a342972..2188797 100644 --- a/lib/babl/nodes/create_pin.rb +++ b/lib/babl/nodes/create_pin.rb @@ -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 diff --git a/lib/babl/nodes/dep.rb b/lib/babl/nodes/dep.rb index c6219e2..3e1ffbc 100644 --- a/lib/babl/nodes/dep.rb +++ b/lib/babl/nodes/dep.rb @@ -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 diff --git a/lib/babl/nodes/each.rb b/lib/babl/nodes/each.rb index 5c584c4..9df6eac 100644 --- a/lib/babl/nodes/each.rb +++ b/lib/babl/nodes/each.rb @@ -18,12 +18,35 @@ 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 + stack_var = Codegen::Variable.new + stack_res = Codegen::Resource.new(ctx.stack) + inner_expression = node.renderer(ctx.move_forward(it_var, :'#')) + + ensure_enumerable = Codegen::Expression.new { |resolver| + val = resolver.resolve(ctx.object) + stack = resolver.resolve(stack_var) + <<~RUBY + unless ::Enumerable === #{val} + Babl::Nodes::Shared::ErrorHandling.raise_message( + 'Not enumerable : ' + #{val}.inspect + "\\n", + #{stack} + ) + end + #{val} + RUBY + } + + local_var = Codegen::Local.new + + Codegen::Expression.new { |resolver| + local = resolver.resolve(local_var) + array = resolver.resolve(ensure_enumerable, stack_var => resolver.resolve(stack_res)) + inner = resolver.resolve(inner_expression, it_var => local) + + "#{array}.map { |#{local}| #{inner} }" + } end def optimize diff --git a/lib/babl/nodes/fixed_array.rb b/lib/babl/nodes/fixed_array.rb index a292d5c..6155ca7 100644 --- a/lib/babl/nodes/fixed_array.rb +++ b/lib/babl/nodes/fixed_array.rb @@ -18,15 +18,18 @@ def pinned_dependencies nodes.map(&:pinned_dependencies).reduce(Utils::Hash::EMPTY) { |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 { |resolver| + '[' + renderers.map { |expr| resolver.resolve(expr) }.join(',') + ']' + } end def optimize optimized_nodes = nodes.map(&:optimize) fixed_array = FixedArray.new(optimized_nodes) return fixed_array unless optimized_nodes.all? { |node| Constant === node } - Constant.new(fixed_array.render(nil).freeze, fixed_array.schema) + Constant.new(fixed_array.nodes.map(&:value).freeze, fixed_array.schema) end end end diff --git a/lib/babl/nodes/goto_pin.rb b/lib/babl/nodes/goto_pin.rb index 7b2e25c..aa07c84 100644 --- a/lib/babl/nodes/goto_pin.rb +++ b/lib/babl/nodes/goto_pin.rb @@ -17,8 +17,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 def optimize diff --git a/lib/babl/nodes/internal_value.rb b/lib/babl/nodes/internal_value.rb index 9dcb9b7..a109c27 100644 --- a/lib/babl/nodes/internal_value.rb +++ b/lib/babl/nodes/internal_value.rb @@ -27,8 +27,8 @@ def pinned_dependencies Utils::Hash::EMPTY end - def render(ctx) - ctx.object + def renderer(ctx) + Codegen::Expression.new { |resolver| resolver.resolve(ctx.object) } end def optimize diff --git a/lib/babl/nodes/is_null.rb b/lib/babl/nodes/is_null.rb index 1dc6636..956f945 100644 --- a/lib/babl/nodes/is_null.rb +++ b/lib/babl/nodes/is_null.rb @@ -20,8 +20,8 @@ def pinned_dependencies Utils::Hash::EMPTY end - def render(ctx) - ::NilClass === ctx.object + def renderer(ctx) + Codegen::Expression.new { |resolver| '::NilClass === ' + resolver.resolve(ctx.object) } end def optimize diff --git a/lib/babl/nodes/merge.rb b/lib/babl/nodes/merge.rb index 66c1918..1080898 100644 --- a/lib/babl/nodes/merge.rb +++ b/lib/babl/nodes/merge.rb @@ -19,10 +19,42 @@ def schema nodes.map(&:schema).reduce(Schema::Object::EMPTY) { |a, b| merge_doc(a, b) } end - def render(ctx) - nodes.map { |node| node.render(ctx) }.compact.reduce({}) { |acc, val| - raise Errors::RenderingError, "Only objects can be merged\n" + ctx.formatted_stack unless ::Hash === val - acc.merge!(val) + def renderer(ctx) + hash_to_check = Codegen::Variable.new + empty_hash = Codegen::Resource.new({}) + stack_var = Codegen::Variable.new + + ensure_hash = Codegen::Expression.new { |resolver| + input = resolver.resolve(hash_to_check) + resolved_empty_hash = resolver.resolve(empty_hash) + stack = resolver.resolve(stack_var) + + invalid_hash = 'Babl::Nodes::Shared::ErrorHandling'\ + ".raise_message('Only objects can be merged', #{stack})" + + empty_hash_if_nil = "nil == #{input} ? #{resolved_empty_hash} : #{invalid_hash}" + + "::Hash === #{input} ? #{input} : #{empty_hash_if_nil}" + } + + exprs = nodes.map { |node| node.renderer(ctx) } + + accumulator = Codegen::Local.new + stack_res = Codegen::Resource.new(ctx.stack) + + Codegen::Expression.new { |resolver| + merges = exprs.map { |expr| resolver.resolve(expr) } + .map { |value| + ensured_hash = resolver.resolve ensure_hash, + hash_to_check => value, + stack_var => resolver.resolve(stack_res) + + "#{resolver.resolve(accumulator)}"\ + ".merge!(#{ensured_hash})" + } + .join("\n") + + "#{resolver.resolve(accumulator)} = {}\n#{merges}\n#{resolver.resolve(accumulator)}" } end diff --git a/lib/babl/nodes/nav.rb b/lib/babl/nodes/nav.rb index b3e6ae1..6716fc6 100644 --- a/lib/babl/nodes/nav.rb +++ b/lib/babl/nodes/nav.rb @@ -18,13 +18,49 @@ def pinned_dependencies node.pinned_dependencies end - def render(ctx) - value = begin - ::Hash === ctx.object ? ctx.object.fetch(property) : ctx.object.send(property) - rescue StandardError => e - raise Errors::RenderingError, "#{e.message}\n" + ctx.formatted_stack(property), e.backtrace - end - node.render(ctx.move_forward(value, property)) + def renderer(ctx) + var = Codegen::Variable.new + new_ctx = ctx.move_forward(var, property) + inner_expression = node.renderer(new_ctx) + + stack_var = Codegen::Variable.new + current_var = Codegen::Variable.new + property_var = Codegen::Variable.new + + navigator = Codegen::Expression.new { |resolver| + stack = resolver.resolve(stack_var) + current = resolver.resolve(current_var) + property = resolver.resolve(property_var) + + <<~RUBY + begin + ::Hash === #{current} ? #{current}.fetch(#{property}) : #{current}.__send__(#{property}) + rescue ::StandardError => e + Babl::Nodes::Shared::ErrorHandling.raise_enriched(e, #{stack}) + end + RUBY + } + + stack_res = Codegen::Resource.new(new_ctx.stack) + local_var = Codegen::Local.new + + Codegen::Expression.new { |resolver| + local = resolver.resolve(local_var) + + navigated = resolver.resolve( + navigator, + stack_var => resolver.resolve(stack_res), + current_var => resolver.resolve(ctx.object), + property_var => property.inspect + ) + + call_inner = resolver.resolve( + inner_expression, + var => local + ) + + "begin; #{local} = #{navigated}; #{call_inner}; end" + } end def optimize diff --git a/lib/babl/nodes/object.rb b/lib/babl/nodes/object.rb index a895be1..13273d5 100644 --- a/lib/babl/nodes/object.rb +++ b/lib/babl/nodes/object.rb @@ -22,17 +22,18 @@ def schema Schema::Object.new(properties, false) end - def render(ctx) - out = {} - nodes.each { |k, v| out[k] = v.render(ctx) } - out + def renderer(ctx) + renderers = nodes.map { |name, node| [name, node.renderer(ctx)] } + Codegen::Expression.new { |resolver| + '{' + renderers.map { |name, expr| "#{name.inspect} => #{resolver.resolve(expr)}" }.join(",\n") + '}' + } end def optimize optimized_nodes = nodes.map { |k, v| [k, v.optimize] }.to_h optimized_object = Object.new(optimized_nodes) return optimized_object unless optimized_nodes.values.all? { |node| Constant === node } - Constant.new(optimized_object.render(nil).freeze, optimized_object.schema) + Constant.new(optimized_object.nodes.map { |k, v| [k, v.value] }.to_h.freeze, optimized_object.schema) end end end diff --git a/lib/babl/nodes/parent.rb b/lib/babl/nodes/parent.rb index 00a49be..dbe31d3 100644 --- a/lib/babl/nodes/parent.rb +++ b/lib/babl/nodes/parent.rb @@ -20,8 +20,8 @@ def pinned_dependencies node.pinned_dependencies end - def render(ctx) - node.render(ctx) + def renderer(ctx) + node.renderer(ctx) end def optimize @@ -60,8 +60,8 @@ def dependencies { PARENT_MARKER => node.dependencies } end - def render(ctx) - node.render(ctx.move_backward) + def renderer(ctx) + node.renderer(ctx.move_backward) end def optimize diff --git a/lib/babl/nodes/shared/error_handling.rb b/lib/babl/nodes/shared/error_handling.rb new file mode 100644 index 0000000..69964bd --- /dev/null +++ b/lib/babl/nodes/shared/error_handling.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module Babl + module Nodes + module Shared + module ErrorHandling + class << self + def raise_enriched(exception, stack) + raise Errors::RenderingError, + "#{exception.message}\nBABL @ #{([:__root__] + stack).join('.')}", + exception.backtrace + end + + def raise_message(message, stack) + raise_enriched Errors::RenderingError.new(message), stack + end + end + end + end + end +end diff --git a/lib/babl/nodes/switch.rb b/lib/babl/nodes/switch.rb index cb1e570..1237b2f 100644 --- a/lib/babl/nodes/switch.rb +++ b/lib/babl/nodes/switch.rb @@ -27,9 +27,28 @@ def schema Schema::AnyOf.canonicalized(nodes.map(&:last).map(&:schema)) end - def render(ctx) - nodes.each { |cond, value| return value.render(ctx) if cond.render(ctx) } - raise Errors::RenderingError, 'A least one switch() condition must be taken' + def renderer(ctx) + message = Codegen::Resource.new('A least one switch() condition must be taken') + failure = Codegen::Expression.new { |resolver| + <<~RUBY + raise Errors::RenderingError, #{resolver.resolve(message)} + RUBY + } + + exprs = nodes.map { |key, val| [key.renderer(ctx), val.renderer(ctx)] } + + Codegen::Expression.new { |resolver| + code = resolver.resolve(failure) + + exprs.reverse.each do |cond, val| + resolved_cond = resolver.resolve(cond) + resolved_val = resolver.resolve(val) + + code = "(#{resolved_cond})?(#{resolved_val}):(#{code})" + end + + code + } end def optimize diff --git a/lib/babl/nodes/terminal_value.rb b/lib/babl/nodes/terminal_value.rb index 84e3782..55522b8 100644 --- a/lib/babl/nodes/terminal_value.rb +++ b/lib/babl/nodes/terminal_value.rb @@ -24,12 +24,6 @@ def pinned_dependencies Utils::Hash::EMPTY end - def render(ctx) - render_object(ctx.object) - rescue TerminalValueError => e - raise Errors::RenderingError, "#{e.message}\n" + ctx.formatted_stack(e.babl_stack), e.backtrace - end - def render_object(obj, stack = nil) case obj when ::String, ::Integer, ::NilClass, ::TrueClass, ::FalseClass then obj @@ -45,6 +39,31 @@ def optimize self end + def renderer(ctx) + stack_var = Codegen::Variable.new + stack_res = Codegen::Resource.new(ctx.stack) + current_var = Codegen::Variable.new + + expr = Codegen::Expression.new { |resolver| + current_obj = resolver.resolve(current_var) + stack = resolver.resolve(stack_var) + + <<~RUBY + begin + Babl::Nodes::TerminalValue.instance.render_object(#{current_obj}) + rescue ::Babl::Nodes::TerminalValue::TerminalValueError => e + Babl::Nodes::Shared::ErrorHandling.raise_enriched(e, #{stack} + e.babl_stack) + end + RUBY + } + + Codegen::Expression.new { |resolver| + resolver.resolve expr, + stack_var => resolver.resolve(stack_res), + current_var => resolver.resolve(ctx.object) + } + end + private def render_array(array, stack) diff --git a/lib/babl/nodes/typed.rb b/lib/babl/nodes/typed.rb index 699b33f..f9450fe 100644 --- a/lib/babl/nodes/typed.rb +++ b/lib/babl/nodes/typed.rb @@ -19,12 +19,35 @@ def pinned_dependencies Utils::Hash::EMPTY end - def render(ctx) - value = ctx.object - if schema.classes.any? { |clazz| clazz === value } - return ::Numeric === value ? TerminalValue.instance.render_object(value) : value - end - raise Errors::RenderingError, "Expected type '#{schema.type}': #{value}\n#{ctx.formatted_stack}" + def renderer(ctx) + stack_var = Codegen::Variable.new + value_var = Codegen::Variable.new + + raiser = Codegen::Expression.new { |resolver| + stack = resolver.resolve(stack_var) + value = resolver.resolve(value_var) + + <<~RUBY + Babl::Nodes::Shared::ErrorHandling.raise_message( + 'Expected type #{schema.type}:' + #{value}.inspect, #{stack} + ) + RUBY + } + + tester = Codegen::Expression.new { |resolver| + value = resolver.resolve(value_var) + test = schema.classes.map { |cl| "::#{cl.name} === #{value}" }.join('||') + # TODO : BigDecimal handling + "#{test} ? #{value} : #{resolver.resolve(raiser)}" + } + + stack_res = Codegen::Resource.new(ctx.stack) + + Codegen::Expression.new { |resolver| + resolver.resolve tester, + stack_var => resolver.resolve(stack_res), + value_var => resolver.resolve(ctx.object) + } end def optimize diff --git a/lib/babl/nodes/with.rb b/lib/babl/nodes/with.rb index 40aaf11..21afed3 100644 --- a/lib/babl/nodes/with.rb +++ b/lib/babl/nodes/with.rb @@ -19,14 +19,55 @@ def pinned_dependencies .reduce(Utils::Hash::EMPTY) { |a, b| Babl::Utils::Hash.deep_merge(a, b) } end - def render(ctx) - values = nodes.map { |n| n.render(ctx) } - value = begin - block.arity.zero? ? ctx.object.instance_exec(&block) : block.call(*values) - rescue StandardError => e - raise Errors::RenderingError, "#{e.message}\n" + ctx.formatted_stack(:__block__), e.backtrace - end - node.render(ctx.move_forward(value, :__block__)) + def renderer(ctx) + # TODO : understandable naming convention + var = Codegen::Variable.new + new_ctx = ctx.move_forward(var, :__block__) + inner_expression = node.renderer(new_ctx) + val_exprs = nodes.map { |n| n.renderer(ctx) } + + stack_var = Codegen::Variable.new + current_var = Codegen::Variable.new + block_var = Codegen::Variable.new + + navigator = Codegen::Expression.new { |resolver| + stack = resolver.resolve(stack_var) + current = resolver.resolve(current_var) + block_var_name = resolver.resolve(block_var) + + blk_call = + if block.arity.zero? + current + '.instance_exec(&' + block_var_name + ')' + else + block_var_name + '.call(' + val_exprs.map { |xp| resolver.resolve(xp) }.join(',') + ')' + end + + <<~RUBY + begin + #{blk_call} + rescue ::StandardError => e + Babl::Nodes::Shared::ErrorHandling.raise_enriched(e, #{stack}) + end + RUBY + } + + result_var = Codegen::Local.new + block_res = Codegen::Resource.new(block) + stack_res = Codegen::Resource.new(new_ctx.stack) + + Codegen::Expression.new { |resolver| + result = resolver.resolve(result_var) + value = resolver.resolve navigator, + block_var => resolver.resolve(block_res), + current_var => resolver.resolve(ctx.object), + stack_var => resolver.resolve(stack_res) + call_inner = resolver.resolve(inner_expression, var => result) + + <<-RUBY + #{result} = #{value} + #{call_inner} + RUBY + } end def optimize diff --git a/lib/babl/rendering/compiled_template.rb b/lib/babl/rendering/compiled_template.rb index a9ad9ea..d6b1476 100644 --- a/lib/babl/rendering/compiled_template.rb +++ b/lib/babl/rendering/compiled_template.rb @@ -2,10 +2,12 @@ require 'multi_json' require 'babl/rendering' require 'babl/utils' +require 'babl/codegen' +require 'benchmark' module Babl module Rendering - class CompiledTemplate < Utils::Value.new(:node, :dependencies, :preloader, :pretty, :json_schema) + class CompiledTemplate < Utils::Value.new(:renderer, :dependencies, :preloader, :pretty, :json_schema) def json(root) data = render(root) ::MultiJson.dump(data, pretty: pretty) @@ -13,8 +15,7 @@ def json(root) def render(root) preloaded_data = preloader.preload([root], dependencies).first - ctx = Context.new(preloaded_data) - node.render(ctx) + renderer.evaluate(preloaded_data) end end end diff --git a/spec/operators/each_spec.rb b/spec/operators/each_spec.rb index 9053402..da56e7f 100644 --- a/spec/operators/each_spec.rb +++ b/spec/operators/each_spec.rb @@ -20,7 +20,7 @@ let(:object) { { box: [{ trololol: 2 }, 42] } } - it { expect { json }.to raise_error(/\__root__\.box\.1\.trololol/) } + it { expect { json }.to raise_error(/\__root__\.box\.#\.trololol/) } end context 'not enumerable' do diff --git a/spec/operators/nav_spec.rb b/spec/operators/nav_spec.rb index 4b068fb..32a1599 100644 --- a/spec/operators/nav_spec.rb +++ b/spec/operators/nav_spec.rb @@ -30,6 +30,12 @@ it { expect(schema).to eq s_anything } end + context 'method name containing spaces' do + template { nav(:"lol captain") } + let(:object) { Struct.new(:"lol captain").new(10) } + it { expect(json).to eq 10 } + end + context 'navigate to symbol' do template { nav(:a) } diff --git a/spec/spec_helper/operator_testing.rb b/spec/spec_helper/operator_testing.rb index 41bc9b8..3b0e8a5 100644 --- a/spec/spec_helper/operator_testing.rb +++ b/spec/spec_helper/operator_testing.rb @@ -17,15 +17,20 @@ def self.extended(base) base.let(:compiled) { template.compile } base.let(:unoptimized_compiled) { template.compile(optimize: false) } base.let(:unchecked_json) { ::MultiJson.load(compiled.json(object)) } + base.let(:unoptimized_unchecked_json) { ::MultiJson.load(unoptimized_compiled.json(object)) } + base.let(:dependencies) { deps = compiled.send(:dependencies) expect(Babl::Utils::Hash.deep_merge(deps, unoptimized_dependencies)).to eq unoptimized_dependencies deps } + + base.let(:schema) { template.send(:precompile).schema } + base.let(:dependencies) { compiled.send(:dependencies) } base.let(:unoptimized_dependencies) { unoptimized_compiled.send(:dependencies) } - base.let(:schema) { compiled.send(:node).schema } - base.let(:unoptimized_schema) { unoptimized_compiled.send(:node).schema } + base.let(:schema) { template.send(:precompile).optimize.send(:node).schema } + base.let(:unoptimized_schema) { template.precompile.schema } base.let(:json_schema) { compiled.json_schema } base.let(:unoptimized_json_schema) { unoptimized_compiled.json_schema }