All files / src/jia render.js

97.86% Statements 229/234
75.78% Branches 72/95
100% Functions 24/24
97.86% Lines 229/234

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 2355x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 10x 10x 5x 5x 23x 23x 5x 5x 18x 18x 5x 5x 2x 2x 5x 5x 5x 5x 5x 5x 5x 5x 5x 23x 23x 8x 8x 23x 16x 1x 1x 1x   23x 5x 5x 5x 5x 5x 5x 5x 17x 17x 17x 17x 17x 17x 5x 5x 5x 5x 5x 5x 5x 18x 12x 12x 12x 10x 10x 1x   18x 5x 5x 5x 5x 5x 5x 5x 5x 50x 50x 50x 50x 11x 11x 11x 11x 11x 11x   50x 5x 5x 5x 5x 5x 5x 5x 56x     56x 5x 5x 5x 5x 5x 5x 5x 44x 44x 44x 44x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 20x 20x 10x 20x 5x 5x 5x 5x 5x 5x 5x 11x 11x 11x 5x 5x 18x 18x 18x 18x 5x 5x 12x 12x 12x 12x 5x 5x 12x 12x 12x 12x 5x 5x 12x 12x 12x 12x 5x 5x 3x 3x 3x 3x 5x 5x 3x 3x 3x 3x 5x 5x 1x 1x 1x 1x 5x 5x 2x 2x 2x 2x 5x 5x 12x 12x 12x 12x 5x 5x 26x 26x 26x 26x 5x 5x 8x 8x 8x 8x 5x 5x 1x 1x 1x 1x 5x 5x 11x 11x 11x 11x 5x 5x 11x 11x 11x 11x 5x 5x 11x 11x 11x 11x  
// @ts-check
 
import { JiaConstraint, JiaExpr, JiaSet, JiaVar } from "./ast.js";
 
/** @typedef {import("./builder.js").JiaBuilder} JiaBuilder */
/** @typedef {import("./ast.js").JiaDomainNode} JiaDomainNode */
 
/**
 * Render a Jia builder to a `.jia` source file.
 * @param {JiaBuilder} model
 * @returns {string}
 */
export function renderJia(model) {
  const lines = [`@model ${model.kind}`, `model ${model.name}`, "", "variables {"];
  for (const group of groupVariables(model.variables)) {
    lines.push(`  ${group.type}: ${group.names.join(", ")}`);
  }
  lines.push("}", "", "domains {");
  for (const domain of model.domains) {
    lines.push(`  ${renderDomain(domain)}`);
  }
  lines.push("}", "", "constraints {");
  for (const constraint of model.constraints) {
    lines.push(`  ${renderConstraint(requireConstraint(constraint))}`);
  }
  lines.push("}");
  if (model.objective) {
    lines.push("", `${model.objective.direction} ${renderExpr(model.objective.expr)}`);
  }
  return `${lines.join("\n")}\n`;
}
 
/**
 * Render a Jia domain statement.
 * @param {JiaDomainNode} domain
 * @returns {string}
 */
export function renderDomain(domain) {
  if (domain.kind === "range") return `${nameOf(domain.variable)} ${renderRange(domain.domain)}`;
  if (domain.kind === "intervalAttr") {
    return `${domain.attr}(${domain.vars.map(nameOf).join(", ")}) ${renderRange(domain.domain)}`;
  }
  if (domain.kind === "optional") return `optional(${domain.vars.map(nameOf).join(", ")})`;
  if (domain.kind === "set") return `${nameOf(domain.set)} = {${domain.members.map(nameOf).join(", ")}}`;
  if (domain.kind === "demand") {
    return `demand(${nameOf(domain.interval)}, ${nameOf(domain.set)}) = ${formatNumber(domain.value)}`;
  }
  throw new Error("Unknown domain kind");
}
 
/**
 * Render a Jia domain range or fixed value.
 * @param {import("./ast.js").JiaDomainSpec | import("./ast.js").JiaIntervalDomainSpec} domain
 * @returns {string}
 */
export function renderRange(domain) {
  if (typeof domain === "number") return `= ${formatNumber(domain)}`;
  if (domain.values) return `in {${domain.values.map(formatNumber).join(", ")}}`;
  const min = domain.min ?? 0;
  const max = domain.max ?? "inf";
  return `in ${formatNumber(min)}..${formatNumber(max)}`;
}
 
/**
 * Render a Jia constraint statement.
 * @param {JiaConstraint} constraint
 * @returns {string}
 */
export function renderConstraint(constraint) {
  if (constraint.kind === "comparison") {
    return `${renderExpr(requireLeft(constraint))} ${requireComparisonOp(constraint)} ${renderExpr(requireRight(constraint))}`;
  }
  if (constraint.kind === "noOverlap") return `no_overlap(${requireItems(constraint).map(nameOf).join(", ")})`;
  if (constraint.kind === "cumulative") return `cumulative(${nameOf(requireSet(constraint))}, ${renderExpr(requireCapacity(constraint))})`;
  if (constraint.kind === "span") return `span(${nameOf(requireParent(constraint))}, ${nameOf(requireSet(constraint))})`;
  if (constraint.kind === "alternative") return `alternative(${nameOf(requireParent(constraint))}, ${nameOf(requireSet(constraint))})`;
  throw new Error("Unknown constraint kind");
}
 
/**
 * Render a Jia expression.
 * @param {JiaExpr} expr
 * @param {number} [parentPrecedence]
 * @returns {string}
 */
