visitMemoryAccess method

  1. @override
Object? visitMemoryAccess(
  1. MemoryAccess memoryAccess
)
inherited

Implementation

@override
Object? visitMemoryAccess(MemoryAccess memoryAccess) {
  // Debugger print info
  final StreamPos linePos = memoryAccess.op.pos;
  final String lineTag = this.lineTag(memoryAccess.op);

  // Recursively descends to the deepest lhs node expression.
  // The result must be a lua object in order to be correct.
  // Otherwise a value is incorrect and an error can be thrown.
  Object? callee = memoryAccess.callee.accept(this)?.unpack();

  if (callee is! LuaObject) {
    final v = debugLuaTypeInfo(callee);
    throw '$linePos Expected lua object for operator "${memoryAccess.op.lexeme}". Was $v.';
  }

  // After the parser, fields and table keys behave the same.
  final bool indexedTable = switch (memoryAccess.type) {
    MemoryAccessType.table || MemoryAccessType.field => true,
    _ => false,
  };

  final bool funcInvocation = memoryAccess.type == MemoryAccessType.call;
  bool fwdSelfArg = memoryAccess.isSelfFwd;

  if (callee.skipSemanitcs) {
    // Check if special case of skipping semantics and evaluation.
    // Regardless is this is a method or field, we don't process it.
    // Visit args and return early.
    //
    // Note that it's not necessary to forward "self"
    // b/c no function body will be executed in this case.
    if (!fwdSelfArg) {
      for (MathExpr expr in memoryAccess.args) {
        expr.accept(this);
      }
    }

    final ret = LuaObjectNoSemantics('ret_nosemantic$lineTag');
    return ret;
  } else if (indexedTable) {
    if (memoryAccess.args.length > 1) {
      throw '$linePos Multiple indexes on "$callee".';
    }

    final Object? idx = memoryAccess.field?.accept(this);
    if (callee.isTable) {
      getValue(v) => switch (v) {
        final LuaObject lo => getValue(lo.value),
        final Object o => o,
        null => null,
      };

      final Object key = switch (memoryAccess.type) {
        MemoryAccessType.field => memoryAccess.field!.token.lexeme,
        _ => getValue(idx),
      };

      if (callee.hasField(key)) {
        return callee.readField(key);
      } else {
        final midx = callee.readMetatable('__index');
        if (midx == null) {
          final res = callee.writeField(key, LuaObject.nil(key.toString()))!;
          return res;
        }
        if (midx is! LuaObject) {
          throw '$linePos Metamethod __index was an invalid type "${debugLuaTypeInfo(midx)}"';
        }

        if (midx.isFunc) {
          return callLuaFunction(midx, args: [callee, key]);
        }

        // Else, expect table for __index.
        return midx.readField(key);
      }
    }

    throw '$linePos Indexing on "$callee" with index "$idx".';
  } else if (funcInvocation) {
    // Depending on whether or not this is a normal function call
    // using the dot "." notation or if this is a special function call
    // using the colon ":" notation, we may need to peak into the rhs
    // which will contain the special (latter) case. If so, we want to
    // use these supplied arguments for invocation.
    LuaObject? callable = callee;
    int argsInLen;
    List<LuaObject> args;
    String callableId = callee.id;

    // This indicates the node is two parts: (lhs, (functioncall))
    // where the lhs is the lua object and the functioncall is a
    // callable property on the object. This will forward lhs
    // as a new first argument.
    if (fwdSelfArg) {
      final rhsMemoryAccess = switch (memoryAccess.field) {
        final MemoryAccess ma => ma,
        _ =>
          throw '$linePos Expected function call after colon ":" operator.',
      };

      // Update the callsite context and fetch the new callableId.
      callableId = switch (rhsMemoryAccess.callee) {
        final RawExpr r => r.token.lexeme,
        final Object? obj =>
          throw '$linePos Expected name after colon ":" operator. Found $obj.',
      };

      // This must be a method on the original callee (lhs).
      callable = switch (callee.deref().readField(callableId)) {
        final LuaObject lua => lua,
        _ => null,
      };

      // Use the rhs args for invocation.
      args = rhsMemoryAccess.args.visitArgPack(this);

      // +1 to include implied self.
      argsInLen = args.length + 1;
    } else {
      args = memoryAccess.args.visitArgPack(this);
      argsInLen = args.length;
    }

    final mcall = switch (callable?.readMetatable('__call')) {
      final LuaObject lo => lo,
      _ => null,
    };

    FuncExpr? func = callable?.funcDef ?? mcall?.funcDef;
    Scope? pscope = callable?.scope ?? mcall?.scope;

    // The first argument to __call is self.
    if (mcall != null) {
      fwdSelfArg = true;
    }

    if (func == null) {
      throw '$linePos Attempt to call a nil value (field "$callableId").';
    }

    final int defInLen = func.args.length;
    final String funcId = switch (func.id) {
      '' => '<anonymous fn>',
      final String s => s,
    };

    // The earlier parser stage would catch if this wasn't true.
    final bool isVariadic =
        func.args.lastOrNull?.id.type == TokenType.kSpread;

    if (!isVariadic && argsInLen != defInLen) {
      // There are a few functions that have "overloads".
      // This means there is acceptable behavior in the lua routine
      // even with less the max number of args.
      // This warning can be supressed on a case-by-case basis.
      final suppressList = [global.findVar('table')?.readField('insert')];

      if (!suppressList.contains(callable)) {
        addWarning(
          '$linePos Function "$funcId" has $defInLen arguments but received $argsInLen.',
        );
      }
    }

    final prevScope = scope;
    pushScope(parent: pscope);
    Object? ret;

    try {
      final List<LuaObject> varg = [];
      final int argCount = switch (isVariadic) {
        true => args.length,
        false => defInLen,
      };

      if (fwdSelfArg && argCount > 0) {
        args.insert(0, LuaObject.variable(func.args.first.lexeme, callee));
      }

      bool buildVarArgTable = false;

      for (int i = 0; i < argCount; i++) {
        // Var args are bundled under a hidden variable
        // named `arg`. They do not count towards the
        // function definition parameter list.
        String lexeme = 'arg$i';
        if (i < func.args.length) {
          final arg = func.args.elementAt(i);
          if (arg.id.type == TokenType.kSpread) {
            buildVarArgTable = true;
          } else {
            lexeme = arg.lexeme;
          }
        }

        final arg = switch (i < args.length) {
          true => args.elementAt(i),
          false => null,
        };

        final next = LuaObject.variable(lexeme, arg);

        if (buildVarArgTable) {
          varg.add(next);
        } else {
          defLocal(next);
        }
      }

      defLocal(
        LuaObject.table('arg', {
          for (int i = 0; i < varg.length; i++) '${i + 1}': varg[i],
        }),
      );

      ret = mcall?.call() ?? callable!.call();
    } on LuaReturnValueException {
      rethrow;
    } catch (e) {
      throw '$linePos ${e.toString()}';
    } finally {
      restoreScope(prevScope);
    }

    return ret;
  }

  throw 'Unexpected code path while accessing memory on $callee.';
}