`RandomChance` type, and two new item fields: `MagicEffectChance`, `MagicEffectDuration`
[k8-i-v-a-n.git] / utils / parser.d
blobe9cd4a9f4d57b2d53852709ed3529c0fccc7e637
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module parser is aliced;
19 import iv.strex;
20 import iv.unarray;
21 import iv.vfs.io;
24 // ////////////////////////////////////////////////////////////////////////// //
25 struct Loc {
26 int line, col;
27 string fname;
29 public:
30 string toString () const {
31 import std.format : format;
32 if (fname.length) return "'%s' (%d,%d)".format(fname, line, col);
33 return "'?' (%d,%d)".format(line, col);
38 struct Token {
39 public:
40 enum TT { EOF, IdNum, Str, Delim }
42 public:
43 string toString () const {
44 import std.format : format;
45 return "Token(%s [%s])".format(type, loc.toString);
48 public:
49 TT type;
50 string value;
51 Loc loc;
53 public:
54 @property bool isEOF () const nothrow @safe @nogc { return (type == TT.EOF); }
55 @property bool isStr () const nothrow @safe @nogc { return (type == TT.Str); }
56 @property bool isIdNum () const nothrow @safe @nogc { return (type == TT.IdNum); }
57 @property bool isDelim () const nothrow @safe @nogc { return (type == TT.Delim); }
59 @property bool isIdNum (const(char)[] s) const nothrow @safe @nogc { return (type == TT.IdNum && value == s); }
60 @property bool isDelim (const(char)[] s) const nothrow @safe @nogc { return (type == TT.Delim && value == s); }
62 @property string unquote () const nothrow {
63 if (!isStr) return value;
64 assert(value.length >= 2 && value[0] == '"' && value[$-1] == '"');
65 string res;
66 usize pos = 1;
67 while (pos < value.length-1) {
68 if (value[pos] == '\\') {
69 ++pos;
70 switch (value[pos++]) {
71 case 'n': res ~= '\n'; break;
72 case 't': res ~= '\t'; break;
73 default: res ~= value[pos-1]; break;
75 } else {
76 res ~= value[pos++];
79 return res;
82 bool opEquals() (in auto ref Token b) const nothrow @safe @nogc {
83 if (type != b.type) return false;
84 if (type == TT.EOF) return true;
85 return (value == b.value);
90 // ////////////////////////////////////////////////////////////////////////// //
91 class SimpleIvanParser {
92 private:
93 VFile fl;
94 char lastChar, nextChar;
96 private:
97 void skipChar () {
98 if (lastChar == 0) return;
99 if (lastChar == '\n') { loc.col = 1; ++loc.line; } else ++loc.col;
100 lastChar = nextChar;
101 if (fl.rawRead((&nextChar)[0..1]).length == 0) { nextChar = 0; return; }
102 if (nextChar == 0) nextChar = ' ';
105 private:
106 void openFile (VFile afl) {
107 fl = afl;
108 lastChar = ' ';
109 fl.rawReadExact((&nextChar)[0..1]);
110 if (nextChar == 0xEF) {
111 skipChar();
112 assert(nextChar == 0xBB);
113 skipChar();
114 assert(nextChar == 0xBF);
115 skipChar();
117 skipChar();
118 loc.col = 1;
119 loc.line = 1;
120 loc.fname = fl.name.idup;
121 nextToken();
124 public:
125 Token tok;
126 Loc loc;
128 public:
129 this (VFile afl) {
130 openFile(afl);
133 final void error (const(char)[] msg) {
134 import std.format : format;
135 throw new Exception("parser error in '%s' at (%d,%d): %s".format(loc.fname, loc.line, loc.col, msg));
138 final void tokerror (const(char)[] msg) {
139 import std.format : format;
140 throw new Exception("parser error in '%s' at (%d,%d): %s".format(tok.loc.fname, tok.loc.line, tok.loc.col, msg));
143 final void errorAt() (in auto ref Loc aloc, const(char)[] msg) {
144 import std.format : format;
145 throw new Exception("parser error in '%s' at (%d,%d): %s".format(aloc.fname, aloc.line, aloc.col, msg));
148 final @property bool eof () const nothrow @safe @nogc { return tok.isEOF; }
150 final void expectDelim (const(char)[] s) {
151 if (!tok.isDelim || tok.value != s) tokerror("delimiter '"~s~"' expected");
152 nextToken();
155 final void expectIdNum (const(char)[] s) {
156 if (!tok.isIdNum || tok.value != s) tokerror("identifier '"~s~"' expected");
157 nextToken();
160 final string expectIdNum () {
161 if (!tok.isIdNum) tokerror("delimiter expected");
162 string res = tok.value;
163 nextToken();
164 return res;
167 final bool eatIdNum (const(char)[] s) { if (tok.isIdNum && tok.value == s) { nextToken(); return true; } return false; }
168 final bool eatDelim (const(char)[] s) { if (tok.isDelim && tok.value == s) { nextToken(); return true; } return false; }
171 final void skipSpaces () {
172 if (lastChar == 0) return;
173 while (lastChar) {
174 if (lastChar <= ' ') { skipChar(); continue; }
175 if (lastChar == '/' && nextChar == '/') {
176 while (lastChar && lastChar != '\n') skipChar();
177 continue;
179 if (lastChar == '/' && nextChar == '*') {
180 skipChar();
181 skipChar();
182 while (lastChar) {
183 if (lastChar == '*' && nextChar == '/') { skipChar(); skipChar(); break; }
184 skipChar();
186 continue;
188 break;
192 void nextToken () {
193 skipSpaces();
194 tok = Token(Token.TT.EOF, null, loc);
195 if (lastChar == 0) return; // EOF
196 if (lastChar == '\'') error("invalid quote");
197 // string?
198 if (lastChar == '"') {
199 tok.type = Token.TT.Str;
200 skipChar();
201 tok.value ~= '"';
202 while (lastChar) {
203 if (lastChar == '\\') { tok.value ~= lastChar; skipChar(); assert(lastChar != 0); }
204 else if (lastChar == '"') break;
205 tok.value ~= lastChar;
206 skipChar();
208 assert(lastChar == '"');
209 tok.value ~= '"';
210 skipChar();
211 return;
213 // id/num
214 if (lastChar >= 128 || isalnum(lastChar) || lastChar == '_') {
215 tok.type = Token.TT.IdNum;
216 while (lastChar >= 128 || isalnum(lastChar) || lastChar == '_') {
217 tok.value ~= lastChar;
218 skipChar();
220 return;
222 // delimiter
223 tok.type = Token.TT.Delim;
224 if (lastChar == '=' && nextChar == '=') { tok.value = "=="; skipChar(); skipChar(); return; }
225 if (lastChar == ':' && nextChar == '=') { tok.value = ":="; skipChar(); skipChar(); return; }
226 if (lastChar == '<' && nextChar == '<') { tok.value = "<<"; skipChar(); skipChar(); return; }
227 if (lastChar == '>' && nextChar == '>') { tok.value = ">>"; skipChar(); skipChar(); return; }
228 if (lastChar == '<' && nextChar == '=') { tok.value = "<="; skipChar(); skipChar(); return; }
229 if (lastChar == '>' && nextChar == '=') { tok.value = ">="; skipChar(); skipChar(); return; }
230 if (lastChar == '!' && nextChar == '=') { tok.value = "!="; skipChar(); skipChar(); return; }
231 if (lastChar == '<' && nextChar == '>') { tok.value = "<>"; skipChar(); skipChar(); return; }
232 tok.value = ""~lastChar;
233 skipChar();
238 // ////////////////////////////////////////////////////////////////////////// //
239 class IvanParser : SimpleIvanParser {
240 private:
241 static struct State {
242 VFile fl;
243 char lastChar, nextChar;
244 Loc loc;
247 private:
248 State[] mStateStack;
249 string basePath;
251 private:
252 bool popState () {
253 if (mStateStack.length == 0) return false;
254 fl = mStateStack[$-1].fl;
255 lastChar = mStateStack[$-1].lastChar;
256 nextChar = mStateStack[$-1].nextChar;
257 loc = mStateStack[$-1].loc;
258 mStateStack.unsafeArraySetLength(mStateStack.length-1);
259 return true;
262 void pushState (const(char)[] newfile) {
263 if (mStateStack.length > 128) throw new Exception("too many includes");
264 assert(newfile.length);
265 mStateStack.unsafeArraySetLength(mStateStack.length+1);
266 mStateStack[$-1].fl = fl;
267 mStateStack[$-1].lastChar = lastChar;
268 mStateStack[$-1].nextChar = nextChar;
269 mStateStack[$-1].loc = loc;
270 openFile(VFile(basePath~newfile));
273 public:
274 this (VFile afl) {
275 import std.path : dirName;
276 super(afl);
277 basePath = afl.name.dirName.idup;
278 if (basePath.length && basePath[$-1] != '/') basePath ~= '/';
281 override void nextToken () {
282 super.nextToken();
283 for (;;) {
284 // eof: pop include
285 if (tok.isEOF) {
286 if (!popState()) return;
287 super.nextToken();
288 continue;
290 // include
291 if (tok.isIdNum("Include")) {
292 super.nextToken(); // get file name
293 if (!tok.isStr) tokerror("string expected");
294 auto fname = tok.unquote;
295 super.nextToken(); // get ";"
296 if (!tok.isDelim(";")) tokerror("';' expected");
297 pushState(fname);
298 continue;
300 // normal token
301 break;
307 // ////////////////////////////////////////////////////////////////////////// //
308 class SimpleField {
309 Loc loc;
310 string name;
311 Token[] value; // just tokens
313 this (SimpleIvanParser par) {
314 // "}" added to list
315 void collectCurly () {
316 int level = 1;
317 while (level) {
318 if (par.tok.isEOF) par.error("unexpected EOF");
319 value ~= par.tok;
320 if (par.tok.isDelim("{")) ++level;
321 else if (par.tok.isDelim("}")) --level;
322 par.nextToken();
326 void collectOne () {
327 while (!par.eof && !par.tok.isDelim(";")) {
328 //{ import iv.vfs.io; writeln(" ONE: ", par.tok); }
329 value ~= par.tok;
330 par.nextToken();
331 if (value[$-1].isDelim("{")) {
332 collectCurly();
333 return;
336 par.expectDelim(";");
337 //{ import iv.vfs.io; writeln(" ONEendloc=", par.tok.loc, "; ", name); }
340 loc = par.tok.loc;
341 name = par.expectIdNum();
342 //{ import iv.vfs.io; writeln("fld: loc=", par.tok.loc, "; ", name); }
343 if (!par.tok.isDelim) par.expectDelim("=");
344 if (par.eatDelim("=")) {
345 // array?
346 if (par.tok.isDelim("{")) {
347 // skip counter and comma
348 value ~= par.tok;
349 par.expectDelim("{");
350 par.expectIdNum();
351 par.expectDelim(",");
352 collectCurly();
353 } else {
354 collectOne();
356 } else if (par.eatDelim("==")) {
357 collectOne();
358 } else if (par.eatDelim(":=")) {
359 value ~= par.tok;
360 par.expectDelim("{");
361 collectCurly();
362 } else {
363 par.expectDelim("=");
365 //{ import iv.vfs.io; writeln("fld: ENDloc=", par.tok.loc, "; ", name); }
368 override bool opEquals (in Object o) const {
369 if (o is null) return false;
370 if (o is this) return true;
371 if (auto op = cast(SimpleField)o) return (name == op.name && value == op.value);
372 return false;
375 override string toString () const {
376 string res = "<";
377 foreach (immutable idx, const ref tk; value) {
378 if (idx != 0) res ~= " ";
379 res ~= tk.value;
381 res ~= ">";
382 return res;
385 @property bool asBool () const nothrow @safe @nogc {
386 if (value.length == 0) return false;
387 if (value.length > 1) return true;
388 if (value[0].isIdNum("false")) return false;
389 if (value[0].isIdNum("0")) return false;
390 return true;
395 // ////////////////////////////////////////////////////////////////////////// //
396 // "{" should be already parsed; will stop after eating ending "}"
397 class SimpleConfig {
398 bool base;
399 bool broken;
400 Loc loc;
401 string name;
402 SimpleField[string] fields;
404 this () { name = "<default>"; }
406 this (SimpleIvanParser par, string aname, bool ignoredups=false) {
407 name = aname;
408 loc = par.tok.loc;
409 for (;;) {
410 if (par.eof) par.expectDelim("}");
411 if (par.tok.isDelim("{")) par.tokerror("wtf?!");
412 if (par.eatDelim("}")) break;
413 parseField(par, ignoredups);
417 void parseField (SimpleIvanParser par, bool ignoredups=false) {
418 auto fld = new SimpleField(par);
419 if (fld.name == "CanBeBurned") return;
420 if (auto ofp = fld.name in fields) {
421 if (*ofp == fld) {
422 writeln("WARNING: ", fld.loc, ": duplicate field '"~fld.name~"' (prev vas at "~ofp.loc.toString~")");
423 } else {
424 if (ignoredups) {
425 writeln("WARNING: ", fld.loc, ": duplicate DIFFERENT field '"~fld.name~"' (prev vas at "~ofp.loc.toString~")");
426 } else {
427 par.errorAt(fld.loc, "duplicate field '"~fld.name~"' (prev vas at "~ofp.loc.toString~")");
431 fields[fld.name] = fld;
434 override bool opEquals (in Object o) const {
435 if (o is null) return false;
436 if (o is this) return true;
437 if (auto op = cast(SimpleConfig)o) {
438 if (name != op.name) return false;
439 bool[string] hit;
440 foreach (const SimpleField fld; op.fields.byValue) {
441 if (auto myfldp = fld.name in fields) {
442 if (*myfldp != fld) return false;
443 hit[fld.name] = true;
444 } else {
445 return false;
448 foreach (const SimpleField fld; fields.byValue) {
449 if (fld.name !in hit) return false;
451 return true;
453 return false;
456 string[] diff (in SimpleConfig other) const {
457 assert(other !is null);
458 string[] res;
459 if (name != other.name) { res ~= "* "~name; res ~= "* "~other.name; }
460 // compare fields
461 bool[string] hit;
462 foreach (const SimpleField myfld; fields.byValue) {
463 if (myfld.name !in other.fields) continue;
464 hit[myfld.name] = true;
465 if (myfld != other.fields[myfld.name]) {
466 res ~= "* "~myfld.name;
467 res ~= " "~myfld.toString;
468 res ~= " "~other.fields[myfld.name].toString;
471 // remove unhit fields
472 foreach (const SimpleField myfld; fields.byValue) {
473 if (myfld.name !in hit) {
474 res ~= "- "~myfld.name;
475 res ~= " "~myfld.toString;
478 // add other unhit fields
479 foreach (const SimpleField itfld; other.fields.byValue) {
480 if (itfld.name !in fields) {
481 res ~= "+ "~itfld.name;
482 res ~= " "~itfld.toString;
485 return res;
488 inout(SimpleField) opIndex (const(char)[] afldname) inout nothrow @safe @nogc {
489 if (auto fp = afldname in fields) return cast(typeof(return))(*fp);
490 return null;
495 // ////////////////////////////////////////////////////////////////////////// //
496 class SimpleDef {
497 bool base;
498 Loc loc;
499 string name;
500 SimpleConfig[string] configs;
502 //this (string aname) { name = aname; }
503 this (SimpleIvanParser par, bool ignoredups=false) {
504 loc = par.tok.loc;
505 name = par.expectIdNum();
506 auto defcfg = new SimpleConfig();
507 defcfg.loc = par.tok.loc;
508 defcfg.base = true;
509 par.expectDelim("{");
510 configs["<default>"] = defcfg;
511 for (;;) {
512 if (par.eof) par.expectDelim("}");
513 if (par.tok.isDelim("{")) par.tokerror("wtf?!");
514 if (par.eatDelim("}")) break;
515 if (par.eatIdNum("Config")) {
516 bool broken = false;
517 string cfg = par.expectIdNum();
518 if (par.eatDelim("|")) {
519 if (cfg == "BROKEN") {
520 cfg = par.expectIdNum();
521 } else {
522 par.expectIdNum("BROKEN");
524 cfg = "BROKEN|"~cfg;
525 broken = true;
526 } else if (cfg == "BROKEN") {
527 cfg ~= "|<default>";
528 broken = true;
530 par.expectDelim(";");
531 par.expectDelim("{");
532 auto cc = new SimpleConfig(par, cfg, ignoredups);
533 cc.broken = broken;
534 if (auto ocp = cc.name in configs) {
535 if (ignoredups) {
536 writeln("WARNING: ", cc.loc, ": duplicate DIFFERENT config '"~cc.name~"' (prev vas at "~ocp.loc.toString~")");
537 } else {
538 par.errorAt(cc.loc, "duplicate config '"~cc.name~"' (prev vas at "~ocp.loc.toString~")");
541 configs[cc.name] = cc;
542 //{ import iv.vfs.io; writeln("ccend: loc=", par.loc, "; ", cc.name); }
543 } else {
544 defcfg.parseField(par, ignoredups);
549 override bool opEquals (in Object o) const {
550 if (o is null) return false;
551 if (o is this) return true;
552 if (auto op = cast(SimpleDef)o) {
553 if (name != op.name) return false;
554 bool[string] hit;
555 foreach (const SimpleConfig cfg; op.configs.byValue) {
556 if (auto mycfgp = cfg.name in configs) {
557 if (*mycfgp != cfg) return false;
558 hit[cfg.name] = true;
559 } else {
560 return false;
563 foreach (const SimpleConfig cfg; configs.byValue) {
564 if (cfg.name !in hit) return false;
566 return true;
568 return false;
571 string[] diff (in SimpleDef other) const {
572 assert(other !is null);
573 string[] res;
574 if (name != other.name) { res ~= "- "~name; res ~= "+ "~other.name; }
575 // compare configs
576 bool[string] hit;
577 foreach (const SimpleConfig mycfg; configs.byValue) {
578 if (mycfg.name !in other.configs) continue;
579 hit[mycfg.name] = true;
580 if (mycfg != other.configs[mycfg.name]) {
581 res ~= "*--* "~mycfg.name~" *--*";
582 res ~= mycfg.diff(other.configs[mycfg.name]);
585 // remove unhit configs
586 foreach (const SimpleConfig mycfg; configs.byValue) {
587 if (mycfg.name !in hit) {
588 res ~= "--- "~mycfg.name;
591 // add other unhit fields
592 foreach (const SimpleConfig itcfg; other.configs.byValue) {
593 if (itcfg.name !in configs) {
594 res ~= "+++ "~itcfg.name;
597 return res;
600 @property bool hasConfig (const(char)[] aconfig) const nothrow @safe @nogc {
601 if (aconfig.length == 0) aconfig = "<default>";
602 return ((aconfig in configs) !is null);
605 @property bool hasBrokenConfig (const(char)[] aconfig) const nothrow @safe {
606 if (aconfig.length == 0) aconfig = "<default>";
607 if (auto cp = aconfig in configs) { if (cp.broken) return true; }
608 auto bkn = "BROKEN|"~aconfig;
609 if (auto cp = bkn in configs) return cp.broken;
610 if (auto cp = "BROKEN|<default>" in configs) return cp.broken;
611 return false;
614 inout(SimpleField) opIndex (const(char)[] aconfig, const(char)[] afldname) inout nothrow @safe @nogc {
615 if (aconfig.length == 0) aconfig = "<default>";
616 if (auto cp = aconfig in configs) {
617 if (auto fld = (*cp)[afldname]) return fld;
619 // check default config
620 if (aconfig != "<default>") {
621 if (auto cp = "<default>" in configs) {
622 if (auto fld = (*cp)[afldname]) return fld;
625 return null;
630 // ////////////////////////////////////////////////////////////////////////// //
631 class SimpleDefList {
632 SimpleDef[string] defs;
633 SimpleDef maindef;
635 this () {}
637 void parse (VFile fl, bool ignoredups=false) { parse(new SimpleIvanParser(fl), ignoredups); }
639 void parse (SimpleIvanParser par, bool ignoredups=false) {
640 while (!par.eof) {
641 auto def = new SimpleDef(par, ignoredups);
642 if (auto odp = def.name in defs) {
643 if (ignoredups) {
644 writeln("WARNING: ", def.loc, ": duplicate DIFFERENT definition '"~def.name~"' (prev vas at "~odp.loc.toString~")");
645 } else {
646 par.errorAt(def.loc, "duplicate definition '"~def.name~"' (prev vas at "~odp.loc.toString~")");
649 defs[def.name] = def;
650 if (def.name == "character" || def.name == "item") {
651 def.base = true;
652 maindef = def;
657 override bool opEquals (in Object o) const {
658 if (o is null) return false;
659 if (o is this) return true;
660 if (auto op = cast(SimpleDefList)o) {
661 bool[string] hit;
662 foreach (const SimpleDef def; op.defs.byValue) {
663 if (auto mydefp = def.name in defs) {
664 if (*mydefp != def) return false;
665 hit[def.name] = true;
666 } else {
667 return false;
670 foreach (const SimpleDef def; defs.byValue) {
671 if (def.name !in hit) return false;
673 return true;
675 return false;
678 SimpleField opIndex (const(char)[] adefname, const(char)[] aconfig, const(char)[] afldname) nothrow @safe @nogc {
679 SimpleField tryfld (SimpleDef def) {
680 if (def is null) return null;
681 // try field in given config
682 auto fld = def[aconfig, afldname];
683 if (fld !is null) return fld;
684 // find field in default config
685 return def["", afldname];
687 // find field in given def
688 if (auto dp = adefname in defs) { if (auto fld = tryfld(*dp)) return fld; }
689 // find field in base def
690 return tryfld(maindef);