visitBinaryExpr method

  1. @override
Object? visitBinaryExpr(
  1. BinaryExpr expr
)
override

Implementation

@override
Object? visitBinaryExpr(BinaryExpr expr) {
  final String lineInfo = this.lineInfo(expr.op);
  final op = expr.op.type;

  LuaObject asLua(Object? obj, String id) =>
      obj?.makeLuaRef() ?? LuaObject.variable(id, obj);

  Object? metamethod(String name, Object? lhs, Object? rhs) {
    final l = asLua(lhs, 'lhs');
    final r = asLua(rhs, 'rhs');

    LuaObject? mm = l.readMetatable(name)?.as<LuaObject>();
    if (mm != null) {
      return callLuaFunction(mm, args: [l, r]);
    }

    mm = r.readMetatable(name)?.as<LuaObject>();
    if (mm != null) {
      return callLuaFunction(mm, args: [l, r]);
    }

    // Metamethod not handled by operands.
    return null;
  }

  num coerceString(String str) {
    final i = num.tryParse(str);
    if (i == null) throw 'Math operation on non-coercible string.';
    return i;
  }

  num asNum(Object? obj) {
    if (obj == null) throw 'Operand was null for binary $op.';
    if (obj is LuaObject) {
      final s = obj.value;
      if (s is String) {
        return coerceString(s);
      }
      final value = obj.valueAs<num>();
      if (value == null) {
        throw 'Failed to coerce lua type "${obj.luaTypeInfo}" to "number".';
      }
      return value;
    } else if (obj is num) {
      return obj;
    } else if (obj is String) {
      return coerceString(obj);
    }

    throw 'Unexpected type while coercing to "number". Found "${obj.runtimeType}".';
  }

  int asInt(Object? obj) => asNum(obj).toInt();

  String strConcat(Object? lhs, Object? rhs) {
    check(LuaObject obj) {
      final mm = switch (obj.readMetatable('__concat')) {
        final LuaObject o => o,
        _ => null,
      };

      if (!(obj.valueAsInt() is int || obj.value is String)) {
        if (mm != null) return mm;
        throw 'Attempt to concat ${obj.luaTypeInfo} value.';
      }

      return null;
    }

    final luaLhs = asLua(lhs, 'lhs');
    final mmLhs = check(luaLhs);

    final luaRhs = asLua(rhs, 'rhs');
    final mmRhs = check(luaRhs);

    if (mmLhs != null) {
      return callLuaFunction(
            mmLhs,
            args: [luaLhs, luaRhs],
          ).firstOrNull?.toString() ??
          'nil';
    }

    if (mmRhs != null) {
      return callLuaFunction(
            mmRhs,
            args: [luaLhs, luaRhs],
          ).firstOrNull?.toString() ??
          'nil';
    }

    final strL = luaLhs.toString();
    final strR = luaRhs.toString();
    return strL + strR;
  }

  bool isEqual(LuaObject? lhs, LuaObject? rhs) {
    if ((lhs?.isTable ?? false) && (rhs?.isTable ?? false)) {
      final ok = metamethod('__eq', lhs, rhs)?.unpack();
      if (ok != null) return ok.isTruthy;
    }

    Object? lval = lhs;
    if (lhs is LuaObject) {
      lval = lhs.value;
    }

    Object? rval = rhs;
    if (rhs is LuaObject) {
      rval = rhs.value;
    }

    return lval == rval;
  }

  bool isLessThan(LuaObject? lhs, LuaObject? rhs) {
    final ok = metamethod('__lt', lhs, rhs)?.unpack();
    if (ok != null) return ok.isTruthy;

    return switch ((lhs?.value, rhs?.value)) {
      (final String s, final String t) => s.compareTo(t) < 0,
      (final num n, final num m) => n < m,
      (final Object? l, final Object? r) =>
        throw 'Operation ${debugLuaTypeInfo(l)} < ${debugLuaTypeInfo(r)} failed.',
    };
  }

  bool isLessThanOrEqual(LuaObject? lhs, LuaObject? rhs) {
    if ((lhs?.isTable ?? false) || (rhs?.isTable ?? false)) {
      final ok = metamethod('__le', lhs, rhs)?.unpack();
      if (ok != null) return ok.isTruthy;
    }

    return switch ((lhs?.value, rhs?.value)) {
      (final String s, final String t) => s.compareTo(t) <= 0,
      (final num n, final num m) => n <= m,
      (final Object? l, final Object? r) =>
        throw 'Operation ${debugLuaTypeInfo(l)} <= ${debugLuaTypeInfo(r)} failed.',
    };
  }

  try {
    final lhs = expr.lhs.accept(this)?.unpack();
    final rhs = expr.rhs.accept(this)?.unpack();

    switch (op) {
      case TokenType.kConcat:
        return strConcat(lhs, rhs);
      case TokenType.kMod:
        final ok = metamethod('__mod', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) % asInt(rhs);
      case TokenType.kAnd:
        if (lhs.isTruthy) return rhs;
        return lhs;
      case TokenType.kOr:
        if (lhs.isTruthy) return lhs;
        return rhs;
      case TokenType.kBitNot:
        final ok = metamethod('__bxor', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) ^ asInt(rhs);
      case TokenType.kBitAnd:
        final ok = metamethod('__band', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) & asInt(rhs);
      case TokenType.kBitOr:
        final ok = metamethod('__bor', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) | asInt(rhs);
      case TokenType.kBitLShift:
        final ok = metamethod('__shl', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) << asInt(rhs);
      case TokenType.kBitRShift:
        final ok = metamethod('__shr', lhs, rhs);
        if (ok != null) return ok;
        return asInt(lhs) >> asInt(rhs);
      case TokenType.kCarrot:
        final ok = metamethod('__pow', lhs, rhs);
        if (ok != null) return ok;
        return math.pow(asNum(lhs), asNum(rhs));
      case TokenType.kDiv:
        final ok = metamethod('__div', lhs, rhs);
        if (ok != null) return ok;
        return asNum(lhs) /
            switch (asNum(rhs)) {
              == 0.0 => throw 'Divide by zero.',
              final num n => n,
            };
      case TokenType.kDivFloor:
        final ok = metamethod('__idiv', lhs, rhs);
        if (ok != null) return ok;
        final d =
            asNum(lhs) /
            switch (asNum(rhs)) {
              == 0.0 => throw 'Divide by zero.',
              final num n => n,
            };
        return d.floor();
      case TokenType.kSub:
        final ok = metamethod('__sub', lhs, rhs);
        if (ok != null) return ok;
        return asNum(lhs) - asNum(rhs);
      case TokenType.kAdd:
        final ok = metamethod('__add', lhs, rhs);
        if (ok != null) return ok;
        return asNum(lhs) + asNum(rhs);
      case TokenType.kMult:
        final ok = metamethod('__mul', lhs, rhs);
        if (ok != null) return ok;
        return asNum(lhs) * asNum(rhs);
      case TokenType.kLTE:
        return isLessThanOrEqual(lhs, rhs);
      case TokenType.kLT:
        return isLessThan(lhs, rhs);
      case TokenType.kGT:
        return isLessThan(rhs, lhs);
      case TokenType.kGTE:
        return isLessThanOrEqual(rhs, lhs);
      case TokenType.kEQ:
        return isEqual(lhs, rhs);
      case TokenType.kNEQ:
        return !isEqual(lhs, rhs);
      default:
        throw 'Unsupported binary operation $op.';
    }
  } catch (e) {
    throw '$lineInfo ${e.toString()}';
  }
}