package terse.vm;

import java.util.Arrays;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.regex.Pattern;

import terse.vm.Cls.JavaMeth;
import terse.vm.Cls.StrMeth;
import terse.vm.Expr.Seq;
import terse.vm.Terp.Frame;

@SuppressWarnings("rawtypes")
public class Pro extends Static implements Comparable {
	public Cls cls;
	Pro[] instVars;

	// =cls "Sys" Pro
	public Pro(Cls cls) {
		this.cls = cls;
		if (cls == null) { // null cls happens during bootup.
			this.instVars = emptyPros;
			if (!Terp.tolerateNullClass) {
				breakHere(this);
				assert Terp.tolerateNullClass;
			}
		} else {
			++cls.countInstances;
			final int allVarsSize = cls.allVars.size();
			if (allVarsSize == 0) {
				this.instVars = emptyPros;
			} else {
				this.instVars = new Pro[allVarsSize];
				for (int i = 0; i < allVarsSize; i++) {
					this.instVars[i] = cls.terp.instNil;
				}
			}
		}
	}

	public String repr() {
		return toString();
	}

	public String toString() {
		return fmt("%s~%s", cls.name, hashCode());
	}

	public void visit(Visitor v) {
		v.visitPro(this);
	}

	public Terp terp() {
		return cls.terp;
	}

	public void say(String msg, Object... objects) {
		cls.terp.say(msg, objects);
	}

	public Pro toss(String msg, Object... objects) {
		return cls.terp.toss(msg, objects);
	}

	public Obj asObj() {
		return null;
	}

	public Num asNum() {
		return null;
	}

	public Str asStr() {
		return null;
	}

	public Cls asCls() {
		return null;
	}

	public Blk asBlk() {
		return null;
	}

	public Undefined asNil() {
		return null;
	}

	public Usr asUsr() {
		return null;
	}

	public Vec asVec() {
		return null;
	}

	public Dict asDict() {
		return null;
	}

	public final Str mustStr() {
		try {
			return (Str) this;
		} catch (Exception ex) {
			return (Str) toss("Should have been a Str, but wasn't: <%s#%s>", this.cls, this);
		}
	}
	public final Num mustNum() {
		try {
			return (Num) this;
		} catch (Exception ex) {
			return (Num) toss("Should have been a Num, but wasn't: <%s#%s>", this.cls, this);
		}
	}
	public final Vec mustVec() {
		try {
			return (Vec) this;
		} catch (Exception ex) {
			return (Vec) toss("Should have been a Vec, but wasn't: <%s#%s>", this.cls, this);
		}
	}
	public final Vec mustPair() {
		try {
			Vec z = (Vec) this;
			if (z.vec.size() != 2) {
				toss("Should have been a Vec of length 2, but was length %d: <%s>", z.vec.size(), z);
			}
			return z;
		} catch (Exception ex) {
			return (Vec) toss("Should have been a Vec, but wasn't: <%s#%s>", this.cls, this);
		}
	}
	public final Dict mustDict() {
		try {
			return (Dict) this;
		} catch (Exception ex) {
			return (Dict) toss("Should have been a Dict, but wasn't: <%s#%s>", this.cls, this);
		}
	}

	// TODO: Some of the following should be down at Obj.
	// Pro should toss subclassResponsibility for anything it can.
	// No one will care, until somebody uses Pro.

	// Most objects are true.
	// nil and Nums that round to 0 are false.
	public boolean truth() {
		//say("Object <%s> class %s is TRUE in Obj::truth.", this, cls.name);
		return true;
	}

	public Pro boolObj(boolean x) {
		return x ? terp().instTrue : terp().instFalse;
	}

	Comparable innerValue() {
		return null;
	}

	public int hashCode() {
		Object innerThis = this.innerValue();
		if (innerThis == null) {
			// Avoid infinite recursion.
			return System.identityHashCode(this);
		} else {
			return innerValue().hashCode();
		}
	}

	public boolean equals(Object obj) {
		if (obj instanceof Pro) {
			Pro that = (Pro) obj;
			if (this.cls != that.cls) {
				return false;
			}
			Object innerThis = this.innerValue();
			Object innerThat = that.innerValue();
			if (innerThis != null && innerThat != null) {
				// Both inner objects exist; compare them.
				boolean z = innerThis.equals(innerThat);
				return z;
			} else {
				// Avoid infinite recursion.
				boolean z = this == that;
				return z;
			}
		} else {
			return false;
		}
	}

	public int compareTo(Object obj) {
		// say("CMP <%s> <%s>", this, obj);
		if (obj instanceof Pro) {
			Pro that = (Pro) obj;
			if (this.cls != that.cls) {
				// say("CMP cls.name <%s> <%s>", this.cls.name, that.cls.name);
				return this.cls.name.compareTo(that.cls.name);
			}
			Object innerThis = this.innerValue();
			Object innerThat = that.innerValue();
			if (innerThis != null && innerThat != null) {
				// Not Equal because innerObject returned Value-like objects.
				// say("CMP inner <%s> <%s>", innerThis, innerThat);
				return ((Comparable) innerThis).compareTo(innerThat);
			}
		}
		// say("CMP identityHashCode <%s> <%s>", System.identityHashCode(this),
		// System.identityHashCode(obj));
		return new Integer(System.identityHashCode(this))
				.compareTo(new Integer(System.identityHashCode(obj)));
	}

	int toNearestInt() {
		Num num = this.asNum();
		if (num == null) {
			toss("Object is a %s, not a Num", this.cls.name);
		}
		int i = (int) Math.floor(num.num + 0.5); // closest int
		return i;
	}

