2003/06/05 00:12:17
[org.ibex.core.git] / src / org / xwt / js / ForthBlock.java
index 6946258..43607be 100644 (file)
@@ -5,318 +5,287 @@ import org.xwt.util.*;
 import java.io.*;
 
 /** a block of Forth bytecode */
-class ForthBlock implements Tokens {
+class ForthBlock implements OpCodes, Tokens {
 
-    /*
-     *  Each instruction is an opcode and an optional literal literal.
-     */
+    int line;
+    String sourceName;
+    int[] op = new int[10];
+    Object[] arg = new Object[10];
+    int size = 0;
 
-       // opcodes:
-       public static final byte arithmetic = -1;  //                               -- arithmetic operators from parser
-       public static final byte LITERAL = -2;     // < String | Number | null >    -- push a literal onto the stack
-       public static final byte ARRAY = -3;       // < size >                      -- create a new array of size <size>
-       public static final byte OBJECT = -4;      //                               -- push an empty object onto the stack
-       public static final byte FUNCTION = -5;    // < bytecode_block >            -- push a new instance of a function with the given bytecode
-       public static final byte DECLARE = -6;     // < name >                      -- declare <name> in the current scope
-       public static final byte THIS = -7;        //                               -- push the topmost non-transparent scope onto the stack
-       public static final byte GET = -8;         //                               -- get stack[0] from stack[1]
-       public static final byte GET_PRESERVE = -80;         //                               -- get stack[0] from stack[1]
-       public static final byte PUT = -9;         //                               -- put stack[1] to key stack[0] on stack[2]; leaves object on the stack
-       public static final byte JT = -13;         // < relative_address >          -- pop the stack; if true, jump to <relative_address>
-       public static final byte JF = -21;         // < relative_address >          -- pop the stack; if false, jump to <relative_address>
-       public static final byte JMP = -22;        // < relative_address >          -- jump to <relative_address>
-        static public final byte POP = -14;        //                               -- discard the top element on the stack
+    public ForthBlock(int line, String sourceName) { this.line = line; this.sourceName = sourceName; }
+    public ForthBlock(int line, String sourceName, int op_, Object arg_) { this(line, sourceName); add(op_, arg_); }
 
-       public static final byte CALL = -15;       // < numargs >                   -- call stack[0] with the topmost <numargs> values as arguments
-
-       public static final byte PUSHKEYS = -19;    //                               -- ??
-       public static final byte EXPR = -20;       //                               -- transitional
-       public static final byte SWAP = -23;       //                               -- transitional
-       public static final byte SCOPE = -30;       //                               -- transitional
-       public static final byte LOOP = -40;       //                               -- transitional
-       public static final byte DUP = -50;       //                               -- transitional
-       public static final byte LABEL = -60;       //                               -- transitional
-
-       int line;
-       String sourceName;
-       int[] op = new int[10];
-       Object[] arg = new Object[10];
-       int size = 0;
-
-       public ForthBlock(int line, String sourceName) { this.line = line; this.sourceName = sourceName; }
-       public ForthBlock(int line, String sourceName, int op_, Object arg_) { this(line, sourceName); add(op_, arg_); }
-
-       public int size() { return size; }
-       public void set(int pos, int op_, Object arg_) { op[pos] = op_; arg[pos] = arg_; }
-       public ForthBlock add(int op_) { return add(op_, null); }
-       public ForthBlock add(int op_, Object arg_) {
-           if (size == op.length - 1) {
-               int[] op2 = new int[op.length * 2]; System.arraycopy(op, 0, op2, 0, op.length); op = op2;
-               Object[] arg2 = new Object[op.length * 2]; System.arraycopy(arg, 0, arg2, 0, arg.length); arg = arg2;
-           }
-           op[size] = op_;
-           arg[size] = arg_;
-           size++;
-           return this;
+    public int size() { return size; }
+    public void set(int pos, int op_, Object arg_) { op[pos] = op_; arg[pos] = arg_; }
+    public ForthBlock add(int op_) { return add(op_, null); }
+    public ForthBlock add(int op_, Object arg_) {
+       if (size == op.length - 1) {
+           int[] op2 = new int[op.length * 2]; System.arraycopy(op, 0, op2, 0, op.length); op = op2;
+           Object[] arg2 = new Object[op.length * 2]; System.arraycopy(arg, 0, arg2, 0, arg.length); arg = arg2;
        }
+       op[size] = op_;
+       arg[size] = arg_;
+       size++;
+       return this;
+    }
        
-       public Object eval(final JS.Scope s) throws ControlTransferException, JS.Exn {
-           return eval(s, new Stack());
-       }
-       public Object eval(final JS.Scope s, Stack t) throws ControlTransferException {
-           for(int i=0; i<size; i++)
-               switch(op[i]) {
-               case LABEL: break; // FIXME
-               case LITERAL: t.push(arg[i]); break;
-               case OBJECT: t.push(new JS.Obj()); break;
-               case ARRAY: t.push(new JS.Array(JS.toNumber(arg[i]).intValue())); break;
-               case DECLARE: s.declare((String)t.pop()); break;
-               case THIS: t.push(s); break;   // FIXME: transparents
-               case JT: if (JS.toBoolean(t.pop())) i += JS.toNumber(arg[i]).intValue() - 1; break;
-               case JF: if (!JS.toBoolean(t.pop())) i += JS.toNumber(arg[i]).intValue() - 1; break;
-               case JMP: i += JS.toNumber(arg[i]).intValue() - 1; break;
-               case POP: t.pop(); break;
-               case SWAP: t.swap(); break;
-               case DUP: t.push(t.peek()); break;
-               case NOP: break;
-               case EXPR: t.push(((ForthBlock)arg[i]).eval(s)); break;
-               case SCOPE: t.push(((ForthBlock)arg[i]).eval(new JS.Scope(s), t)); break;
+    public Object eval(final JS.Scope s) throws ControlTransferException, JS.Exn { return eval(s, new Stack()); }
+    public Object eval(final JS.Scope s, Stack t) throws ControlTransferException {
+       for(int i=0; i<size; i++)
+           switch(op[i]) {
+           case LABEL: break; // FIXME
+           case LITERAL: t.push(arg[i]); break;
+           case OBJECT: t.push(new JS.Obj()); break;
+           case ARRAY: t.push(new JS.Array(JS.toNumber(arg[i]).intValue())); break;
+           case DECLARE: s.declare((String)t.pop()); break;
+           case OpCodes.THIS: t.push(s); break;   // FIXME: transparents
+           case JT: if (JS.toBoolean(t.pop())) i += JS.toNumber(arg[i]).intValue() - 1; break;
+           case JF: if (!JS.toBoolean(t.pop())) i += JS.toNumber(arg[i]).intValue() - 1; break;
+           case JMP: i += JS.toNumber(arg[i]).intValue() - 1; break;
+           case POP: t.pop(); break;
+           case SWAP: t.swap(); break;
+           case DUP: t.push(t.peek()); break;
+           case NOP: break;
+           case EXPR: t.push(((ForthBlock)arg[i]).eval(s)); break;
+           case SCOPE: t.push(((ForthBlock)arg[i]).eval(new JS.Scope(s), t)); break;
 
-               case ASSERT: if (!JS.toBoolean(t.pop())) throw new EvaluatorException(line, sourceName, "assertion failed"); break;
-               case RETURN: throw new ReturnException(t.pop());
-               case THROW: throw new JS.Exn(t.pop());
+           case ASSERT: if (!JS.toBoolean(t.pop())) throw new EvaluatorException(line, sourceName, "assertion failed"); break;
+           case RETURN: throw new ReturnException(t.pop());
+           case THROW: throw new JS.Exn(t.pop());
 
-               case TRY: break;
-               case INSTANCEOF: break;
-               case TYPEOF: break;
-               case PUSHKEYS: {
-                   Object o = t.peek();
-                   Object[] keys = ((JS)o).keys();
-                   JS.Array a = new JS.Array();
-                   a.setSize(keys.length);
-                   for(int j=0; j<keys.length; j++) a.setElementAt(keys[j], j);
-                   t.push(a);
-                   break;
-               }
+           case TRY: break;
+           case INSTANCEOF: break;
+           case TYPEOF: break;
+           case PUSHKEYS: {
+               Object o = t.peek();
+               Object[] keys = ((JS)o).keys();
+               JS.Array a = new JS.Array();
+               a.setSize(keys.length);
+               for(int j=0; j<keys.length; j++) a.setElementAt(keys[j], j);
+               t.push(a);
+               break;
+           }
 
-               case Lexer.BITNOT: t.push(new Long(~JS.toLong(t.pop()))); break;
-               case Lexer.BANG: t.push(new Boolean(!JS.toBoolean(t.pop()))); break;
+           case Lexer.BITNOT: t.push(new Long(~JS.toLong(t.pop()))); break;
+           case Lexer.BANG: t.push(new Boolean(!JS.toBoolean(t.pop()))); break;
                    
-               case Lexer.BREAK: {
-                   // FIXME: make sure this can only appear in proper places
-                   return Boolean.FALSE;
-               }
-               case Lexer.CONTINUE: {
-                   // FIXME: make sure this can only appear in proper places
-                   return Boolean.TRUE;
-               }
-               case LOOP: {
-                   ForthBlock loop = (ForthBlock)arg[i];
-                   Stack t2 = new Stack();
-                   t2.push(Boolean.TRUE);
-                   while (true) {
-                       Boolean result;
-                       try {
-                           result = (Boolean)loop.eval(new JS.Scope(s), t2);
-                       } catch (ContinueException c) {
-                           result = Boolean.TRUE;
-                       } catch (BreakException b) {
-                           result = Boolean.FALSE;
-                       }
-                       if (result == Boolean.FALSE) break;
-                       t2 = new Stack();
-                       t2.push(Boolean.FALSE);
+           case Lexer.BREAK: {
+               // FIXME: make sure this can only appear in proper places
+               return Boolean.FALSE;
+           }
+           case Lexer.CONTINUE: {
+               // FIXME: make sure this can only appear in proper places
+               return Boolean.TRUE;
+           }
+           case LOOP: {
+               ForthBlock loop = (ForthBlock)arg[i];
+               Stack t2 = new Stack();
+               t2.push(Boolean.TRUE);
+               while (true) {
+                   Boolean result;
+                   try {
+                       result = (Boolean)loop.eval(new JS.Scope(s), t2);
+                   } catch (ContinueException c) {
+                       result = Boolean.TRUE;
+                   } catch (BreakException b) {
+                       result = Boolean.FALSE;
                    }
-                   break;
+                   if (result == Boolean.FALSE) break;
+                   t2 = new Stack();
+                   t2.push(Boolean.FALSE);
                }
+               break;
+           }
 
-               case PUT: {
-                   Object val = t.pop();
-                   Object key = t.pop();
-                   ((JS)t.peek()).put(key, val);
-                   t.push(val);
-                   break;
-               }
+           case PUT: {
+               Object val = t.pop();
+               Object key = t.pop();
+               ((JS)t.peek()).put(key, val);
+               t.push(val);
+               break;
+           }
 
-               case GET: {
-                   Object v = t.pop();
-                   Object o = t.pop();
-                   t.push(doGet(o, v));
-                   break;
-               }
-               case GET_PRESERVE: {
-                   Object v = t.pop();
-                   Object o = t.peek();
-                   t.push(v);
-                   t.push(doGet(o, v));
-                   break;
-               }
+           case GET: {
+               Object v = t.pop();
+               Object o = t.pop();
+               t.push(doGet(o, v));
+               break;
+           }
+
+           case GET_PRESERVE: {
+               Object v = t.pop();
+               Object o = t.peek();
+               t.push(v);
+               t.push(doGet(o, v));
+               break;
+           }
                    
-               case CALL: {
-                   JS.Array arguments = new JS.Array();
-                   int numArgs = JS.toNumber(arg[i]).intValue();
-                   arguments.setSize(numArgs);
-                   for(int j=numArgs - 1; j >= 0; j--) arguments.setElementAt(t.pop(), j);
-                   JS.Function f = (JS.Function)t.pop();
-                   if (f == null) throw new JS.Exn(new EvaluatorException(line, sourceName, "attempted to call null"));
-                   t.push(f.call(arguments));
-                   break;
-               }
+           case CALL: {
+               JS.Array arguments = new JS.Array();
+               int numArgs = JS.toNumber(arg[i]).intValue();
+               arguments.setSize(numArgs);
+               for(int j=numArgs - 1; j >= 0; j--) arguments.setElementAt(t.pop(), j);
+               JS.Function f = (JS.Function)t.pop();
+               if (f == null) throw new JS.Exn(new EvaluatorException(line, sourceName, "attempted to call null"));
+               t.push(f.call(arguments));
+               break;
+           }
 
-               case FUNCTION: {
-                   final ForthBlock myBytes = (ForthBlock)arg[i];
-                   t.push(new JS.Function() {
-                           public String toString() { return sourceName + ":" + line; }
-                           public String getSourceName() throws JS.Exn { return sourceName; }
-                           public Object _call(final JS.Array args) throws JS.Exn {
-                               Function save = JS.getCurrentFunction();
-                               JS.currentFunction.put(java.lang.Thread.currentThread(), this);
-                               JS.Scope scope = new JS.Scope(s) {
-                                       // FIXME
-                                       public String getSourceName() { return sourceName; }
-                                       public Object get(Object key) throws JS.Exn {
-                                           if (key.equals("trapee")) return org.xwt.Trap.currentTrapee();
-                                           else if (key.equals("cascade")) return org.xwt.Trap.cascadeFunction;
-                                           return super.get(key);
-                                       }
-                                   };
-                               Stack t0 = new Stack();
-                               t0.push(args);
-                               try {
-                                   return myBytes.eval(scope, t0);
-                               } catch (ReturnException r) {
-                                   return r.retval;
-                               } catch (ControlTransferException c) {
-                                   throw new EvaluatorException(line, sourceName, "error, ControlTransferException tried to leave a function: " + c);
-                               } finally {
-                                   if (save == null) JS.currentFunction.remove(java.lang.Thread.currentThread());
-                                   else JS.currentFunction.put(java.lang.Thread.currentThread(), save);
-                               }
+           case OpCodes.FUNCTION: {
+               final ForthBlock myBytes = (ForthBlock)arg[i];
+               t.push(new JS.Function() {
+                       public String toString() { return sourceName + ":" + line; }
+                       public String getSourceName() throws JS.Exn { return sourceName; }
+                       public Object _call(final JS.Array args) throws JS.Exn {
+                           Function save = JS.getCurrentFunction();
+                           JS.currentFunction.put(java.lang.Thread.currentThread(), this);
+                           JS.Scope scope = new JS.Scope(s) {
+                                   // FIXME
+                                   public String getSourceName() { return sourceName; }
+                                   public Object get(Object key) throws JS.Exn {
+                                       if (key.equals("trapee")) return org.xwt.Trap.currentTrapee();
+                                       else if (key.equals("cascade")) return org.xwt.Trap.cascadeFunction;
+                                       return super.get(key);
+                                   }
+                               };
+                           Stack t0 = new Stack();
+                           t0.push(args);
+                           try {
+                               return myBytes.eval(scope, t0);
+                           } catch (ReturnException r) {
+                               return r.retval;
+                           } catch (ControlTransferException c) {
+                               throw new EvaluatorException(line, sourceName, "error, ControlTransferException tried to leave a function: " + c);
+                           } finally {
+                               if (save == null) JS.currentFunction.remove(java.lang.Thread.currentThread());
+                               else JS.currentFunction.put(java.lang.Thread.currentThread(), save);
                            }
-                       });
-                   break;
-               }
+                       }
+                   });
+               break;
+           }
 
-               case Lexer.INC: case Lexer.DEC: {
-                   boolean isPrefix = JS.toBoolean(arg[i]);
-                   Object key = t.pop();
-                   JS obj = (JS)t.pop();
-                   Number num = JS.toNumber(obj.get(key));
-                   Number val = new Double(op[i] == Lexer.INC ? num.doubleValue() + 1.0 : num.doubleValue() - 1.0);
-                   obj.put(key, val);
-                   t.push(isPrefix ? val : num);
-                   break;
-               }
+           case Lexer.INC: case Lexer.DEC: {
+               boolean isPrefix = JS.toBoolean(arg[i]);
+               Object key = t.pop();
+               JS obj = (JS)t.pop();
+               Number num = JS.toNumber(obj.get(key));
+               Number val = new Double(op[i] == Lexer.INC ? num.doubleValue() + 1.0 : num.doubleValue() - 1.0);
+               obj.put(key, val);
+               t.push(isPrefix ? val : num);
+               break;
+           }
 
-               default: {
-                   Object right = t.pop();
-                   Object left = t.pop();
-                   switch(op[i]) {
+           default: {
+               Object right = t.pop();
+               Object left = t.pop();
+               switch(op[i]) {
 
-                   case Lexer.BITOR: t.push(new Long(JS.toLong(left) | JS.toLong(right))); break;
-                   case Lexer.BITXOR: t.push(new Long(JS.toLong(left) ^ JS.toLong(right))); break;
-                   case Lexer.BITAND: t.push(new Long(JS.toLong(left) & JS.toLong(right))); break;
+               case Lexer.BITOR: t.push(new Long(JS.toLong(left) | JS.toLong(right))); break;
+               case Lexer.BITXOR: t.push(new Long(JS.toLong(left) ^ JS.toLong(right))); break;
+               case Lexer.BITAND: t.push(new Long(JS.toLong(left) & JS.toLong(right))); break;
 
-                   case Lexer.ADD: {
-                       Object l = left;
-                       Object r = right;
-                       if (l instanceof String || r instanceof String) {
-                           if (l == null) l = "null";
-                           if (r == null) r = "null";
-                           if (l instanceof Number && ((Number)l).doubleValue() == ((Number)l).longValue())
-                               l = new Long(((Number)l).longValue());
-                           if (r instanceof Number && ((Number)r).doubleValue() == ((Number)r).longValue())
-                               r = new Long(((Number)r).longValue());
-                           t.push(l.toString() + r.toString()); break;
-                       }
-                       t.push(new Double(JS.toDouble(l) + JS.toDouble(r))); break;
+               case Lexer.ADD: {
+                   Object l = left;
+                   Object r = right;
+                   if (l instanceof String || r instanceof String) {
+                       if (l == null) l = "null";
+                       if (r == null) r = "null";
+                       if (l instanceof Number && ((Number)l).doubleValue() == ((Number)l).longValue())
+                           l = new Long(((Number)l).longValue());
+                       if (r instanceof Number && ((Number)r).doubleValue() == ((Number)r).longValue())
+                           r = new Long(((Number)r).longValue());
+                       t.push(l.toString() + r.toString()); break;
                    }
+                   t.push(new Double(JS.toDouble(l) + JS.toDouble(r))); break;
+               }
                        
-                   case Lexer.SUB: t.push(new Double(JS.toDouble(left) - JS.toDouble(right))); break;
-                   case Lexer.MUL: t.push(new Double(JS.toDouble(left) * JS.toDouble(right))); break;
-                   case Lexer.DIV: t.push(new Double(JS.toDouble(left) / JS.toDouble(right))); break;
-                   case Lexer.MOD: t.push(new Double(JS.toDouble(left) % JS.toDouble(right))); break;
+               case Lexer.SUB: t.push(new Double(JS.toDouble(left) - JS.toDouble(right))); break;
+               case Lexer.MUL: t.push(new Double(JS.toDouble(left) * JS.toDouble(right))); break;
+               case Lexer.DIV: t.push(new Double(JS.toDouble(left) / JS.toDouble(right))); break;
+               case Lexer.MOD: t.push(new Double(JS.toDouble(left) % JS.toDouble(right))); break;
                        
-                   case Lexer.LSH: t.push(new Long(JS.toLong(left) << JS.toLong(right))); break;
-                   case Lexer.RSH: t.push(new Long(JS.toLong(left) >> JS.toLong(right))); break;
-                   case Lexer.URSH: t.push(new Long(JS.toLong(left) >>> JS.toLong(right))); break;
+               case Lexer.LSH: t.push(new Long(JS.toLong(left) << JS.toLong(right))); break;
+               case Lexer.RSH: t.push(new Long(JS.toLong(left) >> JS.toLong(right))); break;
+               case Lexer.URSH: t.push(new Long(JS.toLong(left) >>> JS.toLong(right))); break;
                        
-                       // FIXME: these need to work on strings
-                   case Lexer.LT: t.push(JS.toDouble(left) < JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
-                   case Lexer.LE: t.push(JS.toDouble(left) <= JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
-                   case Lexer.GT: t.push(JS.toDouble(left) > JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
-                   case Lexer.GE: t.push(JS.toDouble(left) >= JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
+                   // FIXME: these need to work on strings
+               case Lexer.LT: t.push(JS.toDouble(left) < JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
+               case Lexer.LE: t.push(JS.toDouble(left) <= JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
+               case Lexer.GT: t.push(JS.toDouble(left) > JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
+               case Lexer.GE: t.push(JS.toDouble(left) >= JS.toDouble(right) ? Boolean.TRUE : Boolean.FALSE); break;
                    
-                   case Lexer.EQ:
-                   case Lexer.NE: {
-                       // FIXME: should use Javascript coercion-equality rules
-                       Object l = left;
-                       Object r = right;
-                       boolean ret;
-                       if (l == null) { Object tmp = r; r = l; l = tmp; }
-                       if (l == null && r == null) ret = true;
-                       else if (l instanceof Boolean) ret = new Boolean(JS.toBoolean(r)).equals(l);
-                       else if (l instanceof Number) ret = JS.toNumber(r).doubleValue() == JS.toNumber(l).doubleValue();
-                       else if (l instanceof String) ret = r != null && l.equals(r.toString());
-                       else ret = l.equals(r);
-                       t.push(new Boolean(op[i] == Lexer.EQ ? ret : !ret)); break;
-                   }
-
-                   default: throw new Error("unknown opcode " + op[i]);
-                   } }
+               case Lexer.EQ:
+               case Lexer.NE: {
+                   // FIXME: should use Javascript coercion-equality rules
+                   Object l = left;
+                   Object r = right;
+                   boolean ret;
+                   if (l == null) { Object tmp = r; r = l; l = tmp; }
+                   if (l == null && r == null) ret = true;
+                   else if (l instanceof Boolean) ret = new Boolean(JS.toBoolean(r)).equals(l);
+                   else if (l instanceof Number) ret = JS.toNumber(r).doubleValue() == JS.toNumber(l).doubleValue();
+                   else if (l instanceof String) ret = r != null && l.equals(r.toString());
+                   else ret = l.equals(r);
+                   t.push(new Boolean(op[i] == Lexer.EQ ? ret : !ret)); break;
                }
-           if (t.size() != 1) {
-               throw new EvaluatorException(line, sourceName, "eval() terminated with " + t.size() + " elements on the stack; one expected");
+
+               default: throw new Error("unknown opcode " + op[i]);
+               } }
            }
-           return t.pop();
+       if (t.size() != 1) {
+           throw new EvaluatorException(line, sourceName, "eval() terminated with " + t.size() + " elements on the stack; one expected");
        }
+       return t.pop();
+    }
 
-       public Object doGet(final Object o, final Object v) {
-           if (o == null) throw new EvaluatorException(line, sourceName, "tried to get property \"" + v + "\" from the null value");
-           if (o instanceof String) {
-               if (v.equals("length")) return new Integer(((String)o).length());
-               else if (v.equals("substring")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           if (args.length() == 1) return ((String)o).substring(JS.toNumber(args.elementAt(0)).intValue());
-                           else if (args.length() == 2) return ((String)o).substring(JS.toNumber(args.elementAt(0)).intValue(),
-                                                                                     JS.toNumber(args.elementAt(1)).intValue());
-                           else throw new Error("String.substring() can only take one or two arguments");
-                       }
-                   };
-               else if (v.equals("toLowerCase")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           return ((String)o).toLowerCase();
-                       } };
-               else if (v.equals("toUpperCase")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           return ((String)o).toString().toUpperCase();
-                       } };
-               else if (v.equals("charAt")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           return ((String)o).charAt(JS.toNumber(args.elementAt(0)).intValue()) + "";
-                       } };
-               else if (v.equals("lastIndexOf")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           if (args.length() != 1) return null;
-                           return new Integer(((String)o).lastIndexOf(args.elementAt(0).toString()));
-                       } };
-               else if (v.equals("indexOf")) return new JS.Function() {
-                       public Object _call(JS.Array args) {
-                           if (args.length() != 1) return null;
-                           return new Integer(((String)o).indexOf(args.elementAt(0).toString()));
-                       } };
-               throw new Error("Not Implemented: propery " + v + " on String objects");
-           } else if (o instanceof Boolean) {
-               throw new Error("Not Implemented: properties on Boolean objects");
-           } else if (o instanceof Number) {
-               Log.log(this, "Not Implemented: properties on Number objects");
-               return null;
-               //throw new Error("Not Implemented: properties on Number objects");
-           } else if (o instanceof JS) {
-               return ((JS)o).get(v);
-           }
+    public Object doGet(final Object o, final Object v) {
+       if (o == null) throw new EvaluatorException(line, sourceName, "tried to get property \"" + v + "\" from the null value");
+       if (o instanceof String) {
+           if (v.equals("length")) return new Integer(((String)o).length());
+           else if (v.equals("substring")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       if (args.length() == 1) return ((String)o).substring(JS.toNumber(args.elementAt(0)).intValue());
+                       else if (args.length() == 2) return ((String)o).substring(JS.toNumber(args.elementAt(0)).intValue(),
+                                                                                 JS.toNumber(args.elementAt(1)).intValue());
+                       else throw new Error("String.substring() can only take one or two arguments");
+                   }
+               };
+           else if (v.equals("toLowerCase")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       return ((String)o).toLowerCase();
+                   } };
+           else if (v.equals("toUpperCase")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       return ((String)o).toString().toUpperCase();
+                   } };
+           else if (v.equals("charAt")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       return ((String)o).charAt(JS.toNumber(args.elementAt(0)).intValue()) + "";
+                   } };
+           else if (v.equals("lastIndexOf")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       if (args.length() != 1) return null;
+                       return new Integer(((String)o).lastIndexOf(args.elementAt(0).toString()));
+                   } };
+           else if (v.equals("indexOf")) return new JS.Function() {
+                   public Object _call(JS.Array args) {
+                       if (args.length() != 1) return null;
+                       return new Integer(((String)o).indexOf(args.elementAt(0).toString()));
+                   } };
+           throw new Error("Not Implemented: propery " + v + " on String objects");
+       } else if (o instanceof Boolean) {
+           throw new Error("Not Implemented: properties on Boolean objects");
+       } else if (o instanceof Number) {
+           Log.log(this, "Not Implemented: properties on Number objects");
            return null;
+           //throw new Error("Not Implemented: properties on Number objects");
+       } else if (o instanceof JS) {
+           return ((JS)o).get(v);
        }
+       return null;
+    }
 
     static class Stack {
        public Object[] os = new Object[256];