Skip to content

Commit

Permalink
Clean scoping mechanism, keeping proper linear parent chains.
Browse files Browse the repository at this point in the history
  • Loading branch information
blambeau committed Jun 13, 2012
1 parent 8373eb2 commit 37dfc72
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 79 deletions.
42 changes: 26 additions & 16 deletions lib/wlang/scope.rb
Expand Up @@ -4,38 +4,35 @@ class Scope
attr_reader :subject
attr_reader :parent

def initialize(subject, parent)
@subject, @parent = subject, parent
def initialize(subject)
@subject = subject
@parent = nil
end

def self.null
@null ||= NullScope.new
end

def self.coerce(arg, parent = nil)
return arg if Scope===arg && parent.nil?
clazz = case arg
when Binding then BindingScope
when Scope then ProxyScope
when Proc then ProcScope
def self.coerce(arg)
case arg
when Binding then BindingScope.new(arg)
when Scope then arg
when Proc then ProcScope.new(arg)
else
ObjectScope
ObjectScope.new(arg)
end
clazz.new(arg, parent)
end

def self.chain(scopes)
scopes.compact.inject(nil){|parent,child|
Scope.coerce(child, parent)
}
scopes.compact.inject(Scope.null){|p,c| p.push(c)}
end

def root
parent.nil? ? self : parent.root
parent ? parent.root : self
end

def push(x)
Scope.coerce(x, self)
append(Scope.coerce(x))
end

def pop
Expand All @@ -61,14 +58,27 @@ def evaluate(expr, *default)

protected

def append(x)
x.prepend(self)
end

def prepend(x)
newp = parent ? parent.prepend(x) : x
dup.with_parent!(newp)
end

def with_parent!(p)
@parent = p
self
end

def safe_parent
parent || Scope.null
end

end # class Scope
end # module WLang
require 'wlang/scope/null_scope'
require 'wlang/scope/proxy_scope'
require 'wlang/scope/object_scope'
require 'wlang/scope/binding_scope'
require 'wlang/scope/proc_scope'
20 changes: 11 additions & 9 deletions lib/wlang/scope/null_scope.rb
Expand Up @@ -3,15 +3,7 @@ class Scope
class NullScope < Scope

def initialize
super(nil,nil)
end

def push(x)
Scope.coerce(x)
end

def pop
nil
super(nil)
end

def fetch(key)
Expand All @@ -23,6 +15,16 @@ def inspect
end
alias :to_s :inspect

protected

def append(x)
x
end

def prepend(x)
x
end

end # class NullScope
end # class Scope
end # module WLang
18 changes: 0 additions & 18 deletions lib/wlang/scope/proxy_scope.rb

This file was deleted.

4 changes: 2 additions & 2 deletions lib/wlang/template.rb
Expand Up @@ -44,8 +44,8 @@ def to_ast
compiler.to_ast(template_content)
end

def call(locs = {}, buffer = '')
scope = WLang::Scope.chain([locals, locs])
def call(locals = {}, buffer = '')
scope = WLang::Scope.chain([self.locals, locals])
dialect_instance.dup.render compiled, scope, buffer
end
alias :render :call
Expand Down
4 changes: 4 additions & 0 deletions spec/integration/scoping/hello.wlang
@@ -0,0 +1,4 @@
---
who: world
---
Hello ${who}!
16 changes: 16 additions & 0 deletions spec/integration/scoping/test_scoping_rules.rb
@@ -0,0 +1,16 @@
require 'spec_helper'
module WLang
describe "WLang scoping rules" do

let(:template){ Template.new(Path.dir / 'hello.wlang') }

specify 'template locals are used by default' do
template.render.should eq("Hello world!")
end

specify 'template locals are overriden by render locals' do
template.render(:who => "wlang").should eq("Hello wlang!")
end

end
end
2 changes: 1 addition & 1 deletion spec/unit/scope/test_binding_scope.rb
Expand Up @@ -10,7 +10,7 @@ class Scope
end

it 'delegates fetch to its parent when not found' do
scope = Scope.coerce(binding, Scope.coerce(:who => "World"))
scope = Scope.coerce(:who => "World").push(binding)
scope.fetch(:who).should eq("World")
end

Expand Down
4 changes: 2 additions & 2 deletions spec/unit/scope/test_chain.rb
@@ -1,8 +1,8 @@
module WLang
describe Scope, '.chain' do

