From: Ross Bamford Date: 2006-09-27T00:06:17+09:00 Subject: Re: [QUIZ][SOLUTION] Code to S-Exp (#95) 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