module batchinterpreter; import std.date; import std.file; import std.stdio; import std.string; import ast; import environment; import formals; import lqtools; import lqtypes; import parser; import special; import stringtools; import tokenizer; import tools; /* modules with builtin functions: */ static import lists, builtins, equivalence, environments, lambdas, numbers, dictionaries, files, metadata; /* class to collect and classify function arguments. */ class FunArgs { FunSig funsig; LqType[string] args; LqType[] restargs; LqType[string] keywords; this(FunSig funsig) { this.funsig = funsig; } void add_plain_arg(LqType expr) { string argname = next_available_arg(); if (argname is null) throw new Exception("too many arguments"); if (string_in_list(argname, this.funsig.args)) { this.args[argname] = expr; } else if (argname == this.funsig.restarg) { this.restargs ~= expr; } else throw new Exception("cannot add arg"); } void add_keyword(string keyword, LqType value) { if (!string_in_list(keyword, this.funsig.keywords)) throw new Exception(format("Invalid keyword: %s", keyword)); if (string_in_list(keyword, this.keywords.keys)) throw new Exception(format("Keyword used twice: %s", keyword)); this.keywords[keyword] = value; } void add_delayed_arg(LqType expr) { add_plain_arg(expr); } void check_completeness() { foreach(string name; this.funsig.args) { if (!string_in_list(name, args.keys)) throw new Exception(format("Missing argument: %s", name)); } } /* Return the name of the next available argument that must be filled. */ string next_available_arg() { foreach(string name; this.funsig.args) { if (!string_in_list(name, args.keys)) return name; } if (this.funsig.restarg !is null) return this.funsig.restarg; return null; } string repr() { string[] stuff = []; foreach(string name; this.args.keys) { stuff ~= format("%s=%s", name, this.args[name].lq_repr()); } if (this.funsig.restarg !is null) { string[] t = []; foreach(LqType q; this.restargs) { t ~= q.lq_repr(); } stuff ~= "(" ~ std.string.join(t, " ") ~ ")"; } foreach(string name; this.keywords.keys) { stuff ~= format("%s=%s", name, this.keywords[name].lq_repr()); } return std.string.join(stuff, ", "); } } class StackFrame { LqType expr; /* expression to be evaluated */ Environment env; bool done = false; LqType[] processed; LqList rest; bool is_special_form = false; FunArgs funargs = null; this(LqType expr, Environment env) { this.expr = expr; this.env = env; this.done = false; if (expr.is_list()) this.rest = cast(LqList) expr; this.processed = []; this.is_special_form = special.is_special_form(this.expr); } /* Return a printable representation of the expression that is being evaluated. */ string repr(bool use_placeholder) { /* non-lists are returned as-is */ if (this.rest is null) return this.expr.lq_repr(); LqType[] elems = this.processed[]; if (use_placeholder) elems ~= new LqSymbol("$$"); elems ~= this.rest.to_list(); string[] reprs = []; foreach (LqType q; elems) { reprs ~= q.lq_repr(); } return "(" ~ std.string.join(reprs, " ") ~ ")"; } } class BatchInterpreter { int _num_calls = 0; int _max_depth = 0; Environment _env, _toplevel; StackFrame[] _call_stack; public: bool show_stack = false; bool show_elapsed = false; long elapsed = 0; /* time that last evaluation took */ this() { _toplevel = create_toplevel_env(); _env = new Environment(_toplevel); _call_stack = []; init(); } void init() { include("lists.lq"); include("predicates.lq"); include("environment.lq"); include("lazy.lq"); include("conditionals.lq"); include("dict.lq"); include("metadata.lq"); include("temp.lq"); /* temporary */ } Environment create_toplevel_env() { Environment env = new Environment(); /* populate... */ env.bind("#t", LQ_TRUE()); env.bind("#f", LQ_FALSE()); /* these functions are bound to methods of BatchInterpreter */ env.bind("apply", new LqBuiltinFunction(&s_apply, "apply")); env.bind("apply-in", new LqBuiltinFunction(&s_apply_in, "apply-in")); env.bind("eval", new LqBuiltinFunction(&s_eval, "eval")); env.bind("eval-in", new LqBuiltinFunction(&s_eval_in, "eval-in")); env.bind("include", new LqBuiltinFunction(&s_include, "include")); env.bind("trace", new LqBuiltinFunction(&s_trace, "trace")); /* modules */ load_builtins(builtins.get_builtins(), env); load_builtins(lists.get_builtins(), env); load_builtins(equivalence.get_builtins(), env); load_builtins(environments.get_builtins(), env); load_builtins(lambdas.get_builtins(), env); load_builtins(numbers.get_builtins(), env); load_builtins(dictionaries.get_builtins(), env); load_builtins(files.get_builtins(), env); load_builtins(metadata.get_builtins(), env); return env; } // returns null or an LqType... LqType eval_step() { _num_calls++; _max_depth = max(_max_depth, _call_stack.length); if (this.show_stack) show_stack_repr(); /* what's on the call stack? */ StackFrame frame = get_top_frame(); LqType expr = frame.expr; /* if we're processing a user-defined function, make sure frame.funargs is set */ if (processing_userdef_function(frame)) { if (frame.funargs is null) frame.funargs = new FunArgs((cast(LqUserDefinedFunction)(frame.processed[0])).funsig); } /* if it's a pair, it's a special form or a function call */ if (expr.type_indicator() == "pair") { LqList rest = frame.rest; /* what's left of the expression? */ if (rest.is_null()) { /* done evaluating all the parts of the list */ /* all of it should be in processed */ if (frame.is_special_form) { SFApplyResult sfr = sf_apply(frame.processed, frame.env); if (sfr.done) return collapse(sfr.result); else { /* TCO: replace current stack frame with new one, containing expr to be evaluated further */ auto newframe = new StackFrame(sfr.result, frame.env); pop_frame(); _call_stack ~= newframe; return null; } } else { LqType result = do_function_call(frame.processed, frame.env); if (result is null) return null; else return collapse(result); } } else { /* there are still elements to be processed */ if (frame.is_special_form && !sf_must_evaluate(frame.processed, (cast(LqList)frame.expr).length())) move_next_element(); else if (processing_userdef_function(frame)) { /* if the next arg is a keyword... */ LqType next = (cast(LqPair)frame.rest).head(); if (next.type_indicator() == "keyword") move_next_element(); else if (next_arg_is_delayed(frame)) delay_next_element; else push_next_element(); } else push_next_element(); return null; } } /* otherwise it's atomic */ else { if (expr.type_indicator() == "symbol") { /* look up symbol in the current frame's environment */ return collapse(lookup_name(expr.toString(), frame.env)); } else return collapse(expr); } return null; /* indicates evaluation isn't done yet */ } LqType eval(string s, Environment env=null) { LqType result = null; LqType[] exprs = feed(s); foreach(LqType expr; exprs) result = eval(expr, env); return result; } private: /* Evaluate an expression (as an LqType), by repeatedly calling eval_step until we get a result back (i.e. not null). */ LqType eval(LqType expr, Environment env=null) { long t1 = std.date.getUTCtime(); long num_steps = 0; LqType result; feed_expr(expr, env); if (_call_stack.length == 0) return null; while (1) { num_steps++; if ((result = eval_step()) !is null) { long t2 = std.date.getUTCtime(); if (this.show_elapsed) { this.elapsed = t2 - t1; writefln("Evaluated %s in: %d", expr.lq_repr(), this.elapsed); writefln("Number of steps: %d", num_steps); } return result; } } } /* Take an expression that we're done evaluating. If it's the last thing left on the call stack, return it. Otherwise, collapse it into the parent expression. */ LqType collapse(LqType expr) { if (this._call_stack.length == 1) return expr; StackFrame current_frame = pop_frame(); StackFrame parent = get_top_frame(); parent.processed ~= expr; if (parent.funargs !is null && !keyword_seen(parent)) parent.funargs.add_plain_arg(expr); /* if we are processing keywords, we don't store things in regular args or restargs anymore */ return null; } LqType lookup_name(string name, Environment env) { EnvLookupResult r = env.lookup(name); return r.result; } LqType do_function_call(LqType[] processed, Environment env) { LqType f = processed[0]; LqType[] args = processed[1..processed.length]; switch (f.type_indicator()) { case "userdef-function": StackFrame oldframe = pop_frame(); /* create new environment with values bound to lambda args; then replace stack frame with new one, containing (begin ) associated with the new env. */ LqUserDefinedFunction udf = cast(LqUserDefinedFunction) f; Environment newenv; if (oldframe.funargs is null) newenv = udf.extend_env(args, env); else { assign_keywords(oldframe); /* uses funargs */ newenv = udf.extend_env(oldframe.funargs, env); /* we pass the caller env to wrap any delayed args */ } LqType expr = lqtools.wrap_in_begin(udf.fbody()); StackFrame newframe = new StackFrame(expr, newenv); _call_stack ~= newframe; return null; break; case "builtin-function": kwdict keywords = formals.parse_keywords(processed); LqBuiltinFunction bf = cast(LqBuiltinFunction) f; return bf.execute(args, keywords, env); default: throw new Exception("not a function call"); } } /* Take an input string, tokenize and parse it, and return a list of expressions derived from it. */ LqType[] feed(string s) { string[] tokens = tokenizer.tokenize(s); LqType[] exprs = parser.parse(tokens); return exprs; } /* Push an expression to be evaluated on the call stack. */ void feed_expr(LqType expr, Environment env=null) { Environment e = (env is null) ? this._env : env; StackFrame frame = new StackFrame(expr, e); this._call_stack = [frame]; this._num_calls = 0; } /* Pop the topmost stack frame and return it. */ StackFrame pop_frame() { StackFrame frame = get_top_frame(); this._call_stack = this._call_stack[0..this._call_stack.length-1]; return frame; } StackFrame get_top_frame() { return this._call_stack[this._call_stack.length-1]; } void show_stack_repr() { string[] reprs = []; foreach(int i, StackFrame f; this._call_stack) reprs ~= f.repr(i<_call_stack.length-1); writefln("[stack/%d] %s", _call_stack.length, std.string.join(reprs, " ")); } void push_next_element() { StackFrame frame = get_top_frame(); LqList rest = frame.rest; LqType next = (cast(LqPair)rest).head(); frame.rest = (cast(LqPair)rest).tail(); StackFrame newframe = new StackFrame(next, frame.env); this._call_stack ~= newframe; } void move_next_element() { /* move next subexpr as-is to processed */ StackFrame frame = get_top_frame(); LqList rest = frame.rest; LqType next = (cast(LqPair)rest).head(); frame.rest = (cast(LqPair)rest).tail(); frame.processed ~= next; } void delay_next_element() { StackFrame frame = get_top_frame(); assert(frame.funargs !is null, "delayed_next_element should only be called with funargs"); move_next_element(); LqType expr = frame.processed[frame.processed.length-1]; frame.funargs.add_delayed_arg(expr); } void load_builtins(bfun[string] funs, Environment env) { foreach(string name; funs.keys) { env.bind(name, new LqBuiltinFunction(funs[name], name)); } } bool processing_userdef_function(StackFrame frame) { return (frame.processed.length > 0 && frame.processed[0].type_indicator() == "userdef-function"); } /* Scan frame.processed for keywords and assign them. This should be done when frame.processed is complete. */ void assign_keywords(StackFrame frame) { kwdict keywords = formals.parse_keywords(frame.processed); foreach(string keyword; keywords.keys) { LqType value = keywords[keyword]; frame.funargs.add_keyword(keyword, value); } } /* Returns true if there's at least one keyword in frame.processed. */ bool keyword_seen(StackFrame frame) { foreach (LqType q; frame.processed) { if (q.type_indicator() == "keyword") return true; } return false; } bool next_arg_is_delayed(StackFrame frame) { if (frame.funargs !is null) { string next_arg = frame.funargs.next_available_arg(); if (next_arg !is null && formals.is_delayed_arg(next_arg)) return true; } return false; } /* to include files when we start the interpreter. */ void include(string path) { /* TODO: make dependent on location of executable */ string full_path = "include/" ~ path; char[] data = cast(string)std.file.read(full_path); eval(data, this._toplevel); } /*** built-in functions ***/ LqType s_apply(LqType[] args, kwdict kwargs, Environment env) { /* (apply f args) */ LqType f = args[0]; LqType fargs = args[1]; assert(f.is_function()); assert(fargs.is_list()); LqType[] items = [f]; items ~= (cast(LqList)fargs).to_list(); /* pass any keywords given to APPLY as well. */ foreach(string kw; kwargs.keys) { items ~= new LqKeyword(kw); items ~= kwargs[kw]; } return do_function_call(items, env); } LqType s_eval(LqType[] args, kwdict kwargs, Environment env) { /* (eval expr) */ StackFrame oldframe = pop_frame(); StackFrame newframe = new StackFrame(args[0], oldframe.env); _call_stack ~= newframe; return null; } LqType s_eval_in(LqType[] args, kwdict kwargs, Environment env) { /* (eval-in expr env) */ check_arglength(args, 2, 2, "eval-in"); check_arg(args[1], 1, ["userdef-function", "environment"], "eval-in"); Environment target_env = env_from_context(args[1]); StackFrame oldframe = pop_frame(); StackFrame newframe = new StackFrame(args[0], target_env); _call_stack ~= newframe; return null; } LqType s_apply_in(LqType[] args, kwdict kwargs, Environment env) { /* (apply-in f unevaluated-args env) */ check_arglength(args, 3, 3, "apply-in"); check_arg(args[0], 0, "userdef-function", "apply-in"); check_arg(args[1], 1, ["empty-list", "pair"], "apply-in"); check_arg(args[2], 2, ["userdef-function", "environment"], "apply-in"); Environment target_env = env_from_context(args[2]); LqType[] items = []; items ~= args[0]; // the function foreach(LqType q; as_list(args[1]).to_list()) items ~= q; // later: keywords? remember that these are already evaluated... LqType new_expr = LqList.from_list(items); StackFrame oldframe = pop_frame(); StackFrame newframe = new StackFrame(new_expr, target_env); _call_stack ~= newframe; move_next_element(); /* move function, so we don't try to evaluate it again */ return null; } /* could be written in pure Liquid later? */ LqType s_include(LqType[] args, kwdict kwargs, Environment env) { /* (include ) */ assert(args[0].type_indicator(), "string"); string path = (cast(LqString)args[0]).lq_str(); char[] data = cast(string)std.file.read(path); LqType[] exprs = feed(data); LqType all = wrap_in_begin(exprs); StackFrame oldframe = pop_frame(); StackFrame newframe = new StackFrame(all, oldframe.env); _call_stack ~= newframe; return null; } LqType s_trace(LqType[] args, kwdict kwargs, Environment env) { /* (trace {#t|#f}) */ /* Turn tracing (i.e., display of evaluation steps) on/off. */ check_arglength(args, 1, 1, "trace"); check_arg(args[0], 0, "boolean", "trace"); bool tracing_on = !(args[0] is LQ_FALSE()); this.show_stack = tracing_on; return LQ_UNSPECIFIED(); } } unittest { /// }