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