Resources Numerical accuracy Calculation policy

CraftScript operators in the interpreter

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.

cs-vm.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

Tests None found by the review

Scalar arithmetic and comparison

64-bit

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.

Every policy that takes this approach