From b1fa73c17b31f268fca5695d0876d7314fbacce3 Mon Sep 17 00:00:00 2001 From: adam Date: Thu, 8 Jul 2004 10:06:03 +0000 Subject: [PATCH] initial import darcs-hash:20040708100603-5007d-2ae50921669b2582d11904ff4db5fbf6234f0414.gz --- src/org/ibex/js/ByteCodes.java | 94 +++ src/org/ibex/js/Directory.java | 117 ++++ src/org/ibex/js/Interpreter.java | 744 ++++++++++++++++++++++ src/org/ibex/js/JS.java | 241 +++++++ src/org/ibex/js/JSArray.java | 262 ++++++++ src/org/ibex/js/JSDate.java | 1253 +++++++++++++++++++++++++++++++++++++ src/org/ibex/js/JSExn.java | 63 ++ src/org/ibex/js/JSFunction.java | 131 ++++ src/org/ibex/js/JSMath.java | 91 +++ src/org/ibex/js/JSReflection.java | 80 +++ src/org/ibex/js/JSRegexp.java | 336 ++++++++++ src/org/ibex/js/JSScope.java | 153 +++++ src/org/ibex/js/Lexer.java | 398 ++++++++++++ src/org/ibex/js/Parser.java | 982 +++++++++++++++++++++++++++++ src/org/ibex/js/PropertyFile.java | 51 ++ src/org/ibex/js/Stream.java | 157 +++++ src/org/ibex/js/Tokens.java | 120 ++++ src/org/ibex/js/Trap.java | 61 ++ 18 files changed, 5334 insertions(+) create mode 100644 src/org/ibex/js/ByteCodes.java create mode 100644 src/org/ibex/js/Directory.java create mode 100644 src/org/ibex/js/Interpreter.java create mode 100644 src/org/ibex/js/JS.java create mode 100644 src/org/ibex/js/JSArray.java create mode 100644 src/org/ibex/js/JSDate.java create mode 100644 src/org/ibex/js/JSExn.java create mode 100644 src/org/ibex/js/JSFunction.java create mode 100644 src/org/ibex/js/JSMath.java create mode 100644 src/org/ibex/js/JSReflection.java create mode 100644 src/org/ibex/js/JSRegexp.java create mode 100644 src/org/ibex/js/JSScope.java create mode 100644 src/org/ibex/js/Lexer.java create mode 100644 src/org/ibex/js/Parser.java create mode 100644 src/org/ibex/js/PropertyFile.java create mode 100644 src/org/ibex/js/Stream.java create mode 100644 src/org/ibex/js/Tokens.java create mode 100644 src/org/ibex/js/Trap.java diff --git a/src/org/ibex/js/ByteCodes.java b/src/org/ibex/js/ByteCodes.java new file mode 100644 index 0000000..e8170f1 --- /dev/null +++ b/src/org/ibex/js/ByteCodes.java @@ -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 index 0000000..e9f7b95 --- /dev/null +++ b/src/org/ibex/js/Directory.java @@ -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 copy 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= 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 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> 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= 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 index 0000000..1e38ce5 --- /dev/null +++ b/src/org/ibex/js/JS.java @@ -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 index 0000000..7b90de7 --- /dev/null +++ b/src/org/ibex/js/JSArray.java @@ -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 '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= 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--) 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 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 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 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= 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 index 0000000..ff4abc5 --- /dev/null +++ b/src/org/ibex/js/JSExn.java @@ -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; iunpauseable 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= 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 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= '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 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;iregardless of pushbacks */ + 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 lexed token */ + protected int line = 0; + + /** the line number of the most recently parsed 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 index 0000000..51b23b8 --- /dev/null +++ b/src/org/ibex/js/Parser.java @@ -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 code */ + 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 minPrecedence and append the + * bytecodes for that expression to appendTo; 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, + * continueExprAfterAssignable 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, + * continueExpr will attempt to extend this expression by + * parsing additional tokens and appending additional bytecodes. + * + * No operators with precedence less than minPrecedence + * 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= 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 index 0000000..3227922 --- /dev/null +++ b/src/org/ibex/js/PropertyFile.java @@ -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 index 0000000..05045e9 --- /dev/null +++ b/src/org/ibex/js/Stream.java @@ -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 index 0000000..8970e14 --- /dev/null +++ b/src/org/ibex/js/Tokens.java @@ -0,0 +1,120 @@ +// Copyright 2004 Adam Megacz, see the COPYING file for licensing [GPL] +package org.ibex.js; + +/** this class contains a public static final int 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 index 0000000..77476f6 --- /dev/null +++ b/src/org/ibex/js/Trap.java @@ -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); + } + } +} + -- 1.7.10.4