Hi,
I've been away for a while, and I like an interesting problem to get me
back into the swing of things, so this quiz came at the perfect time for
me - I've had a lot of fun with it :) I started coding it on Monday
(before I looked at other solutions of course) and, though I'm still not
entirely happy with it, I'd better post it or I'll be still fiddling
with it long after Wednesday.
It's a fairly brief solution, and I decided to use _why's Sandbox
library which looked pretty intriguing. This is my first run with
Sandbox, though, so I'm probably misusing it a bit.
The basic idea is to keep the core classes in the main interpreter as
they are, and use a sandbox in which the the core is gutted to execute
the block. The main file (sexpr.rb) simply defines the Sxp module. When
Sxp.sxp is called, it sets up a new sandbox, passes in the block (via an
instance variable - any better way to do this?) and runs the second
script ('sandboxed.rb') which does the core mods and then calls the
block.
In the sandbox, most calls go through one of the method_missings, which
are set up so that the calls passing through will build up an array
representing the sexpr. This is the result of the last statement in
sandboxed.rb, and so the result of the Sandbox#load call.
It's not without it's problems - neither Strings nor Floats work
properly, and instead will be evaluated normally and (usually) the
result placed in the sexpr. This has some 'interesting' side effects:
$ ruby -rsexpr -e 'Sxp.sxpp { 3 + 3.0 + 3.0 }'
[:+, [:+, 3, 3.0], 3.0]
$ ruby -rsexpr -e 'p Sxp.sxp { 3.0 + 3.0 + 3 }'
[:+, 6.0, 3]
$ ruby -rsexpr -e 'Sxp.sxpp { 3.0 + 3.0 * 3 }'
12.0 # oops :)
$ ruby -rsexpr -e 'Sxp.sxpp { [:a] + [3.0 * 3] }'
[:+, [:a], [9.0]]
$ ruby -rsexpr -e 'Sxp.sxpp { [:a] + terms(3.42 * 3) }'
[:+, [:a], [:terms, 10.26]]
I think the problem here is related to the fact that object classes are
preserved across the sandbox boundary, and the proc passed keeps it's
original scope. This has an effect the other way too - you'll often get
'gutted' arrays in the returned sexpr array...
It does pass the tests posted on the list, though, and for the basic
functional stuff it's probably not too bad.
(Also, Sandbox is *way cool* - thanks _why & MenTaL :). It's gotta
become part of the distribution in the near future).
# ---[sandboxed.rb]---
class Object ; alias :__instance_eval :instance_eval ; end
class Array ; alias :__each :each ; end
[Object, Kernel, Symbol, Fixnum, Bignum, Float, NilClass, FalseClass,
TrueClass, Hash, Array, String].__each do |clz|
clz.class_eval do
instance_methods.__each do |m|
undef_method m unless /^__|^inspect$|^to_(s(?:tr)?|a(?:ry)?)$/.match(m)
end
def method_missing(sym, *args); [sym, self, *args]; end
def to_ary; [self]; end # needed by every class in this world
end
end
# A special method_missing on the main object handles 'function' calls
class << self; def method_missing(sym, *args); [sym, *args]; end; end
__instance_eval &@blk
__END__
# ---[sexpr.rb]---
require 'sandbox'
module Sxp
class << self
def sxp(&blk)
sb = Sandbox.new
sb.main.instance_variable_set(:@blk,
blk || raise(LocalJumpError, "No block given"))
sb.load("#{File.dirname(__FILE__)}/sandboxed.rb")
end
def sxpp(&blk)
p(r = sxp(&blk)) || r
end
end
end
if $0 == __FILE__
require 'test/unit'
class Test::Unit::TestCase
def sxp(&blk)
Sxp.sxpp(&blk) # use the printing version
end
end
class ProvidedSxpTest < Test::Unit::TestCase
def test_sxp_01
assert_equal [:max, [:count, :name]], sxp{max(count(:name))}
end
def test_sxp_02
assert_equal [:count, [:+, 3, 7]], sxp{count(3+7)}
end
def test_sxp_03
assert_equal [:+, 3, :symbol], sxp{3+:symbol}
end
def test_sxp_04
assert_equal [:+, 3, [:count, :field]], sxp{3+count(:field) }
end
def test_sxp_05
assert_equal [:/, 7, :field], sxp{7/:field}
end
def test_sxp_06
assert_equal [:>, :field, 5], sxp{:field > 5}
end
def test_sxp_07
assert_equal 8, sxp{8}
end
def test_sxp_08
assert_equal [:==, :field1, :field2], sxp{:field1 == :field2}
end
def test_sxp_09
assert_raise(TypeError) { 7/:field }
end
def test_sxp_10
assert_raise(NoMethodError) { 7+count(:field) }
end
def test_sxp_11
assert_equal 11, 5+6
end
def test_sxp_12
assert_raise(NoMethodError) { :field > 5 }
end
def test_sxp_13
assert_equal [:+, 3, 'string'], sxp{3+'string'}
end
def test_sxp_14
assert_equal [:abs, [:factorial, 3]], sxp{3.factorial.abs}
end
def test_sxp_15
assert_raise(LocalJumpError) { sxp }
end
def test_sxp_16
assert_equal 3.0, sxp{3.0}
end
def test_sxp_17
assert_equal [:count, 3.0], sxp{count(3.0)}
end
# This test always fails right now, because string methods always get
# called regardless. This is the same with Floats, but apparently not
# on any immediate objects, or the standard Array / Hash classes,
# Bignum, and so on...
#
#def test_sxp_18
# assert_equal [:+, 'longer', 'string'], sxp{'longer'+'string'}
#end
def test_sxp_19
assert_equal [:+, [1,2], [:*, {3=>4}, 1100000000]], sxp{[1,2]+{3=>4}*1100000000}
end
def test_sxp_20
assert_equal [:+, [1,2], [3,4]], sxp{[1,2]+[3,4]}
end
end
class SanderLandSxpTest < Test::Unit::TestCase
def test_more
assert_equal [:==,[:^, 2, 3], [:^, 1, 1]], sxp{ 2^3 == 1^1}
assert_equal [:==, 3.1415, 3] , sxp{3.0 + 0.1415 == 3}
assert_equal [:|, [:==, [:+, :hello, :world], :helloworld],
[:==, [:+, [:+, "hello", " "], "world"], "hello world"]] ,
sxp{ (:hello + :world == :helloworld) | ('hello' + ' ' + 'world' == 'hello world') }
assert_equal [:==, [:+, [:abs, [:factorial, 3]], [:*, [:factorial, 4], 42]],
[:+, [:+, 4000000, [:**, 2, 32]], [:%, 2.7, 1.1]]],
sxp{ 3.factorial.abs + 4.factorial * 42 == 4_000_000 + 2**32 + 2.7 % 1.1 }
end
end
class RobinStockerSxpTest < Test::Unit::TestCase
def test_number
assert_equal 8, sxp { 8 }
assert_equal [:+, 3, 4], sxp { 3 + 4 }
end
def test_environment
assert_equal [:-, 10, [:count, [:*, :field, 4]]],
sxp { 10 - count(:field * 4) }
assert_raise(TypeError) { 7 / :field }
assert_raise(NoMethodError) { 7 + count(:field) }
assert_equal 11, 5 + 6
assert_raise(NoMethodError) { :field > 5 }
end
end
end
__END__
--
Ross Bamford - rosco / roscopeco.REMOVE.co.uk