visitMemoryAccess method

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

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);

  if (callee is! LuaObject) {
    throw '$linePos Expected lua object for operator "${memoryAccess.op.lexeme}", found value: $callee.';
  }

  final bool indexedTable = memoryAccess.type == MemoryAccessType.table;
  final bool funcInvocation = memoryAccess.type == MemoryAccessType.call;
  final bool fwdSelfArg = memoryAccess.op.type == TokenType.kColon;

  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) {
      addError('$linePos Multiple indexes on "$callee".');
      return null;
    }

    Object? idx = memoryAccess.field?.accept(this);
    if (idx == null || (idx is LuaObject && idx.value == null)) {
      addError('$linePos Indexing on "$callee" with nil index.');
      return null;
    }

    // Unpack first.
    if (idx is LuaObject) {
      idx = idx.value;
    }

    if (callee.isTable) {
      final String? key = switch (idx) {
        final String s => s,
        final num n => n.toInt().toString(),
        final Object o => o.toString(),
        null => null,
      };

      if (key == null) {
        addError('$linePos Attempt to index a table with a nil key.');
        return null;
      }

      if (callee.hasField(key)) {
        return callee.readField(key);
      } else {
        return callee.writeField(key, LuaObject.nil(key.toString()));
      }
    }

    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 = memoryAccess.args.length;
    List<MathExpr> args = memoryAccess.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;

      // +1 to include implied self.
      argsInLen = rhsMemoryAccess.args.length + 1;
    }

    final func = switch (callable) {
      final LuaObject lua => lua.funcDef,
      _ => null,
    };

    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>',
      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 int fwdArgCount = fwdSelfArg ? 1 : 0;
    while (args.length + fwdArgCount < defInLen) {
      args.add(NilLiteral(Token.synthesized('nil')));
    }

    pushScope(/*context: callee*/);

    if (fwdSelfArg) {
      defLocal(LuaObject.variable('self', callee));
    }

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

    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 + fwdArgCount}';
      if (i + fwdArgCount < func.args.length) {
        final arg = func.args.elementAt(i + fwdArgCount);
        if (arg.id.type == TokenType.kSpread) {
          buildVarArgTable = true;
        } else {
          lexeme = arg.lexeme;
        }
      }

      final expr = args.elementAt(i);
      final next = LuaObject.variable(lexeme, expr.accept(this));

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

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

    Object? ret;
    try {
      ret = callable!.call();
    } catch (e) {
      throw '$linePos ${e.toString()}';
    } finally {
      popScope();
    }

    return ret?.toLua('ret');
  }

  // Else this must be a field on an object.
  assert(
    memoryAccess.type == MemoryAccessType.field,
    'Codepath expected function invocation.',
  );

  final String fieldName = switch (memoryAccess.field) {
    final RawExpr r => r.token.lexeme,
    final Object? other =>
      throw '$linePos Expected a valid property name on $callee, found: $other',
  };

  // Primitives cannot have fields.
  if (callee.type == LuaType.value) {
    throw '$linePos "$callee" is a ${callee.luaTypeInfo} and cannot have fields.';
  }

  if (callee.hasField(fieldName)) return callee.readField(fieldName);

  // Else, create the field.
  return callee.writeField(fieldName, LuaObject.nil(fieldName));
}