verify method

void verify()

Checks every instruction: its lanes in range, its widths, and its units against its operands'.

Throws ExpressionBytecodeException naming the first that fails.

Implementation

void verify() {
  // The dimension written to each lane, and the constant a lane holds
  // when a `loadConstant` wrote it (for `pow`'s exponent).
  final laneDim = List<int?>.filled(lanes, null);
  final laneConstant = List<double?>.filled(lanes, null);
  Never refuse(int index, String why) =>
      throw ExpressionBytecodeException('instruction $index: $why');
  for (final input in inputs) {
    if (input.lane + input.width > lanes ||
        input._dim >= _dimensions.length) {
      throw ExpressionBytecodeException(
        'input "${input.name}" is out of range',
      );
    }
    for (var i = 0; i < input.width; i++) {
      laneDim[input.lane + i] = input._dim;
    }
  }
  for (var pc = 0, n = 0; pc < code.length; pc += 8, n++) {
    final op = code[pc];
    final width = code[pc + 1] & 0xFF;
    final flags = code[pc + 1] >> 8;
    final dst = code[pc + 2];
    final a = code[pc + 3];
    final b = code[pc + 4];
    final c = code[pc + 5];
    final d = code[pc + 6];
    if (width < 1 || width > 4) refuse(n, 'width $width');
    if (d >= _dimensions.length) refuse(n, 'no dimension $d');
    final out = _dimensions[d];
    final writes = switch (op) {
      ExpressionOp.dot || ExpressionOp.length || ExpressionOp.not => 1,
      _ => width,
    };
    if (dst + writes > lanes) refuse(n, 'writes past the lanes');
    _Dim read(int lane, int count, String which) {
      if (lane + count > lanes) refuse(n, '$which is past the lanes');
      final dims = <int?>{for (var i = 0; i < count; i++) laneDim[lane + i]};
      if (dims.length != 1 || dims.single == null) {
        refuse(n, '$which reads lanes nothing wrote with one dimension');
      }
      return _dimensions[dims.single!];
    }

    int span(int flag) => flags & flag != 0 ? 1 : width;
    void expect(bool ok, String why) {
      if (!ok) refuse(n, why);
    }

    switch (op) {
      case ExpressionOp.loadConstant:
        if (a + width > constants.length) refuse(n, 'no constant $a');
      case ExpressionOp.move || ExpressionOp.negate:
        expect(read(a, width, 'a') == out, 'changes the unit');
      case ExpressionOp.swizzle:
        final source = <int>{
          for (var i = 0; i < width; i++) (b >> 2 * i) & 3,
        };
        final top = source.reduce(math.max);
        expect(read(a, top + 1, 'a') == out, 'changes the unit');
      case ExpressionOp.not:
        expect(read(a, 1, 'a').boolean && out.boolean, 'is not of bools');
      case ExpressionOp.add || ExpressionOp.subtract:
        final x = read(a, span(1), 'a');
        final y = read(b, span(2), 'b');
        expect(x == y && x == out && !x.boolean, 'adds two units');
      case ExpressionOp.multiply || ExpressionOp.divide:
        final x = read(a, span(1), 'a');
        final y = read(b, span(2), 'b');
        expect(!x.boolean && !y.boolean, 'multiplies a bool');
        expect(
          out == x.times(y, op == ExpressionOp.multiply ? 1 : -1),
          'writes the wrong unit',
        );
      case ExpressionOp.less ||
          ExpressionOp.lessEqual ||
          ExpressionOp.greater ||
          ExpressionOp.greaterEqual ||
          ExpressionOp.equal ||
          ExpressionOp.notEqual:
        final x = read(a, 1, 'a');
        final y = read(b, 1, 'b');
        expect(x == y && out.boolean && width == 1, 'compares two units');
      case ExpressionOp.and || ExpressionOp.or:
        expect(
          read(a, 1, 'a').boolean &&
              read(b, 1, 'b').boolean &&
              out.boolean &&
              width == 1,
          'is not of bools',
        );
      case ExpressionOp.select:
        expect(read(a, 1, 'a').boolean, 'selects by a number');
        expect(
          read(b, width, 'b') == out && read(c, width, 'c') == out,
          'selects between two units',
        );
      case ExpressionOp.dot:
        expect(
          out == read(a, width, 'a').times(read(b, width, 'b'), 1),
          'writes the wrong unit',
        );
      case ExpressionOp.length ||
          ExpressionOp.abs ||
          ExpressionOp.floor ||
          ExpressionOp.fract:
        expect(read(a, width, 'a') == out, 'changes the unit');
      case ExpressionOp.normalize || ExpressionOp.sign:
        read(a, width, 'a');
        expect(out == _Dim.none, 'is not a pure number');
      case ExpressionOp.mix:
        final x = read(a, span(1), 'a');
        expect(
          x == read(b, span(2), 'b') &&
              read(c, span(4), 'c') == _Dim.none &&
              x == out,
          'mixes two units',
        );
      case ExpressionOp.clamp:
        final x = read(a, span(1), 'a');
        expect(
          x == read(b, span(2), 'b') &&
              x == read(c, span(4), 'c') &&
              x == out,
          'clamps across units',
        );
      case ExpressionOp.smoothstep:
        final x = read(a, span(1), 'a');
        expect(
          x == read(b, span(2), 'b') &&
              x == read(c, span(4), 'c') &&
              out == _Dim.none,
          'smooths across units',
        );
      case ExpressionOp.step || ExpressionOp.atan2:
        expect(
          read(a, span(1), 'a') == read(b, span(2), 'b') && out == _Dim.none,
          'takes two units',
        );
      case ExpressionOp.min || ExpressionOp.max:
        final x = read(a, span(1), 'a');
        expect(x == read(b, span(2), 'b') && x == out, 'takes two units');
      case ExpressionOp.pow:
        final base = read(a, span(1), 'a');
        expect(read(b, span(2), 'b') == _Dim.none, 'raises to a unit');
        if (base.isNone) {
          expect(out == _Dim.none, 'writes the wrong unit');
        } else {
          final exponent = laneConstant[b];
          expect(
            (flags & 2 != 0 || width == 1) &&
                exponent != null &&
                exponent == exponent.truncateToDouble() &&
                exponent.abs() <= expressionPowerLimit &&
                out == base.scaled(exponent.toInt()),
            'raises a quantity to a power that is not a constant whole '
            'number from -$expressionPowerLimit to $expressionPowerLimit',
          );
        }
      case ExpressionOp.sqrt:
        final x = read(a, width, 'a');
        expect(x == out.scaled(2), 'writes the wrong unit');
      case ExpressionOp.sin || ExpressionOp.cos || ExpressionOp.tan:
        final x = read(a, width, 'a');
        expect(
          (x == _Dim.none || x == _Dim.angle) && out == _Dim.none,
          'takes an angle or a pure number',
        );
      case ExpressionOp.asin ||
          ExpressionOp.acos ||
          ExpressionOp.atan ||
          ExpressionOp.exp ||
          ExpressionOp.log:
        expect(
          read(a, width, 'a') == _Dim.none && out == _Dim.none,
          'takes a pure number',
        );
      default:
        refuse(n, 'no opcode $op');
    }
    for (var i = 0; i < writes; i++) {
      laneDim[dst + i] = d;
      laneConstant[dst + i] = op == ExpressionOp.loadConstant
          ? constants[a + i]
          : null;
    }
  }
  if (resultDimension >= _dimensions.length ||
      resultLane + resultWidth > lanes) {
    throw const ExpressionBytecodeException('the result is out of range');
  }
}