@informalsystems/quint
Version:
Core tool for the Quint specification language
398 lines • 13.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.diffRuntimeValueDoc = void 0;
const chalk_1 = __importDefault(require("chalk"));
const prettierimp_1 = require("../../prettierimp");
const json_bigint_1 = __importDefault(require("json-bigint"));
// Color constants
const GRAY = chalk_1.default.gray;
const RED = chalk_1.default.red;
const GREEN = chalk_1.default.green;
// Helper functions for colored rendering
const tint = (s, c) => (0, prettierimp_1.richtext)(c, s);
const grayRV = (v, config) => prettyRVWith(v, GRAY, config);
const greenRV = (v, config) => prettyRVWith(v, GREEN, config);
const redRV = (v, config) => prettyRVWith(v, RED, config);
/**
* Check if a runtime value should be collapsed based on its size
*/
function shouldCollapseValue(value, config) {
if (!config || config.collapseThreshold <= 0) {
return false;
}
// Try different value types to check their size
const checkers = [
() => value.toOrderedMap().size,
() => value.toMap().size,
() => value.toList().size,
() => (value.isSetLike ? value.toSet().size : 0), // Sets
];
for (const checker of checkers) {
try {
const size = checker();
if (size > config.collapseThreshold) {
return true;
}
}
catch {
// This type check failed, continue to next
continue;
}
}
return false;
}
/**
* Check if a runtime value is a primitive (bool, int, string)
*/
function tryRenderPrimitive(value, color) {
const primitiveCheckers = [
() => tint(String(value.toBool()), color),
() => tint(value.toInt().toString(), color),
() => (0, prettierimp_1.group)([tint('"', color), tint(value.toStr(), color), tint('"', color)]),
];
for (const checker of primitiveCheckers) {
try {
return checker();
}
catch {
// This primitive type check failed, continue to next
continue;
}
}
return undefined;
}
/**
* Main public function to create a diff between two runtime values
*/
function diffRuntimeValueDoc(a, b, config) {
return diffRuntimeValueDocInternal(a, b, config, false);
}
exports.diffRuntimeValueDoc = diffRuntimeValueDoc;
/**
* Internal diff function that tracks nesting context
*/
function diffRuntimeValueDocInternal(a, b, config, isNested = false) {
// Equal values - render in gray, collapse if nested and large
if (a.equals(b)) {
if (isNested && shouldCollapseValue(a, config)) {
return tint('...', GRAY);
}
return grayRV(a, config);
}
// Try to render as primitive values
const atomOld = tryRenderPrimitive(a, RED);
const atomNew = tryRenderPrimitive(b, GREEN);
if (atomOld && atomNew) {
return (0, prettierimp_1.group)([atomOld, (0, prettierimp_1.text)(' => '), atomNew]);
}
// Try different composite types
return (tryDiffRecords(a, b, config) ||
tryDiffMaps(a, b, config) ||
tryDiffLists(a, b, config) ||
tryDiffVariants(a, b, config) ||
tryDiffSets(a, b, config) ||
// Fallback: show as old => new
(0, prettierimp_1.group)([prettyRVWith(a, RED, config), (0, prettierimp_1.text)(' => '), prettyRVWith(b, GREEN, config)]));
}
/**
* Try to diff two values as records
*/
function tryDiffRecords(a, b, config) {
try {
const ra = a.toOrderedMap();
const rb = b.toOrderedMap();
const keys = new Set([...ra.keySeq().toArray(), ...rb.keySeq().toArray()]);
const fields = [];
Array.from(keys)
.sort()
.forEach(k => {
const inA = ra.has(k);
const inB = rb.has(k);
if (inA && inB) {
// Field exists in both - diff the values
fields.push((0, prettierimp_1.group)([
tint(k, GRAY),
tint(':', GRAY),
(0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), diffRuntimeValueDocInternal(ra.get(k), rb.get(k), config, true)]),
]));
}
else if (inA) {
// Field only in A (removed)
fields.push((0, prettierimp_1.group)([tint(k, RED), tint(':', RED), (0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), redRV(ra.get(k), config)])]));
}
else {
// Field only in B (added)
fields.push((0, prettierimp_1.group)([tint(k, GREEN), tint(':', GREEN), (0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), greenRV(rb.get(k), config)])]));
}
});
return nary(GRAY, tint('{', GRAY), fields, tint('}', GRAY), (0, prettierimp_1.line)());
}
catch {
return undefined;
}
}
/**
* Try to diff two values as maps
*/
function tryDiffMaps(a, b, config) {
try {
const ma = a.toMap();
const mb = b.toMap();
const aKeys = ma.keySeq().toArray();
const bKeys = mb.keySeq().toArray();
const consumed = new Set();
const pairs = [];
// Match keys from A with keys in B
aKeys.forEach(ka => {
const idx = matchKeyIdx(ka, bKeys);
if (idx >= 0) {
consumed.add(idx);
pairs.push((0, prettierimp_1.group)([
diffRuntimeValueDocInternal(ka, bKeys[idx], config, true),
tint(' -> ', GRAY),
diffRuntimeValueDocInternal(ma.get(ka), mb.get(bKeys[idx]), config, true),
]));
}
else {
pairs.push((0, prettierimp_1.group)([redRV(ka, config), tint(' -> ', RED), redRV(ma.get(ka), config)]));
}
});
// Add unmatched keys from B
bKeys.forEach((kb, i) => {
if (!consumed.has(i)) {
pairs.push((0, prettierimp_1.group)([greenRV(kb, config), tint(' -> ', GREEN), greenRV(mb.get(kb), config)]));
}
});
return (0, prettierimp_1.group)([tint('Map', GRAY), nary(GRAY, tint('(', GRAY), pairs, tint(')', GRAY))]);
}
catch {
return undefined;
}
}
/**
* Try to diff two values as lists/tuples
*/
function tryDiffLists(a, b, config) {
try {
const la = a.toList().toArray();
const lb = b.toList().toArray();
if (la.length === lb.length) {
// Same length - diff element by element
const items = la.map((x, i) => diffRuntimeValueDocInternal(x, lb[i], config, true));
return nary(GRAY, tint('[', GRAY), items, tint(']', GRAY));
}
else {
// Different length - show as old => new
return (0, prettierimp_1.group)([
nary(RED, tint('[', RED), la.map(x => redRV(x, config)), tint(']', RED)),
(0, prettierimp_1.text)(' => '),
nary(GREEN, tint('[', GREEN), lb.map(x => greenRV(x, config)), tint(']', GREEN)),
]);
}
}
catch {
return undefined;
}
}
/**
* Try to diff two values as variants
*/
function tryDiffVariants(a, b, config) {
try {
const [la, va] = a.toVariant();
const [lb, vb] = b.toVariant();
const payloadIsEmptyTuple = (rv) => {
try {
return rv.toList().toArray().length === 0;
}
catch {
return false;
}
};
const renderVariant = (label, payload, color) => {
const labelDoc = tint(label, color);
return payloadIsEmptyTuple(payload)
? labelDoc
: (0, prettierimp_1.group)([labelDoc, (0, prettierimp_1.text)('('), prettyRVWith(payload, color, config), (0, prettierimp_1.text)(')')]);
};
if (la === lb) {
// Same label - diff the payload
const labelDoc = tint(la, GRAY);
if (payloadIsEmptyTuple(va) && payloadIsEmptyTuple(vb)) {
return labelDoc;
}
else {
return (0, prettierimp_1.group)([labelDoc, (0, prettierimp_1.text)('('), diffRuntimeValueDocInternal(va, vb, config, true), (0, prettierimp_1.text)(')')]);
}
}
else {
// Different labels - show old => new
const oldDoc = renderVariant(la, va, RED);
const newDoc = renderVariant(lb, vb, GREEN);
return (0, prettierimp_1.group)([oldDoc, (0, prettierimp_1.text)(' => '), newDoc]);
}
}
catch {
return undefined;
}
}
/**
* Try to diff two values as sets
*/
function tryDiffSets(a, b, config) {
if (!a.isSetLike && !b.isSetLike) {
return undefined;
}
try {
const sa = a.isSetLike ? Array.from(a.toSet()) : [];
const sb = b.isSetLike ? Array.from(b.toSet()) : [];
const matchedB = new Array(sb.length).fill(false);
const items = [];
// Find matching elements
for (const ea of sa) {
let matched = false;
for (let i = 0; i < sb.length; i++) {
if (!matchedB[i] && ea.equals(sb[i])) {
matchedB[i] = true;
matched = true;
items.push(grayRV(ea, config));
break;
}
}
if (!matched) {
items.push(redRV(ea, config));
}
}
// Add unmatched elements from B
for (let i = 0; i < sb.length; i++) {
if (!matchedB[i]) {
items.push(greenRV(sb[i], config));
}
}
return (0, prettierimp_1.group)([tint('Set', GRAY), nary(GRAY, tint('(', GRAY), items, tint(')', GRAY))]);
}
catch {
return undefined;
}
}
/**
* Helper function for n-ary layouts (records, lists, etc.)
* Same indentation behavior as graphics.ts
*/
function nary(c, left, args, right, padding = prettierimp_1.linebreak) {
const as = (0, prettierimp_1.group)([(0, prettierimp_1.nest)(' ', [padding, (0, prettierimp_1.docJoin)([tint(',', c), (0, prettierimp_1.line)()], args)]), padding]);
return (0, prettierimp_1.group)([left, as, right]);
}
/**
* A neutral pretty-printer for RuntimeValue that applies a single color.
* It deliberately avoids the per-token color scheme in prettyQuintEx, since diff colors
* carry the semantics here.
*/
function prettyRVWith(v, c, config) {
// Try primitive types first
const primitive = tryRenderPrimitive(v, c);
if (primitive) {
return primitive;
}
// Try composite types
return (tryPrettyRecord(v, c, config) ||
tryPrettyMap(v, c, config) ||
tryPrettyList(v, c, config) ||
tryPrettyVariant(v, c, config) ||
tryPrettySet(v, c, config) ||
// Fallback to JSON
tryPrettyFallback(v, c));
}
function tryPrettyRecord(v, c, config) {
try {
const om = v.toOrderedMap();
const kvs = [];
om.forEach((val, key) => {
kvs.push((0, prettierimp_1.group)([tint(key, c), tint(':', c), (0, prettierimp_1.nest)(' ', [(0, prettierimp_1.line)(), prettyRVWith(val, c, config)])]));
});
return nary(c, tint('{', c), kvs, tint('}', c), (0, prettierimp_1.line)());
}
catch {
return undefined;
}
}
function tryPrettyMap(v, c, config) {
try {
const m = v.toMap();
const ps = [];
m.forEach((vv, kk) => {
ps.push((0, prettierimp_1.group)([prettyRVWith(kk, c, config), tint(' -> ', c), prettyRVWith(vv, c, config)]));
});
return (0, prettierimp_1.group)([tint('Map', c), nary(c, tint('(', c), ps, tint(')', c))]);
}
catch {
return undefined;
}
}
function tryPrettyList(v, c, config) {
try {
const list = v.toList();
const items = list.toArray().map(x => prettyRVWith(x, c, config));
return nary(c, tint('[', c), items, tint(']', c));
}
catch {
return undefined;
}
}
function tryPrettyVariant(v, c, config) {
try {
const [label, value] = v.toVariant();
// Check if variant has empty tuple payload
let isEmptyTuple = false;
try {
isEmptyTuple = value.toList().toArray().length === 0;
}
catch {
// Not a tuple or conversion failed
}
const labelDoc = tint(label, c);
return isEmptyTuple
? labelDoc
: (0, prettierimp_1.group)([labelDoc, nary(c, tint('(', c), [prettyRVWith(value, c, config)], tint(')', c))]);
}
catch {
return undefined;
}
}
function tryPrettySet(v, c, config) {
if (!v.isSetLike) {
return undefined;
}
try {
const set = v.toSet();
const elems = Array.from(set).map(x => prettyRVWith(x, c, config));
return (0, prettierimp_1.group)([tint('Set', c), nary(c, tint('(', c), elems, tint(')', c))]);
}
catch {
// Non-enumerable (infinite) set - let it fall through
return undefined;
}
}
function tryPrettyFallback(v, c) {
try {
return tint(json_bigint_1.default.stringify(v), c);
}
catch {
return tint('<value>', c);
}
}
/**
* Helper function to find the index of a matching key in an array
*/
function matchKeyIdx(key, keys) {
for (let i = 0; i < keys.length; i++) {
if (key.equals(keys[i])) {
return i;
}
}
return -1;
}
//# sourceMappingURL=runtimeValueDiff.js.map