initial import
authoradam <adam@megacz.com>
Thu, 8 Jul 2004 10:06:03 +0000 (10:06 +0000)
committeradam <adam@megacz.com>
Thu, 8 Jul 2004 10:06:03 +0000 (10:06 +0000)
darcs-hash:20040708100603-5007d-2ae50921669b2582d11904ff4db5fbf6234f0414.gz

18 files changed:
src/org/ibex/js/ByteCodes.java [new file with mode: 0644]
src/org/ibex/js/Directory.java [new file with mode: 0644]
src/org/ibex/js/Interpreter.java [new file with mode: 0644]
src/org/ibex/js/JS.java [new file with mode: 0644]
src/org/ibex/js/JSArray.java [new file with mode: 0644]
src/org/ibex/js/JSDate.java [new file with mode: 0644]
src/org/ibex/js/JSExn.java [new file with mode: 0644]
src/org/ibex/js/JSFunction.java [new file with mode: 0644]
src/org/ibex/js/JSMath.java [new file with mode: 0644]
src/org/ibex/js/JSReflection.java [new file with mode: 0644]
src/org/ibex/js/JSRegexp.java [new file with mode: 0644]
src/org/ibex/js/JSScope.java [new file with mode: 0644]
src/org/ibex/js/Lexer.java [new file with mode: 0644]
src/org/ibex/js/Parser.java [new file with mode: 0644]
src/org/ibex/js/PropertyFile.java [new file with mode: 0644]
src/org/ibex/js/Stream.java [new file with mode: 0644]
src/org/ibex/js/Tokens.java [new file with mode: 0644]
src/org/ibex/js/Trap.java [new file with mode: 0644]

