UNPKG

finitedomain

Version:

A fast feature rich finite domain solver

946 lines (822 loc) 23.8 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, } from './helpers'; import Solver from './solver'; // BODY_START /** * @param {string} str * @param {Solver} [solver] * @returns {Solver} */ function importer_main(str, solver, _debug) { if (!solver) solver = new Solver(); let pointer = 0; let len = str.length; while (!isEof()) parseStatement(); return solver; function read() { return str[pointer]; } function skip() { ++pointer; } function is(c, desc) { if (read() !== c) THROW('Expected ' + (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) { // no, I don't feel bad for also flaggin a comment as eol :) 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 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 name = parseIdentifier(); skipWhitespaces(); let domain = parseDomain(); skipWhitespaces(); let alts; while (str.slice(pointer, pointer + 6) === 'alias(') { if (!alts) alts = []; alts.push(parseAlias(pointer += 6)); skipWhitespaces(); } let mod = parseModifier(); expectEol(); solver.decl(name, domain, mod, true); // TODO: properly map the alts to the same var index... if (alts) alts.map(name => solver.decl(name, domain, mod, true)); } function parseIdentifier() { if (read() === '\'') return parseQuotedIdentifier(); else return parseUnquotedIdentifier(); } function parseQuotedIdentifier() { is('\''); let start = pointer; while (!isEof() && read() !== '\'') skip(); if (isEof()) THROW('Quoted identifier must be closed'); if (start === pointer) THROW('Expected to parse identifier, found none'); skip(); // quote 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 (isEof()) THROW('Quoted identifier must be closed'); if (start === pointer) THROW('Expected to parse identifier, found none'); return str.slice(start, pointer); } function isValidUnquotedIdentChar(c) { switch (c) { case '(': case ')': case ',': case '[': case ']': case '\'': case '#': return false; } if (isWhite(c)) return false; return true; } function parseAlias() { skipWhitespaces(); let start = pointer; while (true) { let c = read(); if (c === ')') break; if (isNewline(c)) THROW('Alias must be closed with a `)` but wasnt (eol)'); if (isEof()) THROW('Alias must be closed with a `)` but wasnt (eof)'); skip(); } let alias = str.slice(start, pointer); if (!alias) THROW('The alias() can not be empty but was'); skipWhitespaces(); is(')', '`alias` to be closed by `)`'); return alias; } function parseDomain() { // [] // [lo hi] // [[lo hi] [lo hi] ..] // * // 25 // (comma's optional and ignored) let c = read(); if (c === '=') { skip(); skipWhitespaces(); 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'); 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 'minMaxCycle': case 'naive': case 'splitMax': case 'splitMin': break; default: THROW('Expecting a strategy name after the `@` modifier (`' + 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 '|': // 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 '^': // 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 '!&': // 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 '!^': // 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; default: if (cop) THROW('Unknown constraint op: [' + cop + ']'); } expectEol(); } function parseAssignment(C) { // note: if Solver 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 Solver) 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)) return A; // any group without "top-level" op (`A=(B+C)`), or sum() etc 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 '+': return solver.plus(A, parseVexpr(), C); case '-': return solver.minus(A, parseVexpr(), C); case '*': return solver.times(A, parseVexpr(), C); case '/': return solver.div(A, parseVexpr(), C); default: if (rop !== undefined) THROW('Unknown rop: `' + 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 '!^'; } 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; default: 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(); is('=', 'middle part of !=? op'); is('?', 'reifier suffix'); return '!=?'; 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 '+': case '-': case '*': case '/': skip(); return a; default: THROW('Wanted to parse a rop but next char is `' + 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 + 9) === 'distinct(') parseDistinct(); else if (str.slice(pointer, pointer + 5) === 'nall(') parseNall(); else return false; return true; } function parseDistinct() { pointer += 9; skipWhitespaces(); let vals = parseVexpList(); solver.distinct(vals); skipWhitespaces(); is(')', 'distinct call closer'); expectEol(); } 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(); if (read() === '(') { if (ident === 'sum') { v = parseSum(resultVar); } else if (ident === 'product') { v = parseProduct(resultVar); } else if (ident === 'all?') { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsAll(resultVar); } else if (ident === 'nall?') { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsNall(resultVar); } else if (ident === 'none?') { if (freshVar) solver.decl(resultVar, [0, 1], undefined, false, true); v = parseIsNone(resultVar); } else { THROW('Unknown 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 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(); // R = all?(A B C ...) -> X = A * B * C * ..., R = X !=? 0 let x = solver.decl(); // anon var [sub,sup] solver.product(refs, x); let r = solver.isNeq(x, solver.num(0), result); skipWhitespaces(); is(')', 'isall closer'); return r; } function parseIsNall(result) { is('(', 'isnall call opener'); skipWhitespaces(); let refs = parseVexpList(); // R = nall?(A B C ...) -> X = A * B * C * ..., R = X ==? 0 let x = solver.decl(); // anon var [sub,sup] solver.product(refs, x); let r = solver.isEq(x, solver.num(0), result); skipWhitespaces(); is(')', 'isnall closer'); return r; } function parseIsNone(result) { is('(', 'isnone call opener'); skipWhitespaces(); let refs = parseVexpList(); // R = none?(A B C ...) -> X = sum(A * B * C * ...), R = X ==? 0 let x = solver.decl(); // anon var [sub,sup] solver.sum(refs, x); let r = solver.isEq(x, solver.num(0), result); skipWhitespaces(); is(')', 'isnone closer'); return r; } 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 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 (read() === ')') break; if (read() === ',') { skip(); skipWhitespaces(); } 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(); } 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; default: THROW('Unsupported custom rule: ' + ident); } } else if (str.slice(pointer, pointer + 4) === 'mode') { pointer += 4; parseMode(); } else { THROW('Unknown atrule'); } expectEol(); } function parseVarStrat() { let json = readLine(); expectEol(); solver.varStratConfig = JSON.parse(json); } function parseValStrat() { let name = parseIdentifier(); expectEol(); solver.valueStratName = name; } function parseRestCustom() { skipWhitespaces(); if (read() === '=') { skip(); skipWhitespaces(); } return readLine(); } function parseTargets() { skipWhitespaces(); if (read() === '=') { skip(); skipWhitespaces(); } if (str.slice(pointer, pointer + 3) === 'all') { pointer += 3; solver.config.targetedVars = 'all'; } else { is('('); let idents = parseIdentList(); if (idents.length) solver.config.targetedVars = idents; is(')'); } expectEol(); } function parseMode() { skipWhitespaces(); if (read() === '=') { skip(); skipWhitespaces(); } if (str.slice(pointer, pointer + 'constraints'.length) === 'constraints') { // input consists of high level constraints. generate low level optimizations. pointer += 'constraints'.length; } else if (str.slice(pointer, pointer + 'propagators'.length) === 'propagators') { // input consists of low level constraints. try not to generate more. pointer += 'propagators'.length; } } function THROW(msg) { if (_debug) { console.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 - 20), pointer) + '#|#' + str.slice(pointer, Math.min(str.length, pointer + 20)) + '`'; throw new Error(msg); } } // BODY_STOP export default importer_main;