fdp
Version:
Finite Domain Problem reduction system
345 lines (300 loc) • 14.5 kB
JavaScript
import {
SUB,
SUP,
ASSERT,
TRACE,
TRACE_SILENT,
ASSERT_NORDOM,
THROW,
} from '../../fdlib/src/helpers';
import {
domain__debug,
domain_arrToSmallest,
domain_createRange,
domain_createValue,
domain_getValue,
domain_intersection,
} from '../../fdlib/src/domain';
import {
trie_add,
trie_create,
trie_get,
} from '../../fdlib/src/trie';
import {
ML_JMP,
ML_NOOP,
ML_NOOP2,
ML_NOOP3,
ML_NOOP4,
ML_STOP,
ml_enc8,
ml_compileJumpSafe,
ml_validateSkeleton,
ml_walk,
} from './ml';
// BODY_START
const MAX_VAR_COUNT = 0xffff; // 16bit
function $addVar($varTrie, $vars, $domains, $valdist, $constants, $addAlias, $getAnonCounter, $targeted, $targetsFrozen, name, domain, modifier, returnName, returnIndex, _throw) {
TRACE('addVar', name, domain, modifier, returnName ? '(return name)' : '', returnIndex ? '(return index)' : '');
if (typeof name === 'number') {
domain = name;
name = undefined;
}
if (typeof domain === 'number') {
domain = domain_createValue(domain);
} else if (domain === undefined) {
domain = domain_createRange(SUB, SUP);
} else {
domain = domain_arrToSmallest(domain);
}
let newIndex;
let v = domain_getValue(domain);
if (typeof name === 'string' || v < 0 || returnName) {
let wasAnon = name === undefined;
if (wasAnon) {
name = '__' + $getAnonCounter();
TRACE(' - Adding anonymous var for dom=', domain, '->', name);
} else if (name[0] === '_' && name[1] === '_' && name === '__' + parseInt(name.slice(2), 10)) {
THROW('Dont use `__xxx` as var names, that structure is preserved for internal/anonymous var names');
}
newIndex = $vars.length;
let prev = trie_add($varTrie, name, newIndex);
if (prev >= 0) {
if (_throw) _throw('CONSTRAINT_VARS_SHOULD_BE_DECLARED; Dont declare a var [' + name + '] after using it', name, prev);
THROW('CONSTRAINT_VARS_SHOULD_BE_DECLARED; Dont declare a var [' + name + '] after using it', name, prev);
}
$vars.push(name);
$domains.push(domain);
$targeted[newIndex] = wasAnon ? false : !$targetsFrozen(); // note: cannot override frozen values since all names must already be declared when using `@custom targets`
}
// note: if the name is string but domain is constant, we must add the name here as well and immediately alias it to a constant
if (v >= 0 && !returnName) { // TODO: we'll phase out the second condition here soon, but right now constants can still end up as regular vars
// constants are compiled slightly differently
let constIndex = value2index($constants, v);
// actual var names must be registered so they can be looked up, then immediately alias them to a constant
if (newIndex >= 0) $addAlias(newIndex, constIndex, '$addvar');
newIndex = constIndex;
}
if (modifier) {
$valdist[newIndex] = modifier;
switch (modifier.valtype) {
case 'list':
case 'max':
case 'mid':
case 'min':
case 'minMaxCycle':
case 'naive':
case 'splitMax':
case 'splitMin':
break;
default:
if (_throw) _throw('implement me (var mod [' + modifier.valtype + '])');
THROW('implement me (var mod [' + modifier.valtype + '])');
}
}
// deal with explicitly requested return values...
if (returnIndex) return newIndex;
if (returnName) return name;
}
function $name2index($varTrie, $getAlias, name, skipAliasCheck, scanOnly) {
//ASSERT_LOG2('$name2index', name, skipAliasCheck);
let varIndex = trie_get($varTrie, name);
if (!scanOnly && varIndex < 0) THROW('cant use this on constants or vars that have not (yet) been declared', name, varIndex);
if (!skipAliasCheck && varIndex >= 0) varIndex = $getAlias(varIndex);
return varIndex;
}
function $addAlias($domains, $valdist, $aliases, $solveStack, $constants, indexOld, indexNew, _origin) {
TRACE(' - $addAlias' + (_origin ? ' (from ' + _origin + ')' : '') + ': Mapping index = ', indexOld, '(', domain__debug($domains[indexOld]), ') to index = ', indexNew, '(', indexNew >= $domains.length ? 'constant ' + $constants[indexNew] : domain__debug($domains[indexNew]), ')');
ASSERT(typeof indexOld === 'number', 'old index should be a number', indexOld);
ASSERT(typeof indexNew === 'number', 'new index should be a number', indexNew);
if ($aliases[indexOld] === indexNew) {
TRACE('ignore constant (re)assignments. we may want to handle this more efficiently in the future');
return;
}
ASSERT(indexOld !== indexNew, 'cant make an alias for itself', indexOld, indexNew, _origin);
ASSERT(indexOld >= 0 && indexOld <= $domains.length, 'should be valid non-constant var index', indexOld, _origin);
ASSERT(indexNew >= 0, 'should be valid var index', indexNew, _origin);
//ASSERT($domains[indexOld], 'current domain shouldnt be empty', _origin);
ASSERT(!indexOld || (indexOld - 1) in $domains, 'dont create gaps...', indexOld);
$aliases[indexOld] = indexNew;
$domains[indexOld] = false; // mark as aliased. while this isnt a change itself, it could lead to some dedupes
if (!$valdist[indexNew] && $valdist[indexOld]) $valdist[indexNew] = $valdist[indexOld]; // this shouldnt happen for constants...
}
function $getAlias($aliases, index) {
let alias = $aliases[index]; // TODO: is a trie faster compared to property misses?
while (alias !== undefined) {
TRACE_SILENT(' ($getAlias,', index, '=>', alias, ')');
if (alias === index) THROW('alias is itself?', alias, index);
index = alias;
alias = $aliases[index];
}
return index;
}
function $getDomain($domains, $constants, $getAlias, varIndex, skipAliasCheck) {
//ASSERT_LOG2(' - $getDomain', varIndex, skipAliasCheck, $constants[varIndex]);
if (!skipAliasCheck) varIndex = $getAlias(varIndex);
ASSERT(varIndex === $getAlias(varIndex), 'should only skip alias check when already certain the index is de-aliased', skipAliasCheck, varIndex, $getAlias(varIndex));
// constant var indexes start at the end of the max
let v = $constants[varIndex];
if (v !== undefined) {
ASSERT(SUB <= v && v <= SUP, 'only SUB SUP values are valid here');
return domain_createValue(v);
}
return $domains[varIndex];
}
function _assertSetDomain($domains, $constants, $getAlias, varIndex, domain, skipAliasCheck, explicitlyAllowNewValuesForPseudoAlias) {
// there's a bunch of stuff to assert. this function should not be called without ASSERT and should be eliminated as dead code by the minifier...
// args check
ASSERT(typeof varIndex === 'number' && varIndex >= 0 && varIndex <= 0xffff, 'valid varindex', varIndex);
ASSERT_NORDOM(domain);
ASSERT(skipAliasCheck === undefined || skipAliasCheck === true || skipAliasCheck === false, 'skipAliasCheck should be bool or undefined, was:', skipAliasCheck);
let currentDomain = $getDomain($domains, $constants, $getAlias, varIndex, false);
ASSERT(explicitlyAllowNewValuesForPseudoAlias || domain_intersection(currentDomain, domain) === domain, 'should not introduce values into the domain that did not exist before', domain__debug(currentDomain), '->', domain__debug(domain));
ASSERT(domain, 'Should never be set to an empty domain, even with the explicitlyAllowNewValuesForPseudoAlias flag set');
return true;
}
function $setDomain($domains, $constants, $aliases, $addAlias, $getAlias, varIndex, domain, skipAliasCheck, emptyHandled, explicitlyAllowNewValuesForPseudoAlias) {
TRACE_SILENT(' $setDomain, index=', varIndex, ', from=', $constants[$getAlias(varIndex)] !== undefined ? 'constant ' + $constants[$getAlias(varIndex)] : domain__debug($domains[$getAlias(varIndex)]), ', to=', domain__debug(domain), ', skipAliasCheck=', skipAliasCheck, ', emptyHandled=', emptyHandled, ', explicitlyAllowNewValuesForPseudoAlias=', explicitlyAllowNewValuesForPseudoAlias);
if (!domain) {
if (emptyHandled) return; // todo...
THROW('Cannot set to empty domain');
} // handle elsewhere!
ASSERT(_assertSetDomain($domains, $constants, $getAlias, varIndex, domain, skipAliasCheck, explicitlyAllowNewValuesForPseudoAlias));
let value = domain_getValue(domain);
if (value >= 0) return _$setToConstant($constants, $addAlias, varIndex, value);
return _$setToDomain($domains, $constants, $aliases, $getAlias, varIndex, domain, skipAliasCheck);
}
function _$setToConstant($constants, $addAlias, varIndex, value) {
// check if this isnt already a constant.. this case should never happen
// note: pseudo aliases should prevent de-aliasing when finalizing the aliased var
if ($constants[varIndex] !== undefined) {
// TOFIX: this needs to be handled better because a regular var may become mapped to a constant and if it becomes empty then this place cant deal/signal with that properly
if ($constants[varIndex] === value) return;
THROW('Cant update a constant (only to an empty domain, which should be handled differently)');
}
// note: since the constant causes an alias anyways, we dont need to bother with alias lookup here
// note: call site should assert that the varindex domain actually contained the value!
let constantIndex = value2index($constants, value);
$addAlias(varIndex, constantIndex, '$setDomain; because var is now constant ' + value);
}
function _$setToDomain($domains, $constants, $aliases, $getAlias, varIndex, domain, skipAliasCheck) {
if (skipAliasCheck) {
// either index was already unaliased by call site or this is solution generating. unalias the var index just in case.
$aliases[varIndex] = undefined;
} else {
varIndex = $getAlias(varIndex);
}
ASSERT(varIndex < $domains.length || $constants[varIndex] === domain, 'either the var is not a constant or it is being updated to itself');
if (varIndex < $domains.length) {
//TRACE_SILENT(' - now updating index', varIndex,'to', domain__debug(domain));
$domains[varIndex] = domain;
//} else {
// TRACE_SILENT(' - ignoring call, updating a constant to itself?', varIndex, '<', $domains.length, ', ', $constants[varIndex],' === ',domain);
}
}
function value2index(constants, value) {
//ASSERT_LOG2('value2index', value, '->', constants['v' + value]);
ASSERT(value >= SUB && value <= SUP, 'value is OOB', value);
let constantIndex = constants['v' + value];
if (constantIndex >= 0) return constantIndex;
constantIndex = MAX_VAR_COUNT - (constants._count++);
constants['v' + value] = constantIndex;
constants[constantIndex] = value;
return constantIndex;
}
function problem_create() {
let anonCounter = 0;
let varNames = [];
let varTrie = trie_create(); // name -> index (in varNames)
let domains = [];
let constants = {_count: 0};
let aliases = {};
let solveStack = [];
let leafs = [];
// per-var distribution overrides. all vars default to the global distribution setting if set and otherwise naive
let valdist = []; // 1:1 with varNames. contains json objects {valtype: 'name', ...}
let addAlias = $addAlias.bind(undefined, domains, valdist, aliases, solveStack, constants);
let getAlias = $getAlias.bind(undefined, aliases);
let name2index = $name2index.bind(undefined, varTrie, getAlias);
let targeted = [];
let targetsFrozen = false; // false once a targets directive is parsed
return {
varTrie,
varNames,
domains,
valdist,
aliases,
solveStack,
leafs,
input: { // see dsl2ml
varstrat: 'default',
valstrat: 'default',
dsl: '',
},
ml: undefined, // Uint8Array
mapping: undefined, // var index in (this) child to var index of parent
addVar: $addVar.bind(undefined, varTrie, varNames, domains, valdist, constants, addAlias, _ => ++anonCounter, targeted, _ => targetsFrozen),
getVar: name2index, // deprecated
name2index,
addAlias,
getAlias,
getDomain: $getDomain.bind(undefined, domains, constants, getAlias),
setDomain: $setDomain.bind(undefined, domains, constants, aliases, addAlias, getAlias),
isConstant: index => constants[index] !== undefined,
freezeTargets: varNames => {
if (targetsFrozen) THROW('Only one `targets` directive supported');
targetsFrozen = true;
targeted.fill(false);
varNames.forEach(name => targeted[name2index(name, true)] = true);
},
targeted, // for each element in $domains; true if targeted, false if not targeted
};
}
function problem_from(parentProblem) {
TRACE(' - problem_from(): sweeping parent and generating clean child problem');
let childProblem = problem_create(parentProblem._dsl);
let parentMl = parentProblem.ml;
childProblem.mapping = new Array(parentProblem.varNames.length);
let len = parentMl.length;
let childMl = new Uint8Array(len); // worst case there's a lot of empty space to recycle
//childMl.fill(0); // note: we shouldnt need to do this. everything but the left-over space is copied over directly. the left-over space is compiled as a full jump which should ignore the remaining contents of the buffer. it may only be a little confusing to debug if you expect that space to be "empty"
childProblem.ml = childMl;
let childOffset = 0;
let lastJumpSize = 0;
let lastJumpOffset = 0;
let stopOffset = 0;
ml_walk(parentMl, 0, (ml, offset, op, sizeof) => {
switch (op) {
case ML_JMP:
case ML_NOOP:
case ML_NOOP2:
case ML_NOOP3:
case ML_NOOP4:
lastJumpOffset = offset;
lastJumpSize = sizeof;
return; // eliminate these
case ML_STOP:
stopOffset = offset;
}
// copy the bytes to the new problem ml
// TODO: consolidate consecutive copies (probably faster to do one long copy than 10 short ones?)
ml.copy(childMl, childOffset, offset, offset + sizeof);
childOffset += sizeof;
});
TRACE('ML len:', len, 'parent content len:', (stopOffset - lastJumpSize === lastJumpOffset) ? lastJumpOffset + 1 : stopOffset, ', child content len:', childOffset);
ASSERT(childMl[childOffset - 1] === ML_STOP, 'expecting last copied op to be a STOP', childOffset, childMl[childOffset - 1], childMl);
if (childOffset < len) {
TRACE(' - there are', len - childOffset - 1, 'available bytes left, compiling a jump and moving the STOP back');
ml_compileJumpSafe(childMl, childOffset - 1, len - childOffset);
ml_enc8(childMl, len - 1, ML_STOP);
}
TRACE('PML:', parentMl);
TRACE('CML:', childMl);
ASSERT(ml_validateSkeleton(childMl, 'after streaming to a child ml'));
return childProblem;
}
// BODY_STOP
export {
problem_create,
problem_from,
};