Merge pull request #593 from aiken-lang/hex-and-numeric-underscore
Hex and numeric underscore
This commit is contained in:
commit
8ed3979517
|
@ -4,7 +4,8 @@
|
|||
|
||||
### Added
|
||||
|
||||
N/A
|
||||
- **aiken-lang**: numbers can now be written as hexadecimal digits (e.g. `0x42`)
|
||||
- **aiken-lang**: numbers can now be written using numeric underscores as separator (e.g. `1_000_000`)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ dependencies = [
|
|||
"indoc",
|
||||
"itertools",
|
||||
"miette",
|
||||
"num-bigint",
|
||||
"ordinal",
|
||||
"owo-colors",
|
||||
"pretty_assertions",
|
||||
|
|
|
@ -25,6 +25,7 @@ strum = "0.24.1"
|
|||
thiserror = "1.0.39"
|
||||
vec1 = "1.10.1"
|
||||
uplc = { path = '../uplc', version = "1.0.7-alpha" }
|
||||
num-bigint = "0.4.3"
|
||||
|
||||
[target.'cfg(not(target_family="wasm"))'.dependencies]
|
||||
chumsky = "0.9.2"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
builtins::{self, bool},
|
||||
expr::{TypedExpr, UntypedExpr},
|
||||
parser::token::Token,
|
||||
parser::token::{Base, Token},
|
||||
tipo::{PatternConstructor, Type, TypeInfo},
|
||||
};
|
||||
use miette::Diagnostic;
|
||||
|
@ -441,6 +441,7 @@ pub enum Constant {
|
|||
Int {
|
||||
location: Span,
|
||||
value: String,
|
||||
base: Base,
|
||||
},
|
||||
|
||||
String {
|
||||
|
@ -795,6 +796,7 @@ pub enum Pattern<Constructor, Type> {
|
|||
Int {
|
||||
location: Span,
|
||||
value: String,
|
||||
base: Base,
|
||||
},
|
||||
|
||||
/// The creation of a variable.
|
||||
|
@ -880,7 +882,7 @@ impl<A, B> Pattern<A, B> {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum ByteArrayFormatPreference {
|
||||
HexadecimalString,
|
||||
ArrayOfBytes,
|
||||
ArrayOfBytes(Base),
|
||||
Utf8String,
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
TypedRecordUpdateArg, UnOp, UntypedClause, UntypedRecordUpdateArg,
|
||||
},
|
||||
builtins::void,
|
||||
parser::token::Base,
|
||||
tipo::{ModuleValueConstructor, PatternConstructor, Type, ValueConstructor},
|
||||
};
|
||||
|
||||
|
@ -403,6 +404,7 @@ pub enum UntypedExpr {
|
|||
Int {
|
||||
location: Span,
|
||||
value: String,
|
||||
base: Base,
|
||||
},
|
||||
|
||||
String {
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
use itertools::Itertools;
|
||||
use ordinal::Ordinal;
|
||||
use std::sync::Arc;
|
||||
use vec1::Vec1;
|
||||
|
||||
use crate::{
|
||||
ast::{
|
||||
Annotation, Arg, ArgName, AssignmentKind, BinOp, ByteArrayFormatPreference, CallArg,
|
||||
|
@ -14,12 +9,20 @@ use crate::{
|
|||
},
|
||||
docvec,
|
||||
expr::{UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
|
||||
parser::extra::{Comment, ModuleExtra},
|
||||
parser::{
|
||||
extra::{Comment, ModuleExtra},
|
||||
token::Base,
|
||||
},
|
||||
pretty::{
|
||||
break_, concat, flex_break, join, line, lines, nil, prebreak, Document, Documentable,
|
||||
},
|
||||
tipo::{self, Type},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use num_bigint::BigInt;
|
||||
use ordinal::Ordinal;
|
||||
use std::sync::Arc;
|
||||
use vec1::Vec1;
|
||||
|
||||
const INDENT: isize = 2;
|
||||
const DOCS_MAX_COLUMNS: isize = 80;
|
||||
|
@ -348,7 +351,7 @@ impl<'comments> Formatter<'comments> {
|
|||
preferred_format,
|
||||
..
|
||||
} => self.bytearray(bytes, preferred_format),
|
||||
Constant::Int { value, .. } => value.to_doc(),
|
||||
Constant::Int { value, base, .. } => self.int(value, base),
|
||||
Constant::String { value, .. } => self.string(value),
|
||||
}
|
||||
}
|
||||
|
@ -662,7 +665,7 @@ impl<'comments> Formatter<'comments> {
|
|||
.append("\"")
|
||||
.append(Document::String(hex::encode(bytes)))
|
||||
.append("\""),
|
||||
ByteArrayFormatPreference::ArrayOfBytes => "#"
|
||||
ByteArrayFormatPreference::ArrayOfBytes(Base::Decimal { .. }) => "#"
|
||||
.to_doc()
|
||||
.append(
|
||||
flex_break("[", "[")
|
||||
|
@ -672,6 +675,25 @@ impl<'comments> Formatter<'comments> {
|
|||
.append("]"),
|
||||
)
|
||||
.group(),
|
||||
ByteArrayFormatPreference::ArrayOfBytes(Base::Hexadecimal) => "#"
|
||||
.to_doc()
|
||||
.append(
|
||||
flex_break("[", "[")
|
||||
.append(join(
|
||||
bytes.iter().map(|b| {
|
||||
Document::String(if *b < 16 {
|
||||
format!("0x0{b:x}")
|
||||
} else {
|
||||
format!("{b:#x}")
|
||||
})
|
||||
}),
|
||||
break_(",", ", "),
|
||||
))
|
||||
.nest(INDENT)
|
||||
.append(break_(",", ""))
|
||||
.append("]"),
|
||||
)
|
||||
.group(),
|
||||
ByteArrayFormatPreference::Utf8String => nil()
|
||||
.append("\"")
|
||||
.append(Document::String(String::from_utf8(bytes.to_vec()).unwrap()))
|
||||
|
@ -679,6 +701,39 @@ impl<'comments> Formatter<'comments> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn int<'a>(&mut self, s: &'a str, base: &Base) -> Document<'a> {
|
||||
match base {
|
||||
Base::Decimal { numeric_underscore } if *numeric_underscore => {
|
||||
let s = s
|
||||
.chars()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.flat_map(|(i, c)| {
|
||||
if i != 0 && i % 3 == 0 {
|
||||
Some('_')
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.into_iter()
|
||||
.chain(std::iter::once(c))
|
||||
})
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>();
|
||||
|
||||
Document::String(s)
|
||||
}
|
||||
Base::Decimal { .. } => s.to_doc(),
|
||||
Base::Hexadecimal => Document::String(format!(
|
||||
"0x{}",
|
||||
BigInt::parse_bytes(s.as_bytes(), 10)
|
||||
.expect("Invalid parsed hexadecimal digits ?!")
|
||||
.to_str_radix(16),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expr<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
|
||||
let comments = self.pop_comments(expr.start_byte_index());
|
||||
|
||||
|
@ -700,7 +755,7 @@ impl<'comments> Formatter<'comments> {
|
|||
one_liner,
|
||||
} => self.pipeline(expressions, *one_liner),
|
||||
|
||||
UntypedExpr::Int { value, .. } => value.to_doc(),
|
||||
UntypedExpr::Int { value, base, .. } => self.int(value, base),
|
||||
|
||||
UntypedExpr::String { value, .. } => self.string(value),
|
||||
|
||||
|
@ -1507,7 +1562,7 @@ impl<'comments> Formatter<'comments> {
|
|||
pub fn pattern<'a>(&mut self, pattern: &'a UntypedPattern) -> Document<'a> {
|
||||
let comments = self.pop_comments(pattern.location().start);
|
||||
let doc = match pattern {
|
||||
Pattern::Int { value, .. } => value.to_doc(),
|
||||
Pattern::Int { value, base, .. } => self.int(value, base),
|
||||
|
||||
Pattern::Var { name, .. } => name.to_doc(),
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
use chumsky::{chain::Chain, prelude::*};
|
||||
use error::ParseError;
|
||||
use extra::ModuleExtra;
|
||||
use token::Token;
|
||||
use token::{Base, Token};
|
||||
use vec1::{vec1, Vec1};
|
||||
|
||||
pub fn module(
|
||||
|
@ -397,9 +397,12 @@ fn constant_value_parser() -> impl Parser<Token, ast::Constant, Error = ParseErr
|
|||
});
|
||||
|
||||
let constant_int_parser =
|
||||
select! {Token::Int {value} => value}.map_with_span(|value, span| ast::Constant::Int {
|
||||
location: span,
|
||||
value,
|
||||
select! {Token::Int {value, base} => (value, base)}.map_with_span(|(value, base), span| {
|
||||
ast::Constant::Int {
|
||||
location: span,
|
||||
value,
|
||||
base,
|
||||
}
|
||||
});
|
||||
|
||||
let constant_bytearray_parser =
|
||||
|
@ -422,8 +425,8 @@ pub fn bytearray_parser(
|
|||
) -> impl Parser<Token, (ByteArrayFormatPreference, Vec<u8>), Error = ParseError> {
|
||||
let bytearray_list_parser = just(Token::Hash)
|
||||
.ignore_then(
|
||||
select! {Token::Int {value} => value}
|
||||
.validate(|value, span, emit| {
|
||||
select! {Token::Int {value, base, ..} => (value, base)}
|
||||
.validate(|(value, base), span, emit| {
|
||||
let byte: u8 = match value.parse() {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
|
@ -432,18 +435,38 @@ pub fn bytearray_parser(
|
|||
None,
|
||||
Some(error::Pattern::Byte),
|
||||
));
|
||||
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
byte
|
||||
(byte, base)
|
||||
})
|
||||
.separated_by(just(Token::Comma))
|
||||
.allow_trailing()
|
||||
.delimited_by(just(Token::LeftSquare), just(Token::RightSquare)),
|
||||
)
|
||||
.map(|token| (ByteArrayFormatPreference::ArrayOfBytes, token));
|
||||
.validate(|bytes, span, emit| {
|
||||
let base = bytes.iter().fold(Ok(None), |acc, (_, base)| match acc {
|
||||
Ok(None) => Ok(Some(base)),
|
||||
Ok(Some(previous_base)) if previous_base == base => Ok(Some(base)),
|
||||
_ => Err(()),
|
||||
});
|
||||
|
||||
let base = match base {
|
||||
Err(()) => {
|
||||
emit(ParseError::hybrid_notation_in_bytearray(span));
|
||||
Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
}
|
||||
}
|
||||
Ok(None) => Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
Ok(Some(base)) => *base,
|
||||
};
|
||||
|
||||
(bytes.into_iter().map(|(b, _)| b).collect::<Vec<u8>>(), base)
|
||||
})
|
||||
.map(|(bytes, base)| (ByteArrayFormatPreference::ArrayOfBytes(base), bytes));
|
||||
|
||||
let bytearray_hexstring_parser =
|
||||
just(Token::Hash)
|
||||
|
@ -596,12 +619,13 @@ pub fn expr_parser(
|
|||
}
|
||||
});
|
||||
|
||||
let int_parser = select! { Token::Int {value} => value}.map_with_span(|value, span| {
|
||||
expr::UntypedExpr::Int {
|
||||
let int_parser = select! { Token::Int {value, base} => (value, base)}.map_with_span(
|
||||
|(value, base), span| expr::UntypedExpr::Int {
|
||||
location: span,
|
||||
value,
|
||||
}
|
||||
});
|
||||
base,
|
||||
},
|
||||
);
|
||||
|
||||
let record_update_parser = select! {Token::Name { name } => name}
|
||||
.map_with_span(|module, span: Span| (module, span))
|
||||
|
@ -1685,12 +1709,13 @@ pub fn pattern_parser() -> impl Parser<Token, ast::UntypedPattern, Error = Parse
|
|||
location: span,
|
||||
}
|
||||
}),
|
||||
select! {Token::Int {value} => value}.map_with_span(|value, span| {
|
||||
ast::UntypedPattern::Int {
|
||||
select! {Token::Int {value, base} => (value, base)}.map_with_span(
|
||||
|(value, base), span| ast::UntypedPattern::Int {
|
||||
location: span,
|
||||
value,
|
||||
}
|
||||
}),
|
||||
base,
|
||||
},
|
||||
),
|
||||
r.clone()
|
||||
.separated_by(just(Token::Comma))
|
||||
.allow_trailing()
|
||||
|
|
|
@ -55,6 +55,26 @@ impl ParseError {
|
|||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn malformed_base16_digits(span: Span) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::MalformedBase16Digits,
|
||||
span,
|
||||
while_parsing: None,
|
||||
expected: HashSet::new(),
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hybrid_notation_in_bytearray(span: Span) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::HybridNotationInByteArray,
|
||||
span,
|
||||
while_parsing: None,
|
||||
expected: HashSet::new(),
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ParseError {
|
||||
|
@ -114,6 +134,12 @@ pub enum ErrorKind {
|
|||
hint: Option<String>,
|
||||
},
|
||||
|
||||
#[error("I tripped over a malformed hexadecimal digits.")]
|
||||
#[diagnostic(help("{}", formatdoc! {
|
||||
r#"When numbers starts with '0x', they are treated as hexadecimal numbers. Thus, only digits from 0-9 or letter from a-f (or A-F) can be used following a '0x' number declaration. Plus, hexadecimal digits always go by pairs, so the total number of digits must be even (not counting leading zeros)."#
|
||||
}))]
|
||||
MalformedBase16Digits,
|
||||
|
||||
#[error("I tripped over a malformed base16-encoded string literal.")]
|
||||
#[diagnostic(help("{}", formatdoc! {
|
||||
r#"You can declare literal bytearrays from base16-encoded (a.k.a. hexadecimal) string literals.
|
||||
|
@ -131,6 +157,11 @@ pub enum ErrorKind {
|
|||
}))]
|
||||
MalformedBase16StringLiteral,
|
||||
|
||||
#[error("I came across a bytearray declared using two different notations")]
|
||||
#[diagnostic(url("https://aiken-lang.org/language-tour/primitive-types#bytearray"))]
|
||||
#[diagnostic(help("Either use decimal or hexadecimal notation, but don't mix them."))]
|
||||
HybridNotationInByteArray,
|
||||
|
||||
#[error("I failed to understand a when clause guard.")]
|
||||
#[diagnostic(url("https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns"))]
|
||||
#[diagnostic(help("{}", formatdoc! {
|
||||
|
|
|
@ -1,13 +1,58 @@
|
|||
use chumsky::prelude::*;
|
||||
|
||||
use super::{
|
||||
error::ParseError,
|
||||
token::{Base, Token},
|
||||
};
|
||||
use crate::ast::Span;
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use num_bigint::BigInt;
|
||||
use ordinal::Ordinal;
|
||||
|
||||
use super::{error::ParseError, token::Token};
|
||||
|
||||
pub fn lexer() -> impl Parser<char, Vec<(Token, Span)>, Error = ParseError> {
|
||||
let int = text::int(10).map(|value| Token::Int { value });
|
||||
let base10 = text::int(10).map(|value| Token::Int {
|
||||
value,
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
});
|
||||
|
||||
let base10_underscore = one_of("0123456789")
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.at_most(3)
|
||||
.separated_by(just("_"))
|
||||
.at_least(2)
|
||||
.flatten()
|
||||
.collect::<String>()
|
||||
.map(|value| Token::Int {
|
||||
value,
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: true,
|
||||
},
|
||||
});
|
||||
|
||||
let base16 = just("0x")
|
||||
.ignore_then(
|
||||
one_of("0123456789abcdefABCDEF")
|
||||
.repeated()
|
||||
.at_least(2)
|
||||
.collect::<String>(),
|
||||
)
|
||||
.validate(|value: String, span, emit| {
|
||||
let value = match BigInt::parse_bytes(value.as_bytes(), 16) {
|
||||
None => {
|
||||
emit(ParseError::malformed_base16_digits(span));
|
||||
String::new()
|
||||
}
|
||||
Some(n) => n.to_str_radix(10),
|
||||
};
|
||||
|
||||
Token::Int {
|
||||
value,
|
||||
base: Base::Hexadecimal,
|
||||
}
|
||||
});
|
||||
|
||||
let int = choice((base16, base10_underscore, base10));
|
||||
|
||||
let ordinal = text::int(10)
|
||||
.then_with(|index: String| {
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Eq, Copy)]
|
||||
pub enum Base {
|
||||
Decimal { numeric_underscore: bool },
|
||||
Hexadecimal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Eq)]
|
||||
pub enum Token {
|
||||
Error(char),
|
||||
|
@ -7,7 +13,7 @@ pub enum Token {
|
|||
Ordinal { index: u32 },
|
||||
UpName { name: String },
|
||||
DiscardName { name: String },
|
||||
Int { value: String },
|
||||
Int { value: String, base: Base },
|
||||
ByteString { value: String },
|
||||
String { value: String },
|
||||
// Groupings
|
||||
|
@ -97,7 +103,7 @@ impl fmt::Display for Token {
|
|||
}
|
||||
Token::UpName { name } => name,
|
||||
Token::DiscardName { name } => name,
|
||||
Token::Int { value } => value,
|
||||
Token::Int { value, .. } => value,
|
||||
Token::String { value } => value,
|
||||
Token::ByteString { value } => value,
|
||||
Token::NewLineLeftParen => "↳(",
|
||||
|
|
|
@ -833,3 +833,17 @@ fn pipes_and_expressions() {
|
|||
|
||||
assert_fmt(src, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_and_numeric_underscore() {
|
||||
let src = indoc! {r#"
|
||||
fn foo() {
|
||||
let a = 1_000_000 + 1_423 + 10393841
|
||||
let b = 0xa4 - 0xcd
|
||||
let c = #[0xfd, 0x12, 0x00, 0x1b, 0x0a, 0x90]
|
||||
let d = -100_000
|
||||
}
|
||||
"#};
|
||||
|
||||
assert_fmt(src, src);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
ast::{self, Constant, DataType, Function, ModuleConstant, Span, TypeAlias, Use},
|
||||
expr, parser,
|
||||
expr,
|
||||
parser::{self, token::Base},
|
||||
};
|
||||
use chumsky::prelude::*;
|
||||
use indoc::indoc;
|
||||
|
@ -742,6 +743,9 @@ fn plus_binop() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 33..34),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
doc: None,
|
||||
|
@ -803,6 +807,9 @@ fn pipeline() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 35..36),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expr::UntypedExpr::Var {
|
||||
|
@ -861,10 +868,16 @@ fn if_expression() {
|
|||
left: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 27..28),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 31..32),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
location: Span::new((), 16..36),
|
||||
|
@ -880,11 +893,17 @@ fn if_expression() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 49..50),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
body: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 57..58),
|
||||
value: "5".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
location: Span::new((), 45..62),
|
||||
},
|
||||
|
@ -904,6 +923,9 @@ fn if_expression() {
|
|||
body: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 84..85),
|
||||
value: "6".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
location: Span::new((), 71..89),
|
||||
},
|
||||
|
@ -911,6 +933,9 @@ fn if_expression() {
|
|||
final_else: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 101..102),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
doc: None,
|
||||
|
@ -979,6 +1004,9 @@ fn let_bindings() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 39..40),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expr::UntypedExpr::Var {
|
||||
|
@ -1006,10 +1034,16 @@ fn let_bindings() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 88..89),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 91..92),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Var {
|
||||
location: Span::new((), 94..95),
|
||||
|
@ -1102,6 +1136,9 @@ fn block() {
|
|||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 45..46),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 41..42),
|
||||
|
@ -1120,6 +1157,9 @@ fn block() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 56..57),
|
||||
value: "5".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
@ -1196,11 +1236,17 @@ fn when() {
|
|||
patterns: vec1![ast::Pattern::Int {
|
||||
location: Span::new((), 39..40),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}],
|
||||
guard: None,
|
||||
then: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 44..45),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ast::UntypedClause {
|
||||
|
@ -1208,15 +1254,24 @@ fn when() {
|
|||
patterns: vec1![
|
||||
ast::Pattern::Int {
|
||||
location: Span::new((), 50..51),
|
||||
value: "1".to_string()
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
ast::Pattern::Int {
|
||||
location: Span::new((), 54..55),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
ast::Pattern::Int {
|
||||
location: Span::new((), 58..59),
|
||||
value: "5".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
guard: None,
|
||||
|
@ -1228,6 +1283,9 @@ fn when() {
|
|||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 85..86),
|
||||
value: "5".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 75..82),
|
||||
|
@ -1248,11 +1306,17 @@ fn when() {
|
|||
patterns: vec1![ast::Pattern::Int {
|
||||
location: Span::new((), 111..112),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}],
|
||||
guard: None,
|
||||
then: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 116..117),
|
||||
value: "9".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ast::UntypedClause {
|
||||
|
@ -1265,6 +1329,9 @@ fn when() {
|
|||
then: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 127..128),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1329,6 +1396,9 @@ fn anonymous_function() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 64..65),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
return_annotation: Some(ast::Annotation::Constructor {
|
||||
|
@ -1351,6 +1421,9 @@ fn anonymous_function() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 71..72),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Var {
|
||||
location: Span::new((), 76..83),
|
||||
|
@ -1452,6 +1525,9 @@ fn call() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 31..32),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
}],
|
||||
fun: Box::new(expr::UntypedExpr::Var {
|
||||
|
@ -1555,14 +1631,23 @@ fn call() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 98..99),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 101..102),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 104..105),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
tail: None,
|
||||
|
@ -1720,6 +1805,9 @@ fn record_create_labeled() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 50..51),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1778,6 +1866,9 @@ fn record_create_labeled_with_field_access() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 62..63),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1823,6 +1914,9 @@ fn record_create_unlabeled() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 34..35),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
ast::CallArg {
|
||||
|
@ -1880,18 +1974,30 @@ fn parse_tuple() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 26..27),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 29..30),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 32..33),
|
||||
value: "3".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 35..36),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
@ -1985,6 +2091,9 @@ fn parse_tuple2() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 25..27),
|
||||
value: "14".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
}],
|
||||
fun: Box::new(expr::UntypedExpr::Var {
|
||||
|
@ -2010,6 +2119,9 @@ fn parse_tuple2() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 35..37),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -2043,6 +2155,9 @@ fn large_integer_constants() {
|
|||
value: Box::new(ast::Constant::Int {
|
||||
location: Span::new((), 23..47),
|
||||
value: "999999999999999999999999".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
tipo: (),
|
||||
})],
|
||||
|
@ -2066,7 +2181,9 @@ fn plain_bytearray_literals() {
|
|||
value: Box::new(Constant::ByteArray {
|
||||
location: Span::new((), 25..39),
|
||||
bytes: vec![0, 170, 255],
|
||||
preferred_format: ast::ByteArrayFormatPreference::ArrayOfBytes,
|
||||
preferred_format: ast::ByteArrayFormatPreference::ArrayOfBytes(Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
}),
|
||||
}),
|
||||
tipo: (),
|
||||
})],
|
||||
|
@ -2194,6 +2311,9 @@ fn function_invoke() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 25..27),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
}],
|
||||
}),
|
||||
|
@ -2256,6 +2376,9 @@ fn function_ambiguous_sequence() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 30..32),
|
||||
value: "40".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -2289,6 +2412,9 @@ fn function_ambiguous_sequence() {
|
|||
expr::UntypedExpr::Int {
|
||||
location: Span::new((), 67..69),
|
||||
value: "40".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -2311,10 +2437,16 @@ fn function_ambiguous_sequence() {
|
|||
left: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 98..100),
|
||||
value: "40".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 101..102),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
|
@ -2347,6 +2479,9 @@ fn function_ambiguous_sequence() {
|
|||
value: expr::UntypedExpr::Int {
|
||||
location: Span::new((), 134..136),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
}],
|
||||
fun: Box::new(expr::UntypedExpr::Var {
|
||||
|
@ -2375,11 +2510,17 @@ fn function_ambiguous_sequence() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 145..147),
|
||||
value: "14".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 151..153),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
@ -2514,15 +2655,24 @@ fn subtraction_vs_negate() {
|
|||
left: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 14..15),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 16..17),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 22..23),
|
||||
value: "0".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expr::UntypedExpr::Assignment {
|
||||
|
@ -2533,6 +2683,9 @@ fn subtraction_vs_negate() {
|
|||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 35..36),
|
||||
value: "2".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
|
@ -2556,6 +2709,9 @@ fn subtraction_vs_negate() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 45..46),
|
||||
value: "4".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
expr::UntypedExpr::BinOp {
|
||||
|
@ -2571,6 +2727,9 @@ fn subtraction_vs_negate() {
|
|||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 54..55),
|
||||
value: "1".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}],
|
||||
|
@ -2583,6 +2742,9 @@ fn subtraction_vs_negate() {
|
|||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 59..61),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
@ -2635,6 +2797,9 @@ fn clause_guards() {
|
|||
guard: Some(ast::UntypedClauseGuard::Constant(ast::Constant::Int {
|
||||
location: Span::new((), 34..36),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
})),
|
||||
then: expr::UntypedExpr::Var {
|
||||
location: Span::new((), 40..44),
|
||||
|
@ -2758,6 +2923,9 @@ fn clause_guards() {
|
|||
ast::Constant::Int {
|
||||
location: Span::new((), 162..164),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
|
@ -2772,6 +2940,9 @@ fn clause_guards() {
|
|||
ast::Constant::Int {
|
||||
location: Span::new((), 172..174),
|
||||
value: "14".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
|
@ -2808,6 +2979,9 @@ fn clause_guards() {
|
|||
ast::Constant::Int {
|
||||
location: Span::new((), 206..208),
|
||||
value: "14".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
},
|
||||
)),
|
||||
}),
|
||||
|
@ -3011,6 +3185,9 @@ fn trace_expressions() {
|
|||
left: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 108..110),
|
||||
value: "14".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::BinOp {
|
||||
location: Span::new((), 113..122),
|
||||
|
@ -3018,10 +3195,16 @@ fn trace_expressions() {
|
|||
left: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 113..115),
|
||||
value: "42".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
right: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 118..122),
|
||||
value: "1337".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
@ -3337,3 +3520,150 @@ fn brackets_followed_by_parenthesis() {
|
|||
}
|
||||
"#});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_parsing_hex() {
|
||||
let code = indoc! {r#"
|
||||
fn foo() {
|
||||
let i = 0xff
|
||||
}
|
||||
"#};
|
||||
assert_definitions(
|
||||
code,
|
||||
vec![ast::Definition::Fn(Function {
|
||||
arguments: vec![],
|
||||
body: expr::UntypedExpr::Assignment {
|
||||
location: Span::new((), 13..25),
|
||||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 21..25),
|
||||
value: "255".to_string(),
|
||||
base: Base::Hexadecimal,
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 17..18),
|
||||
name: "i".to_string(),
|
||||
},
|
||||
kind: ast::AssignmentKind::Let,
|
||||
annotation: None,
|
||||
},
|
||||
doc: None,
|
||||
location: Span::new((), 0..8),
|
||||
name: "foo".to_string(),
|
||||
public: false,
|
||||
return_annotation: None,
|
||||
return_type: (),
|
||||
end_position: 26,
|
||||
can_error: true,
|
||||
})],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_parsing_hex_bytes() {
|
||||
let code = indoc! {r#"
|
||||
fn foo() {
|
||||
#[ 0x01, 0xa2, 0x03 ]
|
||||
}
|
||||
"#};
|
||||
assert_definitions(
|
||||
code,
|
||||
vec![ast::Definition::Fn(Function {
|
||||
arguments: vec![],
|
||||
body: expr::UntypedExpr::ByteArray {
|
||||
location: Span::new((), 13..34),
|
||||
bytes: vec![1, 162, 3],
|
||||
preferred_format: ast::ByteArrayFormatPreference::ArrayOfBytes(Base::Hexadecimal),
|
||||
},
|
||||
doc: None,
|
||||
location: Span::new((), 0..8),
|
||||
name: "foo".to_string(),
|
||||
public: false,
|
||||
return_annotation: None,
|
||||
return_type: (),
|
||||
end_position: 35,
|
||||
can_error: true,
|
||||
})],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_parsing_numeric_underscore() {
|
||||
let code = indoc! {r#"
|
||||
fn foo() {
|
||||
let i = 1_234_567
|
||||
let j = 1_000_000
|
||||
let k = -10_000
|
||||
}
|
||||
"#};
|
||||
assert_definitions(
|
||||
code,
|
||||
vec![ast::Definition::Fn(Function {
|
||||
arguments: vec![],
|
||||
body: expr::UntypedExpr::Sequence {
|
||||
location: Span::new((), 17..76),
|
||||
expressions: vec![
|
||||
expr::UntypedExpr::Assignment {
|
||||
location: Span::new((), 17..34),
|
||||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 25..34),
|
||||
value: "1234567".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: true,
|
||||
},
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 21..22),
|
||||
name: "i".to_string(),
|
||||
},
|
||||
kind: ast::AssignmentKind::Let,
|
||||
annotation: None,
|
||||
},
|
||||
expr::UntypedExpr::Assignment {
|
||||
location: Span::new((), 39..56),
|
||||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 47..56),
|
||||
value: "1000000".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: true,
|
||||
},
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 43..44),
|
||||
name: "j".to_string(),
|
||||
},
|
||||
kind: ast::AssignmentKind::Let,
|
||||
annotation: None,
|
||||
},
|
||||
expr::UntypedExpr::Assignment {
|
||||
location: Span::new((), 61..76),
|
||||
value: Box::new(expr::UntypedExpr::UnOp {
|
||||
op: ast::UnOp::Negate,
|
||||
location: Span::new((), 69..76),
|
||||
value: Box::new(expr::UntypedExpr::Int {
|
||||
location: Span::new((), 70..76),
|
||||
value: "10000".to_string(),
|
||||
base: Base::Decimal {
|
||||
numeric_underscore: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
pattern: ast::Pattern::Var {
|
||||
location: Span::new((), 65..66),
|
||||
name: "k".to_string(),
|
||||
},
|
||||
kind: ast::AssignmentKind::Let,
|
||||
annotation: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
doc: None,
|
||||
location: Span::new((), 2..10),
|
||||
name: "foo".to_string(),
|
||||
public: false,
|
||||
return_annotation: None,
|
||||
return_type: (),
|
||||
end_position: 77,
|
||||
can_error: true,
|
||||
})],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1304,8 +1304,14 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
|
|||
) -> Result<Constant, Error> {
|
||||
let inferred = match value {
|
||||
Constant::Int {
|
||||
location, value, ..
|
||||
} => Ok(Constant::Int { location, value }),
|
||||
location,
|
||||
value,
|
||||
base,
|
||||
} => Ok(Constant::Int {
|
||||
location,
|
||||
value,
|
||||
base,
|
||||
}),
|
||||
|
||||
Constant::String {
|
||||
location, value, ..
|
||||
|
|
|
@ -182,10 +182,18 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
|
|||
})
|
||||
}
|
||||
|
||||
Pattern::Int { location, value } => {
|
||||
Pattern::Int {
|
||||
location,
|
||||
value,
|
||||
base,
|
||||
} => {
|
||||
self.environment.unify(tipo, int(), location, false)?;
|
||||
|
||||
Ok(Pattern::Int { location, value })
|
||||
Ok(Pattern::Int {
|
||||
location,
|
||||
value,
|
||||
base,
|
||||
})
|
||||
}
|
||||
|
||||
Pattern::List {
|
||||
|
|
Loading…
Reference in New Issue