diff --git a/index.html b/index.html index c1e5b74..17b7cbe 100644 --- a/index.html +++ b/index.html @@ -709,7 +709,7 @@ (Term (Cons (f nat(2)) (Cons (a nat(0)) Empty)) (Func f (Cons (Const a) (Cons (Var nat(1)) Empty)))) |- ? - + a. -------- Eq (Eq a a) @@ -717,27 +717,22 @@ s. (Eq length("$s") nat(1)) ---------------------- Singleton - (Singleton "$s") + ("$s" Singleton) - s. (Singleton "$s") - ---------------------- Rev-1 - (Rev "$s" "$s") + ----------- Palindrome-Empty + ("" Palindrome) - s. ss. ssRev. (Rev "$ss" "$ssRev") (Singleton "$s") - ----------------------- Rev-n - (Rev "$s $ss" "$ssRev $s") + c. ("$c" Singleton) + ------------ Palindrome-Single + ("$c" Palindrome) - s. sRev. (Rev "$s" "$sRev") - ---------------------- Palindrome-Even - (Palindrome "$s $sRev") - - s. sRev. m. (Rev "$s" "$sRev") - ---------------------- Palindrome-Odd - (Palindrome "$s $m $sRev") + c. s. ("$c" Singleton) ("$s" Palindrome) + ------------ Palindrome-Add + ("$c $s $c" Palindrome) - + ------------------ dlads - (Palindrome "a b c b a") + ("a b c b a" Palindrome) |- ? diff --git a/src/StringA.res b/src/StringA.res index ad44264..8fe0df0 100644 --- a/src/StringA.res +++ b/src/StringA.res @@ -14,6 +14,14 @@ module Atom = { type meta = string type subst = Map.t + let prettyPrint = (term: t, ~scope: array) => + `"${Array.map(term, piece => { + switch piece { + | String(str) => str + | Var({idx}) => Util.prettyPrintVar(idx, scope) + | Schematic({schematic, allowed}) => Util.prettyPrintSchematic(schematic, allowed, scope) + } + })->Array.join(" ")}"` let substitute = (term: t, subst: subst) => Array.flatMap(term, piece => { switch piece { @@ -64,6 +72,23 @@ module Atom = { ) nu } + let compose = (s1: subst, s2: subst) => { + let nu = Map.make() + Map.entries(s1)->Iterator.forEach(opt => + switch opt { + | None => () + | Some((key, term)) => Map.set(nu, key, term->substitute(s2)) + } + ) + Map.entries(s2)->Iterator.forEach(opt => + switch opt { + | None => () + | Some((key, term)) => + Map.get(nu, key)->Option.map(_ => ())->Util.Option.getOrElse(() => Map.set(nu, key, term)) + } + ) + nu + } let emptySubst: subst = Map.make() let singletonSubst: (int, t) => subst = (schematic, term) => { @@ -79,7 +104,9 @@ module Atom = { } } - type graphSub = Eps | PieceLitSub(piece) | SchemaSub(int, array) + let tail = a => a->Array.sliceToEnd(~start=1) + let prependWith = (term: t, piece: option): t => + piece->Option.map(p => [p])->Option.getOr([])->Array.concat(term) let unify = (s: array, t: array, ~gen as _=?): Seq.t => { let match = (p1: piece, p2: piece) => { switch (p1, p2) { @@ -134,9 +161,7 @@ module Atom = { let search = (targetCycles: int): (array, bool) => { let moreSolsMightExist = ref(false) // seen is an assoc list - let rec inner = (s, t, cycle: int, seen: array<((t, t), int)>): array< - array<(int, graphSub)>, - > => { + let rec inner = (s, t, cycle: int, seen: array<((t, t), int)>): array => { let (newSeen, thisCycle) = switch seen->Array.findIndexOpt(((e, _)) => e == (s, t)) { | Some(i) => { let (_, thisCycle) = seen[i]->Option.getExn @@ -147,88 +172,95 @@ module Atom = { | None => (Array.concat([((s, t), 1)], seen), 0) } let cycle = max(thisCycle, cycle) - let searchSub = (schematic: int, allowed: array, edge: graphSub): array< - array<(int, graphSub)>, - > => { - let piece = Base.Schematic({schematic, allowed}) - let sub = switch edge { - | Eps => singletonSubst(schematic, []) - | PieceLitSub(p) => singletonSubst(schematic, [p, piece]) - | SchemaSub(s2, a2) => - singletonSubst(schematic, [Schematic({schematic: s2, allowed: a2}), piece]) + let recurse = ( + subst: subst, + s, + t, + ~keepHead: bool=false, + ~prependS: option=?, + ~prependT: option=?, + ): array => { + let (s, t) = if keepHead { + (s, t) + } else { + (s->tail, t->tail) + } + inner( + s->substitute(subst)->prependWith(prependS), + t->substitute(subst)->prependWith(prependT), + cycle, + newSeen, + )->Array.map(res => subst->compose(res)) + } + let matchSingleSchematic = (schematic: int, term: t): array => { + if term->Array.length > 1 && schematicsCountsIn(term)->Belt.Map.Int.has(schematic) { + [] + } else if cycle == targetCycles { + [singletonSubst(schematic, term)] + } else { + [] } - inner(substitute(s, sub), substitute(t, sub), cycle, newSeen)->Array.map(path => - Array.concat(path, [(schematic, edge)]) - ) } if cycle > targetCycles { moreSolsMightExist := true [] + } else if s == t && cycle == targetCycles { + [emptySubst] + } else if s == t { + [] } else { - switch (s[0], t[0]) { - | (None, None) => cycle == targetCycles ? [[]] : [] - | (Some(Schematic({schematic, allowed})), other) - | (other, Some(Schematic({schematic, allowed}))) => - switch other { - | None => searchSub(schematic, allowed, Eps) - | Some(p) => - switch p { - | String(_) => - Array.concat( - searchSub(schematic, allowed, PieceLitSub(p)), - searchSub(schematic, allowed, Eps), - ) - | Schematic({schematic: s2, allowed: a2}) => - if schematic == s2 { - inner( - s->Array.sliceToEnd(~start=1), - t->Array.sliceToEnd(~start=1), - cycle, - newSeen, - ) - } else { - Array.concat( - searchSub(schematic, allowed, Eps), - searchSub(schematic, allowed, SchemaSub(s2, a2)), - ) + switch (s, t) { + | ([Schematic({schematic, _})], t') => matchSingleSchematic(schematic, t') + | (s', [Schematic({schematic, _})]) => matchSingleSchematic(schematic, s') + | (_, _) => { + let schematicCase = (s1: int, a1: array, lhs: t, rhs: t) => { + let schem1 = Base.Schematic({schematic: s1, allowed: a1}) + switch rhs[0] { + | None => recurse(singletonSubst(s1, []), lhs, rhs) + | Some(Schematic({schematic: s2, allowed: a2})) => { + let schem2 = Base.Schematic({schematic: s2, allowed: a2}) + let lhsEpsilon = recurse(singletonSubst(s1, []), lhs, rhs, ~keepHead=true) + if s1 == s2 { + lhsEpsilon->Array.concat(recurse(emptySubst, lhs, rhs)) + } else { + Array.flat([ + lhsEpsilon, + recurse(singletonSubst(s1, [schem2, schem1]), lhs, rhs, ~prependS=schem1), + recurse(singletonSubst(s2, [schem1, schem2]), lhs, rhs, ~prependT=schem2), + ]) + } + } + | Some(other) => + Array.flat([ + recurse(singletonSubst(s1, []), lhs, rhs, ~keepHead=true), + recurse( + singletonSubst(s1, [other, Schematic({schematic: s1, allowed: a1})]), + lhs, + rhs, + ~prependS=schem1, + ), + ]) } - | Var({idx}) => - if Belt.Set.Int.fromArray(allowed)->Belt.Set.Int.has(idx) { - Array.concat( - searchSub(schematic, allowed, PieceLitSub(p)), - searchSub(schematic, allowed, Eps), - ) + } + switch (s[0], t[0]) { + | (None, None) => cycle == targetCycles ? [emptySubst] : [] + | (Some(Schematic({schematic, allowed})), _) => + schematicCase(schematic, allowed, s, t) + | (_, Some(Schematic({schematic, allowed}))) => + schematicCase(schematic, allowed, t, s) + | (Some(p1), Some(p2)) => + if p1 == p2 { + inner(s->tail, t->tail, cycle, newSeen) } else { - searchSub(schematic, allowed, Eps) + [] } + | (_, None) | (None, _) => [] } } - | (p1, p2) if p1 == p2 => - inner(s->Array.sliceToEnd(~start=1), t->Array.sliceToEnd(~start=1), cycle, newSeen) - | _ => [] } } } - let paths = inner(s, t, 0, []) - let substs = paths->Array.map(path => { - let sub = Map.make() - path->Array.forEach(((schem, edge)) => { - Map.set( - sub, - schem, - switch edge { - | Eps => [] - | PieceLitSub(p) => Array.concat(Map.get(sub, schem)->Option.getOr([]), [p]) - | SchemaSub(s2, _) => - Array.concat( - Map.get(sub, schem)->Option.getOr([]), - Map.get(sub, s2)->Option.getOr([]), - ) - }, - ) - }) - sub - }) + let substs = inner(s, t, 0, []) let substsSorted = substs->Array.toSorted((s1, s2) => { let substLength = s => s @@ -245,10 +277,21 @@ module Atom = { }) (substsSorted, moreSolsMightExist.contents) } - Seq.unfold((0, true), ((c, moreSolsMightExist)) => { + let hashSubst = subst => + subst + ->Util.prettyPrintMap(~showV=t => prettyPrint(t, ~scope=[])) + ->Util.Hash.cyrb53 + Seq.unfold((0, true, Belt.Set.Int.empty), ((c, moreSolsMightExist, seen)) => { if moreSolsMightExist { let (substs, moreSolsMightExist) = search(c) - Some(substs->Seq.fromArray, (c + 1, moreSolsMightExist)) + let newSeen = ref(seen) + let uniqueSubsts = substs->Array.filter(subst => { + let hash = hashSubst(subst) + let seenThisSubst = newSeen.contents->Belt.Set.Int.has(hash) + newSeen := newSeen.contents->Belt.Set.Int.add(hash) + !seenThisSubst + }) + Some(uniqueSubsts->Seq.fromArray, (c + 1, moreSolsMightExist, newSeen.contents)) } else { None } @@ -265,7 +308,7 @@ module Atom = { } else if max(maxCountS, maxCountT) <= 2 { pigPug(s, t) } else { - Seq.fromArray([]) + Seq.empty } } @@ -331,15 +374,6 @@ module Atom = { type gen = ref - let prettyPrint = (term: t, ~scope: array) => - `"${Array.map(term, piece => { - switch piece { - | String(str) => str - | Var({idx}) => Util.prettyPrintVar(idx, scope) - | Schematic({schematic, allowed}) => Util.prettyPrintSchematic(schematic, allowed, scope) - } - })->Array.join(" ")}"` - type remaining = string type errorMessage = string type ident = string diff --git a/src/Util.res b/src/Util.res index 212b026..add9fcb 100644 --- a/src/Util.res +++ b/src/Util.res @@ -177,3 +177,21 @@ module Option = { } } } + +module Hash = { + // https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript + let cyrb53: (string, ~seed: int=?) => int = %raw(`(str, seed = 0) => { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for(let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); + }`) +} diff --git a/tests/StringTermTest.res b/tests/StringTermTest.res index 37a1cf0..5efdfa8 100644 --- a/tests/StringTermTest.res +++ b/tests/StringTermTest.res @@ -91,8 +91,8 @@ zoraBlock("unify", t => { }) t->block("schematics appearing at most twice", t => { - t->UnifyUtil.testUnify(x, x, ~expect=[Map.fromArray([(1, [])])]) - t->UnifyUtil.testUnify(x, y, ~expect=[Map.fromArray([(1, []), (2, [])])]) + t->UnifyUtil.testUnify(x, y, ~expect=[Map.fromArray([(1, y)])]) + t->UnifyUtil.testUnify(x, x, ~expect=[Map.fromArray([])]) t->UnifyUtil.testUnify(a, parse(`"?1() a"`), ~expect=[Map.fromArray([(1, [])])]) t->UnifyUtil.testUnify( @@ -109,12 +109,12 @@ zoraBlock("unify", t => { t->UnifyUtil.testUnify( parse(`"a ?1()"`), parse(`"?2() b`), - ~expect=[Map.fromArray([(1, b), (2, a)])], + ~expect=[Map.fromArray([(1, [y, b]->Array.flat), (2, [a, y]->Array.flat)])], ) t->UnifyUtil.testUnify( parse(`"a ?1() a"`), parse(`"?2() b a"`), - ~expect=[Map.fromArray([(1, b), (2, a)])], + ~expect=[Map.fromArray([(1, [y, b]->Array.flat), (2, [a, y]->Array.flat)])], ) t->UnifyUtil.testUnify( parse(`"b ?1() a"`), diff --git a/tests/TestUtil.res b/tests/TestUtil.res index 7d38741..fff6947 100644 --- a/tests/TestUtil.res +++ b/tests/TestUtil.res @@ -83,6 +83,7 @@ module type CAN_UNIFY = { type meta let unify: (t, t, ~gen: gen=?) => Seq.t let substEqual: (subst, subst) => bool + let prettyPrint: (t, ~scope: array) => string let prettyPrintSubst: (subst, ~scope: array) => string }