Implement naive recusrive descent

Ragel doesn't allow us to recurse so we simply
reinvoke the parser for each step.
This commit is contained in:
Tobias Lutke
2012-10-28 21:55:20 -04:00
parent 18b83a58bd
commit cd040dabd8
8 changed files with 1275 additions and 564 deletions
+60 -71
View File
@@ -142,32 +142,41 @@ module Liquid
case key
when nil, ""
return nil
when "blank?"
return :blank
when "empty?"
return :empty
when "blank"
return :blank?
when "empty"
return :empty?
end
puts "resolve(#{key})"
result = Parser.parse(key)
stack = []
result.each do |(sym, value)|
case sym
when :identifier
when :id
stack.push value
when :lookup
left = stack.pop
stack.push find_variable(left)
value = find_variable(left)
stack.push(harden(value))
when :range
right = stack.pop.to_i
left = stack.pop.to_i
stack.push (left..right)
when :buildin
left = stack.pop
value = invoke_buildin(left, value)
stack.push(harden(value))
when :call
parent = stack.pop
key = stack.pop
stack.push lookup_and_evaluate(parent, key)
left = stack.pop
right = stack.pop
value = lookup_and_evaluate(right, left)
stack.push(harden(value))
else
raise "unknown #{sym}"
end
@@ -176,6 +185,22 @@ module Liquid
return stack.first
end
def invoke_buildin(obj, key)
# as weird as this is, liquid unit tests demand that we prioritize hash lookups
# to buildins. So if we got a hash and it has a :first element we need to call that
# instead of sending the first message...
if obj.respond_to?(:has_key?) && obj.has_key?(key)
return lookup_and_evaluate(obj, key)
end
if obj.respond_to?(key)
return obj.send(key)
else
return nil
end
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
scope = @scopes.find { |s| s.has_key?(key) }
@@ -192,71 +217,35 @@ module Liquid
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
# Resolves namespaced queries gracefully.
#
# Example
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.each do |part|
part = resolve($1) if part_resolved = (part =~ square_bracketed)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) and
((object.respond_to?(:has_key?) and object.has_key?(part)) or
(object.respond_to?(:fetch) and part.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = lookup_and_evaluate(object, part)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end # variable
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
return nil unless obj.respond_to?(:[])
if obj.is_a?(Array)
return nil unless key.is_a?(Integer)
end
end # lookup_and_evaluate
value = obj[key]
case value
when Proc
# call the proc
value = (value.arity == 0) ? value.call : value.call(self)
# memozie if possible
obj[key] = value if obj.respond_to?(:[]=)
end
value
end
def harden(value)
value = value.to_liquid
value.context = self if value.respond_to?(:context=)
return value
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
+1101 -416
View File
File diff suppressed because it is too large Load Diff
+61 -48
View File
@@ -1,33 +1,8 @@
=begin
LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true,
'false' => false,
'blank' => :blank?,
'empty' => :empty?
}
# Parser for context#[] method. Generated through ragel from parser.rl
# Only modify parser.rl. Run rake ragel afterwards to generate this file.
#
#VERBOSE=true
def resolve(key)
if LITERALS.key?(key)
LITERALS[key]
else
case key
when /^'(.*)'$/ # Single quoted strings
$1
when /^"(.*)"$/ # Double quoted strings
$1
when /^(-?\d+)$/ # Integer and floats
$1.to_i
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /^(-?\d[\d\.]+)$/ # Floats
$1.to_f
else
variable(key)
end
end
end
=end
%%{
machine fsm;
@@ -39,11 +14,14 @@
emit(:lookup, :instruction, nil, tokens)
}
action call {
emit(:call, :instruction, nil, tokens)
}
action range {
emit(:range, :instruction, nil, tokens)
}
var = [a-zA-Z][0-9A-Za-z_]+;
constants = ( "true" | "false" | "nil" | "null" );
# strings
string = "\"" any* "\"" | "'" any* "'";
@@ -51,41 +29,73 @@
# nothingness
nil = "nil" | "null" ;
# numbers
integer = ('+'|'-')? digit+;
float = ('+'|'-')? digit+ '.' digit+;
# simple values
primitive = (
integer >mark %{ emit(:id, :integer, Integer(data[mark..p-1]), tokens) } |
integer >mark %{ emit(:id, :integer, Integer(data[mark..p-1]), tokens) } |
float >mark %{ emit(:id, :float, Float(data[mark..p-1]), tokens) } |
float >mark %{ emit(:id, :float, Float(data[mark..p-1]), tokens) } |
nil %{ emit(:id, :nil, nil, tokens) } |
"true" %{ emit(:id, :bool, true, tokens) } |
"false" %{ emit(:id, :bool, false, tokens)} |
nil %{ emit(:id, :nil, nil, tokens) } |
"true" %{ emit(:id, :bool, true, tokens) } |
"false" %{ emit(:id, :bool, false, tokens)} |
string >mark %{ emit(:id, :string, data[mark+1..p-2], tokens) }
string >mark %{ emit(:id, :string, data[mark+1..p-2], tokens) }
);
constants = ( "true" | "false" | "nil" | "null" );
entity = (
((alpha [A-Za-z0-9_]*) - (constants)) >mark %{
((alpha [A-Za-z0-9_\-]*) - (constants)) >mark %{
emit(:id, :label, data[mark..p-1], tokens)
emit(:lookup, :variable, nil, tokens)
}
);
# Because of recursion we cannot immediatly resolve the content of this in
# the current grammar. We simply re-invoke the parser here to descend into
# the substring
recur = (
(any+ - ']') >mark %{
self.parse(data[mark..p-1], tokens)
}
);
expr = (
entity |
primitive |
"(" (primitive | entity) ".." (primitive | entity) <: ")" %range |
"[" recur "]" %lookup
);
hash_accessors = (
"[" recur "]" %call |
".first" %{
emit(:buildin, :symbol, "first", tokens)
} |
".last" %{
emit(:buildin, :symbol, "last", tokens)
} |
".size" %{
emit(:buildin, :symbol, "size", tokens)
} |
"." ((alpha [A-Za-z0-9_\-]*) - ("first"|"last"|"size")) >mark %{
emit(:id, :label, data[mark..p-1], tokens)
emit(:call, :variable, nil, tokens)
}
);
main := (
entity |
primitive |
"(" (primitive | entity) ".." (primitive | entity) <: ")" %range |
"[" (primitive | entity) "]" %lookup
expr <: (hash_accessors)*
);
}%%
@@ -97,16 +107,19 @@ module Liquid
%% write data;
def self.emit(sym, type, data, tokens)
puts "emitting: #{type} #{sym} -> #{data.inspect}"
puts "emitting: #{type} #{sym} -> #{data.inspect}" if $VERBOSE
tokens.push [sym, data]
end
def self.parse(data)
def self.parse(data, tokens = [])
puts "--> self.parse with #{data.inspect}, #{tokens.inspect}" if $VERBOSE
eof = data.length
tokens = []
%% write init;
%% write exec;
puts "<-- #{tokens.inspect}" if $VERBOSE
return tokens
end
end