feat: 添加跳转指令支持及条件语句编译实现
This commit is contained in:
@@ -42,6 +42,7 @@ namespace Fig
|
|||||||
SourceLocation location;
|
SourceLocation location;
|
||||||
|
|
||||||
virtual String toString() const = 0;
|
virtual String toString() const = 0;
|
||||||
|
virtual ~AstNode(){};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Program;
|
struct Program;
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ namespace Fig
|
|||||||
LoadK, // iABx 模式: R[A] = Constants[Bx]
|
LoadK, // iABx 模式: R[A] = Constants[Bx]
|
||||||
Return, // iA 模式: 返回 R[A] 的值
|
Return, // iA 模式: 返回 R[A] 的值
|
||||||
|
|
||||||
|
Jmp, // iAsBx: ip += sBx 无条件跳转
|
||||||
|
JmpIfFalse, // iAsBx: 如果 R[A] 为假, ip += sBx
|
||||||
|
|
||||||
Mov, // iABx: R[A] = R[Bx]
|
Mov, // iABx: R[A] = R[Bx]
|
||||||
Add, // iABC: R[A] = R[B] + R[C]
|
Add, // iABC: R[A] = R[B] + R[C]
|
||||||
Sub, // iABC: R[A] = R[B] - R[C]
|
Sub, // iABC: R[A] = R[B] - R[C]
|
||||||
@@ -52,10 +55,18 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
|
|
||||||
// [OpCode: 8] [A: 8] [B: 8] [C: 8]
|
// [OpCode: 8] [A: 8] [B: 8] [C: 8]
|
||||||
[[nodiscard]] inline constexpr Instruction iABC(OpCode op, std::uint8_t a, std::uint8_t b, std::uint8_t c)
|
[[nodiscard]] inline constexpr Instruction iABC(
|
||||||
|
OpCode op, std::uint8_t a, std::uint8_t b, std::uint8_t c)
|
||||||
{
|
{
|
||||||
return static_cast<std::uint32_t>(op) | (static_cast<std::uint32_t>(a) << 8)
|
return static_cast<std::uint32_t>(op) | (static_cast<std::uint32_t>(a) << 8)
|
||||||
| (static_cast<std::uint32_t>(b) << 16) | (static_cast<std::uint32_t>(c) << 24);
|
| (static_cast<std::uint32_t>(b) << 16) | (static_cast<std::uint32_t>(c) << 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
inline constexpr Instruction iAsBx(OpCode op, std::uint8_t a, std::int16_t sbx)
|
||||||
|
{
|
||||||
|
return static_cast<std::uint32_t>(op) | (static_cast<std::uint32_t>(a) << 8)
|
||||||
|
| (static_cast<std::uint32_t>(static_cast<std::uint16_t>(sbx)) << 16);
|
||||||
|
}
|
||||||
} // namespace Op
|
} // namespace Op
|
||||||
} // namespace Fig
|
} // namespace Fig
|
||||||
@@ -61,7 +61,8 @@ namespace Fig
|
|||||||
SourceManager &manager;
|
SourceManager &manager;
|
||||||
FuncState *current = nullptr; // 永远指向当前正在编译的上下文
|
FuncState *current = nullptr; // 永远指向当前正在编译的上下文
|
||||||
public:
|
public:
|
||||||
Compiler(String _fileName, SourceManager &_manager) : fileName(std::move(_fileName)), manager(_manager)
|
Compiler(String _fileName, SourceManager &_manager) :
|
||||||
|
fileName(std::move(_fileName)), manager(_manager)
|
||||||
{
|
{
|
||||||
// 初始化顶级作用域
|
// 初始化顶级作用域
|
||||||
current = new FuncState("global", nullptr);
|
current = new FuncState("global", nullptr);
|
||||||
@@ -196,7 +197,9 @@ namespace Fig
|
|||||||
{
|
{
|
||||||
if (it->depth < current->scopeDepth && !it->isPublic)
|
if (it->depth < current->scopeDepth && !it->isPublic)
|
||||||
{
|
{
|
||||||
assert(false && "ResolveLocal: Attempt to access a private variable from an outer scope!");
|
assert(
|
||||||
|
false
|
||||||
|
&& "ResolveLocal: Attempt to access a private variable from an outer scope!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return it->reg;
|
return it->reg;
|
||||||
@@ -204,7 +207,9 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果在本 Frame 没找到,那就是外层函数的变量 (闭包 Upvalue) 或者全局变量 (Global)。
|
// 如果在本 Frame 没找到,那就是外层函数的变量 (闭包 Upvalue) 或者全局变量 (Global)。
|
||||||
assert(false && "ResolveLocal: Variable not found in current frame (Upvalue/Global not implemented yet)!");
|
assert(
|
||||||
|
false
|
||||||
|
&& "ResolveLocal: Variable not found in current frame (Upvalue/Global not implemented yet)!");
|
||||||
return UINT8_MAX;
|
return UINT8_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +226,33 @@ namespace Fig
|
|||||||
return reg;
|
return reg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发射一条跳转指令,并返回它在代码数组里的绝对索引 (Index)
|
||||||
|
int EmitJump(OpCode op, std::uint8_t aReg = 0)
|
||||||
|
{
|
||||||
|
// 预填 0
|
||||||
|
Emit(Op::iAsBx(op, aReg, 0));
|
||||||
|
return current->proto->code.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填真实偏移量到那条指令里
|
||||||
|
void PatchJump(int instructionIndex)
|
||||||
|
{
|
||||||
|
// 目标地址就是当前代码数组的末尾
|
||||||
|
int target = current->proto->code.size();
|
||||||
|
|
||||||
|
// 相对偏移量 = 目标地址 - 指令自身所在的地址 - 1
|
||||||
|
// (因为 VM 里的 ip 在取指后会自动 +1,所以偏移要减去 1)
|
||||||
|
int offset = target - instructionIndex - 1;
|
||||||
|
|
||||||
|
if (offset < INT16_MIN || offset > INT16_MAX)
|
||||||
|
{
|
||||||
|
assert(false && "PatchJump: Jump offset exceeds 16-bit signed limit!");
|
||||||
|
}
|
||||||
|
Instruction &inst = current->proto->code[instructionIndex];
|
||||||
|
inst = (inst & 0x0000FFFF)
|
||||||
|
| (static_cast<Instruction>(static_cast<std::uint16_t>(offset)) << 16);
|
||||||
|
}
|
||||||
|
|
||||||
SourceLocation makeSourceLocation(AstNode *node)
|
SourceLocation makeSourceLocation(AstNode *node)
|
||||||
{
|
{
|
||||||
SourceLocation location = node->location; // copy
|
SourceLocation location = node->location; // copy
|
||||||
@@ -232,14 +264,19 @@ namespace Fig
|
|||||||
Result<std::uint8_t, Error> CompileIdentiExpr(IdentiExpr *);
|
Result<std::uint8_t, Error> CompileIdentiExpr(IdentiExpr *);
|
||||||
Result<std::uint8_t, Error> CompileLiteral(LiteralExpr *);
|
Result<std::uint8_t, Error> CompileLiteral(LiteralExpr *);
|
||||||
|
|
||||||
Result<std::uint8_t, Error> CompileAssignment(InfixExpr *); // 编译赋值,由 CompileInfixExpr调用
|
Result<std::uint8_t, Error> CompileAssignment(
|
||||||
|
InfixExpr *); // 编译赋值,由 CompileInfixExpr调用
|
||||||
Result<std::uint8_t, Error> CompileInfixExpr(InfixExpr *);
|
Result<std::uint8_t, Error> CompileInfixExpr(InfixExpr *);
|
||||||
|
|
||||||
Result<std::uint8_t, Error> CompileLeftValue(Expr *); // 左值对象,可以是变量、结构体字段或模块对象
|
Result<std::uint8_t, Error> CompileLeftValue(
|
||||||
|
Expr *); // 左值对象,可以是变量、结构体字段或模块对象
|
||||||
|
|
||||||
Result<std::uint8_t, Error> CompileExpr(Expr *);
|
Result<std::uint8_t, Error> CompileExpr(Expr *);
|
||||||
|
|
||||||
|
/* Statements */
|
||||||
Result<void, Error> CompileVarDecl(VarDecl *);
|
Result<void, Error> CompileVarDecl(VarDecl *);
|
||||||
|
Result<void, Error> CompileBlockStmt(BlockStmt *);
|
||||||
|
Result<void, Error> CompileIfStmt(IfStmt *);
|
||||||
Result<void, Error> CompileStmt(Stmt *);
|
Result<void, Error> CompileStmt(Stmt *);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,6 +307,15 @@ namespace Fig
|
|||||||
std::cout << std::format("R{:<3} K[{}]", a, bx);
|
std::cout << std::format("R{:<3} K[{}]", a, bx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case OpCode::Jmp:
|
||||||
|
case OpCode::JmpIfFalse: {
|
||||||
|
// iAsBx
|
||||||
|
std::int16_t sbx = static_cast<std::uint16_t>(inst >> 16);
|
||||||
|
std::cout << std::format("R{:<3} [{}]", a, sbx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case OpCode::Add:
|
case OpCode::Add:
|
||||||
case OpCode::Sub:
|
case OpCode::Sub:
|
||||||
case OpCode::Mul:
|
case OpCode::Mul:
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
return ResolveLocal(ie->name);
|
return ResolveLocal(ie->name);
|
||||||
}
|
}
|
||||||
Result<std::uint8_t, Error> Compiler::CompileLiteral(LiteralExpr *lit) // 编译字面量, 负责转换 token -> Value
|
Result<std::uint8_t, Error> Compiler::CompileLiteral(
|
||||||
|
LiteralExpr *lit) // 编译字面量, 负责转换 token -> Value
|
||||||
{
|
{
|
||||||
const Token &token = lit->token;
|
const Token &token = lit->token;
|
||||||
String lexeme = manager.GetSub(token.index, token.length);
|
String lexeme = manager.GetSub(token.index, token.length);
|
||||||
@@ -56,9 +57,10 @@ namespace Fig
|
|||||||
std::int32_t i = std::stoi(lexeme.toStdString());
|
std::int32_t i = std::stoi(lexeme.toStdString());
|
||||||
v = Value::FromInt(i);
|
v = Value::FromInt(i);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
assert("false" && "CompileLiteral: unsupport literal");
|
{
|
||||||
v = Value::GetNullInstance();
|
assert("false" && "CompileLiteral: unsupport literal");
|
||||||
|
}
|
||||||
|
|
||||||
std::uint8_t targetReg = AllocReg();
|
std::uint8_t targetReg = AllocReg();
|
||||||
std::uint16_t kIndex = AddConstant(v);
|
std::uint16_t kIndex = AddConstant(v);
|
||||||
@@ -66,7 +68,8 @@ namespace Fig
|
|||||||
Emit(Op::iABx(OpCode::LoadK, targetReg, kIndex));
|
Emit(Op::iABx(OpCode::LoadK, targetReg, kIndex));
|
||||||
return targetReg;
|
return targetReg;
|
||||||
}
|
}
|
||||||
Result<std::uint8_t, Error> Compiler::CompileAssignment(InfixExpr *infix) // 编译赋值,由 CompileInfixExpr调用
|
Result<std::uint8_t, Error> Compiler::CompileAssignment(
|
||||||
|
InfixExpr *infix) // 编译赋值,由 CompileInfixExpr调用
|
||||||
{
|
{
|
||||||
// op必须为 =
|
// op必须为 =
|
||||||
const auto &_lhsReg = CompileLeftValue(infix->left); // 必须为左值对象
|
const auto &_lhsReg = CompileLeftValue(infix->left); // 必须为左值对象
|
||||||
@@ -172,7 +175,8 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
return resultReg;
|
return resultReg;
|
||||||
}
|
}
|
||||||
Result<std::uint8_t, Error> Compiler::CompileLeftValue(Expr *expr) // 左值对象,可以是变量、结构体字段或模块对象
|
Result<std::uint8_t, Error> Compiler::CompileLeftValue(
|
||||||
|
Expr *expr) // 左值对象,可以是变量、结构体字段或模块对象
|
||||||
{
|
{
|
||||||
switch (expr->type)
|
switch (expr->type)
|
||||||
{
|
{
|
||||||
@@ -185,7 +189,8 @@ namespace Fig
|
|||||||
makeSourceLocation(expr)));
|
makeSourceLocation(expr)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Result<std::uint8_t, Error> Compiler::CompileExpr(Expr *expr) // 编译表达式,必定返回一个存放结果的寄存器 ID
|
Result<std::uint8_t, Error> Compiler::CompileExpr(
|
||||||
|
Expr *expr) // 编译表达式,必定返回一个存放结果的寄存器 ID
|
||||||
{
|
{
|
||||||
switch (expr->type)
|
switch (expr->type)
|
||||||
{
|
{
|
||||||
@@ -204,7 +209,7 @@ namespace Fig
|
|||||||
{
|
{
|
||||||
return std::unexpected(result.error());
|
return std::unexpected(result.error());
|
||||||
}
|
}
|
||||||
std::uint8_t targetReg = *result;
|
std::uint8_t targetReg = *result;
|
||||||
return targetReg;
|
return targetReg;
|
||||||
}
|
}
|
||||||
case AstType::InfixExpr: {
|
case AstType::InfixExpr: {
|
||||||
|
|||||||
@@ -37,6 +37,117 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
return Result<void, Error>();
|
return Result<void, Error>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<void, Error> Compiler::CompileBlockStmt(BlockStmt *blockStmt)
|
||||||
|
{
|
||||||
|
for (Stmt *stmt : blockStmt->nodes)
|
||||||
|
{
|
||||||
|
const auto &result = CompileStmt(stmt);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, Error> Compiler::CompileIfStmt(IfStmt *stmt)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
if cond1
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else if cond2 #1
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else if cond3 #2
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else #3
|
||||||
|
{
|
||||||
|
}
|
||||||
|
quit #4
|
||||||
|
|
||||||
|
Bytecode:
|
||||||
|
JmpIfFalse cond1 #1
|
||||||
|
; consequent内容
|
||||||
|
; ...
|
||||||
|
; if条件为true, 跳过所有 else/elseif
|
||||||
|
|
||||||
|
Jmp #4
|
||||||
|
|
||||||
|
; #1
|
||||||
|
JmpIfFalse cond2 #2
|
||||||
|
; consequent
|
||||||
|
|
||||||
|
Jmp #4
|
||||||
|
|
||||||
|
; #2
|
||||||
|
JmpIfFalse cond3 #3
|
||||||
|
; consequent
|
||||||
|
|
||||||
|
Jmp #4
|
||||||
|
|
||||||
|
; #3
|
||||||
|
; 没有一次执行分支
|
||||||
|
; else部分
|
||||||
|
; ...
|
||||||
|
|
||||||
|
#4
|
||||||
|
|
||||||
|
*/
|
||||||
|
std::vector<int> exitJumps; // 所有分支都要跳到最后,收集所有jump最后回填
|
||||||
|
const auto &condResult = CompileExpr(stmt->cond);
|
||||||
|
if (!condResult)
|
||||||
|
{
|
||||||
|
return std::unexpected(condResult.error());
|
||||||
|
}
|
||||||
|
std::uint8_t condReg = *condResult;
|
||||||
|
int jumpToNext = EmitJump(OpCode::JmpIfFalse, condReg);
|
||||||
|
FreeReg(condReg);
|
||||||
|
|
||||||
|
const auto &blockResult = CompileStmt(stmt->consequent);
|
||||||
|
if (!blockResult)
|
||||||
|
{
|
||||||
|
return blockResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
exitJumps.push_back(EmitJump(OpCode::Jmp)); // 执行完if直接跳到出口
|
||||||
|
PatchJump(jumpToNext); // 回填,跳到下一个else/elseif
|
||||||
|
|
||||||
|
for (auto *elif : stmt->elifs)
|
||||||
|
{
|
||||||
|
const auto &elifCondResult = CompileExpr(elif->cond);
|
||||||
|
if (!elifCondResult)
|
||||||
|
return std::unexpected(elifCondResult.error());
|
||||||
|
std::uint8_t elifCondReg = *elifCondResult;
|
||||||
|
|
||||||
|
jumpToNext = EmitJump(OpCode::JmpIfFalse, elifCondReg);
|
||||||
|
FreeReg(elifCondReg);
|
||||||
|
|
||||||
|
const auto &blockResult = CompileStmt(elif->consequent);
|
||||||
|
if (!blockResult)
|
||||||
|
{
|
||||||
|
return blockResult;
|
||||||
|
}
|
||||||
|
exitJumps.push_back(EmitJump(OpCode::Jmp)); // 执行完else if,跳到出口
|
||||||
|
PatchJump(jumpToNext); // 跳到下一个分支
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stmt->alternate)
|
||||||
|
{
|
||||||
|
const auto &result = CompileStmt(stmt->alternate);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int exitIndex : exitJumps)
|
||||||
|
{
|
||||||
|
PatchJump(exitIndex); // 回填所有跳转出口的指令
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
Result<void, Error> Compiler::CompileStmt(Stmt *stmt) // 编译语句
|
Result<void, Error> Compiler::CompileStmt(Stmt *stmt) // 编译语句
|
||||||
{
|
{
|
||||||
if (stmt->type == AstType::ExprStmt)
|
if (stmt->type == AstType::ExprStmt)
|
||||||
@@ -48,11 +159,20 @@ namespace Fig
|
|||||||
{
|
{
|
||||||
return std::unexpected(result.error());
|
return std::unexpected(result.error());
|
||||||
}
|
}
|
||||||
|
FreeReg(*result);
|
||||||
}
|
}
|
||||||
else if (stmt->type == AstType::VarDecl)
|
else if (stmt->type == AstType::VarDecl)
|
||||||
{
|
{
|
||||||
return CompileVarDecl(static_cast<VarDecl *>(stmt));
|
return CompileVarDecl(static_cast<VarDecl *>(stmt));
|
||||||
}
|
}
|
||||||
|
else if (stmt->type == AstType::BlockStmt)
|
||||||
|
{
|
||||||
|
return CompileBlockStmt(static_cast<BlockStmt *>(stmt));
|
||||||
|
}
|
||||||
|
else if (stmt->type == AstType::IfStmt)
|
||||||
|
{
|
||||||
|
return CompileIfStmt(static_cast<IfStmt *>(stmt));
|
||||||
|
}
|
||||||
return Result<void, Error>();
|
return Result<void, Error>();
|
||||||
}
|
}
|
||||||
}; // namespace Fig
|
}; // namespace Fig
|
||||||
@@ -94,6 +94,7 @@ namespace Fig
|
|||||||
}
|
}
|
||||||
if (!match(TokenType::RightParen))
|
if (!match(TokenType::RightParen))
|
||||||
{
|
{
|
||||||
|
delete *result;
|
||||||
return std::unexpected(Error(ErrorType::SyntaxError,
|
return std::unexpected(Error(ErrorType::SyntaxError,
|
||||||
"unclosed parenthese in if condition",
|
"unclosed parenthese in if condition",
|
||||||
"insert `)`",
|
"insert `)`",
|
||||||
@@ -155,6 +156,7 @@ namespace Fig
|
|||||||
state = State::ParsingIf;
|
state = State::ParsingIf;
|
||||||
if (!match(TokenType::RightParen))
|
if (!match(TokenType::RightParen))
|
||||||
{
|
{
|
||||||
|
delete *result;
|
||||||
return std::unexpected(Error(ErrorType::SyntaxError,
|
return std::unexpected(Error(ErrorType::SyntaxError,
|
||||||
"unclosed parenthese in if condition",
|
"unclosed parenthese in if condition",
|
||||||
"insert `)`",
|
"insert `)`",
|
||||||
|
|||||||
@@ -89,12 +89,34 @@ namespace Fig
|
|||||||
case OpCode::Exit: {
|
case OpCode::Exit: {
|
||||||
return Value::GetNullInstance();
|
return Value::GetNullInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
case OpCode::LoadK: {
|
case OpCode::LoadK: {
|
||||||
std::uint16_t bx = decodeBx(inst);
|
std::uint16_t bx = decodeBx(inst);
|
||||||
registers[a] = k[bx]; // constants
|
registers[a] = k[bx]; // constants
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case OpCode::Return: {
|
||||||
|
return registers[a];
|
||||||
|
}
|
||||||
|
|
||||||
|
case OpCode::Jmp: {
|
||||||
|
std::int16_t sbx = decodeSBx(inst);
|
||||||
|
ip += sbx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case OpCode::JmpIfFalse: {
|
||||||
|
Value &v = registers[a];
|
||||||
|
bool cond = v.AsBool(); // 条件类型 Compiler检查
|
||||||
|
if (!cond)
|
||||||
|
{
|
||||||
|
std::int16_t sbx = decodeSBx(inst);
|
||||||
|
ip += sbx;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case OpCode::Mov: {
|
case OpCode::Mov: {
|
||||||
std::uint16_t bx = decodeBx(inst);
|
std::uint16_t bx = decodeBx(inst);
|
||||||
registers[a] = registers[bx];
|
registers[a] = registers[bx];
|
||||||
@@ -113,9 +135,6 @@ namespace Fig
|
|||||||
BINARY_COMPARE_OP(GreaterEqual, >=);
|
BINARY_COMPARE_OP(GreaterEqual, >=);
|
||||||
BINARY_COMPARE_OP(LessEqual, <=);
|
BINARY_COMPARE_OP(LessEqual, <=);
|
||||||
|
|
||||||
case OpCode::Return: {
|
|
||||||
return registers[a];
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
assert(false && "VM: Unknown OpCode encountered!");
|
assert(false && "VM: Unknown OpCode encountered!");
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ namespace Fig
|
|||||||
{
|
{
|
||||||
return (inst >> 24) & 0xFF;
|
return (inst >> 24) & 0xFF;
|
||||||
}
|
}
|
||||||
|
inline std::int16_t decodeSBx(Instruction inst)
|
||||||
|
{
|
||||||
|
return static_cast<std::int16_t>(inst >> 16);
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// 执行入口:接收 Proto
|
// 执行入口:接收 Proto
|
||||||
|
|||||||
Reference in New Issue
Block a user