module lqtypes; /* For now, put all Lq* types here... later the bigger ones can be moved to their own modules, if necessary. */ import std.array; import std.string; import std.stdio; import std.stream; import batchinterpreter; import environment; import formals; import stringtools; alias LqType function(LqType[] args, kwdict kwargs, Environment env) bfun; alias LqType delegate(LqType[] args, kwdict kwargs, Environment env) bdeg; abstract class LqType { LqType[string] meta; abstract string lq_repr() { return "undefined"; } string type_indicator() { return "?"; } bool is_list() { return false; } bool is_number() { return false; } bool is_function() { return false; } ulong id() { return cast(ulong)this; /* NOT: &this */ } void set_meta(string name, LqType value) { this.meta[name] = value; } LqType get_meta(string name) { return this.meta[name]; } } abstract class LqNumber: LqType { string type_indicator() { return "number"; } bool is_number() { return true; } } class LqInteger: LqNumber { private long _data = 0; string type_indicator() { return "integer"; } this(long n) { this._data = n; } static LqInteger from_token(string token) { return new LqInteger(atoi(token)); } string lq_repr() { return std.string.toString(this._data); } long value() { return _data; } } class LqFloat: LqNumber { private double _data; string type_indicator() { return "float"; } double value() { return _data; } } class LqString: LqType { string type_indicator() { return "string"; } private string _data = ""; this(string s) { this._data = s; } static LqString from_token(string token) { assert(token.length >= 2 && token[0] == '"' && token[token.length-1] == '"', format("Invalid token: {%s}", token)); /* remove double quotes and unescape things like \\n */ string s = stringtools.unescape(token[1..token.length-1]); return new LqString(s); } string lq_repr() { /* add double quotes and escape special characters like \n */ return '"' ~ stringtools.escape(this._data) ~ '"'; } string lq_str() { return this._data; } } abstract class LqList: LqType { string type_indicator() { return "list"; } abstract bool is_null() { return true; } bool is_list() { return true; } abstract LqType[] to_list() { throw new Exception("abstract"); } /* Convert a flat list of LqType instances to an LqPair. */ static LqList from_list(LqType[] items) { if (items.length == 0) return LQ_EMPTY_LIST(); LqPair first = new LqPair(items[0]); LqPair current = first; foreach(int i, LqType q; items) { if (i==0) continue; LqPair node = new LqPair(items[i]); current.set_tail(node); assert(current.tail() !is LQ_EMPTY_LIST()); current = node; } return first; } abstract long length() { throw new Exception("abstract"); } } class LqPair: LqList { string type_indicator() { return "pair"; } private LqType _head; private LqList _tail; this(LqType head) { this._head = head; this._tail = LQ_EMPTY_LIST(); } this(LqType head, LqList tail) { this._head = head; this._tail = tail; } /* for internal use only: */ void set_head(LqType x) { _head = x; } void set_tail(LqList x) { _tail = x; } LqType head() { return _head; } LqList tail() { return _tail; } bool is_null() { return false; } // non-empty by definition string lq_repr() { if (_tail is LQ_EMPTY_LIST()) { return format("(%s)", _head.lq_repr()); } else { string trepr = _tail.lq_repr(); return format("(%s %s)", _head.lq_repr(), trepr[1..trepr.length-1]); } } LqType[] to_list() { /* Returns a flat Python list with Liquid expressions. Does not convert sublists. Used internally for quick conversion of arglists etc. */ LqType[] collected = [_head]; LqList next = _tail; while (next !is LQ_EMPTY_LIST()) { LqPair n = cast(LqPair)next; collected ~= n.head(); next = cast(LqList)(n.tail()); } return collected; } long length() { long length = 0; LqList node = this; while (node !is LQ_EMPTY_LIST()) { length++; node = (cast(LqPair)node).tail(); } return length; } } class LqEmptyList: LqList { string type_indicator() { return "empty-list"; } string lq_repr() { return "()"; } bool is_null() { return true; } LqType[] to_list() { return []; } long length() { return 0; } } class LqSymbol: LqType { string type_indicator() { return "symbol"; } private string _data = ""; this(string s) { _data = s; } static LqSymbol from_token(string token) { return new LqSymbol(token); // FIXME: use symbol "cache" } string lq_repr() { return _data; } string toString() { return _data; } } class LqFunction: LqType { string type_indicator() { return "function"; } bool is_function() { return true; } } class LqBuiltinFunction: LqFunction { private bfun _f; /* built-in function that is being wrapped */ private bdeg _g; /* OR, a built-in method */ private string _name; string type_indicator() { return "builtin-function"; } this(bfun f, string name=null) { _f = f; _name = name; } this(bdeg f, string name=null) { _g = f; _name = name; } LqType execute(LqType args[], kwdict kwargs, Environment env) { if (_f !is null) return this._f(args, kwargs, env); if (_g !is null) return this._g(args, kwargs, env); } string lq_repr() { return format("#", _name ? _name : ""); } } class LqUserDefinedFunction: LqFunction { private string _name; /* optional */ private LqType[] _fbody; /* one or more expressions */ private string[] _formals; /* for now; later, add restargs, delayed, keywords, etc */ private Environment _env; private FunSig _funsig; string type_indicator() { return "userdef-function"; } this(string[] formals, LqType[] fbody, Environment env, string name=null) { assert(fbody.length > 0, "lambda: function may not have empty body"); this._formals = formals; this._fbody = fbody; this._name = name; this._env = env; this._funsig = funsig_from_formals(formals); } /* Called when we already have a list of processed/evaluated values, rather than a FunArgs instance (e.g. when using APPLY). */ Environment extend_env(LqType[] processed, Environment caller_env) { FunArgs funargs = new FunArgs(this.funsig); /* add regular args */ foreach(string argname; funsig.args) { funargs.add_plain_arg(processed[0]); processed = processed[1..processed.length]; } /* add rest args */ while (processed.length > 0 && processed[0].type_indicator() != "keyword") { funargs.add_plain_arg(processed[0]); processed = processed[1..processed.length]; } /* add keyword args */ funargs.keywords = formals.parse_keywords(processed); return extend_env(funargs, caller_env); } Environment extend_env(FunArgs funargs, Environment caller_env) { /* caller_env is used to wrap any delayed arguments. */ auto newenv = new Environment(this._env); foreach(string name; this.funsig.args) { LqType value = funargs.args[name]; if (is_delayed_arg(name)) { LqType lambda = new LqUserDefinedFunction([], [value], caller_env); newenv.bind(name, lambda); } else newenv.bind(name, value); } if (this.funsig.restarg !is null) { LqList rest = LqList.from_list(funargs.restargs); if (is_delayed_arg(this.funsig.restarg)) { LqType lambda = new LqUserDefinedFunction([], [rest], caller_env); newenv.bind(this.funsig.restarg, lambda); } else newenv.bind(this.funsig.restarg, rest); } /* bind keywords to variables, e.g. :foo gets bound to foo, etc. If a keyword isn't specified, it defaults to #f. */ foreach(string kwname; this.funsig.keywords) { string varname = kwname[1..kwname.length]; /* strip ":" */ LqType value; try { value = funargs.keywords[kwname]; } catch (ArrayBoundsError e) { value = LQ_FALSE(); } //writefln("Binding keyword var: %s to %s", varname, value.lq_repr()); newenv.bind(varname, value); } /* TODO: delayed args */ return newenv; } string lq_repr() { return format("#", this.header_repr()); } string header_repr() { return "(" ~ std.string.join(this._formals, " ") ~ ")"; } LqType[] fbody() { return this._fbody; } Environment env() { return this._env; } FunSig funsig() { return this._funsig; } } class LqKeyword: LqType { string _data; string type_indicator() { return "keyword"; } this(string name) { assert(stringtools.starts_with(name, ":")); this._data = name; /* with the leading ":" */ } static LqKeyword from_token(string token) { return new LqKeyword(token); } string lq_repr() { return this._data; } } class LqBoolean: LqType { string type_indicator() { return "boolean"; } private bool _data; this(bool x) { _data = x; } string lq_repr() { return _data ? "#t" : "#f"; } static LqBoolean from_boolean(bool x) { return x ? LQ_TRUE() : LQ_FALSE(); } } class LqEnvironment: LqType { string type_indicator() { return "environment"; } public Environment env; this(Environment env) { this.env = env; } string lq_repr() { return format("#", this.id()); } } class LqUnspecified: LqType { string type_indicator() { return "unspecified"; } string lq_repr() { return "#"; } } alias LqType[] lqtypepair; enum dictionary_mode { VALUE, IDENTITY }; class LqDictionary: LqType { /* A dictionary type, kind of like Python's. We store a key (always a string) and a pair (key, value) of LqTypes. The string-key is determined by the dictionary type: VALUE => key based on key.lq_repr() IDENTITY => key based on key.id() This is to avoid ambiguity between lookups based on object id and object value. (E.g. two strings with the same value may have different object identities, so this could cause confusion when looking them up.) */ string type_indicator() { return "dict"; } private lqtypepair[string] _data; private dictionary_mode _mode; private bool _static = false, _readonly = false; this(dictionary_mode mode=dictionary_mode.VALUE) { this._mode = mode; } string lq_repr() { string s = ""; lqtypepair[] zitems = items(); foreach(lqtypepair pair; zitems) s ~= format(" (%s %s)", pair[0].lq_repr(), pair[1].lq_repr()); s = "(dict" ~ s ~ ")"; /* todo: add keywords? */ return s; } /* dictionary API */ void add(LqType key, LqType value) { string skey = get_key(key); this._data[skey] = [key, value]; } bool remove(LqType key) { string skey = get_key(key); if (!string_in_list(skey, this._data.keys)) return false; this._data.remove(skey); /* NO error if doesn't exist */ return true; } lqtypepair get(LqType key) { string skey = get_key(key); return this._data[skey]; } LqType[] keys() { LqType[] keys = []; foreach(string key; this._data.keys.sort) { keys ~= this._data[key][0]; } return keys; } LqType[] values() { LqType[] values = []; foreach(string key; this._data.keys.sort) { values ~= this._data[key][1]; } return values; } lqtypepair[] items() { lqtypepair[] items = []; foreach(string key; this._data.keys.sort) { items ~= this._data[key]; } return items; } int length() { return this._data.length; } private: string get_key(LqType x) { switch (this._mode) { case dictionary_mode.VALUE: return x.lq_repr(); break; case dictionary_mode.IDENTITY: return std.string.toString(x.id()); break; default: throw new Exception("unknown dictionary mode"); } } } /* eventually, we could define this in pure Liquid... */ class LqModule: LqType { private Environment _env; private string _path; private string _name; string type_indicator() { return "module"; } this(Environment builtin_env, string path) { this._env = new Environment(builtin_env); this._path = path; } void load(string raw_data) { } LqType lookup(string name) { return this._env.lookup_local(name); /* don't look in parent environments, we don't want builtins to be accessed this way */ } void set(string name, LqType value) { this._env.bind(name, value); } string lq_repr() { return "#"; } } class LqFile: LqType { private std.stream.File _file; private std.stream.FileMode _mode; string type_indicator() { return "file"; } /* open with an existing file, probably stdin/stdout */ this(File f) { } /* possible file modes: In, Out, OutNew, Append */ this(string filename, FileMode mode) { this._file = new std.stream.File(filename, mode); this._mode = mode; } string lq_repr() { return "#"; // FIXME } File file() { return _file; } FileMode mode() { return _mode; } } /*** singletons ***/ LqEmptyList LQ_EMPTY_LIST() { static LqEmptyList cached; if (cached is null) { cached = new LqEmptyList(); return cached; } else return cached; } LqBoolean LQ_TRUE() { static LqBoolean cached; if (cached is null) { cached = new LqBoolean(true); return cached; } else return cached; } LqBoolean LQ_FALSE() { static LqBoolean cached; if (cached is null) { cached = new LqBoolean(false); return cached; } else return cached; } LqUnspecified LQ_UNSPECIFIED() { static LqUnspecified cached; if (cached is null) { cached = new LqUnspecified(); return cached; } else return cached; }