001package gudusoft.gsqlparser.lineage.v2.fold;
002
003import gudusoft.gsqlparser.lineage.v2.Finding;
004import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
005import gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex;
006
007import java.util.ArrayDeque;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collections;
011import java.util.Comparator;
012import java.util.Deque;
013import java.util.HashMap;
014import java.util.HashSet;
015import java.util.Iterator;
016import java.util.LinkedHashMap;
017import java.util.LinkedHashSet;
018import java.util.List;
019import java.util.Map;
020import java.util.Set;
021import java.util.TreeMap;
022import java.util.TreeSet;
023
024import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.intRef;
025import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.items;
026
027/**
028 * FOLD-V1 ({@code lineage-spec/algorithms/fold-v1.md}): folds the ExplainGraph of one statement of a document
029 * into its Fact Occurrences, FactCandidates and write requirements.
030 *
031 * <p>The kernel reads the document the way the reference runner ({@code conformance/run.py}, class
032 * {@code Projector}) does, so that a validator built on it reports what the reference reports. It also computes
033 * what the reference leaves out and the specification defines: the derived-unit rules of Section 7.3 (every walk
034 * of a statement whose derived text is only {@code OBSERVED} is {@code ASSUMED}, every write at most
035 * {@code PARTIAL}), the bindings of every Occurrence (Sections 7.1, item 6 and 8.1, item 2), the ordinals of
036 * FactCandidates (identity-v1, Section 5.4), and the complete requirements of every write (Sections 7.4 and
037 * 7.5).</p>
038 *
039 * <p>Exploration is over states (node, label, choices, proof) and terminates on every graph (Section 6). Its
040 * cost grows with the states reached and, for a group of linked ambiguous references, with the number of worlds
041 * (the product of their alternatives); both are bounded, and a statement over a bound throws
042 * {@link FoldLimitException}.</p>
043 *
044 * @since 4.2.17
045 */
046public final class FoldKernel {
047
048    /** The most exploration states one statement may reach (every state counts, the first and the last too). */
049    public static final int DEFAULT_MAX_STATES = 500000;
050    /** The most worlds one group of linked references, or one Fact test, may examine. */
051    public static final int DEFAULT_MAX_WORLDS = 1 << 16;
052
053    static final String SOURCE = "SOURCE";
054    static final String TARGET = "TARGET";
055    private static final String PROVEN = "PROVEN";
056    private static final String ASSUMED = "ASSUMED";
057    private static final List<String> CONTAINER_NODE_KINDS = Arrays.asList("RESULT_SET", "CTE", "SUBQUERY",
058            "SET_OPERATION", "TABLE_FUNCTION", "PIVOT");
059    private static final Map<String, Integer> RANK = new HashMap<String, Integer>();
060
061    static {
062        RANK.put("COMPLETE", 2);
063        RANK.put("PARTIAL", 1);
064        RANK.put("UNKNOWN", 0);
065    }
066
067    /** The alternative "something not listed" of an INCOMPLETE candidate set (Section 7.1, item 1). */
068    static final Object NOT_LISTED = new Object() {
069        @Override
070        public String toString() {
071            return "NOT_LISTED";
072        }
073    };
074
075    private final DocumentIndex index;
076    private final int maxStates;
077    private final int maxWorlds;
078    private final Object defaultCompleteness;
079    private final Map<Long, Object> statementCompleteness = new HashMap<Long, Object>();
080    /** The candidate sets of the ExplainGraph by ref; the last one with a ref wins, as in the reference. */
081    private final Map<Long, Map<?, ?>> graphCsets = new HashMap<Long, Map<?, ?>>();
082    private final Map<Object, Set<String>> refSide = new HashMap<Object, Set<String>>();
083    private final Map<Object, Position> refPosition = new HashMap<Object, Position>();
084    private final List<Map<?, ?>> allEdges;
085    private final TreeMap<Long, List<Map<?, ?>>> edgesByStatement = new TreeMap<Long, List<Map<?, ?>>>();
086    private final List<Long> statements;
087
088    public FoldKernel(DocumentIndex index) {
089        this(index, DEFAULT_MAX_STATES, DEFAULT_MAX_WORLDS);
090    }
091
092    public FoldKernel(DocumentIndex index, int maxStates, int maxWorlds) {
093        this.index = index;
094        this.maxStates = maxStates;
095        this.maxWorlds = maxWorlds;
096        Map<?, ?> graph = index.graph();
097        defaultCompleteness = graph.get("completeness");
098        for (Map<?, ?> entry : items(graph.get("statementCompleteness"))) {
099            statementCompleteness.put(intRef(entry.get("statementRef")), entry.get("completeness"));
100        }
101        for (Map<?, ?> cset : items(graph.get("candidateSets"))) {
102            Long ref = intRef(cset.get("ref"));
103            if (ref != null) {
104                graphCsets.put(ref, cset);
105            }
106        }
107        allEdges = index.all("microEdge");
108        for (Map<?, ?> edge : allEdges) {
109            for (Choice choice : choices(edge)) {
110                Set<String> sides = refSide.get(choice.ref);
111                if (sides == null) {
112                    sides = new TreeSet<String>();
113                    refSide.put(choice.ref, sides);
114                }
115                sides.add(choice.side);
116                Position position = Position.of(edge);
117                Position known = refPosition.get(choice.ref);
118                if (known == null || position.compareTo(known) < 0) {
119                    refPosition.put(choice.ref, position);
120                }
121            }
122            Long statement = intRef(edge.get("statementRef"));
123            if (statement != null) {
124                List<Map<?, ?>> list = edgesByStatement.get(statement);
125                if (list == null) {
126                    list = new ArrayList<Map<?, ?>>();
127                    edgesByStatement.put(statement, list);
128                }
129                list.add(edge);
130            }
131        }
132        TreeSet<Long> all = new TreeSet<Long>(edgesByStatement.keySet());
133        for (Map<?, ?> statement : index.all("statement")) {
134            all.add(intRef(statement.get("ref")));
135        }
136        statements = Collections.unmodifiableList(new ArrayList<Long>(all));
137    }
138
139    // ---------------------------------------------------------------------------------------------
140    // Values compared the way the reference compares them: integers by value, everything else by equals.
141    // ---------------------------------------------------------------------------------------------
142
143    static Object norm(Object value) {
144        Long ref = intRef(value);
145        return ref != null ? ref : value;
146    }
147
148    static boolean same(Object a, Object b) {
149        Object x = norm(a);
150        Object y = norm(b);
151        return x == null ? y == null : x.equals(y);
152    }
153
154    static int hash(Object value) {
155        Object x = norm(value);
156        return x == null ? 0 : x.hashCode();
157    }
158
159    /** Orders refs (and null, the key of a reference without a candidate set) the way the reference sorts ints. */
160    static final Comparator<Object> REF_ORDER = new Comparator<Object>() {
161        @Override
162        public int compare(Object a, Object b) {
163            Object x = norm(a);
164            Object y = norm(b);
165            if (x == null || y == null) {
166                return x == null ? (y == null ? 0 : -1) : 1;
167            }
168            if (x instanceof Long && y instanceof Long) {
169                return ((Long) x).compareTo((Long) y);
170            }
171            if (x instanceof Long || y instanceof Long) {
172                return x instanceof Long ? -1 : 1;
173            }
174            return compareCodePoints(String.valueOf(x), String.valueOf(y));
175        }
176    };
177
178    /** Unsigned UTF-8 byte order, which is code point order. */
179    static int compareCodePoints(String a, String b) {
180        int i = 0;
181        int j = 0;
182        while (i < a.length() && j < b.length()) {
183            int x = a.codePointAt(i);
184            int y = b.codePointAt(j);
185            if (x != y) {
186                return x < y ? -1 : 1;
187            }
188            i += Character.charCount(x);
189            j += Character.charCount(y);
190        }
191        return (a.length() - i) == 0 ? ((b.length() - j) == 0 ? 0 : -1) : 1;
192    }
193
194    static Set<Set<StatementFold.Pair>> frozenOutcomes(Set<Set<StatementFold.Pair>> outcomes) {
195        Set<Set<StatementFold.Pair>> out = new LinkedHashSet<Set<StatementFold.Pair>>();
196        for (Set<StatementFold.Pair> outcome : outcomes) {
197            out.add(Collections.unmodifiableSet(new LinkedHashSet<StatementFold.Pair>(outcome)));
198        }
199        return Collections.unmodifiableSet(out);
200    }
201
202    // ---------------------------------------------------------------------------------------------
203    // Classification (Section 3)
204    // ---------------------------------------------------------------------------------------------
205
206    public DocumentIndex getIndex() {
207        return index;
208    }
209
210    /** Every statement of the document and every statement a micro-edge names, in ref order. */
211    public List<Long> getStatements() {
212        return statements;
213    }
214
215    /** The micro-edges of one statement, in ref order. */
216    public List<Map<?, ?>> edgesOf(long statement) {
217        List<Map<?, ?>> edges = edgesByStatement.get(statement);
218        return edges == null ? Collections.<Map<?, ?>>emptyList() : Collections.unmodifiableList(edges);
219    }
220
221    /** The ExplainGraph completeness of a statement: its {@code statementCompleteness} entry or the graph's. */
222    public Object completeness(long statement) {
223        return statementCompleteness.containsKey(statement) ? statementCompleteness.get(statement)
224                : defaultCompleteness;
225    }
226
227    public boolean kept(Object ref) {
228        return index.get("endpoint", ref) != null;
229    }
230
231    Map<?, ?> node(Object ref) {
232        return index.get("node", ref);
233    }
234
235    public boolean folded(Object ref) {
236        Map<?, ?> node = node(ref);
237        return node != null && !"TERMINAL".equals(node.get("kind"));
238    }
239
240    boolean terminal(Object ref) {
241        Map<?, ?> node = node(ref);
242        return node != null && "TERMINAL".equals(node.get("kind"));
243    }
244
245    /** True when the ref names a candidate set of the ExplainGraph (the source of an UNRESOLVED reference). */
246    public boolean graphCset(Object ref) {
247        Long key = intRef(ref);
248        return key != null && graphCsets.containsKey(key);
249    }
250
251    /**
252     * The kept field-class endpoints (Section 3.1, 3.2): fields, declared variables and routine parameters, which have
253     * a value a later statement can read; a write targets one of them.
254     */
255    static final java.util.List<String> FIELD_CLASS_KINDS = java.util.Arrays.asList("FIELD", "VARIABLE", "PARAMETER");
256
257    boolean isField(Object ref) {
258        Map<?, ?> endpoint = index.get("endpoint", ref);
259        return endpoint != null && FIELD_CLASS_KINDS.contains(endpoint.get("kind"));
260    }
261
262    private String endClass(Object ref) {
263        Map<?, ?> endpoint = index.get("endpoint", ref);
264        if (endpoint != null) {
265            Object kind = endpoint.get("kind");
266            return "ROW_SET".equals(kind) ? "ROWSET" : FIELD_CLASS_KINDS.contains(kind) ? "FIELD" : null;
267        }
268        Map<?, ?> node = node(ref);
269        if (node != null) {
270            Object kind = node.get("kind");
271            if ("ROW_SET".equals(kind)) {
272                return "ROWSET";
273            }
274            return CONTAINER_NODE_KINDS.contains(kind) ? null : "FIELD";
275        }
276        return graphCset(ref) ? "FIELD" : null;
277    }
278
279    /** True when the statement lies in a derived source unit whose text is only observed (Section 7.3). */
280    public boolean observed(long statement) {
281        Map<?, ?> record = index.get("statement", statement);
282        Map<?, ?> unit = record == null ? null : index.get("sourceUnit", record.get("sourceUnitRef"));
283        return unit != null && "OBSERVED".equals(unit.get("derivedTextStatus"));
284    }
285
286    // ---------------------------------------------------------------------------------------------
287    // Ambiguous references (Sections 6 and 7.1)
288    // ---------------------------------------------------------------------------------------------
289
290    static final class Choice {
291        final Object ref;
292        final Object alternative;
293        final String side;
294
295        Choice(Object ref, Object alternative, String side) {
296            this.ref = norm(ref);
297            this.alternative = alternative == NOT_LISTED || alternative == UNPAIRED ? alternative : norm(alternative);
298            this.side = side;
299        }
300    }
301
302    static boolean ambiguous(Object binding) {
303        if (!(binding instanceof Map<?, ?>)) {
304            return false;
305        }
306        Object status = ((Map<?, ?>) binding).get("status");
307        return "CANDIDATE".equals(status) || "UNRESOLVED".equals(status);
308    }
309
310    /**
311     * (reference, alternative, side) for each ambiguous binding of a micro-edge, source side first. A walk keeps the
312     * references as written (they make its group, Section 7.1, item 4); the worlds are those of their deciding
313     * references (decidedSigma).
314     */
315    List<Choice> choices(Map<?, ?> edge) {
316        return rawChoices(edge);
317    }
318
319    /** Whether a reference is decided by another (its candidate set has decidedBy, Section 4.3). */
320    boolean isDecided(Object ref) {
321        Map<?, ?> cset = ref instanceof Long ? graphCsets.get(ref) : null;
322        return cset != null && cset.get("decidedBy") instanceof Map<?, ?>;
323    }
324
325    /**
326     * Sigma.extend, and two references decided by one reference take one of its alternatives (Section 7.1, item 1):
327     * null when they take two.
328     */
329    Sigma extend(Sigma sigma, List<Choice> choices) {
330        Sigma out = sigma.extend(choices);
331        if (out == null || choices.isEmpty()) {
332            return out;
333        }
334        boolean decided = false;
335        for (Object r : out.refs) {
336            decided |= isDecided(r);
337        }
338        if (!decided) {
339            return out;
340        }
341        // every choice as worlds see it, the deciding reference's own included (Codex 13a #4)
342        Map<Object, Object> taken = new HashMap<Object, Object>();
343        for (int i = 0; i < out.refs.length; i++) {
344            Choice d = decide(new Choice(out.refs[i], out.alternatives[i], null));
345            if (taken.containsKey(d.ref) && !sameAlternative(taken.get(d.ref), d.alternative)) {
346                return null;
347            }
348            taken.put(d.ref, d.alternative);
349        }
350        return out;
351    }
352
353    /** The choices of a walk as worlds see them: a decided reference's as its deciding reference's (Section 7.1). */
354    Sigma decidedSigma(Sigma sigma) {
355        boolean decided = false;
356        for (Object r : sigma.refs) {
357            decided |= isDecided(r);
358        }
359        if (!decided) {
360            return sigma;
361        }
362        TreeMap<Object, Object> chosen = new TreeMap<Object, Object>(REF_ORDER);
363        for (int i = 0; i < sigma.refs.length; i++) {
364            Choice d = decide(new Choice(sigma.refs[i], sigma.alternatives[i], null));
365            if (!chosen.containsKey(d.ref)) {
366                chosen.put(d.ref, d.alternative);
367            }
368        }
369        return new Sigma(chosen.keySet().toArray(), chosen.values().toArray());
370    }
371
372    /** A decided reference's alternative of an edge that its alignment pairs with no alternative. */
373    static final Object UNPAIRED = new Object() {
374        @Override
375        public String toString() {
376            return "UNPAIRED";
377        }
378    };
379
380    /**
381     * The choice of the deciding reference that a decided reference's choice is: the alternative its alignment pairs
382     * with the candidate (UNPAIRED when none, which the shape check reports). Other choices are returned as they are.
383     */
384    Choice decide(Choice choice) {
385        Map<?, ?> cset = choice.ref instanceof Long ? graphCsets.get(choice.ref) : null;
386        Object decided = cset == null ? null : cset.get("decidedBy");
387        if (!(decided instanceof Map<?, ?>)) {
388            return choice;
389        }
390        Object deciding = ((Map<?, ?>) decided).get("candidateSetRef");
391        if (choice.alternative != NOT_LISTED) {
392            for (Map<?, ?> pair : items(((Map<?, ?>) decided).get("alignment"))) {
393                if (sameAlternative(norm(pair.get("candidateRef")), choice.alternative)) {
394                    return new Choice(deciding, pair.get("alternativeRef"), choice.side);
395                }
396            }
397        }
398        return new Choice(deciding, UNPAIRED, choice.side);
399    }
400
401    /** The candidate set a binding names, as the reference the fold chooses for: its deciding set when it is decided. */
402    Object choiceRef(Object candidateSetRef) {
403        Long key = intRef(candidateSetRef);
404        Map<?, ?> cset = key == null ? null : graphCsets.get(key);
405        Object decided = cset == null ? null : cset.get("decidedBy");
406        return decided instanceof Map<?, ?> ? norm(((Map<?, ?>) decided).get("candidateSetRef")) : norm(candidateSetRef);
407    }
408
409    /** (reference, alternative, side) for each ambiguous binding of a micro-edge as written, source side first. */
410    List<Choice> rawChoices(Map<?, ?> edge) {
411        List<Choice> out = new ArrayList<Choice>(2);
412        Object binding = edge.get("sourceBinding");
413        if (ambiguous(binding)) {
414            Object source = edge.get("sourceRef");
415            out.add(new Choice(((Map<?, ?>) binding).get("candidateSetRef"), graphCset(source) ? NOT_LISTED : source,
416                    SOURCE));
417        }
418        binding = edge.get("targetBinding");
419        if (ambiguous(binding)) {
420            out.add(new Choice(((Map<?, ?>) binding).get("candidateSetRef"), edge.get("targetRef"), TARGET));
421        }
422        return out;
423    }
424
425    /** The members of a candidate set of the graph. */
426    List<Object> members(Object ref) {
427        List<Object> out = alternatives(ref);
428        out.remove(NOT_LISTED);
429        return out;
430    }
431
432    /** The alternatives of a reference: its members, and NOT_LISTED unless its membership is COMPLETE. */
433    List<Object> alternatives(Object ref) {
434        Long key = intRef(ref);
435        Map<?, ?> cset = key == null ? null : graphCsets.get(key);
436        List<Object> out = new ArrayList<Object>();
437        if (cset != null && cset.get("candidateRefs") instanceof List<?>) {
438            for (Object member : (List<?>) cset.get("candidateRefs")) {
439                if (intRef(member) != null) {
440                    out.add(intRef(member));
441                }
442            }
443        }
444        if (cset == null || !"COMPLETE".equals(cset.get("membership"))) {
445            out.add(NOT_LISTED);
446        }
447        return out;
448    }
449
450    boolean referenceComplete(Object ref) {
451        Long key = intRef(ref);
452        Map<?, ?> cset = key == null ? null : graphCsets.get(key);
453        return cset != null && "COMPLETE".equals(cset.get("membership"));
454    }
455
456    Set<String> sidesOf(Object ref) {
457        Set<String> sides = refSide.get(norm(ref));
458        return sides == null ? Collections.<String>emptySet() : sides;
459    }
460
461    /** The choices of a walk: one alternative per reference, as an immutable sorted map. */
462    static final class Sigma {
463        static final Sigma EMPTY = new Sigma(new Object[0], new Object[0]);
464        final Object[] refs;
465        final Object[] alternatives;
466        private final int hash;
467
468        private Sigma(Object[] refs, Object[] alternatives) {
469            this.refs = refs;
470            this.alternatives = alternatives;
471            int h = 1;
472            for (int i = 0; i < refs.length; i++) {
473                h = h * 31 + hash(refs[i]);
474                h = h * 31 + (alternatives[i] == NOT_LISTED ? 7 : hash(alternatives[i]));
475            }
476            this.hash = h;
477        }
478
479        boolean isEmpty() {
480            return refs.length == 0;
481        }
482
483        int indexOf(Object ref) {
484            for (int i = 0; i < refs.length; i++) {
485                if (same(refs[i], ref)) {
486                    return i;
487                }
488            }
489            return -1;
490        }
491
492        /** The choices extended by one micro-edge's, or null when the edge takes another alternative. */
493        Sigma extend(List<Choice> choices) {
494            TreeMap<Object, Object> chosen = null;
495            for (Choice choice : choices) {
496                boolean has;
497                Object current;
498                if (chosen != null) {
499                    has = chosen.containsKey(choice.ref);
500                    current = chosen.get(choice.ref);
501                } else {
502                    int at = indexOf(choice.ref);
503                    has = at >= 0;
504                    current = has ? alternatives[at] : null;
505                }
506                if (has) {
507                    if (!sameAlternative(current, choice.alternative)) {
508                        return null;
509                    }
510                    continue;
511                }
512                if (chosen == null) {
513                    chosen = new TreeMap<Object, Object>(REF_ORDER);
514                    for (int i = 0; i < refs.length; i++) {
515                        chosen.put(refs[i], alternatives[i]);
516                    }
517                }
518                chosen.put(choice.ref, choice.alternative);
519            }
520            return chosen == null ? this : new Sigma(chosen.keySet().toArray(), chosen.values().toArray());
521        }
522
523        @Override
524        public boolean equals(Object other) {
525            if (!(other instanceof Sigma)) {
526                return false;
527            }
528            Sigma sigma = (Sigma) other;
529            if (sigma.hash != hash || sigma.refs.length != refs.length) {
530                return false;
531            }
532            for (int i = 0; i < refs.length; i++) {
533                if (!same(refs[i], sigma.refs[i]) || !sameAlternative(alternatives[i], sigma.alternatives[i])) {
534                    return false;
535                }
536            }
537            return true;
538        }
539
540        @Override
541        public int hashCode() {
542            return hash;
543        }
544    }
545
546    static boolean sameAlternative(Object a, Object b) {
547        if (a == NOT_LISTED || b == NOT_LISTED) {
548            return a == b;
549        }
550        return same(a, b);
551    }
552
553    /**
554     * Section 7.1, item 2 ({@code COVERS} of Section 9): the choice sets hold in every world. Splits the worlds on
555     * the smallest reference at a time, with an explicit stack.
556     */
557    boolean covers(Set<Sigma> sigmas) {
558        final class Frame {
559            final List<Sigma> sets;
560            Object ref;
561            List<Object> alternatives;
562            int next;
563
564            Frame(List<Sigma> sets) {
565                this.sets = sets;
566            }
567        }
568        Deque<Frame> stack = new ArrayDeque<Frame>();
569        List<Sigma> worlds = new ArrayList<Sigma>();
570        for (Sigma sigma : sigmas) {
571            worlds.add(decidedSigma(sigma)); // a decided reference holds in its deciding reference's worlds
572        }
573        stack.push(new Frame(worlds));
574        Boolean returned = null;
575        long work = 0;
576        while (!stack.isEmpty()) {
577            Frame frame = stack.peek();
578            if (returned != null) {
579                if (!returned) {
580                    stack.pop();
581                    continue;
582                }
583                returned = null;
584            }
585            if (frame.alternatives == null) {
586                if (++work > maxWorlds) {
587                    throw new FoldLimitException("deciding a Fact needs more than " + maxWorlds + " worlds");
588                }
589                boolean anyEmpty = false;
590                Object smallest = null;
591                boolean first = true;
592                for (Sigma sigma : frame.sets) {
593                    if (sigma.isEmpty()) {
594                        anyEmpty = true;
595                        break;
596                    }
597                    if (first || REF_ORDER.compare(sigma.refs[0], smallest) < 0) {
598                        smallest = sigma.refs[0];
599                        first = false;
600                    }
601                }
602                if (anyEmpty || frame.sets.isEmpty()) {
603                    returned = anyEmpty;
604                    stack.pop();
605                    continue;
606                }
607                frame.ref = smallest;
608                frame.alternatives = alternatives(smallest);
609            }
610            if (frame.next < frame.alternatives.size()) {
611                Object alternative = frame.alternatives.get(frame.next++);
612                List<Sigma> rest = new ArrayList<Sigma>();
613                for (Sigma sigma : frame.sets) {
614                    int at = sigma.indexOf(frame.ref);
615                    if (at < 0) {
616                        rest.add(sigma);
617                    } else if (sameAlternative(sigma.alternatives[at], alternative)) {
618                        rest.add(without(sigma, at));
619                    }
620                }
621                stack.push(new Frame(rest));
622                continue;
623            }
624            returned = Boolean.TRUE;
625            stack.pop();
626        }
627        return returned != null && returned;
628    }
629
630    private static Sigma without(Sigma sigma, int at) {
631        Object[] refs = new Object[sigma.refs.length - 1];
632        Object[] alternatives = new Object[refs.length];
633        for (int i = 0, j = 0; i < sigma.refs.length; i++) {
634            if (i != at) {
635                refs[j] = sigma.refs[i];
636                alternatives[j++] = sigma.alternatives[i];
637            }
638        }
639        return refs.length == 0 ? Sigma.EMPTY : new Sigma(refs, alternatives);
640    }
641
642    // ---------------------------------------------------------------------------------------------
643    // Section 4.1: micro-edge shapes
644    // ---------------------------------------------------------------------------------------------
645
646    /** {@code SEMANTIC_FOLD_MALFORMED_EDGE} for every micro-edge of the document whose shape is not conformant. */
647    public List<Finding> malformedEdges() {
648        List<Finding> findings = new ArrayList<Finding>();
649        for (Map<?, ?> edge : allEdges) {
650            String problem = shapeProblem(edge);
651            if (problem != null) {
652                findings.add(new Finding("SEMANTIC_FOLD_MALFORMED_EDGE", "explainGraph.microEdges ref " + edge.get("ref"),
653                        problem));
654            }
655        }
656        return findings;
657    }
658
659    String shapeProblem(Map<?, ?> edge) {
660        Object source = edge.get("sourceRef");
661        Object target = edge.get("targetRef");
662        String sourceClass = endClass(source);
663        String targetClass = endClass(target);
664        Object statement = edge.get("statementRef");
665        String problem = null;
666        if (sourceClass == null || targetClass == null) {
667            problem = "an end is neither field-class nor row-set-class";
668        } else if ("VALUE".equals(edge.get("kind")) && ("ROWSET".equals(sourceClass) || "ROWSET".equals(targetClass))) {
669            problem = "a VALUE micro-edge touches a row set";
670        } else if ("ROW".equals(edge.get("kind")) && "FIELD".equals(sourceClass) && "FIELD".equals(targetClass)
671                && "ROW_PROPAGATION".equals(edge.get("role"))) {
672            problem = "a field-to-field ROW micro-edge with role ROW_PROPAGATION";
673        } else if (terminal(target)) {
674            problem = "a micro-edge into a TERMINAL node";
675        } else if (graphCset(source) && !(edge.get("sourceBinding") instanceof Map<?, ?>
676                && "UNRESOLVED".equals(((Map<?, ?>) edge.get("sourceBinding")).get("status")))) {
677            problem = "a micro-edge from a candidate set without an UNRESOLVED source binding";
678        } else {
679            for (Object end : new Object[] {source, target}) {
680                Map<?, ?> node = node(end);
681                if (node != null && !same(node.get("statementRef"), statement)) {
682                    problem = "an end belongs to another statement";
683                }
684            }
685            for (Choice choice : rawChoices(edge)) {
686                if (!(choice.ref instanceof Long) || !graphCsets.containsKey(choice.ref)) {
687                    problem = "an ambiguous binding names no candidate set of the graph";
688                } else if (choice.alternative != NOT_LISTED && !containsAlternative(members(choice.ref),
689                        choice.alternative)) {
690                    problem = "an ambiguous binding's edge reads or writes no member of its candidate set";
691                } else {
692                    Choice decided = decide(choice);
693                    if (decided.alternative == UNPAIRED || !(decided.ref instanceof Long)
694                            || !graphCsets.containsKey(decided.ref)) {
695                        problem = "a decided reference's candidate has no alternative of a deciding set";
696                    }
697                }
698            }
699        }
700        return problem;
701    }
702
703    private static boolean containsAlternative(List<Object> alternatives, Object alternative) {
704        for (Object a : alternatives) {
705            if (sameAlternative(a, alternative)) {
706                return true;
707            }
708        }
709        return false;
710    }
711
712    // ---------------------------------------------------------------------------------------------
713    // Section 9: exploration
714    // ---------------------------------------------------------------------------------------------
715
716    static FoldLabel edgeLabel(Map<?, ?> edge) {
717        return FoldLabel.of(edge.get("kind"), edge.get("role"));
718    }
719
720    /** Section 7.2, item 1, for one micro-edge; observed is Section 7.3, item 2. */
721    static boolean assumed(Map<?, ?> edge, boolean observed) {
722        if (observed) {
723            return true;
724        }
725        for (String member : new String[] {"sourceBinding", "targetBinding"}) {
726            Object binding = edge.get(member);
727            if (binding instanceof Map<?, ?> && ASSUMED.equals(((Map<?, ?>) binding).get("status"))) {
728                return true;
729            }
730        }
731        return ASSUMED.equals(edge.get("proofStatus"));
732    }
733
734    private static final class State {
735        final Object node;
736        final FoldLabel label;
737        final Sigma sigma;
738        final boolean assumed;
739        private final int hash;
740
741        State(Object node, FoldLabel label, Sigma sigma, boolean assumed) {
742            this.node = norm(node);
743            this.label = label;
744            this.sigma = sigma;
745            this.assumed = assumed;
746            this.hash = ((hash(this.node) * 31 + label.hashCode()) * 31 + sigma.hashCode()) * 2 + (assumed ? 1 : 0);
747        }
748
749        @Override
750        public boolean equals(Object other) {
751            if (!(other instanceof State)) {
752                return false;
753            }
754            State state = (State) other;
755            return state.hash == hash && state.assumed == assumed && same(node, state.node)
756                    && label.equals(state.label) && sigma.equals(state.sigma);
757        }
758
759        @Override
760        public int hashCode() {
761            return hash;
762        }
763    }
764
765    /** One fold walk, or several with the same first edge, target, label, choices and proof. */
766    static final class Walk {
767        final Map<?, ?> first;
768        final Object source;
769        final Object target;
770        final FoldLabel label;
771        final Sigma sigma;
772        final boolean assumed;
773        final Set<Long> support = new TreeSet<Long>();
774        final List<Map<?, ?>> lastEdges = new ArrayList<Map<?, ?>>();
775        /** Fold walks (micro-edge refs in walk order) that together cover {@link #support}. */
776        final List<List<Long>> covering = new ArrayList<List<Long>>();
777
778        Walk(Map<?, ?> first, State end) {
779            this.first = first;
780            this.source = norm(first.get("sourceRef"));
781            this.target = end.node;
782            this.label = end.label;
783            this.sigma = end.sigma;
784            this.assumed = end.assumed;
785        }
786    }
787
788    private static final class Predecessor {
789        final State state;
790        final Long edge;
791
792        Predecessor(State state, Long edge) {
793            this.state = state;
794            this.edge = edge;
795        }
796    }
797
798    /** Every fold walk of a statement, in the order the reference emits them, merged as {@link Walk} says. */
799    List<Walk> walks(long statement) {
800        List<Map<?, ?>> edges = edgesOf(statement);
801        boolean observed = observed(statement);
802        Map<Object, List<Map<?, ?>>> out = new HashMap<Object, List<Map<?, ?>>>();
803        for (Map<?, ?> edge : edges) {
804            Object source = norm(edge.get("sourceRef"));
805            List<Map<?, ?>> list = out.get(source);
806            if (list == null) {
807                list = new ArrayList<Map<?, ?>>();
808                out.put(source, list);
809            }
810            list.add(edge);
811        }
812        List<Walk> walks = new ArrayList<Walk>();
813        long[] states = {0};
814        for (Map<?, ?> first : edges) {
815            Object source = first.get("sourceRef");
816            if (!(kept(source) || graphCset(source))) {
817                continue;
818            }
819            Sigma sigma0 = extend(Sigma.EMPTY, choices(first));
820            if (sigma0 == null) {
821                continue;
822            }
823            State x0 = new State(first.get("targetRef"), edgeLabel(first), sigma0, assumed(first, observed));
824            count(states, statement);
825            Long firstRef = intRef(first.get("ref"));
826            LinkedHashMap<State, Walk> emitted = new LinkedHashMap<State, Walk>();
827            Map<State, List<State>> befores = new HashMap<State, List<State>>();
828            Map<State, List<Predecessor>> pred = new HashMap<State, List<Predecessor>>();
829            if (kept(x0.node)) {
830                Walk walk = new Walk(first, x0);
831                walk.lastEdges.add(first);
832                walk.support.add(firstRef);
833                walk.covering.add(Collections.singletonList(firstRef));
834                emitted.put(x0, walk);
835            } else {
836                List<Predecessor> start = new ArrayList<Predecessor>(1);
837                start.add(new Predecessor(null, firstRef));
838                pred.put(x0, start);
839                Set<State> visited = new HashSet<State>();
840                visited.add(x0);
841                ArrayDeque<State> queue = new ArrayDeque<State>();
842                queue.add(x0);
843                while (!queue.isEmpty()) {
844                    State state = queue.poll();
845                    List<Map<?, ?>> next = out.get(state.node);
846                    if (next == null) {
847                        continue;
848                    }
849                    for (Map<?, ?> edge : next) {
850                        Sigma chosen = extend(state.sigma, choices(edge));
851                        if (chosen == null) {
852                            continue;
853                        }
854                        State y = new State(edge.get("targetRef"), state.label.compose(edgeLabel(edge)), chosen,
855                                state.assumed || assumed(edge, observed));
856                        Long edgeRef = intRef(edge.get("ref"));
857                        if (kept(y.node)) {
858                            Walk walk = emitted.get(y);
859                            if (walk == null) {
860                                count(states, statement);
861                                walk = new Walk(first, y);
862                                emitted.put(y, walk);
863                                befores.put(y, new ArrayList<State>());
864                            }
865                            walk.lastEdges.add(edge);
866                            walk.support.add(edgeRef);
867                            befores.get(y).add(state);
868                        } else {
869                            List<Predecessor> list = pred.get(y);
870                            if (list == null) {
871                                list = new ArrayList<Predecessor>(2);
872                                pred.put(y, list);
873                            }
874                            list.add(new Predecessor(state, edgeRef));
875                            if (visited.add(y)) {
876                                count(states, statement);
877                                queue.add(y);
878                            }
879                        }
880                    }
881                }
882            }
883            for (Map.Entry<State, Walk> entry : emitted.entrySet()) {
884                Walk walk = entry.getValue();
885                List<State> from = befores.get(entry.getKey());
886                if (from != null) {
887                    // insertion order, so that the covering walks are the same on every JVM
888                    Set<State> reaching = new LinkedHashSet<State>(from);
889                    Deque<State> stack = new ArrayDeque<State>(from);
890                    while (!stack.isEmpty()) {
891                        State s = stack.pop();
892                        List<Predecessor> list = pred.get(s);
893                        if (list == null) {
894                            continue;
895                        }
896                        for (Predecessor p : list) {
897                            walk.support.add(p.edge);
898                            if (p.state != null && reaching.add(p.state)) {
899                                stack.push(p.state);
900                            }
901                        }
902                    }
903                    cover(walk, x0, from, reaching, pred);
904                }
905                walks.add(walk);
906            }
907        }
908        return walks;
909    }
910
911    private void count(long[] states, long statement) {
912        if (++states[0] > maxStates) {
913            throw new FoldLimitException("statement ref " + statement + " needs more than " + maxStates
914                    + " exploration states");
915        }
916    }
917
918    /**
919     * Section 8.2, items 3 and 4: fold walks that each yield the record and that together contain every micro-edge
920     * of its support subgraph. Each walk is a path of the explored state graph from the first edge to an emission of
921     * the record, so it holds in the record's worlds; one walk is added for each micro-edge not yet covered.
922     */
923    private static void cover(Walk walk, State x0, List<State> befores, Set<State> reaching,
924                              Map<State, List<Predecessor>> pred) {
925        // the emissions: the state before the last edge, and that edge, in the order they were found
926        List<Object[]> emissions = new ArrayList<Object[]>();
927        for (int k = 0; k < befores.size(); k++) {
928            emissions.add(new Object[] {befores.get(k), intRef(walk.lastEdges.get(k).get("ref"))});
929        }
930        Map<State, List<Object[]>> successors = new LinkedHashMap<State, List<Object[]>>();
931        for (State s : reaching) {
932            List<Predecessor> list = pred.get(s);
933            if (list == null) {
934                continue;
935            }
936            for (Predecessor p : list) {
937                if (p.state != null && reaching.contains(p.state)) {
938                    List<Object[]> next = successors.get(p.state);
939                    if (next == null) {
940                        next = new ArrayList<Object[]>();
941                        successors.put(p.state, next);
942                    }
943                    next.add(new Object[] {s, p.edge});
944                }
945            }
946        }
947        Set<Long> uncovered = new TreeSet<Long>(walk.support);
948        while (!uncovered.isEmpty()) {
949            Long edge = uncovered.iterator().next();
950            List<Long> found = null;
951            for (Object[] emission : emissions) {
952                if (edge.equals(emission[1])) {
953                    found = concat(prefix(x0, (State) emission[0], pred, reaching), (Long) emission[1]);
954                    break;
955                }
956            }
957            for (Iterator<State> it = reaching.iterator(); found == null && it.hasNext(); ) {
958                State s = it.next();
959                for (Predecessor p : pred.get(s)) {
960                    if (!edge.equals(p.edge)) {
961                        continue;
962                    }
963                    List<Long> head = p.state == null ? new ArrayList<Long>() : prefix(x0, p.state, pred, reaching);
964                    head.add(p.edge);
965                    found = suffix(head, s, successors, emissions);
966                    break;
967                }
968            }
969            if (found == null) {
970                throw new IllegalStateException("micro-edge " + edge + " of a support subgraph lies on no walk");
971            }
972            uncovered.removeAll(found);
973            walk.covering.add(Collections.unmodifiableList(found));
974        }
975    }
976
977    private static List<Long> concat(List<Long> head, Long edge) {
978        head.add(edge);
979        return head;
980    }
981
982    /** The micro-edges of a shortest walk from the first edge to the given state, the first edge included. */
983    private static List<Long> prefix(State x0, State to, Map<State, List<Predecessor>> pred, Set<State> reaching) {
984        Map<State, Predecessor> parent = new HashMap<State, Predecessor>();
985        Deque<State> queue = new ArrayDeque<State>();
986        queue.add(to);
987        parent.put(to, null);
988        State start = null;
989        while (!queue.isEmpty()) {
990            State s = queue.poll();
991            for (Predecessor p : pred.get(s)) {
992                if (p.state == null) {
993                    start = s;
994                    break;
995                }
996                if (!parent.containsKey(p.state)) {
997                    parent.put(p.state, new Predecessor(s, p.edge));
998                    queue.add(p.state);
999                }
1000            }
1001            if (start != null) {
1002                break;
1003            }
1004        }
1005        // start is x0; follow the parents (each names the state after it and the edge between) back to `to`
1006        List<Long> out = new ArrayList<Long>();
1007        out.add(pred.get(start).get(0).edge);
1008        for (State s = start; parent.get(s) != null; s = parent.get(s).state) {
1009            out.add(parent.get(s).edge);
1010        }
1011        return out;
1012    }
1013
1014    /** The walk extended from a state to an emission along the explored successors. */
1015    private static List<Long> suffix(List<Long> head, State from, Map<State, List<Object[]>> successors,
1016                                     List<Object[]> emissions) {
1017        Map<State, Object[]> parent = new HashMap<State, Object[]>();
1018        Deque<State> queue = new ArrayDeque<State>();
1019        queue.add(from);
1020        parent.put(from, null);
1021        while (!queue.isEmpty()) {
1022            State s = queue.poll();
1023            for (Object[] emission : emissions) {
1024                if (emission[0].equals(s)) {
1025                    List<Long> tail = new ArrayList<Long>();
1026                    for (State t = s; parent.get(t) != null; t = (State) parent.get(t)[0]) {
1027                        tail.add(0, (Long) parent.get(t)[1]);
1028                    }
1029                    List<Long> out = new ArrayList<Long>(head);
1030                    out.addAll(tail);
1031                    out.add((Long) emission[1]);
1032                    return out;
1033                }
1034            }
1035            List<Object[]> next = successors.get(s);
1036            if (next == null) {
1037                continue;
1038            }
1039            for (Object[] step : next) {
1040                State t = (State) step[0];
1041                if (!parent.containsKey(t)) {
1042                    parent.put(t, new Object[] {s, step[1]});
1043                    queue.add(t);
1044                }
1045            }
1046        }
1047        return null;
1048    }
1049
1050    // ---------------------------------------------------------------------------------------------
1051    // Section 7.1: Facts and the remainder
1052    // ---------------------------------------------------------------------------------------------
1053
1054    private static final class PropositionKey {
1055        final Object source;
1056        final Object target;
1057        final FoldLabel label;
1058
1059        PropositionKey(Object source, Object target, FoldLabel label) {
1060            this.source = source;
1061            this.target = target;
1062            this.label = label;
1063        }
1064
1065        @Override
1066        public boolean equals(Object other) {
1067            if (!(other instanceof PropositionKey)) {
1068                return false;
1069            }
1070            PropositionKey key = (PropositionKey) other;
1071            return same(source, key.source) && same(target, key.target) && label.equals(key.label);
1072        }
1073
1074        @Override
1075        public int hashCode() {
1076            return (hash(source) * 31 + hash(target)) * 31 + label.hashCode();
1077        }
1078    }
1079
1080    /** One Fact Occurrence before its ordinal is known: (source, target, label, path, offset). */
1081    private static final class FactKey {
1082        final PropositionKey proposition;
1083        final Object path;
1084        final Long offset;
1085
1086        FactKey(PropositionKey proposition, Object path, Long offset) {
1087            this.proposition = proposition;
1088            this.path = path;
1089            this.offset = offset;
1090        }
1091
1092        @Override
1093        public boolean equals(Object other) {
1094            if (!(other instanceof FactKey)) {
1095                return false;
1096            }
1097            FactKey key = (FactKey) other;
1098            return proposition.equals(key.proposition) && same(path, key.path) && same(offset, key.offset);
1099        }
1100
1101        @Override
1102        public int hashCode() {
1103            return (proposition.hashCode() * 31 + hash(path)) * 31 + hash(offset);
1104        }
1105    }
1106
1107    private static final class FactEntry {
1108        boolean proven;
1109        final Set<Long> support = new TreeSet<Long>();
1110        final List<Walk> walks = new ArrayList<Walk>();
1111    }
1112
1113    static Long offsetOf(Object position) {
1114        return position instanceof Map<?, ?> ? intRef(((Map<?, ?>) position).get("utf8ByteOffset0")) : null;
1115    }
1116
1117    /** Folds one statement (Section 9). */
1118    public StatementFold fold(long statement) {
1119        List<Walk> walks = walks(statement);
1120        LinkedHashMap<PropositionKey, List<Walk>> propositions = new LinkedHashMap<PropositionKey, List<Walk>>();
1121        for (Walk walk : walks) {
1122            PropositionKey key = new PropositionKey(walk.source, walk.target, walk.label);
1123            List<Walk> list = propositions.get(key);
1124            if (list == null) {
1125                list = new ArrayList<Walk>();
1126                propositions.put(key, list);
1127            }
1128            list.add(walk);
1129        }
1130        LinkedHashMap<FactKey, FactEntry> facts = new LinkedHashMap<FactKey, FactEntry>();
1131        List<Walk> remainder = new ArrayList<Walk>();
1132        for (Map.Entry<PropositionKey, List<Walk>> entry : propositions.entrySet()) {
1133            PropositionKey proposition = entry.getKey();
1134            Set<Sigma> sigmas = new LinkedHashSet<Sigma>();
1135            for (Walk walk : entry.getValue()) {
1136                sigmas.add(walk.sigma);
1137            }
1138            if (kept(proposition.source) && covers(sigmas)) {
1139                for (Walk walk : entry.getValue()) {
1140                    FactKey key = new FactKey(proposition, walk.first.get("referencePath"),
1141                            offsetOf(walk.first.get("referenceStart")));
1142                    FactEntry fact = facts.get(key);
1143                    if (fact == null) {
1144                        fact = new FactEntry();
1145                        facts.put(key, fact);
1146                    }
1147                    if (walk.sigma.isEmpty() && !walk.assumed) {
1148                        fact.proven = true;
1149                    }
1150                    fact.support.addAll(walk.support);
1151                    fact.walks.add(walk);
1152                }
1153            } else {
1154                for (Walk walk : entry.getValue()) {
1155                    if (!walk.sigma.isEmpty()) {
1156                        remainder.add(walk);
1157                    }
1158                }
1159            }
1160        }
1161        List<StatementFold.FactOccurrence> occurrences = factOccurrences(facts);
1162        List<StatementFold.Candidate> candidates = candidates(remainder);
1163        assignCandidateOrdinals(candidates);
1164        return new StatementFold(statement, occurrences, candidates, writes(statement, occurrences, candidates));
1165    }
1166
1167    private static final Comparator<Long> OFFSET_ORDER = new Comparator<Long>() {
1168        @Override
1169        public int compare(Long a, Long b) {
1170            if (a == null || b == null) {
1171                return a == null ? (b == null ? 0 : 1) : -1;
1172            }
1173            return a.compareTo(b);
1174        }
1175    };
1176
1177    private List<StatementFold.FactOccurrence> factOccurrences(LinkedHashMap<FactKey, FactEntry> facts) {
1178        // Section 8.1: the ordinal ranks the Occurrences of one Fact and path by position, missing positions last.
1179        LinkedHashMap<List<Object>, List<FactKey>> groups = new LinkedHashMap<List<Object>, List<FactKey>>();
1180        for (FactKey key : facts.keySet()) {
1181            List<Object> group = Arrays.<Object>asList(subjectKey(key.proposition.source),
1182                    subjectKey(key.proposition.target), key.proposition.label, new PathKey(key.path));
1183            List<FactKey> list = groups.get(group);
1184            if (list == null) {
1185                list = new ArrayList<FactKey>();
1186                groups.put(group, list);
1187            }
1188            list.add(key);
1189        }
1190        List<StatementFold.FactOccurrence> out = new ArrayList<StatementFold.FactOccurrence>();
1191        for (List<FactKey> list : groups.values()) {
1192            Collections.sort(list, new Comparator<FactKey>() {
1193                @Override
1194                public int compare(FactKey a, FactKey b) {
1195                    int c = OFFSET_ORDER.compare(a.offset, b.offset);
1196                    if (c == 0) {
1197                        c = REF_ORDER.compare(a.proposition.source, b.proposition.source);
1198                    }
1199                    return c != 0 ? c : REF_ORDER.compare(a.proposition.target, b.proposition.target);
1200                }
1201            });
1202            for (int ordinal = 0; ordinal < list.size(); ordinal++) {
1203                FactKey key = list.get(ordinal);
1204                FactEntry entry = facts.get(key);
1205                List<List<Long>> covering = new ArrayList<List<Long>>();
1206                for (Walk walk : entry.walks) {
1207                    covering.addAll(walk.covering);
1208                }
1209                List<Walk> deciding = decidingWalks(entry);
1210                List<Map<?, ?>> firsts = new ArrayList<Map<?, ?>>();
1211                List<Map<?, ?>> lasts = new ArrayList<Map<?, ?>>();
1212                for (Walk walk : deciding) {
1213                    firsts.add(walk.first);
1214                    lasts.addAll(walk.lastEdges);
1215                }
1216                out.add(new StatementFold.FactOccurrence(key.proposition.source, key.proposition.target,
1217                        key.proposition.label, key.path, key.offset, ordinal, entry.proven ? PROVEN : ASSUMED,
1218                        entry.support, earliestBinding(firsts, "sourceBinding"), earliestBinding(lasts, "targetBinding"),
1219                        minimalCover(covering, entry.support)));
1220            }
1221        }
1222        Collections.sort(out, new Comparator<StatementFold.FactOccurrence>() {
1223            @Override
1224            public int compare(StatementFold.FactOccurrence a, StatementFold.FactOccurrence b) {
1225                int c = REF_ORDER.compare(a.getSourceRef(), b.getSourceRef());
1226                if (c == 0) {
1227                    c = REF_ORDER.compare(a.getTargetRef(), b.getTargetRef());
1228                }
1229                if (c == 0) {
1230                    c = a.getLabel().sortText().compareTo(b.getLabel().sortText());
1231                }
1232                if (c == 0) {
1233                    c = PathKey.ORDER.compare(new PathKey(a.getPath()), new PathKey(b.getPath()));
1234                }
1235                return c != 0 ? c : a.getOrdinal() - b.getOrdinal();
1236            }
1237        });
1238        return out;
1239    }
1240
1241    /** The walks whose bindings an Occurrence reports: those that decide its proof (Section 7.2, item 2). */
1242    private static List<Walk> decidingWalks(FactEntry entry) {
1243        List<Walk> unconditional = new ArrayList<Walk>();
1244        for (Walk walk : entry.walks) {
1245            if (walk.sigma.isEmpty() && (!entry.proven || !walk.assumed)) {
1246                unconditional.add(walk);
1247            }
1248        }
1249        return unconditional.isEmpty() ? entry.walks : unconditional;
1250    }
1251
1252    /**
1253     * The identity an endpoint stands for in an Occurrence subject (identity-v1, Section 5.4): its portable id when
1254     * it has one, so that two refs of one endpoint rank as one subject; otherwise its ref.
1255     */
1256    Object subjectKey(Object ref) {
1257        Map<?, ?> endpoint = index.get("endpoint", ref);
1258        Object identity = endpoint == null ? null : endpoint.get("identity");
1259        if (identity instanceof Map<?, ?> && "PORTABLE".equals(((Map<?, ?>) identity).get("status"))
1260                && ((Map<?, ?>) identity).get("id") instanceof String) {
1261            return "id:" + ((Map<?, ?>) identity).get("id");
1262        }
1263        return "ref:" + norm(ref);
1264    }
1265
1266    /** The walks of a cover, dropping each walk whose edges the others already cover, in order. */
1267    static List<List<Long>> minimalCover(List<List<Long>> walks, Set<Long> support) {
1268        List<List<Long>> distinct = new ArrayList<List<Long>>(new LinkedHashSet<List<Long>>(walks));
1269        Set<Long> covered = new HashSet<Long>();
1270        List<List<Long>> out = new ArrayList<List<Long>>();
1271        for (List<Long> walk : distinct) {
1272            if (!covered.containsAll(walk)) {
1273                out.add(walk);
1274                covered.addAll(walk);
1275            }
1276        }
1277        if (!covered.containsAll(support)) {
1278            throw new IllegalStateException("covering walks miss " + support);
1279        }
1280        return out;
1281    }
1282
1283    /** A path value with a total order: absent first, then strings in code point order. */
1284    static final class PathKey {
1285        static final Comparator<PathKey> ORDER = new Comparator<PathKey>() {
1286            @Override
1287            public int compare(PathKey a, PathKey b) {
1288                if (a.path == null || b.path == null) {
1289                    return a.path == null ? (b.path == null ? 0 : -1) : 1;
1290                }
1291                return compareCodePoints(String.valueOf(a.path), String.valueOf(b.path));
1292            }
1293        };
1294        final Object path;
1295
1296        PathKey(Object path) {
1297            this.path = path;
1298        }
1299
1300        @Override
1301        public boolean equals(Object other) {
1302            return other instanceof PathKey && same(path, ((PathKey) other).path);
1303        }
1304
1305        @Override
1306        public int hashCode() {
1307            return hash(path);
1308        }
1309    }
1310
1311    /** The position of a reference: its offset (absent last), then its path in code point order. */
1312    static final class Position implements Comparable<Position> {
1313        final Long offset;
1314        final String path;
1315
1316        Position(Long offset, String path) {
1317            this.offset = offset;
1318            this.path = path;
1319        }
1320
1321        static Position of(Map<?, ?> edge) {
1322            Object path = edge.get("referencePath");
1323            return new Position(offsetOf(edge.get("referenceStart")),
1324                    path instanceof String && !((String) path).isEmpty() ? (String) path : "");
1325        }
1326
1327        @Override
1328        public int compareTo(Position other) {
1329            int c = OFFSET_ORDER.compare(offset, other.offset);
1330            return c != 0 ? c : compareCodePoints(path, other.path);
1331        }
1332    }
1333
1334    /**
1335     * The binding on the given side of the micro-edge earliest in the text (position, then the canonical JSON of
1336     * the binding), so that equal inputs give equal bindings whatever their refs.
1337     */
1338    static Map<?, ?> earliestBinding(List<Map<?, ?>> edges, String member) {
1339        Map<?, ?> best = null;
1340        Position bestPosition = null;
1341        byte[] bestBytes = null;
1342        for (Map<?, ?> edge : edges) {
1343            Object binding = edge.get(member);
1344            Map<?, ?> map = binding instanceof Map<?, ?> ? (Map<?, ?>) binding : null;
1345            Position position = Position.of(edge);
1346            byte[] bytes = map == null ? new byte[0] : CanonicalJson.jcsBytes(map);
1347            if (bestPosition == null) {
1348                best = map;
1349                bestPosition = position;
1350                bestBytes = bytes;
1351                continue;
1352            }
1353            int c = position.compareTo(bestPosition);
1354            if (c < 0 || (c == 0 && CanonicalJson.compareUnsigned(bytes, bestBytes) < 0)) {
1355                best = map;
1356                bestPosition = position;
1357                bestBytes = bytes;
1358            }
1359        }
1360        return best;
1361    }
1362
1363    // ---------------------------------------------------------------------------------------------
1364    // Section 7.1, items 4 to 6: FactCandidates
1365    // ---------------------------------------------------------------------------------------------
1366
1367    private List<StatementFold.Candidate> candidates(List<Walk> remainder) {
1368        TreeMap<String, List<Walk>> byLabel = new TreeMap<String, List<Walk>>();
1369        for (Walk walk : remainder) {
1370            String key = walk.label.sortText();
1371            List<Walk> list = byLabel.get(key);
1372            if (list == null) {
1373                list = new ArrayList<Walk>();
1374                byLabel.put(key, list);
1375            }
1376            list.add(walk);
1377        }
1378        List<StatementFold.Candidate> results = new ArrayList<StatementFold.Candidate>();
1379        for (List<Walk> group : byLabel.values()) {
1380            Map<Object, Object> parent = new HashMap<Object, Object>();
1381            for (Walk walk : group) {
1382                Object[] refs = walk.sigma.refs;
1383                for (Object r : refs) {
1384                    if (!parent.containsKey(r)) {
1385                        parent.put(r, r);
1386                    }
1387                }
1388                for (int i = 1; i < refs.length; i++) {
1389                    parent.put(find(parent, refs[i]), find(parent, refs[0]));
1390                }
1391            }
1392            TreeMap<Object, List<Walk>> components = new TreeMap<Object, List<Walk>>(REF_ORDER);
1393            for (Walk walk : group) {
1394                Object root = find(parent, walk.sigma.refs[0]);
1395                List<Walk> list = components.get(root);
1396                if (list == null) {
1397                    list = new ArrayList<Walk>();
1398                    components.put(root, list);
1399                }
1400                list.add(walk);
1401            }
1402            for (List<Walk> component : components.values()) {
1403                results.add(candidate(component.get(0).label, component));
1404            }
1405        }
1406        return results;
1407    }
1408
1409    private static Object find(Map<Object, Object> parent, Object x) {
1410        Object current = x;
1411        while (!same(parent.get(current), current)) {
1412            current = parent.get(current);
1413        }
1414        return current;
1415    }
1416
1417    private StatementFold.Candidate candidate(FoldLabel label, List<Walk> walks) {
1418        // the references of the group as written (sides, deciding reference), and those whose alternatives make the
1419        // worlds: a decided reference's deciding reference (Section 7.1, items 1 and 5)
1420        TreeSet<Object> rawSet = new TreeSet<Object>(REF_ORDER);
1421        TreeSet<Object> refSet = new TreeSet<Object>(REF_ORDER);
1422        List<Sigma> worldSigmas = new ArrayList<Sigma>();
1423        for (Walk walk : walks) {
1424            rawSet.addAll(Arrays.asList(walk.sigma.refs));
1425            Sigma decided = decidedSigma(walk.sigma);
1426            worldSigmas.add(decided);
1427            refSet.addAll(Arrays.asList(decided.refs));
1428        }
1429        List<Object> rawRefs = new ArrayList<Object>(rawSet);
1430        List<Object> refs = new ArrayList<Object>(refSet);
1431        List<List<Object>> alternatives = new ArrayList<List<Object>>();
1432        long worlds = 1;
1433        for (Object r : refs) {
1434            List<Object> list = alternatives(r);
1435            alternatives.add(list);
1436            worlds *= list.size();
1437            if (worlds > maxWorlds) {
1438                throw new FoldLimitException("a group of " + refs.size() + " linked references has more than "
1439                        + maxWorlds + " worlds");
1440            }
1441        }
1442        List<Set<StatementFold.Pair>> outcomes = new ArrayList<Set<StatementFold.Pair>>();
1443        boolean emptyWorld = false;
1444        String proof = PROVEN;
1445        int n = walks.size();
1446        int[][] at = new int[n][];
1447        int[][] alt = new int[n][];
1448        StatementFold.Pair[] pairOf = new StatementFold.Pair[n];
1449        for (int k = 0; k < n; k++) {
1450            Sigma sigma = worldSigmas.get(k);
1451            at[k] = new int[sigma.refs.length];
1452            alt[k] = new int[sigma.refs.length];
1453            for (int j = 0; j < sigma.refs.length; j++) {
1454                at[k][j] = refs.indexOf(sigma.refs[j]);
1455                alt[k][j] = -1;
1456                List<Object> options = alternatives.get(at[k][j]);
1457                for (int a = 0; a < options.size(); a++) {
1458                    if (sameAlternative(options.get(a), sigma.alternatives[j])) {
1459                        alt[k][j] = a;
1460                        break;
1461                    }
1462                }
1463            }
1464            pairOf[k] = new StatementFold.Pair(walks.get(k).source, walks.get(k).target);
1465        }
1466        // an alternative listed twice is one value: compare slots by the first slot holding their value
1467        int[][] canonical = new int[refs.size()][];
1468        for (int r = 0; r < refs.size(); r++) {
1469            List<Object> options = alternatives.get(r);
1470            canonical[r] = new int[options.size()];
1471            for (int a = 0; a < options.size(); a++) {
1472                canonical[r][a] = a;
1473                for (int b = 0; b < a; b++) {
1474                    if (sameAlternative(options.get(a), options.get(b))) {
1475                        canonical[r][a] = b;
1476                        break;
1477                    }
1478                }
1479            }
1480        }
1481        int[] choice = new int[refs.size()];
1482        // per world of a single deciding reference: its alternative and its pairs (the decision of an end)
1483        boolean decides = refs.size() == 1 && !rawRefs.equals(refs);
1484        List<Object> worldAlternative = new ArrayList<Object>();
1485        List<Set<StatementFold.Pair>> worldPairs = new ArrayList<Set<StatementFold.Pair>>();
1486        for (long w = 0; w < worlds; w++) {
1487            Set<StatementFold.Pair> pairs = new LinkedHashSet<StatementFold.Pair>();
1488            Set<StatementFold.Pair> proven = new HashSet<StatementFold.Pair>();
1489            for (int k = 0; k < n; k++) {
1490                boolean holds = true;
1491                for (int j = 0; j < at[k].length && holds; j++) {
1492                    holds = alt[k][j] >= 0 && canonical[at[k][j]][choice[at[k][j]]] == alt[k][j];
1493                }
1494                if (holds) {
1495                    pairs.add(pairOf[k]);
1496                    if (!walks.get(k).assumed) {
1497                        proven.add(pairOf[k]);
1498                    }
1499                }
1500            }
1501            if (!proven.containsAll(pairs)) {
1502                proof = ASSUMED;
1503            }
1504            if (pairs.isEmpty()) {
1505                emptyWorld = true;
1506            } else {
1507                outcomes.add(pairs);
1508            }
1509            if (decides) {
1510                worldAlternative.add(alternatives.get(0).get(choice[0]));
1511                worldPairs.add(pairs);
1512            }
1513            for (int i = refs.size() - 1; i >= 0; i--) {
1514                if (++choice[i] < alternatives.get(i).size()) {
1515                    break;
1516                }
1517                choice[i] = 0;
1518            }
1519        }
1520        boolean unresolved = false;
1521        TreeSet<Object> sourceSet = new TreeSet<Object>(REF_ORDER);
1522        TreeSet<Object> targetSet = new TreeSet<Object>(REF_ORDER);
1523        for (Walk walk : walks) {
1524            if (graphCset(walk.source)) {
1525                unresolved = true;
1526            } else {
1527                sourceSet.add(walk.source);
1528            }
1529            targetSet.add(walk.target);
1530        }
1531        List<Object> sources = new ArrayList<Object>(sourceSet);
1532        List<Object> targets = new ArrayList<Object>(targetSet);
1533        Set<String> sides = new HashSet<String>();
1534        boolean complete = !emptyWorld && !unresolved;
1535        for (Object r : rawRefs) {
1536            sides.addAll(sidesOf(r));
1537            complete &= referenceComplete(r);
1538        }
1539        for (Object r : refs) {
1540            complete &= referenceComplete(r);
1541        }
1542        StatementFold.End targetEnd = !sides.contains(TARGET) && targets.size() == 1
1543                ? StatementFold.End.fixed(targets.get(0))
1544                : StatementFold.End.set(targets, complete && targets.size() >= 2 ? "COMPLETE" : "INCOMPLETE",
1545                decides ? decision(refs.get(0), targets, false, worldAlternative, worldPairs) : null);
1546        StatementFold.End sourceEnd = !targetEnd.isFixed() && !sides.contains(SOURCE) && sources.size() == 1 && !unresolved
1547                ? StatementFold.End.fixed(sources.get(0))
1548                : StatementFold.End.set(sources, complete && sources.size() >= 2 ? "COMPLETE" : "INCOMPLETE",
1549                decides ? decision(refs.get(0), sources, true, worldAlternative, worldPairs) : null);
1550        Set<Set<StatementFold.Pair>> effective = new LinkedHashSet<Set<StatementFold.Pair>>();
1551        for (Set<StatementFold.Pair> outcome : outcomes) {
1552            Set<StatementFold.Pair> kept = new LinkedHashSet<StatementFold.Pair>();
1553            for (StatementFold.Pair pair : outcome) {
1554                if (!graphCset(pair.getSource())) {
1555                    kept.add(pair);
1556                }
1557            }
1558            if (!kept.isEmpty()) {
1559                effective.add(kept);
1560            }
1561        }
1562        Set<Set<StatementFold.Pair>> implied = impliedOutcomes(sourceEnd, targetEnd);
1563        Object deciding = null;
1564        Position decidingPosition = null;
1565        for (Object r : rawRefs) {
1566            Position position = refPosition.get(r);
1567            if (position == null) {
1568                position = new Position(null, "");
1569            }
1570            if (decidingPosition == null || position.compareTo(decidingPosition) < 0) {
1571                deciding = r;
1572                decidingPosition = position;
1573            }
1574        }
1575        Set<Long> support = new TreeSet<Long>();
1576        for (Walk walk : walks) {
1577            support.addAll(walk.support);
1578        }
1579        Position known = refPosition.get(deciding);
1580        Object path = known == null || known.path.isEmpty() ? null : known.path;
1581        List<List<Long>> covering = new ArrayList<List<Long>>();
1582        for (Walk walk : walks) {
1583            covering.addAll(walk.covering);
1584        }
1585        StatementFold.Candidate result = new StatementFold.Candidate(label, rawRefs, sourceEnd, targetEnd,
1586                effective.equals(implied) ? null : effective, effective.isEmpty() ? implied : effective, proof, support,
1587                path, known == null ? null : known.offset, minimalCover(covering, support));
1588        result.sourceBinding = candidateBinding(SOURCE, sourceEnd, rawRefs, walks, support);
1589        result.targetBinding = candidateBinding(TARGET, targetEnd, rawRefs, walks, support);
1590        return result;
1591    }
1592
1593    /**
1594     * Section 7.1, item 5: the decision of a candidate-set end whose group's worlds are those of one deciding reference:
1595     * each member is paired with the one alternative in whose world it is an end of a pair; null when a member is one in
1596     * several worlds or none, or an alternative would be paired twice (then the end is not decided).
1597     */
1598    private StatementFold.Decision decision(Object deciding, List<Object> members, boolean source,
1599            List<Object> worldAlternative, List<Set<StatementFold.Pair>> worldPairs) {
1600        if (members.isEmpty()) {
1601            return null;
1602        }
1603        List<StatementFold.Pair> alignment = new ArrayList<StatementFold.Pair>();
1604        List<Object> used = new ArrayList<Object>();
1605        for (Object member : members) {
1606            Object found = null;
1607            int count = 0;
1608            for (int w = 0; w < worldPairs.size(); w++) {
1609                if (worldAlternative.get(w) == NOT_LISTED) {
1610                    continue;
1611                }
1612                for (StatementFold.Pair pair : worldPairs.get(w)) {
1613                    if (same(source ? pair.getSource() : pair.getTarget(), member)) {
1614                        found = worldAlternative.get(w);
1615                        count++;
1616                        break;
1617                    }
1618                }
1619            }
1620            if (count != 1 || containsAlternative(used, found)) {
1621                return null;
1622            }
1623            used.add(found);
1624            alignment.add(new StatementFold.Pair(found, member));
1625        }
1626        Map<?, ?> cset = graphCsets.get(intRef(deciding));
1627        return new StatementFold.Decision(deciding, members(deciding), cset == null ? null : cset.get("membership"),
1628                alignment);
1629    }
1630
1631    static Set<Set<StatementFold.Pair>> impliedOutcomes(StatementFold.End source, StatementFold.End target) {
1632        List<Object> sources = source.isFixed() ? Collections.singletonList(source.getEndpointRef()) : source.getMembers();
1633        List<Object> targets = target.isFixed() ? Collections.singletonList(target.getEndpointRef()) : target.getMembers();
1634        Set<Set<StatementFold.Pair>> out = new LinkedHashSet<Set<StatementFold.Pair>>();
1635        for (Object s : sources) {
1636            for (Object t : targets) {
1637                out.add(Collections.singleton(new StatementFold.Pair(s, t)));
1638            }
1639        }
1640        return out;
1641    }
1642
1643    /** Section 7.1, item 6. */
1644    private StatementFold.CandidateBinding candidateBinding(String side, StatementFold.End end, List<Object> refs,
1645                                                            List<Walk> walks, Set<Long> support) {
1646        String member = SOURCE.equals(side) ? "sourceBinding" : "targetBinding";
1647        if (!end.isFixed()) {
1648            Object deciding = null;
1649            Position decidingPosition = null;
1650            for (Object r : refs) {
1651                if (!sidesOf(r).contains(side)) {
1652                    continue;
1653                }
1654                Position position = refPosition.get(r);
1655                if (position == null) {
1656                    position = new Position(null, "");
1657                }
1658                if (decidingPosition == null || position.compareTo(decidingPosition) < 0) {
1659                    deciding = r;
1660                    decidingPosition = position;
1661                }
1662            }
1663            if (decidingPosition != null) {
1664                List<Map<?, ?>> edges = new ArrayList<Map<?, ?>>();
1665                for (Long ref : support) {
1666                    Map<?, ?> edge = index.get("microEdge", ref);
1667                    Object binding = edge == null ? null : edge.get(member);
1668                    if (ambiguous(binding) && same(((Map<?, ?>) binding).get("candidateSetRef"), deciding)) {
1669                        edges.add(edge);
1670                    }
1671                }
1672                if (!edges.isEmpty()) {
1673                    return new StatementFold.CandidateBinding(earliestBinding(edges, member), true);
1674                }
1675            }
1676        }
1677        List<Map<?, ?>> edges = new ArrayList<Map<?, ?>>();
1678        for (Walk walk : walks) {
1679            if (SOURCE.equals(side)) {
1680                edges.add(walk.first);
1681            } else {
1682                edges.addAll(walk.lastEdges);
1683            }
1684        }
1685        return new StatementFold.CandidateBinding(earliestBinding(edges, member), false);
1686    }
1687
1688    /** identity-v1, Section 5.4: the rank among FactCandidates of the same subject and path, by position. */
1689    private void assignCandidateOrdinals(List<StatementFold.Candidate> candidates) {
1690        LinkedHashMap<List<Object>, List<StatementFold.Candidate>> groups =
1691                new LinkedHashMap<List<Object>, List<StatementFold.Candidate>>();
1692        for (StatementFold.Candidate candidate : candidates) {
1693            List<Object> subject = Arrays.<Object>asList(candidate.getLabel(), endKey(candidate.getSource()),
1694                    endKey(candidate.getTarget()), new PathKey(candidate.getPath()));
1695            List<StatementFold.Candidate> list = groups.get(subject);
1696            if (list == null) {
1697                list = new ArrayList<StatementFold.Candidate>();
1698                groups.put(subject, list);
1699            }
1700            list.add(candidate);
1701        }
1702        for (List<StatementFold.Candidate> list : groups.values()) {
1703            List<StatementFold.Candidate> sorted = new ArrayList<StatementFold.Candidate>(list);
1704            Collections.sort(sorted, new Comparator<StatementFold.Candidate>() {
1705                @Override
1706                public int compare(StatementFold.Candidate a, StatementFold.Candidate b) {
1707                    int c = OFFSET_ORDER.compare(a.getOffset(), b.getOffset());
1708                    if (c != 0) {
1709                        return c;
1710                    }
1711                    List<Object> x = a.getReferences();
1712                    List<Object> y = b.getReferences();
1713                    for (int i = 0; i < Math.min(x.size(), y.size()); i++) {
1714                        c = REF_ORDER.compare(x.get(i), y.get(i));
1715                        if (c != 0) {
1716                            return c;
1717                        }
1718                    }
1719                    return x.size() - y.size();
1720                }
1721            });
1722            for (int i = 0; i < sorted.size(); i++) {
1723                sorted.get(i).ordinal = i;
1724            }
1725        }
1726    }
1727
1728    /** A FactCandidate side as identity-v1 frames it: one endpoint identity, or a membership and sorted identities. */
1729    private List<Object> endKey(StatementFold.End end) {
1730        if (end.isFixed()) {
1731            return Arrays.<Object>asList("ENDPOINT", subjectKey(end.getEndpointRef()));
1732        }
1733        TreeSet<String> members = new TreeSet<String>();
1734        for (Object member : end.getMembers()) {
1735            members.add(String.valueOf(subjectKey(member)));
1736        }
1737        return Arrays.<Object>asList("CANDIDATE_SET", end.getMembership(), new ArrayList<String>(members));
1738    }
1739
1740    // ---------------------------------------------------------------------------------------------
1741    // Sections 7.4 and 7.5: what each write needs
1742    // ---------------------------------------------------------------------------------------------
1743
1744    /** The micro-edges of one statement by source. */
1745    Map<Object, List<Map<?, ?>>> outOf(long statement) {
1746        Map<Object, List<Map<?, ?>>> out = new LinkedHashMap<Object, List<Map<?, ?>>>();
1747        for (Map<?, ?> edge : edgesOf(statement)) {
1748            Object source = norm(edge.get("sourceRef"));
1749            List<Map<?, ?>> list = out.get(source);
1750            if (list == null) {
1751                list = new ArrayList<Map<?, ?>>();
1752                out.put(source, list);
1753            }
1754            list.add(edge);
1755        }
1756        return out;
1757    }
1758
1759    /**
1760     * The kept nodes a node reaches (fold-v1, Section 1): along walks, so a sequence that takes two alternatives of
1761     * one reference does not count (Section 7.1, item 1), through folded interior nodes; VALUE micro-edges only when
1762     * asked.
1763     */
1764    Set<Object> reachFrom(Map<Object, List<Map<?, ?>>> out, Object start, boolean valueOnly, long statement) {
1765        Set<Object> reached = new TreeSet<Object>(REF_ORDER);
1766        Set<List<Object>> seen = new HashSet<List<Object>>();
1767        Deque<Object[]> stack = new ArrayDeque<Object[]>();
1768        stack.push(new Object[] {norm(start), Sigma.EMPTY});
1769        long[] states = {0};
1770        while (!stack.isEmpty()) {
1771            Object[] state = stack.pop();
1772            List<Map<?, ?>> next = out.get(state[0]);
1773            if (next == null) {
1774                continue;
1775            }
1776            for (Map<?, ?> edge : next) {
1777                if (valueOnly && !"VALUE".equals(edge.get("kind"))) {
1778                    continue;
1779                }
1780                Sigma chosen = extend((Sigma) state[1], choices(edge));
1781                if (chosen == null) {
1782                    continue;
1783                }
1784                Object target = norm(edge.get("targetRef"));
1785                if (kept(target)) {
1786                    reached.add(target);
1787                } else if (folded(target) && seen.add(Arrays.<Object>asList(target, chosen))) {
1788                    count(states, statement);
1789                    stack.push(new Object[] {target, chosen});
1790                }
1791            }
1792        }
1793        return reached;
1794    }
1795
1796    /**
1797     * Section 7.4: for each kept node that an UNKNOWN_INPUT terminal or a folded node whose upstreamCompleteness is
1798     * not COMPLETE reaches, the lowest such completeness and the diagnostics of those nodes.
1799     */
1800    Map<Object, Object[]> lowering(long statement, Map<Object, List<Map<?, ?>>> out) {
1801        Map<Object, Object[]> lowered = new HashMap<Object, Object[]>();
1802        for (Object source : out.keySet()) {
1803            Map<?, ?> node = node(source);
1804            Object completeness;
1805            if (terminal(source) && "UNKNOWN_INPUT".equals(node.get("inputKind"))) {
1806                completeness = "PARTIAL";
1807            } else if (folded(source) && node.containsKey("upstreamCompleteness")
1808                    && !"COMPLETE".equals(node.get("upstreamCompleteness"))) {
1809                completeness = node.get("upstreamCompleteness");
1810            } else {
1811                continue;
1812            }
1813            for (Object target : reachFrom(out, source, false, statement)) {
1814                Object[] entry = lowered.get(target);
1815                if (entry == null) {
1816                    entry = new Object[] {"COMPLETE", new TreeSet<Long>()};
1817                    lowered.put(target, entry);
1818                }
1819                entry[0] = lower((String) entry[0], completeness);
1820                @SuppressWarnings("unchecked")
1821                Set<Long> diagnostics = (Set<Long>) entry[1];
1822                addRefs(diagnostics, node.get("diagnosticRefs"));
1823            }
1824        }
1825        return lowered;
1826    }
1827
1828    /** Section 7.5: the kept fields each terminal of the statement reaches through VALUE micro-edges, as inputs. */
1829    Map<Object, Set<StatementFold.TerminalInput>> terminalInputs(long statement, Map<Object, List<Map<?, ?>>> out) {
1830        Map<Object, Set<StatementFold.TerminalInput>> inputs =
1831                new TreeMap<Object, Set<StatementFold.TerminalInput>>(REF_ORDER);
1832        for (Map<?, ?> node : index.all("node")) {
1833            if ("TERMINAL".equals(node.get("kind")) && same(node.get("statementRef"), statement)) {
1834                for (Object target : reachFrom(out, node.get("ref"), true, statement)) {
1835                    if (isField(target)) {
1836                        Set<StatementFold.TerminalInput> set = inputs.get(target);
1837                        if (set == null) {
1838                            set = new LinkedHashSet<StatementFold.TerminalInput>();
1839                            inputs.put(target, set);
1840                        }
1841                        set.add(terminalInput(node.get("inputKind"), node));
1842                    }
1843                }
1844            }
1845        }
1846        return inputs;
1847    }
1848
1849    /** A terminal input as Section 7.5 compares it; two REDACTED literals of one write are indistinguishable. */
1850    static StatementFold.TerminalInput terminalInput(Object kind, Map<?, ?> obj) {
1851        Object disclosure = obj.get("disclosure");
1852        Object value = "PRESENT".equals(disclosure) ? obj.get("literal")
1853                : "HASHED".equals(disclosure) ? CanonicalJson.jcsText(obj.get("token")) : null;
1854        boolean parameter = "PARAMETER".equals(kind);
1855        return new StatementFold.TerminalInput(kind, disclosure, value, parameter ? obj.get("parameterText") : null,
1856                parameter ? obj.get("parameterOrdinal") : null);
1857    }
1858
1859    static String lower(String bound, Object completeness) {
1860        Integer rank = completeness instanceof String ? RANK.get(completeness) : null;
1861        return rank != null && rank < RANK.get(bound) ? (String) completeness : bound;
1862    }
1863
1864    static int rank(Object completeness) {
1865        Integer rank = completeness instanceof String ? RANK.get(completeness) : null;
1866        return rank == null ? 0 : rank;
1867    }
1868
1869    /** The written fields of a statement: those a micro-edge enters, and the targets of its write records. */
1870    Set<Object> writtenFields(long statement) {
1871        TreeSet<Object> out = new TreeSet<Object>(REF_ORDER);
1872        for (Map<?, ?> edge : edgesOf(statement)) {
1873            if (isField(edge.get("targetRef"))) {
1874                out.add(norm(edge.get("targetRef")));
1875            }
1876        }
1877        for (Map<?, ?> write : index.all("tfo")) {
1878            if (same(write.get("statementRef"), statement) && isField(write.get("targetRef"))) {
1879                out.add(norm(write.get("targetRef")));
1880            }
1881        }
1882        return out;
1883    }
1884
1885    private List<StatementFold.WriteRequirement> writes(long statement, List<StatementFold.FactOccurrence> facts,
1886                                                        List<StatementFold.Candidate> candidates) {
1887        boolean observed = observed(statement);
1888        Map<Object, List<Map<?, ?>>> out = outOf(statement);
1889        Map<Object, Set<StatementFold.TerminalInput>> terminals = terminalInputs(statement, out);
1890        Map<Object, Object[]> lowered = lowering(statement, out);
1891        TreeSet<Object> targets = new TreeSet<Object>(REF_ORDER);
1892        targets.addAll(writtenFields(statement));
1893        targets.addAll(terminals.keySet());
1894        List<StatementFold.WriteRequirement> requirements = new ArrayList<StatementFold.WriteRequirement>();
1895        for (Object target : targets) {
1896            String bound = observed ? "PARTIAL" : "COMPLETE";
1897            Set<Long> diagnostics = new TreeSet<Long>();
1898            List<StatementFold.FactOccurrence> inputs = new ArrayList<StatementFold.FactOccurrence>();
1899            Set<PropositionKey> seen = new HashSet<PropositionKey>();
1900            for (StatementFold.FactOccurrence fact : facts) {
1901                if (same(fact.getTargetRef(), target)
1902                        && seen.add(new PropositionKey(fact.getSourceRef(), fact.getTargetRef(), fact.getLabel()))) {
1903                    inputs.add(fact);
1904                }
1905            }
1906            List<StatementFold.Candidate> candidateInputs = new ArrayList<StatementFold.Candidate>();
1907            for (StatementFold.Candidate candidate : candidates) {
1908                StatementFold.End end = candidate.getTarget();
1909                boolean reaches = end.isFixed() ? same(end.getEndpointRef(), target) : containsRef(end.getMembers(), target);
1910                if (reaches) {
1911                    candidateInputs.add(candidate);
1912                    bound = lower(bound, "PARTIAL");
1913                }
1914            }
1915            if (enteredByChoice(statement, target)) {
1916                bound = lower(bound, "PARTIAL"); // written only in some worlds (Section 7.4)
1917            }
1918            Object[] lowering = lowered.get(target);
1919            if (lowering != null) {
1920                bound = lower(bound, lowering[0]);
1921                @SuppressWarnings("unchecked")
1922                Set<Long> lowering1 = (Set<Long>) lowering[1];
1923                diagnostics.addAll(lowering1);
1924            }
1925            Set<StatementFold.TerminalInput> inputsOfKind = terminals.get(target);
1926            requirements.add(new StatementFold.WriteRequirement(target, bound, inputs, candidateInputs,
1927                    inputsOfKind == null ? Collections.<StatementFold.TerminalInput>emptySet() : inputsOfKind,
1928                    diagnostics));
1929        }
1930        return requirements;
1931    }
1932
1933    /**
1934     * Section 7.4: whether every micro-edge of the statement that enters the target has a CANDIDATE or UNRESOLVED
1935     * target-side binding: the statement writes it only when one of those references denotes it.
1936     */
1937    boolean enteredByChoice(long statement, Object target) {
1938        boolean entered = false;
1939        for (Map<?, ?> edge : edgesOf(statement)) {
1940            if (same(edge.get("targetRef"), target)) {
1941                if (!ambiguous(edge.get("targetBinding"))) {
1942                    return false;
1943                }
1944                entered = true;
1945            }
1946        }
1947        return entered;
1948    }
1949
1950    static boolean containsRef(List<Object> refs, Object ref) {
1951        for (Object r : refs) {
1952            if (same(r, ref)) {
1953                return true;
1954            }
1955        }
1956        return false;
1957    }
1958
1959    static void addRefs(Set<Long> out, Object refs) {
1960        if (refs instanceof List<?>) {
1961            for (Object r : (List<?>) refs) {
1962                if (intRef(r) != null) {
1963                    out.add(intRef(r));
1964                }
1965            }
1966        }
1967    }
1968}