heya-unify
Version:
Unify: a unification tool with a deep equivalence and partitioning of objects.
609 lines (566 loc) • 14.8 kB
JavaScript
/* UMD.define */ (typeof define=="function"&&define||function(d,f,m){m={module:module,require:require};module.exports=f.apply(null,d.map(function(n){return m[n]||require(n)}))})
([], function(){
"use strict";
// AnyVar
var _ = {};
// Env
function Env(){
this.variables = {};
this.values = {};
}
Env.prototype = {
declaredClass: "unify/Env",
bindVar: function(name1, name2){
var vars = this.variables, u, t, k;
if(vars.hasOwnProperty(name1)){
u = vars[name1];
if(vars.hasOwnProperty(name2)){
t = vars[name2];
for(k in t){
if(t.hasOwnProperty(k)){
vars[k] = u;
u[k] = 1;
}
}
}else{
vars[name2] = u;
u[name2] = 1;
}
}else{
if(vars.hasOwnProperty(name2)){
u = vars[name1] = vars[name2];
u[name1] = 1;
}else{
u = vars[name1] = vars[name2] = {};
u[name1] = u[name2] = 1;
}
}
},
bindVal: function(name, val){
if(this.variables.hasOwnProperty(name)){
var names = this.variables[name];
for(var k in names){
if(names.hasOwnProperty(k)){
this.values[k] = val;
}
}
}else{
this.values[name] = val;
}
}
};
// Command
function Command(f, l, r){
this.f = f;
this.l = l;
this.r = r;
}
Command.prototype.declaredClass = "unify/Command";
// Custom unifier
function Unifier(){}
Unifier.prototype.declaredClass = "unify/Unifier";
function isUnifier(x){
return x && x instanceof Unifier;
}
// Unifier should define a method:
// unify(val, ls, rs, env):
// val is a value we are unifying with
// ls is a stack of left arguments
// rs is a stack of right arguments corresponding to ls
// env is an environment
// the result should be true/false for success/failure
// Var
var unique = 0;
function Var(name){
this.name = name || ("var" + unique++);
}
Var.prototype = Object.create(Unifier.prototype);
Var.prototype.declaredClass = "unify/Var";
Var.prototype.bound = function(env){
return env.values.hasOwnProperty(this.name);
};
Var.prototype.alias = function(name, env){
var t = env.variables[this.name];
return t && t[name];
};
Var.prototype.get = function(env){
return env.values[this.name];
};
Var.prototype.unify = function(val, ls, rs, env){
if(this.bound(env)){
ls.push(this.get(env));
rs.push(val);
return true;
}
// the next case is taken care of in unify() directly
// the case of unbound variable
//if(val === _ || val === this){
// return true;
//}
if(val instanceof Var){
if(val.bound(env)){
env.bindVal(this.name, val.get(env));
}else{
env.bindVar(this.name, val.name);
}
return true;
}
env.bindVal(this.name, val);
return true;
};
function isVariable(x){
return x && x instanceof Var;
}
function variable(name){
return new Var(name);
}
// type wrapper
function Wrap(type, o){
this.type = type;
this.object = o;
}
Wrap.prototype = Object.create(Unifier.prototype);
Wrap.prototype.declaredClass = "unify/Wrap";
Wrap.prototype.unify = function(val, ls, rs, env){
var ops;
if(this.object instanceof Array){
// unify arrays
if(!val) return false;
if(val instanceof Array){
// with a naked array
return unifyArrays(this.object, this.type, this,
val, env.arrayType ? "open" : "exact", null, ls, rs, env);
}
if(val instanceof Wrap){
// with a wrapped array
return val.object instanceof Array &&
unifyArrays(this.object, this.type, this,
val.object, val.type, val, ls, rs, env);
}
return false;
}
// unify objects
if(!val || val instanceof Array) return false;
if(val instanceof Wrap){
// with a wrapped object
return !(val.object instanceof Array) &&
unifyObjects(this.object, this.type, this,
val.object, val.type, val, ls, rs, env);
}
// with a naked object
return typeof val == "object" &&
unifyObjects(this.object, this.type, this,
val, env.objectType ? "open" : "exact", null, ls, rs, env);
};
function isWrapped(o){
return o && o instanceof Wrap;
}
function open(o){
return new Wrap("open", o);
}
function isOpen(o){
return o && o instanceof Wrap && o.type === "open";
}
function soft(o){
return new Wrap("soft", o);
}
function isSoft(o){
return o && o instanceof Wrap && o.type === "soft";
}
// registry of well-known constructors
var registry = [
Array, unifyArray,
Date, unifyDate,
RegExp, unifyRegExp
];
var filters = [];
function unifyArray(l, r, ls, rs, env){
if(!r || !(r instanceof Array) || !env.arrayType && l.length != r.length) return false;
for(var i = 0, n = Math.min(l.length, r.length); i < n; ++i){
ls.push(l[i]);
rs.push(r[i]);
}
return true;
}
function unifyDate(l, r, ls, rs, env){
return r && r instanceof Date && l.getTime() == r.getTime();
}
function unifyRegExp(l, r, ls, rs, env){
return r && r instanceof RegExp && l.source == r.source &&
l.global == r.global && l.multiline == r.multiline &&
l.ignoreCase == r.ignoreCase;
}
// unification of arrays
var arrayOps = {
exact: {
exact: {
precheck: function(l, r){ return l.length == r.length; }
},
open: {
precheck: function(l, r){ return l.length >= r.length; }
},
soft: {
precheck: function(l, r){ return l.length >= r.length; },
fix: function(){ this.l.type = "exact"; }
}
},
open: {
open: {},
soft: {}
},
soft: {
soft: {
update: function(){
if(this.l.length > this.r.length){
this.r.push.apply(this.r, this.l.slice(this.r.length));
}else if(this.l.length < this.r.length){
this.l.push.apply(this.l, this.r.slice(this.l.length));
}
}
}
}
};
arrayOps.exact.exact.compare = arrayOps.exact.open.compare = arrayOps.exact.soft.compare =
function(l, r, ls, rs){
for(var i = 0, n = r.length; i < n; ++i){
ls.push(l[i]);
rs.push(r[i]);
}
};
arrayOps.open.open.compare = arrayOps.open.soft.compare = arrayOps.soft.soft.compare =
function(l, r, ls, rs){
for(var i = 0, n = Math.min(l.length, r.length); i < n; ++i){
ls.push(l[i]);
rs.push(r[i]);
}
};
arrayOps.exact.soft.update = arrayOps.open.soft.update =
function(){
if(this.l.length > this.r.length){
this.r.push.apply(this.r, this.l.slice(this.r.length));
}
};
function unifyArrays(l, lt, lm, r, rt, rm, ls, rs, env){
if(lt > rt){
var t = l; l = r; r = t;
t = lm; lm = rm; rm = t;
t = lt; lt = rt; rt = t;
}
var ops = arrayOps[lt][rt];
if(ops.precheck && !ops.precheck(l, r)) return false;
if(ops.fix && rm) ls.push(new Command(ops.fix, rm));
if(ops.update && l.length != r.length) ls.push(new Command(ops.update, l, r));
ops.compare(l, r, ls, rs, env);
return true;
}
// unification of objects
var hasOwnProperty = Object.prototype.hasOwnProperty;
var objectOps = {
exact: {
exact: {
precheck: function(l, r){
if(typeof l.hasOwnProperty == 'function'){
if(typeof r.hasOwnProperty == 'function'){
for(var k in l){
if(l.hasOwnProperty(k) && !r.hasOwnProperty(k)) return false;
}
}else{
for(var k in l){
if(l.hasOwnProperty(k) && !hasOwnProperty.call(r, k)) return false;
}
}
}else{
if(typeof r.hasOwnProperty == 'function'){
for(var k in l){
if(hasOwnProperty.call(l, k) && !r.hasOwnProperty(k)) return false;
}
}else{
for(var k in l){
if(hasOwnProperty.call(l, k) && !hasOwnProperty.call(r, k)) return false;
}
}
}
return true;
}
},
open: {},
soft: {
fix: function(){ this.l.type = "exact"; }
}
},
open: {
open: {},
soft: {}
},
soft: {
soft: {
update: function(){
if(typeof this.l.hasOwnProperty == 'function'){
if(typeof this.r.hasOwnProperty == 'function'){
for(var k in this.l){
if(this.l.hasOwnProperty(k) && !this.r.hasOwnProperty(k)){
this.r[k] = this.l[k];
}
}
for(k in this.r){
if(this.r.hasOwnProperty(k) && !this.l.hasOwnProperty(k)){
this.l[k] = this.r[k];
}
}
}else{
for(var k in this.l){
if(this.l.hasOwnProperty(k) && !hasOwnProperty.call(this.r, k)){
this.r[k] = this.l[k];
}
}
for(k in this.r){
if(this.r.hasOwnProperty(k) && !hasOwnProperty.call(this.l, k)){
this.l[k] = this.r[k];
}
}
}
}else{
if(typeof this.r.hasOwnProperty == 'function'){
for(var k in this.l){
if(hasOwnProperty.call(this.l, k) && !this.r.hasOwnProperty(k)){
this.r[k] = this.l[k];
}
}
for(k in this.r){
if(hasOwnProperty.call(this.rk) && !this.l.hasOwnProperty(k)){
this.l[k] = this.r[k];
}
}
}else{
if(typeof this.r.hasOwnProperty == 'function'){
for(var k in this.l){
if(hasOwnProperty.call(this.l, k) && !hasOwnProperty.call(this.r, k)){
this.r[k] = this.l[k];
}
}
for(k in this.r){
if(hasOwnProperty.call(this.rk) && !hasOwnProperty.call(this.l, k)){
this.l[k] = this.r[k];
}
}
}
}
}
}
}
}
};
objectOps.exact.exact.compare = objectOps.exact.open.compare = objectOps.exact.soft.compare =
function(l, r, ls, rs){
if(typeof l.hasOwnProperty == 'function'){
if(typeof r.hasOwnProperty == 'function'){
for(var k in r){
if(r.hasOwnProperty(k)){
if(!l.hasOwnProperty(k)){
return false;
}
ls.push(l[k]);
rs.push(r[k])
}
}
}else{
for(var k in r){
if(hasOwnProperty.call(r, k)){
if(!l.hasOwnProperty(k)){
return false;
}
ls.push(l[k]);
rs.push(r[k])
}
}
}
}else{
if(typeof r.hasOwnProperty == 'function'){
for(var k in r){
if(r.hasOwnProperty(k)){
if(!hasOwnProperty.call(l, k)){
return false;
}
ls.push(l[k]);
rs.push(r[k])
}
}
}else{
for(var k in r){
if(hasOwnProperty.call(r, k)){
if(!hasOwnProperty.call(l, k)){
return false;
}
ls.push(l[k]);
rs.push(r[k])
}
}
}
}
return true;
};
objectOps.open.open.compare = objectOps.open.soft.compare = objectOps.soft.soft.compare =
function(l, r, ls, rs){
if(typeof l.hasOwnProperty == 'function'){
if(typeof r.hasOwnProperty == 'function'){
for(var k in r){
if(r.hasOwnProperty(k) && l.hasOwnProperty(k)){
ls.push(l[k]);
rs.push(r[k])
}
}
}else{
for(var k in r){
if(hasOwnProperty.call(r, k) && l.hasOwnProperty(k)){
ls.push(l[k]);
rs.push(r[k])
}
}
}
}else{
if(typeof r.hasOwnProperty == 'function'){
for(var k in r){
if(r.hasOwnProperty(k) && hasOwnProperty.call(l, k)){
ls.push(l[k]);
rs.push(r[k])
}
}
}else{
for(var k in r){
if(hasOwnProperty.call(r, k) && hasOwnProperty.call(l, k)){
ls.push(l[k]);
rs.push(r[k])
}
}
}
}
return true;
};
objectOps.exact.soft.update = objectOps.open.soft.update =
function(){
if(typeof this.l.hasOwnProperty == 'function'){
if(typeof this.r.hasOwnProperty == 'function'){
for(var k in this.l){
if(this.l.hasOwnProperty(k) && !this.r.hasOwnProperty(k)){
this.r[k] = this.l[k];
}
}
}else{
for(var k in this.l){
if(this.l.hasOwnProperty(k) && !hasOwnProperty.call(this.r, k)){
this.r[k] = this.l[k];
}
}
}
}else{
if(typeof this.r.hasOwnProperty == 'function'){
for(var k in this.l){
if(hasOwnProperty.call(this.l, k) && !this.r.hasOwnProperty(k)){
this.r[k] = this.l[k];
}
}
}else{
for(var k in this.l){
if(hasOwnProperty.call(this.l, k) && !hasOwnProperty.call(this.r, k)){
this.r[k] = this.l[k];
}
}
}
}
};
function unifyObjects(l, lt, lm, r, rt, rm, ls, rs, env){
if(lt > rt){
var t = l; l = r; r = t;
t = lm; lm = rm; rm = t;
t = lt; lt = rt; rt = t;
}
var ops = objectOps[lt][rt];
if(ops.precheck && !ops.precheck(l, r)) return false;
if(ops.fix && rm) ls.push(new Command(ops.fix, rm));
if(ops.update) ls.push(new Command(ops.update, l, r));
return ops.compare(l, r, ls, rs, env);
}
// unification
function unify(l, r, env){
env = env || new Env();
var ls = [l], rs = [r], objectType = env.objectType ? "open" : "exact";
main: while(ls.length){
// perform a command, or extract a pair
l = ls.pop();
if(l && l instanceof Command){
l.f();
continue;
}
r = rs.pop();
// direct unity or anyvar
if(l === r || l === _ || r === _){
continue;
}
// process variables (variables have priority)
if(l instanceof Var){
if(l.unify(r, ls, rs, env)) continue;
return null;
}
if(r instanceof Var){
if(r.unify(l, ls, rs, env)) continue;
return null;
}
// invoke custom unifiers
if(l instanceof Unifier){
if(l.unify(r, ls, rs, env)) continue;
return null;
}
if(r instanceof Unifier){
if(r.unify(l, ls, rs, env)) continue;
return null;
}
// check rough types
if(typeof l != typeof r){
return null;
}
// special case: NaN
if(typeof l == "number" && isNaN(l) && isNaN(r)){
continue;
}
// cut off impossible combinations
if(typeof l != "object" && typeof l != "function" || !l || !r){
return null;
}
// process registered constructors
var registry = unify.registry;
for(var i = 0, len = registry.length; i < len; i += 2){
if(l instanceof registry[i] || r instanceof registry[i]){
if(registry[i + 1](l, r, ls, rs, env)) continue main;
return null;
}
}
// process registered filters
registry = unify.filters;
for(i = 0, len = registry.length; i < len; i += 2){
if(registry[i](l, r)){
if(registry[i + 1](l, r, ls, rs, env)) continue main;
return null;
}
}
// process naked objects
if(!unifyObjects(l, objectType, null, r, objectType, null, ls, rs, env)) return null;
}
return env;
}
// exports
unify._ = unify.any = _;
unify.registry = registry;
unify.filters = filters;
unify.Env = Env;
unify.Unifier = Unifier;
unify.Variable = Var;
unify.variable = variable;
unify.open = open;
unify.soft = soft;
unify.isUnifier = isUnifier;
unify.isVariable = isVariable;
unify.isWrapped = isWrapped;
unify.isOpen = isOpen;
unify.isSoft = isSoft;
return unify;
});