added interpreter
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
fn main(): String {
|
fn main(): i64 {
|
||||||
let a = "asdf";
|
let user = User{id: 4};
|
||||||
return a ;
|
return user.instance_method();
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TypeAliasResolver } from "../types/type_alias_resolution";
|
|||||||
import { TypeSystem } from "../types/type_system";
|
import { TypeSystem } from "../types/type_system";
|
||||||
import { TypeChecker } from "../types/type_checker";
|
import { TypeChecker } from "../types/type_checker";
|
||||||
import { TypeResolver } from "../types/type_resolver";
|
import { TypeResolver } from "../types/type_resolver";
|
||||||
|
import { TreeWalkInterpreter } from "../interpreter";
|
||||||
|
|
||||||
export const run = defineCommand({
|
export const run = defineCommand({
|
||||||
name: "run",
|
name: "run",
|
||||||
@@ -33,7 +34,9 @@ export const run = defineCommand({
|
|||||||
typeChecker.withModule(aliasResolvedAst, typeSystem);
|
typeChecker.withModule(aliasResolvedAst, typeSystem);
|
||||||
typeSystem.solve();
|
typeSystem.solve();
|
||||||
const typeResolvedAst = typeResolver.withModule(aliasResolvedAst, typeSystem);
|
const typeResolvedAst = typeResolver.withModule(aliasResolvedAst, typeSystem);
|
||||||
console.log(JSON.stringify(typeResolvedAst, null, 2));
|
const interpreter = new TreeWalkInterpreter();
|
||||||
|
const result = interpreter.withModule(typeResolvedAst);
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
// console.log(JSON.stringify(aliasResolvedAst, null, 2));
|
// console.log(JSON.stringify(aliasResolvedAst, null, 2));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
92
packages/boringlang/src/interpreter/builtins.ts
Normal file
92
packages/boringlang/src/interpreter/builtins.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Module } from "../parse/ast";
|
||||||
|
import { Context, FunctionRef } from "./context";
|
||||||
|
|
||||||
|
export function contextFromModule(module: Module): Context {
|
||||||
|
const ctx: Context = {
|
||||||
|
environment: {},
|
||||||
|
currentModule: module,
|
||||||
|
};
|
||||||
|
ctx.environment["i8"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["i16"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["i32"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["i64"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["f8"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["f16"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["f32"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["f64"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["String"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["Void"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
ctx.environment["Never"] = { namedEntity: "NamedType", isA: "Scalar", fields: {}, impls: [] };
|
||||||
|
|
||||||
|
// add functions, structs, and traits to the context
|
||||||
|
for (const item of module.items) {
|
||||||
|
if (item.moduleItem === "StructTypeDeclaration") {
|
||||||
|
ctx.environment[item.name.text] = {
|
||||||
|
namedEntity: "NamedType",
|
||||||
|
isA: "Struct",
|
||||||
|
fields: Object.fromEntries(item.fields.map((field) => [field.name.text, field.type])),
|
||||||
|
impls: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.moduleItem === "TraitTypeDeclaration") {
|
||||||
|
ctx.environment[item.name.text] = {
|
||||||
|
namedEntity: "NamedType",
|
||||||
|
isA: "Trait",
|
||||||
|
fields: {},
|
||||||
|
impls: [
|
||||||
|
{
|
||||||
|
trait: item.name.text,
|
||||||
|
functions: Object.fromEntries(
|
||||||
|
item.functions.map((fn) => {
|
||||||
|
return [
|
||||||
|
fn.name,
|
||||||
|
{
|
||||||
|
functionType: "UserFunction",
|
||||||
|
function: fn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.moduleItem === "Function") {
|
||||||
|
ctx.environment[item.declaration.name.text] = {
|
||||||
|
namedEntity: "Variable",
|
||||||
|
value: {
|
||||||
|
value: "FunctionValue",
|
||||||
|
partial: [],
|
||||||
|
ref: {
|
||||||
|
functionType: "UserFunction",
|
||||||
|
function: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now that structs and traits are added, add impls
|
||||||
|
for (const item of module.items) {
|
||||||
|
if (item.moduleItem === "Impl") {
|
||||||
|
const struct = ctx.environment[item.struct.name.text];
|
||||||
|
if (!struct || struct.namedEntity !== "NamedType" || struct.isA !== "Struct") {
|
||||||
|
throw Error("Impl for non-struct");
|
||||||
|
}
|
||||||
|
const functions: Record<string, FunctionRef> = {};
|
||||||
|
for (const fn of item.functions) {
|
||||||
|
if (
|
||||||
|
fn.declaration.arguments.length &&
|
||||||
|
fn.declaration.arguments[0].type.typeUsage == "NamedTypeUsage" &&
|
||||||
|
fn.declaration.arguments[0].type.name.text === item.struct.name.text
|
||||||
|
) {
|
||||||
|
functions[fn.declaration.name.text] = { functionType: "UserFunction", function: fn };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct.impls.push({
|
||||||
|
trait: item.trait?.name.text ?? null,
|
||||||
|
functions: functions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
74
packages/boringlang/src/interpreter/context.ts
Normal file
74
packages/boringlang/src/interpreter/context.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { TypeUsage, Function, Module } from "../parse/ast";
|
||||||
|
|
||||||
|
export interface NumericValue {
|
||||||
|
value: "NumericValue";
|
||||||
|
number: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoolValue {
|
||||||
|
value: "BoolValue";
|
||||||
|
bool: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringValue {
|
||||||
|
value: "StringValue";
|
||||||
|
string: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserFunction {
|
||||||
|
functionType: "UserFunction";
|
||||||
|
function: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuiltinFunction {
|
||||||
|
functionType: "BuiltinFunction";
|
||||||
|
function: (value: Value[]) => Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionRef = UserFunction | BuiltinFunction;
|
||||||
|
|
||||||
|
export interface FunctionValue {
|
||||||
|
value: "FunctionValue";
|
||||||
|
partial: Value[];
|
||||||
|
ref: FunctionRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitValue {
|
||||||
|
value: "UnitValue";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StructValue {
|
||||||
|
value: "StructValue";
|
||||||
|
source: NamedType;
|
||||||
|
fields: Record<string, Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Value =
|
||||||
|
| NumericValue
|
||||||
|
| BoolValue
|
||||||
|
| StringValue
|
||||||
|
| FunctionValue
|
||||||
|
| StructValue
|
||||||
|
| UnitValue;
|
||||||
|
|
||||||
|
export interface Variable {
|
||||||
|
namedEntity: "Variable";
|
||||||
|
value: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvImpl {
|
||||||
|
trait: string | null;
|
||||||
|
functions: Record<string, FunctionRef>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamedType {
|
||||||
|
namedEntity: "NamedType";
|
||||||
|
isA: "Scalar" | "Trait" | "Struct";
|
||||||
|
fields: Record<string, TypeUsage>;
|
||||||
|
impls: EnvImpl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
environment: Record<string, NamedType | Variable>;
|
||||||
|
currentModule: Module;
|
||||||
|
}
|
||||||
305
packages/boringlang/src/interpreter/index.ts
Normal file
305
packages/boringlang/src/interpreter/index.ts
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import {
|
||||||
|
AssignmentStatement,
|
||||||
|
Block,
|
||||||
|
Expression,
|
||||||
|
Function,
|
||||||
|
FunctionCall,
|
||||||
|
LetStatement,
|
||||||
|
Module,
|
||||||
|
Operation,
|
||||||
|
ReturnStatement,
|
||||||
|
StructGetter,
|
||||||
|
StructTypeDeclaration,
|
||||||
|
TypeUsage,
|
||||||
|
} from "../parse/ast";
|
||||||
|
import { contextFromModule } from "./builtins";
|
||||||
|
import { Context, Value } from "./context";
|
||||||
|
|
||||||
|
interface ExpressionResultValue {
|
||||||
|
resultType: "Value";
|
||||||
|
value: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpressionResultReturn {
|
||||||
|
resultType: "Return";
|
||||||
|
value: Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpressionResult = ExpressionResultValue | ExpressionResultReturn;
|
||||||
|
|
||||||
|
export class TreeWalkInterpreter {
|
||||||
|
withModule = (module: Module) => {
|
||||||
|
const ctx = contextFromModule(module);
|
||||||
|
const main = ctx.environment["main"];
|
||||||
|
if (
|
||||||
|
!main ||
|
||||||
|
!(main.namedEntity === "Variable") ||
|
||||||
|
!(main.value.value === "FunctionValue") ||
|
||||||
|
!(main.value.ref.functionType === "UserFunction")
|
||||||
|
) {
|
||||||
|
throw Error("No main function");
|
||||||
|
}
|
||||||
|
return this.withFunction(ctx, main.value.ref.function);
|
||||||
|
};
|
||||||
|
|
||||||
|
withFunction = (ctx: Context, fn: Function): Value => {
|
||||||
|
let result = this.withBlock(ctx, fn.block);
|
||||||
|
return result.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
withBlock = (ctx: Context, block: Block): ExpressionResult => {
|
||||||
|
let last: ExpressionResult = { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
for (const statement of block.statements) {
|
||||||
|
if (statement.statementType === "AssignmentStatement") {
|
||||||
|
last = this.withAssignmentStatement(ctx, statement);
|
||||||
|
}
|
||||||
|
if (statement.statementType === "LetStatement") {
|
||||||
|
last = this.withLetStatement(ctx, statement);
|
||||||
|
}
|
||||||
|
if (statement.statementType === "Expression") {
|
||||||
|
last = this.withExpression(ctx, statement);
|
||||||
|
}
|
||||||
|
if (statement.statementType === "ReturnStatement") {
|
||||||
|
last = this.withReturnStatement(ctx, statement);
|
||||||
|
}
|
||||||
|
if (last.resultType === "Return") {
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
};
|
||||||
|
|
||||||
|
withAssignmentStatement = (ctx: Context, statement: AssignmentStatement): ExpressionResult => {
|
||||||
|
let result = this.withExpression(ctx, statement.expression);
|
||||||
|
if (result.resultType === "Return") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (statement.source.expressionType == "VariableUsage") {
|
||||||
|
ctx.environment[statement.source.name.text] = {
|
||||||
|
namedEntity: "Variable",
|
||||||
|
value: result.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (statement.source.expressionType == "StructGetter") {
|
||||||
|
let source = this.withStructGetter(ctx, statement.source);
|
||||||
|
if (source.resultType === "Return") {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
if (source.value.value !== "StructValue") {
|
||||||
|
throw Error("set attr on nonstruct, should never happen due to type system");
|
||||||
|
}
|
||||||
|
source.value.fields[statement.source.attribute.text] = result.value;
|
||||||
|
}
|
||||||
|
return { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
};
|
||||||
|
|
||||||
|
withLetStatement = (ctx: Context, statement: LetStatement): ExpressionResult => {
|
||||||
|
let result = this.withExpression(ctx, statement.expression);
|
||||||
|
if (result.resultType === "Return") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
ctx.environment[statement.variableName.text] = {
|
||||||
|
namedEntity: "Variable",
|
||||||
|
value: result.value,
|
||||||
|
};
|
||||||
|
return { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
};
|
||||||
|
|
||||||
|
withReturnStatement = (ctx: Context, statement: ReturnStatement): ExpressionResult => {
|
||||||
|
let result = this.withExpression(ctx, statement.source);
|
||||||
|
if (result.resultType === "Return") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return { resultType: "Return", value: result.value };
|
||||||
|
};
|
||||||
|
|
||||||
|
withExpression = (ctx: Context, expression: Expression): ExpressionResult => {
|
||||||
|
if (expression.subExpression.expressionType === "LiteralInt") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: parseInt(expression.subExpression.value) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "LiteralFloat") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: parseFloat(expression.subExpression.value) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "LiteralString") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "StringValue", string: expression.subExpression.value },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "LiteralBool") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "BoolValue", bool: expression.subExpression.value === "true" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "LiteralStruct") {
|
||||||
|
const def = ctx.environment[expression.subExpression.name.text];
|
||||||
|
if (def.namedEntity !== "NamedType") {
|
||||||
|
throw Error("Not a struct");
|
||||||
|
}
|
||||||
|
const fields: Record<string, Value> = {};
|
||||||
|
for (const field of expression.subExpression.fields) {
|
||||||
|
const fieldResult = this.withExpression(ctx, field.expression);
|
||||||
|
if (fieldResult.resultType === "Return") {
|
||||||
|
return fieldResult;
|
||||||
|
}
|
||||||
|
fields[field.name.text] = fieldResult.value;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: {
|
||||||
|
value: "StructValue",
|
||||||
|
source: def,
|
||||||
|
fields: fields,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "FunctionCall") {
|
||||||
|
return this.withFunctionCall(ctx, expression.subExpression);
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "VariableUsage") {
|
||||||
|
const variableValue = ctx.environment[expression.subExpression.name.text];
|
||||||
|
if (!variableValue || variableValue.namedEntity !== "Variable") {
|
||||||
|
throw Error(`not found: ${expression.subExpression.name.text}`);
|
||||||
|
}
|
||||||
|
return { resultType: "Value", value: variableValue.value };
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "IfExpression") {
|
||||||
|
const condition = this.withExpression(ctx, expression.subExpression.condition);
|
||||||
|
if (condition.resultType === "Return") {
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
if (condition.value.value === "BoolValue" && condition.value.bool === true) {
|
||||||
|
return this.withBlock(ctx, expression.subExpression.block);
|
||||||
|
} else {
|
||||||
|
if (expression.subExpression.else) {
|
||||||
|
return this.withBlock(ctx, expression.subExpression.else);
|
||||||
|
} else {
|
||||||
|
return { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "StructGetter") {
|
||||||
|
return this.withStructGetter(ctx, expression.subExpression);
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "Block") {
|
||||||
|
return this.withBlock(ctx, expression.subExpression);
|
||||||
|
}
|
||||||
|
if (expression.subExpression.expressionType === "Operation") {
|
||||||
|
return this.withOperation(ctx, expression.subExpression);
|
||||||
|
}
|
||||||
|
// not actually possible, but makes the type system happy
|
||||||
|
return { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
};
|
||||||
|
|
||||||
|
withFunctionCall = (ctx: Context, fnCall: FunctionCall): ExpressionResult => {
|
||||||
|
const source = this.withExpression(ctx, fnCall.source);
|
||||||
|
if (source.resultType === "Return") {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
const argValues: Value[] = [];
|
||||||
|
for (const arg of fnCall.arguments) {
|
||||||
|
const argValue = this.withExpression(ctx, arg);
|
||||||
|
if (argValue.resultType === "Return") {
|
||||||
|
return argValue;
|
||||||
|
}
|
||||||
|
argValues.push(argValue.value);
|
||||||
|
}
|
||||||
|
if (source.value.value !== "FunctionValue") {
|
||||||
|
throw Error("type error: function call source must be a function");
|
||||||
|
}
|
||||||
|
if (source.value.ref.functionType === "UserFunction") {
|
||||||
|
const fn = source.value.partial;
|
||||||
|
const fnCtx = contextFromModule(ctx.currentModule);
|
||||||
|
let i = 0;
|
||||||
|
for (const arg of source.value.partial) {
|
||||||
|
fnCtx.environment[source.value.ref.function.declaration.arguments[i].name.text] = {
|
||||||
|
namedEntity: "Variable",
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
for (const arg of argValues) {
|
||||||
|
fnCtx.environment[source.value.ref.function.declaration.arguments[i].name.text] = {
|
||||||
|
namedEntity: "Variable",
|
||||||
|
value: arg,
|
||||||
|
};
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return { resultType: "Value", value: this.withFunction(fnCtx, source.value.ref.function) };
|
||||||
|
}
|
||||||
|
// builtin
|
||||||
|
let allValues = source.value.partial.concat(argValues);
|
||||||
|
return { resultType: "Value", value: source.value.ref.function(allValues) };
|
||||||
|
};
|
||||||
|
|
||||||
|
withStructGetter = (ctx: Context, structGetter: StructGetter): ExpressionResult => {
|
||||||
|
const source = this.withExpression(ctx, structGetter.source);
|
||||||
|
if (source.resultType === "Return") {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
if (source.value.value !== "StructValue") {
|
||||||
|
throw Error("get attr of non-struct");
|
||||||
|
}
|
||||||
|
if (source.value.fields[structGetter.attribute.text]) {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: source.value.fields[structGetter.attribute.text],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for (const impl of source.value.source.impls) {
|
||||||
|
for (const [name, method] of Object.entries(impl.functions)) {
|
||||||
|
if (name === structGetter.attribute.text) {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "FunctionValue", partial: [source.value], ref: method },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not actually possible, but makes the type system happy
|
||||||
|
return { resultType: "Value", value: { value: "UnitValue" } };
|
||||||
|
};
|
||||||
|
|
||||||
|
withOperation = (ctx: Context, op: Operation): ExpressionResult => {
|
||||||
|
const left = this.withExpression(ctx, op.left);
|
||||||
|
if (left.resultType === "Return") {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
const right = this.withExpression(ctx, op.left);
|
||||||
|
if (right.resultType === "Return") {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
if (left.value.value !== "NumericValue" || right.value.value !== "NumericValue") {
|
||||||
|
throw Error("Operation on a Nan");
|
||||||
|
}
|
||||||
|
if (op.op === "+") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: left.value.number + right.value.number },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (op.op === "-") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: left.value.number - right.value.number },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (op.op === "*") {
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: left.value.number * right.value.number },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultType: "Value",
|
||||||
|
value: { value: "NumericValue", number: left.value.number / right.value.number },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user