	static void addBuiltinMethodsForPro(Terp terp) {
		terp.tPro.addMethod(new JavaMeth(terp.tPro, "class", "cls") {
			@Override
			public Pro apply(Frame f, Pro r, Pro[] args) {
				// say("<Pro class> r=%s r.cls=%s r.cls.name=%s r.cls.name=%s",
				// r,
				// r.cls, r.cls.name, r.cls.name);
				return r.cls;
			}
		});
	}

	/** Special marker object, for message to super. */
	public static class Super extends Pro {
		// =cls "Sys" Super Pro
		Super(Terp terp) {
			super(terp.tSuper);
		}

		public String toString() {
			return "super ";
		}
	}

	// =cls "Sys" Obj Pro
	public static class Obj extends Pro {
		public Obj(Cls cls) {
			super(cls);
		}

		public Obj asObj() {
			return this;
		}

		// =meth Obj "eval" eval: "Evaluate a string as code in this receiver."
		public Pro eval(String code) {
			// say("EVAL <%s> <<< <%s>", this, code);
			Expr.Top top = Parser.parseMethod(cls, "__eval__", code);
			Pro z = top.eval(cls.terp.newFrame(null, this, top));
			assert z != null : fmt("Null result in %s.eval <%s>", this, code);
			// say("EVAL <%s> >>> <%s>", this, z);
			return z;
		}
		// =meth Obj "eval" eval:arg: "Evaluate a string as code in this receiver."
		public Pro eval(String code, Pro a) {
			// say("EVAL <%s> <<< <%s> <%s>", this, code, a);
			Expr.Top top = Parser.parseMethod(cls, "__eval__:", code);
			Frame f = cls.terp.newFrame(null, this, top);
			f.locals[0] = a;
			Pro z = top.eval(f);
			assert z != null : fmt("Null result in %s.eval <%s>", this, code);
			// say("EVAL <%s> >>> <%s>", this, z);
			return z;
		}
		// =meth Obj "eval" eval:arg:arg: "Evaluate a string as code in this receiver."
		public Pro eval(String code, Pro a, Pro b) {
			// say("EVAL <%s> <<< <%s> <%s> <%s>", this, code, a, b);
			Expr.Top top = Parser.parseMethod(cls, "__eval__:__eval__:", code);
			Frame f = cls.terp.newFrame(null, this, top);
			f.locals[0] = a;
			f.locals[1] = b;
			Pro z = top.eval(f);
			assert z != null : fmt("Null result in %s.eval <%s>", this, code);
			// say("EVAL <%s> >>> <%s>", this, z);
			return z;
		}

