UNPKG

fdp

Version:

Finite Domain Problem reduction system

1,400 lines (1,217 loc) 105 kB
// problem optimizer // take an input problem and determine whether constraints can be pruned // or domains cut before actually generating their propagators import { $CHANGED, $REJECTED, $SOLVED, $STABLE, ASSERT, ASSERT_NORDOM, getTerm, TRACE, TRACE_MORPH, THROW, } from '../../fdlib/src/helpers'; import { domain__debug, domain_createEmpty, domain_createBoolyPair, domain_createValue, domain_divby, domain_getValue, domain_hasNoZero, domain_hasZero, domain_intersection, domain_intersectionValue, domain_invMul, domain_isBool, domain_isBooly, domain_isBoolyPair, domain_isSolved, domain_isZero, domain_max, domain_min, domain_minus, domain_mul, domain_plus, domain_removeGte, domain_removeGtUnsafe, domain_removeLte, domain_removeLtUnsafe, domain_removeValue, domain_sharesNoElements, domain_size, } from '../../fdlib/src/domain'; import { ML_ALL, ML_NOLEAF, ML_NOBOOL, 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, OFFSET_C_A, OFFSET_C_B, //OFFSET_C_C, OFFSET_C_R, SIZEOF_V, SIZEOF_W, SIZEOF_VVV, SIZEOF_C, SIZEOF_C_2, SIZEOF_CR_2, ml__debug, ml__opName, ml_c2c2, ml_cr2c2, ml_cr2cr2, ml_dec8, ml_dec16, ml_dec32, ml_enc8, ml_enc16, ml_eliminate, ml_compileJumpAndConsolidate, ml_compileJumpSafe, ml_heapSort16bitInline, ml_validateSkeleton, ml_vvv2c2, } from './ml'; import { m2d__debug, } from './ml2dsl'; // BODY_START function min_run(ml, problem, domains, names, firstRun, once) { TRACE('min_run, loop:', firstRun, ', byte code:', ml.length < 50 ? ml.join(' ') : '<big>'); TRACE(ml__debug(ml, 0, 20, problem)); // now we can access the ml in terms of bytes, jeuj let state = min_optimizeConstraints(ml, problem, domains, names, firstRun, once); if (state === $SOLVED) { TRACE('minimizing solved it!', state); // all constraints have been eliminated } else if (state === $REJECTED) { TRACE('minimizing rejected it!', state); // an empty domain was found or a literal failed a test } else { TRACE('pre-optimization finished, not yet solved'); } return state; } function min_optimizeConstraints(ml, problem, domains, names, firstRun, once) { TRACE('min_optimizeConstraints', ml.length < 50 ? ml.join(' ') : ''); TRACE(problem.domains.length > 100 ? '' : ' <' + problem.domains.map((d, i) => i + ' : ' + problem.varNames[i] + ' : ' + domain__debug(problem.getDomain(i))).join('>, <') + '>'); TRACE('minimize sweep, ml len=', ml.length, ', firstRun=', firstRun, 'once=', once); let varChanged = true; let onlyJumps = false; let emptyDomain = false; let lastPcOffset = 0; let lastOp = 0; let pc = 0; let loops = 0; let constraints = 0; // count a constraint going forward, ignore jumps, reduce when restarting from same pc let restartedRelevantOp = false; // annoying, but restartedRelevantOp could mean more scrubbing is required. but it may not... let term = getTerm(); let { addVar, addAlias, getAlias, getDomain, setDomain, solveStack, } = problem; //function addPseudoAlias(indexA, indexB, A, B) { // ASSERT(domain_isBoolyPair(A) && domain_isBoolyPair(B), 'assuming A and B are booly pairs'); // ASSERT(A !== B, 'booly pairs that are equal are regular aliases so dont call this function on them'); // // addAlias(indexA, indexB); // A is replaced by B // // // consider them aliases but add a special solve stack // // entry to restore the max to A if B turns out nonzero // solveStack.push((_, force, getDomain, setDomain) => { // TRACE(' - deduper psuedo alias; was:', domain__debug(A), '!^', domain__debug(B), ', now:', domain__debug(getDomain(indexA)), '!^', domain__debug(getDomain(indexB))); // let vB = force(indexB); // TRACE(' - B forced to', vB); // if (vB > 0) { // setDomain(indexA, domain_removeValue(A, 0), true, true); // } // // ASSERT(getDomain(indexA)); // ASSERT(getDomain(indexB)); // ASSERT(domain_isSolved(getDomain(indexA))); // ASSERT(domain_isSolved(getDomain(indexB))); // ASSERT((domain_getValue(getDomain(indexA)) === 0) === (domain_getValue(getDomain(indexB)) === 0)); // }); //} while (!onlyJumps && (varChanged || restartedRelevantOp)) { ++loops; //term.log('- looping', loops); term.time('-> min_loop ' + loops); TRACE('min outer loop'); varChanged = false; onlyJumps = true; // until proven otherwise restartedRelevantOp = false; pc = 0; constraints = 0; let ops = min_innerLoop(); TRACE('changed?', varChanged, 'only jumps?', onlyJumps, 'empty domain?', emptyDomain, 'restartedRelevantOp?', restartedRelevantOp); if (emptyDomain) { term.log('Empty domain at', lastPcOffset, 'for opcode', lastOp, [ml__debug(ml, lastPcOffset, 1, problem)], ml.slice(lastPcOffset, lastPcOffset + 10)); term.error('Empty domain, problem rejected'); } term.timeEnd('-> min_loop ' + loops); term.log(' - ops this loop:', ops, 'constraints:', constraints); if (emptyDomain) return $REJECTED; if (onlyJumps) return $SOLVED; TRACE('\n## Intermediate state: ##'); TRACE(ml__debug(ml, 0, 20, problem)); TRACE(m2d__debug(problem)); if (once) break; firstRun = false; } if (loops === 1) return $STABLE; return $CHANGED; // #################################################################################### function readIndex(ml, offset) { // get an index from ml. check for alias, and if so, immediately compile back the alias to improve future fetches. let index = ml_dec16(ml, offset); let alias = getAlias(index); if (alias !== index) ml_enc16(ml, offset, alias); return alias; } function getDomainFast(index) { ASSERT(index >= 0 && index <= 0xffff, 'expecting valid index', index); ASSERT(getAlias(index) === index, 'index should be unaliased', index, getAlias(index)); let domain = getDomain(index, true); ASSERT(domain, 'domain cant be falsy', domain); ASSERT_NORDOM(domain); if (!domain) setEmpty(index, 'bad state (empty domain should have been detected sooner)'); return domain; } function updateDomain(index, domain, desc) { TRACE(' - updateDomain; {', index, '} updated from', domain__debug(getDomain(index)), 'to', domain__debug(domain)); ASSERT(!domain || domain_intersection(getDomain(index), domain), 'should never add new values to a domain, only remove them', 'index=', index, 'old=', domain__debug(getDomain(index)), 'new=', domain__debug(domain), 'desc=', desc); setDomain(index, domain, false, true); if (domain) { varChanged = true; } else { TRACE(' - (updateDomain: EMPTY DOMAIN)'); emptyDomain = true; } return emptyDomain; } function setEmpty(index, desc) { TRACE(' - :\'( setEmpty({', index, '})', desc); emptyDomain = true; if (index >= 0) updateDomain(index, domain_createEmpty(), 'explicitly empty' + (desc ? '; ' + desc : '')); } function min_innerLoop() { let ops = 0; onlyJumps = true; let wasMetaOp = false; // jumps, start, stop, etc. not important if they "change" while (pc < ml.length && !emptyDomain) { ++ops; ++constraints; wasMetaOp = false; let pcStart = pc; lastPcOffset = pc; let op = ml[pc]; lastOp = op; //ASSERT(ml_validateSkeleton(ml, 'min_innerLoop')); TRACE(' # CU pc=' + pcStart, ', op=', op, ml__opName(op)); TRACE(' -> op: ' + ml__debug(ml, pc, 1, problem, true)); switch (op) { case ML_START: if (pc !== 0) { TRACE('reading a op=zero at position ' + pc + ' which should not happen', ml.slice(Math.max(pc - 100, 0), pc), '<here>', ml.slice(pc, pc + 100)); return THROW(' ! optimizer problem @', pc); } wasMetaOp = true; ++pc; --constraints; // not a constraint break; case ML_STOP: TRACE(' ! good end @', pcStart); wasMetaOp = true; --constraints; // not a constraint return ops; case ML_LT: TRACE('- lt vv @', pcStart); min_lt(ml, pc); break; case ML_LTE: TRACE('- lte vv @', pcStart); min_lte(ml, pc); break; case ML_NONE: TRACE('- none @', pcStart); min_none(ml, pc); break; case ML_XOR: TRACE('- xor @', pcStart); min_xor(ml, pc); break; case ML_XNOR: TRACE('- xnor @', pcStart); min_xnor(ml, pc); break; case ML_IMP: TRACE('- imp @', pcStart); min_imp(ml, pc); break; case ML_NIMP: TRACE('- nimp @', pcStart); min_nimp(ml, pc); break; case ML_DIFF: TRACE('- diff @', pcStart); min_diff(ml, pc); break; case ML_ALL: TRACE('- all() @', pcStart); min_all(ml, pc); break; case ML_ISDIFF: TRACE('- isdiff @', pcStart); min_isDiff(ml, pc); break; case ML_NALL: TRACE('- nall @', pcStart); min_nall(ml, pc); break; case ML_SAME: TRACE('- same @', pcStart); min_same(ml, pc); break; case ML_SOME: TRACE('- some @', pcStart); min_some(ml, pc); break; case ML_ISLT: TRACE('- islt vvv @', pcStart); min_isLt(ml, pc); break; case ML_ISLTE: TRACE('- islte vvv @', pcStart); min_isLte(ml, pc); break; case ML_ISALL: TRACE('- isall @', pcStart); min_isAll(ml, pc); break; case ML_ISNALL: TRACE('- isnall @', pcStart); min_isNall(ml, pc); break; case ML_ISSAME: TRACE('- issame @', pcStart); min_isSame(ml, pc); break; case ML_ISSOME: TRACE('- issome @', pcStart); min_isSome(ml, pc); break; case ML_ISNONE: TRACE('- isnone @', pcStart); min_isNone(ml, pc); break; case ML_MINUS: TRACE('- minus @', pcStart); min_minus(ml, pc); break; case ML_DIV: TRACE('- div @', pcStart); min_div(ml, pc); break; case ML_SUM: TRACE('- sum @', pcStart); min_sum(ml, pc); break; case ML_PRODUCT: TRACE('- product @', pcStart); min_product(ml, pc); break; case ML_NOBOOL: TRACE('- nobool @', pc); pc += SIZEOF_V; wasMetaOp = true; break; case ML_NOLEAF: TRACE('- noleaf @', pc); pc += SIZEOF_V; wasMetaOp = true; break; case ML_NOOP: TRACE('- noop @', pc); min_moveTo(ml, pc, 1); --constraints; // not a constraint wasMetaOp = true; break; case ML_NOOP2: TRACE('- noop2 @', pc); min_moveTo(ml, pc, 2); --constraints; // not a constraint wasMetaOp = true; break; case ML_NOOP3: TRACE('- noop3 @', pc); min_moveTo(ml, pc, 3); --constraints; // not a constraint wasMetaOp = true; break; case ML_NOOP4: TRACE('- noop4 @', pc); min_moveTo(ml, pc, 4); --constraints; // not a constraint wasMetaOp = true; break; case ML_JMP: TRACE('- jmp @', pc); min_moveTo(ml, pc, SIZEOF_V + ml_dec16(ml, pc + 1)); --constraints; // not a constraint wasMetaOp = true; break; case ML_JMP32: TRACE('- jmp32 @', pc); min_moveTo(ml, pc, SIZEOF_W + ml_dec32(ml, pc + 1)); --constraints; // not a constraint wasMetaOp = true; break; default: THROW('(mn) unknown op: 0x' + op.toString(16), ' at', pc); } if (pc === pcStart && !emptyDomain) { TRACE(' - restarting op from same pc...'); if (!wasMetaOp) restartedRelevantOp = true; // TODO: undo this particular step if the restart results in the offset becoming a jmp? --constraints; // constraint may have been eliminated } } if (emptyDomain) { return ops; } return THROW('Derailed; expected to find STOP before EOF'); } function min_moveTo(ml, offset, len) { TRACE(' - trying to move from', offset, 'to', offset + len, 'delta = ', len); switch (ml_dec8(ml, offset + len)) { case ML_NOOP: case ML_NOOP2: case ML_NOOP3: case ML_NOOP4: case ML_JMP: case ML_JMP32: TRACE('- moving to another jump so merging them now'); ml_compileJumpAndConsolidate(ml, offset, len); pc = offset; // restart, make sure the merge worked break; default: pc = offset + len; break; } } function min_same(ml, offset) { // loop through the args and alias each one to the previous. then eliminate the constraint. it is an artifact. let argCount = ml_dec16(ml, offset + 1); TRACE(' = min_same', argCount, 'x'); if (argCount === 2) { if (readIndex(ml, offset + OFFSET_C_A) === readIndex(ml, offset + OFFSET_C_B)) { TRACE(' - argcount=2 and A==B so eliminating constraint'); ml_eliminate(ml, offset, SIZEOF_C_2); return; } } if (argCount > 1) { TRACE(' - aliasing all args to the first arg, intersecting all domains, and eliminating constraint'); let firstIndex = readIndex(ml, offset + SIZEOF_C); let F = getDomain(firstIndex, true); TRACE(' - indexF=', firstIndex, ', F=', domain__debug(F)); for (let i = 1; i < argCount; ++i) { let indexD = readIndex(ml, offset + SIZEOF_C + i * 2); if (indexD !== firstIndex) { let D = getDomain(indexD, true); TRACE(' - pos:', i, ', aliasing index', indexD, 'to F, intersecting', domain__debug(D), 'with', domain__debug(F), 'to', domain__debug(domain_intersection(F, D))); F = intersectAndAlias(indexD, firstIndex, D, F); if (!F) { TRACE(' !! Caused an empty domain. Failing.'); break; } } } } TRACE(' - eliminating same() constraint'); ml_eliminate(ml, offset, SIZEOF_C + argCount * 2); } function min_diff_2(ml, offset) { TRACE(' - min_diff_2'); ASSERT(ml_dec16(ml, offset + 1) === 2, 'should be arg count = 2'); let offsetA = offset + OFFSET_C_A; let offsetB = offset + OFFSET_C_B; let indexA = readIndex(ml, offsetA); let indexB = readIndex(ml, offsetB); let A = getDomainFast(indexA); let B = getDomainFast(indexB); TRACE(' -', indexA, '!=', indexB, ' -> ', domain__debug(A), '!=', domain__debug(B)); if (!A || !B) return true; if (indexA === indexB) { TRACE(' - A != A, falsum, artifact case'); setEmpty(indexA, 'X!=X falsum'); return true; } let solved = false; // if either is solved then the other domain should // become the result of unsolved_set "minus" solved_set let vA = domain_getValue(A); if (vA >= 0) { let oB = B; B = domain_removeValue(B, vA); if (oB !== B && updateDomain(indexB, B, 'A != B with A solved')) return true; solved = true; } else { let vB = domain_getValue(B); if (domain_getValue(B) >= 0) { let oA = A; A = domain_removeValue(A, vB); if (A !== oA && updateDomain(indexA, A, 'A != B with B solved')) return true; solved = true; } } // if the two domains share no elements the constraint is already satisfied if (!solved && !domain_intersection(A, B)) solved = true; TRACE(' ->', domain__debug(A), '!=', domain__debug(B), ', solved?', solved); // solved if the two domains (now) intersect to an empty domain if (solved) { TRACE(' - No element overlapping between', indexA, 'and', indexB, 'left so we can eliminate this diff'); ASSERT(domain_sharesNoElements(A, B), 'if A or B solves, the code should have solved the diff'); ml_eliminate(ml, offset, SIZEOF_C_2); return true; } TRACE(' - min_diff_2 changed nothing'); return false; } function min_lt(ml, offset) { let offsetA = offset + OFFSET_C_A; let offsetB = offset + OFFSET_C_B; let indexA = readIndex(ml, offsetA); let indexB = readIndex(ml, offsetB); let A = getDomainFast(indexA); let B = getDomainFast(indexB); TRACE(' = min_lt', indexA, '<', indexB, ' -> ', domain__debug(A), '<', domain__debug(B)); if (indexA === indexB) return setEmpty(indexA, 'X<X falsum'); // (relevant) artifact case if (!A || !B) return; // relative comparison is easy; cut away any non-intersecting // values that violate the desired outcome. only when a A and // B have multiple intersecting values we have to keep this // constraint let oA = A; A = domain_removeGte(A, domain_max(B)); if (A !== oA) { TRACE(' - updating A to', domain__debug(A)); if (updateDomain(indexA, A, 'A lt B')) return; } let oB = B; B = domain_removeLte(B, domain_min(A)); if (B !== oB) { TRACE(' - updating B to', domain__debug(B)); if (updateDomain(indexB, B, 'A lt B')) return; } // any value in A must be < any value in B if (domain_max(A) < domain_min(B)) { TRACE(' - Eliminating lt because max(A)<min(B)'); ml_eliminate(ml, offset, SIZEOF_C_2); } else { TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + SIZEOF_C_2; } } function min_lte(ml, offset) { let offsetA = offset + OFFSET_C_A; let offsetB = offset + OFFSET_C_B; let indexA = readIndex(ml, offsetA); let indexB = readIndex(ml, offsetB); let A = getDomainFast(indexA); let B = getDomainFast(indexB); TRACE(' = min_lte', indexA, '<=', indexB, ' -> ', domain__debug(A), '<=', domain__debug(B)); if (!A || !B) return; if (indexA === indexB) { TRACE(' - Eliminating lte because max(A)<=min(A) is a tautology (once solved)'); ml_eliminate(ml, offset, SIZEOF_C_2); return; } // relative comparison is easy; cut away any non-intersecting // values that violate the desired outcome. only when a A and // B have multiple intersecting values we have to keep this // constraint TRACE(' - index A!=B so remove all >max(B) from A', domain__debug(A), domain_max(B), '->', domain__debug(domain_removeGtUnsafe(A, domain_max(B)))); let oA = A; A = domain_removeGtUnsafe(A, domain_max(B)); if (A !== oA) { TRACE(' - Updating A to', domain__debug(A)); if (updateDomain(indexA, A, 'A lte B')) return; } // A is (now) empty so just remove it let oB = B; B = domain_removeLtUnsafe(B, domain_min(A)); if (B !== oB) { TRACE(' - Updating B to', domain__debug(B)); if (updateDomain(indexB, B, 'A lte B')) return; } TRACE(' ->', domain__debug(A), '<=', domain__debug(B), ', bp?', domain_isBoolyPair(A), '<=', domain_isBoolyPair(B), ', max:', domain_max(A), '<=', domain_max(B)); // any value in A must be < any value in B if (domain_max(A) <= domain_min(B)) { TRACE(' - Eliminating lte because max(A)<=min(B)'); ml_eliminate(ml, offset, SIZEOF_C_2); } else if (domain_isBoolyPair(A) && domain_isBoolyPair(B) && domain_max(A) <= domain_max(B)) { TRACE(' - A and B boolypair with max(A)<=max(B) so this is implication'); TRACE_MORPH('A <= B', 'B -> A'); ml_c2c2(ml, offset, 2, ML_IMP, indexA, indexB); // have to add a solvestack entry to prevent a solution [01]->1 which would satisfy IMP but not LTE solveStack.push((_, force, getDomain, setDomain) => { TRACE(' - min_lte; enforcing LTE', indexA, '<=', indexB, ' => ', domain__debug(getDomain(indexA)), '<=', domain__debug(getDomain(indexB))); let A = getDomain(indexA); let B = getDomain(indexB); if (domain_hasNoZero(A)) { B = domain_removeValue(B, 0); setDomain(indexB, B); } else if (domain_isZero(B) || domain_isBooly(A)) { A = domain_removeGtUnsafe(A, 0); setDomain(indexA, A); } ASSERT(getDomain(indexA)); ASSERT(getDomain(indexB)); ASSERT(domain_max(getDomain(indexA)) <= domain_min(getDomain(indexB)), 'must hold lte', domain__debug(A), '<=', domain__debug(B)); }); } else { TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + SIZEOF_C_2; } } function min_nall(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let offsetArgs = offset + SIZEOF_C; let opSize = SIZEOF_C + argCount * 2; TRACE(' = min_nall', argCount, 'x'); TRACE(' - ml for this nall:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2))); TRACE(' -', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2))))); if (!argCount) return setEmpty(-1, 'nall without args is probably a bug'); if (argCount === 2) { if (min_nall_2(ml, offset)) return; } let countStart = argCount; let lastIndex = -1; let lastDomain; // a nall only ensures at least one of its arg solves to zero for (let i = argCount - 1; i >= 0; --i) { // backwards because we're pruning dud indexes let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - loop i=', i, 'index=', index, 'domain=', domain__debug(domain)); if (!domain) return; if (domain_min(domain) > 0 || lastIndex === index) { // remove var from list TRACE(lastIndex === index ? ' - removing redundant dupe var from nall' : ' - domain contains no zero so remove var from this constraint'); // now // - move all indexes bigger than the current back one position // - compile the new count back in // - compile a NOOP in the place of the last element TRACE(' - moving further domains one space forward (from ', i + 1, ' / ', argCount, ')'); min_spliceArgSlow(ml, offsetArgs, argCount, i, false); --argCount; } else if (domain_isZero(domain)) { // remove constraint TRACE(' - domain solved to zero so constraint is satisfied'); ml_eliminate(ml, offset, SIZEOF_C + 2 * countStart); return; } else { // arg contains a 0 and is unsolved TRACE(' - domain contains zero and is not solved so leave it alone'); lastIndex = index; lastDomain = domain; } } if (argCount === 0) { TRACE(' - Nall has no var left to be zero; rejecting problem'); // this is a bad state: all vars were removed from this constraint which // means none of the args were zero and the constraint doesnt hold return setEmpty(lastIndex, 'nall; none of the args were zero'); } else if (argCount === 1) { TRACE(' - Nall has one var left; forcing it to zero'); // force set last index to zero and remove constraint. this should not // reject (because then the var would have been removed in loop above) // but do it "safe" anyways, just in case. let domain = domain_removeGtUnsafe(lastDomain, 0); if (lastDomain !== domain && updateDomain(lastIndex, domain)) return; ml_eliminate(ml, offset, SIZEOF_C + 2 * countStart); } else if (countStart !== argCount) { TRACE(' - recording new argcount and freeing up space'); ml_enc16(ml, offsetCount, argCount); // write new count let free = (countStart - argCount) * 2; ml_compileJumpSafe(ml, offset + opSize - free, free); // note: still have to restart op because ml_jump may have clobbered the old end of the op with a new jump } else { TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + opSize; } } function min_nall_2(ml, offset) { let offsetA = offset + OFFSET_C_A; let offsetB = offset + OFFSET_C_B; let indexA = readIndex(ml, offsetA); let indexB = readIndex(ml, offsetB); let A = getDomainFast(indexA); let B = getDomainFast(indexB); TRACE(' = min_nall_2', indexA, '!&', indexB, ' -> ', domain__debug(A), '!&', domain__debug(B)); ASSERT(ml_dec16(ml, offset + 1) === 2, 'nall should have 2 args'); if (!A || !B) return true; if (indexA === indexB) { TRACE(' - indexA==indexB so A=0 and eliminate constraint'); let oA = A; A = domain_removeGtUnsafe(A, 0); if (A !== oA) updateDomain(indexA, A, '`A !& A` means A must be zero'); ml_eliminate(ml, offset, SIZEOF_C_2); return true; } if (domain_isZero(A) || domain_isZero(B)) { TRACE(' - A=0 or B=0, eliminating constraint'); ml_eliminate(ml, offset, SIZEOF_C_2); return true; } if (domain_hasNoZero(A)) { TRACE(' - A>=1 so B must be 0'); let oB = B; B = domain_removeGtUnsafe(B, 0); if (B !== oB) updateDomain(indexB, B, 'nall[2] B'); ml_eliminate(ml, offset, SIZEOF_C_2); return true; } if (domain_hasNoZero(B)) { TRACE(' - B>=1 so A must be 0'); let oA = A; A = domain_removeGtUnsafe(A, 0); if (A !== oA) updateDomain(indexA, A, 'nall[2] A'); ml_eliminate(ml, offset, SIZEOF_C_2); return true; } TRACE(' - min_nall_2 changed nothing'); return false; } function min_some(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let offsetArgs = offset + SIZEOF_C; let opSize = SIZEOF_C + argCount * 2; TRACE(' = min_some', argCount, 'x'); TRACE(' - ml for this some:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2))); TRACE(' -', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2))))); if (!argCount) return setEmpty(-1, 'some without args is probably a bug'); if (argCount === 2) { if (min_some_2(ml, offset)) return; } let countStart = argCount; let lastIndex = -1; let lastDomain; // a some only ensures at least one of its arg solves to zero for (let i = argCount - 1; i >= 0; --i) { // backwards because we're pruning dud indexes let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - loop i=', i, 'index=', index, 'domain=', domain__debug(domain)); if (!domain) return; if (domain_isZero(domain) || lastIndex === index) { // remove var from list TRACE(lastIndex === index ? ' - removing redundant dupe var from some' : ' - domain contains no zero so remove var from this constraint'); // now // - move all indexes bigger than the current back one position // - compile the new count back in // - compile a NOOP in the place of the last element TRACE(' - moving further domains one space forward (from ', i + 1, ' / ', argCount, ')'); min_spliceArgSlow(ml, offsetArgs, argCount, i, false); --argCount; } else if (domain_hasNoZero(domain)) { // remove constraint TRACE(' - domain solved to nonzero so constraint is satisfied'); ml_eliminate(ml, offset, SIZEOF_C + 2 * countStart); return; } else { // arg contains a 0 and is unsolved TRACE(' - domain contains zero and is not solved so leave it alone'); lastIndex = index; lastDomain = domain; } } if (argCount === 0) { TRACE(' - Some has no var left to be zero; rejecting problem'); // this is a bad state: all vars were removed from this constraint which // means all of the args were zero and the constraint doesnt hold return setEmpty(lastIndex, 'some; all of the args were zero'); } else if (argCount === 1) { TRACE(' - Some has one var left; forcing it to nonzero'); // force set last index to nonzero and remove constraint. this should not // reject (because then the var would have been removed in loop above) // but do it "safe" anyways, just in case. let domain = domain_removeValue(lastDomain, 0); if (lastDomain !== domain && updateDomain(lastIndex, domain)) return; ml_eliminate(ml, offset, SIZEOF_C + 2 * countStart); } else if (countStart !== argCount) { TRACE(' - recording new argcount and freeing up space'); ml_enc16(ml, offsetCount, argCount); // write new count let free = (countStart - argCount) * 2; ml_compileJumpSafe(ml, offset + opSize - free, free); // note: still have to restart op because ml_jump may have clobbered the old end of the op with a new jump } else { TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + opSize; } } function min_isAll(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let opSize = SIZEOF_C + 2 + argCount * 2; let offsetArgs = offset + SIZEOF_C; let offsetR = offset + opSize - 2; let indexR = readIndex(ml, offsetR); let R = getDomainFast(indexR); TRACE(' = min_isAll', argCount, 'x'); TRACE(' - ml for this isAll (' + opSize + 'b):', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', indexR, '= all?(', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2)), ')'); TRACE(' -', domain__debug(R), '= all?(', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2)))), ')'); if (!R) return; if (domain_isZero(R)) { TRACE(' - R is 0 so morph to nall and revisit'); // compile to nall and revisit ml_enc8(ml, offset, ML_NALL); ml_compileJumpSafe(ml, offset + opSize - 2, 2); // difference between nall with R=0 and an isAll is the result var (16bit) return; } if (domain_hasNoZero(R)) { TRACE(' - R is non-zero so remove zero from all args and eliminate constraint'); for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); if (!domain) return; let newDomain = domain_removeValue(domain, 0); if (newDomain !== domain && updateDomain(index, newDomain)) return; } ml_eliminate(ml, offset, opSize); return; } // R is unresolved. check whether R can be determined ASSERT(domain_min(R) === 0 && domain_max(R) > 0, 'R is unresolved here', R); let allNonZero = true; let someAreZero = false; let someNonZero = false; for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); // reflect isAll, // R=0 when at least one arg is zero // R>0 when all args have no zero if (domain_isZero(domain)) { TRACE(' - found a zero, breaking loop because R=0'); someAreZero = true; break; // this permanently sets R to 0; no need to loop further } else if (domain_min(domain) === 0) { // arg has zero and non-zero values so R (at least) cant be set to 1 yet allNonZero = false; } else { someNonZero = true; } } if (someAreZero) { TRACE(' - At least one arg was zero so R=0 and constraint can be removed'); let oR = R; R = domain_removeGtUnsafe(R, 0); if (R !== oR) updateDomain(indexR, R); ml_eliminate(ml, offset, opSize); return; } if (allNonZero) { TRACE(' - No arg had zero so R=1 and constraint can be removed'); let oR = R; R = domain_removeValue(R, 0); if (R !== oR) updateDomain(indexR, R); ml_eliminate(ml, offset, opSize); return; } // remove all non-zero values from the list. this reduces their connection count and simplifies this isall if (someNonZero) { let removed = 0; for (let i = argCount - 1; i >= 0; --i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - checking if index', index, '(domain', domain__debug(domain), ') contains no zero so we can remove it from this isall'); if (domain_hasNoZero(domain)) { // now // - move all indexes bigger than the current back one position // - compile the new count back in // - compile a NOOP in the place of the last element TRACE(' - moving further domains one space forward (from ', i + 1, ' / ', argCount, ')'); min_spliceArgSlow(ml, offsetArgs, argCount, i, true); --argCount; ++removed; } } ml_enc16(ml, offset + 1, argCount); // now "blank out" the space of eliminated constants, they should be at the end of the op let newOpSize = opSize - removed * 2; ml_compileJumpSafe(ml, offset + newOpSize, opSize - newOpSize); TRACE(' - Removed', removed, 'non-zero args from unsolved isall, has', argCount, 'left'); TRACE(' - ml for this sum now:', ml.slice(offset, offset + opSize).join(' ')); if (argCount === 1) _min_isAllMorphToXnor(ml, offset, argCount, offsetArgs, indexR); return; } if (argCount === 1) return _min_isAllMorphToXnor(ml, offset, argCount, offsetArgs, indexR); TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + opSize; } function _min_isAllMorphToXnor(ml, offset, argCount, offsetArgs, indexR) { // while this usually only happens when eliminating non-zeroes, there may be an artifact where a script // generated an isall with just one arg. kind of silly but whatever, right. TRACE(' - Only one arg remaining; morphing to a XNOR'); ASSERT(argCount > 0, 'isall must have at least one arg in order to have enough space for the xnor morph'); let index = readIndex(ml, offsetArgs); ml_cr2c2(ml, offset, argCount, ML_XNOR, indexR, index); varChanged = true; // the xnor may need optimization } function min_isNall(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let opSize = SIZEOF_C + argCount * 2 + 2; let offsetArgs = offset + SIZEOF_C; let offsetR = offset + opSize - 2; let indexR = readIndex(ml, offsetR); let R = getDomainFast(indexR); TRACE(' = min_isNall', argCount, 'x'); TRACE(' - ml for this isNall:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', indexR, '= nall?(', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2)), ')'); TRACE(' -', domain__debug(R), '= nall?(', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2)))), ')'); if (!R) return; if (domain_isZero(R)) { TRACE(' - R=0 so; !nall so; all so; we must remove zero from all args and eliminate constraint'); for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); if (!domain) return; let newDomain = domain_removeValue(domain, 0); if (newDomain !== domain && updateDomain(index, newDomain)) return; } ml_eliminate(ml, offset, opSize); return; } if (domain_hasNoZero(R)) { TRACE(' - R>0 so; nall. just morph and revisit'); ml_enc8(ml, offset, ML_NALL); ml_compileJumpSafe(ml, offset + opSize - 2, 2); // difference between nall and isNall is the result var (16bit) return; } // R is unresolved. check whether R can be determined ASSERT(domain_min(R) === 0 && domain_max(R) > 0, 'R is unresolved here', R); let allNonZero = true; let someAreZero = false; for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); // reflect isNall, // R=0 when all args have no zero // R>0 when at least one arg is zero if (domain_isZero(domain)) { TRACE(' - found a zero, breaking loop because R=0'); someAreZero = true; break; // this permanently sets R to 0; no need to loop further } else if (domain_min(domain) === 0) { // arg has zero and non-zero values so R (at least) cant be set to 1 yet allNonZero = false; } } if (someAreZero) { TRACE(' - At least one arg was zero so R>=1 and constraint can be removed'); let oR = R; R = domain_removeValue(R, 0); if (R !== oR) updateDomain(indexR, R, 'isnall, R>=1 because at least one var was zero'); ml_eliminate(ml, offset, opSize); } else if (allNonZero) { TRACE(' - No arg had a zero so R=0 and constraint can be removed'); let oR = R; R = domain_removeGtUnsafe(R, 0); if (R !== oR) updateDomain(indexR, R); ml_eliminate(ml, offset, opSize); } else { // TODO: prune all args here that are nonzero? is that worth it? TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + opSize; } } function min_isSome(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let opSize = SIZEOF_C + argCount * 2 + 2; let offsetArgs = offset + SIZEOF_C; let offsetR = offset + opSize - 2; let indexR = readIndex(ml, offsetR); let R = getDomainFast(indexR); TRACE(' = min_isSome', argCount, 'x'); TRACE(' - ml for this isSome:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', indexR, '= some?(', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2)), ')'); TRACE(' -', domain__debug(R), '= some?(', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2)))), ')'); if (!R) return; if (domain_isZero(R)) { TRACE(' - R=0 so; !some so; none so; we must force zero to all args and eliminate constraint'); for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); if (!domain) return; let newDomain = domain_removeGtUnsafe(domain, 0); if (newDomain !== domain && updateDomain(index, newDomain)) return; } ml_eliminate(ml, offset, opSize); return; } if (domain_hasNoZero(R)) { TRACE(' - R>0 so; some. just morph and revisit'); ml_enc8(ml, offset, ML_SOME); ml_compileJumpSafe(ml, offset + opSize - 2, 2); // difference between some and isSome is the result var (16bit) return; } // R is unresolved. check whether R can be determined ASSERT(domain_min(R) === 0 && domain_max(R) > 0, 'R is unresolved here', R); let someNonZero = false; let allZero = true; let someZero = false; for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); // reflect isSome, // R=0 when all args are zero (already checked above) // R>0 when at least one arg is nonzero if (domain_hasNoZero(domain)) { TRACE(' - found a nonzero, breaking loop because R>0'); someNonZero = true; break; // this permanently sets R to 0; no need to loop further } else if (domain_isZero(domain)) { someZero = true; } else { allZero = false; } } if (someNonZero) { TRACE(' - At least one arg was zero so R>=1 and constraint can be removed'); let oR = R; R = domain_removeValue(R, 0); if (R !== oR) updateDomain(indexR, R, 'issome, R>=1 because at least one var was nonzero'); ml_eliminate(ml, offset, opSize); } else if (allZero) { TRACE(' - All vars were zero so R=0 and constraint can be removed'); let oR = R; R = domain_removeGtUnsafe(R, 0); if (R !== oR) updateDomain(indexR, R, 'issome, R>=1 because all vars were zero'); ml_eliminate(ml, offset, opSize); } else if (someZero) { TRACE(' - Some vars were zero, removing them from the args'); // force constants to the end ml_heapSort16bitInline(ml, pc + SIZEOF_C, argCount); // we know // - these args do not contain non-zero args // - all constants are moved to the back // - there is at least one constant 0 // - not all args were 0 // so we can move back the result var let argOffset = offsetArgs + argCount * 2 - 2; TRACE(' - offset:', offset, ', argCount:', argCount, ', args offset:', offsetArgs, ', first arg domain:', domain__debug(getDomain(readIndex(ml, offsetArgs))), ', last arg offset:', argOffset, ', last domain:', domain__debug(getDomain(readIndex(ml, argOffset)))); TRACE(' - op before:', ml__debug(ml, offset, 1, problem)); ASSERT(domain_isZero(getDomain(readIndex(ml, argOffset))), 'at least the last arg should be zero', domain__debug(getDomain(readIndex(ml, argOffset)))); ASSERT(!domain_isZero(getDomain(readIndex(ml, offsetArgs))), 'the first arg should not be zero', domain__debug(getDomain(readIndex(ml, offsetArgs)))); // search for the first non-zero arg let newArgCount = argCount; while (domain_isZero(getDomain(readIndex(ml, argOffset)))) { argOffset -= 2; --newArgCount; } TRACE(' - last non-zero arg is arg number', newArgCount, 'at', argOffset, ', index:', readIndex(ml, argOffset), ', domain:', domain__debug(getDomain(readIndex(ml, argOffset)))); if (newArgCount === 1) { TRACE(' - there is one arg left, morph to XNOR'); TRACE_MORPH('R = some?(A 0 0 ..)', 'R !^ A'); let indexA = readIndex(ml, offsetArgs); ml_cr2c2(ml, offset, argCount, ML_XNOR, indexR, indexA); } else { TRACE(' - moving R to the first zero arg at offset', argOffset + 2, 'and compiling a jump for the rest'); // copy the result var over the first zero arg ml_enc16(ml, offset + 1, newArgCount); ml_enc16(ml, argOffset + 2, indexR); ml_compileJumpSafe(ml, argOffset + 4, (argCount - newArgCount) * 2); ASSERT(ml_validateSkeleton(ml, 'min_isSome; pruning zeroes')); } TRACE(' - op after:', ml__debug(ml, offset, 1, problem)); } else { // TODO: prune all args here that are zero? is that worth it? TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + opSize; } } function min_isNone(ml, offset) { let offsetCount = offset + 1; let argCount = ml_dec16(ml, offsetCount); let opSize = SIZEOF_C + argCount * 2 + 2; let offsetArgs = offset + SIZEOF_C; let offsetR = offset + opSize - 2; let indexR = readIndex(ml, offsetR); let R = getDomainFast(indexR); TRACE(' = min_isNone', argCount, 'x'); TRACE(' - ml for this isNone:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' -', indexR, '= none?(', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2)), ')'); TRACE(' -', domain__debug(R), '= none?(', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2)))), ')'); if (!R) return; if (domain_hasNoZero(R)) { TRACE(' - R>=1 so set all args to zero and eliminate'); for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); if (!domain) return; let newDomain = domain_removeGtUnsafe(domain, 0); if (newDomain !== domain && updateDomain(index, newDomain)) return; } ml_eliminate(ml, offset, opSize); return; } if (domain_isZero(R)) { TRACE(' - R=0 so this is a SOME. just morph and revisit'); TRACE_MORPH('0 = none?(A B C ...)', 'some(A B C ...)'); ml_enc8(ml, offset, ML_SOME); ml_compileJumpSafe(ml, offset + opSize - 2, 2); // difference between some and isNone is the result var (16bit) return; } // R has a zero or is zero, determine whether there is any nonzero arg, or whether they are all zero let nonZero = false; let allZero = true; for (let i = 0; i < argCount; ++i) { let index = readIndex(ml, offsetArgs + i * 2); let domain = getDomainFast(index); TRACE(' - index=', index, 'dom=', domain__debug(domain)); // reflect isNone, // R=0 when at least one arg is nonzero // R>0 when all args are zero if (domain_hasNoZero(domain)) { nonZero = true; break; } if (!domain_isZero(domain)) { allZero = false; } } if (nonZero) { TRACE(' - at least one arg had no zero so R=0, eliminate constraint'); let oR = R; R = domain_removeGtUnsafe(R, 0); if (R !== oR) updateDomain(indexR, R, 'isnone R=0 because an arg had no zero'); ml_eliminate(ml, offset, opSize); } else if (allZero) { TRACE(' - isnone, all args are 0 so R>=1, remove constraint'); let oR = R; R = domain_removeValue(R, 0); if (R !== oR) updateDomain(indexR, R, 'isnone R>=1 because all args were zero'); ml_eliminate(ml, offset, opSize); } else { // TODO: prune all args here that are zero? is that worth it? TRACE(' - not only jumps...'); onlyJumps = false; pc = offset + SIZEOF_C + argCount * 2 + 2; } } function min_diff(ml, offset) { let argCount = ml_dec16(ml, offset + 1); let offsetArgs = offset + SIZEOF_C; let opSize = SIZEOF_C + argCount * 2; TRACE(' = min_diff', argCount, 'x'); TRACE(' - ml for this diff:', ml.slice(offset, offset + opSize).join(' ')); TRACE(' - indexes:', Array.from(Array(argCount)).map((n, i) => readIndex(ml, offsetArgs + i * 2)).join(', ')); TRACE(' - domains:', Array.from(Array(argCount)).map((n, i) => domain__debug(getDomainFast(readIndex(ml, offsetArgs + i * 2)))).join(', ')); ASSERT(argCount, 'should have at least one arg or be eliminated'); if (!argCount) return setEmpty(-1, 'diff without args is probably a bug'); let countStart = argCount; // a diff is basically a pyramid of neq's; one for each unique pair of the set // we loop back to front because we're splicing out vars while looping for (let i = argCount - 1; i >= 0; --i) { let indexA = readIndex(ml, offsetArgs + i * 2); let A = getDomainFast(indexA); TRACE(' - loop i=', i, 'index=', indexA, 'domain=', domain__debug(A)); if (!A) return; let v = domain_getValue(A); if (v >= 0) { TRACE(' - solved, so removing', v, 'from all other domains and index', indexA, 'from the constraint'); for (let j = 0; j < argCount; ++j) { // gotta loop through all args. args wont be removed in this loop. if (j !== i) { let indexB = readIndex(ml, offsetArgs + j * 2); let oB = getDomainFast(indexB); TRACE(' - loop j=', j, 'index=', indexB, 'domain=', domain__debug(oB)); if (indexA === indexB) return updateDomain(indexA, domain_createEmpty(), 'diff had this var twice, x!=x is a falsum'); // edge case let B = domain_removeValue(oB, v); if (B !== oB && updateDomain(indexB, B, 'diff arg')) return; } } // so none of the other args have v and none of them ended up empty // now // - move all indexes bigger than the current back one position // - compile the new count back in // - compile a NOOP in the place of the last element TRACE(' - moving further domains one space forward (from ', i + 1, ' / ', argCount, ')', i + 1 < argCount); min_spliceArgSlow(ml, offsetArgs, argCount, i, true); // move R as well --argCount; } } if (argCount <= 1) { TRACE(' - Count is', argCount, '; eliminating constraint'); ASSERT(argCount >= 0, 'cant be negative'); ml_eliminate(ml, offset, opSize); } else if (argCount !== countStart) { TRACE(' - recompiling new count (', argCount, ')'); ml_enc16(ml, offset + 1, argCount); TRACE(' - compiling noop into empty spots'); // this hasnt happened yet ml_compileJumpSafe(ml, offsetArgs + argCount * 2, (countStart - argCount) * 2); // need to restart op because the skip may have clobbered the next op offset } else if (argCount === 2 && min_diff_2(ml, offset)) { // do nothing. min_diff_2 has already done something. } else { TRACE(' - not only jumps...', opSize); onlyJumps = false; pc = offset + opSize; } } function min_sum_2(ml, sumOffset) {