export function renderExpr(expr, parentPrecedence = 0) {
  if (expr.kind === "number") return formatNumber(requireNumber(expr));
  if (expr.kind === "var") return requireName(expr);
  if (expr.kind === "call") return `${requireName(expr)}(${requireArgs(expr).join(", ")})`;
  if (expr.kind === "neg") return `-${renderExpr(requireValue(expr), 3)}`;
  if (expr.kind === "binary") {
    const op = requireArithmeticOp(expr);
    const precedence = op === "*" ? 2 : 1;
    const rendered = `${renderExpr(requireExprLeft(expr), precedence)} ${op} ${renderExpr(requireExprRight(expr), precedence + rightAssociativityBump(op))}`;
    return precedence < parentPrecedence ? `(${rendered})` : rendered;
  }
  throw new Error("Unknown expression kind");
}
 
/**
 * Convert a named Jia value to its source identifier.
 * @param {unknown} value
 * @returns {string}
 */
export function nameOf(value) {
  if (value instanceof JiaVar || value instanceof JiaSet) return value.name;
  if (typeof value === "string") return value;
  throw new Error(`Expected named Jia value, got ${String(value)}`);
}
 
/**
 * Format a number for Jia source.
 * @param {number | "inf"} value
 * @returns {string}
 */
export function formatNumber(value) {
  if (value === Infinity || value === "inf") return "inf";
  if (value === -Infinity) return "-inf";
  return Number.isInteger(value) ? String(value) : String(value);
}
 
/**
 * Group adjacent variable declarations of the same Jia type.
 * @param {{ type: string, value: JiaVar | JiaSet }[]} vars
 * @returns {{ type: string, names: string[] }[]}
 */
function groupVariables(vars) {
  /** @type {{ type: string, names: string[] }[]} */
  const groups = [];
  for (const variable of vars) {
    const last = groups.at(-1);
    if (last?.type === variable.type) last.names.push(variable.value.name);
    else groups.push({ type: variable.type, names: [variable.value.name] });
  }
  return groups;
}
 
/**
 * @param {string} op
 * @returns {number}
 */
function rightAssociativityBump(op) {
  return op === "-" ? 1 : 0;
}
 
/** @param {JiaConstraint} constraint @returns {JiaConstraint} */
function requireConstraint(constraint) {
  if (constraint.constraint === undefined) throw new Error("Expected named Jia constraint");
  return constraint.constraint;
}
 
/** @param {JiaConstraint} constraint @returns {import("./ast.js").JiaComparisonOp} */
function requireComparisonOp(constraint) {
  if (constraint.op === undefined) throw new Error("Expected Jia comparison op");
  return constraint.op;
}
 
/** @param {JiaConstraint} constraint @returns {JiaExpr} */
function requireLeft(constraint) {
  if (constraint.left === undefined) throw new Error("Expected Jia left expression");
  return constraint.left;
}
 
/** @param {JiaConstraint} constraint @returns {JiaExpr} */
function requireRight(constraint) {
  if (constraint.right === undefined) throw new Error("Expected Jia right expression");
  return constraint.right;
}
 
/** @param {JiaConstraint} constraint @returns {(JiaSet | JiaVar)[]} */
function requireItems(constraint) {
  if (constraint.items === undefined) throw new Error("Expected Jia constraint items");
  return constraint.items;
}
 
/** @param {JiaConstraint} constraint @returns {JiaSet} */
function requireSet(constraint) {
  if (constraint.set === undefined) throw new Error("Expected Jia constraint set");
  return constraint.set;
}
 
/** @param {JiaConstraint} constraint @returns {JiaExpr} */
function requireCapacity(constraint) {
  if (constraint.capacity === undefined) throw new Error("Expected Jia cumulative capacity");
  return constraint.capacity;
}
 
/** @param {JiaConstraint} constraint @returns {JiaVar} */
function requireParent(constraint) {
  if (constraint.parent === undefined) throw new Error("Expected Jia parent interval");
  return constraint.parent;
}
 
/** @param {JiaExpr} expr @returns {number} */
function requireNumber(expr) {
  if (expr.value === undefined || expr.value instanceof JiaExpr) throw new Error("Expected Jia number expression");
  return expr.value;
}
 
/** @param {JiaExpr} expr @returns {string} */
function requireName(expr) {
  if (expr.name === undefined) throw new Error("Expected Jia named expression");
  return expr.name;
}
 
/** @param {JiaExpr} expr @returns {string[]} */
function requireArgs(expr) {
  if (expr.args === undefined) throw new Error("Expected Jia call args");
  return expr.args;
}
 
/** @param {JiaExpr} expr @returns {JiaExpr} */
function requireValue(expr) {
  if (!(expr.value instanceof JiaExpr)) throw new Error("Expected Jia nested expression");
  return expr.value;
}
 
/** @param {JiaExpr} expr @returns {import("./ast.js").JiaArithmeticOp} */
function requireArithmeticOp(expr) {
  if (expr.op === undefined) throw new Error("Expected Jia arithmetic op");
  return expr.op;
}
 
/** @param {JiaExpr} expr @returns {JiaExpr} */
function requireExprLeft(expr) {
  if (expr.left === undefined) throw new Error("Expected Jia left expression");
  return expr.left;
}
 
/** @param {JiaExpr} expr @returns {JiaExpr} */
function requireExprRight(expr) {
  if (expr.right === undefined) throw new Error("Expected Jia right expression");
  return expr.right;
}