		static void addBuiltinMethodsForObj(final Terp terp) {
			new JavaMeth(terp.tObj, "must", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (!r.truth()) {
						toss("MUST be True, but isn't: %s", r);
					}
					return r;
				}
			};
			new JavaMeth(terp.tObj, "must:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (!r.truth()) {
						StringBuffer sb = new StringBuffer();
						if (args[0].cls == terp.tBlk) {
							Pro x = ((Blk) args[0]).eval("self vec");
							sb.append("; ");
							sb.append(x.toString());
						}
						toss("MUST be True, but isn't: %s %s", r, sb);
					}
					return r;
				}
			};
			new JavaMeth(terp.tObj, "cant:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (r.truth()) {
						StringBuffer sb = new StringBuffer();
						if (args[0].cls == terp.tBlk) {
							Pro x = ((Blk) args[0]).eval("self vec");
							sb.append("; ");
							sb.append(x.toString());
						}
						toss("CANT be True, but is: <%s> <%s>", r, sb);
					}
					return r;
				}
			};
			new JavaMeth(terp.tObj, "err:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					toss("ERROR: <%s>; <%s>", r, args[0]);
					return r;
				}
			};
			new JavaMeth(terp.tObj, "err", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					toss("ERROR: <%s>", r);
					return r;
				}
			};
			new JavaMeth(terp.tObj, "say", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					say("#SAY# <%s>", r);
					return r;
				}
			};
			new JavaMeth(terp.tObj, "say:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					say("#SAY# <%s>; <%s>", r, args[0]);
					return r;
				}
			};
			new JavaMeth(terp.tObj, "hash", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newNum(r.hashCode());
				}
			};
			new JavaMeth(terp.tObj, "sysHash", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newNum(System.identityHashCode(r));
				}
			};
			new JavaMeth(terp.tObj, "not", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return boolObj(!r.truth());
				}
			};
			new JavaMeth(terp.tObj, "ifNil:", "ifn:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (r.asNil() != null) {
						Blk blk = args[0].asBlk();
						return blk.body.eval(f);
					} else {
						return terp.instNil;
					}
				}
			};
			new JavaMeth(terp.tObj, "ifNotNil:", "ifnn:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (r.asNil() == null) {
						Blk blk = args[0].asBlk();
						return blk.body.eval(f);
					} else {
						return terp.instNil;
					}
				}
			};
			new JavaMeth(terp.tObj, "ifNil:ifNotNil:", "ifn:ifnn:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Blk blk = args[r.asNil() == null ? 1 : 0].asBlk();
					say("ifNil:ifNotNil: --> Choosing block %d: %s",
							r.asNil() == null ? 1 : 0, blk);
					return blk.body.eval(f);
				}
			};
			new JavaMeth(terp.tObj, "ifNotNil:ifNil:", "ifnn:ifn:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Blk blk = args[r.asNil() == null ? 0 : 1].asBlk();
					say("ifNotNil:ifNil: --> Choosing block %d: %s",
							r.asNil() == null ? 0 : 1, blk);
					return blk.body.eval(f);
				}
			};
			new JavaMeth(terp.tObj, "ifYes:", "y:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (r.truth()) {
						Blk blk = args[0].asBlk();
						return blk.evalWithoutArgs();
					} else {
						return terp.instNil;
					}
				}
			};
			new JavaMeth(terp.tObj, "ifNo:", "n:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					if (r.truth()) {
						return terp.instNil;
					} else {
						Blk blk = args[0].asBlk();
						return blk.evalWithoutArgs();
					}
				}
			};
			new JavaMeth(terp.tObj, "ifYes:ifNo:", "y:n:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Blk blk = args[r.truth() ? 0 : 1].asBlk();
					return blk.body.eval(f);
				}
			};
			new JavaMeth(terp.tObj, "ifNo:ifYes:", "n:y:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Blk blk = args[r.truth() ? 1 : 0].asBlk();
					return blk.body.eval(f);
				}
			};
			new JavaMeth(terp.tObj, "repr", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					return terp.newStr(r.repr());
				}
			};
			new JavaMeth(terp.tObj, "str", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					return terp.newStr(r.toString());
				}
			};
			new JavaMeth(terp.tObj, "eval:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str code = args[0].asStr();
					if (code == null) {
						say("Expected a Str arg to eval: but got this instead: <%s: %s>",
								args[0].cls, args[0]);
					}

					say("EVAL <- %s  eval:  %s", r, code);
					Pro z = r.asObj().eval(code.str);
					say("EVAL -> %s", z);
					// say("EVAL -> %s", z);
					return z;
				}
			};
			new JavaMeth(terp.tObj, "isInstanceOf:", "isa:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Cls query = args[0].asCls();
					if (query == null) {
						toss("isInstOf: expected a Cls as it arg");
					}
					for (Cls p = r.cls; p != null; p = query.supercls) {
						if (p == query) {
							return terp.instTrue;
						}
					}
					return terp.instFalse;
				}
			};
			new JavaMeth(terp.tObj, "equals:", "") {
				public Obj apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					return r.equals(args[0]) ? terp.instTrue : terp.instFalse;
				}
			};
			new JavaMeth(terp.tObj, "isSameAs:", "is:") {
				public Obj apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					return r == args[0] ? terp.instTrue : terp.instFalse;
				}
			};
		}
	}

	// =cls "Sys" Sys Obj
	public final static class Sys extends Obj {
		public Sys(Terp terp) {
			super(terp.tSys);
		}

		static void addBuiltinMethodsForSys(final Terp terp) {
			terp.tSysCls.addMethod(new StrMeth(terp.tSysCls, "envAt:", "", "") {
				public Pro apply1(Frame f, Pro r, Str a) {
					Pro z = terp.newStr(terp.env.get(a.str));
					return z == null ? terp.instNil : z;
				}
			});
			terp.tSysCls.addMethod(new JavaMeth(terp.tSysCls, "trigraphs", "",
					"keyboard character substitution dict") {
				public Dict apply(Frame f, Pro r, Pro[] args) {
					Dict d = new Dict(terp);
					for (int i = 0; i < Parser.TRIGRAPH_DATA.length; i += 2) {
						d.dict.put(terp.newStr("/"+Parser.TRIGRAPH_DATA[i]), terp.newStr(Parser.TRIGRAPH_DATA[i + 1]));
					}
					return d;
				}
			});
		}
	}
	// =cls "Sys" Bogus Sys slot1,slot2,slot3

	public final static class Undefined extends Obj { // Nil
		// =cls "Data" Undefined Obj
		public Undefined(Cls cls) {
			super(cls);
		}

		@Override
		public Undefined asNil() {
			return this;
		}

		@Override
		public String toString() {
			return "Nil";
		}

		@Override
		public void visit(Visitor v) {
			v.visitUndefined(this);
		}

		// Most objects are true.
		// nil and Nums that round to 0 are false.
		@Override
		public boolean truth() {
			return false;
		}

		static void addBuiltinMethodsForFal(final Terp terp) {
		}
	}

	public final static class Blk extends Obj {
		Expr.Block block;
		Expr body;
		Expr[] params;
		Frame f;

		// =cls "Data" Blk Obj
		public Blk(Expr.Block block, Frame f) {
			super(f.terp().tBlk);
			this.block = block;
			this.body = block.body;
			this.params = block.params;
			this.f = f;
		}

		public Blk asBlk() {
			return this;
		}

		public String toString() {
			return block.toString();
		}

		Pro evalWithoutArgs() { // Cannot just call it eval(), because that
			// names takes a Frame, and this does not.
			if (this.params.length != 0) {
				toss("Block takes <%d> params, but calling it with 0 arsg: Blk=<%s>",
						this.params.length, this);
			}
			// Notice the block runs in its own frame, not the caller's frame.
			return this.body.eval(this.f);
		}

		Pro evalWith1Arg(Pro arg0) {
			if (this.params.length != 1) {
				toss("Block takes <%d> params, but calling it with 1 arg: Blk=<%s> arg0=<%s>",
						this.params.length, this, arg0);
			}
			// Notice the block runs in its own frame, not the caller's frame.
			((Expr.LValue)params[0]).store(this.f, arg0);
			return this.body.eval(this.f);
		}

		Pro evalWith2Args(Pro arg0, Pro arg1) {
			if (this.params.length != 2) {
				toss("Block takes <%d> params, but calling it with 2 args: Blk=<%s> arg0=<%s> arg1=<%s>",
						this.params.length, this, arg0, arg1);
			}
			// Notice the block runs in its own frame, not the caller's frame.
			((Expr.LValue)params[0]).store(this.f, arg0);
			((Expr.LValue)params[1]).store(this.f, arg1);
			return this.body.eval(this.f);
		}
		// =meth Blk "macro" implode,imp "Implode strings and ints (as chars) and subvectors."
		public Str implode() {
			// Copied from "vec"
			Expr.Seq seq = (Seq) this.body;
			Pro[] arr = new Pro[seq.body.length];
			for (int i = 0; i < arr.length; i++) {
				arr[i] = seq.body[i].eval(this.f);
			}
			Vec v = terp().newVec(arr);
			StringBuilder sb = new StringBuilder();
			implodeRecursive(sb, v);
			return terp().newStr(sb.toString());
		}
		private static void implodeRecursive(StringBuilder sb, Vec v) {
			for (int i = 0; i < v.vec.size(); i++) {
				Pro x = v.vec.get(i);
				if (x.cls == v.terp().tNum) {
					char ch = (char) (((Num)x).toNearestInt());
					sb.append(ch);
				} else if (x.cls == v.terp().tVec) {
					implodeRecursive(sb, ((Vec)x));
				} else {
					sb.append(x.toString());
				}
			}
		}

		// =meth Blk "macro" dyct "Construct a Dict from a list of tuples."
		public Dict dyct() {
			final Vec list = this.eval("self run").mustVec();
			final int n = list.vec.size();
			final Pro[] arr = new Pro[n];
			for (int i = 0; i < n; i++) {
				arr[i] = list.vec.get(i).mustPair();
			}
			return terp().newDict(arr);
		}
		static void addBuiltinMethodsForBlk(final Terp terp) {

			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "or", "",
					"Return true if any member evaluates true.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (x.truth())
							return terp.instTrue;
					}
					return terp.instFalse;
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "and", "",
					"Return false if any member evaluates untrue.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (!x.truth())
							return terp.instFalse;
					}
					return terp.instTrue;
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "vec", "v",
					"Evalute members of the block to make a Vec.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						arr[i] = seq.body[i].eval(blk.f);
					}
					return terp.newVec(arr);
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "dict", "d",
					"Evalute pairs of the block to make a Dict.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						arr[i] = seq.body[i].eval(blk.f); // should be a pair.
					}
					return terp.newDict(arr);
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "run", "value",
					"Evaluate the block.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					return r.asBlk().evalWithoutArgs();
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "run:", "value:",
					"Run the block with one argument.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					return r.asBlk().evalWith1Arg(args[0]);
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "run:with:", "value:value:",
					"Run the block with two arguments.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					return r.asBlk().evalWith2Args(args[0], args[1]);
				}
			});
			terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "catch", "",
					"Catch exceptions.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Pro z = terp.instNil;
					Pro err = terp.instNil;
					Pro info = terp.instNil;
					try {
						z = r.asBlk().evalWithoutArgs();
					} catch (RuntimeException ex) {
						err = terp.newStr(ex.toString());
						StackTraceElement[] t = ex.getStackTrace();
						StringBuffer sb = new StringBuffer();
						for (int i = 0; i < t.length; i++) {
							sb.append(t[i].toString());
							sb.append("\n");
						}
						info = terp.newStr(sb.toString());
					}
					return terp.newVec(pros(z, err, info));
				}
			});
		}
	}

	public final static class Num extends Obj {
		public double num;

		// =cls "Data" Num Obj
		Num(Terp t, double num) {
			super(t.tNum);
			this.num = num;
		}

		public Num asNum() {
			return this;
		}

		public String toString() {
			long truncated = (long) num;
			if (num == truncated) {
				return fmt("%d", truncated);
			} else {
				return fmt("%s", num);
			}
		}

		@Override
		Comparable innerValue() {
			return new Double(num);
		}

		public void visit(Visitor v) {
			v.visitNum(this);
		}

		// Most objects are true.
		// nil and Nums that round to 0 are false.
		public boolean truth() {
			return toNearestInt() != 0;
		}
		// =meth Num "binop" == "eq two Nums"
		public Num _eq_(Num a) {
			return terp().newNum(this.num == a.num ? 1 : 0);
		}
		// =meth Num "binop" != "ne two Nums"
		public Num _ne_(Num a) {
			return terp().newNum(this.num != a.num ? 1 : 0);
		}
		// =meth Num "binop" < "lt two Nums"
		public Num _lt_(Num a) {
			return terp().newNum(this.num < a.num ? 1 : 0);
		}
		// =meth Num "binop" <= "le two Nums"
		public Num _le_(Num a) {
			return terp().newNum(this.num <= a.num ? 1 : 0);
		}
		// =meth Num "binop" > "gt two Nums"
		public Num _gt_(Num a) {
			return terp().newNum(this.num > a.num ? 1 : 0);
		}
		// =meth Num "binop" >= "ge two Nums"
		public Num _ge_(Num a) {
			return terp().newNum(this.num >= a.num ? 1 : 0);
		}
		// =meth Num "binop" + "add two Nums"
		public Num _pl_(Num a) {
			return terp().newNum(this.num + a.num);
		}
		// =meth Num "binop" - "add two Nums"
		public Num _mi_(Num a) {
			return terp().newNum(this.num - a.num);
		}
		// =meth Num "binop" | "bitwise-or two Nums as 32bit integers"
		public Num _or_(Num a) {
			return terp().newNum((int)this.num | (int)a.num);
		}
		// =meth Num "binop" ^ "bitwise-xor two Nums as 32bit integers"
		public Num _xo_(Num a) {
			return terp().newNum((int)this.num ^ (int)a.num);
		}
		// =meth Num "binop" * "multiply two Nums"
		public Num _ti_(Num a) {
			return terp().newNum(this.num * a.num);
		}
		// =meth Num "binop" / "divide two Nums"
		public Num _di_(Num a) {
			return terp().newNum(this.num / a.num);
		}
		// =meth Num "binop" % "modulo two Nums"
		public Num _mo_(Num a) {
			return terp().newNum(this.num % a.num);
		}
		// =meth Num "binop" & "bitwise-and two Nums as 32bit integers"
		public Num _an_(Num a) {
			return terp().newNum((int)this.num & (int)a.num);
		}

		abstract static class BinaryNumPredMeth extends JavaMeth {
			BinaryNumPredMeth(Terp terp, String name) {
				super(terp.tNum, name, null, "");
			}

			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Num s = r.asNum();
				Num a = args[0].asNum();
				if (s == null) {
					toss("Receiver of Num relop not a Num");
				}
				if (a == null) {
					toss("Argument of Num relop not a Num");
				}
				return boolObj(compute(s.num, a.num));
			}

			abstract boolean compute(double snum, double anum);
		}

		abstract static class BinaryNumMeth extends JavaMeth {
			BinaryNumMeth(Terp terp, String name) {
				super(terp.tNum, name, null, "");
			}

			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Num s = r.asNum();
				Num a = args[0].asNum();
				if (s == null) {
					toss("Receiver of Num binop not a Num");
				}
				if (a == null) {
					toss("Argument of Num binop not a Num");
				}
				return new Num(f.terp(), compute(s.num, a.num));
			}

			abstract double compute(double snum, double anum);
		}

		abstract static class UnaryNumMeth extends JavaMeth {
			UnaryNumMeth(Terp terp, String name) {
				super(terp.tNum, name, null, "");
			}

			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 0;
				Num s = r.asNum();
				if (s == null) {
					toss("Receiver of Num unary op not a Num");
				}
				return new Num(f.terp(), compute(s.num));
			}

			abstract double compute(double anum);
		}

		static void addBuiltinMethodsForNum(final Terp terp) {
			terp.tNum.addMethod(new JavaMeth(terp.tNum, "iota", "", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Num num = r.asNum();
					int n = num.toNearestInt();
					assert args.length == 0;
					Pro[] arr = new Pro[n];
					for (int i = 0; i < n; i++) {
						arr[i] = terp.newNum(i);
					}
					return new Vec(arr);
				}
			});

			terp.tNum.addMethod(new JavaMeth(terp.tNum, "do:", "",
					"Iterate the argument block this many times, "
							+ "with one argument ranging from 0 up to, "
							+ "not including, self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Num num = r.asNum();
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					double stop = num.num - 0.5;
					for (double i = 0; i < stop; i++) {
						blk.evalWith1Arg(new Num(f.terp(), i));
					}
					return f.terp().instNil;
				}
			});

			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "eq:") {
				boolean compute(double snum, double anum) {
					return snum == anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ne:") {
				boolean compute(double snum, double anum) {
					return snum != anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "lt:") {
				boolean compute(double snum, double anum) {
					return snum < anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "le:") {
				boolean compute(double snum, double anum) {
					return snum <= anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "gt:") {
				boolean compute(double snum, double anum) {
					return snum > anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ge:") {
				boolean compute(double snum, double anum) {
					return snum >= anum;
				}
			});

			terp.tNum.addMethod(new BinaryNumMeth(terp, "pl:") {
				double compute(double snum, double anum) {
					return snum + anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "mi:") {
				double compute(double snum, double anum) {
					return snum - anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "ti:") {
				double compute(double snum, double anum) {
					return snum * anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "fdiv:") {
				double compute(double snum, double anum) {
					return snum / anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "idiv:") {
				double compute(double snum, double anum) {
					return (long) snum / (long) anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "imod:") {
				double compute(double snum, double anum) {
					return (long) snum % (long) anum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "neg") {
				double compute(double snum) {
					return -snum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "sgn") {
				double compute(double snum) {
					return snum < 0 ? -1 : snum > 0 ? 1 : 0;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "int") {
				double compute(double snum) {
					return (long) snum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "floor") {
				double compute(double snum) {
					return Math.floor(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "abs") {
				double compute(double snum) {
					return Math.abs(snum);
				}
			});
			terp.tNumCls.addMethod(new JavaMeth(terp.tNumCls, "e", "",
					"e, the base of natural logs") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newNum(Math.E);
				}
			});
			terp.tNumCls.addMethod(new JavaMeth(terp.tNumCls, "pi", "", "pi") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newNum(Math.PI);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "sin") {
				double compute(double snum) {
					return Math.sin(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "cos") {
				double compute(double snum) {
					return Math.cos(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "tan") {
				double compute(double snum) {
					return Math.tan(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "asin") {
				double compute(double snum) {
					return Math.asin(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "acos") {
				double compute(double snum) {
					return Math.acos(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "atan") {
				double compute(double snum) {
					return Math.atan(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "ln") {
				double compute(double snum) {
					return Math.log(snum);
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "log10") {
				double compute(double snum) {
					return Math.log10(snum);
				}
			});

			terp.tNum.addMethod(new UnaryNumMeth(terp, "exp") {
				double compute(double snum) {
					return Math.exp(snum);
				}
			});
			terp.tNum.addMethod(new JavaMeth(terp.tNum, "toNumber", "num",
					"num is a NOP on a Num") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return r;
				}
			});
		}
	}

	public final static class Buf extends Obj {
		StringBuffer buf;

		// =cls "Data" Buf Obj
		Buf(Terp t) {
			super(t.tBuf);
			this.buf = new StringBuffer();
		}

		Buf(Terp t, String s) {
			super(t.tBuf);
			this.buf = new StringBuffer(s);
		}

		public String toString() {
			return fmt("~Buf~'%s'~", buf.toString());
		}

		static void addBuiltinMethodsForBuf(final Terp terp) {
			terp.tBuf.addMethod(new JavaMeth(terp.tBuf, "str", "s",
					"Get Str from Buf.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					Buf self = (Buf) r;
					return terp.newStr(self.buf.toString());
				}
			});
			terp.tBuf.addMethod(new JavaMeth(terp.tBuf, "append:", "ap:",
					"Append the argument to the Buf, modifying self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Buf self = (Buf) r;
					Str a = args[0].asStr();
					if (a == null) {
						toss("Argument of Buf>>ap: not a Str");
					}
					self.buf.append(a.str);
					return r;
				}
			});
			terp.tBufCls.addMethod(new JavaMeth(terp.tBufCls, "new", "",
					"Create new empty Buf.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					return new Buf(terp);
				}
			});
			terp.tBufCls.addMethod(new JavaMeth(terp.tBufCls, "append:", "ap:",
					"Append the argument to the Buf, modifying self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str a = args[0].asStr();
					if (a == null) {
						toss("Argument of Buf>>ap: not a Str");
					}
					return new Buf(terp, a.str);
				}
			});
		}
	}

	public final static class Str extends Obj {
		public String str;

		// =cls "Data" Str Obj
		Str(Terp t, String str) {
			super(t.tStr);
			this.str = str;
		}

		public Str asStr() {
			return this;
		}

		public String repr() {
			if (str == null) {
				return "<?NULL?>";
			}
			return fmt("'%s'", str.replaceAll("'", "''"));
		}

		public String toString() {
			return str;
		}

		@Override
		Comparable innerValue() {
			return str;
		}

		public void visit(Visitor v) {
			v.visitStr(this);
		}
		
		// =meth Str "access" applySubstitutions ""
		public Str applySubstitutions() {
			return terp().newStr(Parser.charSubsts(this.str));
		}

		abstract static class BinaryStrPredMeth extends JavaMeth {
			BinaryStrPredMeth(Terp terp, String name) {
				super(terp.tStr, name, null, "");
			}

			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Str s = (Str) r;
				Str a = args[0].asStr();
				if (a == null) {
					toss("Argument of Str relop not a Str");
				}
				return boolObj(compute(s.str, a.str));
			}

			abstract boolean compute(String sstr, String astr);
		}

		static void addBuiltinMethodsForStr(final Terp terp) {
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "split:", "",
					"Split string into a Vec, using arg as delimiter.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str s = (Str) r;
					Str a = args[0].asStr();
					if (a == null) {
						toss("Argument of Str>>spli: not a Str");
					}
					// Using Java's split caused infinite recursion in
					// Pattern:
					// String[] parts =
					// s.str.split(Pattern.quote(a.str));
					String[] parts = splitNonEmpty(s.str, a.str.charAt(0));
					Pro[] arr = new Pro[parts.length];
					for (int i = 0; i < parts.length; i++) {
						arr[i] = terp.newStr(parts[i]);
					}
					return terp.newVec(arr);
				}
			});
			terp.tStr
					.addMethod(new JavaMeth(terp.tStr, "append:", "ap:",
							"Append the argument (as a string) to the string, modifying self.") {
						public Pro apply(Frame f, Pro r, Pro[] args) {
							assert args.length == 1;
							Str s = (Str) r;
							Str a = args[0].asStr();
							if (a == null) {
								toss("Argument of Str>>ap: not a Str");
							}
							return new Str(terp, s.str + a.str);
						}
					});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "substr:to:", "ss:to:",
					"Substring starting at first index, "
							+ "ending before second index, like Java substr.") { // Substring
						public Pro apply(Frame f, Pro r, Pro[] args) {
							assert args.length == 2;
							Str s = (Str) r;
							assert (s != null);
							int a = args[0].toNearestInt();
							int b = args[1].toNearestInt();
							return new Str(terp, s.str.substring(a, b));
						}
					});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "head", "hd",
					"First char of Str.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Str self = (Str) r;
					if (self.str.length() == 0) {
						toss("Cannot take head of empty Str");
					}
					return terp.newStr(self.str.substring(0, 1));
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "tail", "tl",
					"All but first char of Str.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Str self = (Str) r;
					if (self.str.length() == 0) {
						toss("Cannot take tail of empty Str");
					}
					return terp.newStr(self.str.substring(1));
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "length", "len",
					"Length of Str.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newNum(((Str) r).str.length());
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "toNumber", "num",
					"Convert ASCII Str to Num") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Str self = (Str) r;
					double x = 0;
					try {
						x = new Float(self.str);
					} catch (NumberFormatException ex) {
						toss("Cannot convert Str to Num: <%s>", self.str);
					}
					return terp.newNum(x);
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "toLower", "low",
					"Convert string to lowercase.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newStr(((Str) r).str.toLowerCase());
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "toUpper", "upp",
					"Convert string to uppercase.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newStr(((Str) r).str.toUpperCase());
				}
			});
			terp.tStr.addMethod(new JavaMeth(terp.tStr, "trimWhite", "trimw",
					"Trim whitespace from front and back.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.newStr(((Str) r).str.trim());
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "eq:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) == 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ne:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) != 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "lt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) < 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "le:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) <= 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "gt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) > 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ge:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) >= 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "starts:") {
				boolean compute(String sstr, String astr) {
					return sstr.startsWith(astr);
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ends:") {
				boolean compute(String sstr, String astr) {
					return sstr.endsWith(astr);
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "matchp:") {
				boolean compute(String sstr, String astr) {
					return Pattern.matches(astr, sstr);
				}
			});
		}
	}

	public final static class Vec extends Obj {
		public ArrayList<Pro> vec;

		// =cls "Data" Vec Obj
		Vec(Terp t) {
			super(t.tVec);
			this.vec = new ArrayList<Pro>();
		}

		Vec(Pro[] nonemptyArray) {
			super(nonemptyArray[0].cls.terp.tVec);
			this.vec = new ArrayList<Pro>();
			for (int i = 0; i < nonemptyArray.length; i++) {
				this.vec.add(nonemptyArray[i]);
			}
		}

		public Vec asVec() {
			return this;
		}

		public String toString() {
			StringBuffer sb = new StringBuffer("Vec(");
			for (int i = 0; i < vec.size(); i++) {
				assert vec.get(i) != null;
				sb.append(vec.get(i) == null ? " nil\"NULL\" " : vec.get(i)
						.repr());
				sb.append(". ");
			}
			sb.append(") ");
			return sb.toString();
		}

		public void visit(Visitor v) {
			v.visitVec(this);
		}

		@Override
		public boolean equals(Object o) {
			if (o instanceof Vec) {
				Vec that = (Vec) o;
				int sz = this.vec.size();
				if (sz == that.vec.size()) {
					for (int i = 0; i < sz; i++) {
						if (!this.vec.get(i).equals(that.vec.get(i))) {
							return false;
						}
					}
					return true;
				}
			}
			return false;
		}

		@Override
		public int compareTo(Object o) {
			if (o instanceof Vec) {
				Vec that = (Vec) o;
				int sz = this.vec.size();
				if (sz == that.vec.size()) {
					for (int i = 0; i < sz; i++) {
						int z = this.vec.get(i).compareTo(that.vec.get(i));
						if (z != 0) {
							return z;
						}
					}
					return 0;
				} else {
					return new Integer(this.vec.size()).compareTo(new Integer(
							that.vec.size()));
				}
			} else if (o instanceof Pro) {
				// Within our Pro world, order by classname, providing stability
				// after filein/fileout.
				Pro that = (Pro) o;
				return this.cls.name.compareTo(that.cls.name);
			} else {
				// Against foreign objects, fall back to identity hash.
				return new Integer(System.identityHashCode(this))
						.compareTo(new Integer(System.identityHashCode(o)));
			}
		}

		public int hashCode() {
			int z = 0;
			int sz = this.vec.size();
			for (int i = 0; i < sz; i++) {
				z = 13 * z + this.vec.get(i).hashCode();
			}
			return z;
		}

		int toNearestIndex(Pro a) {
			final int vlen = vec.size();
			if (vlen == 0) {
				toss("Cannot index into empty Vec");
			}
			Num num = a.asNum();
			if (num == null) {
				toss("Index is a %s, not a Num, in <Vec at:>", a.cls.name);
			}
			int i = (int) Math.floor(num.num + 0.5); // closest int
			i = ((i % vlen) + vlen) % vlen; // Positive-only Modulus
			return i;
		}

		// =meth Vec "access" len "return length of the Vec"
		public Num _len() {
			return cls.terp.newNum(vec.size());
		}

		// =meth Vec "access" at: "get element at index a, modulo length of Vec"
		public Pro at_(Num a) {
			return vec.get(toNearestIndex(a));
		}

		// =meth Vec "access" at:put:,at:p:
		// "put element b at given a, modulo length of Vec"
		public void at_put_(Num a, Pro b) {
			vec.set(toNearestIndex(a), b);
		}

		// =meth Vec "access" append:,ap: "add new element ato end of Vec"
		public void append_(Pro a) {
			vec.add(a);
		}

		static void addBuiltinMethodsForVec(final Terp terp) {
			// terp.tVec.addMethod(new JavaMeth(terp.tVec, "len", null,
			// "Length of the Vec.") {
			// public Num apply(Frame f, Pro r, Pro[] args) {
			// return new Num(terp, r.asVec().vec.size());
			// }
			// });
			// terp.tVec.addMethod(new JavaMeth(terp.tVec, "at:", null,
			// "Item at given index") {
			// public Pro apply(Frame f, Pro r, Pro[] args) {
			// Vec self = r.asVec();
			// return self.vec.get(self.toNearestIndex(args[0]));
			// }
			// });
			// terp.tVec
			// .addMethod(new JavaMeth(terp.tVec, "at:put:", "at:p:", "") {
			// public Pro apply(Frame f, Pro r, Pro[] args) {
			// Vec self = r.asVec();
			// self.vec.set(self.toNearestIndex(args[0]), args[1]);
			// return r;
			// }
			// });
			// terp.tVec.addMethod(new JavaMeth(terp.tVec, "append:", "ap:", "")
			// {
			// public Pro apply(Frame f, Pro r, Pro[] args) {
			// Vec self = r.asVec();
			// self.vec.add(args[0]);
			// return r;
			// }
			// });
			// terp.tVec.addMethod(new JavaMeth(terp.tVec, "setLen:", "sl:", "")
			// {
			// public Pro apply(Frame f, Pro r, Pro[] args) {
			// Vec self = r.asVec();
			// int oldSize = self.vec.size();
			// Num num = args[0].asNum();
			// if (num == null) {
			// toss("Expected argument to be a Num; got a <%s>.",
			// args[0].cls.name);
			// }
			// int newSize = (int) Math.floor(num.num + 0.5); // closest
			// // int
			// self.vec.setSize(newSize);
			// for (int i = oldSize; i < newSize; i++) {
			// self.vec.set(i, terp.tUndefined);
			// }
			// return r;
			// }
			// });
			terp.tVec.addMethod(new JavaMeth(terp.tVec, "doWithEach:", "do:",
					"Iterate the block with one argument, "
							+ "for each item in the Vec.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = r.asVec();
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					int stop = vec.vec.size();
					for (int i = 0; i < stop; i++) {
						blk.evalWith1Arg(vec.vec.get(i));
					}
					return terp.instNil;
				}
			});
			terp.tVecCls.addMethod(new JavaMeth(terp.tVecCls, "new", "",
					"Create a new empty Vec object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Vec(terp);
				}
			});
			terp.tVecCls.addMethod(new JavaMeth(terp.tVecCls, "append:", "ap:",
					"Create a new Vec of length 1 with the given item in it."
							+ "  Same as 'new append:'.") {
				// Shortcut for "new append:"
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = new Vec(terp);
					vec.vec.add(args[0]);
					return vec;
				}
			});
		}
	}

	public final static class Dict extends Obj {
		public HashMap<Pro, Pro> dict;

		// =cls "Data" Dict Obj
		Dict(Terp t) {
			super(t.tDict);
			this.dict = new HashMap<Pro, Pro>();
		}

		public Dict asDict() {
			return this;
		}

		public String toString() {
			Vec[] aarr = sortedAssocs();
			StringBuffer sb = new StringBuffer("Dict{");
			for (Vec a : aarr) {
				sb.append("(");
				sb.append(a.vec.get(0).repr());
				sb.append(", ");
				sb.append(a.vec.get(1).repr());
				sb.append("). ");
			}
			sb.append("} ");
			return sb.toString();
		}

		public boolean equals(Object o) {
			if (o instanceof Dict) {
				Dict that = (Dict) o;
				int sz = this.dict.size();
				if (sz == that.dict.size()) {
					Vec[] a = this.sortedAssocs();
					Vec[] b = that.sortedAssocs();
					for (int i = 0; i < sz; i++) {
						if (!a[i].vec.get(0).equals(b[i].vec.get(0))
								|| !a[i].vec.get(1).equals(b[i].vec.get(1))) {
							return false;
						}
					}
					return true;
				}
			}
			return false;
		}

		public int hashCode() {
			int z = 0;
			Vec[] a = this.sortedAssocs();
			for (int i = 0; i < a.length; i++) {
				z = 13 * z + a[i].vec.get(0).hashCode();
				z = 13 * z + a[i].vec.get(1).hashCode();
			}
			return z;
		}

		public void visit(Visitor v) {
			v.visitDict(this);
		}

		public Vec[] sortedAssocs() {
			Vec[] z = new Vec[dict.size()];
			int i = 0;
			for (Pro key : dict.keySet()) {
				z[i] = new Vec(pros(key, dict.get(key)));
				++i;
			}
			Arrays.sort(z);
			return z;
		}

		static void addBuiltinMethodsForDict(final Terp terp) {
			terp.tDict.addMethod(new JavaMeth(terp.tDict, "len", "",
					"Number of entries in the Dict.") {
				public Num apply(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asDict().dict.size());
				}
			});
			terp.tDict.addMethod(new JavaMeth(terp.tDict, "dir", "",
					"Number of entries in the Dict.") {
				public Vec apply(Frame f, Pro r, Pro[] args) {
					Vec z = terp.newVec(new Vec[0]);
					for (Vec aa : r.asDict().sortedAssocs()) {
						z.vec.add(aa.vec.get(0));
					}
					return z;
				}
			});
			terp.tDict
					.addMethod(new JavaMeth(terp.tDict, "at:", "",
							"Return the value stored at the given key, or Nil if it is missing.") {
						public Pro apply(Frame f, Pro r, Pro[] args) {
							Dict self = r.asDict();
							Pro z = self.dict.get(args[0]);
							if (z == null) {
								return terp.instNil;
							}
							return z;
						}
					});
			terp.tDict.addMethod(new JavaMeth(terp.tDict, "at:put:", "at:p:",
					"") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Dict self = r.asDict();
					self.dict.put(args[0], args[1]);
					return r;
				}
			});
			terp.tDictCls.addMethod(new JavaMeth(terp.tDictCls, "new", "",
					"Create a new empty Dict object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Dict(terp);
				}
			});
		}
	}

	public static class Visitor {
		protected Terp t;

		public Visitor(Terp t) {
			super();
			this.t = t;
		}

		public void visitPro(Pro a) {
			t.toss("SubclassResponsibility(Visitor::visitPro)");
		}

		public void visitCls(Cls a) {
			visitPro(a);
		}

		public void visitNum(Num a) {
			visitPro(a);
		}

		public void visitStr(Str a) {
			visitPro(a);
		}

		public void visitUndefined(Undefined a) {
			visitPro(a);
		}

		public void visitVec(Vec a) {
			visitPro(a);
		}

		public void visitDict(Dict a) {
			visitPro(a);
		}

		public void visitUsr(Usr a) {
			visitPro(a);
		}
	}
}