it 'returns nil on empty chain' do
Scope.chain([]).should be_nil
it 'returns the NullScope on empty chain' do
Scope.chain([]).should eq(Scope.null)
end

it 'returns a single scope on singleton' do
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/scope/test_coerce.rb
Expand Up @@ -24,9 +24,9 @@ module WLang
Scope.coerce(s).should eq(s)
end

it 'builds ProxyScope on Scopes' do
it 'returns the Scope on a Scope' do
s = Scope.coerce({})
Scope.coerce(s, Scope.null).should be_a(Scope::ProxyScope)
Scope.coerce(s).should eq(s)
end

end # describe Scope
Expand Down
2 changes: 1 addition & 1 deletion spec/unit/scope/test_null_scope.rb
Expand Up @@ -14,7 +14,7 @@ class Scope
end

it 'returns pushed scope on push' do
pushed = ObjectScope.new(12, nil)
pushed = ObjectScope.new(12)
scope.push(pushed).should eq(pushed)
end

Expand Down
2 changes: 1 addition & 1 deletion spec/unit/scope/test_object_scope.rb
Expand Up @@ -24,7 +24,7 @@ def x.[](k); "World"; end
end

it 'delegates fetch to its parent when not found' do
scope = Scope.coerce(12, Scope.coerce({:who => "World"}))
scope = Scope.coerce({:who => "World"}).push(12)
scope.fetch(:who).should eq("World")
end

Expand Down
2 changes: 1 addition & 1 deletion spec/unit/scope/test_proc_scope.rb
Expand Up @@ -9,7 +9,7 @@ class Scope
end

it 'delegates fetch to its parent when not found' do
scope = Scope.coerce(lambda{ nil }, Scope.coerce({:who => "World"}))
scope = Scope.coerce({:who => "World"}).push(lambda{ nil })
scope.fetch(:who).should eq("World")
end

Expand Down
22 changes: 0 additions & 22 deletions spec/unit/scope/test_proxy_scope.rb

This file was deleted.

61 changes: 61 additions & 0 deletions spec/unit/scope/test_push.rb
@@ -0,0 +1,61 @@
require 'spec_helper'
module WLang
describe Scope, 'push' do

let(:x_scope){ Scope.coerce(:x) }

subject{ x_scope.push(pushed) }

before do
subject.should be_a(Scope)
end

after do
x_scope.parent.should be_nil
x_scope.subject.should eq(:x)
x_scope.root.should eq(x_scope)
end

context 'when pushing a simple value' do
let(:pushed){ :y }

it 'returns the scope with correct subject' do
subject.subject.should eq(:y)
end

it 'sets the parent correctly on created scope' do
subject.parent.should eq(x_scope)
end

it 'returns the correct root scope' do
subject.root.should eq(x_scope)
end
end

context 'when pushing another scope' do
let(:pushed){ Scope.coerce(:y).push(:z) }

it 'returns the scope with most specific subject' do
subject.subject.should eq(:z)
end

it 'rechains parents correctly' do
subject.parent.subject.should eq(:y)
subject.parent.parent.subject.should eq(:x)
subject.parent.parent.parent.should be_nil
end

it 'returns the correct root scope' do
subject.root.should eq(x_scope)
end

it 'does not touch the original scope' do
pushed.subject.should eq(:z)
pushed.parent.subject.should eq(:y)
pushed.parent.parent.should be_nil
pushed.root.should eq(pushed.parent)
end
end

end
end
7 changes: 3 additions & 4 deletions spec/unit/test_scope.rb
Expand Up @@ -2,12 +2,11 @@
module WLang
describe Scope do

let(:scope){ Scope.coerce({:who => "World"}, Scope.null) }
let(:scope){ Scope.coerce({:who => "World"}) }

it 'acts like a stack' do
s = scope
s.evaluate(:who).should eq("World")
lambda{ s.pop.evaluate(:who) }.should throw_symbol(:fail)
s = scope.push(:who => "World2")
s.evaluate(:who).should eq("World2")
s = s.pop
Expand All @@ -29,9 +28,9 @@ module WLang
end

it 'gives access to the root' do
scope.root.should eq(Scope.null)
scope.root.should eq(scope)
scope.with(:other => "World2") do |s|
s.root.should eq(Scope.null)
s.root.should eq(scope)
end
end

Expand Down

0 comments on commit 37dfc72

Please sign in to comment.