feat: improve REPL with syntax highlighting and documentation
REPL improvements: - Syntax highlighting for keywords (magenta), types (blue), strings (green), numbers (yellow), comments (gray) - :doc command to show documentation for functions - :browse command to list module exports - Added docs for List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test modules Example usage: lux> :doc List.map lux> :browse String lux> :doc fn Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
325
src/main.rs
325
src/main.rs
@@ -42,6 +42,8 @@ Commands:
|
|||||||
:quit, :q Exit the REPL
|
:quit, :q Exit the REPL
|
||||||
:type <expr> Show the type of an expression
|
:type <expr> Show the type of an expression
|
||||||
:info <name> Show info about a binding
|
:info <name> Show info about a binding
|
||||||
|
:doc <name> Show documentation for a function/type
|
||||||
|
:browse <mod> List exports of a module (List, String, Option, etc.)
|
||||||
:env Show user-defined bindings
|
:env Show user-defined bindings
|
||||||
:clear Clear the environment
|
:clear Clear the environment
|
||||||
:load <file> Load and execute a file
|
:load <file> Load and execute a file
|
||||||
@@ -1500,7 +1502,7 @@ impl LuxHelper {
|
|||||||
|
|
||||||
let commands = vec![
|
let commands = vec![
|
||||||
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
|
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
|
||||||
":trace", ":traces", ":info", ":i", ":env",
|
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
@@ -1588,6 +1590,100 @@ impl Highlighter for LuxHelper {
|
|||||||
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
|
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
|
||||||
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
|
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
|
||||||
|
let mut result = String::with_capacity(line.len() * 2);
|
||||||
|
let mut chars = line.char_indices().peekable();
|
||||||
|
|
||||||
|
while let Some((i, c)) = chars.next() {
|
||||||
|
if c == '"' {
|
||||||
|
// String literal - highlight in green
|
||||||
|
result.push_str("\x1b[32m\"");
|
||||||
|
let mut escaped = false;
|
||||||
|
for (_, ch) in chars.by_ref() {
|
||||||
|
result.push(ch);
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if ch == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if ch == '"' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
} else if c == ':' && i == 0 {
|
||||||
|
// Command - highlight in cyan
|
||||||
|
result.push_str("\x1b[36m:");
|
||||||
|
for (_, ch) in chars.by_ref() {
|
||||||
|
if ch.is_whitespace() {
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
result.push(ch);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
// Continue with the rest
|
||||||
|
for (_, ch) in chars.by_ref() {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
} else if c.is_ascii_digit() || (c == '-' && chars.peek().map(|(_, ch)| ch.is_ascii_digit()).unwrap_or(false)) {
|
||||||
|
// Number - highlight in yellow
|
||||||
|
result.push_str("\x1b[33m");
|
||||||
|
result.push(c);
|
||||||
|
while let Some(&(_, ch)) = chars.peek() {
|
||||||
|
if ch.is_ascii_digit() || ch == '.' {
|
||||||
|
result.push(ch);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
} else if c.is_alphabetic() || c == '_' {
|
||||||
|
// Identifier or keyword
|
||||||
|
let start = i;
|
||||||
|
let mut end = i + c.len_utf8();
|
||||||
|
while let Some(&(j, ch)) = chars.peek() {
|
||||||
|
if ch.is_alphanumeric() || ch == '_' {
|
||||||
|
end = j + ch.len_utf8();
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let word = &line[start..end];
|
||||||
|
|
||||||
|
// Check if it's a keyword
|
||||||
|
if self.keywords.contains(word) {
|
||||||
|
result.push_str("\x1b[35m"); // Magenta for keywords
|
||||||
|
result.push_str(word);
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
} else if word.starts_with(char::is_uppercase) {
|
||||||
|
result.push_str("\x1b[34m"); // Blue for types/constructors
|
||||||
|
result.push_str(word);
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
} else {
|
||||||
|
result.push_str(word);
|
||||||
|
}
|
||||||
|
} else if c == '/' && chars.peek().map(|(_, ch)| *ch == '/').unwrap_or(false) {
|
||||||
|
// Comment - highlight in gray
|
||||||
|
result.push_str("\x1b[90m");
|
||||||
|
result.push(c);
|
||||||
|
for (_, ch) in chars.by_ref() {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cow::Owned(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validator for LuxHelper {}
|
impl Validator for LuxHelper {}
|
||||||
@@ -1789,6 +1885,21 @@ fn handle_command(
|
|||||||
interp.print_traces();
|
interp.print_traces();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
":doc" | ":d" => {
|
||||||
|
if let Some(name) = arg {
|
||||||
|
show_doc(name);
|
||||||
|
} else {
|
||||||
|
println!("Usage: :doc <name>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
":browse" | ":b" => {
|
||||||
|
if let Some(module) = arg {
|
||||||
|
browse_module(module);
|
||||||
|
} else {
|
||||||
|
println!("Usage: :browse <module>");
|
||||||
|
println!("Available modules: List, String, Option, Result, Console, Random, File, Http");
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Unknown command: {}", cmd);
|
println!("Unknown command: {}", cmd);
|
||||||
println!("Type :help for help");
|
println!("Type :help for help");
|
||||||
@@ -1820,6 +1931,218 @@ fn show_environment(checker: &TypeChecker, helper: &LuxHelper) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_doc(name: &str) {
|
||||||
|
// Built-in documentation for common functions
|
||||||
|
let doc = match name {
|
||||||
|
// List functions
|
||||||
|
"List.length" => Some(("List.length : List<T> -> Int", "Returns the number of elements in a list.")),
|
||||||
|
"List.head" => Some(("List.head : List<T> -> Option<T>", "Returns the first element of a list, or None if empty.")),
|
||||||
|
"List.tail" => Some(("List.tail : List<T> -> Option<List<T>>", "Returns all elements except the first, or None if empty.")),
|
||||||
|
"List.map" => Some(("List.map : (List<A>, fn(A) -> B) -> List<B>", "Applies a function to each element, returning a new list.")),
|
||||||
|
"List.filter" => Some(("List.filter : (List<T>, fn(T) -> Bool) -> List<T>", "Returns elements for which the predicate returns true.")),
|
||||||
|
"List.fold" => Some(("List.fold : (List<T>, U, fn(U, T) -> U) -> U", "Reduces a list to a single value using an accumulator function.")),
|
||||||
|
"List.reverse" => Some(("List.reverse : List<T> -> List<T>", "Returns a new list with elements in reverse order.")),
|
||||||
|
"List.concat" => Some(("List.concat : (List<T>, List<T>) -> List<T>", "Concatenates two lists.")),
|
||||||
|
"List.take" => Some(("List.take : (List<T>, Int) -> List<T>", "Returns the first n elements.")),
|
||||||
|
"List.drop" => Some(("List.drop : (List<T>, Int) -> List<T>", "Returns all elements after the first n.")),
|
||||||
|
"List.get" => Some(("List.get : (List<T>, Int) -> Option<T>", "Returns the element at index n, or None if out of bounds.")),
|
||||||
|
"List.contains" => Some(("List.contains : (List<T>, T) -> Bool", "Returns true if the list contains the element.")),
|
||||||
|
"List.all" => Some(("List.all : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if all elements satisfy the predicate.")),
|
||||||
|
"List.any" => Some(("List.any : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if any element satisfies the predicate.")),
|
||||||
|
"List.find" => Some(("List.find : (List<T>, fn(T) -> Bool) -> Option<T>", "Returns the first element satisfying the predicate.")),
|
||||||
|
"List.sort" => Some(("List.sort : List<Int> -> List<Int>", "Sorts a list of integers in ascending order.")),
|
||||||
|
"List.range" => Some(("List.range : (Int, Int) -> List<Int>", "Creates a list of integers from start to end (exclusive).")),
|
||||||
|
|
||||||
|
// String functions
|
||||||
|
"String.length" => Some(("String.length : String -> Int", "Returns the number of characters in a string.")),
|
||||||
|
"String.concat" => Some(("String.concat : (String, String) -> String", "Concatenates two strings.")),
|
||||||
|
"String.substring" => Some(("String.substring : (String, Int, Int) -> String", "Returns a substring from start to end index.")),
|
||||||
|
"String.split" => Some(("String.split : (String, String) -> List<String>", "Splits a string by a delimiter.")),
|
||||||
|
"String.join" => Some(("String.join : (List<String>, String) -> String", "Joins a list of strings with a delimiter.")),
|
||||||
|
"String.trim" => Some(("String.trim : String -> String", "Removes leading and trailing whitespace.")),
|
||||||
|
"String.contains" => Some(("String.contains : (String, String) -> Bool", "Returns true if the string contains the substring.")),
|
||||||
|
"String.replace" => Some(("String.replace : (String, String, String) -> String", "Replaces all occurrences of a pattern.")),
|
||||||
|
"String.startsWith" => Some(("String.startsWith : (String, String) -> Bool", "Returns true if the string starts with the prefix.")),
|
||||||
|
"String.endsWith" => Some(("String.endsWith : (String, String) -> Bool", "Returns true if the string ends with the suffix.")),
|
||||||
|
"String.toUpper" => Some(("String.toUpper : String -> String", "Converts to uppercase.")),
|
||||||
|
"String.toLower" => Some(("String.toLower : String -> String", "Converts to lowercase.")),
|
||||||
|
|
||||||
|
// Option functions
|
||||||
|
"Option.map" => Some(("Option.map : (Option<A>, fn(A) -> B) -> Option<B>", "Applies a function to the value inside Some, or returns None.")),
|
||||||
|
"Option.flatMap" => Some(("Option.flatMap : (Option<A>, fn(A) -> Option<B>) -> Option<B>", "Like map, but the function returns an Option.")),
|
||||||
|
"Option.getOrElse" => Some(("Option.getOrElse : (Option<T>, T) -> T", "Returns the value inside Some, or the default if None.")),
|
||||||
|
"Option.isSome" => Some(("Option.isSome : Option<T> -> Bool", "Returns true if the value is Some.")),
|
||||||
|
"Option.isNone" => Some(("Option.isNone : Option<T> -> Bool", "Returns true if the value is None.")),
|
||||||
|
|
||||||
|
// Result functions
|
||||||
|
"Result.map" => Some(("Result.map : (Result<A, E>, fn(A) -> B) -> Result<B, E>", "Applies a function to the Ok value.")),
|
||||||
|
"Result.flatMap" => Some(("Result.flatMap : (Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>", "Like map, but the function returns a Result.")),
|
||||||
|
"Result.getOrElse" => Some(("Result.getOrElse : (Result<T, E>, T) -> T", "Returns the Ok value, or the default if Err.")),
|
||||||
|
"Result.isOk" => Some(("Result.isOk : Result<T, E> -> Bool", "Returns true if the value is Ok.")),
|
||||||
|
"Result.isErr" => Some(("Result.isErr : Result<T, E> -> Bool", "Returns true if the value is Err.")),
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
"Console.print" => Some(("Console.print : String -> Unit", "Prints a string to the console.")),
|
||||||
|
"Console.readLine" => Some(("Console.readLine : () -> String", "Reads a line from standard input.")),
|
||||||
|
"Random.int" => Some(("Random.int : (Int, Int) -> Int", "Generates a random integer in the range [min, max].")),
|
||||||
|
"Random.float" => Some(("Random.float : () -> Float", "Generates a random float in [0, 1).")),
|
||||||
|
"Random.bool" => Some(("Random.bool : () -> Bool", "Generates a random boolean.")),
|
||||||
|
"File.read" => Some(("File.read : String -> String", "Reads the contents of a file.")),
|
||||||
|
"File.write" => Some(("File.write : (String, String) -> Unit", "Writes content to a file.")),
|
||||||
|
"File.exists" => Some(("File.exists : String -> Bool", "Returns true if the file exists.")),
|
||||||
|
"Http.get" => Some(("Http.get : String -> String", "Performs an HTTP GET request.")),
|
||||||
|
"Http.post" => Some(("Http.post : (String, String) -> String", "Performs an HTTP POST request with a body.")),
|
||||||
|
"Time.now" => Some(("Time.now : () -> Int", "Returns the current Unix timestamp in milliseconds.")),
|
||||||
|
"Sql.open" => Some(("Sql.open : String -> SqlConn", "Opens a SQLite database file.")),
|
||||||
|
"Sql.openMemory" => Some(("Sql.openMemory : () -> SqlConn", "Opens an in-memory SQLite database.")),
|
||||||
|
"Sql.query" => Some(("Sql.query : (SqlConn, String) -> List<Row>", "Executes a SQL query and returns all rows.")),
|
||||||
|
"Sql.execute" => Some(("Sql.execute : (SqlConn, String) -> Int", "Executes a SQL statement and returns affected rows.")),
|
||||||
|
"Postgres.connect" => Some(("Postgres.connect : String -> Int", "Connects to a PostgreSQL database.")),
|
||||||
|
"Postgres.query" => Some(("Postgres.query : (Int, String) -> List<Row>", "Executes a SQL query on PostgreSQL.")),
|
||||||
|
"Postgres.execute" => Some(("Postgres.execute : (Int, String) -> Int", "Executes a SQL statement on PostgreSQL.")),
|
||||||
|
|
||||||
|
// Language constructs
|
||||||
|
"fn" => Some(("fn name(args): Type = body", "Defines a function.")),
|
||||||
|
"let" => Some(("let name = value", "Binds a value to a name.")),
|
||||||
|
"if" => Some(("if condition then expr1 else expr2", "Conditional expression.")),
|
||||||
|
"match" => Some(("match value { pattern => expr, ... }", "Pattern matching expression.")),
|
||||||
|
"effect" => Some(("effect Name { fn op(...): Type }", "Defines an effect with operations.")),
|
||||||
|
"handler" => Some(("handler { op => body }", "Defines an effect handler.")),
|
||||||
|
"with" => Some(("fn f(): T with {Effect1, Effect2}", "Declares effects a function may perform.")),
|
||||||
|
"type" => Some(("type Name<T> = ...", "Defines a type alias.")),
|
||||||
|
"run" => Some(("run expr with { handler }", "Executes an expression with effect handlers.")),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match doc {
|
||||||
|
Some((sig, desc)) => {
|
||||||
|
println!("\x1b[1m{}\x1b[0m", sig);
|
||||||
|
println!();
|
||||||
|
println!(" {}", desc);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("No documentation for '{}'", name);
|
||||||
|
println!("Try :doc List.map or :browse List");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browse_module(module: &str) {
|
||||||
|
let exports: Vec<(&str, &str)> = match module {
|
||||||
|
"List" => vec![
|
||||||
|
("length", "List<T> -> Int"),
|
||||||
|
("head", "List<T> -> Option<T>"),
|
||||||
|
("tail", "List<T> -> Option<List<T>>"),
|
||||||
|
("get", "(List<T>, Int) -> Option<T>"),
|
||||||
|
("map", "(List<A>, fn(A) -> B) -> List<B>"),
|
||||||
|
("filter", "(List<T>, fn(T) -> Bool) -> List<T>"),
|
||||||
|
("fold", "(List<T>, U, fn(U, T) -> U) -> U"),
|
||||||
|
("reverse", "List<T> -> List<T>"),
|
||||||
|
("concat", "(List<T>, List<T>) -> List<T>"),
|
||||||
|
("take", "(List<T>, Int) -> List<T>"),
|
||||||
|
("drop", "(List<T>, Int) -> List<T>"),
|
||||||
|
("contains", "(List<T>, T) -> Bool"),
|
||||||
|
("all", "(List<T>, fn(T) -> Bool) -> Bool"),
|
||||||
|
("any", "(List<T>, fn(T) -> Bool) -> Bool"),
|
||||||
|
("find", "(List<T>, fn(T) -> Bool) -> Option<T>"),
|
||||||
|
("sort", "List<Int> -> List<Int>"),
|
||||||
|
("range", "(Int, Int) -> List<Int>"),
|
||||||
|
("forEach", "(List<T>, fn(T) -> Unit) -> Unit"),
|
||||||
|
],
|
||||||
|
"String" => vec![
|
||||||
|
("length", "String -> Int"),
|
||||||
|
("concat", "(String, String) -> String"),
|
||||||
|
("substring", "(String, Int, Int) -> String"),
|
||||||
|
("split", "(String, String) -> List<String>"),
|
||||||
|
("join", "(List<String>, String) -> String"),
|
||||||
|
("trim", "String -> String"),
|
||||||
|
("contains", "(String, String) -> Bool"),
|
||||||
|
("replace", "(String, String, String) -> String"),
|
||||||
|
("startsWith", "(String, String) -> Bool"),
|
||||||
|
("endsWith", "(String, String) -> Bool"),
|
||||||
|
("toUpper", "String -> String"),
|
||||||
|
("toLower", "String -> String"),
|
||||||
|
("lines", "String -> List<String>"),
|
||||||
|
],
|
||||||
|
"Option" => vec![
|
||||||
|
("map", "(Option<A>, fn(A) -> B) -> Option<B>"),
|
||||||
|
("flatMap", "(Option<A>, fn(A) -> Option<B>) -> Option<B>"),
|
||||||
|
("getOrElse", "(Option<T>, T) -> T"),
|
||||||
|
("isSome", "Option<T> -> Bool"),
|
||||||
|
("isNone", "Option<T> -> Bool"),
|
||||||
|
],
|
||||||
|
"Result" => vec![
|
||||||
|
("map", "(Result<A, E>, fn(A) -> B) -> Result<B, E>"),
|
||||||
|
("flatMap", "(Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>"),
|
||||||
|
("getOrElse", "(Result<T, E>, T) -> T"),
|
||||||
|
("isOk", "Result<T, E> -> Bool"),
|
||||||
|
("isErr", "Result<T, E> -> Bool"),
|
||||||
|
],
|
||||||
|
"Console" => vec![
|
||||||
|
("print", "String -> Unit"),
|
||||||
|
("readLine", "() -> String"),
|
||||||
|
],
|
||||||
|
"Random" => vec![
|
||||||
|
("int", "(Int, Int) -> Int"),
|
||||||
|
("float", "() -> Float"),
|
||||||
|
("bool", "() -> Bool"),
|
||||||
|
],
|
||||||
|
"File" => vec![
|
||||||
|
("read", "String -> String"),
|
||||||
|
("write", "(String, String) -> Unit"),
|
||||||
|
("exists", "String -> Bool"),
|
||||||
|
("delete", "String -> Unit"),
|
||||||
|
],
|
||||||
|
"Http" => vec![
|
||||||
|
("get", "String -> String"),
|
||||||
|
("post", "(String, String) -> String"),
|
||||||
|
],
|
||||||
|
"Time" => vec![
|
||||||
|
("now", "() -> Int"),
|
||||||
|
],
|
||||||
|
"Sql" => vec![
|
||||||
|
("open", "String -> SqlConn"),
|
||||||
|
("openMemory", "() -> SqlConn"),
|
||||||
|
("close", "SqlConn -> Unit"),
|
||||||
|
("query", "(SqlConn, String) -> List<Row>"),
|
||||||
|
("queryOne", "(SqlConn, String) -> Option<Row>"),
|
||||||
|
("execute", "(SqlConn, String) -> Int"),
|
||||||
|
("beginTx", "SqlConn -> Unit"),
|
||||||
|
("commit", "SqlConn -> Unit"),
|
||||||
|
("rollback", "SqlConn -> Unit"),
|
||||||
|
],
|
||||||
|
"Postgres" => vec![
|
||||||
|
("connect", "String -> Int"),
|
||||||
|
("close", "Int -> Unit"),
|
||||||
|
("query", "(Int, String) -> List<Row>"),
|
||||||
|
("queryOne", "(Int, String) -> Option<Row>"),
|
||||||
|
("execute", "(Int, String) -> Int"),
|
||||||
|
("beginTx", "Int -> Unit"),
|
||||||
|
("commit", "Int -> Unit"),
|
||||||
|
("rollback", "Int -> Unit"),
|
||||||
|
],
|
||||||
|
"Test" => vec![
|
||||||
|
("assert", "(Bool, String) -> Unit"),
|
||||||
|
("assertEqual", "(T, T) -> Unit"),
|
||||||
|
("assertTrue", "Bool -> Unit"),
|
||||||
|
("assertFalse", "Bool -> Unit"),
|
||||||
|
("fail", "String -> Unit"),
|
||||||
|
],
|
||||||
|
_ => {
|
||||||
|
println!("Unknown module: {}", module);
|
||||||
|
println!("Available modules: List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("\x1b[1mmodule {}\x1b[0m", module);
|
||||||
|
println!();
|
||||||
|
for (name, sig) in exports {
|
||||||
|
println!(" \x1b[34m{}.{}\x1b[0m : {}", module, name, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_type(expr_str: &str, checker: &mut TypeChecker) {
|
fn show_type(expr_str: &str, checker: &mut TypeChecker) {
|
||||||
// Wrap expression in a let to parse it
|
// Wrap expression in a let to parse it
|
||||||
let wrapped = format!("let _expr_ = {}", expr_str);
|
let wrapped = format!("let _expr_ = {}", expr_str);
|
||||||
|
|||||||
Reference in New Issue
Block a user