mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 11:41:56 +03:00
Implement print_err
and readln
(#754)
This commit is contained in:
parent
81bde28589
commit
ad9eb285fa
@ -0,0 +1,61 @@
|
|||||||
|
package org.enso.interpreter.node.expression.builtin.io;
|
||||||
|
|
||||||
|
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||||
|
import com.oracle.truffle.api.dsl.CachedContext;
|
||||||
|
import com.oracle.truffle.api.dsl.Specialization;
|
||||||
|
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||||
|
import com.oracle.truffle.api.nodes.NodeInfo;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import org.enso.interpreter.Language;
|
||||||
|
import org.enso.interpreter.node.expression.builtin.BuiltinRootNode;
|
||||||
|
import org.enso.interpreter.runtime.Context;
|
||||||
|
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
|
||||||
|
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition.ExecutionMode;
|
||||||
|
import org.enso.interpreter.runtime.callable.function.Function;
|
||||||
|
import org.enso.interpreter.runtime.callable.function.FunctionSchema.CallStrategy;
|
||||||
|
import org.enso.interpreter.runtime.state.Stateful;
|
||||||
|
|
||||||
|
@NodeInfo(shortName = "IO.print_err", description = "Prints its argument to standard error.")
|
||||||
|
public abstract class PrintErrNode extends BuiltinRootNode {
|
||||||
|
PrintErrNode(Language language) {
|
||||||
|
super(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Specialization
|
||||||
|
Stateful doPrint(VirtualFrame frame, @CachedContext(Language.class) Context ctx) {
|
||||||
|
print(ctx.getErr(), Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[1]);
|
||||||
|
Object state = Function.ArgumentsHelper.getState(frame.getArguments());
|
||||||
|
|
||||||
|
return new Stateful(state, ctx.getUnit().newInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
@TruffleBoundary
|
||||||
|
private void print(PrintStream err, Object object) {
|
||||||
|
err.println(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Function} object ignoring its first argument and printing the second to the
|
||||||
|
* standard error stream.
|
||||||
|
*
|
||||||
|
* @param language the current {@link Language} instance
|
||||||
|
* @return a {@link Function} object wrapping the behavior of this node
|
||||||
|
*/
|
||||||
|
public static Function makeFunction(Language language) {
|
||||||
|
return Function.fromBuiltinRootNode(
|
||||||
|
PrintErrNodeGen.create(language),
|
||||||
|
CallStrategy.ALWAYS_DIRECT,
|
||||||
|
new ArgumentDefinition(0, "this", ExecutionMode.EXECUTE),
|
||||||
|
new ArgumentDefinition(1, "value", ExecutionMode.EXECUTE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the source-level name of this node.
|
||||||
|
*
|
||||||
|
* @return the source-level name of this node
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "IO.print_err";
|
||||||
|
}
|
||||||
|
}
|
@ -15,22 +15,22 @@ import org.enso.interpreter.runtime.callable.function.FunctionSchema;
|
|||||||
import org.enso.interpreter.runtime.state.Stateful;
|
import org.enso.interpreter.runtime.state.Stateful;
|
||||||
|
|
||||||
/** Allows for printing arbitrary values to the standard output. */
|
/** Allows for printing arbitrary values to the standard output. */
|
||||||
@NodeInfo(shortName = "IO.println", description = "Root of the IO.println method.")
|
@NodeInfo(shortName = "IO.println", description = "Prints its argument to standard out.")
|
||||||
public abstract class PrintNode extends BuiltinRootNode {
|
public abstract class PrintlnNode extends BuiltinRootNode {
|
||||||
PrintNode(Language language) {
|
PrintlnNode(Language language) {
|
||||||
super(language);
|
super(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Specialization
|
@Specialization
|
||||||
Stateful doPrint(VirtualFrame frame, @CachedContext(Language.class) Context ctx) {
|
Stateful doPrint(VirtualFrame frame, @CachedContext(Language.class) Context ctx) {
|
||||||
doPrint(ctx.getOut(), Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[1]);
|
print(ctx.getOut(), Function.ArgumentsHelper.getPositionalArguments(frame.getArguments())[1]);
|
||||||
Object state = Function.ArgumentsHelper.getState(frame.getArguments());
|
Object state = Function.ArgumentsHelper.getState(frame.getArguments());
|
||||||
|
|
||||||
return new Stateful(state, ctx.getUnit().newInstance());
|
return new Stateful(state, ctx.getUnit().newInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
@CompilerDirectives.TruffleBoundary
|
@CompilerDirectives.TruffleBoundary
|
||||||
private void doPrint(PrintStream out, Object object) {
|
private void print(PrintStream out, Object object) {
|
||||||
out.println(object);
|
out.println(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ public abstract class PrintNode extends BuiltinRootNode {
|
|||||||
*/
|
*/
|
||||||
public static Function makeFunction(Language language) {
|
public static Function makeFunction(Language language) {
|
||||||
return Function.fromBuiltinRootNode(
|
return Function.fromBuiltinRootNode(
|
||||||
PrintNodeGen.create(language),
|
PrintlnNodeGen.create(language),
|
||||||
FunctionSchema.CallStrategy.ALWAYS_DIRECT,
|
FunctionSchema.CallStrategy.ALWAYS_DIRECT,
|
||||||
new ArgumentDefinition(0, "this", ArgumentDefinition.ExecutionMode.EXECUTE),
|
new ArgumentDefinition(0, "this", ArgumentDefinition.ExecutionMode.EXECUTE),
|
||||||
new ArgumentDefinition(1, "value", ArgumentDefinition.ExecutionMode.EXECUTE));
|
new ArgumentDefinition(1, "value", ArgumentDefinition.ExecutionMode.EXECUTE));
|
@ -0,0 +1,65 @@
|
|||||||
|
package org.enso.interpreter.node.expression.builtin.io;
|
||||||
|
|
||||||
|
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||||
|
import com.oracle.truffle.api.dsl.CachedContext;
|
||||||
|
import com.oracle.truffle.api.dsl.Specialization;
|
||||||
|
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||||
|
import com.oracle.truffle.api.nodes.NodeInfo;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.enso.interpreter.Language;
|
||||||
|
import org.enso.interpreter.node.expression.builtin.BuiltinRootNode;
|
||||||
|
import org.enso.interpreter.runtime.Context;
|
||||||
|
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
|
||||||
|
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition.ExecutionMode;
|
||||||
|
import org.enso.interpreter.runtime.callable.function.Function;
|
||||||
|
import org.enso.interpreter.runtime.callable.function.FunctionSchema.CallStrategy;
|
||||||
|
import org.enso.interpreter.runtime.error.RuntimeError;
|
||||||
|
import org.enso.interpreter.runtime.state.Stateful;
|
||||||
|
|
||||||
|
@NodeInfo(shortName = "IO.readln", description = "Reads a line from standard in.")
|
||||||
|
public abstract class ReadlnNode extends BuiltinRootNode {
|
||||||
|
public ReadlnNode(Language language) {
|
||||||
|
super(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Specialization
|
||||||
|
Stateful doRead(VirtualFrame frame, @CachedContext(Language.class) Context ctx) {
|
||||||
|
return read(ctx.getIn(), Function.ArgumentsHelper.getState(frame.getArguments()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@TruffleBoundary
|
||||||
|
private Stateful read(BufferedReader in, Object state) {
|
||||||
|
try {
|
||||||
|
String str = in.readLine();
|
||||||
|
|
||||||
|
return new Stateful(state, str);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return new Stateful(state, new RuntimeError("Empty input stream."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Function} object ignoring its first argument and reading from the standard
|
||||||
|
* input stream.
|
||||||
|
*
|
||||||
|
* @param language the current {@link Language} instance
|
||||||
|
* @return a {@link Function} object wrapping the behavior of this node
|
||||||
|
*/
|
||||||
|
public static Function makeFunction(Language language) {
|
||||||
|
return Function.fromBuiltinRootNode(
|
||||||
|
ReadlnNodeGen.create(language),
|
||||||
|
CallStrategy.ALWAYS_DIRECT,
|
||||||
|
new ArgumentDefinition(0, "this", ExecutionMode.EXECUTE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the source-level name of this node.
|
||||||
|
*
|
||||||
|
* @return the source-level name of the node
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "IO.readln";
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.interpreter.node.expression.builtin.io;
|
package org.enso.interpreter.node.expression.builtin.system;
|
||||||
|
|
||||||
import com.oracle.truffle.api.CompilerDirectives;
|
import com.oracle.truffle.api.CompilerDirectives;
|
||||||
import com.oracle.truffle.api.frame.VirtualFrame;
|
import com.oracle.truffle.api.frame.VirtualFrame;
|
||||||
@ -10,7 +10,7 @@ import org.enso.interpreter.runtime.callable.function.Function;
|
|||||||
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
|
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
|
||||||
import org.enso.interpreter.runtime.state.Stateful;
|
import org.enso.interpreter.runtime.state.Stateful;
|
||||||
|
|
||||||
@NodeInfo(shortName = "IO.nano_time", description = "Gets the nanosecond resolution system time.")
|
@NodeInfo(shortName = "System.nano_time", description = "Gets the nanosecond resolution system time.")
|
||||||
public final class NanoTimeNode extends BuiltinRootNode {
|
public final class NanoTimeNode extends BuiltinRootNode {
|
||||||
private NanoTimeNode(Language language) {
|
private NanoTimeNode(Language language) {
|
||||||
super(language);
|
super(language);
|
||||||
@ -47,6 +47,6 @@ public final class NanoTimeNode extends BuiltinRootNode {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "IO.nano_time";
|
return "System.nano_time";
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,8 +14,10 @@ import org.enso.interpreter.node.expression.builtin.function.ExplicitCallFunctio
|
|||||||
import org.enso.interpreter.node.expression.builtin.interop.generic.*;
|
import org.enso.interpreter.node.expression.builtin.interop.generic.*;
|
||||||
import org.enso.interpreter.node.expression.builtin.interop.syntax.MethodDispatchNode;
|
import org.enso.interpreter.node.expression.builtin.interop.syntax.MethodDispatchNode;
|
||||||
import org.enso.interpreter.node.expression.builtin.interop.syntax.ConstructorDispatchNode;
|
import org.enso.interpreter.node.expression.builtin.interop.syntax.ConstructorDispatchNode;
|
||||||
import org.enso.interpreter.node.expression.builtin.io.NanoTimeNode;
|
import org.enso.interpreter.node.expression.builtin.io.PrintErrNode;
|
||||||
import org.enso.interpreter.node.expression.builtin.io.PrintNode;
|
import org.enso.interpreter.node.expression.builtin.io.PrintlnNode;
|
||||||
|
import org.enso.interpreter.node.expression.builtin.io.ReadlnNode;
|
||||||
|
import org.enso.interpreter.node.expression.builtin.system.NanoTimeNode;
|
||||||
import org.enso.interpreter.node.expression.builtin.interop.java.*;
|
import org.enso.interpreter.node.expression.builtin.interop.java.*;
|
||||||
import org.enso.interpreter.node.expression.builtin.number.AddNode;
|
import org.enso.interpreter.node.expression.builtin.number.AddNode;
|
||||||
import org.enso.interpreter.node.expression.builtin.number.DivideNode;
|
import org.enso.interpreter.node.expression.builtin.number.DivideNode;
|
||||||
@ -94,6 +96,7 @@ public class Builtins {
|
|||||||
new ArgumentDefinition(0, "head", ArgumentDefinition.ExecutionMode.EXECUTE),
|
new ArgumentDefinition(0, "head", ArgumentDefinition.ExecutionMode.EXECUTE),
|
||||||
new ArgumentDefinition(1, "rest", ArgumentDefinition.ExecutionMode.EXECUTE));
|
new ArgumentDefinition(1, "rest", ArgumentDefinition.ExecutionMode.EXECUTE));
|
||||||
AtomConstructor io = new AtomConstructor("IO", scope).initializeFields();
|
AtomConstructor io = new AtomConstructor("IO", scope).initializeFields();
|
||||||
|
AtomConstructor system = new AtomConstructor("System", scope).initializeFields();
|
||||||
AtomConstructor panic = new AtomConstructor("Panic", scope).initializeFields();
|
AtomConstructor panic = new AtomConstructor("Panic", scope).initializeFields();
|
||||||
AtomConstructor error = new AtomConstructor("Error", scope).initializeFields();
|
AtomConstructor error = new AtomConstructor("Error", scope).initializeFields();
|
||||||
AtomConstructor state = new AtomConstructor("State", scope).initializeFields();
|
AtomConstructor state = new AtomConstructor("State", scope).initializeFields();
|
||||||
@ -120,8 +123,11 @@ public class Builtins {
|
|||||||
scope.registerConstructor(java);
|
scope.registerConstructor(java);
|
||||||
scope.registerConstructor(createPolyglot(language));
|
scope.registerConstructor(createPolyglot(language));
|
||||||
|
|
||||||
scope.registerMethod(io, "println", PrintNode.makeFunction(language));
|
scope.registerMethod(io, "println", PrintlnNode.makeFunction(language));
|
||||||
scope.registerMethod(io, "nano_time", NanoTimeNode.makeFunction(language));
|
scope.registerMethod(io, "print_err", PrintErrNode.makeFunction(language));
|
||||||
|
scope.registerMethod(io, "readln", ReadlnNode.makeFunction(language));
|
||||||
|
|
||||||
|
scope.registerMethod(system, "nano_time", NanoTimeNode.makeFunction(language));
|
||||||
|
|
||||||
scope.registerMethod(panic, "throw", PanicNode.makeFunction(language));
|
scope.registerMethod(panic, "throw", PanicNode.makeFunction(language));
|
||||||
scope.registerMethod(panic, "recover", CatchPanicNode.makeFunction(language));
|
scope.registerMethod(panic, "recover", CatchPanicNode.makeFunction(language));
|
||||||
|
@ -3,7 +3,11 @@ package org.enso.interpreter.runtime;
|
|||||||
import com.oracle.truffle.api.TruffleFile;
|
import com.oracle.truffle.api.TruffleFile;
|
||||||
import com.oracle.truffle.api.TruffleLanguage;
|
import com.oracle.truffle.api.TruffleLanguage;
|
||||||
import com.oracle.truffle.api.TruffleLanguage.Env;
|
import com.oracle.truffle.api.TruffleLanguage.Env;
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -30,6 +34,8 @@ public class Context {
|
|||||||
private final Env environment;
|
private final Env environment;
|
||||||
private final Compiler compiler;
|
private final Compiler compiler;
|
||||||
private final PrintStream out;
|
private final PrintStream out;
|
||||||
|
private final PrintStream err;
|
||||||
|
private final BufferedReader in;
|
||||||
private final List<Package> packages;
|
private final List<Package> packages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,6 +48,8 @@ public class Context {
|
|||||||
this.language = language;
|
this.language = language;
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.out = new PrintStream(environment.out());
|
this.out = new PrintStream(environment.out());
|
||||||
|
this.err = new PrintStream(environment.err());
|
||||||
|
this.in = new BufferedReader(new InputStreamReader(environment.in()));
|
||||||
|
|
||||||
List<File> packagePaths = OptionsHelper.getPackagesPaths(environment);
|
List<File> packagePaths = OptionsHelper.getPackagesPaths(environment);
|
||||||
|
|
||||||
@ -125,6 +133,24 @@ public class Context {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the standard error stream for this context.
|
||||||
|
*
|
||||||
|
* @return the standard error stream for this context
|
||||||
|
*/
|
||||||
|
public PrintStream getErr() {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the standard input stream for this context.
|
||||||
|
*
|
||||||
|
* @return the standard input stream for this context
|
||||||
|
*/
|
||||||
|
public BufferedReader getIn() {
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new module scope that automatically imports all the builtin types and methods.
|
* Creates a new module scope that automatically imports all the builtin types and methods.
|
||||||
*
|
*
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package org.enso.interpreter.test
|
package org.enso.interpreter.test
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.{
|
||||||
|
ByteArrayOutputStream,
|
||||||
|
PipedInputStream,
|
||||||
|
PipedOutputStream,
|
||||||
|
PrintStream
|
||||||
|
}
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
import com.oracle.truffle.api.instrumentation.EventBinding
|
import com.oracle.truffle.api.instrumentation.EventBinding
|
||||||
@ -72,12 +77,19 @@ trait InterpreterRunner {
|
|||||||
value.execute(l.map(_.asInstanceOf[AnyRef]): _*)
|
value.execute(l.map(_.asInstanceOf[AnyRef]): _*)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val output = new ByteArrayOutputStream()
|
val output = new ByteArrayOutputStream()
|
||||||
|
val err = new ByteArrayOutputStream()
|
||||||
|
val inOut = new PipedOutputStream()
|
||||||
|
val inOutPrinter = new PrintStream(inOut, true)
|
||||||
|
val in = new PipedInputStream(inOut)
|
||||||
|
|
||||||
val ctx = Context
|
val ctx = Context
|
||||||
.newBuilder(LanguageInfo.ID)
|
.newBuilder(LanguageInfo.ID)
|
||||||
.allowExperimentalOptions(true)
|
.allowExperimentalOptions(true)
|
||||||
.allowAllAccess(true)
|
.allowAllAccess(true)
|
||||||
.out(output)
|
.out(output)
|
||||||
|
.err(err)
|
||||||
|
.in(in)
|
||||||
.build()
|
.build()
|
||||||
lazy val executionContext = new PolyglotContext(ctx)
|
lazy val executionContext = new PolyglotContext(ctx)
|
||||||
|
|
||||||
@ -127,12 +139,22 @@ trait InterpreterRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def consumeErr: List[String] = {
|
||||||
|
val result = err.toString
|
||||||
|
err.reset()
|
||||||
|
result.linesIterator.toList
|
||||||
|
}
|
||||||
|
|
||||||
def consumeOut: List[String] = {
|
def consumeOut: List[String] = {
|
||||||
val result = output.toString
|
val result = output.toString
|
||||||
output.reset()
|
output.reset()
|
||||||
result.linesIterator.toList
|
result.linesIterator.toList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def feedInput(string: String): Unit = {
|
||||||
|
inOutPrinter.println(string)
|
||||||
|
}
|
||||||
|
|
||||||
def getReplInstrument: ReplDebuggerInstrument = {
|
def getReplInstrument: ReplDebuggerInstrument = {
|
||||||
ctx.getEngine.getInstruments
|
ctx.getEngine.getInstruments
|
||||||
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
|
.get(ReplDebuggerInstrument.INSTRUMENT_ID)
|
||||||
|
@ -64,4 +64,31 @@ class TextTest extends InterpreterTest {
|
|||||||
eval(code)
|
eval(code)
|
||||||
consumeOut shouldEqual List("\"Grzegorz Brzeczyszczykiewicz\"")
|
consumeOut shouldEqual List("\"Grzegorz Brzeczyszczykiewicz\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"Text literals" should "be able to be printed to standard error" in {
|
||||||
|
val errString = "\"My error string\""
|
||||||
|
val resultStr = errString.drop(1).dropRight(1)
|
||||||
|
|
||||||
|
val code =
|
||||||
|
s"""
|
||||||
|
|main = IO.print_err $errString
|
||||||
|
|""".stripMargin
|
||||||
|
|
||||||
|
eval(code)
|
||||||
|
consumeErr shouldEqual List(resultStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
"Text literals" should "be able to be read from standard in" in {
|
||||||
|
val inputString = "foobarbaz"
|
||||||
|
|
||||||
|
val code =
|
||||||
|
"""
|
||||||
|
|main =
|
||||||
|
| IO.readln + " yay!"
|
||||||
|
|""".stripMargin
|
||||||
|
|
||||||
|
feedInput(inputString)
|
||||||
|
|
||||||
|
eval(code) shouldEqual "foobarbaz yay!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,6 @@ class ParserTest extends AnyFlatSpec with Matchers {
|
|||||||
val module = Parser().run(input)
|
val module = Parser().run(input)
|
||||||
val idmap1 = module.idMap
|
val idmap1 = module.idMap
|
||||||
val idmap2 = Parser().run(new Reader(input), idmap1).idMap
|
val idmap2 = Parser().run(new Reader(input), idmap1).idMap
|
||||||
println(module.zipWithOffset)
|
|
||||||
assertSpan(input, module)
|
assertSpan(input, module)
|
||||||
assert(module.show() == new Reader(input).toString())
|
assert(module.show() == new Reader(input).toString())
|
||||||
assert(idmap1 == idmap2)
|
assert(idmap1 == idmap2)
|
||||||
|
Loading…
Reference in New Issue
Block a user