UNPKG

fdo

Version:

A brute force Finite Domain Solver

1,219 lines (1,069 loc) 33.4 kB
// this is an import function for config // it converts a DSL string to a $config // see /docs/dsl.txt for syntax // see exporter.js to convert a config to this DSL import { SUB, SUP, getTerm, } from '../../fdlib/src/helpers'; import { config_setOption, } from './config'; import FDO from './fdo'; // BODY_START /** * @param {string} str * @param {FDO} [solver] * @param {boolean} [_debug] Log out entire input with error token on fail? * @returns {FDO} */ function importer_main(str, solver, _debug) { if (!solver) solver = new FDO(); let pointer = 0; let len = str.length; while (!isEof()) parseStatement(); return solver; function read() { return str[pointer]; } function readD(delta) { return str[pointer + delta]; } function skip() { ++pointer; } function is(c, desc) { if (read() !== c) THROW('Expected ' + (desc ? desc + ' ' : '') + '`' + c + '`, found `' + read() + '`'); skip(); } function skipWhitespaces() { while (pointer < len && isWhitespace(read())) skip(); } function skipWhites() { while (!isEof()) { let c = read(); if (isWhite(c)) { skip(); } else if (isComment(c)) { skipComment(); } else { break; } } } function isWhitespace(s) { return s === ' ' || s === '\t'; } function isNewline(s) { return s === '\n' || s === '\r'; } function isComment(s) { return s === '#'; } function isWhite(s) { return isWhitespace(s) || isNewline(s); } function expectEol() { skipWhitespaces(); if (pointer < len) { let c = read(); if (c === '#') { skipComment(); } else if (isNewline(c)) { skip(); } else { THROW('Expected EOL but got `' + read() + '`'); } } } function atEol() { if (pointer >= len) return true; let c = read(); return c === '#' || isNewline(c); } function isEof() { return pointer >= len; } function parseStatement() { // either: // - start with colon: var decl // - start with hash: line comment // - empty: empty // - otherwise: constraint skipWhites(); switch (read()) { case ':': return parseVar(); case '#': return skipComment(); case '@': return parseAtRule(); default: if (!isEof()) return parseUndefConstraint(); } } function parseVar() { skip(); // is(':') skipWhitespaces(); let nameNames = parseIdentifier(); skipWhitespaces(); if (read() === ',') { nameNames = [nameNames]; do { skip(); skipWhitespaces(); nameNames.push(parseIdentifier()); skipWhitespaces(); } while (!isEof() && read() === ','); } if (read() === '=') { skip(); skipWhitespaces(); } let domain = parseDomain(); skipWhitespaces(); let mod = parseModifier(); expectEol(); if (typeof nameNames === 'string') { solver.decl(nameNames, domain, mod, true); } else { nameNames.forEach(name => solver.decl(name, domain, mod, true)); } } function parseIdentifier() { if (read() === '\'') return parseQuotedIdentifier(); else return parseUnquotedIdentifier(); } function parseQuotedIdentifier() { is('\'', 'start of Quoted identifier'); let start = pointer; let c = read(); while (!isEof() && !isNewline(c) && c !== '\'') { skip(); c = read(); } if (isEof()) THROW('Quoted identifier must be closed'); if (start === pointer) THROW('Expected to parse identifier, found none'); is('\'', 'end of Quoted identifier'); return str.slice(start, pointer - 1); // return unquoted ident } function parseUnquotedIdentifier() { // anything terminated by whitespace let start = pointer; if (read() >= '0' && read() <= '9') THROW('Unquoted ident cant start with number'); while (!isEof() && isValidUnquotedIdentChar(read())) skip(); if (start === pointer) THROW('Expected to parse identifier, found none [' + read() + ']'); return str.slice(start, pointer); } function isValidUnquotedIdentChar(c) { // meh. i syntactically dont care about unicode chars so if you want to use them i wont stop you here return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_' || c === '$' || c === '-' || c > '~'); } function parseDomain() { // [] // [lo hi] // [[lo hi] [lo hi] ..] // * // 25 // (comma's optional and ignored) let c = read(); let domain; switch (c) { case '[': is('[', 'domain start'); skipWhitespaces(); domain = []; if (read() === '[') { do { skip(); skipWhitespaces(); let lo = parseNumber(); skipWhitespaces(); if (read() === ',') { skip(); skipWhitespaces(); } let hi = parseNumber(); skipWhitespaces(); is(']', 'range-end'); skipWhitespaces(); domain.push(lo, hi); if (read() === ',') { skip(); skipWhitespaces(); } } while (read() === '['); } else if (read() !== ']') { do { skipWhitespaces(); let lo = parseNumber(); skipWhitespaces(); if (read() === ',') { skip(); skipWhitespaces(); } let hi = parseNumber(); skipWhitespaces(); domain.push(lo, hi); if (read() === ',') { skip(); skipWhitespaces(); } } while (read() !== ']'); } is(']', 'domain-end'); if (domain.length === 0) THROW('Empty domain [] in dsl, this problem will always reject'); return domain; case '*': skip(); return [SUB, SUP]; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': let v = parseNumber(); skipWhitespaces(); return [v, v]; } THROW('Expecting valid domain start, found `' + c + '`'); } function parseModifier() { if (read() !== '@') return; skip(); let mod = {}; let start = pointer; while (read() >= 'a' && read() <= 'z') skip(); let stratName = str.slice(start, pointer); switch (stratName) { case 'list': parseList(mod); break; case 'markov': parseMarkov(mod); break; case 'max': case 'mid': case 'min': case 'naive': break; case 'minMaxCycle': case 'splitMax': case 'splitMin': default: THROW('implement me (var mod) [`' + stratName + '`]'); } mod.valtype = stratName; return mod; } function parseList(mod) { skipWhitespaces(); if (str.slice(pointer, pointer + 5) !== 'prio(') THROW('Expecting the priorities to follow the `@list`'); pointer += 5; mod.list = parseNumList(); is(')', 'list end'); } function parseMarkov(mod) { while (true) { skipWhitespaces(); if (str.slice(pointer, pointer + 7) === 'matrix(') { // TOFIX: there is no validation here. apply stricter and safe matrix parsing let matrix = str.slice(pointer + 7, pointer = str.indexOf(')', pointer)); let code = 'return ' + matrix; let func = Function(code); /* eslint no-new-func: "off" */ mod.matrix = func(); if (pointer === -1) THROW('The matrix must be closed by a `)` but did not find any'); } else if (str.slice(pointer, pointer + 7) === 'legend(') { pointer += 7; mod.legend = parseNumList(); skipWhitespaces(); is(')', 'legend closer'); } else if (str.slice(pointer, pointer + 7) === 'expand(') { pointer += 7; mod.expandVectorsWith = parseNumber(); skipWhitespaces(); is(')', 'expand closer'); } else { break; } skip(); } } function skipComment() { is('#', 'comment start'); //is('#', 'comment hash'); while (!isEof() && !isNewline(read())) skip(); if (!isEof()) skip(); } function parseUndefConstraint() { // parse a constraint that does not return a value itself // first try to parse single value constraints without value like markov() and distinct() if (parseUexpr()) return; // so the first value must be a value returning expr let A = parseVexpr(); // returns a var name or a constant value skipWhitespaces(); let cop = parseCop(); skipWhitespaces(); switch (cop) { case '=': parseAssignment(A); break; case '==': solver.eq(A, parseVexpr()); break; case '!=': solver.neq(A, parseVexpr()); break; case '<': solver.lt(A, parseVexpr()); break; case '<=': solver.lte(A, parseVexpr()); break; case '>': solver.gt(A, parseVexpr()); break; case '>=': solver.gte(A, parseVexpr()); break; case '&': // force A and B to non-zero (artifact) // (could easily be done at compile time) // for now we mul the args and force the result non-zero, this way neither arg can be zero // TODO: this could be made "safer" with more work; `(A/A)+(B/B) > 0` doesnt risk going oob, i think. and otherwise we could sum two ==?0 reifiers to equal 2. just relatively very expensive. solver.neq(solver.mul(A, parseVexpr()), solver.num(0)); break; case '!&': // nand is a nall with just two args... // it is the opposite from AND, and so is the implementation // (except since we can force to 0 instead of "nonzero" we can drop the eq wrapper) solver.mul(A, parseVexpr(), solver.num(0)); break; case '|': // force at least one of A and B to be non-zero (both is fine too) // if we add both args and check the result for non-zero then at least one arg must be non-zero solver.neq(solver.plus(A, parseVexpr()), solver.num(0)); break; case '!|': // unconditionally force A and B to zero solver.eq(A, solver.num(0)); solver.eq(parseVexpr(), solver.num(0)); break; case '^': // force A zero and B nonzero or A nonzero and B zero (anything else rejects) // this is more tricky/expensive to implement than AND and OR... // x=A+B,x==A^x==B owait // (A==?0)+(B==?0)==1 solver.eq(solver.plus(solver.isEq(A, 0), solver.isEq(parseVexpr(), 0)), 1); break; case '!^': // xor means A and B both solve to zero or both to non-zero // (A==?0)==(B==?0) solver.eq(solver.isEq(A, solver.num(0)), solver.isEq(parseVexpr(), solver.num(0))); break; case '->': // I think this could be implemented in various ways // A -> B => ((A !=? 0) <= (B !=? 0)) & ((B ==? 0) <= (A ==? 0)) // (if A is nonzero then B must be nonzero, otherwise B can be anything. But also if B is zero then // A must be zero and otherwise A can be anything. They must both hold to simulate an implication.) let B = parseVexpr(); // (A !=? 0) <= (B !=? 0)) solver.lte(solver.isNeq(A, solver.num(0)), solver.isNeq(B, solver.num(0))); // (B ==? 0) <= (A ==? 0) solver.lte(solver.isEq(B, solver.num(0)), solver.isEq(A, solver.num(0))); break; case '!->': // force A to nonzero and B to zero solver.gt(A, solver.num(0)); solver.eq(parseVexpr(), solver.num(0)); break; default: if (cop) THROW('Unknown cop that starts with: [' + cop + ']'); } expectEol(); } function parseAssignment(C) { // note: if FDO api changes this may return the wrong value... // it should always return the "result var" var name or constant // (that would be C, but C may be undefined here and created by FDO) let freshVar = typeof C === 'string' && !solver.hasVar(C); if (freshVar) C = solver.decl(C); let A = parseVexpr(C, freshVar); skipWhitespaces(); let c = read(); if (isEof() || isNewline(c) || isComment(c)) { // any group without "top-level" op (`A=(B+C)`), or sum() etc // but also something like `x = 5` (which we cant detect here) // so just to make sure those cases dont fall through add an // extra eq. this should resolve immediately without change to // cases like `x = sum()` solver.eq(A, C); return A; } return parseAssignRest(A, C, freshVar); } function parseAssignRest(A, C, freshVar) { let rop = parseRop(); skipWhitespaces(); switch (rop) { case '==?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isEq(A, parseVexpr(), C); case '!=?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isNeq(A, parseVexpr(), C); case '<?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isLt(A, parseVexpr(), C); case '<=?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isLte(A, parseVexpr(), C); case '>?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isGt(A, parseVexpr(), C); case '>=?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return solver.isGte(A, parseVexpr(), C); case '|?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return compileIssome(C, [A, parseVexpr()]); case '!|?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return compileIsnone(C, [A, parseVexpr()]); case '&?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return compileIsall(C, [A, parseVexpr()]); case '!&?': if (freshVar) solver.decl(C, [0, 1], undefined, false, true); return compileIsnall(C, [A, parseVexpr()]); case '+': return solver.plus(A, parseVexpr(), C); case '-': return solver.minus(A, parseVexpr(), C); case '*': return solver.mul(A, parseVexpr(), C); case '/': return solver.div(A, parseVexpr(), C); default: if (rop !== undefined) THROW('Expecting right paren or rop, got: `' + rop + '`'); return A; } } function parseCop() { let c = read(); switch (c) { case '=': skip(); if (read() === '=') { skip(); return '=='; } return '='; case '!': skip(); c = read(); if (c === '=') { skip(); return '!='; } if (c === '&') { skip(); return '!&'; } if (c === '^') { skip(); return '!^'; } if (c === '|') { skip(); return '!|'; } if (c === '-' && readD(1) === '>') { skip(); skip(); return '!->'; } return '!'; case '<': skip(); if (read() === '=') { skip(); return '<='; } return '<'; case '>': skip(); if (read() === '=') { skip(); return '>='; } return '>'; case '&': case '|': case '^': skip(); return c; case '#': THROW('Expected to parse a cop but found a comment instead'); break; case '-': if (readD(1) === '>') { skip(); skip(); return '->'; } break; } if (isEof()) THROW('Expected to parse a cop but reached eof instead'); THROW('Unknown cop char: `' + c + '`'); } function parseRop() { let a = read(); switch (a) { case '=': skip(); let b = read(); if (b === '=') { skip(); is('?', 'reifier suffix'); return '==?'; } else { return '='; } case '!': skip(); if (read() === '=') { is('=', 'middle part of !=? op'); is('?', 'reifier suffix'); return '!=?'; } else if (read() === '|') { is('|', 'middle part of !|? op'); is('?', 'reifier suffix'); return '!|?'; } else if (read() === '&') { is('&', 'middle part of !&? op'); is('?', 'reifier suffix'); return '!&?'; } else { THROW('invalid rop char after ! [' + read() + ']'); break; } case '<': skip(); if (read() === '=') { skip(); is('?', 'reifier suffix'); return '<=?'; } else { is('?', 'reifier suffix'); return '<?'; } case '>': skip(); if (read() === '=') { skip(); is('?', 'reifier suffix'); return '>=?'; } else { is('?', 'reifier suffix'); return '>?'; } case '|': skip(); is('?', 'reifier suffix'); return '|?'; case '&': skip(); is('?', 'reifier suffix'); return '&?'; case '+': case '-': case '*': case '/': skip(); return a; default: THROW('Expecting right paren or rop, got: `' + a + '`'); } } function parseUexpr() { // it's not very efficient (we could parse an ident before and check that result here) but it'll work for now if (str.slice(pointer, pointer + 4) === 'all(') parseAll(); else if (str.slice(pointer, pointer + 9) === 'distinct(') parseDistinct(9); else if (str.slice(pointer, pointer + 5) === 'diff(') parseDistinct(5); else if (str.slice(pointer, pointer + 5) === 'nall(') parseNall(); else if (str.slice(pointer, pointer + 5) === 'none(') parseNone(); else if (str.slice(pointer, pointer + 5) === 'same(') parseSame(); else if (str.slice(pointer, pointer + 5) === 'some(') parseSome(); else if (str.slice(pointer, pointer + 5) === 'xnor(') parseXnor(); else return false; return true; } function parseVexpList() { let list = []; skipWhitespaces(); while (!isEof() && read() !== ')') { let v = parseVexpr(); list.push(v); skipWhitespaces(); if (read() === ',') { skip(); skipWhitespaces(); } } return list; } function parseVexpr(resultVar, freshVar) { // valcall, ident, number, group let c = read(); let v; if (c === '(') v = parseGrouping(); else if (c === '[') { let d = parseDomain(); if (d[0] === d[1] && d.length === 2) v = d[0]; else v = solver.decl(undefined, d); } else if (c >= '0' && c <= '9') { v = parseNumber(); } else { let ident = parseIdentifier(); let d = read(); if (ident === 'sum' && d === '(') { v = parseSum(resultVar); } else if (ident === 'product' && d === '(') { v = parseProduct(resultVar); } else if (ident === 'all' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsAll(resultVar); } else if (ident === 'diff' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsDiff(resultVar); } else if (ident === 'nall' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsNall(resultVar); } else if (ident === 'none' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsNone(resultVar); } else if (ident === 'same' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsSame(resultVar); } else if (ident === 'some' && d === '?' && (skip(), read() === '(')) { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsSome(resultVar); } else if (d === '?') { THROW('Unknown reifier constraint func: [' + ident + ']'); } else { v = ident; } } return v; } function parseGrouping() { is('(', 'group open'); skipWhitespaces(); let A = parseVexpr(); skipWhitespaces(); if (read() === '=') { if (read() !== '=') { parseAssignment(A); skipWhitespaces(); is(')', 'group closer'); return A; } } if (read() === ')') { // just wrapping a vexpr is okay skip(); return A; } let C = parseAssignRest(A); skipWhitespaces(); is(')', 'group closer'); return C; } function parseNumber() { let start = pointer; while (read() >= '0' && read() <= '9') skip(); if (start === pointer) { THROW('Expecting to parse a number but did not find any digits [' + start + ',' + pointer + '][' + read() + ']'); } return parseInt(str.slice(start, pointer), 10); } function parseAll() { pointer += 4; skipWhitespaces(); let refs = parseVexpList(); // R can only be 0 if (at least) one of the args is zero. so by removing // 0 from R's domain we require all args nonzero. cheap hack. let r = solver.product(refs, solver.decl(undefined, [1, SUP])); skipWhitespaces(); is(')', 'ALL closer'); return r; } function parseDistinct(delta) { pointer += delta; skipWhitespaces(); let vals = parseVexpList(); if (!vals.length) THROW('Expecting at least one expression'); solver.distinct(vals); skipWhitespaces(); is(')', 'distinct call closer'); expectEol(); } function parseSum(result) { is('(', 'sum call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = solver.sum(refs, result); skipWhitespaces(); is(')', 'sum closer'); return r; } function parseProduct(result) { is('(', 'product call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = solver.product(refs, result); skipWhitespaces(); is(')', 'product closer'); return r; } function parseIsAll(result) { is('(', 'isall call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = compileIsall(result, refs); skipWhitespaces(); is(')', 'isall closer'); return r; } function compileIsall(result, args) { // R = all?(A B C ...) -> X = A * B * C * ..., R = X !=? 0 let x = solver.decl(); // anon var [sub,sup] solver.product(args, x); return solver.isNeq(x, solver.num(0), result); } function parseIsDiff(result) { is('(', 'isdiff call opener'); skipWhitespaces(); let refs = parseVexpList(); // R = diff?(A B C ...) // => // x e args, y e args, x!=y // => // Rxy = dom(x) !=? dom(y) // c = sum(Rxy ...) // R = c ==? argCount let reifs = []; for (let i = 0; i < refs.length; ++i) { let indexA = refs[i]; for (let j = i + 1; j < refs.length; ++j) { let indexB = refs[j]; reifs.push(solver.isNeq(indexA, indexB)); } } solver.isEq(solver.sum(reifs), solver.num(reifs.length), result); skipWhitespaces(); is(')', 'isdiff closer'); return result; } function parseIsNall(result) { is('(', 'isnall call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = compileIsnall(result, refs); skipWhitespaces(); is(')', 'isnall closer'); return r; } function compileIsnall(result, args) { // R = nall?(A B C ...) -> X = A * B * C * ..., R = X ==? 0 let x = solver.decl(); // anon var [sub,sup] solver.product(args, x); return solver.isEq(x, solver.num(0), result); } function parseIsNone(result) { is('(', 'isnone call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = compileIsnone(result, refs); skipWhitespaces(); is(')', 'isnone closer'); return r; } function compileIsnone(result, args) { // R = none?(A B C ...) -> X = sum(A B C ...), R = X ==? 0 let x = solver.decl(); // anon var [sub,sup] solver.sum(args, x); return solver.isEq(x, solver.num(0), result); } function parseIsSame(result) { is('(', 'issame call opener'); skipWhitespaces(); let refs = parseVexpList(); // R = same?(A B C ...) -> A==?B,B==?C,C==?..., sum(reifs) === reifs.length let reifs = []; for (let i = 1; i < refs.length; ++i) { let r = solver.decl(undefined, [0, 1]); solver.isEq(refs[i - 1], refs[i], r); reifs.push(r); } let x = solver.decl(); // anon var [sub,sup] solver.sum(reifs, x); let r = solver.isEq(x, solver.num(reifs.length), result); skipWhitespaces(); is(')', 'issame closer'); return r; } function parseIsSome(result) { is('(', 'issome call opener'); skipWhitespaces(); let refs = parseVexpList(); let r = compileIssome(result, refs); skipWhitespaces(); is(')', 'issome closer'); return r; } function compileIssome(result, args) { // R = some?(A B C ...) -> X = sum(A B C ...), R = X !=? 0 let x = solver.decl(); // anon var [sub,sup] solver.sum(args, x); return solver.isNeq(x, solver.num(0), result); } function parseNall() { pointer += 5; skipWhitespaces(); let refs = parseVexpList(); // TODO: could also sum reifiers but i think this is way more efficient. for the time being. solver.product(refs, solver.num(0)); skipWhitespaces(); is(')', 'nall closer'); expectEol(); } function parseNone() { pointer += 5; skipWhitespaces(); let refs = parseVexpList(); solver.sum(refs, solver.num(0)); // lazy way out but should resolve immediately anyways skipWhitespaces(); is(')', 'none closer'); expectEol(); } function parseSame() { pointer += 5; skipWhitespaces(); let refs = parseVexpList(); for (let i = 1; i < refs.length; ++i) { solver.eq(refs[i - 1], refs[i]); } skipWhitespaces(); is(')', 'same closer'); expectEol(); } function parseSome() { pointer += 5; skipWhitespaces(); let refs = parseVexpList(); solver.sum(refs, solver.decl(undefined, [1, SUP])); skipWhitespaces(); is(')', 'some closer'); expectEol(); } function parseXnor() { pointer += 5; skipWhitespaces(); let refs = parseVexpList(); skipWhitespaces(); is(')', 'xnor() closer'); expectEol(); // xnor(A B C) // => // x=X+B+C (if x is 0, all the args were zero: "none") // y=X*B*C (if y is not 0, none of the args were zero: "all") // (x==0) + (y!=0) == 1 (must all be zero or all be nonzero) let x = solver.decl(); // anon var [sub,sup] let y = solver.decl(); // anon var [sub,sup] solver.sum(refs, x); solver.product(refs, y); solver.plus(solver.isEq(x, 0), solver.isNeq(y, 0), 1); } function parseNumstr() { let start = pointer; while (read() >= '0' && read() <= '9') skip(); return str.slice(start, pointer); } function parseNumList() { let nums = []; skipWhitespaces(); let numstr = parseNumstr(); while (numstr) { nums.push(parseInt(numstr, 10)); skipWhitespaces(); if (read() === ',') { ++pointer; skipWhitespaces(); } numstr = parseNumstr(); } if (!nums.length) THROW('Expected to parse a list of at least some numbers but found none'); return nums; } function parseIdentList() { let idents = []; while (true) { skipWhitespaces(); if (atEol()) THROW('Missing target char at eol/eof'); if (read() === ')') break; if (read() === ',') { skip(); skipWhitespaces(); if (atEol()) THROW('Trailing comma not supported'); } if (read() === ',') THROW('Double comma not supported'); let ident = parseIdentifier(); idents.push(ident); } if (!idents.length) THROW('Expected to parse a list of at least some identifiers but found none'); return idents; } function readLine() { let line = ''; while (!isEof() && !isNewline(read())) { line += read(); skip(); } return line; } function parseAtRule() { is('@'); // mostly temporary hacks while the dsl stabilizes... if (str.slice(pointer, pointer + 6) === 'custom') { pointer += 6; skipWhitespaces(); let ident = parseIdentifier(); skipWhitespaces(); if (read() === '=') { skip(); skipWhitespaces(); if (read() === '=') THROW('Unexpected double eq sign'); } switch (ident) { case 'var-strat': parseVarStrat(); break; case 'val-strat': parseValStrat(); break; case 'set-valdist': skipWhitespaces(); let target = parseIdentifier(); let config = parseRestCustom(); solver.setValueDistributionFor(target, JSON.parse(config)); break; case 'targets': parseTargets(); break; case 'nobool': case 'noleaf': case 'free': skipWhitespaces(); if (read() === ',') THROW('Leading comma not supported'); if (atEol()) THROW('Expected to parse some var values'); // ignore. it's a presolver debug tool readLine(); break; default: THROW('Unsupported custom rule: ' + ident); } } else { THROW('Unknown atrule'); } expectEol(); } function parseVarStrat() { // @custom var-strat [fallback] [=] naive // @custom var-strat [fallback] [=] size // @custom var-strat [fallback] [=] min // @custom var-strat [fallback] [=] max // @custom var-strat [fallback] [=] throw // @custom var-strat [fallback] [inverted] [list] (a b c) skipWhitespaces(); let fallback = false; if (read() === 'f') { // inverted let ident = parseIdentifier(); if (ident !== 'fallback') THROW('Expecting `fallback` here'); fallback = true; skipWhitespaces(); } let inverted = false; if (read() === 'i') { // inverted let ident = parseIdentifier(); if (ident !== 'inverted') THROW('Expecting `inverted` here'); inverted = true; skipWhitespaces(); } if (read() === 'l' || read() === '(') { if (read() === 'l') { // list (optional keyword) if (parseIdentifier() !== 'list') THROW('Unexpected ident after `inverted` (only expecting `list` or the list)'); skipWhitespaces(); } is('('); let priorityByName = parseIdentList(); if (priorityByName.length) config_setOption(solver.config, fallback ? 'varStrategyFallback' : 'varStrategy', {type: 'list', inverted, priorityByName}); else config_setOption(solver.config, fallback ? 'varStrategyFallback' : 'varStrategy', {type: 'naive'}); skipWhitespaces(); is(')'); } else { if (read() === '=') { skip(); skipWhitespaces(); } if (inverted) THROW('The `inverted` keyword is only valid for a prio list'); // parse ident and use that as the vardist let ident = parseIdentifier(); if (ident === 'list') THROW('Use a grouped list of idents for vardist=list'); if (ident !== 'naive' && ident !== 'size' && ident !== 'min' && ident !== 'max' && ident !== 'throw') THROW('Unknown var dist [' + ident + ']'); config_setOption(solver.config, fallback ? 'varStrategyFallback' : 'varStrategy', {type: ident}); } } function parseValStrat() { let name = parseIdentifier(); expectEol(); solver.config.valueStratName = name; } function parseRestCustom() { skipWhitespaces(); if (read() === '=') { skip(); skipWhitespaces(); } return readLine(); } function parseTargets() { skipWhitespaces(); if (str.slice(pointer, pointer + 3) === 'all') { pointer += 3; solver.config.targetedVars = 'all'; } else { is('(', 'ONLY_USE_WITH_SOME_TARGET_VARS'); skipWhitespaces(); if (read() === ',') THROW('Leading comma not supported'); let idents = parseIdentList(); if (idents.length) solver.config.targetedVars = idents; is(')'); } } function THROW(msg) { if (_debug) { getTerm().log(str.slice(0, pointer) + '##|PARSER_IS_HERE[' + msg + ']|##' + str.slice(pointer)); } msg = 'Importer parser error: ' + msg + ', source at #|#: `' + str.slice(Math.max(0, pointer - 70), pointer) + '#|#' + str.slice(pointer, Math.min(str.length, pointer + 70)) + '`'; throw new Error(msg); } } // BODY_STOP export default importer_main;