checkpoint; builds again
This commit is contained in:
325
src/ir/ast.rs
325
src/ir/ast.rs
@@ -9,36 +9,58 @@ use proptest::{
|
||||
prelude::Arbitrary,
|
||||
strategy::{BoxedStrategy, Strategy},
|
||||
};
|
||||
use std::{fmt, str::FromStr};
|
||||
use std::{fmt, str::FromStr, sync::atomic::AtomicUsize};
|
||||
|
||||
/// We're going to represent variables as interned strings.
|
||||
///
|
||||
/// These should be fast enough for comparison that it's OK, since it's going to end up
|
||||
/// being pretty much the pointer to the string.
|
||||
type Variable = ArcIntern<String>;
|
||||
pub type Variable = ArcIntern<String>;
|
||||
|
||||
/// Generate a new symbol that is guaranteed to be different from every other symbol
|
||||
/// currently known.
|
||||
///
|
||||
/// This function will use the provided string as a base name for the symbol, but
|
||||
/// extend it with numbers and characters to make it unique. While technically you
|
||||
/// could roll-over these symbols, you probably don't need to worry about it.
|
||||
pub fn gensym(base: &str) -> Variable {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
ArcIntern::new(format!(
|
||||
"{}<{}>",
|
||||
base,
|
||||
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
|
||||
))
|
||||
}
|
||||
|
||||
/// The representation of a program within our IR. For now, this is exactly one file.
|
||||
///
|
||||
/// In addition, for the moment there's not really much of interest to hold here besides
|
||||
/// the list of statements read from the file. Order is important. In the future, you
|
||||
/// could imagine caching analysis information in this structure.
|
||||
/// A program consists of a series of statements and functions. The statements should
|
||||
/// be executed in order. The functions currently may not reference any variables
|
||||
/// at the top level, so their order only matters in relation to each other (functions
|
||||
/// may not be referenced before they are defined).
|
||||
///
|
||||
/// `Program` implements both [`Pretty`] and [`Arbitrary`]. The former should be used
|
||||
/// to print the structure whenever possible, especially if you value your or your
|
||||
/// user's time. The latter is useful for testing that conversions of `Program` retain
|
||||
/// their meaning. All `Program`s generated through [`Arbitrary`] are guaranteed to be
|
||||
/// syntactically valid, although they may contain runtime issue like over- or underflow.
|
||||
///
|
||||
/// The type variable is, somewhat confusingly, the current definition of a type within
|
||||
/// the IR. Since the makeup of this structure may change over the life of the compiler,
|
||||
/// it's easiest to just make it an argument.
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
pub struct Program<Type> {
|
||||
// For now, a program is just a vector of statements. In the future, we'll probably
|
||||
// extend this to include a bunch of other information, but for now: just a list.
|
||||
pub(crate) items: Vec<TopLevel>,
|
||||
pub(crate) items: Vec<TopLevel<Type>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Program
|
||||
impl<'a, 'b, D, A, Type> Pretty<'a, D, A> for &'b Program<Type>
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
&'b Type: Pretty<'a, D, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
let mut result = allocator.nil();
|
||||
@@ -56,17 +78,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Program {
|
||||
impl<Type: core::fmt::Debug> Arbitrary for Program<Type> {
|
||||
type Parameters = crate::syntax::arbitrary::GenerationEnvironment;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||
crate::syntax::Program::arbitrary_with(args)
|
||||
.prop_map(|x| {
|
||||
x.type_infer()
|
||||
.expect("arbitrary_with should generate type-correct programs")
|
||||
})
|
||||
.boxed()
|
||||
unimplemented!()
|
||||
//crate::syntax::Program::arbitrary_with(args)
|
||||
// .prop_map(|x| {
|
||||
// x.type_infer()
|
||||
// .expect("arbitrary_with should generate type-correct programs")
|
||||
// })
|
||||
// .boxed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,84 +99,35 @@ impl Arbitrary for Program {
|
||||
/// will likely be added in the future, but for now: just statements
|
||||
/// and functions
|
||||
#[derive(Debug)]
|
||||
pub enum TopLevel {
|
||||
Statement(Statement),
|
||||
Function(Variable, Vec<Variable>, Vec<Statement>, Expression),
|
||||
pub enum TopLevel<Type> {
|
||||
Statement(Expression<Type>),
|
||||
Function(Variable, Vec<(Variable, Type)>, Type, Expression<Type>),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b TopLevel
|
||||
impl<'a, 'b, D, A, Type> Pretty<'a, D, A> for &'b TopLevel<Type>
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
&'b Type: Pretty<'a, D, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
TopLevel::Function(name, args, stmts, expr) => {
|
||||
let base = allocator
|
||||
.text("function")
|
||||
.append(allocator.space())
|
||||
.append(allocator.text(name.as_ref().to_string()))
|
||||
.append(allocator.space())
|
||||
.append(
|
||||
pretty_comma_separated(
|
||||
allocator,
|
||||
&args.iter().map(PrettySymbol::from).collect(),
|
||||
)
|
||||
.parens(),
|
||||
)
|
||||
.append(allocator.space());
|
||||
|
||||
let mut body = allocator.nil();
|
||||
for stmt in stmts {
|
||||
body = body
|
||||
.append(stmt.pretty(allocator))
|
||||
.append(allocator.text(";"))
|
||||
.append(allocator.hardline());
|
||||
}
|
||||
body = body.append(expr.pretty(allocator));
|
||||
body = body.append(allocator.hardline());
|
||||
body = body.braces();
|
||||
base.append(body)
|
||||
}
|
||||
|
||||
TopLevel::Statement(stmt) => stmt.pretty(allocator),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of a statement in the language.
|
||||
///
|
||||
/// For now, this is either a binding site (`x = 4`) or a print statement
|
||||
/// (`print x`). Someday, though, more!
|
||||
///
|
||||
/// As with `Program`, this type implements [`Pretty`], which should
|
||||
/// be used to display the structure whenever possible. It does not
|
||||
/// implement [`Arbitrary`], though, mostly because it's slightly
|
||||
/// complicated to do so.
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub enum Statement {
|
||||
Binding(Location, Variable, Type, Expression),
|
||||
Print(Location, Type, Variable),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Statement
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
Statement::Binding(_, var, _, expr) => allocator
|
||||
.text(var.as_ref().to_string())
|
||||
TopLevel::Function(name, args, _, expr) => allocator
|
||||
.text("function")
|
||||
.append(allocator.space())
|
||||
.append(allocator.text("="))
|
||||
.append(allocator.text(name.as_ref().to_string()))
|
||||
.append(allocator.space())
|
||||
.append(
|
||||
pretty_comma_separated(
|
||||
allocator,
|
||||
&args.iter().map(|(x, _)| PrettySymbol::from(x)).collect(),
|
||||
)
|
||||
.parens(),
|
||||
)
|
||||
.append(allocator.space())
|
||||
.append(expr.pretty(allocator)),
|
||||
Statement::Print(_, _, var) => allocator
|
||||
.text("print")
|
||||
.append(allocator.space())
|
||||
.append(allocator.text(var.as_ref().to_string())),
|
||||
|
||||
TopLevel::Statement(stmt) => stmt.pretty(allocator),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,21 +145,27 @@ where
|
||||
/// that the referenced data will always either be a constant or a
|
||||
/// variable reference.
|
||||
#[derive(Debug)]
|
||||
pub enum Expression {
|
||||
Atomic(ValueOrRef),
|
||||
Cast(Location, Type, ValueOrRef),
|
||||
Primitive(Location, Type, Primitive, Vec<ValueOrRef>),
|
||||
pub enum Expression<Type> {
|
||||
Atomic(ValueOrRef<Type>),
|
||||
Cast(Location, Type, ValueOrRef<Type>),
|
||||
Primitive(Location, Type, Primitive, Vec<ValueOrRef<Type>>),
|
||||
Block(Location, Type, Vec<Expression<Type>>),
|
||||
Print(Location, Variable),
|
||||
Bind(Location, Variable, Type, Box<Expression<Type>>),
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
impl<Type: Clone + TypeWithVoid> Expression<Type> {
|
||||
/// Return a reference to the type of the expression, as inferred or recently
|
||||
/// computed.
|
||||
pub fn type_of(&self) -> &Type {
|
||||
pub fn type_of(&self) -> Type {
|
||||
match self {
|
||||
Expression::Atomic(ValueOrRef::Ref(_, t, _)) => t,
|
||||
Expression::Atomic(ValueOrRef::Value(_, t, _)) => t,
|
||||
Expression::Cast(_, t, _) => t,
|
||||
Expression::Primitive(_, t, _, _) => t,
|
||||
Expression::Atomic(ValueOrRef::Ref(_, t, _)) => t.clone(),
|
||||
Expression::Atomic(ValueOrRef::Value(_, t, _)) => t.clone(),
|
||||
Expression::Cast(_, t, _) => t.clone(),
|
||||
Expression::Primitive(_, t, _, _) => t.clone(),
|
||||
Expression::Block(_, t, _) => t.clone(),
|
||||
Expression::Print(_, _) => Type::void(),
|
||||
Expression::Bind(_, _, _, _) => Type::void(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,14 +176,18 @@ impl Expression {
|
||||
Expression::Atomic(ValueOrRef::Value(l, _, _)) => l,
|
||||
Expression::Cast(l, _, _) => l,
|
||||
Expression::Primitive(l, _, _, _) => l,
|
||||
Expression::Block(l, _, _) => l,
|
||||
Expression::Print(l, _) => l,
|
||||
Expression::Bind(l, _, _, _) => l,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Expression
|
||||
impl<'a, 'b, D, A, Type> Pretty<'a, D, A> for &'b Expression<Type>
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
&'b Type: Pretty<'a, D, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
@@ -229,6 +213,35 @@ where
|
||||
Expression::Primitive(_, _, op, exprs) => {
|
||||
allocator.text(format!("!!{:?} with {} arguments!!", op, exprs.len()))
|
||||
}
|
||||
Expression::Block(_, _, exprs) => match exprs.split_last() {
|
||||
None => allocator.text("()"),
|
||||
Some((last, &[])) => last.pretty(allocator),
|
||||
Some((last, start)) => {
|
||||
let mut result = allocator.text("{").append(allocator.hardline());
|
||||
|
||||
for stmt in start.iter() {
|
||||
result = result
|
||||
.append(stmt.pretty(allocator))
|
||||
.append(allocator.text(";"))
|
||||
.append(allocator.hardline());
|
||||
}
|
||||
|
||||
result
|
||||
.append(last.pretty(allocator))
|
||||
.append(allocator.hardline())
|
||||
.append(allocator.text("}"))
|
||||
}
|
||||
},
|
||||
Expression::Print(_, var) => allocator
|
||||
.text("print")
|
||||
.append(allocator.space())
|
||||
.append(allocator.text(var.as_ref().to_string())),
|
||||
Expression::Bind(_, var, _, expr) => allocator
|
||||
.text(var.as_ref().to_string())
|
||||
.append(allocator.space())
|
||||
.append(allocator.text("="))
|
||||
.append(allocator.space())
|
||||
.append(expr.pretty(allocator)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,12 +301,12 @@ impl fmt::Display for Primitive {
|
||||
/// at this level. Instead, expressions that take arguments take one
|
||||
/// of these, which can only be a constant or a reference.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ValueOrRef {
|
||||
pub enum ValueOrRef<Type> {
|
||||
Value(Location, Type, Value),
|
||||
Ref(Location, Type, ArcIntern<String>),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b ValueOrRef
|
||||
impl<'a, 'b, D, A, Type> Pretty<'a, D, A> for &'b ValueOrRef<Type>
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
@@ -306,8 +319,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValueOrRef> for Expression {
|
||||
fn from(value: ValueOrRef) -> Self {
|
||||
impl<Type> From<ValueOrRef<Type>> for Expression<Type> {
|
||||
fn from(value: ValueOrRef<Type>) -> Self {
|
||||
Expression::Atomic(value)
|
||||
}
|
||||
}
|
||||
@@ -434,3 +447,121 @@ impl fmt::Display for Type {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrimitiveType> for Type {
|
||||
fn from(value: PrimitiveType) -> Self {
|
||||
Type::Primitive(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TypeOrVar {
|
||||
Primitive(PrimitiveType),
|
||||
Variable(Location, ArcIntern<String>),
|
||||
Function(Vec<TypeOrVar>, Box<TypeOrVar>),
|
||||
}
|
||||
|
||||
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b TypeOrVar
|
||||
where
|
||||
A: 'a,
|
||||
D: ?Sized + DocAllocator<'a, A>,
|
||||
{
|
||||
fn pretty(self, allocator: &'a D) -> pretty::DocBuilder<'a, D, A> {
|
||||
match self {
|
||||
TypeOrVar::Primitive(x) => allocator.text(format!("{}", x)),
|
||||
TypeOrVar::Variable(_, x) => allocator.text(x.to_string()),
|
||||
TypeOrVar::Function(args, rettype) => {
|
||||
pretty_comma_separated(allocator, &args.iter().collect())
|
||||
.parens()
|
||||
.append(allocator.space())
|
||||
.append(allocator.text("->"))
|
||||
.append(allocator.space())
|
||||
.append(rettype.pretty(allocator))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeOrVar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TypeOrVar::Primitive(x) => x.fmt(f),
|
||||
TypeOrVar::Variable(_, v) => write!(f, "{}", v),
|
||||
TypeOrVar::Function(args, rettype) => {
|
||||
write!(f, "<function:")?;
|
||||
match args.split_last() {
|
||||
None => write!(f, "()")?,
|
||||
Some((single, &[])) => {
|
||||
write!(f, "({})", single)?;
|
||||
}
|
||||
Some((last_one, rest)) => {
|
||||
write!(f, "(")?;
|
||||
for arg in rest.iter() {
|
||||
write!(f, "{}, ", arg);
|
||||
}
|
||||
write!(f, "{})", last_one)?;
|
||||
}
|
||||
}
|
||||
write!(f, "->")?;
|
||||
rettype.fmt(f)?;
|
||||
write!(f, ">")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOrVar {
|
||||
/// Generate a fresh type variable that is different from all previous type variables.
|
||||
///
|
||||
/// This type variable is guaranteed to be unique across the process lifetime. Overuse
|
||||
/// of this function could potentially cause overflow problems, but you're going to have
|
||||
/// to try really hard (like, 2^64 times) to make that happen. The location bound to
|
||||
/// this address will be purely manufactured; if you want to specify a location, use
|
||||
/// [`TypeOrVar::new_located`].
|
||||
pub fn new() -> Self {
|
||||
Self::new_located(Location::manufactured())
|
||||
}
|
||||
|
||||
/// Generate a fresh type variable that is different from all previous type variables.
|
||||
///
|
||||
/// This type variable is guaranteed to be unique across the process lifetime. Overuse
|
||||
/// of this function could potentially cause overflow problems, but you're going to have
|
||||
/// to try really hard (like, 2^64 times) to make that happen.
|
||||
pub fn new_located(loc: Location) -> Self {
|
||||
TypeOrVar::Variable(loc, gensym("t"))
|
||||
}
|
||||
}
|
||||
|
||||
trait TypeWithVoid {
|
||||
fn void() -> Self;
|
||||
}
|
||||
|
||||
impl TypeWithVoid for Type {
|
||||
fn void() -> Self {
|
||||
Type::Primitive(PrimitiveType::Void)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeWithVoid for TypeOrVar {
|
||||
fn void() -> Self {
|
||||
TypeOrVar::Primitive(PrimitiveType::Void)
|
||||
}
|
||||
}
|
||||
|
||||
//impl From<Type> for TypeOrVar {
|
||||
// fn from(value: Type) -> Self {
|
||||
// TypeOrVar::Type(value)
|
||||
// }
|
||||
//}
|
||||
|
||||
impl<T: Into<Type>> From<T> for TypeOrVar {
|
||||
fn from(value: T) -> Self {
|
||||
match value.into() {
|
||||
Type::Primitive(p) => TypeOrVar::Primitive(p),
|
||||
Type::Function(args, ret) => TypeOrVar::Function(
|
||||
args.into_iter().map(Into::into).collect(),
|
||||
Box::new((*ret).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::{Primitive, Type, ValueOrRef};
|
||||
use crate::eval::{EvalEnvironment, EvalError, Value};
|
||||
use crate::ir::{Expression, Program, Statement, TopLevel};
|
||||
use crate::ir::{Expression, Program, TopLevel};
|
||||
|
||||
impl Program {
|
||||
impl<Type> Program<Type> {
|
||||
/// Evaluate the program, returning either an error or a string containing everything
|
||||
/// the program printed out.
|
||||
///
|
||||
@@ -15,16 +15,7 @@ impl Program {
|
||||
match stmt {
|
||||
TopLevel::Function(_, _, _, _) => unimplemented!(),
|
||||
|
||||
TopLevel::Statement(Statement::Binding(_, name, _, value)) => {
|
||||
let actual_value = value.eval(&env)?;
|
||||
env = env.extend(name.clone(), actual_value);
|
||||
}
|
||||
|
||||
TopLevel::Statement(Statement::Print(_, _, name)) => {
|
||||
let value = env.lookup(name.clone())?;
|
||||
let line = format!("{} = {}\n", name, value);
|
||||
stdout.push_str(&line);
|
||||
}
|
||||
TopLevel::Statement(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,17 +23,21 @@ impl Program {
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
impl<T> Expression<T>
|
||||
where
|
||||
T: Clone + Into<Type>,
|
||||
{
|
||||
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
|
||||
match self {
|
||||
Expression::Atomic(x) => x.eval(env),
|
||||
|
||||
Expression::Cast(_, t, valref) => {
|
||||
let value = valref.eval(env)?;
|
||||
let ty = t.clone().into();
|
||||
|
||||
match t {
|
||||
match ty {
|
||||
Type::Primitive(pt) => Ok(pt.safe_cast(&value)?),
|
||||
Type::Function(_, _) => Err(EvalError::CastToFunction(t.to_string())),
|
||||
Type::Function(_, _) => Err(EvalError::CastToFunction(ty.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +56,19 @@ impl Expression {
|
||||
Primitive::Divide => Ok(Value::calculate("/", arg_values)?),
|
||||
}
|
||||
}
|
||||
|
||||
Expression::Block(_, _, _) => {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
Expression::Print(_, _) => unimplemented!(),
|
||||
|
||||
Expression::Bind(_, _, _, _) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueOrRef {
|
||||
impl<T> ValueOrRef<T> {
|
||||
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
|
||||
match self {
|
||||
ValueOrRef::Value(_, _, v) => match v {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::ast::{Expression, Program, Statement, TopLevel};
|
||||
use super::ast::{Expression, Program, TopLevel};
|
||||
use internment::ArcIntern;
|
||||
use std::collections::HashSet;
|
||||
|
||||
impl Program {
|
||||
impl<T> Program<T> {
|
||||
/// Get the complete list of strings used within the program.
|
||||
///
|
||||
/// For the purposes of this function, strings are the variables used in
|
||||
@@ -18,37 +18,18 @@ impl Program {
|
||||
}
|
||||
}
|
||||
|
||||
impl TopLevel {
|
||||
impl<T> TopLevel<T> {
|
||||
fn register_strings(&self, string_set: &mut HashSet<ArcIntern<String>>) {
|
||||
match self {
|
||||
TopLevel::Function(_, _, stmts, body) => {
|
||||
for stmt in stmts.iter() {
|
||||
stmt.register_strings(string_set);
|
||||
}
|
||||
body.register_strings(string_set);
|
||||
}
|
||||
TopLevel::Function(_, _, _, body) => body.register_strings(string_set),
|
||||
TopLevel::Statement(stmt) => stmt.register_strings(string_set),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Statement {
|
||||
fn register_strings(&self, string_set: &mut HashSet<ArcIntern<String>>) {
|
||||
match self {
|
||||
Statement::Binding(_, name, _, expr) => {
|
||||
string_set.insert(name.clone());
|
||||
expr.register_strings(string_set);
|
||||
}
|
||||
|
||||
Statement::Print(_, _, name) => {
|
||||
string_set.insert(name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
impl<T> Expression<T> {
|
||||
fn register_strings(&self, _string_set: &mut HashSet<ArcIntern<String>>) {
|
||||
// nothing has a string in here, at the moment
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user