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 | 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 40x 40x 40x 73x 73x 73x 69x 69x 40x 269x 269x 269x 269x 269x 269x 269x 269x 40x 40x 73x 4x 4x 73x 36x 40x 269x 269x 269x 269x 269x 269x 4x 4x 4x 4x 4x 4x 4x 4x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 269x 27x 27x 27x 27x 27x 173x 4x 4x 3x 3x 3x 3x 3x 3x 3x 3x 4x 4x 169x 173x 36x 36x 1x 1x 1x 1x 1x 1x 1x 1x 36x 173x 27x 27x 27x 269x 269x 269x 269x 269x 269x 269x 2x 2x | /**
* @file contracts.js
* @description Checks declared compiler contracts against what a trace shows
* the code actually did.
*
* Avenx's contracts (`static`, `pure`, `deterministic`, `isolated`) are
* declared in a component and checked by the compiler with pattern matching
* over source text. That catches the obvious cases and misses everything
* reached indirectly: a computed that calls a helper that calls `Date.now()`
* looks pure to a regular expression.
*
* A trace closes that gap without any new machinery. It already records where
* every non-deterministic global was read and where every state write
* happened, and every node knows its causal ancestry — so asking "did anything
* inside a unit that declared itself `deterministic` read the clock?" is a walk
* up the parent chain. The contract stops being a build-time assertion and
* becomes a claim checked against the run.
*
* The diagnostic codes are the compiler's existing ones, deliberately: a
* developer who has seen AVX_W33 at build time should recognise it when it
* arrives from a recording, and `avenx explain AVX_W33` should still be the
* right thing to type.
* @module lib/core/trace/contracts
*/
import { AvenxErrorCodes } from '../runtime/AvenxError.js';
import { TraceNodeType, indexNodes } from './schema.js';
/**
* Walks a node's ancestry, nearest first.
* @param {object} node - The starting node.
* @param {Map<number, object>} byId - Nodes keyed by id.
* @yields {object} Each ancestor, nearest first.
*/
function* ancestors(node, byId) {
let current = node;
while (current && current.parent !== null && current.parent !== undefined) {
const parent = byId.get(current.parent);
if (!parent) {
return;
}
yield parent;
current = parent;
}
}
/**
* Finds the nearest ancestor that declared a given contract.
* @param {object} node - The starting node.
* @param {Map<number, object>} byId - Nodes keyed by id.
* @param {string} contract - The contract name.
* @returns {object|null} The declaring ancestor, if any.
*/
function nearestDeclaring(node, byId, contract) {
for (const ancestor of ancestors(node, byId)) {
if (Array.isArray(ancestor.contracts) && ancestor.contracts.includes(contract)) {
return ancestor;
}
}
return null;
}
/**
* Describes a unit for a diagnostic message.
* @param {object} node - An action or computed node.
* @returns {string} A short label such as `CartItem.incQty()`.
*/
function label(node) {
const ownerName = node.component || node.bridge;
const owner = ownerName ? `${ownerName}.` : '';
if (node.type === TraceNodeType.ACTION) {
return `${owner}${node.name}()`;
}
if (node.type === TraceNodeType.COMPUTED) {
return `${owner}${node.name}`;
}
return owner || node.type;
}
/**
* Checks a trace's declared contracts against what it recorded.
*
* Two contracts are checkable from a trace:
*
* - `deterministic` — a unit that declared it must not have read `Date.now()`,
* `new Date()` or `Math.random()`. The trace records exactly where those
* reads happened.
* - `pure` — a unit that declared it must not have mutated reactive state.
* The trace records every write and what caused it.
*
* `static` and `isolated` are structural claims the compiler can decide
* completely from source, so a trace adds nothing to them and they are not
* re-checked here.
* @param {object} trace - The trace to check.
* @returns {Array<{code: string, contract: string, unit: string, detail: string, nodeId: number}>}
* One entry per violation, in recorded order.
*/
export function findContractViolations(trace) {
const nodes = (trace && trace.nodes) || [];
const byId = indexNodes({ nodes });
const violations = [];
for (const node of nodes) {
if (node.type === TraceNodeType.GLOBAL) {
const declaring = nearestDeclaring(node, byId, 'deterministic');
if (declaring) {
violations.push({
code: AvenxErrorCodes.COMPILER_CONTRACT_DETERMINISTIC_VIOLATION,
contract: 'deterministic',
unit: label(declaring),
detail: `read ${node.source} during this trace`,
nodeId: node.id,
});
}
continue;
}
if (node.type === TraceNodeType.WRITE) {
const declaring = nearestDeclaring(node, byId, 'pure');
if (declaring) {
violations.push({
code: AvenxErrorCodes.COMPILER_CONTRACT_PURE_VIOLATION,
contract: 'pure',
unit: label(declaring),
detail: `wrote ${node.path} during this trace`,
nodeId: node.id,
});
}
}
}
return violations;
}
/**
* Renders a violation as a single diagnostic line.
* @param {object} violation - A violation from {@link findContractViolations}.
* @returns {string} A line suitable for `avenx trace view`.
*/
export function formatViolation(violation) {
return `${violation.code} ${violation.unit} is declared \`${violation.contract}\` but ${violation.detail}`;
}
|