Carries out the arithmetic a script writes: scalar operations, matrix division and powers, and the values of a counted loop.
How the precision is kept
Two plain numbers are combined with one 64-bit operation, with no conversion. Other numbers, texts and arrays go to the tensor kernels in cs-tensor.js, and other value types to their own operator methods. Loop values are computed as start + k × step for each k, not by adding the step repeatedly.
Calculations under this policy
3 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
Adds, subtracts, multiplies, divides and compares two plain numbers.
a + b, a − b, a × b, a ÷ b, a < b, a = b
When both operands are plain numbers the result is one 64-bit operation. Equality is exact (===) with no tolerance. Division by zero is not trapped and gives Inf or NaN.
cs-vm.js
lines 368 to 383
PartCraft 0.8.1
case OP.BIN: {
const b = st[--sp], a = st[sp - 1], op = BINOPS[code[pc + 1]];
if (typeof a === 'number' && typeof b === 'number') {
let r;
switch (op) {
case '+': r = a + b; break; case '-': r = a - b; break;
case '*': case '.*': r = a * b; break; case '/': case './': r = a / b; break;
case '<': r = a < b; break; case '>': r = a > b; break; case '<=': r = a <= b; break; case '>=': r = a >= b; break;
case '==': r = a === b; break; case '~=': r = a !== b; break;
default: r = binary(op, a, b, rt);
}
st[sp - 1] = r;
} else st[sp - 1] = binary(op, a, b, rt);
pc += 2;
break;
}
Matrix multiply, divide and power
64-bit
The *, /, \ and ^ operators on matrices.
A ÷ B = (solve(Bᵀ, Aᵀ))ᵀ; Aⁿ by repeated squaring; A⁻ⁿ = (A⁻¹)ⁿ
Right division is done as a left solve on the transposes. A whole-number power uses repeated squaring; a negative power first solves A × X = I and then raises X. The exponent is tested and halved with e & 1 and e >>= 1, which work on 32 bits, so an exponent of 2³¹ or more is not handled.
cs-vm.js
lines 45 to 79
PartCraft 0.8.1
/** Binary operator on any values (dense fast paths, then type methods). */
export function binary(op, a, b, rt) {
if (a instanceof CsList || a === NOVALUE) a = single(a);
if (b instanceof CsList || b === NOVALUE) b = single(b);
if (!isDenseLike(a) || !isDenseLike(b)) {
const m = methodFor(OPNAME[op], [a, b]);
if (m) return m([a, b], { ...rt, nargout: 1 });
const bad = !isDenseLike(a) ? a : b;
throw new CsError(t('script.vm.op_value', { op, got: describe(bad) }));
}
switch (op) {
case '*': return K.matmul(a, b);
case '/': {
if (typeof b === 'number' || typeof b === 'boolean' || (isTensor(b) && b.data.length === 1)) return K.binop('./', a, b);
return K.transpose(K.solve(K.transpose(b), K.transpose(a)));
}
case '\\': {
if (typeof a === 'number' || typeof a === 'boolean' || (isTensor(a) && a.data.length === 1)) return K.binop('.\\', a, b);
return K.solve(a, b);
}
case '^': {
const sa = typeof a === 'number' || typeof a === 'boolean' || (isTensor(a) && a.data.length === 1);
const sb = typeof b === 'number' || typeof b === 'boolean' || (isTensor(b) && b.data.length === 1);
if (sa && sb) return K.binop('.^', a, b);
if (!sb) throw new CsError(t('script.vm.mpower_matrix'));
const p = num(b), [r, c] = sizeOf(a);
if (r !== c) throw new CsError(t('script.vm.mpower_square', { size: `${r}×${c}` }));
if (!Number.isInteger(p)) throw new CsError(t('script.vm.mpower_whole'));
let base = p < 0 ? K.solve(a, eye(r)) : a, e = Math.abs(p), out = eye(r);
while (e > 0) { if (e & 1) out = K.matmul(out, base); base = K.matmul(base, base); e >>= 1; }
return out;
}
default: return K.binop(op, a, b);
}
}
Values of a counted loop
64-bit
The value the loop variable takes on each pass of for x = a:s:b.
xₖ = a + k × s; last value = b when |xₖ − b| < 10⁻¹⁰ × max(1, |b|)
Each value is computed from the start, so the error does not grow with the number of passes. The number of passes comes from rangeCount in cs-tensor.js. Only the last value is snapped to the end, with a tolerance scaled to the end value.
cs-vm.js
lines 417 to 429
PartCraft 0.8.1
case OP.FORR_INIT: {
const b = single(st[--sp]), s = single(st[--sp]), a = single(st[--sp]);
const A = rnum(a, 'start'), S = rnum(s, 'step'), B = rnum(b, 'end');
slots[code[pc + 1]] = { a: A, s: S, b: B, n: K.rangeCount(A, S, B), k: 0 };
pc += 2;
break;
}
case OP.FORR_NEXT: {
const r = slots[code[pc + 2]];
if (r.k < r.n) { let x = r.a + r.k * r.s; if (r.k === r.n - 1 && r.n > 1 && Math.abs(x - r.b) < 1e-10 * Math.max(1, Math.abs(r.b))) x = r.b; slots[code[pc + 1]] = x; r.k++; pc += 4; }
else pc = code[pc + 3];
break;
}
In detail
Method
A stack machine. Scalar pairs take a fast path; matrices use matmul, solve and binop from cs-tensor.js. A whole-number matrix power uses repeated squaring.
Number types
64-bit numbers; tensors from cs-values.js (Float64Array, or Int32Array and Uint8Array for integer and logical arrays).
Tolerances
The last value of a counted loop is set to the end value when it is within 1e-10 × max(1, |end|) of it. Equality has no tolerance.
Limits
A matrix power takes whole-number exponents only, and the exponent is halved with 32-bit bit operations.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.