diff --git a/src/org/ibex/js/ByteCodes.java b/src/org/ibex/js/ByteCodes.java
new file mode 100644 (file)
index 0000000..e8170f1
--- /dev/null
@@ -0,0 +1,94 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+/**
+ *  Constants for the various JavaScript ByteCode operations.
+ *
+ *  Each instruction is an opcode and an optional literal literal;
+ *  some Tokens are also valid; see Tokens.java
+ */
+interface ByteCodes {
+
+    /** push the literal onto the stack */
+    public static final byte LITERAL = -2;
+
+    /** push a new array onto the stack with length equal to the literal */
+    public static final byte ARRAY = -3;         
+
+    /** push an empty object onto the stack */
+    public static final byte OBJECT = -4;        
+
+    /** create a new instance; literal is a reference to the corresponding ForthBlock */
+    public static final byte NEWFUNCTION = -5;      
+
+    /** if given a non-null argument declare its argument in the current scope and push
+        it to the stack, else, declares the element on the top of the stack and leaves it
+        there */
+    public static final byte DECLARE = -6;       
+
+    /** push a reference to the current scope onto the stack */
+    public static final byte TOPSCOPE = -7;
+
+    /** if given a null literal pop two elements off the stack; push stack[-1].get(stack[top])
+        else pop one element off the stack, push stack[top].get(literal) */
+    public static final byte GET = -8;           
+
+    /** push stack[-1].get(stack[top]) */
+    public static final byte GET_PRESERVE = -9; 
+
+    /** pop two elements off the stack; stack[-2].put(stack[-1], stack[top]); push stack[top] */
+    public static final byte PUT = -10;           
+
+    /** literal is a relative address; pop stacktop and jump if the value is true */
+    public static final byte JT = -11;           
+
+    /** literal is a relative address; pop stacktop and jump if the value is false */
+    public static final byte JF = -12;           
+
+    /** literal is a relative address; jump to it */
+    public static final byte JMP = -13;          
+
+    /** discard the top stack element */
+    static public final byte POP = -14;          
+
+    /** pop element; call stack[top](stack[-n], stack[-n+1]...) where n is the number of args to the function */
+    public static final byte CALL = -15;         
+
+    /** pop an element; push a JS.JSArray containing the keys of the popped element */
+    public static final byte PUSHKEYS = -16;     
+
+    /** push the top element down so that (arg) elements are on top of it; all other elements retain ordering */
+    public static final byte SWAP = -17;         
+
+    /** execute the bytecode block pointed to by the literal in a fresh scope with parentScope==THIS */
+    public static final byte NEWSCOPE = -18;        
+
+    /** execute the bytecode block pointed to by the literal in a fresh scope with parentScope==THIS */
+    public static final byte OLDSCOPE = -19;
+
+    /** push a copy of the top stack element */
+    public static final byte DUP = -20;          
+
+    /** a NOP; confers a label upon the following instruction */
+    public static final byte LABEL = -21;          
+
+    /** execute the ForthBlock pointed to by the literal until BREAK encountered; push TRUE onto the stack for the first iteration
+     *  and FALSE for all subsequent iterations */
+    public static final byte LOOP = -22;        
+     
+    /** similar effect a a GET followed by a CALL */
+    public static final byte CALLMETHOD = -23;
+
+    /** finish a finally block and carry out whatever instruction initiated the finally block */
+    public static final byte FINALLY_DONE = -24;
+    
+    /** finish a finally block and carry out whatever instruction initiated the finally block */
+    public static final byte MAKE_GRAMMAR = -25;
+    
+    public static final String[] bytecodeToString = new String[] {
+        "", "", "LITERAL", "ARRAY", "OBJECT", "NEWFUNCTION", "DECLARE", "TOPSCOPE",
+        "GET", "GET_PRESERVE", "PUT", "JT", "JF", "JMP", "POP", "CALL", "PUSHKEYS",
+        "SWAP", "NEWSCOPE", "OLDSCOPE", "DUP", "LABEL", "LOOP", "CALLMETHOD",
+        "FINALLY_DONE", "MAKE_GRAMMAR"
+    };
+}
diff --git a/src/org/ibex/js/Directory.java b/src/org/ibex/js/Directory.java
new file mode 100644 (file)
index 0000000..e9f7b95
--- /dev/null
@@ -0,0 +1,117 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.util.*;
+import java.io.*;
+
+// FEATURE: support for move
+// FEATURE: support for bytestreams
+// FEATURE: cache directories so we can do equality checking on them?
+// FEATURE: autoconvert "true" to true and "0.3" to 0.3 on readback
+
+/** 
+ * A crude mechanism for using a filesystem as object storage.
+ *
+ *  This object represents a directory; writing a string, number, or
+ *  boolean to any of its properties will create a file with the
+ *  (encoded) property name as its filename and the "stringified"
+ *  value as its contents.
+ *
+ *  Writing 'null' to one of this object's properties will
+ *  [recursively if necessary] delete the corresponding directory
+ *  entry.
+ *  
+ *  Writing any other object to one of this object's properties will
+ *  create a new Directory object and copy the other object's keys()
+ *  into the new Directory.  This means that assigning one directory
+ *  to a property of another directory will <i>copy</i> the directory,
+ *  not move it.  There is currently no way to move directories.
+ *
+ *  If an object is written to a property that already has an entry,
+ *  the old one is deleted (equivalent to writing 'null') first.
+ * 
+ *  WARNING: when instantiating a Directory object with a file
+ *  argument that points to a non-directory File, this class will
+ *  delete that file and create a directory!
+ */
+public class Directory extends JS {
+
+    File f;
+
+    /** 
+     *  Create the directory object.  Existing directories will be
+     *  preserved; if a file is present it will be obliterated.
+     */ 
+    public Directory(File f) throws IOException {
+        this.f = f;
+        if (!f.exists()) new Directory(new File(f.getParent()));
+        if (!f.isDirectory()) destroy(f);
+        f.mkdirs();
+    }
+
+    private static void destroy(File f) throws IOException {
+        if (!f.exists()) return;
+        if (f.isDirectory()) {
+            String[] entries = f.list();
+            for(int i=0; i<entries.length; i++) destroy(new File(f.getAbsolutePath() + File.separatorChar + entries[i]));
+        }
+        f.delete();
+    }
+
+    public void put(Object key0, Object val) throws JSExn {
+        try {
+            if (key0 == null) return;
+            String key = toString(key0);
+            File f2 = new File(f.getAbsolutePath() + File.separatorChar + FileNameEncoder.encode(key));
+            destroy(f2);
+            if (val == null) return;
+            if (val instanceof JS) {
+                Directory d2 = new Directory(f2);
+                Enumeration e = ((JS)val).keys();
+                while(e.hasMoreElements()) {
+                    String k = (String)e.nextElement();
+                    Object v = ((JS)val).get(k);
+                    d2.put(k, v);
+                }
+            } else {
+                OutputStream out = new FileOutputStream(f2);
+                Writer w = new OutputStreamWriter(out);
+                w.write(toString(val));
+                w.flush();
+                out.close();
+            }
+        } catch (IOException ioe) {
+            throw new JSExn.IO(ioe);
+        }
+    }
+
+    public Object get(Object key0) throws JSExn {
+        try {
+            if (key0 == null) return null;
+            String key = toString(key0);
+            File f2 = new File(f.getAbsolutePath() + File.separatorChar + FileNameEncoder.encode(key));
+            if (!f2.exists()) return null;
+            if (f2.isDirectory()) return new Directory(f2);
+            char[] chars = new char[((int)f2.length()) * 2];
+            int numchars = 0;
+            Reader r = new InputStreamReader(new FileInputStream(f2));
+            while(true) {
+                int numread = r.read(chars, numchars, chars.length - numchars);
+                if (numread == -1) return new String(chars, 0, numchars);
+                numchars += numread;
+            }
+        } catch (IOException ioe) {
+            throw new JSExn.IO(ioe);
+        }
+    }
+
+    public Enumeration keys() {
+        final String[] elements = f.list();
+        return new Enumeration() {
+                int i = 0;
+                public boolean hasMoreElements() { return i < elements.length; }
+                public Object nextElement() { return FileNameEncoder.decode(elements[i++]); }
+            };
+    }
+}
diff --git a/src/org/ibex/js/Interpreter.java b/src/org/ibex/js/Interpreter.java
new file mode 100644 (file)
index 0000000..504bbd2
--- /dev/null
@@ -0,0 +1,744 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+import org.ibex.util.*;
+import java.util.*;
+
+/** Encapsulates a single JS interpreter (ie call stack) */
+class Interpreter implements ByteCodes, Tokens {
+
+
+    // Thread-Interpreter Mapping /////////////////////////////////////////////////////////////////////////
+
+    static Interpreter current() { return (Interpreter)threadToInterpreter.get(Thread.currentThread()); }
+    private static Hashtable threadToInterpreter = new Hashtable();
+
+    
+    // Instance members and methods //////////////////////////////////////////////////////////////////////
+    
+    int pausecount;               ///< the number of times pause() has been invoked; -1 indicates unpauseable
+    JSFunction f = null;          ///< the currently-executing JSFunction
+    JSScope scope;                ///< the current top-level scope (LIFO stack via NEWSCOPE/OLDSCOPE)
+    Vec stack = new Vec();        ///< the object stack
+    int pc = 0;                   ///< the program counter
+
+    Interpreter(JSFunction f, boolean pauseable, JSArray args) {
+        stack.push(new Interpreter.CallMarker(this));    // the "root function returned" marker -- f==null
+        this.f = f;
+        this.pausecount = pauseable ? 0 : -1;
+        this.scope = new JSScope(f.parentScope);
+        stack.push(args);
+    }
+    
+    /** this is the only synchronization point we need in order to be threadsafe */
+    synchronized Object resume() throws JSExn {
+        Thread t = Thread.currentThread();
+        Interpreter old = (Interpreter)threadToInterpreter.get(t);
+        threadToInterpreter.put(t, this);
+        try {
+            return run();
+        } finally {
+            if (old == null) threadToInterpreter.remove(t);
+            else threadToInterpreter.put(t, old);
+        }
+    }
+
+    static int getLine() {
+        Interpreter c = Interpreter.current();
+        return c == null || c.f == null || c.pc < 0 || c.pc >= c.f.size ? -1 : c.f.line[c.pc];
+    }
+
+    static String getSourceName() {
+        Interpreter c = Interpreter.current();
+        return c == null || c.f == null ? null : c.f.sourceName;
+    } 
+
+    private static JSExn je(String s) { return new JSExn(getSourceName() + ":" + getLine() + " " + s); }
+
+    // FIXME: double check the trap logic
+    private Object run() throws JSExn {
+
+        // if pausecount changes after a get/put/call, we know we've been paused
+        final int initialPauseCount = pausecount;
+
+        OUTER: for(;; pc++) {
+        try {
+            if (f == null) return stack.pop();
+            int op = f.op[pc];
+            Object arg = f.arg[pc];
+            if(op == FINALLY_DONE) {
+                FinallyData fd = (FinallyData) stack.pop();
+                if(fd == null) continue OUTER; // NOP
+                if(fd.exn != null) throw fd.exn;
+                op = fd.op;
+                arg = fd.arg;
+            }
+            switch(op) {
+            case LITERAL: stack.push(arg); break;
+            case OBJECT: stack.push(new JS()); break;
+            case ARRAY: stack.push(new JSArray(JS.toNumber(arg).intValue())); break;
+            case DECLARE: scope.declare((String)(arg==null ? stack.peek() : arg)); if(arg != null) stack.push(arg); break;
+            case TOPSCOPE: stack.push(scope); break;
+            case JT: if (JS.toBoolean(stack.pop())) pc += JS.toNumber(arg).intValue() - 1; break;
+            case JF: if (!JS.toBoolean(stack.pop())) pc += JS.toNumber(arg).intValue() - 1; break;
+            case JMP: pc += JS.toNumber(arg).intValue() - 1; break;
+            case POP: stack.pop(); break;
+            case SWAP: {
+                int depth = (arg == null ? 1 : JS.toInt(arg));
+                Object save = stack.elementAt(stack.size() - 1);
+                for(int i=stack.size() - 1; i > stack.size() - 1 - depth; i--)
+                    stack.setElementAt(stack.elementAt(i-1), i);
+                stack.setElementAt(save, stack.size() - depth - 1);
+                break; }
+            case DUP: stack.push(stack.peek()); break;
+            case NEWSCOPE: scope = new JSScope(scope); break;
+            case OLDSCOPE: scope = scope.getParentScope(); break;
+            case ASSERT:
+                if (JS.checkAssertions && !JS.toBoolean(stack.pop()))
+                    throw je("ibex.assertion.failed" /*FEATURE: line number*/); break;
+            case BITNOT: stack.push(JS.N(~JS.toLong(stack.pop()))); break;
+            case BANG: stack.push(JS.B(!JS.toBoolean(stack.pop()))); break;
+            case NEWFUNCTION: stack.push(((JSFunction)arg)._cloneWithNewParentScope(scope)); break;
+            case LABEL: break;
+
+            case TYPEOF: {
+                Object o = stack.pop();
+                if (o == null) stack.push(null);
+                else if (o instanceof JS) stack.push("object");
+                else if (o instanceof String) stack.push("string");
+                else if (o instanceof Number) stack.push("number");
+                else if (o instanceof Boolean) stack.push("boolean");
+                else throw new Error("this should not happen");
+                break;
+            }
+
+            case PUSHKEYS: {
+                Object o = stack.peek();
+                Enumeration e = ((JS)o).keys();
+                JSArray a = new JSArray();
+                while(e.hasMoreElements()) a.addElement(e.nextElement());
+                stack.push(a);
+                break;
+            }
+
+            case LOOP:
+                stack.push(new LoopMarker(pc, pc > 0 && f.op[pc - 1] == LABEL ? (String)f.arg[pc - 1] : (String)null, scope));
+                stack.push(Boolean.TRUE);
+                break;
+
+            case BREAK:
+            case CONTINUE:
+                while(stack.size() > 0) {
+                    Object o = stack.pop();
+                    if (o instanceof CallMarker) je("break or continue not within a loop");
+                    if (o instanceof TryMarker) {
+                        if(((TryMarker)o).finallyLoc < 0) continue; // no finally block, keep going
+                        stack.push(new FinallyData(op, arg));
+                        scope = ((TryMarker)o).scope;
+                        pc = ((TryMarker)o).finallyLoc - 1;
+                        continue OUTER;
+                    }
+                    if (o instanceof LoopMarker) {
+                        if (arg == null || arg.equals(((LoopMarker)o).label)) {
+                            int loopInstructionLocation = ((LoopMarker)o).location;
+                            int endOfLoop = ((Integer)f.arg[loopInstructionLocation]).intValue() + loopInstructionLocation;
+                            scope = ((LoopMarker)o).scope;
+                            if (op == CONTINUE) { stack.push(o); stack.push(Boolean.FALSE); }
+                            pc = op == BREAK ? endOfLoop - 1 : loopInstructionLocation;
+                            continue OUTER;
+                        }
+                    }
+                }
+                throw new Error("CONTINUE/BREAK invoked but couldn't find LoopMarker at " +
+                                getSourceName() + ":" + getLine());
+
+            case TRY: {
+                int[] jmps = (int[]) arg;
+                // jmps[0] is how far away the catch block is, jmps[1] is how far away the finally block is
+                // each can be < 0 if the specified block does not exist
+                stack.push(new TryMarker(jmps[0] < 0 ? -1 : pc + jmps[0], jmps[1] < 0 ? -1 : pc + jmps[1], this));
+                break;
+            }
+
+            case RETURN: {
+                Object retval = stack.pop();
+                while(stack.size() > 0) {
+                    Object o = stack.pop();
+                    if (o instanceof TryMarker) {
+                        if(((TryMarker)o).finallyLoc < 0) continue;
+                        stack.push(retval); 
+                        stack.push(new FinallyData(RETURN));
+                        scope = ((TryMarker)o).scope;
+                        pc = ((TryMarker)o).finallyLoc - 1;
+                        continue OUTER;
+                    } else if (o instanceof CallMarker) {
+                        if (scope instanceof Trap.TrapScope) { // handles return component of a read trap
+                            Trap.TrapScope ts = (Trap.TrapScope)scope;
+                            if (retval != null && retval instanceof Boolean && ((Boolean)retval).booleanValue())
+                                ts.cascadeHappened = true;
+                            if (!ts.cascadeHappened) {
+                                ts.cascadeHappened = true;
+                                Trap t = ts.t.next;
+                                while (t != null && t.f.numFormalArgs == 0) t = t.next;
+                                if (t == null) {
+                                    ((JS)ts.t.trapee).put(ts.t.name, ts.val);
+                                    if (pausecount > initialPauseCount) { pc++; return null; }   // we were paused
+                                } else {
+                                    stack.push(o);
+                                    JSArray args = new JSArray();
+                                    args.addElement(ts.val);
+                                    stack.push(args);
+                                    f = t.f;
+                                    scope = new Trap.TrapScope(f.parentScope, t, ts.val);
+                                    pc = -1;
+                                    continue OUTER;
+                                }
+                            }
+                        }
+                        scope = ((CallMarker)o).scope;
+                        pc = ((CallMarker)o).pc - 1;
+                        f = (JSFunction)((CallMarker)o).f;
+                        stack.push(retval);
+                        continue OUTER;
+                    }
+                }
+                throw new Error("error: RETURN invoked but couldn't find a CallMarker!");
+            }
+
+            case PUT: {
+                Object val = stack.pop();
+                Object key = stack.pop();
+                Object target = stack.peek();
+                if (target == null)
+                    throw je("tried to put a value to the " + key + " property on the null value");
+                if (!(target instanceof JS))
+                    throw je("tried to put a value to the " + key + " property on a " + target.getClass().getName());
+                if (key == null)
+                    throw je("tried to assign \"" + (val==null?"(null)":val.toString()) + "\" to the null key");
+
+                Trap t = null;
+                if (target instanceof JSScope && key.equals("cascade")) {
+                   Trap.TrapScope ts = null;
+                    JSScope p = (JSScope)target; // search the scope-path for the trap
+                   if (target instanceof Trap.TrapScope) {
+                       ts = (Trap.TrapScope)target;
+                   }
+                   else {
+                       while (ts == null && p.getParentScope() != null) {
+                           p = p.getParentScope();
+                           if (p instanceof Trap.TrapScope) {
+                               ts = (Trap.TrapScope)p;
+                           }
+                       }
+                   }
+                   t = ts.t.next;
+                   ts.cascadeHappened = true;
+                    while (t != null && t.f.numFormalArgs == 0) t = t.next;
+                    if (t == null) { target = ts.t.trapee; key = ts.t.name; }
+
+                } else if (target instanceof Trap.TrapScope && key.equals(((Trap.TrapScope)target).t.name)) {
+                    throw je("tried to put to " + key + " inside a trap it owns; use cascade instead"); 
+
+                } else if (target instanceof JS) {
+                    if (target instanceof JSScope) {
+                        JSScope p = (JSScope)target; // search the scope-path for the trap
+                        t = p.getTrap(key);
+                        while (t == null && p.getParentScope() != null) { p = p.getParentScope(); t = p.getTrap(key); }
+                    } else {
+                        t = ((JS)target).getTrap(key);
+                    }
+                    while (t != null && t.f.numFormalArgs == 0) t = t.next; // find the first write trap
+                }
+                if (t != null) {
+                    stack.push(new CallMarker(this));
+                    JSArray args = new JSArray();
+                    args.addElement(val);
+                    stack.push(args);
+                    f = t.f;
+                    scope = new Trap.TrapScope(f.parentScope, t, val);
+                    pc = -1;
+                    break;
+                }
+                ((JS)target).put(key, val);
+                if (pausecount > initialPauseCount) { pc++; return null; }   // we were paused
+                stack.push(val);
+                break;
+            }
+
+            case GET:
+            case GET_PRESERVE: {
+                Object o, v;
+                if (op == GET) {
+                    v = arg == null ? stack.pop() : arg;
+                    o = stack.pop();
+                } else {
+                    v = stack.pop();
+                    o = stack.peek();
+                    stack.push(v);
+                }
+                Object ret = null;
+                if (v == null) throw je("tried to get the null key from " + o);
+                if (o == null) throw je("tried to get property \"" + v + "\" from the null object");
+                if (o instanceof String || o instanceof Number || o instanceof Boolean) {
+                    ret = getFromPrimitive(o,v);
+                    stack.push(ret);
+                    break;
+                } else if (o instanceof JS) {
+                    Trap t = null;
+                    if (o instanceof Trap.TrapScope && v.equals("cascade")) {
+                        t = ((Trap.TrapScope)o).t.next;
+                        while (t != null && t.f.numFormalArgs != 0) t = t.next;
+                        if (t == null) { v = ((Trap.TrapScope)o).t.name; o = ((Trap.TrapScope)o).t.trapee; }
+
+                    } else if (o instanceof JS) {
+                        if (o instanceof JSScope) {
+                            JSScope p = (JSScope)o; // search the scope-path for the trap
+                            t = p.getTrap(v);
+                            while (t == null && p.getParentScope() != null) { p = p.getParentScope(); t = p.getTrap(v); }
+                        } else {
+                            t = ((JS)o).getTrap(v);
+                        }
+                        while (t != null && t.f.numFormalArgs != 0) t = t.next; // get first read trap
+                    }
+                    if (t != null) {
+                        stack.push(new CallMarker(this));
+                        JSArray args = new JSArray();
+                        stack.push(args);
+                        f = t.f;
+                        scope = new Trap.TrapScope(f.parentScope, t, null);
+                        ((Trap.TrapScope)scope).cascadeHappened = true;
+                        pc = -1;
+                        break;
+                    }
+                    ret = ((JS)o).get(v);
+                    if (ret == JS.METHOD) ret = new Stub((JS)o, v);
+                    if (pausecount > initialPauseCount) { pc++; return null; }   // we were paused
+                    stack.push(ret);
+                    break;
+                }
+                throw je("tried to get property " + v + " from a " + o.getClass().getName());
+            }
+            
+            case CALL: case CALLMETHOD: {
+                int numArgs = JS.toInt(arg);
+                Object method = null;
+                Object ret = null;
+                Object object = stack.pop();
+
+                if (op == CALLMETHOD) {
+                    if (object == JS.METHOD) {
+                        method = stack.pop();
+                        object = stack.pop();
+                    } else if (object == null) {
+                        Object name = stack.pop();
+                        stack.pop();
+                        throw new JSExn("function '"+name+"' not found");
+                    } else {
+                        stack.pop();
+                        stack.pop();
+                    }
+                }
+                Object[] rest = numArgs > 3 ? new Object[numArgs - 3] : null;
+                for(int i=numArgs - 1; i>2; i--) rest[i-3] = stack.pop();
+                Object a2 = numArgs <= 2 ? null : stack.pop();
+                Object a1 = numArgs <= 1 ? null : stack.pop();
+                Object a0 = numArgs <= 0 ? null : stack.pop();
+
+                if (object instanceof String || object instanceof Number || object instanceof Boolean) {
+                    ret = callMethodOnPrimitive(object, method, a0, a1, a2, null, numArgs);
+
+                } else if (object instanceof JSFunction) {
+                    // FIXME: use something similar to call0/call1/call2 here
+                    JSArray arguments = new JSArray();
+                    for(int i=0; i<numArgs; i++) arguments.addElement(i==0?a0:i==1?a1:i==2?a2:rest[i-3]);
+                    stack.push(new CallMarker(this));
+                    stack.push(arguments);
+                    f = (JSFunction)object;
+                    scope = new JSScope(f.parentScope);
+                    pc = -1;
+                    break;
+
+                } else if (object instanceof JS) {
+                    JS c = (JS)object;
+                    ret = method == null ? c.call(a0, a1, a2, rest, numArgs) : c.callMethod(method, a0, a1, a2, rest, numArgs);
+
+                } else {
+                    throw new JSExn("can't call a " + object + " @" + pc + "\n" + f.dump());
+
+                }
+                if (pausecount > initialPauseCount) { pc++; return null; }
+                stack.push(ret);
+                break;
+            }
+
+            case THROW:
+                throw new JSExn(stack.pop(), stack, f, pc, scope);
+
+                /* FIXME
+            case MAKE_GRAMMAR: {
+                final Grammar r = (Grammar)arg;
+                final JSScope final_scope = scope;
+                Grammar r2 = new Grammar() {
+                        public int match(String s, int start, Hash v, JSScope scope) throws JSExn {
+                            return r.match(s, start, v, final_scope);
+                        }
+                        public int matchAndWrite(String s, int start, Hash v, JSScope scope, String key) throws JSExn {
+                            return r.matchAndWrite(s, start, v, final_scope, key);
+                        }
+                        public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+                            Hash v = new Hash();
+                            r.matchAndWrite((String)a0, 0, v, final_scope, "foo");
+                            return v.get("foo");
+                        }
+                    };
+                Object obj = stack.pop();
+                if (obj != null && obj instanceof Grammar) r2 = new Grammar.Alternative((Grammar)obj, r2);
+                stack.push(r2);
+                break;
+            }
+                */
+            case ADD_TRAP: case DEL_TRAP: {
+                Object val = stack.pop();
+                Object key = stack.pop();
+                Object obj = stack.peek();
+                // A trap addition/removal
+                JS js = obj instanceof JSScope ? ((JSScope)obj).top() : (JS) obj;
+                if(op == ADD_TRAP) js.addTrap(key, (JSFunction)val);
+                else js.delTrap(key, (JSFunction)val);
+                break;
+            }
+
+            case ASSIGN_SUB: case ASSIGN_ADD: {
+                Object val = stack.pop();
+                Object key = stack.pop();
+                Object obj = stack.peek();
+                // The following setup is VERY important. The generated bytecode depends on the stack
+                // being setup like this (top to bottom) KEY, OBJ, VAL, KEY, OBJ
+                stack.push(key);
+                stack.push(val);
+                stack.push(obj);
+                stack.push(key);
+                break;
+            }
+
+            case ADD: {
+                int count = ((Number)arg).intValue();
+                if(count < 2) throw new Error("this should never happen");
+                if(count == 2) {
+                    // common case
+                    Object right = stack.pop();
+                    Object left = stack.pop();
+                    if(left instanceof String || right instanceof String)
+                        stack.push(JS.toString(left).concat(JS.toString(right)));
+                    else stack.push(JS.N(JS.toDouble(left) + JS.toDouble(right)));
+                } else {
+                    Object[] args = new Object[count];
+                    while(--count >= 0) args[count] = stack.pop();
+                    if(args[0] instanceof String) {
+                        StringBuffer sb = new StringBuffer(64);
+                        for(int i=0;i<args.length;i++) sb.append(JS.toString(args[i]));
+                        stack.push(sb.toString());
+                    } else {
+                        int numStrings = 0;
+                        for(int i=0;i<args.length;i++) if(args[i] instanceof String) numStrings++;
+                        if(numStrings == 0) {
+                            double d = 0.0;
+                            for(int i=0;i<args.length;i++) d += JS.toDouble(args[i]);
+                            stack.push(JS.N(d));
+                        } else {
+                            int i=0;
+                            StringBuffer sb = new StringBuffer(64);
+                            if(!(args[0] instanceof String || args[1] instanceof String)) {
+                                double d=0.0;
+                                do {
+                                    d += JS.toDouble(args[i++]);
+                                } while(!(args[i] instanceof String));
+                                sb.append(JS.toString(JS.N(d)));
+                            }
+                            while(i < args.length) sb.append(JS.toString(args[i++]));
+                            stack.push(sb.toString());
+                        }
+                    }
+                }
+                break;
+            }
+
+            default: {
+                Object right = stack.pop();
+                Object left = stack.pop();
+                switch(op) {
+                        
+                case BITOR: stack.push(JS.N(JS.toLong(left) | JS.toLong(right))); break;
+                case BITXOR: stack.push(JS.N(JS.toLong(left) ^ JS.toLong(right))); break;
+                case BITAND: stack.push(JS.N(JS.toLong(left) & JS.toLong(right))); break;
+
+                case SUB: stack.push(JS.N(JS.toDouble(left) - JS.toDouble(right))); break;
+                case MUL: stack.push(JS.N(JS.toDouble(left) * JS.toDouble(right))); break;
+                case DIV: stack.push(JS.N(JS.toDouble(left) / JS.toDouble(right))); break;
+                case MOD: stack.push(JS.N(JS.toDouble(left) % JS.toDouble(right))); break;
+                        
+                case LSH: stack.push(JS.N(JS.toLong(left) << JS.toLong(right))); break;
+                case RSH: stack.push(JS.N(JS.toLong(left) >> JS.toLong(right))); break;
+                case URSH: stack.push(JS.N(JS.toLong(left) >>> JS.toLong(right))); break;
+                        
+                case LT: case LE: case GT: case GE: {
+                    if (left == null) left = JS.N(0);
+                    if (right == null) right = JS.N(0);
+                    int result = 0;
+                    if (left instanceof String || right instanceof String) {
+                        result = left.toString().compareTo(right.toString());
+                    } else {
+                        result = (int)java.lang.Math.ceil(JS.toDouble(left) - JS.toDouble(right));
+                    }
+                    stack.push(JS.B((op == LT && result < 0) || (op == LE && result <= 0) ||
+                               (op == GT && result > 0) || (op == GE && result >= 0)));
+                    break;
+                }
+                    
+                case EQ:
+                case NE: {
+                    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 (r == null) ret = false; // l != null, so its false
+                    else if (l instanceof Boolean) ret = JS.B(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);
+                    stack.push(JS.B(op == EQ ? ret : !ret)); break;
+                }
+
+                default: throw new Error("unknown opcode " + op);
+                } }
+            }
+
+        } catch(JSExn e) {
+            while(stack.size() > 0) {
+                Object o = stack.pop();
+                if (o instanceof CatchMarker || o instanceof TryMarker) {
+                    boolean inCatch = o instanceof CatchMarker;
+                    if(inCatch) {
+                        o = stack.pop();
+                        if(((TryMarker)o).finallyLoc < 0) continue; // no finally block, keep going
+                    }
+                    if(!inCatch && ((TryMarker)o).catchLoc >= 0) {
+                        // run the catch block, this will implicitly run the finally block, if it exists
+                        stack.push(o);
+                        stack.push(catchMarker);
+                        stack.push(e.getObject());
+                        f = ((TryMarker)o).f;
+                        scope = ((TryMarker)o).scope;
+                        pc = ((TryMarker)o).catchLoc - 1;
+                        continue OUTER;
+                    } else {
+                        stack.push(new FinallyData(e));
+                        f = ((TryMarker)o).f;
+                        scope = ((TryMarker)o).scope;
+                        pc = ((TryMarker)o).finallyLoc - 1;
+                        continue OUTER;
+                    }
+                }
+            }
+            throw e;
+        } // end try/catch
+        } // end for
+    }
+
+
+
+    // Markers //////////////////////////////////////////////////////////////////////
+
+    public static class CallMarker {
+        int pc;
+        JSScope scope;
+        JSFunction f;
+        public CallMarker(Interpreter cx) { pc = cx.pc + 1; scope = cx.scope; f = cx.f; }
+    }
+    
+    public static class CatchMarker { }
+    private static CatchMarker catchMarker = new CatchMarker();
+    
+    public static class LoopMarker {
+        public int location;
+        public String label;
+        public JSScope scope;
+        public LoopMarker(int location, String label, JSScope scope) {
+            this.location = location;
+            this.label = label;
+            this.scope = scope;
+        }
+    }
+    public static class TryMarker {
+        public int catchLoc;
+        public int finallyLoc;
+        public JSScope scope;
+        public JSFunction f;
+        public TryMarker(int catchLoc, int finallyLoc, Interpreter cx) {
+            this.catchLoc = catchLoc;
+            this.finallyLoc = finallyLoc;
+            this.scope = cx.scope;
+            this.f = cx.f;
+        }
+    }
+    public static class FinallyData {
+        public int op;
+        public Object arg;
+        public JSExn exn;
+        public FinallyData(int op) { this(op,null); }
+        public FinallyData(int op, Object arg) { this.op = op; this.arg = arg; }
+        public FinallyData(JSExn exn) { this.exn = exn; } // Just throw this exn
+    }
+
+
+    // Operations on Primitives //////////////////////////////////////////////////////////////////////
+
+    static Object callMethodOnPrimitive(Object o, Object method, Object arg0, Object arg1, Object arg2, Object[] rest, int alength) throws JSExn {
+        if (method == null || !(method instanceof String) || "".equals(method))
+            throw new JSExn("attempt to call a non-existant method on a primitive");
+
+        if (o instanceof Number) {
+            //#switch(method)
+            case "toFixed": throw new JSExn("toFixed() not implemented");
+            case "toExponential": throw new JSExn("toExponential() not implemented");
+            case "toPrecision": throw new JSExn("toPrecision() not implemented");
+            case "toString": {
+                int radix = alength >= 1 ? JS.toInt(arg0) : 10;
+                return Long.toString(((Number)o).longValue(),radix);
+            }
+            //#end
+        } else if (o instanceof Boolean) {
+            // No methods for Booleans
+            throw new JSExn("attempt to call a method on a Boolean");
+        }
+
+        String s = JS.toString(o);
+        int slength = s.length();
+        //#switch(method)
+        case "substring": {
+            int a = alength >= 1 ? JS.toInt(arg0) : 0;
+            int b = alength >= 2 ? JS.toInt(arg1) : slength;
+            if (a > slength) a = slength;
+            if (b > slength) b = slength;
+            if (a < 0) a = 0;
+            if (b < 0) b = 0;
+            if (a > b) { int tmp = a; a = b; b = tmp; }
+            return s.substring(a,b);
+        }
+        case "substr": {
+            int start = alength >= 1 ? JS.toInt(arg0) : 0;
+            int len = alength >= 2 ? JS.toInt(arg1) : Integer.MAX_VALUE;
+            if (start < 0) start = slength + start;
+            if (start < 0) start = 0;
+            if (len < 0) len = 0;
+            if (len > slength - start) len = slength - start;
+            if (len <= 0) return "";
+            return s.substring(start,start+len);
+        }
+        case "charAt": {
+            int p = alength >= 1 ? JS.toInt(arg0) : 0;
+            if (p < 0 || p >= slength) return "";
+            return s.substring(p,p+1);
+        }
+        case "charCodeAt": {
+            int p = alength >= 1 ? JS.toInt(arg0) : 0;
+            if (p < 0 || p >= slength) return JS.N(Double.NaN);
+            return JS.N(s.charAt(p));
+        }
+        case "concat": {
+            StringBuffer sb = new StringBuffer(slength*2).append(s);
+            for(int i=0;i<alength;i++) sb.append(i==0?arg0:i==1?arg1:i==2?arg2:rest[i-3]);
+            return sb.toString();
+        }
+        case "indexOf": {
+            String search = alength >= 1 ? arg0.toString() : "null";
+            int start = alength >= 2 ? JS.toInt(arg1) : 0;
+            // Java's indexOf handles an out of bounds start index, it'll return -1
+            return JS.N(s.indexOf(search,start));
+        }
+        case "lastIndexOf": {
+            String search = alength >= 1 ? arg0.toString() : "null";
+            int start = alength >= 2 ? JS.toInt(arg1) : 0;
+            // Java's indexOf handles an out of bounds start index, it'll return -1
+            return JS.N(s.lastIndexOf(search,start));            
+        }
+        case "match": return JSRegexp.stringMatch(s,arg0);
+        case "replace": return JSRegexp.stringReplace(s,arg0,arg1);
+        case "search": return JSRegexp.stringSearch(s,arg0);
+        case "split": return JSRegexp.stringSplit(s,arg0,arg1,alength);
+        case "toLowerCase": return s.toLowerCase();
+        case "toUpperCase": return s.toUpperCase();
+        case "toString": return s;
+        case "slice": {
+            int a = alength >= 1 ? JS.toInt(arg0) : 0;
+            int b = alength >= 2 ? JS.toInt(arg1) : slength;
+            if (a < 0) a = slength + a;
+            if (b < 0) b = slength + b;
+            if (a < 0) a = 0;
+            if (b < 0) b = 0;
+            if (a > slength) a = slength;
+            if (b > slength) b = slength;
+            if (a > b) return "";
+            return s.substring(a,b);
+        }
+        //#end
+        throw new JSExn("Attempted to call non-existent method: " + method);
+    }
+    
+    static Object getFromPrimitive(Object o, Object key) throws JSExn {
+        boolean returnJS = false;
+        if (o instanceof Boolean) {
+            throw new JSExn("Booleans do not have properties");
+        } else if (o instanceof Number) {
+            if (key.equals("toPrecision") || key.equals("toExponential") || key.equals("toFixed"))
+                returnJS = true;
+        }
+        if (!returnJS) {
+            // the string stuff applies to everything
+            String s = o.toString();
+            
+            // this is sort of ugly, but this list should never change
+            // These should provide a complete (enough) implementation of the ECMA-262 String object
+
+            //#switch(key)
+            case "length": return JS.N(s.length());
+            case "substring": returnJS = true; break; 
+            case "charAt": returnJS = true; break; 
+            case "charCodeAt": returnJS = true; break; 
+            case "concat": returnJS = true; break; 
+            case "indexOf": returnJS = true; break; 
+            case "lastIndexOf": returnJS = true; break; 
+            case "match": returnJS = true; break; 
+            case "replace": returnJS = true; break; 
+            case "search": returnJS = true; break; 
+            case "slice": returnJS = true; break; 
+            case "split": returnJS = true; break; 
+            case "toLowerCase": returnJS = true; break; 
+            case "toUpperCase": returnJS = true; break; 
+            case "toString": returnJS = true; break; 
+            case "substr": returnJS = true; break;  
+           //#end
+        }
+        if (returnJS) {
+            final Object target = o;
+            final String method = key.toString();
+            return new JS() {
+                    public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+                        if (nargs > 2) throw new JSExn("cannot call that method with that many arguments");
+                        return callMethodOnPrimitive(target, method, a0, a1, a2, rest, nargs);
+                    }
+            };
+        }
+        return null;
+    }
+
+    private static class Stub extends JS {
+        private Object method;
+        JS obj;
+        public Stub(JS obj, Object method) { this.obj = obj; this.method = method; }
+        public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+            return ((JS)obj).callMethod(method, a0, a1, a2, rest, nargs);
+        }
+    }
+}
diff --git a/src/org/ibex/js/JS.java b/src/org/ibex/js/JS.java
new file mode 100644 (file)
index 0000000..1e38ce5
--- /dev/null
@@ -0,0 +1,241 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.io.*;
+import java.util.*;
+
+/** The minimum set of functionality required for objects which are manipulated by JavaScript */
+public class JS extends org.ibex.util.BalancedTree { 
+
+    public static boolean checkAssertions = false;
+
+    public static final Object METHOD = new Object();
+    public final JS unclone() { return _unclone(); }
+    public Enumeration keys() throws JSExn { return entries == null ? emptyEnumeration : entries.keys(); }
+    public Object get(Object key) throws JSExn { return entries == null ? null : entries.get(key, null); }
+    public void put(Object key, Object val) throws JSExn { (entries==null?entries=new Hash():entries).put(key,null,val); }
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        throw new JSExn("attempted to call the null value (method "+method+")");
+    }    
+    public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        throw new JSExn("you cannot call this object (class=" + this.getClass().getName() +")");
+    }
+
+    JS _unclone() { return this; }
+    public static class Cloneable extends JS {
+        public Object jsclone() throws JSExn {
+            return new Clone(this);
+        }
+    }
+
+    public static class Clone extends JS.Cloneable {
+        protected JS.Cloneable clonee = null;
+        JS _unclone() { return clonee.unclone(); }
+        public JS.Cloneable getClonee() { return clonee; }
+        public Clone(JS.Cloneable clonee) { this.clonee = clonee; }
+        public boolean equals(Object o) {
+            if (!(o instanceof JS)) return false;
+            return unclone() == ((JS)o).unclone();
+        }
+        public Enumeration keys() throws JSExn { return clonee.keys(); }
+        public Object get(Object key) throws JSExn { return clonee.get(key); }
+        public void put(Object key, Object val) throws JSExn { clonee.put(key, val); }
+        public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+            return clonee.callMethod(method, a0, a1, a2, rest, nargs);
+        }    
+        public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+            return clonee.call(a0, a1, a2, rest, nargs);
+        }
+    }
+
+    // Static Interpreter Control Methods ///////////////////////////////////////////////////////////////
+
+    /** log a message with the current JavaScript sourceName/line */
+    public static void log(Object message) { info(message); }
+    public static void debug(Object message) { Log.debug(Interpreter.getSourceName() + ":" + Interpreter.getLine(), message); }
+    public static void info(Object message) { Log.info(Interpreter.getSourceName() + ":" + Interpreter.getLine(), message); }
+    public static void warn(Object message) { Log.warn(Interpreter.getSourceName() + ":" + Interpreter.getLine(), message); }
+    public static void error(Object message) { Log.error(Interpreter.getSourceName() + ":" + Interpreter.getLine(), message); }
+
+    public static class NotPauseableException extends Exception { NotPauseableException() { } }
+
+    /** returns a callback which will restart the context; expects a value to be pushed onto the stack when unpaused */
+    public static UnpauseCallback pause() throws NotPauseableException {
+        Interpreter i = Interpreter.current();
+        if (i.pausecount == -1) throw new NotPauseableException();
+        i.pausecount++;
+        return new JS.UnpauseCallback(i);
+    }
+
+    public static class UnpauseCallback implements Task {
+        Interpreter i;
+        UnpauseCallback(Interpreter i) { this.i = i; }
+        public void perform() throws JSExn { unpause(null); }
+        public void unpause(Object o) throws JSExn {
+            // FIXME: if o instanceof JSExn, throw it into the JSworld
+            i.stack.push(o);
+            i.resume();
+        }
+    }
+
+
+
+    // Static Helper Methods ///////////////////////////////////////////////////////////////////////////////////
+
+    /** coerce an object to a Boolean */
+    public static boolean toBoolean(Object o) {
+        if (o == null) return false;
+        if (o instanceof Boolean) return ((Boolean)o).booleanValue();
+        if (o instanceof Long) return ((Long)o).longValue() != 0;
+        if (o instanceof Integer) return ((Integer)o).intValue() != 0;
+        if (o instanceof Number) {
+            double d = ((Number) o).doubleValue();
+            // NOTE: d == d is a test for NaN. It should be faster than Double.isNaN()
+            return d != 0.0 && d == d;
+        }
+        if (o instanceof String) return ((String)o).length() != 0;
+        return true;
+    }
+
+    /** coerce an object to a Long */
+    public static long toLong(Object o) { return toNumber(o).longValue(); }
+
+    /** coerce an object to an Int */
+    public static int toInt(Object o) { return toNumber(o).intValue(); }
+
+    /** coerce an object to a Double */
+    public static double toDouble(Object o) { return toNumber(o).doubleValue(); }
+
+    /** coerce an object to a Number */
+    public static Number toNumber(Object o) {
+        if (o == null) return ZERO;
+        if (o instanceof Number) return ((Number)o);
+
+        // NOTE: There are about 3 pages of rules in ecma262 about string to number conversions
+        //       We aren't even close to following all those rules.  We probably never will be.
+        if (o instanceof String) try { return N((String)o); } catch (NumberFormatException e) { return N(Double.NaN); }
+        if (o instanceof Boolean) return ((Boolean)o).booleanValue() ? N(1) : ZERO;
+        throw new Error("toNumber() got object of type " + o.getClass().getName() + " which we don't know how to handle");
+    }
+
+    /** coerce an object to a String */
+    public static String toString(Object o) {
+        if(o == null) return "null";
+        if(o instanceof String) return (String) o;
+        if(o instanceof Integer || o instanceof Long || o instanceof Boolean) return o.toString();
+        if(o instanceof JSArray) return o.toString();
+        if(o instanceof JSDate) return o.toString();
+        if(o instanceof Double || o instanceof Float) {
+            double d = ((Number)o).doubleValue();
+            if((int)d == d) return Integer.toString((int)d);
+            return o.toString();
+        }
+        if (o instanceof JS) return ((JS)o).coerceToString();   // HACK for now, this will probably go away
+        throw new RuntimeException("can't coerce "+o+" [" + o.getClass().getName() + "] to type String.");
+    }
+
+    public String coerceToString() {
+        throw new RuntimeException("can't coerce "+this+" [" + getClass().getName() + "] to type String.");
+    }
+
+    // Instance Methods ////////////////////////////////////////////////////////////////////
+
+    public static final Integer ZERO = new Integer(0);
+    // this gets around a wierd fluke in the Java type checking rules for ?..:
+    public static final Object T = Boolean.TRUE;
+    public static final Object F = Boolean.FALSE;
+
+    public static final Boolean B(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
+    public static final Boolean B(int i) { return i==0 ? Boolean.FALSE : Boolean.TRUE; }
+    public static final Number N(String s) { return s.indexOf('.') == -1 ? N(Integer.parseInt(s)) : new Double(s); }
+    public static final Number N(double d) { return (int)d == d ? N((int)d) : new Double(d); }
+    public static final Number N(long l) { return N((int)l); }
+
+    private static final Integer[] smallIntCache = new Integer[65535 / 4];
+    private static final Integer[] largeIntCache = new Integer[65535 / 4];
+    public static final Number N(int i) {
+        Integer ret = null;
+        int idx = i + smallIntCache.length / 2;
+        if (idx < smallIntCache.length && idx > 0) {
+            ret = smallIntCache[idx];
+            if (ret != null) return ret;
+        }
+        else ret = largeIntCache[Math.abs(idx % largeIntCache.length)];
+        if (ret == null || ret.intValue() != i) {
+            ret = new Integer(i);
+            if (idx < smallIntCache.length && idx > 0) smallIntCache[idx] = ret;
+            else largeIntCache[Math.abs(idx % largeIntCache.length)] = ret;
+        }
+        return ret;
+    }
+    
+    private static Enumeration emptyEnumeration = new Enumeration() {
+            public boolean hasMoreElements() { return false; }
+            public Object nextElement() { throw new NoSuchElementException(); }
+        };
+    
+    private Hash entries = null;
+
+    public static JS fromReader(String sourceName, int firstLine, Reader sourceCode) throws IOException {
+        return JSFunction._fromReader(sourceName, firstLine, sourceCode);
+    }
+
+    // HACK: caller can't know if the argument is a JSFunction or not...
+    public static JS cloneWithNewParentScope(JS j, JSScope s) {
+        return ((JSFunction)j)._cloneWithNewParentScope(s);
+    }
+
+
+    // Trap support //////////////////////////////////////////////////////////////////////////////
+
+    /** override and return true to allow placing traps on this object.
+     *  if isRead true, this is a read trap, otherwise write trap
+     **/
+    protected boolean isTrappable(Object name, boolean isRead) { return true; }
+
+    /** performs a put, triggering traps if present; traps are run in an unpauseable interpreter */
+    public void putAndTriggerTraps(Object key, Object value) throws JSExn {
+        Trap t = getTrap(key);
+        if (t != null) t.invoke(value);
+        else put(key, value);
+    }
+
+    /** performs a get, triggering traps if present; traps are run in an unpauseable interpreter */
+    public Object getAndTriggerTraps(Object key) throws JSExn {
+        Trap t = getTrap(key);
+        if (t != null) return t.invoke();
+        else return get(key);
+    }
+
+    /** retrieve a trap from the entries hash */
+    protected final Trap getTrap(Object key) {
+        return entries == null ? null : (Trap)entries.get(key, Trap.class);
+    }
+
+    /** retrieve a trap from the entries hash */
+    protected final void putTrap(Object key, Trap value) {
+        if (entries == null) entries = new Hash();
+        entries.put(key, Trap.class, value);
+    }
+
+    /** adds a trap, avoiding duplicates */
+    protected final void addTrap(Object name, JSFunction f) throws JSExn {
+        if (f.numFormalArgs > 1) throw new JSExn("traps must take either one argument (write) or no arguments (read)");
+        boolean isRead = f.numFormalArgs == 0;
+        if (!isTrappable(name, isRead)) throw new JSExn("not allowed "+(isRead?"read":"write")+" trap on property: "+name);
+        for(Trap t = getTrap(name); t != null; t = t.next) if (t.f == f) return;
+        putTrap(name, new Trap(this, name.toString(), f, (Trap)getTrap(name)));
+    }
+
+    /** deletes a trap, if present */
+    protected final void delTrap(Object name, JSFunction f) {
+        Trap t = (Trap)getTrap(name);
+        if (t == null) return;
+        if (t.f == f) { putTrap(t.name, t.next); return; }
+        for(; t.next != null; t = t.next) if (t.next.f == f) { t.next = t.next.next; return; }
+    }
+
+
+} 
diff --git a/src/org/ibex/js/JSArray.java b/src/org/ibex/js/JSArray.java
new file mode 100644 (file)
index 0000000..7b90de7
--- /dev/null
@@ -0,0 +1,262 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.util.*;
+
+/** A JavaScript JSArray */
+public class JSArray extends JS {
+    private static final Object NULL = new Object();
+    
+    public JSArray() { }
+    public JSArray(int size) { setSize(size); }
+    
+    private static int intVal(Object o) {
+        if (o instanceof Number) {
+            int intVal = ((Number)o).intValue();
+            if (intVal == ((Number)o).doubleValue()) return intVal;
+            return Integer.MIN_VALUE;
+        }
+        if (!(o instanceof String)) return Integer.MIN_VALUE;
+        String s = (String)o;
+        for(int i=0; i<s.length(); i++) if (s.charAt(i) < '0' || s.charAt(i) > '9') return Integer.MIN_VALUE;
+        return Integer.parseInt(s);
+    }
+    
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        //#switch(method)
+        case "pop": {
+            int oldSize = size();
+            if(oldSize == 0) return null;
+            return removeElementAt(oldSize-1);
+        }
+        case "reverse": return reverse();
+        case "toString": return join(",");
+        case "shift":
+            if(length() == 0) return null;
+            return removeElementAt(0);
+        case "join":
+            return join(nargs == 0 ? "," : JS.toString(a0));
+        case "sort":
+            return sort(nargs < 1 ? null : a0);
+        case "slice":
+            int start = toInt(nargs < 1 ? null : a0);
+            int end = nargs < 2 ? length() : toInt(a1);
+            return slice(start, end);
+        case "push": {
+            int oldSize = size();
+            for(int i=0; i<nargs; i++) insertElementAt(i==0?a0:i==1?a1:i==2?a2:rest[i-3],oldSize+i);
+            return N(oldSize + nargs);
+        }
+        case "unshift":
+            for(int i=0; i<nargs; i++) insertElementAt(i==0?a0:i==1?a1:i==2?a2:rest[i-3],i);
+            return N(size());
+        case "splice":
+            JSArray array = new JSArray();
+            for(int i=0; i<nargs; i++) array.addElement(i==0?a0:i==1?a1:i==2?a2:rest[i-3]);
+            return splice(array);
+        //#end
+        return super.callMethod(method, a0, a1, a2, rest, nargs);
+    }
+        
+    public Object get(Object key) throws JSExn {
+        int i = intVal(key);
+        if (i != Integer.MIN_VALUE) {
+            if (i < 0 || i >= size()) return null;
+            return elementAt(i);
+        }
+        //#switch(key)
+        case "pop": return METHOD;
+        case "reverse": return METHOD;
+        case "toString": return METHOD;
+        case "shift": return METHOD;
+        case "join": return METHOD;
+        case "sort": return METHOD;
+        case "slice": return METHOD;
+        case "push": return METHOD;
+        case "unshift": return METHOD;
+        case "splice": return METHOD;
+        case "length": return N(size());
+        //#end
+        return super.get(key);
+    }
+
+    public void put(Object key, Object val) throws JSExn {
+        if (key.equals("length")) setSize(toInt(val));
+        int i = intVal(key);
+        if (i == Integer.MIN_VALUE)
+            super.put(key, val);
+        else {
+            int oldSize = size();
+            if(i < oldSize) {
+                setElementAt(val,i);
+            } else {
+                if(i > oldSize) setSize(i);
+                insertElementAt(val,i);
+            }
+        }
+    }
+
+    public Enumeration keys() {
+        return new Enumeration() {
+                private int n = size();
+                public boolean hasMoreElements() { return n > 0; }
+                public Object nextElement() {
+                    if(n == 0) throw new NoSuchElementException();
+                    return new Integer(--n);
+                }
+            };
+    }
+
+    public final void setSize(int newSize) {
+        // FEATURE: This could be done a lot more efficiently in BalancedTree
+        int oldSize = size();
+        for(int i=oldSize;i<newSize;i++) insertElementAt(null,i);
+        for(int i=oldSize-1;i>=newSize;i--) removeElementAt(i);
+    }
+    
+    public final int length() { return size(); }
+    public final Object elementAt(int i) { 
+        if(i < 0 || i >= size()) throw new ArrayIndexOutOfBoundsException(i);
+        Object o = getNode(i);
+        return o == NULL ? null : o;
+    }
+    public final void addElement(Object o) { 
+        insertNode(size(),o==null ? NULL : o);
+    }
+    public final void setElementAt(Object o, int i) {
+        if(i < 0 || i >= size()) throw new ArrayIndexOutOfBoundsException(i);
+        replaceNode(i,o==null ? NULL : o);
+    }
+    public final void insertElementAt(Object o, int i) {
+        if(i < 0 || i > size()) throw new ArrayIndexOutOfBoundsException(i);
+        insertNode(i,o==null ? NULL : o);
+    }
+    public final Object removeElementAt(int i) {
+        if(i < 0 || i >= size()) throw new ArrayIndexOutOfBoundsException(i);
+        Object o = deleteNode(i);
+        return o == NULL ? null : o;
+    }
+    
+    public final int size() { return treeSize(); }
+    public String typeName() { return "array"; }
+        
+    private Object join(String sep) {
+        int length = size();
+        if(length == 0) return "";
+        StringBuffer sb = new StringBuffer(64);
+        int i=0;
+        while(true) {
+            Object o = elementAt(i);
+            if(o != null) sb.append(JS.toString(o));
+            if(++i == length) break;
+            sb.append(sep);
+        }
+        return sb.toString();
+    }
+    
+    // FEATURE: Implement this more efficiently
+    private Object reverse() {
+        int size = size();
+        if(size < 2) return this;
+        Vec vec = toVec();
+        clear();
+        for(int i=size-1,j=0;i>=0;i--,j++) insertElementAt(vec.elementAt(i),j);
+        return this;
+    }
+    
+    private Object slice(int start, int end) {
+        int length = length();
+        if(start < 0) start = length+start;
+        if(end < 0) end = length+end;
+        if(start < 0) start = 0;
+        if(end < 0) end = 0;
+        if(start > length) start = length;
+        if(end > length) end = length;
+        JSArray a = new JSArray(end-start);
+        for(int i=0;i<end-start;i++)
+            a.setElementAt(elementAt(start+i),i);
+        return a;
+    }
+    
+    private static final Vec.CompareFunc defaultSort = new Vec.CompareFunc() {
+        public int compare(Object a, Object b) {
+            return JS.toString(a).compareTo(JS.toString(b));
+        }
+    };
+    private Object sort(Object tmp) throws JSExn {
+        Vec vec = toVec();
+        if(tmp instanceof JS) {
+            final JSArray funcArgs = new JSArray(2);
+            final JS jsFunc = (JS) tmp;
+            vec.sort(new Vec.CompareFunc() {
+                public int compare(Object a, Object b) {
+                    try {
+                        funcArgs.setElementAt(a,0);
+                        funcArgs.setElementAt(b,1);
+                        return JS.toInt(jsFunc.call(a, b, null, null, 2));
+                    } catch (Exception e) {
+                        // FIXME
+                        throw new JSRuntimeExn(e.toString());
+                    }
+                }
+            });
+        } else {
+            vec.sort(defaultSort);
+        }
+        setFromVec(vec);
+        return this;
+    }
+    
+    private Object splice(JSArray args) {
+        int oldLength = length();
+        int start = JS.toInt(args.length() < 1 ? null : args.elementAt(0));
+        int deleteCount = JS.toInt(args.length() < 2 ? null : args.elementAt(1));
+        int newCount = args.length() - 2;
+        if(newCount < 0) newCount = 0;
+        if(start < 0) start = oldLength+start;
+        if(start < 0) start = 0;
+        if(start > oldLength) start = oldLength;
+        if(deleteCount < 0) deleteCount = 0;
+        if(deleteCount > oldLength-start) deleteCount = oldLength-start;
+        int newLength = oldLength - deleteCount + newCount;
+        int lengthChange = newLength - oldLength;
+        JSArray ret = new JSArray(deleteCount);
+        for(int i=0;i<deleteCount;i++)
+            ret.setElementAt(elementAt(start+i),i);
+        if(lengthChange > 0) {
+            setSize(newLength);
+            for(int i=newLength-1;i>=start+newCount;i--)
+                setElementAt(elementAt(i-lengthChange),i);
+        } else if(lengthChange < 0) {
+            for(int i=start+newCount;i<newLength;i++)
+                setElementAt(elementAt(i-lengthChange),i);
+            setSize(newLength);
+        }
+        for(int i=0;i<newCount;i++)
+            setElementAt(args.elementAt(i+2),start+i);
+        return ret;
+    }
+    
+    protected Vec toVec() {
+        int count = size();
+        Vec vec = new Vec();
+        vec.setSize(count);
+        for(int i=0;i<count;i++) {
+            Object o = getNode(i);
+            vec.setElementAt(o == NULL ? null : o,i);
+        }
+        return vec;
+    }
+    
+    protected void setFromVec(Vec vec) {
+        int count = vec.size();
+        clear();
+        for(int i=0;i<count;i++) {
+            Object o = vec.elementAt(i);
+            insertNode(i,o==null ? NULL : o);
+        }
+    }
+    
+    public String toString() { return JS.toString(join(",")); }
+}
diff --git a/src/org/ibex/js/JSDate.java b/src/org/ibex/js/JSDate.java
new file mode 100644 (file)
index 0000000..694122c
--- /dev/null
@@ -0,0 +1,1253 @@
+/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * The contents of this file are subject to the Netscape Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/NPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express oqr
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is Rhino code, released
+ * May 6, 1999.
+ *
+ * The Initial Developer of the Original Code is Netscape
+ * Communications Corporation.  Portions created by Netscape are
+ * Copyright (C) 1997-1999 Netscape Communications Corporation. All
+ * Rights Reserved.
+ *
+ * Contributor(s):
+ * Norris Boyd
+ * Mike McCabe
+ *
+ * Alternatively, the contents of this file may be used under the
+ * terms of the GNU Public License (the "GPL"), in which case the
+ * provisions of the GPL are applicable instead of those above.
+ * If you wish to allow use of your version of this file only
+ * under the terms of the GPL and not to allow others to use your
+ * version of this file under the NPL, indicate your decision by
+ * deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL.  If you do not delete
+ * the provisions above, a recipient may use your version of this
+ * file under either the NPL or the GPL.
+ */
+
+package org.ibex.js;
+
+import java.text.DateFormat;
+
+/**
+ * This class implements the Date native object.
+ * See ECMA 15.9.
+ * @author Mike McCabe
+ * @author Adam Megacz (many modifications
+ */
+public class JSDate extends JS {
+
+    public JSDate() {
+        if (thisTimeZone == null) {
+            // j.u.TimeZone is synchronized, so setting class statics from it
+            // should be OK.
+            thisTimeZone = java.util.TimeZone.getDefault();
+            LocalTZA = thisTimeZone.getRawOffset();
+        }
+    }
+
+    public String toString() { return date_format(date, FORMATSPEC_FULL); }
+
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        switch(nargs) {
+            case 0: {
+                //#switch(method)
+                case "toString": return date_format(date, FORMATSPEC_FULL);
+                case "toTimeString": return date_format(date, FORMATSPEC_TIME);
+                case "toDateString": return date_format(date, FORMATSPEC_DATE);
+                case "toLocaleString": return toLocaleString(date);
+                case "toLocaleTimeString": return toLocaleTimeString(date);
+                case "toLocaleDateString": return toLocaleDateString(date);
+                case "toUTCString": return toUTCString(date);
+                case "valueOf": return N(this.date);
+                case "getTime": return N(this.date);
+                case "getYear": return N(getYear(date));
+                case "getFullYear": return N(YearFromTime(LocalTime(date)));
+                case "getUTCFullYear": return N(YearFromTime(date));
+                case "getMonth": return N(MonthFromTime(LocalTime(date)));
+                case "getUTCMonth": return N(MonthFromTime(date));
+                case "getDate": return N(DateFromTime(LocalTime(date)));
+                case "getUTCDate": return N(DateFromTime(date));
+                case "getDay": return N(WeekDay(LocalTime(date)));
+                case "getUTCDay": return N(WeekDay(date));
+                case "getHours": return N(HourFromTime(LocalTime(date)));
+                case "getUTCHours": return N(HourFromTime(date));
+                case "getMinutes": return N(MinFromTime(LocalTime(date)));
+                case "getUTCMinutes": return N(MinFromTime(date));
+                case "getSeconds": return N(SecFromTime(LocalTime(date)));
+                case "getUTCSeconds": return N(SecFromTime(date));
+                case "getMilliseconds": return N(msFromTime(LocalTime(date)));
+                case "getUTCMilliseconds": return N(msFromTime(date));
+                case "getTimezoneOffset": return N(getTimezoneOffset(date));
+                //#end
+                return super.callMethod(method, a0, a1, a2, rest, nargs);
+            }
+            case 1: {
+                //#switch(method)
+                case "setTime": return N(this.setTime(toDouble(a0)));
+                case "setYear": return N(this.setYear(toDouble(a0)));
+                //#end
+                // fall through
+            }
+            default: {
+                Object[] args = new Object[nargs];
+                for(int i=0; i<nargs; i++) args[i] = i==0 ? a0 : i==1 ? a1 : i==2 ? a2 : rest[i-3];
+                //#switch(method)
+                case "setMilliseconds": return N(this.makeTime(args, 1, true));
+                case "setUTCMilliseconds": return N(this.makeTime(args, 1, false));
+                case "setSeconds": return N(this.makeTime(args, 2, true));
+                case "setUTCSeconds": return N(this.makeTime(args, 2, false));
+                case "setMinutes": return N(this.makeTime(args, 3, true));
+                case "setUTCMinutes": return N(this.makeTime(args, 3, false));
+                case "setHours": return N(this.makeTime(args, 4, true));
+                case "setUTCHours": return N(this.makeTime(args, 4, false));
+                case "setDate": return N(this.makeDate(args, 1, true));
+                case "setUTCDate": return N(this.makeDate(args, 1, false));
+                case "setMonth": return N(this.makeDate(args, 2, true));
+                case "setUTCMonth": return N(this.makeDate(args, 2, false));
+                case "setFullYear": return N(this.makeDate(args, 3, true));
+                case "setUTCFullYear": return N(this.makeDate(args, 3, false));
+                //#end
+            }
+        }
+        return super.callMethod(method, a0, a1, a2, rest, nargs);
+    }
+
+    public Object get(Object key) throws JSExn {
+        //#switch(key)
+        case "toString": return METHOD;
+        case "toTimeString": return METHOD;
+        case "toDateString": return METHOD;
+        case "toLocaleString": return METHOD;
+        case "toLocaleTimeString": return METHOD;
+        case "toLocaleDateString": return METHOD;
+        case "toUTCString": return METHOD;
+        case "valueOf": return METHOD;
+        case "getTime": return METHOD;
+        case "getYear": return METHOD;
+        case "getFullYear": return METHOD;
+        case "getUTCFullYear": return METHOD;
+        case "getMonth": return METHOD;
+        case "getUTCMonth": return METHOD;
+        case "getDate": return METHOD;
+        case "getUTCDate": return METHOD;
+        case "getDay": return METHOD;
+        case "getUTCDay": return METHOD;
+        case "getHours": return METHOD;
+        case "getUTCHours": return METHOD;
+        case "getMinutes": return METHOD;
+        case "getUTCMinutes": return METHOD;
+        case "getSeconds": return METHOD;
+        case "getUTCSeconds": return METHOD;
+        case "getMilliseconds": return METHOD;
+        case "getUTCMilliseconds": return METHOD;
+        case "getTimezoneOffset": return METHOD;
+        case "setTime": return METHOD;
+        case "setYear": return METHOD;
+        case "setMilliseconds": return METHOD;
+        case "setUTCMilliseconds": return METHOD;
+        case "setSeconds": return METHOD;
+        case "setUTCSeconds": return METHOD;
+        case "setMinutes": return METHOD;
+        case "setUTCMinutes": return METHOD;
+        case "setHours": return METHOD;
+        case "setUTCHours": return METHOD;
+        case "setDate": return METHOD;
+        case "setUTCDate": return METHOD;
+        case "setMonth": return METHOD;
+        case "setUTCMonth": return METHOD;
+        case "setFullYear": return METHOD;
+        case "setUTCFullYear": return METHOD;
+        //#end
+        return super.get(key);
+    }
+
+    /* ECMA helper functions */
+
+    private static final double HalfTimeDomain = 8.64e15;
+    private static final double HoursPerDay    = 24.0;
+    private static final double MinutesPerHour = 60.0;
+    private static final double SecondsPerMinute = 60.0;
+    private static final double msPerSecond    = 1000.0;
+    private static final double MinutesPerDay  = (HoursPerDay * MinutesPerHour);
+    private static final double SecondsPerDay  = (MinutesPerDay * SecondsPerMinute);
+    private static final double SecondsPerHour = (MinutesPerHour * SecondsPerMinute);
+    private static final double msPerDay       = (SecondsPerDay * msPerSecond);
+    private static final double msPerHour      = (SecondsPerHour * msPerSecond);
+    private static final double msPerMinute    = (SecondsPerMinute * msPerSecond);
+
+    private static double Day(double t) {
+        return java.lang.Math.floor(t / msPerDay);
+    }
+
+    private static double TimeWithinDay(double t) {
+        double result;
+        result = t % msPerDay;
+        if (result < 0)
+            result += msPerDay;
+        return result;
+    }
+
+    private static int DaysInYear(int y) {
+        if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0))
+            return 366;
+        else
+            return 365;
+    }
+
+
+    /* math here has to be f.p, because we need
+     *  floor((1968 - 1969) / 4) == -1
+     */
+    private static double DayFromYear(double y) {
+        return ((365 * ((y)-1970) + java.lang.Math.floor(((y)-1969)/4.0)
+                 - java.lang.Math.floor(((y)-1901)/100.0) + java.lang.Math.floor(((y)-1601)/400.0)));
+    }
+
+    private static double TimeFromYear(double y) {
+        return DayFromYear(y) * msPerDay;
+    }
+
+    private static int YearFromTime(double t) {
+        int lo = (int) java.lang.Math.floor((t / msPerDay) / 366) + 1970;
+        int hi = (int) java.lang.Math.floor((t / msPerDay) / 365) + 1970;
+        int mid;
+
+        /* above doesn't work for negative dates... */
+        if (hi < lo) {
+            int temp = lo;
+            lo = hi;
+            hi = temp;
+        }
+
+        /* Use a simple binary search algorithm to find the right
+           year.  This seems like brute force... but the computation
+           of hi and lo years above lands within one year of the
+           correct answer for years within a thousand years of
+           1970; the loop below only requires six iterations
+           for year 270000. */
+        while (hi > lo) {
+            mid = (hi + lo) / 2;
+            if (TimeFromYear(mid) > t) {
+                hi = mid - 1;
+            } else {
+                if (TimeFromYear(mid) <= t) {
+                    int temp = mid + 1;
+                    if (TimeFromYear(temp) > t) {
+                        return mid;
+                    }
+                    lo = mid + 1;
+                }
+            }
+        }
+        return lo;
+    }
+
+    private static boolean InLeapYear(double t) {
+        return DaysInYear(YearFromTime(t)) == 366;
+    }
+
+    private static int DayWithinYear(double t) {
+        int year = YearFromTime(t);
+        return (int) (Day(t) - DayFromYear(year));
+    }
+    /*
+     * The following array contains the day of year for the first day of
+     * each month, where index 0 is January, and day 0 is January 1.
+     */
+
+    private static double DayFromMonth(int m, boolean leap) {
+        int day = m * 30;
+
+        if (m >= 7) { day += m / 2 - 1; }
+        else if (m >= 2) { day += (m - 1) / 2 - 1; }
+        else { day += m; }
+
+        if (leap && m >= 2) { ++day; }
+
+        return day;
+    }
+
+    private static int MonthFromTime(double t) {
+        int d, step;
+
+        d = DayWithinYear(t);
+
+        if (d < (step = 31))
+            return 0;
+
+        // Originally coded as step += (InLeapYear(t) ? 29 : 28);
+        // but some jits always returned 28!
+        if (InLeapYear(t))
+            step += 29;
+        else
+            step += 28;
+
+        if (d < step)
+            return 1;
+        if (d < (step += 31))
+            return 2;
+        if (d < (step += 30))
+            return 3;
+        if (d < (step += 31))
+            return 4;
+        if (d < (step += 30))
+            return 5;
+        if (d < (step += 31))
+            return 6;
+        if (d < (step += 31))
+            return 7;
+        if (d < (step += 30))
+            return 8;
+        if (d < (step += 31))
+            return 9;
+        if (d < (step += 30))
+            return 10;
+        return 11;
+    }
+
+    private static int DateFromTime(double t) {
+        int d, step, next;
+
+        d = DayWithinYear(t);
+        if (d <= (next = 30))
+            return d + 1;
+        step = next;
+
+        // Originally coded as next += (InLeapYear(t) ? 29 : 28);
+        // but some jits always returned 28!
+        if (InLeapYear(t))
+            next += 29;
+        else
+            next += 28;
+
+        if (d <= next)
+            return d - step;
+        step = next;
+        if (d <= (next += 31))
+            return d - step;
+        step = next;
+        if (d <= (next += 30))
+            return d - step;
+        step = next;
+        if (d <= (next += 31))
+            return d - step;
+        step = next;
+        if (d <= (next += 30))
+            return d - step;
+        step = next;
+        if (d <= (next += 31))
+            return d - step;
+        step = next;
+        if (d <= (next += 31))
+            return d - step;
+        step = next;
+        if (d <= (next += 30))
+            return d - step;
+        step = next;
+        if (d <= (next += 31))
+            return d - step;
+        step = next;
+        if (d <= (next += 30))
+            return d - step;
+        step = next;
+
+        return d - step;
+    }
+
+    private static int WeekDay(double t) {
+        double result;
+        result = Day(t) + 4;
+        result = result % 7;
+        if (result < 0)
+            result += 7;
+        return (int) result;
+    }
+
+    private static double Now() {
+        return (double) System.currentTimeMillis();
+    }
+
+    /* Should be possible to determine the need for this dynamically
+     * if we go with the workaround... I'm not using it now, because I
+     * can't think of any clean way to make toLocaleString() and the
+     * time zone (comment) in toString match the generated string
+     * values.  Currently it's wrong-but-consistent in all but the
+     * most recent betas of the JRE - seems to work in 1.1.7.
+     */
+    private final static boolean TZO_WORKAROUND = false;
+    private static double DaylightSavingTA(double t) {
+        if (!TZO_WORKAROUND) {
+            java.util.Date date = new java.util.Date((long) t);
+            if (thisTimeZone.inDaylightTime(date))
+                return msPerHour;
+            else
+                return 0;
+        } else {
+            /* Use getOffset if inDaylightTime() is broken, because it
+             * seems to work acceptably.  We don't switch over to it
+             * entirely, because it requires (expensive) exploded date arguments,
+             * and the api makes it impossible to handle dst
+             * changeovers cleanly.
+             */
+
+            // Hardcode the assumption that the changeover always
+            // happens at 2:00 AM:
+            t += LocalTZA + (HourFromTime(t) <= 2 ? msPerHour : 0);
+
+            int year = YearFromTime(t);
+            double offset = thisTimeZone.getOffset(year > 0 ? 1 : 0,
+                                                   year,
+                                                   MonthFromTime(t),
+                                                   DateFromTime(t),
+                                                   WeekDay(t),
+                                                   (int)TimeWithinDay(t));
+
+            if ((offset - LocalTZA) != 0)
+                return msPerHour;
+            else
+                return 0;
+            //         return offset - LocalTZA;
+        }
+    }
+
+    private static double LocalTime(double t) {
+        return t + LocalTZA + DaylightSavingTA(t);
+    }
+
+    public static double internalUTC(double t) {
+        return t - LocalTZA - DaylightSavingTA(t - LocalTZA);
+    }
+
+    private static int HourFromTime(double t) {
+        double result;
+        result = java.lang.Math.floor(t / msPerHour) % HoursPerDay;
+        if (result < 0)
+            result += HoursPerDay;
+        return (int) result;
+    }
+
+    private static int MinFromTime(double t) {
+        double result;
+        result = java.lang.Math.floor(t / msPerMinute) % MinutesPerHour;
+        if (result < 0)
+            result += MinutesPerHour;
+        return (int) result;
+    }
+
+    private static int SecFromTime(double t) {
+        double result;
+        result = java.lang.Math.floor(t / msPerSecond) % SecondsPerMinute;
+        if (result < 0)
+            result += SecondsPerMinute;
+        return (int) result;
+    }
+
+    private static int msFromTime(double t) {
+        double result;
+        result =  t % msPerSecond;
+        if (result < 0)
+            result += msPerSecond;
+        return (int) result;
+    }
+
+    private static double MakeTime(double hour, double min,
+                                   double sec, double ms)
+    {
+        return ((hour * MinutesPerHour + min) * SecondsPerMinute + sec)
+            * msPerSecond + ms;
+    }
+
+    private static double MakeDay(double year, double month, double date) {
+        double result;
+        boolean leap;
+        double yearday;
+        double monthday;
+
+        year += java.lang.Math.floor(month / 12);
+
+        month = month % 12;
+        if (month < 0)
+            month += 12;
+
+        leap = (DaysInYear((int) year) == 366);
+
+        yearday = java.lang.Math.floor(TimeFromYear(year) / msPerDay);
+        monthday = DayFromMonth((int) month, leap);
+
+        result = yearday
+            + monthday
+            + date - 1;
+        return result;
+    }
+
+    private static double MakeDate(double day, double time) {
+        return day * msPerDay + time;
+    }
+
+    private static double TimeClip(double d) {
+        if (d != d ||
+            d == Double.POSITIVE_INFINITY ||
+            d == Double.NEGATIVE_INFINITY ||
+            java.lang.Math.abs(d) > HalfTimeDomain)
+        {
+            return Double.NaN;
+        }
+        if (d > 0.0)
+            return java.lang.Math.floor(d + 0.);
+        else
+            return java.lang.Math.ceil(d + 0.);
+    }
+
+    /* end of ECMA helper functions */
+
+    /* find UTC time from given date... no 1900 correction! */
+    public static double date_msecFromDate(double year, double mon,
+                                            double mday, double hour,
+                                            double min, double sec,
+                                            double msec)
+    {
+        double day;
+        double time;
+        double result;
+
+        day = MakeDay(year, mon, mday);
+        time = MakeTime(hour, min, sec, msec);
+        result = MakeDate(day, time);
+        return result;
+    }
+
+
+    private static final int MAXARGS = 7;
+    private static double jsStaticJSFunction_UTC(Object[] args) {
+        double array[] = new double[MAXARGS];
+        int loop;
+        double d;
+
+        for (loop = 0; loop < MAXARGS; loop++) {
+            if (loop < args.length) {
+                d = _toNumber(args[loop]);
+                if (d != d || Double.isInfinite(d)) {
+                    return Double.NaN;
+                }
+                array[loop] = toDouble(args[loop]);
+            } else {
+                array[loop] = 0;
+            }
+        }
+
+        /* adjust 2-digit years into the 20th century */
+        if (array[0] >= 0 && array[0] <= 99)
+            array[0] += 1900;
+
+            /* if we got a 0 for 'date' (which is out of range)
+             * pretend it's a 1.  (So Date.UTC(1972, 5) works) */
+        if (array[2] < 1)
+            array[2] = 1;
+
+        d = date_msecFromDate(array[0], array[1], array[2],
+                              array[3], array[4], array[5], array[6]);
+        d = TimeClip(d);
+        return d;
+        //        return N(d);
+    }
+
+    /*
+     * Use ported code from jsdate.c rather than the locale-specific
+     * date-parsing code from Java, to keep js and rhino consistent.
+     * Is this the right strategy?
+     */
+
+    /* for use by date_parse */
+
+    /* replace this with byte arrays?  Cheaper? */
+    private static String wtb[] = {
+        "am", "pm",
+        "monday", "tuesday", "wednesday", "thursday", "friday",
+        "saturday", "sunday",
+        "january", "february", "march", "april", "may", "june",
+        "july", "august", "september", "october", "november", "december",
+        "gmt", "ut", "utc", "est", "edt", "cst", "cdt",
+        "mst", "mdt", "pst", "pdt"
+        /* time zone table needs to be expanded */
+    };
+
+    private static int ttb[] = {
+        -1, -2, 0, 0, 0, 0, 0, 0, 0,     /* AM/PM */
+        2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
+        10000 + 0, 10000 + 0, 10000 + 0, /* UT/UTC */
+        10000 + 5 * 60, 10000 + 4 * 60,  /* EDT */
+        10000 + 6 * 60, 10000 + 5 * 60,
+        10000 + 7 * 60, 10000 + 6 * 60,
+        10000 + 8 * 60, 10000 + 7 * 60
+    };
+
+    /* helper for date_parse */
+    private static boolean date_regionMatches(String s1, int s1off,
+                                              String s2, int s2off,
+                                              int count)
+    {
+        boolean result = false;
+        /* return true if matches, otherwise, false */
+        int s1len = s1.length();
+        int s2len = s2.length();
+
+        while (count > 0 && s1off < s1len && s2off < s2len) {
+            if (Character.toLowerCase(s1.charAt(s1off)) !=
+                Character.toLowerCase(s2.charAt(s2off)))
+                break;
+            s1off++;
+            s2off++;
+            count--;
+        }
+
+        if (count == 0) {
+            result = true;
+        }
+        return result;
+    }
+
+    private static double date_parseString(String s) {
+        double msec;
+
+        int year = -1;
+        int mon = -1;
+        int mday = -1;
+        int hour = -1;
+        int min = -1;
+        int sec = -1;
+        char c = 0;
+        char si = 0;
+        int i = 0;
+        int n = -1;
+        double tzoffset = -1;
+        char prevc = 0;
+        int limit = 0;
+        boolean seenplusminus = false;
+
+        if (s == null)  // ??? Will s be null?
+            return Double.NaN;
+        limit = s.length();
+        while (i < limit) {
+            c = s.charAt(i);
+            i++;
+            if (c <= ' ' || c == ',' || c == '-') {
+                if (i < limit) {
+                    si = s.charAt(i);
+                    if (c == '-' && '0' <= si && si <= '9') {
+                        prevc = c;
+                    }
+                }
+                continue;
+            }
+            if (c == '(') { /* comments) */
+                int depth = 1;
+                while (i < limit) {
+                    c = s.charAt(i);
+                    i++;
+                    if (c == '(')
+                        depth++;
+                    else if (c == ')')
+                        if (--depth <= 0)
+                            break;
+                }
+                continue;
+            }
+            if ('0' <= c && c <= '9') {
+                n = c - '0';
+                while (i < limit && '0' <= (c = s.charAt(i)) && c <= '9') {
+                    n = n * 10 + c - '0';
+                    i++;
+                }
+
+                /* allow TZA before the year, so
+                 * 'Wed Nov 05 21:49:11 GMT-0800 1997'
+                 * works */
+
+                /* uses of seenplusminus allow : in TZA, so Java
+                 * no-timezone style of GMT+4:30 works
+                 */
+                if ((prevc == '+' || prevc == '-')/*  && year>=0 */) {
+                    /* make ':' case below change tzoffset */
+                    seenplusminus = true;
+
+                    /* offset */
+                    if (n < 24)
+                        n = n * 60; /* EG. "GMT-3" */
+                    else
+                        n = n % 100 + n / 100 * 60; /* eg "GMT-0430" */
+                    if (prevc == '+')       /* plus means east of GMT */
+                        n = -n;
+                    if (tzoffset != 0 && tzoffset != -1)
+                        return Double.NaN;
+                    tzoffset = n;
+                } else if (n >= 70  ||
+                           (prevc == '/' && mon >= 0 && mday >= 0 && year < 0)) {
+                    if (year >= 0)
+                        return Double.NaN;
+                    else if (c <= ' ' || c == ',' || c == '/' || i >= limit)
+                        year = n < 100 ? n + 1900 : n;
+                    else
+                        return Double.NaN;
+                } else if (c == ':') {
+                    if (hour < 0)
+                        hour = /*byte*/ n;
+                    else if (min < 0)
+                        min = /*byte*/ n;
+                    else
+                        return Double.NaN;
+                } else if (c == '/') {
+                    if (mon < 0)
+                        mon = /*byte*/ n-1;
+                    else if (mday < 0)
+                        mday = /*byte*/ n;
+                    else
+                        return Double.NaN;
+                } else if (i < limit && c != ',' && c > ' ' && c != '-') {
+                    return Double.NaN;
+                } else if (seenplusminus && n < 60) {  /* handle GMT-3:30 */
+                    if (tzoffset < 0)
+                        tzoffset -= n;
+                    else
+                        tzoffset += n;
+                } else if (hour >= 0 && min < 0) {
+                    min = /*byte*/ n;
+                } else if (min >= 0 && sec < 0) {
+                    sec = /*byte*/ n;
+                } else if (mday < 0) {
+                    mday = /*byte*/ n;
+                } else {
+                    return Double.NaN;
+                }
+                prevc = 0;
+            } else if (c == '/' || c == ':' || c == '+' || c == '-') {
+                prevc = c;
+            } else {
+                int st = i - 1;
+                int k;
+                while (i < limit) {
+                    c = s.charAt(i);
+                    if (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')))
+                        break;
+                    i++;
+                }
+                if (i <= st + 1)
+                    return Double.NaN;
+                for (k = wtb.length; --k >= 0;)
+                    if (date_regionMatches(wtb[k], 0, s, st, i-st)) {
+                        int action = ttb[k];
+                        if (action != 0) {
+                            if (action < 0) {
+                                /*
+                                 * AM/PM. Count 12:30 AM as 00:30, 12:30 PM as
+                                 * 12:30, instead of blindly adding 12 if PM.
+                                 */
+                                if (hour > 12 || hour < 0) {
+                                    return Double.NaN;
+                                } else {
+                                    if (action == -1 && hour == 12) { // am
+                                        hour = 0;
+                                    } else if (action == -2 && hour != 12) {// pm
+                                        hour += 12;
+                                    }
+                                }
+                            } else if (action <= 13) { /* month! */
+                                if (mon < 0) {
+                                    mon = /*byte*/ (action - 2);
+                                } else {
+                                    return Double.NaN;
+                                }
+                            } else {
+                                tzoffset = action - 10000;
+                            }
+                        }
+                        break;
+                    }
+                if (k < 0)
+                    return Double.NaN;
+                prevc = 0;
+            }
+        }
+        if (year < 0 || mon < 0 || mday < 0)
+            return Double.NaN;
+        if (sec < 0)
+            sec = 0;
+        if (min < 0)
+            min = 0;
+        if (hour < 0)
+            hour = 0;
+        if (tzoffset == -1) { /* no time zone specified, have to use local */
+            double time;
+            time = date_msecFromDate(year, mon, mday, hour, min, sec, 0);
+            return internalUTC(time);
+        }
+
+        msec = date_msecFromDate(year, mon, mday, hour, min, sec, 0);
+        msec += tzoffset * msPerMinute;
+        return msec;
+    }
+
+    private static double jsStaticJSFunction_parse(String s) {
+        return date_parseString(s);
+    }
+
+    private static final int FORMATSPEC_FULL = 0;
+    private static final int FORMATSPEC_DATE = 1;
+    private static final int FORMATSPEC_TIME = 2;
+
+    private static String date_format(double t, int format) {
+        if (t != t)
+            return NaN_date_str;
+
+        StringBuffer result = new StringBuffer(60);
+        double local = LocalTime(t);
+
+        /* offset from GMT in minutes.  The offset includes daylight savings,
+           if it applies. */
+        int minutes = (int) java.lang.Math.floor((LocalTZA + DaylightSavingTA(t))
+                                       / msPerMinute);
+        /* map 510 minutes to 0830 hours */
+        int offset = (minutes / 60) * 100 + minutes % 60;
+
+        String dateStr = Integer.toString(DateFromTime(local));
+        String hourStr = Integer.toString(HourFromTime(local));
+        String minStr = Integer.toString(MinFromTime(local));
+        String secStr = Integer.toString(SecFromTime(local));
+        String offsetStr = Integer.toString(offset > 0 ? offset : -offset);
+        int year = YearFromTime(local);
+        String yearStr = Integer.toString(year > 0 ? year : -year);
+
+        /* Tue Oct 31 09:41:40 GMT-0800 (PST) 2000 */
+        /* Tue Oct 31 2000 */
+        /* 09:41:40 GMT-0800 (PST) */
+
+        if (format != FORMATSPEC_TIME) {
+            result.append(days[WeekDay(local)]);
+            result.append(' ');
+            result.append(months[MonthFromTime(local)]);
+            if (dateStr.length() == 1)
+                result.append(" 0");
+            else
+                result.append(' ');
+            result.append(dateStr);
+            result.append(' ');
+        }
+
+        if (format != FORMATSPEC_DATE) {
+            if (hourStr.length() == 1)
+                result.append('0');
+            result.append(hourStr);
+            if (minStr.length() == 1)
+                result.append(":0");
+            else
+                result.append(':');
+            result.append(minStr);
+            if (secStr.length() == 1)
+                result.append(":0");
+            else
+                result.append(':');
+            result.append(secStr);
+            if (offset > 0)
+                result.append(" GMT+");
+            else
+                result.append(" GMT-");
+            for (int i = offsetStr.length(); i < 4; i++)
+                result.append('0');
+            result.append(offsetStr);
+
+            if (timeZoneFormatter == null)
+                timeZoneFormatter = new java.text.SimpleDateFormat("zzz");
+
+            if (timeZoneFormatter != null) {
+                result.append(" (");
+                java.util.Date date = new java.util.Date((long) t);
+                result.append(timeZoneFormatter.format(date));
+                result.append(')');
+            }
+            if (format != FORMATSPEC_TIME)
+                result.append(' ');
+        }
+
+        if (format != FORMATSPEC_TIME) {
+            if (year < 0)
+                result.append('-');
+            for (int i = yearStr.length(); i < 4; i++)
+                result.append('0');
+            result.append(yearStr);
+        }
+
+        return result.toString();
+    }
+
+    private static double _toNumber(Object o) { return JS.toDouble(o); }
+    private static double _toNumber(Object[] o, int index) { return JS.toDouble(o[index]); }
+    private static double toDouble(double d) { return d; }
+
+    public JSDate(Object a0, Object a1, Object a2, Object[] rest, int nargs) {
+
+        JSDate obj = this;
+        switch (nargs) {
+            case 0: {
+                obj.date = Now();
+                return;
+            }
+            case 1: {
+                double date;
+                if (a0 instanceof JS)
+                    a0 = ((JS) a0).toString();
+                if (!(a0 instanceof String)) {
+                    // if it's not a string, use it as a millisecond date
+                    date = _toNumber(a0);
+                } else {
+                    // it's a string; parse it.
+                    String str = (String) a0;
+                    date = date_parseString(str);
+                }
+                obj.date = TimeClip(date);
+                return;
+            }
+            default: {
+                // multiple arguments; year, month, day etc.
+                double array[] = new double[MAXARGS];
+                array[0] = toDouble(a0);
+                array[1] = toDouble(a1);
+                if (nargs >= 2) array[2] = toDouble(a2);
+                for(int i=0; i<nargs; i++) {
+                    double d = _toNumber(i==0?a0:i==1?a1:i==2?a2:rest[i-3]);
+                    if (d != d || Double.isInfinite(d)) {
+                        obj.date = Double.NaN;
+                        return;
+                    }
+                    array[i] = d;
+                }
+                
+                /* adjust 2-digit years into the 20th century */
+                if (array[0] >= 0 && array[0] <= 99)
+                    array[0] += 1900;
+                
+                /* if we got a 0 for 'date' (which is out of range)
+                 * pretend it's a 1 */
+                if (array[2] < 1)
+                    array[2] = 1;
+                
+                double day = MakeDay(array[0], array[1], array[2]);
+                double time = MakeTime(array[3], array[4], array[5], array[6]);
+                time = MakeDate(day, time);
+                time = internalUTC(time);
+                obj.date = TimeClip(time);
+                
+                return;
+            }
+        }
+    }
+
+    /* constants for toString, toUTCString */
+    private static String NaN_date_str = "Invalid Date";
+
+    private static String[] days = {
+        "Sun","Mon","Tue","Wed","Thu","Fri","Sat"
+    };
+
+    private static String[] months = {
+        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+    };
+
+    private static String toLocale_helper(double t,
+                                          java.text.DateFormat formatter)
+    {
+        if (t != t)
+            return NaN_date_str;
+
+        java.util.Date tempdate = new java.util.Date((long) t);
+        return formatter.format(tempdate);
+    }
+
+    private static String toLocaleString(double date) {
+        if (localeDateTimeFormatter == null)
+            localeDateTimeFormatter =
+                DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
+
+        return toLocale_helper(date, localeDateTimeFormatter);
+    }
+
+    private static String toLocaleTimeString(double date) {
+        if (localeTimeFormatter == null)
+            localeTimeFormatter = DateFormat.getTimeInstance(DateFormat.LONG);
+
+        return toLocale_helper(date, localeTimeFormatter);
+    }
+
+    private static String toLocaleDateString(double date) {
+        if (localeDateFormatter == null)
+            localeDateFormatter = DateFormat.getDateInstance(DateFormat.LONG);
+
+        return toLocale_helper(date, localeDateFormatter);
+    }
+
+    private static String toUTCString(double date) {
+        StringBuffer result = new StringBuffer(60);
+
+        String dateStr = Integer.toString(DateFromTime(date));
+        String hourStr = Integer.toString(HourFromTime(date));
+        String minStr = Integer.toString(MinFromTime(date));
+        String secStr = Integer.toString(SecFromTime(date));
+        int year = YearFromTime(date);
+        String yearStr = Integer.toString(year > 0 ? year : -year);
+
+        result.append(days[WeekDay(date)]);
+        result.append(", ");
+        if (dateStr.length() == 1)
+            result.append('0');
+        result.append(dateStr);
+        result.append(' ');
+        result.append(months[MonthFromTime(date)]);
+        if (year < 0)
+            result.append(" -");
+        else
+            result.append(' ');
+        int i;
+        for (i = yearStr.length(); i < 4; i++)
+            result.append('0');
+        result.append(yearStr);
+
+        if (hourStr.length() == 1)
+            result.append(" 0");
+        else
+            result.append(' ');
+        result.append(hourStr);
+        if (minStr.length() == 1)
+            result.append(":0");
+        else
+            result.append(':');
+        result.append(minStr);
+        if (secStr.length() == 1)
+            result.append(":0");
+        else
+            result.append(':');
+        result.append(secStr);
+
+        result.append(" GMT");
+        return result.toString();
+    }
+
+    private static double getYear(double date) {
+        int result = YearFromTime(LocalTime(date));
+        result -= 1900;
+        return result;
+    }
+
+    private static double getTimezoneOffset(double date) {
+        return (date - LocalTime(date)) / msPerMinute;
+    }
+
+    public double setTime(double time) {
+        this.date = TimeClip(time);
+        return this.date;
+    }
+
+    private double makeTime(Object[] args, int maxargs, boolean local) {
+        int i;
+        double conv[] = new double[4];
+        double hour, min, sec, msec;
+        double lorutime; /* Local or UTC version of date */
+
+        double time;
+        double result;
+
+        double date = this.date;
+
+        /* just return NaN if the date is already NaN */
+        if (date != date)
+            return date;
+
+        /* Satisfy the ECMA rule that if a function is called with
+         * fewer arguments than the specified formal arguments, the
+         * remaining arguments are set to undefined.  Seems like all
+         * the Date.setWhatever functions in ECMA are only varargs
+         * beyond the first argument; this should be set to undefined
+         * if it's not given.  This means that "d = new Date();
+         * d.setMilliseconds()" returns NaN.  Blech.
+         */
+        if (args.length == 0)
+            args = new Object[] { null };
+
+        for (i = 0; i < args.length && i < maxargs; i++) {
+            conv[i] = _toNumber(args[i]);
+
+            // limit checks that happen in MakeTime in ECMA.
+            if (conv[i] != conv[i] || Double.isInfinite(conv[i])) {
+                this.date = Double.NaN;
+                return this.date;
+            }
+            conv[i] = toDouble(conv[i]);
+        }
+
+        if (local)
+            lorutime = LocalTime(date);
+        else
+            lorutime = date;
+
+        i = 0;
+        int stop = args.length;
+
+        if (maxargs >= 4 && i < stop)
+            hour = conv[i++];
+        else
+            hour = HourFromTime(lorutime);
+
+        if (maxargs >= 3 && i < stop)
+            min = conv[i++];
+        else
+            min = MinFromTime(lorutime);
+
+        if (maxargs >= 2 && i < stop)
+            sec = conv[i++];
+        else
+            sec = SecFromTime(lorutime);
+
+        if (maxargs >= 1 && i < stop)
+            msec = conv[i++];
+        else
+            msec = msFromTime(lorutime);
+
+        time = MakeTime(hour, min, sec, msec);
+        result = MakeDate(Day(lorutime), time);
+
+        if (local)
+            result = internalUTC(result);
+        date = TimeClip(result);
+
+        this.date = date;
+        return date;
+    }
+
+    private double setHours(Object[] args) {
+        return makeTime(args, 4, true);
+    }
+
+    private double setUTCHours(Object[] args) {
+        return makeTime(args, 4, false);
+    }
+
+    private double makeDate(Object[] args, int maxargs, boolean local) {
+        int i;
+        double conv[] = new double[3];
+        double year, month, day;
+        double lorutime; /* local or UTC version of date */
+        double result;
+
+        double date = this.date;
+
+        /* See arg padding comment in makeTime.*/
+        if (args.length == 0)
+            args = new Object[] { null };
+
+        for (i = 0; i < args.length && i < maxargs; i++) {
+            conv[i] = _toNumber(args[i]);
+
+            // limit checks that happen in MakeDate in ECMA.
+            if (conv[i] != conv[i] || Double.isInfinite(conv[i])) {
+                this.date = Double.NaN;
+                return this.date;
+            }
+            conv[i] = toDouble(conv[i]);
+        }
+
+        /* return NaN if date is NaN and we're not setting the year,
+         * If we are, use 0 as the time. */
+        if (date != date) {
+            if (args.length < 3) {
+                return Double.NaN;
+            } else {
+                lorutime = 0;
+            }
+        } else {
+            if (local)
+                lorutime = LocalTime(date);
+            else
+                lorutime = date;
+        }
+
+        i = 0;
+        int stop = args.length;
+
+        if (maxargs >= 3 && i < stop)
+            year = conv[i++];
+        else
+            year = YearFromTime(lorutime);
+
+        if (maxargs >= 2 && i < stop)
+            month = conv[i++];
+        else
+            month = MonthFromTime(lorutime);
+
+        if (maxargs >= 1 && i < stop)
+            day = conv[i++];
+        else
+            day = DateFromTime(lorutime);
+
+        day = MakeDay(year, month, day); /* day within year */
+        result = MakeDate(day, TimeWithinDay(lorutime));
+
+        if (local)
+            result = internalUTC(result);
+
+        date = TimeClip(result);
+
+        this.date = date;
+        return date;
+    }
+
+    private double setYear(double year) {
+        double day, result;
+        if (year != year || Double.isInfinite(year)) {
+            this.date = Double.NaN;
+            return this.date;
+        }
+
+        if (this.date != this.date) {
+            this.date = 0;
+        } else {
+            this.date = LocalTime(this.date);
+        }
+
+        if (year >= 0 && year <= 99)
+            year += 1900;
+
+        day = MakeDay(year, MonthFromTime(this.date), DateFromTime(this.date));
+        result = MakeDate(day, TimeWithinDay(this.date));
+        result = internalUTC(result);
+
+        this.date = TimeClip(result);
+        return this.date;
+    }
+
+
+    //    private static final int
+    //        Id_toGMTString  =  Id_toUTCString; // Alias, see Ecma B.2.6
+// #/string_id_map#
+
+    /* cached values */
+    private static java.util.TimeZone thisTimeZone;
+    private static double LocalTZA;
+    private static java.text.DateFormat timeZoneFormatter;
+    private static java.text.DateFormat localeDateTimeFormatter;
+    private static java.text.DateFormat localeDateFormatter;
+    private static java.text.DateFormat localeTimeFormatter;
+
+    private double date;
+
+    public long getRawTime() { return (long)this.date; }
+}
+
+
diff --git a/src/org/ibex/js/JSExn.java b/src/org/ibex/js/JSExn.java
new file mode 100644 (file)
index 0000000..ff4abc5
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.io.*;
+
+/** An exception which can be thrown and caught by JavaScript code */
+public class JSExn extends Exception { 
+    private Vec backtrace = new Vec();
+    private Object js = null; 
+    public JSExn(Object js) {
+        this.js = js;
+        if (Interpreter.current() != null)
+            fill(Interpreter.current().stack, Interpreter.current().f, Interpreter.current().pc, Interpreter.current().scope);
+    }
+    public JSExn(Object js, Vec stack, JSFunction f, int pc, JSScope scope) { this.js = js; fill(stack, f, pc, scope); }
+    private void fill(Vec stack, JSFunction f, int pc, JSScope scope) {
+        addBacktrace(f.sourceName + ":" + f.line[pc]);
+        if (scope != null && scope instanceof Trap.TrapScope)
+            addBacktrace("trap on property \"" + ((Trap.TrapScope)scope).t.name + "\"");
+        for(int i=stack.size()-1; i>=0; i--) {
+            Object element = stack.elementAt(i);
+            if (element instanceof Interpreter.CallMarker) {
+                Interpreter.CallMarker cm = (Interpreter.CallMarker)element;
+                if (cm.f != null)
+                    addBacktrace(cm.f.sourceName + ":" + cm.f.line[cm.pc-1]);
+                if (cm.scope != null && cm.scope instanceof Trap.TrapScope)
+                    addBacktrace("trap on property \"" + ((Trap.TrapScope)cm.scope).t.name + "\"");
+            }
+        }
+    }
+    public void printStackTrace() { printStackTrace(System.err); }
+    public void printStackTrace(PrintWriter pw) {
+        for(int i=0; i<backtrace.size(); i++) pw.println("    at " + (String) backtrace.elementAt(i));
+        super.printStackTrace(pw);
+    }
+    public void printStackTrace(PrintStream ps) {
+        for(int i=0; i<backtrace.size(); i++) ps.println("    at " + (String) backtrace.elementAt(i));
+        super.printStackTrace(ps);
+    }
+    public String toString() { return "JSExn: " + js; }
+    public String getMessage() { return toString(); }
+    public Object getObject() { return js; } 
+    public void addBacktrace(String line) { backtrace.addElement(line); }
+
+
+    public static class IO extends JSExn {
+        public IO(java.io.IOException ioe) {
+            super("ibex.io: " + ioe.toString());
+            JS.warn(ioe);
+        }
+    }
+} 
+
+/** should only be used for failed coercions */
+class JSRuntimeExn extends RuntimeException {
+    private Object js = null; 
+    public JSRuntimeExn(Object js) { this.js = js; } 
+    public String toString() { return "JSRuntimeExn: " + js; }
+    public String getMessage() { return toString(); }
+    public Object getObject() { return js; } 
+}
+
diff --git a/src/org/ibex/js/JSFunction.java b/src/org/ibex/js/JSFunction.java
new file mode 100644 (file)
index 0000000..4bf41e4
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+import java.io.*;
+import org.ibex.util.*;
+
+/** A JavaScript function, compiled into bytecode */
+class JSFunction extends JS implements ByteCodes, Tokens, Task {
+
+
+    // Fields and Accessors ///////////////////////////////////////////////
+
+    int numFormalArgs = 0;         ///< the number of formal arguments
+
+    String sourceName;             ///< the source code file that this block was drawn from
+    private int firstLine = -1;    ///< the first line of this script
+
+    int[] line = new int[10];      ///< the line numbers
+    int[] op = new int[10];        ///< the instructions
+    Object[] arg = new Object[10]; ///< the arguments to the instructions
+    int size = 0;                  ///< the number of instruction/argument pairs
+
+    JSScope parentScope;           ///< the default scope to use as a parent scope when executing this
+
+
+    // Public //////////////////////////////////////////////////////////////////////////////
+
+    // FEATURE: make sure that this can only be called from the Scheduler...
+    /** if you enqueue a function, it gets invoked in its own pauseable context */
+    public void perform() throws JSExn {
+        Interpreter i = new Interpreter(this, true, new JSArray());
+        i.resume();
+    }
+
+    /** parse and compile a function */
+    public static JSFunction _fromReader(String sourceName, int firstLine, Reader sourceCode) throws IOException {
+        JSFunction ret = new JSFunction(sourceName, firstLine, null);
+        if (sourceCode == null) return ret;
+        Parser p = new Parser(sourceCode, sourceName, firstLine);
+        while(true) {
+            int s = ret.size;
+            p.parseStatement(ret, null);
+            if (s == ret.size) break;
+        }
+        ret.add(-1, LITERAL, null); 
+        ret.add(-1, RETURN);
+        return ret;
+    }
+
+    public JSFunction _cloneWithNewParentScope(JSScope s) {
+        JSFunction ret = new JSFunction(sourceName, firstLine, s);
+        // Reuse the same op, arg, line, and size variables for the new "instance" of the function
+        // NOTE: Neither *this* function nor the new function should be modified after this call
+        ret.op = this.op;
+        ret.arg = this.arg;
+        ret.line = this.line;
+        ret.size = this.size;
+        ret.numFormalArgs = this.numFormalArgs;
+        return ret;
+    }
+
+    /** Note: code gets run in an <i>unpauseable</i> context. */
+    public Object call(Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        JSArray args = new JSArray();
+        if (nargs > 0) args.addElement(a0);
+        if (nargs > 1) args.addElement(a1);
+        if (nargs > 2) args.addElement(a2);
+        for(int i=3; i<nargs; i++) args.addElement(rest[i-3]);
+        Interpreter cx = new Interpreter(this, false, args);
+        return cx.resume();
+    }
+
+    public JSScope getParentScope() { return parentScope; }
+
+    // Adding and Altering Bytecodes ///////////////////////////////////////////////////
+
+    JSFunction(String sourceName, int firstLine, JSScope parentScope) {
+        this.sourceName = sourceName;
+        this.firstLine = firstLine;
+        this.parentScope = parentScope;
+    }
+
+    int get(int pos) { return op[pos]; }
+    Object getArg(int pos) { return arg[pos]; }
+    void set(int pos, int op_, Object arg_) { op[pos] = op_; arg[pos] = arg_; }
+    void set(int pos, Object arg_) { arg[pos] = arg_; }
+    int pop() { size--; arg[size] = null; return op[size]; }
+    void paste(JSFunction other) { for(int i=0; i<other.size; i++) add(other.line[i], other.op[i], other.arg[i]); }
+    JSFunction add(int line, int op_) { return add(line, op_, null); }
+    JSFunction add(int line, int op_, Object arg_) {
+        if (size == op.length - 1) {
+            int[] line2 = new int[op.length * 2]; System.arraycopy(this.line, 0, line2, 0, op.length); this.line = line2;
+            Object[] arg2 = new Object[op.length * 2]; System.arraycopy(arg, 0, arg2, 0, arg.length); arg = arg2;
+            int[] op2 = new int[op.length * 2]; System.arraycopy(op, 0, op2, 0, op.length); op = op2;
+        }
+        this.line[size] = line;
+        op[size] = op_;
+        arg[size] = arg_;
+        size++;
+        return this;
+    }
+    
+
+    // Debugging //////////////////////////////////////////////////////////////////////
+
+    public String toString() { return "JSFunction [" + sourceName + ":" + firstLine + "]"; }
+
+    public String dump() {
+        StringBuffer sb = new StringBuffer(1024);
+        sb.append("\n" + sourceName + ": " + firstLine + "\n");
+        for (int i=0; i < size; i++) {
+            sb.append(i).append(" (").append(line[i]).append(") :");
+            if (op[i] < 0) sb.append(bytecodeToString[-op[i]]);
+            else sb.append(codeToString[op[i]]);
+            sb.append(" ");
+            sb.append(arg[i] == null ? "(no arg)" : arg[i]);
+            if((op[i] == JF || op[i] == JT || op[i] == JMP) && arg[i] != null && arg[i] instanceof Number) {
+                sb.append(" jump to ").append(i+((Number) arg[i]).intValue());
+            } else  if(op[i] == TRY) {
+                int[] jmps = (int[]) arg[i];
+                sb.append(" catch: ").append(jmps[0] < 0 ? "No catch block" : ""+(i+jmps[0]));
+                sb.append(" finally: ").append(jmps[1] < 0 ? "No finally block" : ""+(i+jmps[1]));
+            }
+            sb.append("\n");
+        }
+        return sb.toString();
+    } 
+
+
+}
+
diff --git a/src/org/ibex/js/JSMath.java b/src/org/ibex/js/JSMath.java
new file mode 100644 (file)
index 0000000..04828ed
--- /dev/null
@@ -0,0 +1,91 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL ]
+
+package org.ibex.js; 
+
+/** The JavaScript Math object */
+public class JSMath extends JS {
+
+    public static JSMath singleton = new JSMath();
+
+    private static final Double E       = new Double(java.lang.Math.E);
+    private static final Double PI      = new Double(java.lang.Math.PI);
+    private static final Double LN10    = new Double(java.lang.Math.log(10));
+    private static final Double LN2     = new Double(java.lang.Math.log(2));
+    private static final Double LOG10E  = new Double(1/java.lang.Math.log(10));
+    private static final Double LOG2E   = new Double(1/java.lang.Math.log(2));
+    private static final Double SQRT1_2 = new Double(1/java.lang.Math.sqrt(2));
+    private static final Double SQRT2   = new Double(java.lang.Math.sqrt(2));
+
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        switch(nargs) {
+            case 0: {
+                //#switch(method)
+                case "random": return new Double(java.lang.Math.random());
+                //#end
+                break;
+            }
+            case 1: {
+                //#switch(method)
+                case "ceil": return new Long((long)java.lang.Math.ceil(toDouble(a0)));
+                case "floor": return new Long((long)java.lang.Math.floor(toDouble(a0)));
+                case "round": return new Long((long)java.lang.Math.round(toDouble(a0)));
+                case "abs": return new Double(java.lang.Math.abs(toDouble(a0)));
+                case "sin": return new Double(java.lang.Math.sin(toDouble(a0)));
+                case "cos": return new Double(java.lang.Math.cos(toDouble(a0)));
+                case "tan": return new Double(java.lang.Math.tan(toDouble(a0)));
+                case "asin": return new Double(java.lang.Math.asin(toDouble(a0)));
+                case "acos": return new Double(java.lang.Math.acos(toDouble(a0)));
+                case "atan": return new Double(java.lang.Math.atan(toDouble(a0)));
+                case "sqrt": return new Double(java.lang.Math.sqrt(toDouble(a0)));
+                case "exp": return new Double(java.lang.Math.exp(toDouble(a0)));
+                case "log": return new Double(java.lang.Math.log(toDouble(a0)));
+                //#end
+                break;
+            }
+            case 2: {
+                //#switch(method)
+                case "min": return new Double(java.lang.Math.min(toDouble(a0), toDouble(a1)));
+                case "max": return new Double(java.lang.Math.max(toDouble(a0), toDouble(a1)));
+                case "pow": return new Double(java.lang.Math.pow(toDouble(a0), toDouble(a1)));
+                case "atan2": return new Double(java.lang.Math.atan2(toDouble(a0), toDouble(a1)));
+                //#end
+                break;
+            }
+        }
+        return super.callMethod(method, a0, a1, a2, rest, nargs);
+    }
+
+    public void put(Object key, Object val) { }
+
+    public Object get(Object key) throws JSExn {
+        //#switch(key)
+        case "E": return E;
+        case "LN10": return LN10;
+        case "LN2": return LN2;
+        case "LOG10E": return LOG10E;
+        case "LOG2E": return LOG2E;
+        case "PI": return PI;
+        case "SQRT1_2": return SQRT1_2;
+        case "SQRT2": return SQRT2;
+        case "ceil": return METHOD;
+        case "floor": return METHOD;
+        case "round": return METHOD;
+        case "min": return METHOD;
+        case "max": return METHOD;
+        case "pow": return METHOD;
+        case "atan2": return METHOD;
+        case "abs": return METHOD;
+        case "sin": return METHOD;
+        case "cos": return METHOD;
+        case "tan": return METHOD;
+        case "asin": return METHOD;
+        case "acos": return METHOD;
+        case "atan": return METHOD;
+        case "sqrt": return METHOD;
+        case "exp": return METHOD;
+        case "log": return METHOD;
+        case "random": return METHOD;
+        //#end
+        return super.get(key);
+    }
+}
diff --git a/src/org/ibex/js/JSReflection.java b/src/org/ibex/js/JSReflection.java
new file mode 100644 (file)
index 0000000..69fb9b3
--- /dev/null
@@ -0,0 +1,80 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.io.*;
+import java.util.*;
+import java.lang.reflect.*;
+
+/** Automatic JS-ification via Reflection (not for use in the core) */
+public class JSReflection extends JS {
+
+    public static Object wrap(Object o) throws JSExn {
+        if (o == null) return null;
+        if (o instanceof String) return o;
+        if (o instanceof Boolean) return o;
+        if (o instanceof Number) return o;
+        if (o instanceof JS) return o;
+        if (o instanceof Object[]) {
+            // FIXME: get element type here
+        }
+        throw new JSExn("Reflection object tried to return a " + o.getClass().getName());
+    }
+
+    public static class Array extends JS {
+        final Object[] arr;
+        public Array(Object[] arr) { this.arr = arr; }
+        public Enumeration keys() throws JSExn { return new CounterEnumeration(arr.length); }
+        public Object get(Object key) throws JSExn { return wrap(arr[toInt(key)]); }
+        public void put(Object key, Object val) throws JSExn { throw new JSExn("can't write to org.ibex.js.Reflection.Array's"); }
+    }
+
+    // FIXME public static class Hash { }
+    // FIXME public Enumeration keys() throws JSExn {  }
+
+    public Object get(Object key) throws JSExn {
+        String k = toString(key);
+        try {
+            Field f = this.getClass().getField(k);
+            return wrap(f.get(this));
+        } catch (NoSuchFieldException nfe) {
+        } catch (IllegalAccessException nfe) {
+        } catch (SecurityException nfe) { }
+
+        try {
+            Method[] methods = this.getClass().getMethods();
+            for(int i=0; i<methods.length; i++) if (methods[i].getName().equals(k)) return METHOD;
+        } catch (SecurityException nfe) { }
+        return null;
+    }
+
+    public void put(Object key, Object val) throws JSExn {
+        throw new JSExn("put() not supported yet");
+    }
+
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        String k = toString(method);
+        try {
+            Method[] methods = this.getClass().getMethods();
+            for(int j=0; j<methods.length; j++) {
+                if (methods[j].getName().equals(k) && methods[j].getParameterTypes().length == nargs) {
+                    Object[] args = new Object[nargs];
+                    for(int i = 0; i<args.length; i++) {
+                        if (i==0) args[i] = a0;
+                        else if (i==1) args[i] = a1;
+                        else if (i==2) args[i] = a2;
+                        else args[i] = rest[i-3];
+                    }
+                    return wrap(methods[j].invoke(this, args));
+                }
+            }
+        } catch (IllegalAccessException nfe) {
+        } catch (InvocationTargetException it) {
+            Throwable ite = it.getTargetException();
+            if (ite instanceof JSExn) throw ((JSExn)ite);
+            JS.warn(ite);
+            throw new JSExn("unhandled reflected exception: " + ite.toString());
+        } catch (SecurityException nfe) { }
+        throw new JSExn("called a reflection method with the wrong number of arguments");
+    }    
+} 
diff --git a/src/org/ibex/js/JSRegexp.java b/src/org/ibex/js/JSRegexp.java
new file mode 100644 (file)
index 0000000..da22d2e
--- /dev/null
@@ -0,0 +1,336 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+import gnu.regexp.*;
+
+/** A JavaScript regular expression object */
+public class JSRegexp extends JS {
+    private boolean global;
+    private RE re;
+    private int lastIndex;
+    
+    public JSRegexp(Object arg0, Object arg1) throws JSExn {
+        if(arg0 instanceof JSRegexp) {
+            JSRegexp r = (JSRegexp) arg0;
+            this.global = r.global;
+            this.re = r.re;
+            this.lastIndex = r.lastIndex;
+        } else {
+            String pattern = (String)arg0;
+            String sFlags = null;
+            int flags = 0;
+            if(arg1 != null) sFlags = (String)arg1;
+            if(sFlags == null) sFlags = "";
+            for(int i=0;i<sFlags.length();i++) {
+                switch(sFlags.charAt(i)) {
+                    case 'i': flags |= RE.REG_ICASE; break;
+                    case 'm': flags |= RE.REG_MULTILINE; break;
+                    case 'g': global = true; break;
+                    default: throw new JSExn("Invalid flag in regexp \"" + sFlags.charAt(i) + "\"");
+                }
+            }
+            re = newRE(pattern,flags);
+            put("source", pattern);
+            put("global", B(global));
+            put("ignoreCase", B(flags & RE.REG_ICASE));
+            put("multiline", B(flags & RE.REG_MULTILINE));
+        }
+    }
+
+    public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+        switch(nargs) {
+            case 1: {
+                //#switch(method)
+                case "exec": {
+                    String s = (String)a0;
+                    int start = global ? lastIndex : 0;
+                    if(start < 0 || start >= s.length()) { lastIndex = 0; return null; }
+                    REMatch match = re.getMatch(s,start);
+                    if(global) lastIndex = match == null ? s.length() : match.getEndIndex();
+                    return match == null ? null : matchToExecResult(match,re,s);
+                }
+                case "test": {
+                    String s = (String)a0;
+                    if (!global) return B(re.getMatch(s) != null);
+                    int start = global ? lastIndex : 0;
+                    if(start < 0 || start >= s.length()) { lastIndex = 0; return null; }
+                    REMatch match = re.getMatch(s,start);
+                    lastIndex = match != null ? s.length() : match.getEndIndex();
+                    return B(match != null);
+                }
+                case "toString": return toString(a0);
+                case "stringMatch": return stringMatch(a0,a1);
+                case "stringSearch": return stringSearch(a0,a1);
+                //#end
+                break;
+            }
+            case 2: {
+                //#switch(method)
+                case "stringReplace": return stringReplace(a0, a1,a2);
+                //#end
+                break;
+            }
+        }
+        return super.callMethod(method, a0, a1, a2, rest, nargs);
+    }
+    
+    public Object get(Object key) throws JSExn {
+        //#switch(key)
+        case "exec": return METHOD;
+        case "test": return METHOD;
+        case "toString": return METHOD;
+        case "lastIndex": return N(lastIndex);
+        //#end
+        return super.get(key);
+    }
+    
+    public void put(Object key, Object value) throws JSExn {
+        if(key.equals("lastIndex")) lastIndex = JS.toNumber(value).intValue();
+        super.put(key,value);
+    }
+  
+    private static Object matchToExecResult(REMatch match, RE re, String s) {
+        try {
+            JS ret = new JS();
+            ret.put("index", N(match.getStartIndex()));
+            ret.put("input",s);
+            int n = re.getNumSubs();
+            ret.put("length", N(n+1));
+            ret.put("0",match.toString());
+            for(int i=1;i<=n;i++) ret.put(Integer.toString(i),match.toString(i));
+            return ret;
+        } catch (JSExn e) {
+            throw new Error("this should never happen");
+        }
+    }
+    
+    public String toString() {
+        try {
+            StringBuffer sb = new StringBuffer();
+            sb.append('/');
+            sb.append(get("source"));
+            sb.append('/');
+            if(global) sb.append('g');
+            if(Boolean.TRUE.equals(get("ignoreCase"))) sb.append('i');
+            if(Boolean.TRUE.equals(get("multiline"))) sb.append('m');
+            return sb.toString();
+        } catch (JSExn e) {
+            throw new Error("this should never happen");
+        }
+    }
+    
+    public static Object stringMatch(Object o, Object arg0) throws JSExn {
+        String s = o.toString();
+        RE re;
+        JSRegexp regexp = null;
+        if(arg0 instanceof JSRegexp) {
+            regexp = (JSRegexp) arg0;
+            re = regexp.re;
+        } else {
+            re = newRE(arg0.toString(),0);
+        }
+        
+        if(regexp == null) {
+            REMatch match = re.getMatch(s);
+            return matchToExecResult(match,re,s);
+        }
+        if(!regexp.global) return regexp.callMethod("exec", s, null, null, null, 1);
+        
+        JSArray ret = new JSArray();
+        REMatch[] matches = re.getAllMatches(s);
+        for(int i=0;i<matches.length;i++) ret.addElement(matches[i].toString());
+        regexp.lastIndex = matches.length > 0 ? matches[matches.length-1].getEndIndex() : s.length();
+        return ret;
+    }
+    
+    public static Object stringSearch(Object o, Object arg0) throws JSExn  {
+        String s = o.toString();
+        RE re = arg0 instanceof JSRegexp ? ((JSRegexp)arg0).re : newRE(arg0.toString(),0);
+        REMatch match = re.getMatch(s);
+        return match == null ? N(-1) : N(match.getStartIndex());
+    }
+    
+    public static Object stringReplace(Object o, Object arg0, Object arg1) throws JSExn {
+        String s = o.toString();
+        RE re;
+        JSFunction replaceFunc = null;
+        String replaceString = null;
+        JSRegexp regexp = null;
+        if(arg0 instanceof JSRegexp) {
+            regexp = (JSRegexp) arg0;
+            re = regexp.re;
+        } else {
+            re = newRE(arg0.toString(),0);
+        }
+        if(arg1 instanceof JSFunction)
+            replaceFunc = (JSFunction) arg1;
+        else
+            replaceString = JS.toString(arg1.toString());
+        REMatch[] matches;
+        if(regexp != null && regexp.global) {
+            matches = re.getAllMatches(s);
+            if(regexp != null) {
+                if(matches.length > 0)
+                    regexp.lastIndex = matches[matches.length-1].getEndIndex();
+                else
+                    regexp.lastIndex = s.length();
+            }
+        } else {
+            REMatch match = re.getMatch(s);
+            if(match != null)
+                matches = new REMatch[]{ match };
+            else
+                matches = new REMatch[0];
+        }
+        
+        StringBuffer sb = new StringBuffer(s.length());
+        int pos = 0;
+        char[] sa = s.toCharArray();
+        for(int i=0;i<matches.length;i++) {
+            REMatch match = matches[i];
+            sb.append(sa,pos,match.getStartIndex()-pos);
+            pos = match.getEndIndex();
+            if(replaceFunc != null) {
+                int n = (regexp == null ? 0 : re.getNumSubs());
+                int numArgs = 3 + n;
+                Object[] rest = new Object[numArgs - 3];
+                Object a0 = match.toString();
+                Object a1 = null;
+                Object a2 = null;
+                for(int j=1;j<=n;j++)
+                    switch(j) {
+                        case 1: a1 = match.toString(j); break;
+                        case 2: a2 = match.toString(j); break;
+                        default: rest[j - 3] = match.toString(j); break;
+                    }
+                switch(numArgs) {
+                    case 3:
+                        a1 = N(match.getStartIndex());
+                        a2 = s;
+                        break;
+                    case 4:
+                        a2 = N(match.getStartIndex());
+                        rest[0] = s;
+                        break;
+                    default:
+                        rest[rest.length - 2] = N(match.getStartIndex());
+                        rest[rest.length - 1] = s;
+                }
+
+                // note: can't perform pausing operations in here
+                sb.append((String)replaceFunc.call(a0, a1, a2, rest, numArgs));
+
+            } else {
+                sb.append(mySubstitute(match,replaceString,s));
+            }
+        }
+        int end = matches.length == 0 ? 0 : matches[matches.length-1].getEndIndex();
+        sb.append(sa,end,sa.length-end);
+        return sb.toString();
+    }
+    
+    private static String mySubstitute(REMatch match, String s, String source) {
+        StringBuffer sb = new StringBuffer();
+        int i,n;
+        char c,c2;
+        for(i=0;i<s.length()-1;i++) {
+           c = s.charAt(i);
+            if(c != '$') {
+                sb.append(c);
+                continue;
+            }
+            i++;
+            c = s.charAt(i);
+            switch(c) {
+                case '0': case '1': case '2': case '3': case '4':
+                case '5': case '6': case '7': case '8': case '9':
+                    if(i < s.length()-1 && (c2 = s.charAt(i+1)) >= '0' && c2 <= '9') {
+                        n = (c - '0') * 10 + (c2 - '0');
+                        i++;
+                    } else {
+                        n = c - '0';
+                    }
+                    if(n > 0)
+                        sb.append(match.toString(n));
+                    break;
+                case '$':
+                    sb.append('$'); break;
+                case '&':
+                    sb.append(match.toString()); break;
+                case '`':
+                    sb.append(source.substring(0,match.getStartIndex())); break;
+                case '\'':
+                    sb.append(source.substring(match.getEndIndex())); break;
+                default:
+                    sb.append('$');
+                    sb.append(c);
+            }
+        }
+        if(i < s.length()) sb.append(s.charAt(i));
+        return sb.toString();
+    }
+                    
+    
+    public static Object stringSplit(String s, Object arg0, Object arg1, int nargs) {
+        int limit = nargs < 2 ? Integer.MAX_VALUE : JS.toInt(arg1);
+        if(limit < 0) limit = Integer.MAX_VALUE;
+        if(limit == 0) return new JSArray();
+        
+        RE re = null;
+        JSRegexp regexp = null;
+        String sep = null;
+        JSArray ret = new JSArray();
+        int p = 0;
+        
+        if(arg0 instanceof JSRegexp) {
+            regexp = (JSRegexp) arg0;
+            re = regexp.re;
+        } else {
+            sep = arg0.toString();
+        }
+        
+        // special case this for speed. additionally, the code below doesn't properly handle
+        // zero length strings
+        if(sep != null && sep.length()==0) {
+            int len = s.length();
+            for(int i=0;i<len;i++)
+                ret.addElement(s.substring(i,i+1));
+            return ret;
+        }
+        
+        OUTER: while(p < s.length()) {
+            if(re != null) {
+                REMatch m = re.getMatch(s,p);
+                if(m == null) break OUTER;
+                boolean zeroLength = m.getStartIndex() == m.getEndIndex();
+                ret.addElement(s.substring(p,zeroLength ? m.getStartIndex()+1 : m.getStartIndex()));
+                p = zeroLength ? p + 1 : m.getEndIndex();
+                if(!zeroLength) {
+                    for(int i=1;i<=re.getNumSubs();i++) {
+                        ret.addElement(m.toString(i));
+                        if(ret.length() == limit) break OUTER;
+                    }
+                }
+            } else {
+                int x = s.indexOf(sep,p);
+                if(x == -1) break OUTER;
+                ret.addElement(s.substring(p,x));
+                p = x + sep.length();
+            }
+            if(ret.length() == limit) break;
+        }
+        if(p < s.length() && ret.length() != limit)
+            ret.addElement(s.substring(p));
+        return ret;
+    }
+    
+    public static RE newRE(String pattern, int flags) throws JSExn {
+        try {
+            return new RE(pattern,flags,RESyntax.RE_SYNTAX_PERL5);
+        } catch(REException e) {
+            throw new JSExn(e.toString());
+        }
+    }
+    
+    public String typeName() { return "regexp"; }
+}
diff --git a/src/org/ibex/js/JSScope.java b/src/org/ibex/js/JSScope.java
new file mode 100644 (file)
index 0000000..675ddc7
--- /dev/null
@@ -0,0 +1,153 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+// FIXME: should allow parentScope to be a JS, not a JSScope
+/** Implementation of a JavaScript Scope */
+public class JSScope extends JS { 
+
+    private JSScope parentScope;
+
+    private static final Object NULL_PLACEHOLDER = new Object();
+
+    public JSScope(JSScope parentScope) { this.parentScope = parentScope; }
+    public void declare(String s) throws JSExn { super.put(s, NULL_PLACEHOLDER); }
+    public JSScope getParentScope() { return parentScope; }
+
+    public Object get(Object key) throws JSExn {
+        Object o = super.get(key);
+        if (o != null) return o == NULL_PLACEHOLDER ? null : o;
+        else return parentScope == null ? null : parentScope.get(key);
+    }
+
+    public boolean has(Object key) throws JSExn { return super.get(key) != null; }
+    public void put(Object key, Object val) throws JSExn {
+        if (parentScope != null && !has(key)) parentScope.put(key, val);
+        else super.put(key, val == null ? NULL_PLACEHOLDER : val);
+    }
+    
+    public JSScope top() { 
+        JSScope s = this;
+        while(s.parentScope != null) s = s.parentScope;
+        return s;
+    }
+
+    public static class Global extends JSScope {
+        private final static Double NaN = new Double(Double.NaN);
+        private final static Double POSITIVE_INFINITY = new Double(Double.POSITIVE_INFINITY);
+
+        public Global() { super(null); }
+        public Object get(Object key) throws JSExn {
+            //#switch(key)
+            case "NaN": return NaN;
+            case "Infinity": return POSITIVE_INFINITY;
+            case "undefined": return null;
+            case "stringFromCharCode": return METHOD;
+            case "parseInt": return METHOD;
+            case "isNaN": return METHOD;
+            case "isFinite": return METHOD;
+            case "decodeURI": return METHOD;
+            case "decodeURIComponent": return METHOD;
+            case "encodeURI": return METHOD;
+            case "encodeURIComponent": return METHOD;
+            case "escape": return METHOD;
+            case "unescape": return METHOD;
+            case "parseInt": return METHOD;
+            //#end
+            return super.get(key);
+        }
+
+        public Object callMethod(Object method, Object a0, Object a1, Object a2, Object[] rest, int nargs) throws JSExn {
+            switch(nargs) {
+                case 0: {
+                    //#switch(method)
+                    case "stringFromCharCode":
+                        char buf[] = new char[nargs];
+                        for(int i=0; i<nargs; i++) buf[i] = (char)(JS.toInt(i==0?a0:i==1?a1:i==2?a2:rest[i-3]) & 0xffff);
+                        return new String(buf);
+                    //#end
+                    break;
+                }
+                case 1: {
+                    //#switch(method)
+                    case "parseInt": return parseInt(a0, N(0));
+                    case "isNaN": { double d = toDouble(a0); return d == d ? F : T; }
+                    case "isFinite": { double d = toDouble(a0); return (d == d && !Double.isInfinite(d)) ? T : F; }
+                    case "decodeURI": throw new JSExn("unimplemented");
+                    case "decodeURIComponent": throw new JSExn("unimplemented");
+                    case "encodeURI": throw new JSExn("unimplemented");
+                    case "encodeURIComponent": throw new JSExn("unimplemented");
+                    case "escape": throw new JSExn("unimplemented");
+                    case "unescape": throw new JSExn("unimplemented");
+                    //#end
+                    break;
+                }
+                case 2: {
+                    //#switch(method)
+                    case "parseInt": return parseInt(a0, a1);
+                    //#end
+                    break;
+                }
+            }
+            return super.callMethod(method, a0, a1, a2, rest, nargs);
+        }
+
+        private Object parseInt(Object arg, Object r) {
+            int radix = JS.toInt(r);
+            String s = (String)arg;
+            int start = 0;
+            int length = s.length();
+            int sign = 1;
+            long n = 0;
+            if(radix != 0 && (radix < 2 || radix > 36)) return NaN;
+            while(start < length && Character.isWhitespace(s.charAt(start))) start++;
+            if((length >= start+1) && (s.charAt(start) == '+' || s.charAt(start) == '-')) {
+                sign = s.charAt(start) == '+' ? 1 : -1;
+                start++;
+            }
+            if(radix == 0 && length >= start+1 && s.charAt(start) == '0') {
+                start++;
+                if(length >= start+1 && (s.charAt(start) == 'x' || s.charAt(start) == 'X')) {
+                    start++;
+                    radix = 16;
+                } else {
+                    radix = 8;
+                    if(length == start || Character.digit(s.charAt(start+1),8)==-1) return JS.ZERO;
+                }
+            }
+            if(radix == 0) radix = 10;
+            if(length == start || Character.digit(s.charAt(start),radix) == -1) return NaN;
+            // try the fast way first
+            try {
+                String s2 = start == 0 ? s : s.substring(start);
+                return JS.N(sign*Integer.parseInt(s2,radix));
+            } catch(NumberFormatException e) { }
+            // fall through to a slower but emca-compliant method
+            for(int i=start;i<length;i++) {
+                int digit = Character.digit(s.charAt(i),radix);
+                if(digit < 0) break;
+                n = n*radix + digit;
+                if(n < 0) return NaN; // overflow;
+            }
+            if(n <= Integer.MAX_VALUE) return JS.N(sign*(int)n);
+            return JS.N((long)sign*n);
+        }
+
+        private Object parseFloat(Object arg) {
+            String s = (String)arg;
+            int start = 0;
+            int length = s.length();
+            while(start < length && Character.isWhitespace(s.charAt(0))) start++;
+            int end = length;
+            // as long as the string has no trailing garbage,this is fast, its slow with
+            // trailing garbage
+            while(start < end) {
+                try {
+                    return JS.N(s.substring(start,length));
+                } catch(NumberFormatException e) { }
+                end--;
+            }
+            return NaN;
+        }
+    }
+}
+
diff --git a/src/org/ibex/js/Lexer.java b/src/org/ibex/js/Lexer.java
new file mode 100644 (file)
index 0000000..ac6f6e1
--- /dev/null
@@ -0,0 +1,398 @@
+// Derived from org.mozilla.javascript.TokenStream [NPL]
+
+/**
+ * The contents of this file are subject to the Netscape Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/NPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Initial Developer of the Original Code is Netscape
+ * Communications Corporation.
+ *
+ * Contributor(s): Roger Lawrence, Mike McCabe
+ */
+
+package org.ibex.js;
+import java.io.*;
+
+/** Lexes a stream of characters into a stream of Tokens */
+class Lexer implements Tokens {
+
+    /** for debugging */
+    public static void main(String[] s) throws IOException {
+        Lexer l = new Lexer(new InputStreamReader(System.in), "stdin", 0);
+        int tok = 0;
+        while((tok = l.getToken()) != -1) System.out.println(codeToString[tok]);
+    }
+
+    /** the token that was just parsed */
+    protected int op;
+   /** the most recently parsed token, <i>regardless of pushbacks</i> */
+    protected int mostRecentlyReadToken;
+
+    /** if the token just parsed was a NUMBER, this is the numeric value */
+    protected Number number = null;
+
+    /** if the token just parsed was a NAME or STRING, this is the string value */
+    protected String string = null;
+
+    /** the line number of the most recently <i>lexed</i> token */
+    protected int line = 0;
+
+    /** the line number of the most recently <i>parsed</i> token */
+    protected int parserLine = 0;
+
+    /** the column number of the current token */
+    protected int col = 0;
+
+    /** the name of the source code file being lexed */
+    protected String sourceName;
+
+    private SmartReader in;
+    public Lexer(Reader r, String sourceName, int line) throws IOException {
+        this.sourceName = sourceName;
+        this.line = line;
+        this.parserLine = line;
+        in = new SmartReader(r);
+    }
+
+
+    // Predicates ///////////////////////////////////////////////////////////////////////
+
+    private static boolean isAlpha(int c) { return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); }
+    private static boolean isDigit(int c) { return (c >= '0' && c <= '9'); }
+    private static int xDigitToInt(int c) {
+        if ('0' <= c && c <= '9') return c - '0';
+        else if ('a' <= c && c <= 'f') return c - ('a' - 10);
+        else if ('A' <= c && c <= 'F') return c - ('A' - 10);
+        else return -1;
+    }
+
+    
+    // Token Subtype Handlers /////////////////////////////////////////////////////////
+
+    private int getKeyword(String name) throws IOException {
+        //#switch(name)
+        case "if": return IF;
+        case "lt": return LT;
+        case "gt": return GT;
+        case "in": return IN;
+        case "do": return DO;
+        case "and": return AND;
+        case "or": return OR;
+        case "for": return FOR;
+        case "int": return RESERVED;
+        case "new": return RESERVED;
+        case "try": return TRY;
+        case "var": return VAR;
+        case "byte": return RESERVED;
+        case "case": return CASE;
+        case "char": return RESERVED;
+        case "else": return ELSE;
+        case "enum": return RESERVED;
+        case "goto": return RESERVED;
+        case "long": return RESERVED;
+        case "null": return NULL;
+        case "true": return TRUE;
+        case "with": return RESERVED;
+        case "void": return RESERVED;
+        case "class": return RESERVED;
+        case "break": return BREAK;
+        case "while": return WHILE;
+        case "false": return FALSE;
+        case "const": return RESERVED;
+        case "final": return RESERVED;
+        case "super": return RESERVED;
+        case "throw": return THROW;
+        case "catch": return CATCH;
+        case "class": return RESERVED;
+        case "delete": return RESERVED;
+        case "return": return RETURN;
+        case "throws": return RESERVED;
+        case "double": return RESERVED;
+        case "assert": return ASSERT;
+        case "public": return RESERVED;
+        case "switch": return SWITCH;
+        case "typeof": return TYPEOF;
+        case "package": return RESERVED;
+        case "default": return DEFAULT;
+        case "finally": return FINALLY;
+        case "boolean": return RESERVED;
+        case "private": return RESERVED;
+        case "extends": return RESERVED;
+        case "abstract": return RESERVED;
+        case "continue": return CONTINUE;
+        case "debugger": return RESERVED;
+        case "function": return FUNCTION;
+        case "volatile": return RESERVED;
+        case "interface": return RESERVED;
+        case "protected": return RESERVED;
+        case "transient": return RESERVED;
+        case "implements": return RESERVED;
+        case "instanceof": return RESERVED;
+        case "synchronized": return RESERVED;
+        //#end
+        return -1;
+    }
+
+    private int getIdentifier(int c) throws IOException {
+        in.startString();
+        while (Character.isJavaIdentifierPart((char)(c = in.read())));
+        in.unread();
+        String str = in.getString();
+        int result = getKeyword(str);
+        if (result == RESERVED) throw new LexerException("The reserved word \"" + str + "\" is not permitted in Ibex scripts");
+        if (result != -1) return result;
+        this.string = str.intern();
+        return NAME;
+    }
+    
+    private int getNumber(int c) throws IOException {
+        int base = 10;
+        in.startString();
+        double dval = Double.NaN;
+        long longval = 0;
+        boolean isInteger = true;
+        
+        // figure out what base we're using
+        if (c == '0') {
+            if (Character.toLowerCase((char)(c = in.read())) == 'x') { base = 16; in.startString(); }
+            else if (isDigit(c)) base = 8;
+        }
+        
+        while (0 <= xDigitToInt(c) && !(base < 16 && isAlpha(c))) c = in.read();
+        if (base == 10 && (c == '.' || c == 'e' || c == 'E')) {
+            isInteger = false;
+            if (c == '.') do { c = in.read(); } while (isDigit(c));
+            if (c == 'e' || c == 'E') {
+                c = in.read();
+                if (c == '+' || c == '-') c = in.read();
+                if (!isDigit(c)) throw new LexerException("float listeral did not have an exponent value");
+                do { c = in.read(); } while (isDigit(c));
+            }
+        }
+        in.unread();
+
+        String numString = in.getString();
+        if (base == 10 && !isInteger) {
+            try { dval = (Double.valueOf(numString)).doubleValue(); }
+            catch (NumberFormatException ex) { throw new LexerException("invalid numeric literal: \"" + numString + "\""); }
+        } else {
+            if (isInteger) {
+                longval = Long.parseLong(numString, base);
+                dval = (double)longval;
+            } else {
+                dval = Double.parseDouble(numString);
+                longval = (long) dval;
+                if (longval == dval) isInteger = true;
+            }
+        }
+        
+        if (!isInteger) this.number = JS.N(dval);
+        else this.number = JS.N(longval);
+        return NUMBER;
+    }
+    
+    private int getString(int c) throws IOException {
+        StringBuffer stringBuf = null;
+        int quoteChar = c;
+        c = in.read();
+        in.startString(); // start after the first "
+        while(c != quoteChar) {
+            if (c == '\n' || c == -1) throw new LexerException("unterminated string literal");
+            if (c == '\\') {
+                if (stringBuf == null) {
+                    in.unread();   // Don't include the backslash
+                    stringBuf = new StringBuffer(in.getString());
+                    in.read();
+                }
+                switch (c = in.read()) {
+                case 'b': c = '\b'; break;
+                case 'f': c = '\f'; break;
+                case 'n': c = '\n'; break;
+                case 'r': c = '\r'; break;
+                case 't': c = '\t'; break;
+                case 'v': c = '\u000B'; break;
+                case '\\': c = '\\'; break;
+                case 'u': {
+                    int v = 0;
+                    for(int i=0; i<4; i++) {
+                        int ci = in.read();
+                        if (!((ci >= '0' && ci <= '9') || (ci >= 'a' && ci <= 'f') || (ci >= 'A' && ci <= 'F')))
+                            throw new LexerException("illegal character '" + ((char)c) + "' in \\u unicode escape sequence");
+                        v = (v << 8) | Integer.parseInt(ci + "", 16);
+                    }
+                    c = (char)v;
+                    break;
+                }
+                default:
+                    // just use the character that was escaped
+                    break;
+                }
+            }
+            if (stringBuf != null) stringBuf.append((char) c);
+            c = in.read();
+        }
+        if (stringBuf != null) this.string = stringBuf.toString().intern();
+        else {
+            in.unread(); // miss the trailing "
+            this.string = in.getString().intern();
+            in.read();
+        }
+        return STRING;
+    }
+
+    private int _getToken() throws IOException {
+        int c;
+        do { c = in.read(); } while (c == '\u0020' || c == '\u0009' || c == '\u000C' || c == '\u000B' || c == '\n' );
+        if (c == -1) return -1;
+        if (c == '\\' || Character.isJavaIdentifierStart((char)c)) return getIdentifier(c);
+        if (isDigit(c) || (c == '.' && isDigit(in.peek()))) return getNumber(c);
+        if (c == '"' || c == '\'') return getString(c);
+        switch (c) {
+        case ';': return SEMI;
+        case '[': return LB;
+        case ']': return RB;
+        case '{': return LC;
+        case '}': return RC;
+        case '(': return LP;
+        case ')': return RP;
+        case ',': return COMMA;
+        case '?': return HOOK;
+        case ':': return !in.match(':') ? COLON : in.match('=') ? GRAMMAR : le(":: is not a valid token");
+        case '.': return DOT;
+        case '|': return in.match('|') ? OR : (in.match('=') ? ASSIGN_BITOR : BITOR);
+        case '^': return in.match('=') ? ASSIGN_BITXOR : BITXOR;
+        case '&': return in.match('&') ? AND : in.match('=') ? ASSIGN_BITAND : BITAND;
+        case '=': return !in.match('=') ? ASSIGN : in.match('=') ? SHEQ : EQ;
+        case '!': return !in.match('=') ? BANG : in.match('=') ? SHNE : NE;
+        case '%': return in.match('=') ? ASSIGN_MOD : MOD;
+        case '~': return BITNOT;
+        case '+': return in.match('=') ? ASSIGN_ADD : in.match('+') ? (in.match('=') ? ADD_TRAP : INC) : ADD;
+        case '-': return in.match('=') ? ASSIGN_SUB: in.match('-') ? (in.match('=') ? DEL_TRAP : DEC) : SUB;
+        case '*': return in.match('=') ? ASSIGN_MUL : MUL;
+        case '<': return !in.match('<') ? (in.match('=') ? LE : LT) : in.match('=') ? ASSIGN_LSH : LSH;
+        case '>': return !in.match('>') ? (in.match('=') ? GE : GT) :
+            in.match('>') ? (in.match('=') ? ASSIGN_URSH : URSH) : (in.match('=') ? ASSIGN_RSH : RSH);
+        case '/':
+            if (in.match('=')) return ASSIGN_DIV;
+            if (in.match('/')) { while ((c = in.read()) != -1 && c != '\n'); in.unread(); return getToken(); }
+            if (!in.match('*')) return DIV;
+            while ((c = in.read()) != -1 && !(c == '*' && in.match('/'))) {
+                if (c == '\n' || c != '/' || !in.match('*')) continue;
+                if (in.match('/')) return getToken();
+                throw new LexerException("nested comments are not permitted");
+            }
+            if (c == -1) throw new LexerException("unterminated comment");
+            return getToken();  // `goto retry'
+        default: throw new LexerException("illegal character: \'" + ((char)c) + "\'");
+        }
+    }
+
+    private int le(String s) throws LexerException { if (true) throw new LexerException(s); return 0; }
+
+    // SmartReader ////////////////////////////////////////////////////////////////
+
+    /** a Reader that tracks line numbers and can push back tokens */
+    private class SmartReader {
+        PushbackReader reader = null;
+        int lastread = -1;
+
+        public SmartReader(Reader r) { reader = new PushbackReader(r); }
+        public void unread() throws IOException { unread((char)lastread); }
+        public void unread(char c) throws IOException {
+            reader.unread(c);
+            if(c == '\n') col = -1;
+            else col--;
+            if (accumulator != null) accumulator.setLength(accumulator.length() - 1);
+        }
+        public boolean match(char c) throws IOException { if (peek() == c) { reader.read(); return true; } else return false; }
+        public int peek() throws IOException {
+            int peeked = reader.read();
+            if (peeked != -1) reader.unread((char)peeked);
+            return peeked;
+        }
+        public int read() throws IOException {
+            lastread = reader.read();
+            if (accumulator != null) accumulator.append((char)lastread);
+            if (lastread != '\n' && lastread != '\r') col++;
+            if (lastread == '\n') {
+                // col is -1 if we just unread a newline, this is sort of ugly
+                if (col != -1) parserLine = ++line;
+                col = 0;
+            }
+            return lastread;
+        }
+
+        // FEATURE: could be much more efficient
+        StringBuffer accumulator = null;
+        public void startString() {
+            accumulator = new StringBuffer();
+            accumulator.append((char)lastread);
+        }
+        public String getString() throws IOException {
+            String ret = accumulator.toString().intern();
+            accumulator = null;
+            return ret;
+        }
+    }
+
+
+    // Token PushBack code ////////////////////////////////////////////////////////////
+
+    private int pushBackDepth = 0;
+    private int[] pushBackInts = new int[10];
+    private Object[] pushBackObjects = new Object[10];
+
+    /** push back a token */
+    public final void pushBackToken(int op, Object obj) {
+        if (pushBackDepth >= pushBackInts.length - 1) {
+            int[] newInts = new int[pushBackInts.length * 2];
+            System.arraycopy(pushBackInts, 0, newInts, 0, pushBackInts.length);
+            pushBackInts = newInts;
+            Object[] newObjects = new Object[pushBackObjects.length * 2];
+            System.arraycopy(pushBackObjects, 0, newObjects, 0, pushBackObjects.length);
+            pushBackObjects = newObjects;
+        }
+        pushBackInts[pushBackDepth] = op;
+        pushBackObjects[pushBackDepth] = obj;
+        pushBackDepth++;
+    }
+
+    /** push back the most recently read token */
+    public final void pushBackToken() { pushBackToken(op, number != null ? (Object)number : (Object)string); }
+
+    /** read a token but leave it in the stream */
+    public final int peekToken() throws IOException {
+        int ret = getToken();
+        pushBackToken();
+        return ret;
+    }
+
+    /** read a token */
+    public final int getToken() throws IOException {
+        number = null;
+        string = null;
+        if (pushBackDepth == 0) {
+            mostRecentlyReadToken = op;
+            return op = _getToken();
+        }
+        pushBackDepth--;
+        op = pushBackInts[pushBackDepth];
+        if (pushBackObjects[pushBackDepth] != null) {
+            number = pushBackObjects[pushBackDepth] instanceof Number ? (Number)pushBackObjects[pushBackDepth] : null;
+            string = pushBackObjects[pushBackDepth] instanceof String ? (String)pushBackObjects[pushBackDepth] : null;
+        }
+        return op;
+    }
+
+    class LexerException extends IOException {
+        public LexerException(String s) { super(sourceName + ":" + line + "," + col + ": " + s); }
+    }
+}
diff --git a/src/org/ibex/js/Parser.java b/src/org/ibex/js/Parser.java
new file mode 100644 (file)
index 0000000..51b23b8
--- /dev/null
@@ -0,0 +1,982 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+import org.ibex.util.*;
+import java.io.*;
+
+/**
+ *  Parses a stream of lexed tokens into a tree of JSFunction's.
+ *
+ *  There are three kinds of things we parse: blocks, statements, and
+ *  expressions.
+ *
+ *  - Expressions are a special type of statement that evaluates to a
+ *    value (for example, "break" is not an expression, * but "3+2"
+ *    is).  Some tokens sequences start expressions (for * example,
+ *    literal numbers) and others continue an expression which * has
+ *    already been begun (for example, '+').  Finally, some *
+ *    expressions are valid targets for an assignment operation; after
+ *    * each of these expressions, continueExprAfterAssignable() is
+ *    called * to check for an assignment operation.
+ *
+ *  - A statement ends with a semicolon and does not return a value.
+ *
+ *  - A block is a single statement or a sequence of statements
+ *    surrounded by curly braces.
+ *
+ *  Each parsing method saves the parserLine before doing its actual
+ *  work and restores it afterwards.  This ensures that parsing a
+ *  subexpression does not modify the line number until a token
+ *  *after* the subexpression has been consumed by the parent
+ *  expression.
+ *
+ *  Technically it would be a better design for this class to build an
+ *  intermediate parse tree and use that to emit bytecode.  Here's the
+ *  tradeoff:
+ *
+ *  Advantages of building a parse tree:
+ *  - easier to apply optimizations
+ *  - would let us handle more sophisticated languages than JavaScript
+ *
+ *  Advantages of leaving out the parse tree
+ *  - faster compilation
+ *  - less load on the garbage collector
+ *  - much simpler code, easier to understand
+ *  - less error-prone
+ *
+ *  Fortunately JS is such a simple language that we can get away with
+ *  the half-assed approach and still produce a working, complete
+ *  compiler.
+ *
+ *  The bytecode language emitted doesn't really cause any appreciable
+ *  semantic loss, and is itself a parseable language very similar to
+ *  Forth or a postfix variant of LISP.  This means that the bytecode
+ *  can be transformed into a parse tree, which can be manipulated.
+ *  So if we ever want to add an optimizer, it could easily be done by
+ *  producing a parse tree from the bytecode, optimizing that tree,
+ *  and then re-emitting the bytecode.  The parse tree node class
+ *  would also be much simpler since the bytecode language has so few
+ *  operators.
+ *
+ *  Actually, the above paragraph is slightly inaccurate -- there are
+ *  places where we push a value and then perform an arbitrary number
+ *  of operations using it before popping it; this doesn't parse well.
+ *  But these cases are clearly marked and easy to change if we do
+ *  need to move to a parse tree format.
+ */
+class Parser extends Lexer implements ByteCodes {
+
+
+    // Constructors //////////////////////////////////////////////////////
+
+    public Parser(Reader r, String sourceName, int line) throws IOException { super(r, sourceName, line); }
+
+    /** for debugging */
+    public static void main(String[] s) throws IOException {
+        JS block = JS.fromReader("stdin", 0, new InputStreamReader(System.in));
+        if (block == null) return;
+        System.out.println(block);
+    }
+
+
+    // Statics ////////////////////////////////////////////////////////////
+
+    static byte[] precedence = new byte[MAX_TOKEN + 1];
+    static boolean[] isRightAssociative = new boolean[MAX_TOKEN + 1];
+    // Use this as the precedence when we want anything up to the comma
+    private final static int NO_COMMA = 2;
+    static {
+        isRightAssociative[ASSIGN] =
+            isRightAssociative[ASSIGN_BITOR] =
+            isRightAssociative[ASSIGN_BITXOR] =
+            isRightAssociative[ASSIGN_BITAND] =
+            isRightAssociative[ASSIGN_LSH] =
+            isRightAssociative[ASSIGN_RSH] =
+            isRightAssociative[ASSIGN_URSH] =
+            isRightAssociative[ASSIGN_ADD] =
+            isRightAssociative[ASSIGN_SUB] =
+            isRightAssociative[ASSIGN_MUL] =
+            isRightAssociative[ASSIGN_DIV] =
+            isRightAssociative[ASSIGN_MOD] =
+            isRightAssociative[ADD_TRAP] =
+            isRightAssociative[DEL_TRAP] =
+            true;
+
+        precedence[COMMA] = 1;
+        // 2 is intentionally left unassigned. we use minPrecedence==2 for comma separated lists
+        precedence[ASSIGN] =
+            precedence[ASSIGN_BITOR] =
+            precedence[ASSIGN_BITXOR] =
+            precedence[ASSIGN_BITAND] =
+            precedence[ASSIGN_LSH] =
+            precedence[ASSIGN_RSH] =
+            precedence[ASSIGN_URSH] =
+            precedence[ASSIGN_ADD] =
+            precedence[ASSIGN_SUB] =
+            precedence[ASSIGN_MUL] =
+            precedence[ASSIGN_DIV] =
+            precedence[ADD_TRAP] =
+            precedence[DEL_TRAP] =
+            precedence[ASSIGN_MOD] = 3;
+        precedence[HOOK] = 4;
+        precedence[OR] = 5;
+        precedence[AND] = 6;
+        precedence[BITOR] = 7;
+        precedence[BITXOR] = 8;
+        precedence[BITAND] = 9;
+        precedence[EQ] = precedence[NE] = precedence[SHEQ] = precedence[SHNE] = 10;
+        precedence[LT] = precedence[LE] = precedence[GT] = precedence[GE] = 11;
+        precedence[LSH] = precedence[RSH] = precedence[URSH] = 12;
+        precedence[ADD] = precedence[SUB] = 12;
+        precedence[MUL] = precedence[DIV] = precedence[MOD] = 13;
+        precedence[BITNOT] =  precedence[BANG] = precedence[TYPEOF] = 14;
+        precedence[DOT] = precedence[LB] = precedence[LP] =  precedence[INC] = precedence[DEC] = 15;
+    }
+
+
+    // Parsing Logic /////////////////////////////////////////////////////////
+
+    /** gets a token and throws an exception if it is not <tt>code</tt> */
+    private void consume(int code) throws IOException {
+        if (getToken() != code) {
+            if(code == NAME) switch(op) {
+                case RETURN: case TYPEOF: case BREAK: case CONTINUE: case TRY: case THROW:
+                case ASSERT: case NULL: case TRUE: case FALSE: case IN: case IF: case ELSE:
+                case SWITCH: case CASE: case DEFAULT: case WHILE: case VAR: case WITH:
+                case CATCH: case FINALLY:
+                    throw pe("Bad variable name; '" + codeToString[op].toLowerCase() + "' is a javascript keyword");
+            }
+            throw pe("expected " + codeToString[code] + ", got " + (op == -1 ? "EOF" : codeToString[op]));
+        }
+    }
+
+    /**
+     *  Parse the largest possible expression containing no operators
+     *  of precedence below <tt>minPrecedence</tt> and append the
+     *  bytecodes for that expression to <tt>appendTo</tt>; the
+     *  appended bytecodes MUST grow the stack by exactly one element.
+     */ 
+    private void startExpr(JSFunction appendTo, int minPrecedence) throws IOException {
+        int saveParserLine = parserLine;
+        _startExpr(appendTo, minPrecedence);
+        parserLine = saveParserLine;
+    }
+    private void _startExpr(JSFunction appendTo, int minPrecedence) throws IOException {
+        int tok = getToken();
+        JSFunction b = appendTo;
+
+        switch (tok) {
+        case -1: throw pe("expected expression");
+
+        // all of these simply push values onto the stack
+        case NUMBER: b.add(parserLine, LITERAL, number); break;
+        case STRING: b.add(parserLine, LITERAL, string); break;
+        case NULL: b.add(parserLine, LITERAL, null); break;
+        case TRUE: case FALSE: b.add(parserLine, LITERAL, JS.B(tok == TRUE)); break;
+
+        // (.foo) syntax
+        case DOT: {
+            consume(NAME);
+            b.add(parserLine, TOPSCOPE);
+            b.add(parserLine, LITERAL, "");
+            b.add(parserLine, GET);
+            b.add(parserLine, LITERAL, string);
+            b.add(parserLine, GET);
+            continueExpr(b, minPrecedence);
+            break;
+        }
+
+        case LB: {
+            b.add(parserLine, ARRAY, JS.ZERO);                       // push an array onto the stack
+            int size0 = b.size;
+            int i = 0;
+            if (peekToken() != RB)
+                while(true) {                                               // iterate over the initialization values
+                    b.add(parserLine, LITERAL, JS.N(i++));           // push the index in the array to place it into
+                    if (peekToken() == COMMA || peekToken() == RB)
+                        b.add(parserLine, LITERAL, null);                   // for stuff like [1,,2,]
+                    else
+                        startExpr(b, NO_COMMA);                             // push the value onto the stack
+                    b.add(parserLine, PUT);                                 // put it into the array
+                    b.add(parserLine, POP);                                 // discard the value remaining on the stack
+                    if (peekToken() == RB) break;
+                    consume(COMMA);
+                }
+            b.set(size0 - 1, JS.N(i));                               // back at the ARRAY instruction, write the size of the array
+            consume(RB);
+            break;
+        }
+        case SUB: {  // negative literal (like "3 * -1")
+            consume(NUMBER);
+            b.add(parserLine, LITERAL, JS.N(number.doubleValue() * -1));
+            break;
+        }
+        case LP: {  // grouping (not calling)
+            startExpr(b, -1);
+            consume(RP);
+            break;
+        }
+        case INC: case DEC: {  // prefix (not postfix)
+            startExpr(b, precedence[tok]);
+            int prev = b.size - 1;
+            if (b.get(prev) == GET && b.getArg(prev) != null)
+                b.set(prev, LITERAL, b.getArg(prev));
+            else if(b.get(prev) == GET)
+                b.pop();
+            else
+                throw pe("prefixed increment/decrement can only be performed on a valid assignment target");
+            b.add(parserLine, GET_PRESERVE, Boolean.TRUE);
+            b.add(parserLine, LITERAL, JS.N(1));
+            b.add(parserLine, tok == INC ? ADD : SUB, JS.N(2));
+            b.add(parserLine, PUT, null);
+            b.add(parserLine, SWAP, null);
+            b.add(parserLine, POP, null);
+            break;
+        }
+        case BANG: case BITNOT: case TYPEOF: {
+            startExpr(b, precedence[tok]);
+            b.add(parserLine, tok);
+            break;
+        }
+        case LC: { // object constructor
+            b.add(parserLine, OBJECT, null);                                     // put an object on the stack
+            if (peekToken() != RC)
+                while(true) {
+                    if (peekToken() != NAME && peekToken() != STRING)
+                        throw pe("expected NAME or STRING");
+                    getToken();
+                    b.add(parserLine, LITERAL, string);                          // grab the key
+                    consume(COLON);
+                    startExpr(b, NO_COMMA);                                      // grab the value
+                    b.add(parserLine, PUT);                                      // put the value into the object
+                    b.add(parserLine, POP);                                      // discard the remaining value
+                    if (peekToken() == RC) break;
+                    consume(COMMA);
+                    if (peekToken() == RC) break;                                // we permit {,,} -- I'm not sure if ECMA does
+                }
+            consume(RC);
+            break;
+        }
+        case NAME: {
+            b.add(parserLine, TOPSCOPE);
+            b.add(parserLine, LITERAL, string);
+            continueExprAfterAssignable(b,minPrecedence);
+            break;
+        }
+        case FUNCTION: {
+            consume(LP);
+            int numArgs = 0;
+            JSFunction b2 = new JSFunction(sourceName, parserLine, null);
+            b.add(parserLine, NEWFUNCTION, b2);
+
+            // function prelude; arguments array is already on the stack
+            b2.add(parserLine, TOPSCOPE);
+            b2.add(parserLine, SWAP);
+            b2.add(parserLine, DECLARE, "arguments");                     // declare arguments (equivalent to 'var arguments;')
+            b2.add(parserLine, SWAP);                                     // set this.arguments and leave the value on the stack
+            b2.add(parserLine, PUT);
+
+            while(peekToken() != RP) {                                    // run through the list of argument names
+                numArgs++;
+                if (peekToken() == NAME) {
+                    consume(NAME);                                        // a named argument
+                    String varName = string;
+                    
+                    b2.add(parserLine, DUP);                              // dup the args array 
+                    b2.add(parserLine, GET, JS.N(numArgs - 1));   // retrieve it from the arguments array
+                    b2.add(parserLine, TOPSCOPE);
+                    b2.add(parserLine, SWAP);
+                    b2.add(parserLine, DECLARE, varName);                  // declare the name
+                    b2.add(parserLine, SWAP);
+                    b2.add(parserLine, PUT); 
+                    b2.add(parserLine, POP);                               // pop the value
+                    b2.add(parserLine, POP);                               // pop the scope                  
+                }
+                if (peekToken() == RP) break;
+                consume(COMMA);
+            }
+            consume(RP);
+
+            b2.numFormalArgs = numArgs;
+            b2.add(parserLine, POP);                                      // pop off the arguments array
+            b2.add(parserLine, POP);                                      // pop off TOPSCOPE
+            
+           if(peekToken() != LC)
+                throw pe("JSFunctions must have a block surrounded by curly brackets");
+                
+            parseBlock(b2, null);                                   // the function body
+
+            b2.add(parserLine, LITERAL, null);                        // in case we "fall out the bottom", return NULL
+            b2.add(parserLine, RETURN);
+
+            break;
+        }
+        default: throw pe("expected expression, found " + codeToString[tok] + ", which cannot start an expression");
+        }
+
+        // attempt to continue the expression
+        continueExpr(b, minPrecedence);
+    }
+    /*
+    private Grammar parseGrammar(Grammar g) throws IOException {
+        int tok = getToken();
+        if (g != null)
+            switch(tok) {
+                case BITOR: return new Grammar.Alternative(g, parseGrammar(null));
+                case ADD:   return parseGrammar(new Grammar.Repetition(g, 1, Integer.MAX_VALUE));
+                case MUL:   return parseGrammar(new Grammar.Repetition(g, 0, Integer.MAX_VALUE));
+                case HOOK:  return parseGrammar(new Grammar.Repetition(g, 0, 1));
+            }
+        Grammar g0 = null;
+        switch(tok) {
+            //case NUMBER: g0 = new Grammar.Literal(number); break;
+            case NAME: g0 = new Grammar.Reference(string); break;
+            case STRING:
+                g0 = new Grammar.Literal(string);
+                if (peekToken() == DOT) {
+                    String old = string;
+                    consume(DOT);
+                    consume(DOT);
+                    consume(STRING);
+                    if (old.length() != 1 || string.length() != 1) throw pe("literal ranges must be single-char strings");
+                    g0 = new Grammar.Range(old.charAt(0), string.charAt(0));
+                }
+                break;
+            case LP:     g0 = parseGrammar(null); consume(RP); break;
+            default:     pushBackToken(); return g;
+        }
+        if (g == null) return parseGrammar(g0);
+        return parseGrammar(new Grammar.Juxtaposition(g, g0));
+    }
+    */
+    /**
+     *  Assuming that a complete assignable (lvalue) has just been
+     *  parsed and the object and key are on the stack,
+     *  <tt>continueExprAfterAssignable</tt> will attempt to parse an
+     *  expression that modifies the assignable.  This method always
+     *  decreases the stack depth by exactly one element.
+     */
+    private void continueExprAfterAssignable(JSFunction b,int minPrecedence) throws IOException {
+        int saveParserLine = parserLine;
+        _continueExprAfterAssignable(b,minPrecedence);
+        parserLine = saveParserLine;
+    }
+    private void _continueExprAfterAssignable(JSFunction b,int minPrecedence) throws IOException {
+        if (b == null) throw new Error("got null b; this should never happen");
+        int tok = getToken();
+        if (minPrecedence != -1 && (precedence[tok] < minPrecedence || (precedence[tok] == minPrecedence && !isRightAssociative[tok])))
+            // force the default case
+            tok = -1;
+        switch(tok) {
+            /*
+        case GRAMMAR: {
+            b.add(parserLine, GET_PRESERVE);
+            Grammar g = parseGrammar(null);
+            if (peekToken() == LC) {
+                g.action = new JSFunction(sourceName, parserLine, null);
+                parseBlock((JSFunction)g.action);
+                ((JSFunction)g.action).add(parserLine, LITERAL, null);         // in case we "fall out the bottom", return NULL              
+                ((JSFunction)g.action).add(parserLine, RETURN);
+            }
+            b.add(parserLine, MAKE_GRAMMAR, g);
+            b.add(parserLine, PUT);
+            break;
+        }
+            */
+        case ASSIGN_BITOR: case ASSIGN_BITXOR: case ASSIGN_BITAND: case ASSIGN_LSH: case ASSIGN_RSH: case ASSIGN_URSH:
+        case ASSIGN_MUL: case ASSIGN_DIV: case ASSIGN_MOD: case ASSIGN_ADD: case ASSIGN_SUB: case ADD_TRAP: case DEL_TRAP: {
+            if (tok != ADD_TRAP && tok != DEL_TRAP) b.add(parserLine, GET_PRESERVE);
+            
+            startExpr(b,  precedence[tok]);
+            
+            if (tok != ADD_TRAP && tok != DEL_TRAP) {
+                // tok-1 is always s/^ASSIGN_// (0 is BITOR, 1 is ASSIGN_BITOR, etc) 
+                b.add(parserLine, tok - 1, tok-1==ADD ? JS.N(2) : null);
+                b.add(parserLine, PUT);
+                b.add(parserLine, SWAP);
+                b.add(parserLine, POP);
+            } else {
+                b.add(parserLine, tok);
+            }
+            break;
+        }
+        case INC: case DEC: { // postfix
+            b.add(parserLine, GET_PRESERVE, Boolean.TRUE);
+            b.add(parserLine, LITERAL, JS.N(1));
+            b.add(parserLine, tok == INC ? ADD : SUB, JS.N(2));
+            b.add(parserLine, PUT, null);
+            b.add(parserLine, SWAP, null);
+            b.add(parserLine, POP, null);
+            b.add(parserLine, LITERAL, JS.N(1));
+            b.add(parserLine, tok == INC ? SUB : ADD, JS.N(2));   // undo what we just did, since this is postfix
+            break;
+        }
+        case ASSIGN: {
+            startExpr(b, precedence[tok]);
+            b.add(parserLine, PUT);
+            b.add(parserLine, SWAP);
+            b.add(parserLine, POP);
+            break;
+        }
+        case LP: {
+
+            // Method calls are implemented by doing a GET_PRESERVE
+            // first.  If the object supports method calls, it will
+            // return JS.METHOD
+            int n = parseArgs(b, 2);
+            b.add(parserLine, GET_PRESERVE);
+            b.add(parserLine, CALLMETHOD, JS.N(n));
+            break;
+        }
+        default: {
+            pushBackToken();
+            if(b.get(b.size-1) == LITERAL && b.getArg(b.size-1) != null)
+                b.set(b.size-1,GET,b.getArg(b.size-1));
+            else
+                b.add(parserLine, GET);
+            return;
+        }
+        }
+    }
+
+
+    /**
+     *  Assuming that a complete expression has just been parsed,
+     *  <tt>continueExpr</tt> will attempt to extend this expression by
+     *  parsing additional tokens and appending additional bytecodes.
+     *
+     *  No operators with precedence less than <tt>minPrecedence</tt>
+     *  will be parsed.
+     *
+     *  If any bytecodes are appended, they will not alter the stack
+     *  depth.
+     */
+    private void continueExpr(JSFunction b, int minPrecedence) throws IOException {
+        int saveParserLine = parserLine;
+        _continueExpr(b, minPrecedence);
+        parserLine = saveParserLine;
+    }
+    private void _continueExpr(JSFunction b, int minPrecedence) throws IOException {
+        if (b == null) throw new Error("got null b; this should never happen");
+        int tok = getToken();
+        if (tok == -1) return;
+        if (minPrecedence != -1 && (precedence[tok] < minPrecedence || (precedence[tok] == minPrecedence && !isRightAssociative[tok]))) {
+            pushBackToken();
+            return;
+        }
+
+        switch (tok) {
+        case LP: {  // invocation (not grouping)
+            int n = parseArgs(b, 1);
+            b.add(parserLine, CALL, JS.N(n));
+            break;
+        }
+        case BITOR: case BITXOR: case BITAND: case SHEQ: case SHNE: case LSH:
+        case RSH: case URSH: case MUL: case DIV: case MOD:
+        case GT: case GE: case EQ: case NE: case LT: case LE: case SUB: {
+            startExpr(b, precedence[tok]);
+            b.add(parserLine, tok);
+            break;
+        }
+        case ADD: {
+            int count=1;
+            int nextTok;
+            do {
+                startExpr(b,precedence[tok]);
+                count++;
+                nextTok = getToken();
+            } while(nextTok == tok);
+            pushBackToken();
+            b.add(parserLine, tok, JS.N(count));
+            break;
+        }
+        case OR: case AND: {
+            b.add(parserLine, tok == AND ? JSFunction.JF : JSFunction.JT, JS.ZERO);       // test to see if we can short-circuit
+            int size = b.size;
+            startExpr(b, precedence[tok]);                                     // otherwise check the second value
+            b.add(parserLine, JMP, JS.N(2));                            // leave the second value on the stack and jump to the end
+            b.add(parserLine, LITERAL, tok == AND ?
+                  JS.B(false) : JS.B(true));                     // target of the short-circuit jump is here
+            b.set(size - 1, JS.N(b.size - size));                     // write the target of the short-circuit jump
+            break;
+        }
+        case DOT: {
+            // support foo..bar syntax for foo[""].bar
+            if (peekToken() == DOT) {
+                string = "";
+            } else {
+                consume(NAME);
+            }
+            b.add(parserLine, LITERAL, string);
+            continueExprAfterAssignable(b,minPrecedence);
+            break;
+        }
+        case LB: { // subscripting (not array constructor)
+            startExpr(b, -1);
+            consume(RB);
+            continueExprAfterAssignable(b,minPrecedence);
+            break;
+        }
+        case HOOK: {
+            b.add(parserLine, JF, JS.ZERO);                // jump to the if-false expression
+            int size = b.size;
+            startExpr(b, minPrecedence);                          // write the if-true expression
+            b.add(parserLine, JMP, JS.ZERO);               // if true, jump *over* the if-false expression     
+            b.set(size - 1, JS.N(b.size - size + 1));    // now we know where the target of the jump is
+            consume(COLON);
+            size = b.size;
+            startExpr(b, minPrecedence);                          // write the if-false expression
+            b.set(size - 1, JS.N(b.size - size + 1));    // this is the end; jump to here
+            break;
+        }
+        case COMMA: {
+            // pop the result of the previous expression, it is ignored
+            b.add(parserLine,POP);
+            startExpr(b,-1);
+            break;
+        }
+        default: {
+            pushBackToken();
+            return;
+        }
+        }
+
+        continueExpr(b, minPrecedence);                           // try to continue the expression
+    }
+    
+    // parse a set of comma separated function arguments, assume LP has already been consumed
+    // if swap is true, (because the function is already on the stack) we will SWAP after each argument to keep it on top
+    private int parseArgs(JSFunction b, int pushdown) throws IOException {
+        int i = 0;
+        while(peekToken() != RP) {
+            i++;
+            if (peekToken() != COMMA) {
+                startExpr(b, NO_COMMA);
+                b.add(parserLine, SWAP, JS.N(pushdown));
+                if (peekToken() == RP) break;
+            }
+            consume(COMMA);
+        }
+        consume(RP);
+        return i;
+    }
+    
+    /** Parse a block of statements which must be surrounded by LC..RC. */
+    void parseBlock(JSFunction b) throws IOException { parseBlock(b, null); }
+    void parseBlock(JSFunction b, String label) throws IOException {
+        int saveParserLine = parserLine;
+        _parseBlock(b, label);
+        parserLine = saveParserLine;
+    }
+    void _parseBlock(JSFunction b, String label) throws IOException {
+        if (peekToken() == -1) return;
+        else if (peekToken() != LC) parseStatement(b, null);
+        else {
+            consume(LC);
+            while(peekToken() != RC && peekToken() != -1) parseStatement(b, null);
+            consume(RC);
+        }
+    }
+
+    /** Parse a single statement, consuming the RC or SEMI which terminates it. */
+    void parseStatement(JSFunction b, String label) throws IOException {
+        int saveParserLine = parserLine;
+        _parseStatement(b, label);
+        parserLine = saveParserLine;
+    }
+    void _parseStatement(JSFunction b, String label) throws IOException {
+        int tok = peekToken();
+        if (tok == -1) return;
+        switch(tok = getToken()) {
+            
+        case THROW: case ASSERT: case RETURN: {
+            if (tok == RETURN && peekToken() == SEMI)
+                b.add(parserLine, LITERAL, null);
+            else
+                startExpr(b, -1);
+            b.add(parserLine, tok);
+            consume(SEMI);
+            break;
+        }
+        case BREAK: case CONTINUE: {
+            if (peekToken() == NAME) consume(NAME);
+            b.add(parserLine, tok, string);
+            consume(SEMI);
+            break;
+        }
+        case VAR: {
+            b.add(parserLine, TOPSCOPE);                         // push the current scope
+            while(true) {
+                consume(NAME);
+                b.add(parserLine, DECLARE, string);               // declare it
+                if (peekToken() == ASSIGN) {                     // if there is an '=' after the variable name
+                    consume(ASSIGN);
+                    startExpr(b, NO_COMMA);
+                    b.add(parserLine, PUT);                      // assign it
+                    b.add(parserLine, POP);                      // clean the stack
+                } else {
+                    b.add(parserLine, POP);                      // pop the string pushed by declare
+                }   
+                if (peekToken() != COMMA) break;
+                consume(COMMA);
+            }
+            b.add(parserLine, POP);                              // pop off the topscope
+            if ((mostRecentlyReadToken != RC || peekToken() == SEMI) && peekToken() != -1 && mostRecentlyReadToken != SEMI) consume(SEMI);
+            break;
+        }
+        case IF: {
+            consume(LP);
+            startExpr(b, -1);
+            consume(RP);
+            
+            b.add(parserLine, JF, JS.ZERO);                    // if false, jump to the else-block
+            int size = b.size;
+            parseStatement(b, null);
+            
+            if (peekToken() == ELSE) {
+                consume(ELSE);
+                b.add(parserLine, JMP, JS.ZERO);               // if we took the true-block, jump over the else-block
+                b.set(size - 1, JS.N(b.size - size + 1));
+                size = b.size;
+                parseStatement(b, null);
+            }
+            b.set(size - 1, JS.N(b.size - size + 1));        // regardless of which branch we took, b[size] needs to point here
+            break;
+        }
+        case WHILE: {
+            consume(LP);
+            if (label != null) b.add(parserLine, LABEL, label);
+            b.add(parserLine, LOOP);
+            int size = b.size;
+            b.add(parserLine, POP);                                   // discard the first-iteration indicator
+            startExpr(b, -1);
+            b.add(parserLine, JT, JS.N(2));                    // if the while() clause is true, jump over the BREAK
+            b.add(parserLine, BREAK);
+            consume(RP);
+            parseStatement(b, null);
+            b.add(parserLine, CONTINUE);                              // if we fall out of the end, definately continue
+            b.set(size - 1, JS.N(b.size - size + 1));        // end of the loop
+            break;
+        }
+        case SWITCH: {
+            consume(LP);
+            if (label != null) b.add(parserLine, LABEL, label);
+            b.add(parserLine, LOOP);
+            int size0 = b.size;
+            startExpr(b, -1);
+            consume(RP);
+            consume(LC);
+            while(true)
+                if (peekToken() == CASE) {                         // we compile CASE statements like a bunch of if..else's
+                    consume(CASE);
+                    b.add(parserLine, DUP);                        // duplicate the switch() value; we'll consume one copy
+                    startExpr(b, -1);
+                    consume(COLON);
+                    b.add(parserLine, EQ);                         // check if we should do this case-block
+                    b.add(parserLine, JF, JS.ZERO);         // if not, jump to the next one
+                    int size = b.size;
+                    while(peekToken() != CASE && peekToken() != DEFAULT && peekToken() != RC) parseStatement(b, null);
+                    b.set(size - 1, JS.N(1 + b.size - size));
+                } else if (peekToken() == DEFAULT) {
+                    consume(DEFAULT);
+                    consume(COLON);
+                    while(peekToken() != CASE && peekToken() != DEFAULT && peekToken() != RC) parseStatement(b, null);
+                } else if (peekToken() == RC) {
+                    consume(RC);
+                    b.add(parserLine, BREAK);                      // break out of the loop if we 'fall through'
+                    break;
+                } else {
+                    throw pe("expected CASE, DEFAULT, or RC; got " + codeToString[peekToken()]);
+                }
+            b.set(size0 - 1, JS.N(b.size - size0 + 1));      // end of the loop
+            break;
+        }
+            
+        case DO: {
+            if (label != null) b.add(parserLine, LABEL, label);
+            b.add(parserLine, LOOP);
+            int size = b.size;
+            parseStatement(b, null);
+            consume(WHILE);
+            consume(LP);
+            startExpr(b, -1);
+            b.add(parserLine, JT, JS.N(2));                  // check the while() clause; jump over the BREAK if true
+            b.add(parserLine, BREAK);
+            b.add(parserLine, CONTINUE);
+            consume(RP);
+            consume(SEMI);
+            b.set(size - 1, JS.N(b.size - size + 1));      // end of the loop; write this location to the LOOP instruction
+            break;
+        }
+            
+        case TRY: {
+            b.add(parserLine, TRY); // try bytecode causes a TryMarker to be pushed
+            int tryInsn = b.size - 1;
+            // parse the expression to be TRYed
+            parseStatement(b, null); 
+            // pop the try  marker. this is pushed when the TRY bytecode is executed                              
+            b.add(parserLine, POP);
+            // jump forward to the end of the catch block, start of the finally block                             
+            b.add(parserLine, JMP);                                  
+            int successJMPInsn = b.size - 1;
+            
+            if (peekToken() != CATCH && peekToken() != FINALLY)
+                throw pe("try without catch or finally");
+            
+            int catchJMPDistance = -1;
+            if (peekToken() == CATCH) {
+                Vec catchEnds = new Vec();
+                boolean catchAll = false;
+                
+                catchJMPDistance = b.size - tryInsn;
+                
+                while(peekToken() == CATCH && !catchAll) {
+                    String exceptionVar;
+                    getToken();
+                    consume(LP);
+                    consume(NAME);
+                    exceptionVar = string;
+                    int[] writebacks = new int[] { -1, -1, -1 };
+                    if (peekToken() != RP) {
+                        // extended Ibex catch block: catch(e faultCode "foo.bar.baz")
+                        consume(NAME);
+                        b.add(parserLine, DUP);
+                        b.add(parserLine, LITERAL, string);
+                        b.add(parserLine, GET);
+                        b.add(parserLine, DUP);
+                        b.add(parserLine, LITERAL, null);
+                        b.add(parserLine, EQ);
+                        b.add(parserLine, JT);
+                        writebacks[0] = b.size - 1;
+                        if (peekToken() == STRING) {
+                            consume(STRING);
+                            b.add(parserLine, DUP);
+                            b.add(parserLine, LITERAL, string);
+                            b.add(parserLine, LT);
+                            b.add(parserLine, JT);
+                            writebacks[1] = b.size - 1;
+                            b.add(parserLine, DUP);
+                            b.add(parserLine, LITERAL, string + "/");   // (slash is ASCII after dot)
+                            b.add(parserLine, GE);
+                            b.add(parserLine, JT);
+                            writebacks[2] = b.size - 1;
+                        } else {
+                            consume(NUMBER);
+                            b.add(parserLine, DUP);
+                            b.add(parserLine, LITERAL, number);
+                            b.add(parserLine, EQ);
+                            b.add(parserLine, JF);
+                            writebacks[1] = b.size - 1;
+                        }
+                        b.add(parserLine, POP);  // pop the element thats on the stack from the compare
+                    } else {
+                        catchAll = true;
+                    }
+                    consume(RP);
+                    // the exception is on top of the stack; put it to the chosen name
+                    b.add(parserLine, NEWSCOPE);
+                    b.add(parserLine, TOPSCOPE);
+                    b.add(parserLine, SWAP);
+                    b.add(parserLine, LITERAL,exceptionVar);
+                    b.add(parserLine, DECLARE);
+                    b.add(parserLine, SWAP);
+                    b.add(parserLine, PUT);
+                    b.add(parserLine, POP);
+                    b.add(parserLine, POP);
+                    parseBlock(b, null);
+                    b.add(parserLine, OLDSCOPE);
+                    
+                    b.add(parserLine, JMP);
+                    catchEnds.addElement(new Integer(b.size-1));
+                    
+                    for(int i=0; i<3; i++) if (writebacks[i] != -1) b.set(writebacks[i], JS.N(b.size-writebacks[i]));
+                    b.add(parserLine, POP); // pop the element thats on the stack from the compare
+                }
+                
+                if(!catchAll)
+                    b.add(parserLine, THROW);
+                
+                for(int i=0;i<catchEnds.size();i++) {
+                    int n = ((Integer)catchEnds.elementAt(i)).intValue();
+                    b.set(n, JS.N(b.size-n));
+                }
+                
+                // pop the try and catch markers
+                b.add(parserLine,POP);
+                b.add(parserLine,POP);
+            }
+                        
+            // jump here if no exception was thrown
+            b.set(successJMPInsn, JS.N(b.size - successJMPInsn)); 
+                        
+            int finallyJMPDistance = -1;
+            if (peekToken() == FINALLY) {
+                b.add(parserLine, LITERAL, null); // null FinallyData
+                finallyJMPDistance = b.size - tryInsn;
+                consume(FINALLY);
+                parseStatement(b, null);
+                b.add(parserLine,FINALLY_DONE); 
+            }
+            
+            // setup the TRY arguments
+            b.set(tryInsn, new int[] { catchJMPDistance, finallyJMPDistance });
+            
+            break;
+        }
+            
+        case FOR: {
+            consume(LP);
+            
+            tok = getToken();
+            boolean hadVar = false;                                      // if it's a for..in, we ignore the VAR
+            if (tok == VAR) { hadVar = true; tok = getToken(); }
+            String varName = string;
+            boolean forIn = peekToken() == IN;                           // determine if this is a for..in loop or not
+            pushBackToken(tok, varName);
+            
+            if (forIn) {
+                consume(NAME);
+                consume(IN);
+                startExpr(b,-1);
+                consume(RP);
+                
+                b.add(parserLine, PUSHKEYS);
+                b.add(parserLine, DUP); 
+                b.add(parserLine, LITERAL, "length"); 
+                b.add(parserLine, GET);
+                // Stack is now: n, keys, obj, ...
+                
+                int size = b.size;
+                b.add(parserLine, LOOP);
+                b.add(parserLine, POP);
+                // Stack is now: LoopMarker, n, keys, obj, ...
+                // NOTE: This will break if the interpreter ever becomes more strict 
+                //       and prevents bytecode from messing with the Markers
+                b.add(parserLine, SWAP, JS.N(3));
+                // Stack is now: Tn, keys, obj, LoopMarker, ...
+                
+                b.add(parserLine, LITERAL, JS.N(1));
+                b.add(parserLine, SUB);
+                b.add(parserLine, DUP);
+                // Stack is now: index, keys, obj, LoopMarker
+                b.add(parserLine, LITERAL, JS.ZERO);
+                b.add(parserLine, LT);
+                // Stack is now index<0, index, keys, obj, LoopMarker, ...
+                
+                b.add(parserLine, JF, JS.N(5)); // if we're >= 0 jump 5 down (to NEWSCOPE)
+                // Move the LoopMarker back  into place - this is sort of ugly
+                b.add(parserLine, SWAP, JS.N(3));
+                b.add(parserLine, SWAP, JS.N(3));
+                b.add(parserLine, SWAP, JS.N(3));
+                // Stack is now: LoopMarker, -1, keys, obj, ...
+                b.add(parserLine, BREAK);
+                
+                b.add(parserLine, NEWSCOPE);
+                if(hadVar) {
+                    b.add(parserLine, DECLARE, varName);
+                    b.add(parserLine, POP);
+                }
+                
+                // Stack is now: index, keys, obj, LoopMarker, ...
+                b.add(parserLine, GET_PRESERVE);     // key, index, keys, obj, LoopMarker, ...
+                b.add(parserLine, TOPSCOPE);         // scope, key, index, keys, obj, LoopMarker, ...
+                b.add(parserLine, SWAP);             // key, scope, index, keys, obj, LoopMarker, ...
+                b.add(parserLine, LITERAL, varName); // varName, key, scope, index, keys, obj, LoopMaker, ...
+                b.add(parserLine, SWAP);             // key, varName, scope, index, keys, obj, LoopMarker, ...
+                b.add(parserLine, PUT);              // key, scope, index, keys, obj, LoopMarker, ...
+                b.add(parserLine, POP);              // scope, index, keys, obj, LoopMarker
+                b.add(parserLine, POP);              // index, keys, obj, LoopMarker, ...
+                // Move the LoopMarker back into place - this is sort of ugly
+                b.add(parserLine, SWAP, JS.N(3));
+                b.add(parserLine, SWAP, JS.N(3));
+                b.add(parserLine, SWAP, JS.N(3));
+                
+                parseStatement(b, null);
+                
+                b.add(parserLine, OLDSCOPE);
+                b.add(parserLine, CONTINUE);
+                // jump here on break
+                b.set(size, JS.N(b.size - size));
+                
+                b.add(parserLine, POP); // N
+                b.add(parserLine, POP); // KEYS
+                b.add(parserLine, POP); // OBJ
+                
+            } else {
+                if (hadVar) pushBackToken(VAR, null);                    // yeah, this actually matters
+                b.add(parserLine, NEWSCOPE);                             // grab a fresh scope
+                    
+                parseStatement(b, null);                                 // initializer
+                JSFunction e2 =                                    // we need to put the incrementor before the test
+                    new JSFunction(sourceName, parserLine, null);  // so we save the test here
+                if (peekToken() != SEMI)
+                    startExpr(e2, -1);
+                else
+                    e2.add(parserLine, JSFunction.LITERAL, Boolean.TRUE);         // handle the for(foo;;foo) case
+                consume(SEMI);
+                if (label != null) b.add(parserLine, LABEL, label);
+                b.add(parserLine, LOOP);
+                int size2 = b.size;
+                    
+                b.add(parserLine, JT, JS.ZERO);                   // if we're on the first iteration, jump over the incrementor
+                int size = b.size;
+                if (peekToken() != RP) {                                 // do the increment thing
+                    startExpr(b, -1);
+                    b.add(parserLine, POP);
+                }
+                b.set(size - 1, JS.N(b.size - size + 1));
+                consume(RP);
+                    
+                b.paste(e2);                                             // ok, *now* test if we're done yet
+                b.add(parserLine, JT, JS.N(2));                   // break out if we don't meet the test
+                b.add(parserLine, BREAK);
+                parseStatement(b, null);
+                b.add(parserLine, CONTINUE);                             // if we fall out the bottom, CONTINUE
+                b.set(size2 - 1, JS.N(b.size - size2 + 1));     // end of the loop
+                    
+                b.add(parserLine, OLDSCOPE);                             // get our scope back
+            }
+            break;
+        }
+                
+        case NAME: {  // either a label or an identifier; this is the one place we're not LL(1)
+            String possiblyTheLabel = string;
+            if (peekToken() == COLON) {      // label
+                consume(COLON);
+                parseStatement(b, possiblyTheLabel);
+                break;
+            } else {                         // expression
+                pushBackToken(NAME, possiblyTheLabel);  
+                startExpr(b, -1);
+                b.add(parserLine, POP);
+                if ((mostRecentlyReadToken != RC || peekToken() == SEMI) && peekToken() != -1 && mostRecentlyReadToken != SEMI) consume(SEMI);
+                break;
+            }
+        }
+
+        case SEMI: return;                                               // yep, the null statement is valid
+
+        case LC: {  // blocks are statements too
+            pushBackToken();
+            b.add(parserLine, NEWSCOPE);
+            parseBlock(b, label);
+            b.add(parserLine, OLDSCOPE);
+            break;
+        }
+
+        default: {  // hope that it's an expression
+            pushBackToken();
+            startExpr(b, -1);
+            b.add(parserLine, POP);
+            if ((mostRecentlyReadToken != RC || peekToken() == SEMI) && peekToken() != -1 && mostRecentlyReadToken != SEMI) consume(SEMI);
+            break;
+        }
+        }
+    }
+
+
+    // ParserException //////////////////////////////////////////////////////////////////////
+    private IOException pe(String s) { return new IOException(sourceName + ":" + line + " " + s); }
+    
+}
+
diff --git a/src/org/ibex/js/PropertyFile.java b/src/org/ibex/js/PropertyFile.java
new file mode 100644 (file)
index 0000000..3227922
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] 
+package org.ibex.js; 
+
+import org.ibex.util.*; 
+import java.util.*;
+import java.io.*;
+
+/** A JS interface to a Java '.properties' file; very crude */
+public class PropertyFile extends JS {
+
+    private final Properties p = new Properties();
+    private final Hash cache = new Hash(10, 3);
+    private File f;
+
+    private class Minion extends JS {
+        private final String prefix;
+        Minion(String prefix) { this.prefix = prefix; }
+        public Number coerceToNumber()  { return N(coerceToString()); }
+        public Boolean coerceToBoolean() { return B(coerceToString().equals("true")); }
+        public String coerceToString()  { return (String)p.get(prefix.substring(0, prefix.length() - 1)); }
+        public Enumeration keys() throws JSExn { throw new JSExn("PropertyFile.keys() not supported"); }
+        public Object get(Object key) throws JSExn {
+            if (toString(key).equals("")) return coerceToString();
+            Object ret = p.get(prefix + escape(toString(key)));
+            if (ret != null) return ret;
+            return new Minion(prefix + escape(toString(key)) + ".");
+        }
+        public void put(Object key, Object val) throws JSExn {
+            try {
+                p.put(prefix + (prefix.equals("") ? "" : ".") + escape(toString(key)), toString(val));
+                File fnew = new File(f.getName() + ".new");
+                FileOutputStream fo = new FileOutputStream(fnew);
+                p.save(fo, "");
+                fo.close();
+                fnew.renameTo(f);
+                f = fnew;
+            } catch (IOException e) { throw new JSExn(e); }
+        }
+    }
+
+    public static String escape(String s) {
+        return s.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\.", "\\\\.").replaceAll("=","\\\\="); }
+    public PropertyFile(File f) throws IOException { this.f = f; this.p.load(new FileInputStream(f)); }
+    public void put(Object key, Object val) throws JSExn { new Minion("").put(key, val); }
+    public Enumeration keys() throws JSExn { return new Minion("").keys(); }
+    public Object get(Object key) throws JSExn {
+        Object ret = p.get(toString(key));
+        if (ret != null) return ret;
+        return new Minion(escape(toString(key)));
+    }
+}
diff --git a/src/org/ibex/js/Stream.java b/src/org/ibex/js/Stream.java
new file mode 100644 (file)
index 0000000..05045e9
--- /dev/null
@@ -0,0 +1,157 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+import java.io.*;
+import java.util.zip.*;
+import org.ibex.util.*;
+import org.ibex.plat.*;
+import org.ibex.net.*;
+
+/**
+ *   Essentiall an InputStream "factory".  You can repeatedly ask a
+ *   Stream for an InputStream, and each InputStream you get back will
+ *   be totally independent of the others (ie separate stream position
+ *   and state) although they draw from the same data source.
+ */
+public abstract class Stream extends JS.Cloneable {
+
+    // Public Interface //////////////////////////////////////////////////////////////////////////////
+
+    public static InputStream getInputStream(Object js) throws IOException { return ((Stream)((JS)js).unclone()).getInputStream();}
+    public static class NotCacheableException extends Exception { }
+
+    // streams are "sealed" by default to prevent accidental object leakage
+    public void put(Object key, Object val) { }
+    private Cache getCache = new Cache(100);
+    protected Object _get(Object key) { return null; }
+    public final Object get(Object key) {
+        Object ret = getCache.get(key);
+        if (ret == null) getCache.put(key, ret = _get(key));
+        return ret;
+    }
+
+    // Private Interface //////////////////////////////////////////////////////////////////////////////
+
+    public abstract InputStream getInputStream() throws IOException;
+    protected String getCacheKey() throws NotCacheableException { throw new NotCacheableException(); }
+
+    /** HTTP or HTTPS resource */
+    public static class HTTP extends Stream {
+        private String url;
+        public String toString() { return "Stream.HTTP:" + url; }
+        public HTTP(String url) { while (url.endsWith("/")) url = url.substring(0, url.length() - 1); this.url = url; }
+        public Object _get(Object key) { return new HTTP(url + "/" + (String)key); }
+        public String getCacheKey(Vec path) throws NotCacheableException { return url; }
+        public InputStream getInputStream() throws IOException { return new org.ibex.net.HTTP(url).GET(); }
+    }
+
+    /** byte arrays */
+    public static class ByteArray extends Stream {
+        private byte[] bytes;
+        private String cacheKey;
+        public ByteArray(byte[] bytes, String cacheKey) { this.bytes = bytes; this.cacheKey = cacheKey; }
+        public String getCacheKey() throws NotCacheableException {
+            if (cacheKey == null) throw new NotCacheableException(); return cacheKey; }
+        public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(bytes); }
+    }
+
+    /** a file */
+    public static class File extends Stream {
+        private String path;
+        public File(String path) { this.path = path; }
+        public String toString() { return "file:" + path; }
+        public String getCacheKey() throws NotCacheableException { throw new NotCacheableException(); /* already on disk */ }
+        public InputStream getInputStream() throws IOException { return new FileInputStream(path); }
+        public Object _get(Object key) { return new File(path + java.io.File.separatorChar + (String)key); }
+    }
+
+    /** "unwrap" a Zip archive */
+    public static class Zip extends Stream {
+        private Stream parent;
+        private String path;
+        public Zip(Stream parent) { this(parent, null); }
+        public Zip(Stream parent, String path) {
+            while(path != null && path.startsWith("/")) path = path.substring(1);
+            this.parent = parent;
+            this.path = path;
+        }
+        public String getCacheKey() throws NotCacheableException { return parent.getCacheKey() + "!zip:"; }
+        public Object _get(Object key) { return new Zip(parent, path==null?(String)key:path+'/'+(String)key); }
+        public InputStream getInputStream() throws IOException {
+            InputStream pis = parent.getInputStream();
+            ZipInputStream zis = new ZipInputStream(pis);
+            ZipEntry ze = zis.getNextEntry();
+            while(ze != null && !ze.getName().equals(path)) ze = zis.getNextEntry();
+            if (ze == null) throw new IOException("requested file (" + path + ") not found in archive");
+            return new KnownLength.KnownLengthInputStream(zis, (int)ze.getSize());
+        }
+    }
+
+    /** "unwrap" a Cab archive */
+    public static class Cab extends Stream {
+        private Stream parent;
+        private String path;
+        public Cab(Stream parent) { this(parent, null); }
+        public Cab(Stream parent, String path) { this.parent = parent; this.path = path; }
+        public String getCacheKey() throws NotCacheableException { return parent.getCacheKey() + "!cab:"; }
+        public Object _get(Object key) { return new Cab(parent, path==null?(String)key:path+'/'+(String)key); }
+        public InputStream getInputStream() throws IOException { return new MSPack(parent.getInputStream()).getInputStream(path); }
+    }
+
+    /** the Builtin resource */
+    public static class Builtin extends Stream {
+        public String getCacheKey() throws NotCacheableException { throw new NotCacheableException(); }
+        public InputStream getInputStream() throws IOException { return Platform.getBuiltinInputStream(); }
+    }
+
+    /** shadow resource which replaces the graft */
+    public static class ProgressWatcher extends Stream {
+        final Stream watchee;
+        JS callback;
+        public ProgressWatcher(Stream watchee, JS callback) { this.watchee = watchee; this.callback = callback; }
+        public String getCacheKey() throws NotCacheableException { return watchee.getCacheKey(); }
+        public InputStream getInputStream() throws IOException {
+            final InputStream is = watchee.getInputStream();
+            return new FilterInputStream(is) {
+                    int bytesDownloaded = 0;
+                    public int read() throws IOException {
+                        int ret = super.read();
+                        if (ret != -1) bytesDownloaded++;
+                        return ret;
+                    }
+                    public int read(byte[] b, int off, int len) throws IOException {
+                        int ret = super.read(b, off, len);
+                        if (ret != 1) bytesDownloaded += ret;
+                        Scheduler.add(new Task() { public void perform() throws IOException, JSExn {
+                            callback.call(N(bytesDownloaded),
+                                          N(is instanceof KnownLength ? ((KnownLength)is).getLength() : 0), null, null, 2);
+                        } });
+                        return ret;
+                    }
+                };
+        }
+    }
+
+    /** subclass from this if you want a CachedInputStream for each path */
+    public static class CachedStream extends Stream {
+        private Stream parent;
+        private boolean disk = false;
+        private String key;
+        public String getCacheKey() throws NotCacheableException { return key; }
+        CachedInputStream cis = null;
+        public CachedStream(Stream p, String s, boolean d) throws NotCacheableException {
+            this.parent = p; this.disk = d; this.key = p.getCacheKey();
+        }
+        public InputStream getInputStream() throws IOException {
+            if (cis != null) return cis.getInputStream();
+            if (!disk) {
+                cis = new CachedInputStream(parent.getInputStream());
+            } else {
+                java.io.File f = org.ibex.core.LocalStorage.Cache.getCacheFileForKey(key);
+                if (f.exists()) return new FileInputStream(f);
+                cis = new CachedInputStream(parent.getInputStream(), f);
+            }
+            return cis.getInputStream();
+        }
+    }
+}
diff --git a/src/org/ibex/js/Tokens.java b/src/org/ibex/js/Tokens.java
new file mode 100644 (file)
index 0000000..8970e14
--- /dev/null
@@ -0,0 +1,120 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+/** this class contains a <tt>public static final int</tt> for each valid token */
+interface Tokens {
+
+    // Token Constants //////////////////////////////////////////////////////////
+
+    // arithmetic operations; also valid as bytecodes
+    public static final int BITOR         = 0;   // |
+    public static final int ASSIGN_BITOR  = 1;   // |=
+    public static final int BITXOR        = 2;   // ^
+    public static final int ASSIGN_BITXOR = 3;   // ^=
+    public static final int BITAND        = 4;   // &
+    public static final int ASSIGN_BITAND = 5;   // &=
+    public static final int LSH           = 6;   // <<
+    public static final int ASSIGN_LSH    = 7;   // <<=
+    public static final int RSH           = 8;   // >>
+    public static final int ASSIGN_RSH    = 9;   // >>=
+    public static final int URSH          = 10;  // >>>
+    public static final int ASSIGN_URSH   = 11;  // >>>=
+    public static final int ADD           = 12;  // +
+    public static final int ASSIGN_ADD    = 13;  // +=
+    public static final int SUB           = 14;  // -
+    public static final int ASSIGN_SUB    = 15;  // -=
+    public static final int MUL           = 16;  // *
+    public static final int ASSIGN_MUL    = 17;  // *=
+    public static final int DIV           = 18;  // /
+    public static final int ASSIGN_DIV    = 19;  // /=
+    public static final int MOD           = 20;  // %
+    public static final int ASSIGN_MOD    = 21;  // %=
+    public static final int BITNOT        = 22;  // ~
+    public static final int ASSIGN_BITNOT = 23;  // ~=
+
+    // logical operations; also valid as bytecodes
+    public static final int OR            = 24;  // ||
+    public static final int AND           = 25;  // &&
+    public static final int BANG          = 26;  // !
+
+    // equality operations; also valid as bytecodes
+    public static final int EQ            = 27;  // ==
+    public static final int NE            = 28;  // !=
+    public static final int LT            = 29;  // <
+    public static final int LE            = 30;  // <=
+    public static final int GT            = 31;  // >
+    public static final int GE            = 32;  // >=
+    public static final int SHEQ          = 33;  // ===
+    public static final int SHNE          = 34;  // !==
+
+    // other permissible bytecode tokens
+    public static final int RETURN        = 35;  // return
+    public static final int TYPEOF        = 36;  // typeof
+    public static final int BREAK         = 37;  // break keyword
+    public static final int CONTINUE      = 38;  // continue keyword
+    public static final int TRY           = 39;  // try
+    public static final int THROW         = 40;  // throw
+    public static final int ASSERT        = 41;  // assert keyword
+
+    public static final int NAME          = 42;  // *** identifiers ***
+    public static final int NUMBER        = 43;  // *** numeric literals ***
+    public static final int STRING        = 44;  // *** string literals ***
+    public static final int NULL          = 45;  // null
+    public static final int THIS          = 46;  // this
+    public static final int FALSE         = 47;  // false
+    public static final int TRUE          = 48;  // true
+    public static final int IN            = 49;  // in
+
+    public static final int SEMI          = 50;  // ;
+    public static final int LB            = 51;  // [
+    public static final int RB            = 52;  // ]
+    public static final int LC            = 53;  // {
+    public static final int RC            = 54;  // }
+    public static final int LP            = 55;  // (
+    public static final int RP            = 56;  // )
+    public static final int COMMA         = 57;  // ,
+    public static final int ASSIGN        = 58;  // =
+    public static final int HOOK          = 59;  // ?
+    public static final int COLON         = 60;  // :
+    public static final int INC           = 61;  // ++
+    public static final int DEC           = 62;  // --
+    public static final int DOT           = 63;  // .
+    public static final int FUNCTION      = 64;  // function
+    public static final int IF            = 65;  // if keyword
+    public static final int ELSE          = 66;  // else keyword
+    public static final int SWITCH        = 67;  // switch keyword
+    public static final int CASE          = 68;  // case keyword
+    public static final int DEFAULT       = 69;  // default keyword
+    public static final int WHILE         = 70;  // while keyword
+    public static final int DO            = 71;  // do keyword
+    public static final int FOR           = 72;  // for keyword
+    public static final int VAR           = 73;  // var keyword
+    public static final int WITH          = 74;  // with keyword
+    public static final int CATCH         = 75;  // catch keyword
+    public static final int FINALLY       = 76;  // finally keyword
+    public static final int RESERVED      = 77;  // reserved keyword
+    public static final int GRAMMAR       = 78;  // the grammar-definition operator (::=)
+    public static final int ADD_TRAP      = 79;  // the add-trap operator (++=)
+    public static final int DEL_TRAP      = 80;  // the del-trap operator (--=)
+    public static final int MAX_TOKEN = DEL_TRAP;
+
+    public final static String[] codeToString = new String[] {
+        "BITOR", "ASSIGN_BITOR", "BITXOR", "ASSIGN_BITXOR", "BITAND",
+        "ASSIGN_BITAND", "LSH", "ASSIGN_LSH", "RSH", "ASSIGN_RSH",
+        "URSH", "ASSIGN_URSH", "ADD", "ASSIGN_ADD", "SUB",
+        "ASSIGN_SUB", "MUL", "ASSIGN_MUL", "DIV", "ASSIGN_DIV", "MOD",
+        "ASSIGN_MOD", "BITNOT", "ASSIGN_BITNOT", "OR", "AND", "BANG",
+        "EQ", "NE", "LT", "LE", "GT", "GE", "SHEQ", "SHNE", "RETURN",
+        "TYPEOF", "BREAK", "CONTINUE", "TRY", "THROW", "ASSERT", "NAME",
+        "NUMBER", "STRING", "NULL", "THIS", "FALSE", "TRUE", "IN",
+        "SEMI", "LB", "RB", "LC", "RC", "LP", "RP", "COMMA", "ASSIGN",
+        "HOOK", "COLON", "INC", "DEC", "DOT", "FUNCTION", "IF",
+        "ELSE", "SWITCH", "CASE", "DEFAULT", "WHILE", "DO", "FOR",
+        "VAR", "WITH", "CATCH", "FINALLY", "RESERVED", "GRAMMAR",
+        "ADD_TRAP", "DEL_TRAP"
+    };
+
+}
+
+
diff --git a/src/org/ibex/js/Trap.java b/src/org/ibex/js/Trap.java
new file mode 100644 (file)
index 0000000..77476f6
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.ibex.js;
+
+/**
+ *  This class encapsulates a single trap placed on a given node. The
+ *  traps for a given property name on a given box are maintained as a
+ *  linked list stack, with the most recently placed trap at the head
+ *  of the list.
+ */
+class Trap {
+
+    JS trapee = null;          ///< the box on which this trap was placed
+    Object name = null;        ///< the property that the trap was placed on
+
+    JSFunction f = null;       ///< the function for this trap
+    Trap next = null;          ///< the next trap down the trap stack
+
+    Trap(JS b, String n, JSFunction f, Trap nx) {
+        trapee = b; name = n; this.f = f; this.next = nx;
+    }
+
+    static final JSFunction putInvoker = new JSFunction("putInvoker", 0, null);
+    static final JSFunction getInvoker = new JSFunction("getInvoker", 0, null);
+
+    static {
+        putInvoker.add(1, ByteCodes.PUT, null);
+        putInvoker.add(2, Tokens.RETURN, null);
+        getInvoker.add(1, ByteCodes.GET, null);
+        getInvoker.add(2, Tokens.RETURN, null);
+    }
+    
+    void invoke(Object value) throws JSExn {
+        Interpreter i = new Interpreter(putInvoker, false, null);
+        i.stack.push(trapee);
+        i.stack.push(name);
+        i.stack.push(value);
+        i.resume();
+    }
+
+    Object invoke() throws JSExn {
+        Interpreter i = new Interpreter(getInvoker, false, null);
+        i.stack.push(trapee);
+        i.stack.push(name);
+        return i.resume();
+    }
+
+    // FIXME: review; is necessary?
+    static class TrapScope extends JSScope {
+        Trap t;
+        Object val = null;
+        boolean cascadeHappened = false;
+        public TrapScope(JSScope parent, Trap t, Object val) { super(parent); this.t = t; this.val = val; }
+        public Object get(Object key) throws JSExn {
+            if (key.equals("trapee")) return t.trapee;
+            if (key.equals("callee")) return t.f;
+            if (key.equals("trapname")) return t.name;
+            return super.get(key);
+        }
+    }
+}
+