UNPKG

fdp

Version:

Finite Domain Problem reduction system

1,635 lines (1,415 loc) 45.1 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, ASSERT, getTerm, TRACE, } from '../../fdlib/src/helpers'; import { domain_createRange, } from '../../fdlib/src/domain'; import { ML_ALL, ML_NOBOOL, ML_NOLEAF, ML_DIFF, ML_DIV, ML_IMP, ML_ISALL, ML_ISDIFF, ML_ISLT, ML_ISLTE, ML_ISNALL, ML_ISNONE, ML_ISSAME, ML_ISSOME, ML_JMP, ML_JMP32, ML_LT, ML_LTE, ML_MINUS, ML_NALL, ML_NIMP, ML_NONE, ML_NOOP, ML_NOOP2, ML_NOOP3, ML_NOOP4, ML_PRODUCT, ML_SAME, ML_SOME, ML_START, ML_STOP, ML_SUM, ML_XNOR, ML_XOR, SIZEOF_V, SIZEOF_W, } from './ml'; // BODY_START // ords (number matching is faster, especially since we use a buffer anyways) const $$AND = 38; const $$AT = 64; const $$BANG = 33; const $$COLON = 58; const $$COMMA = 44; const $$CR = 10; const $$LF = 13; const $$DASH = 45; const $$DIV = 47; const $$EQ = 61; const $$GT = 62; const $$HASH = 35; const $$LEFTBRACK = 91; const $$LEFTPAREN = 40; const $$LT = 60; const $$OR = 124; const $$PLUS = 43; const $$QM = 63; const $$SPACE = 32; const $$RIGHTBRACK = 93; const $$RIGHTPAREN = 41; const $$SQUOTE = 39; const $$STAR = 42; const $$TAB = 9; const $$XOR = 94; const $$0 = 48; const $$1 = 49; const $$2 = 50; const $$3 = 51; const $$4 = 52; const $$5 = 53; const $$6 = 54; const $$7 = 55; const $$8 = 56; const $$9 = 57; const $$a = 97; const $$c = 99; const $$d = 100; const $$e = 101; const $$f = 102; const $$g = 103; const $$i = 105; const $$l = 108; const $$m = 109; const $$n = 110; const $$o = 111; const $$p = 112; const $$r = 114; const $$s = 115; const $$t = 116; const $$x = 120; const $$z = 122; const $$A = 65; const $$Z = 98; /** * Compile the constraint dsl to a bytecode * * @param {string} dslStr * @param {Object} problem * @param {boolean} [_debug] Improved error reporting when true * @returns {string} */ function dsl2ml(dslStr, problem, _debug) { TRACE('# dsl2ml:', [dslStr.slice(0, 100).replace(/ +/g, ' ') + (dslStr.replace(/ +/g, ' ').length > 100 ? '...' : '')]); problem.input.varstrat = 'default'; problem.input.valstrat = 'default'; problem.input.dsl = dslStr; let { addVar, setDomain, name2index, } = problem; let constraints = 0; let freeDirective = -1; // for `@custom free x`. this var tries to ensure exactly x bytes are "free" let dslPointer = 0; let dslBuf; if (typeof Buffer === 'undefined') { dslBuf = new window.TextEncoder('utf-8').encode(dslStr); } else { dslBuf = new Uint8Array(Buffer.from(dslStr, 'binary')); } ASSERT(dslBuf instanceof Uint8Array); let len = dslBuf.length; let mlBufSize = Math.ceil(dslBuf.length / 5); // 20% is arbitrary choice. grown dynamically when needed let mlBuffer = new Uint8Array(mlBufSize).fill(0); let mlPointer = 0; // this is for a hack let lastAssignmentIndex = -1; let lastUnknownIndex = -1; encode8bit(ML_START); while (!isEof()) parseStatement(); if (freeDirective > 0) { // compile a jump of given size. this will be considered available space TRACE('forcing', freeDirective, 'bytes of available space'); compileJump(freeDirective); } encode8bit(ML_STOP); // this step will be undone but serves to ensure the buffer isnt grown in the actual compilation step (which happens after the available-space-checks) --mlPointer; if (freeDirective < 0) { // compile a jump for the remainder of the space, if any, which could be used by the recycle mechanisms // only do this here when the free directive is absent let leftFree = (mlBufSize - mlPointer) - 1; // STOP will occupy 1 byte TRACE('space available', leftFree, 'bytes'); if (leftFree > 0) compileJump(leftFree); } encode8bit(ML_STOP); // put the STOP at the end // if there is now still space left, we need to crop it because freeDirective was set and didnt consume it all if (mlBufSize - mlPointer) { TRACE('cropping excess available space', mlBufSize, mlPointer, mlBufSize - mlPointer); // if the free directive was given, remove any excess free space // note that one more byte needs to be compiled after this (ML_STOP) mlBuffer = mlBuffer.slice(0, mlPointer); } ASSERT(mlPointer === mlBuffer.length, 'mlPointer should now be at the first unavailable cell of the buffer', mlPointer, mlBuffer.length, mlBuffer); problem.ml = mlBuffer; if (!problem.input.targets) problem.input.targets = 'all'; getTerm().log('# dsl2ml: parsed', constraints, 'constraints and', problem.domains.length, 'vars'); return; // ######################################################################## function startConstraint(op) { ++constraints; encode8bit(op); } function encode8bit(num) { ASSERT(typeof num === 'number' && num >= 0 && num <= 0xff, 'OOB number'); TRACE('encode8bit:', num, 'dsl pointer:', dslPointer, ', ml pointer:', mlPointer); if (mlPointer >= mlBufSize) grow(); mlBuffer[mlPointer++] = num; } function encode16bit(num) { TRACE('encode16bit:', num, '->', num >> 8, num & 0xff, 'dsl pointer:', dslPointer, ', ml pointer:', mlPointer, '/', mlBufSize); ASSERT(typeof num === 'number', 'Encoding 16bit must be num', typeof num, num); ASSERT(num >= 0, 'OOB num', num); if (num > 0xffff) THROW('Need 32bit num support but missing it', num); if (mlPointer >= mlBufSize - 1) grow(); mlBuffer[mlPointer++] = (num >> 8) & 0xff; mlBuffer[mlPointer++] = num & 0xff; } function encode32bit(num) { TRACE('encode32bit:', num, '->', (num >> 24) & 0xff, (num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff, 'dsl pointer:', dslPointer, ', ml pointer:', mlPointer); ASSERT(typeof num === 'number', 'Encoding 32bit must be num', typeof num, num); ASSERT(num >= 0, 'OOB num', num); if (num > 0xffffffff) THROW('This requires 64bit support', num); if (mlPointer >= mlBufSize - 3) grow(); mlBuffer[mlPointer++] = (num >> 24) & 0xff; mlBuffer[mlPointer++] = (num >> 16) & 0xff; mlBuffer[mlPointer++] = (num >> 8) & 0xff; mlBuffer[mlPointer++] = num & 0xff; } function grow(forcedExtraSpace) { TRACE(' - grow(' + (forcedExtraSpace || '') + ') from', mlBufSize); // grow the buffer by 10% or set it to `force` // you can't really grow existing buffers, instead you create a bigger buffer and copy the old one into it... let oldSize = mlBufSize; if (forcedExtraSpace) mlBufSize += forcedExtraSpace; else mlBufSize += Math.max(Math.ceil(mlBufSize * 0.1), 10); ASSERT(mlBufSize > mlBuffer.length, 'grow() should grow() at least a bit...', mlBuffer.length, '->', mlBufSize); if (typeof Buffer === 'undefined') { if (ArrayBuffer.transfer) mlBuffer = new Uint8Array(ArrayBuffer.transfer(mlBuffer.buffer, mlBufSize)); else mlBuffer = new Uint8Array(ArrayBufferTransferPoly(mlBuffer.buffer, mlBufSize)); } else { mlBuffer = new Uint8Array(Buffer.concat([mlBuffer], mlBufSize)); // wont actually concat, but will copy the existing buffer into a buffer of given size mlBuffer.fill(0, oldSize); } ASSERT(mlBuffer instanceof Uint8Array); } function read() { return dslBuf[dslPointer]; } function readD(delta) { return dslBuf[dslPointer + delta]; } function substr_expensive(start, stop) { // use sparingly! return String.fromCharCode(...dslBuf.slice(start, stop)); } function skip() { ++dslPointer; } function is(c, desc) { if (!desc) desc = ''; if (read() !== c) THROW('Expected ' + desc + ' `' + c + '`, found `' + read() + '`'); skip(); } function skipWhitespaces() { while (dslPointer < 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) { // make sure you dont actually want isNewlineChar() return s === $$SPACE || s === $$TAB; } function isNewlineChar(s) { return s === $$CR || s === $$LF; } function atEol(c) { return isNewlineChar(c) || isComment(c) || isEof(); } function isLineEnd(s) { // the line ends at a newline or a comment return s === $$CR || s === $$LF || s === $$HASH; } function isComment(s) { return s === $$HASH; } function isWhite(s) { return isWhitespace(s) || isNewlineChar(s); } function expectEol() { skipWhitespaces(); if (dslPointer < len) { let c = read(); if (c === $$HASH) { skipComment(); } else if (isNewlineChar(c)) { skip(); } else { THROW('Expected EOL but got `' + read() + '`'); } } } function isEof() { return dslPointer >= len; } function parseStatement() { // either: // - start with colon: var decl // - start with hash: line comment // - empty: empty // - otherwise: constraint skipWhites(); ASSERT(read() !== $$HASH, 'comments should be parsed by skipWhites'); switch (read()) { case $$COLON: parseVar(); return; case $$AT: parseAtRule(); return; default: if (!isEof()) { parseVoidConstraint(); return; } } } function parseVar() { skip(); // is($$COLON) skipWhitespaces(); let nameNames = parseIdentifier(); skipWhitespaces(); if (read() === $$COMMA) { nameNames = [nameNames]; do { skip(); skipWhitespaces(); nameNames.push(parseIdentifier()); skipWhitespaces(); } while (!isEof() && read() === $$COMMA); } if (read() === $$EQ) { skip(); skipWhitespaces(); } let domain = parseDomain(); skipWhitespaces(); let mod = parseModifier(); expectEol(); if (typeof nameNames === 'string') { addParsedVar(nameNames, domain, mod); } else { nameNames.forEach(name => addParsedVar(name, domain, mod)); } } function addParsedVar(name, domain, mod) { return addVar(name, domain, mod, false, true, THROW); } function parseIdentifier() { if (read() === $$SQUOTE) return parseQuotedIdentifier(); else return parseUnquotedIdentifier(); } function parseQuotedIdentifier() { is($$SQUOTE); let ident = ''; while (!isEof()) { let c = read(); if (c === $$SQUOTE) break; if (c !== $$HASH && isLineEnd(c)) THROW('Quoted identifier wasnt closed at eol'); ident += String.fromCharCode(c); skip(); } if (isEof()) THROW('Quoted identifier wasnt closed at eof'); if (!ident) THROW('Expected to parse identifier, found none'); skip(); // quote return ident; // return unquoted ident } function parseUnquotedIdentifier() { // anything terminated by whitespace let c = read(); let ident = ''; if (c >= $$0 && c <= $$9) THROW('Unquoted ident cant start with number'); while (!isEof()) { c = read(); if (!isValidUnquotedIdentChar(c)) break; ident += String.fromCharCode(c); skip(); } if (!ident) THROW('Expected to parse identifier, found none'); return ident; } function isValidUnquotedIdentChar(c) { switch (c) { case $$LEFTPAREN: case $$RIGHTPAREN: case $$COMMA: case $$LEFTBRACK: case $$RIGHTBRACK: case $$SQUOTE: case $$HASH: return false; } if (isWhite(c)) return false; return true; } function parseDomain() { // [] // [lo hi] // [[lo hi] [lo hi] ..] // * // 25 // (comma's optional and ignored) let domain; let c = read(); switch (c) { case $$LEFTBRACK: is($$LEFTBRACK, 'domain start'); skipWhitespaces(); domain = []; if (read() === $$LEFTBRACK) { // range inside the domain that is wrapped in brakcets do { skip(); skipWhitespaces(); let lo = parseNumber(); skipWhitespaces(); if (read() === $$COMMA) { skip(); skipWhitespaces(); } let hi = parseNumber(); skipWhitespaces(); is($$RIGHTBRACK, 'range-end'); skipWhitespaces(); domain.push(lo, hi); if (read() === $$COMMA) { skip(); skipWhitespaces(); } } while (read() === $$LEFTBRACK); } else { // individual ranges not wrapped while (read() !== $$RIGHTBRACK) { skipWhitespaces(); let lo = parseNumber(); skipWhitespaces(); if (read() === $$COMMA) { skip(); skipWhitespaces(); } let hi = parseNumber(); skipWhitespaces(); domain.push(lo, hi); if (read() === $$COMMA) { skip(); skipWhitespaces(); } } } is($$RIGHTBRACK, 'domain-end'); if (domain.length === 0) THROW('Empty domain [] in dsl, this problem will always reject'); return domain; case $$STAR: 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() !== $$AT) return; skip(); let mod = {}; let stratName = ''; while (true) { let c = read(); if (!((c >= $$a && c <= $$z) || (c >= $$A && c <= $$Z))) break; stratName += String.fromCharCode(c); skip(); } switch (stratName) { case 'list': parseList(mod); break; case 'markov': parseMarkov(mod); break; case 'max': case 'mid': case 'min': case 'naive': mod.valtype = stratName; break; case 'minMaxCycle': case 'splitMax': case 'splitMin': THROW('TODO: implement this modifier [' + stratName + ']'); break; default: THROW('implement me (var mod) [`' + stratName + '`]'); } mod.valtype = stratName; return mod; } function parseList(mod) { skipWhitespaces(); if (!(readD(0) === $$p && readD(1) === $$r && readD(2) === $$i && readD(3) === $$o && readD(4) === $$LEFTPAREN)) { THROW('Expecting the priorities to follow the `@list`'); } dslPointer += 5; mod.valtype = 'list'; mod.list = parseNumList(); is($$RIGHTPAREN, 'list end'); } function parseMarkov(mod) { mod.valtype = 'markov'; let repeat = true; while (repeat) { repeat = false; skipWhitespaces(); switch (read()) { case $$m: // matrix if (readD(1) === $$a && readD(2) === $$t && readD(3) === $$r && readD(4) === $$i && readD(5) === $$x && readD(6) === $$LEFTPAREN) { // TOFIX: there is no validation here. apply stricter and safe matrix parsing dslPointer += 7; let start = dslPointer; while (read() !== $$RIGHTPAREN && !isEof()) skip(); if (isEof()) THROW('The matrix must be closed by a `)` but did not find any'); ASSERT(read() === $$RIGHTPAREN, 'code should only stop at eof or )'); let matrix = substr_expensive(start, dslPointer); let code = 'return ' + matrix; let func = Function(code); /* eslint no-new-func: "off" */ mod.matrix = func(); is($$RIGHTPAREN, 'end of matrix'); // kind of a redundant double check. could also just skip() here. repeat = true; } break; case $$l: // legend if (readD(1) === $$e && readD(2) === $$g && readD(3) === $$e && readD(4) === $$n && readD(5) === $$d && readD(6) === $$LEFTPAREN) { dslPointer += 7; skipWhitespaces(); mod.legend = parseNumList(); skipWhitespaces(); is($$RIGHTPAREN, 'legend closer'); repeat = true; } break; case $$e: // expand if (readD(1) === $$x && readD(2) === $$p && readD(3) === $$a && readD(4) === $$n && readD(5) === $$d && readD(6) === $$LEFTPAREN) { dslPointer += 7; skipWhitespaces(); mod.expandVectorsWith = parseNumber(); skipWhitespaces(); is($$RIGHTPAREN, 'expand closer'); repeat = true; } break; } } } function skipComment() { is($$HASH, 'comment start'); //is('#', 'comment hash'); while (!isEof() && !isNewlineChar(read())) skip(); if (!isEof()) skip(); } function parseVoidConstraint() { // parse a constraint that does not return a value itself // first try to parse single value constraints without value like markov() and diff() if (parseUexpr()) return; // so the first value must be a value returning expr parseComplexVoidConstraint(); expectEol(); } function parseComplexVoidConstraint() { // parse a constraint that at least starts with a Vexpr but ultimately doesnt return anything let indexA = parseVexpr(undefined, true); skipWhitespaces(); // `A==B<eof>` then A==B would be part of A and the parser would want to parse a cop here. there's a test case. if (isEof()) THROW('Expected to parse a cop but reached eof instead'); let cop = parseCop(); skipWhitespaces(); if (cop === '=') { lastAssignmentIndex = indexA; parseAssignment(indexA); } else { ASSERT(cop, 'the cop parser should require to parse a valid cop'); let indexB = parseVexpr(); compileVoidConstraint(indexA, cop, indexB); } } function compileVoidConstraint(indexA, cop, indexB) { switch (cop) { case '==': startConstraint(ML_SAME); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '!=': startConstraint(ML_DIFF); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '<': startConstraint(ML_LT); encode16bit(2); encode16bit(indexA); encode16bit(indexB); break; case '<=': startConstraint(ML_LTE); encode16bit(2); encode16bit(indexA); encode16bit(indexB); break; case '>': startConstraint(ML_LT); encode16bit(2); encode16bit(indexB); encode16bit(indexA); break; case '>=': startConstraint(ML_LTE); encode16bit(2); encode16bit(indexB); encode16bit(indexA); break; case '&': startConstraint(ML_ALL); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '!&': startConstraint(ML_NALL); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '|': startConstraint(ML_SOME); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '!|': startConstraint(ML_NONE); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '^': startConstraint(ML_XOR); encode16bit(2); // this brings the op size in line with all other ops. kind of a waste but so be it. encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '!^': startConstraint(ML_XNOR); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); break; case '->': startConstraint(ML_IMP); encode16bit(2); encode16bit(indexA); encode16bit(indexB); break; case '!->': startConstraint(ML_NIMP); encode16bit(2); encode16bit(indexA); encode16bit(indexB); break; default: THROW('Unknown constraint op: [' + cop + ']'); } } function parseAssignment(indexC) { let indexA = parseVexpr(indexC); skipWhitespaces(); let c = read(); if (isEof() || isLineEnd(c)) { // any var, literal, or group without "top-level" op (`A=5`, `A=X`, `A=(B+C)`, `A=sum(...)`, etc) if (indexA !== indexC) { compileVoidConstraint(indexA, '==', indexC); } } else { let rop = parseRop(); if (!rop) THROW('Expecting right paren or rop, got [' + rop + ']'); skipWhitespaces(); let indexB = parseVexpr(); return compileValueConstraint(indexA, rop, indexB, indexC); } } function compileValueConstraint(indexA, rop, indexB, indexC) { let wasReifier = false; switch (rop) { case '==?': startConstraint(ML_ISSAME); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); encode16bit(indexC); wasReifier = true; break; case '!=?': startConstraint(ML_ISDIFF); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); encode16bit(indexC); wasReifier = true; break; case '<?': startConstraint(ML_ISLT); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); wasReifier = true; break; case '<=?': startConstraint(ML_ISLTE); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); wasReifier = true; break; case '&?': startConstraint(ML_ISALL); encode16bit(2); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '!&?': startConstraint(ML_ISNALL); encode16bit(2); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '|?': startConstraint(ML_ISSOME); encode16bit(2); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '!|?': startConstraint(ML_ISNONE); encode16bit(2); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '+': startConstraint(ML_SUM); encode16bit(2); // count encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); encode16bit(indexC); break; case '-': startConstraint(ML_MINUS); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '*': startConstraint(ML_PRODUCT); encode16bit(2); encode16bit(indexA < indexB ? indexA : indexB); encode16bit(indexA < indexB ? indexB : indexA); encode16bit(indexC); break; case '/': startConstraint(ML_DIV); encode16bit(indexA); encode16bit(indexB); encode16bit(indexC); break; case '>?': return compileValueConstraint(indexB, '<?', indexA, indexC); case '>=?': return compileValueConstraint(indexB, '<=?', indexA, indexC); default: THROW('Expecting right paren or rop, got [' + rop + ']'); } if (wasReifier && indexC === lastAssignmentIndex && indexC === lastUnknownIndex) setDomain(indexC, domain_createRange(0, 1)); return indexC; } function parseCop() { let c = read(); switch (c) { case $$EQ: skip(); if (read() === $$EQ) { skip(); return '=='; } return '='; case $$BANG: skip(); let r = read(); if (r === $$EQ) { skip(); return '!='; } if (r === $$AND) { skip(); return '!&'; } if (r === $$XOR) { skip(); return '!^'; } if (r === $$OR) { skip(); return '!|'; } if (r === $$DASH && readD(1) === $$GT) { skip(); skip(); return '!->'; } return THROW('Unknown cop that starts with [!]'); case $$LT: skip(); if (read() === $$EQ) { skip(); return '<='; } return '<'; case $$GT: skip(); if (read() === $$EQ) { skip(); return '>='; } return '>'; case $$DASH: if (readD(1) === $$GT) { skip(); skip(); return '->'; } break; // error case $$AND: skip(); return '&'; case $$OR: skip(); return '|'; case $$XOR: skip(); return '^'; case $$HASH: return THROW('Expected to parse a cop but found a comment instead'); } if (isEof()) THROW('Expected to parse a cop but reached eof instead'); THROW('Unknown cop char: `' + c + '`'); } function parseRop() { switch (read()) { case $$EQ: skip(); if (read() === $$EQ) { skip(); is($$QM, 'reifier suffix'); return '==?'; } else { return '='; } case $$BANG: skip(); let r = ''; if (read() === $$EQ) { is($$EQ, 'middle part of !=? op'); r = '!=?'; } else if (read() === $$AND) { is($$AND, 'middle part of !&? op'); r = '!&?'; } else if (read() === $$OR) { is($$OR, 'middle part of !|? op'); r = '!|?'; } else { THROW('invalid rop that starts with a bang'); } is($$QM, 'reifier suffix'); return r; case $$LT: skip(); if (read() === $$EQ) { skip(); is($$QM, 'reifier suffix'); return '<=?'; } else { is($$QM, 'reifier suffix'); return '<?'; } case $$GT: skip(); if (read() === $$EQ) { skip(); is($$QM, 'reifier suffix'); return '>=?'; } else { is($$QM, 'reifier suffix'); return '>?'; } case $$OR: skip(); is($$QM, 'issome suffix'); return '|?'; case $$AND: skip(); is($$QM, 'isall suffix'); return '&?'; case $$PLUS: skip(); return '+'; case $$DASH: skip(); return '-'; case $$STAR: skip(); return '*'; case $$DIV: skip(); return '/'; default: return ''; } } function parseUexpr() { // it's not very efficient (we could parse an ident before and check that result here) but it'll work for now let c = read(); // distinct is legacy support, same as diff() if (c === $$d && readD(1) === $$i && readD(2) === $$s && readD(3) === $$t && readD(4) === $$i && readD(5) === $$n && readD(6) === $$c && readD(7) === $$t && readD(8) === $$LEFTPAREN) { parseCalledListConstraint(ML_DIFF, 9); return true; } if (c === $$d && readD(1) === $$i && readD(2) === $$f && readD(3) === $$f && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_DIFF, 5); return true; } if (c === $$a && readD(1) === $$l && readD(2) === $$l && readD(3) === $$LEFTPAREN) { parseCalledListConstraint(ML_ALL, 4); return true; } if (c === $$n && readD(1) === $$a && readD(2) === $$l && readD(3) === $$l && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_NALL, 5); return true; } if (c === $$s && readD(1) === $$a && readD(2) === $$m && readD(3) === $$e && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_SAME, 5); return true; } if (c === $$s && readD(1) === $$o && readD(2) === $$m && readD(3) === $$e && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_SOME, 5); return true; } if (c === $$n && readD(1) === $$o && readD(2) === $$n && readD(3) === $$e && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_NONE, 5); return true; } if (c === $$x && readD(1) === $$n && readD(2) === $$o && readD(3) === $$r && readD(4) === $$LEFTPAREN) { parseCalledListConstraint(ML_XNOR, 5); return true; } return false; } function parseCalledListConstraint(opcode, delta) { dslPointer += delta; skipWhitespaces(); let vals = parseVexpList(); ASSERT(vals.length <= 255, 'dont do lists with more than 255 vars :('); startConstraint(opcode); encode16bit(vals.length); vals.forEach(encode16bit); skipWhitespaces(); is($$RIGHTPAREN, 'parseCalledListConstraint call closer'); expectEol(); } function parseVexpList() { let list = []; skipWhitespaces(); while (!isEof() && read() !== $$RIGHTPAREN) { let index = parseVexpr(); list.push(index); skipWhitespaces(); if (read() === $$COMMA) { skip(); skipWhitespaces(); } } if (!list.length) THROW('Expecting at least one expression in the list'); return list; } function parseVexpr(resultIndex, canBeUnknown) { // valcall, ident, number, group // ALWAYS return a var or constant INDEX! // resultIndex is only passed on if this was an explicit // assignment (like the index of `X` in `X = sum(A B C)`) let c = read(); let index; if (c === $$LEFTPAREN) { index = parseGrouping(); } else if (c === $$LEFTBRACK) { let domain = parseDomain(); index = addVar(undefined, domain, false, false, true); } else if (c >= $$0 && c <= $$9) { let num = parseNumber(); index = addVar(undefined, num, false, false, true); } else { let ident = parseIdentifier(); if (read() === $$LEFTPAREN) { if (ident === 'sum') index = parseArgs(ML_SUM, resultIndex, false); else if (ident === 'product') index = parseArgs(ML_PRODUCT, resultIndex, false); else if (ident === 'all?') index = parseArgs(ML_ISALL, resultIndex, true); else if (ident === 'diff?') index = parseArgs(ML_ISDIFF, resultIndex, true); else if (ident === 'nall?') index = parseArgs(ML_ISNALL, resultIndex, true); else if (ident === 'none?') index = parseArgs(ML_ISNONE, resultIndex, true); else if (ident === 'same?') index = parseArgs(ML_ISSAME, resultIndex, true); else if (ident === 'some?') index = parseArgs(ML_ISSOME, resultIndex, true); else THROW('Unknown reifier constraint func: ' + ident); } else { // implicitly declare unknown vars as [SUB,SUP] index = name2index(ident, false, true); if (index < 0) { if (canBeUnknown) lastUnknownIndex = index = addVar(ident, undefined, false, false, true); else THROW('CONSTRAINT_VARS_SHOULD_BE_DECLARED; Unknown var [' + ident + ']'); } } } TRACE('parseVexpr resulted in index:', index); return index; } function parseGrouping() { is($$LEFTPAREN, 'group open'); skipWhitespaces(); let indexA = parseVexpr(); skipWhitespaces(); // just wrapping a vexpr is okay, otherwise it needs a rop if (read() !== $$RIGHTPAREN) { let rop = parseRop(); if (!rop) THROW('Expecting right paren or rop'); skipWhitespaces(); let indexB = parseVexpr(); let indexC = addVar(undefined, rop[rop.length - 1] === '?' ? [0, 1] : undefined, false, false, true); indexA = compileValueConstraint(indexA, rop, indexB, indexC); skipWhitespaces(); } is($$RIGHTPAREN, 'group closer'); return indexA; } function parseNumber() { let numstr = parseNumstr(); if (!numstr) { THROW('Expecting to parse a number but did not find any digits c=[ord(' + read() + ')=' + String.fromCharCode(read()) + ']'); } return parseInt(numstr, 10); } function parseArgs(op, resultIndex, defaultBoolResult) { is($$LEFTPAREN, 'args call opener'); skipWhitespaces(); let refs = parseVexpList(); // note: the var may not declared if the constraint was anonymously grouped (ie `(sum(A B)>10)`) if (resultIndex === undefined) resultIndex = addVar(undefined, defaultBoolResult ? [0, 1] : undefined, false, false, true); else if (resultIndex === lastAssignmentIndex && resultIndex === lastUnknownIndex && defaultBoolResult) setDomain(resultIndex, domain_createRange(0, 1)); TRACE('parseArgs refs:', resultIndex, ' = all(', refs, '), defaultBoolResult:', defaultBoolResult); startConstraint(op); encode16bit(refs.length); // count refs.sort((a, b) => a - b); refs.forEach(encode16bit); encode16bit(resultIndex); skipWhitespaces(); is($$RIGHTPAREN, 'args closer'); return resultIndex; } function parseNumstr() { let numstr = ''; while (!isEof()) { let c = read(); if (c < $$0 || c > $$9) break; numstr += String.fromCharCode(c); skip(); } return numstr; } function parseNumList() { let nums = []; skipWhitespaces(); let numstr = parseNumstr(); while (numstr) { nums.push(parseInt(numstr, 10)); skipWhitespaces(); if (read() === $$COMMA) { ++dslPointer; skipWhitespaces(); } numstr = parseNumstr(); } if (!nums.length) THROW('Expected to parse a list of at least some numbers but found none'); return nums; } function parseIdentsTo(target) { let idents = parseIdents(target); if (!idents.length) THROW('Expected to parse a list of at least some identifiers but found none'); return idents; } function parseIdents(target) { let idents = []; skipWhitespaces(); while (!isEof()) { let c = read(); if (c === target) return idents; if (isLineEnd(c)) break; if (c === $$COMMA) { if (!idents.length) THROW('Leading comma not supported'); skip(); skipWhitespaces(); if (atEol(read())) THROW('Trailing comma not supported'); // mmmm or should we? dont believe it to be relevant for this language c = read(); if (c === $$COMMA) THROW('Double comma not supported'); } let ident = parseIdentifier(); idents.push(ident); skipWhitespaces(); } if (target === undefined) return idents; THROW('Missing target char at eol/eof'); } function readLineRest() { let str = ''; while (!isEof()) { let c = read(); if (isNewlineChar(c)) break; str += String.fromCharCode(c); skip(); } return str; } function parseAtRule() { is($$AT); // mostly temporary hacks while the dsl stabilizes... let ruleName = parseIdentifier(); if (ruleName === 'custom') { skipWhitespaces(); let ident = parseIdentifier(); skipWhitespaces(); if (read() === $$EQ) { skip(); skipWhitespaces(); } switch (ident) { case 'var-strat': parseVarStrat(); break; case 'val-strat': parseValStrat(); break; case 'set-valdist': skipWhitespaces(); let target = parseIdentifier(); let config = parseRestCustom(); setValDist(name2index(target, true), JSON.parse(config)); break; case 'noleaf': { skipWhitespaces(); let idents = parseIdentsTo(undefined); for (let i = 0, len = idents.length; i < len; ++i) { // debug vars are never considered leaf vars until we change that (to something else and update this to something that still does the same thing) // this is for testing as a simple tool to prevent many trivial optimizations to kick in. it's not flawless. // encode 3x to artificially inflate the count beyond most tricks // these should not be deduped... but keep in mind that a noleafed alias gets double the counts let index = name2index(idents[i]); for (let j = 0; j < 3; ++j) { encode8bit(ML_NOLEAF); encode16bit(index); } } break; } case 'nobool': { // debugging tool; bounty should consider this var a non-booly regardless of whether it actually is skipWhitespaces(); let idents = parseIdentsTo(undefined); for (let i = 0, len = idents.length; i < len; ++i) { let index = name2index(idents[i]); encode8bit(ML_NOBOOL); encode16bit(index); } break; } case 'free': skipWhitespaces(); let size = parseNumber(); TRACE('Found a jump of', size); freeDirective = size; break; case 'targets': parseTargets(); break; default: THROW('Unsupported custom rule: ' + ident); } } else { THROW('Unknown atrule [' + ruleName + ']'); } expectEol(); } function setValDist(varIndex, dist) { ASSERT(typeof varIndex === 'number', 'expecting var indexes'); ASSERT(problem.valdist[varIndex] === undefined, 'not expecting valdists to be set twice for the same var'); problem.valdist[varIndex] = dist; } function compileJump(size) { TRACE('compileJump(' + size + '), mlPointer=', mlPointer); ASSERT(size > 0, 'dont call this function on size=0'); switch (size) { case 0: // dead code. test code should catch these cases at call site. runtime can still just ignore it. break; // ignore. only expliclty illustrates no free space case 1: encode8bit(ML_NOOP); break; case 2: encode8bit(ML_NOOP2); encode8bit(0); break; case 3: encode8bit(ML_NOOP3); encode8bit(0); encode8bit(0); break; case 4: encode8bit(ML_NOOP4); encode8bit(0); encode8bit(0); encode8bit(0); break; default: // because we manually update mlPointer the buffer may not grow accordingly. so do that immediately grow(mlPointer + size + 1); // 1 for opcode if (size < 0xffff) { encode8bit(ML_JMP); encode16bit(size - SIZEOF_V); mlPointer += size - SIZEOF_V; } else { encode8bit(ML_JMP32); encode32bit(size - SIZEOF_W); mlPointer += size - SIZEOF_W; } // buffer is explicitly fill(0)'d so no need to clear it out here (otherwise we probably should) } } 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) let fallback = false; // list only let inverted = false; // list only let issed = false; // had equal sign (illegal for list) if (read() === $$f) { let ident = parseIdentifier(); if (ident !== 'fallback') THROW('Expecting var strat name, found [' + ident + ']'); fallback = true; skipWhitespaces(); } if (read() === $$i) { let ident = parseIdentifier(); if (ident !== 'inverted') THROW('Expecting var strat name, found [' + ident + ']'); inverted = true; skipWhitespaces(); } if (read() === $$EQ) { skip(); issed = true; skipWhitespaces(); } if (read() === $$LEFTPAREN) { parseVarStratList(fallback, inverted); } else { let ident = parseIdentifier(); if (ident === 'naive' || ident === 'size' || ident === 'min' || ident === 'max' || ident === 'throw') { if (inverted) THROW('The `inverted` keyword is only used with a list'); if (fallback) { addFallbackToVarStrat(ident); } else { problem.input.varstrat = ident; } } else if (ident === 'list') { skipWhitespaces(); if (issed) THROW('The `=` should not be used for a list'); if (read() !== $$LEFTPAREN) THROW('Expecting list of idents now'); parseVarStratList(fallback, inverted); } else { THROW('Unknown var strat [' + ident + ']'); } } skipWhitespaces(); } function parseVarStratList(fallback, inverted) { is($$LEFTPAREN, 'List open'); skipWhitespaces(); let idents = parseIdents($$RIGHTPAREN); skipWhitespaces(); is($$RIGHTPAREN, 'List must be closed'); let strat = {type: 'list', inverted, priorityByName: idents}; if (fallback) { addFallbackToVarStrat(strat); } else { problem.input.varstrat = strat; } } function addFallbackToVarStrat(strat) { let vs = problem.input.varstrat; ASSERT(vs, 'should set the var strat before declaring its backup'); // should we just throw for this? if (typeof vs === 'string') vs = problem.input.varstrat = {type: vs}; while (vs.fallback) { if (typeof vs.fallback === 'string') { vs = vs.fallback = {type: vs.fallback}; } else { vs = vs.fallback; } } vs.fallback = strat; } function parseValStrat() { problem.input.valstrat = parseIdentifier(); } function parseRestCustom() { skipWhitespaces(); if (read() === $$EQ) { skip(); skipWhitespaces(); } return readLineRest(); } function parseTargets() { skipWhitespaces(); if (read() === $$EQ) THROW('Unexpected double eq sign'); if (read() === $$a && readD(1) === $$l && readD(2) === $$l) { dslPointer += 3; } else { is($$LEFTPAREN, 'ONLY_USE_WITH_SOME_TARGET_VARS; The @targets left-paren'); let list = parseIdentsTo($$RIGHTPAREN); problem.freezeTargets(list); is($$RIGHTPAREN, 'The @targets right-paren'); } expectEol(); } function THROW(msg) { if (_debug) { TRACE(String.fromCharCode(...dslBuf.slice(0, dslPointer)) + '##|PARSER_IS_HERE[' + msg + ']|##' + String.fromCharCode(...dslBuf.slice(dslPointer))); } msg += ', source at ' + dslPointer + ' #|#: `' + String.fromCharCode(...dslBuf.slice(Math.max(0, dslPointer - 20), dslPointer)) + '#|#' + String.fromCharCode(...dslBuf.slice(dslPointer, Math.min(dslBuf.length, dslPointer + 20))) + '`'; throw new Error(msg); } } function ArrayBufferTransferPoly(source, length) { // c/p https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer source = Object(source); var dest = new ArrayBuffer(length); if (!(source instanceof ArrayBuffer) || !(dest instanceof ArrayBuffer)) { throw new TypeError('Source and destination must be ArrayBuffer instances'); } if (dest.byteLength >= source.byteLength) { var nextOffset = 0; var leftBytes = source.byteLength; var wordSizes = [8, 4, 2, 1]; wordSizes.forEach(function(_wordSize_) { if (leftBytes >= _wordSize_) { var done = transferWith(_wordSize_, source, dest, nextOffset, leftBytes); nextOffset = done.nextOffset; leftBytes = done.leftBytes; } }); } return dest; function transferWith(wordSize, source, dest, nextOffset, leftBytes) { var ViewClass = Uint8Array; switch (wordSize) { case 8: ViewClass = Float64Array; break; case 4: ViewClass = Float32Array; break; case 2: ViewClass = Uint16Array; break; case 1: ViewClass = Uint8Array; break; default: ViewClass = Uint8Array; break; } var view_source = new ViewClass(source, nextOffset, Math.trunc(leftBytes / wordSize)); var view_dest = new ViewClass(dest, nextOffset, Math.trunc(leftBytes / wordSize)); for (var i = 0; i < view_dest.length; i++) { view_dest[i] = view_source[i]; } return { nextOffset: view_source.byteOffset + view_source.byteLength, leftBytes: source.byteLength - (view_source.byteOffset + view_source.byteLength), }; } } // BODY_STOP export { dsl2ml, };