mirror of
https://github.com/typst/typst
synced 2025-08-24 03:34:14 +08:00
refactor number lexing to improve error messages
This commit is contained in:
parent
e632bffc2e
commit
36be52c61f
@ -807,45 +807,48 @@ impl Lexer<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn number(&mut self, mut start: usize, c: char) -> SyntaxKind {
|
fn number(&mut self, mut start: usize, first_c: char) -> SyntaxKind {
|
||||||
// Handle alternative integer bases.
|
// Handle alternative integer bases.
|
||||||
let mut base = 10;
|
let mut base = 10;
|
||||||
if c == '0' {
|
let mut is_float = false; // `true` implies `base == 10`
|
||||||
if self.s.eat_if('b') {
|
match first_c {
|
||||||
base = 2;
|
'0' if self.s.eat_if('b') => base = 2,
|
||||||
} else if self.s.eat_if('o') {
|
'0' if self.s.eat_if('o') => base = 8,
|
||||||
base = 8;
|
'0' if self.s.eat_if('x') => base = 16,
|
||||||
} else if self.s.eat_if('x') {
|
'.' => is_float = true,
|
||||||
base = 16;
|
_ => {}
|
||||||
}
|
}
|
||||||
if base != 10 {
|
if base != 10 {
|
||||||
start = self.s.cursor();
|
start = self.s.cursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the initial digits.
|
||||||
|
if base == 16 {
|
||||||
|
self.s.eat_while(char::is_ascii_alphanumeric);
|
||||||
|
} else {
|
||||||
|
self.s.eat_while(char::is_ascii_digit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the first part (integer or fractional depending on `first`).
|
// Maybe read a floating point number.
|
||||||
self.s.eat_while(if base == 16 {
|
if base == 10 {
|
||||||
char::is_ascii_alphanumeric
|
|
||||||
} else {
|
|
||||||
char::is_ascii_digit
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read the fractional part if not already done.
|
// Read the fractional part if not already done.
|
||||||
// Make sure not to confuse a range for the decimal separator.
|
// Make sure not to confuse a range for the decimal separator.
|
||||||
if c != '.'
|
if first_c != '.'
|
||||||
&& !self.s.at("..")
|
&& !self.s.at("..")
|
||||||
&& !self.s.scout(1).is_some_and(is_id_start)
|
&& !self.s.scout(1).is_some_and(is_id_start)
|
||||||
&& self.s.eat_if('.')
|
&& self.s.eat_if('.')
|
||||||
&& base == 10
|
|
||||||
{
|
{
|
||||||
|
is_float = true;
|
||||||
self.s.eat_while(char::is_ascii_digit);
|
self.s.eat_while(char::is_ascii_digit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the exponent.
|
// Read the exponent.
|
||||||
if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 {
|
if !self.s.at("em") && self.s.eat_if(['e', 'E']) {
|
||||||
|
is_float = true;
|
||||||
self.s.eat_if(['+', '-']);
|
self.s.eat_if(['+', '-']);
|
||||||
self.s.eat_while(char::is_ascii_digit);
|
self.s.eat_while(char::is_ascii_digit);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read the suffix.
|
// Read the suffix.
|
||||||
let suffix_start = self.s.cursor();
|
let suffix_start = self.s.cursor();
|
||||||
@ -856,37 +859,53 @@ impl Lexer<'_> {
|
|||||||
let number = self.s.get(start..suffix_start);
|
let number = self.s.get(start..suffix_start);
|
||||||
let suffix = self.s.from(suffix_start);
|
let suffix = self.s.from(suffix_start);
|
||||||
|
|
||||||
let kind = if i64::from_str_radix(number, base).is_ok() {
|
let mut suffix_result = match suffix {
|
||||||
SyntaxKind::Int
|
"" => Ok(None),
|
||||||
} else if base == 10 && number.parse::<f64>().is_ok() {
|
"pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" => Ok(Some(())),
|
||||||
SyntaxKind::Float
|
_ => Err(eco_format!("invalid number suffix: {suffix}")),
|
||||||
} else {
|
|
||||||
return self.error(match base {
|
|
||||||
2 => eco_format!("invalid binary number: 0b{}", number),
|
|
||||||
8 => eco_format!("invalid octal number: 0o{}", number),
|
|
||||||
16 => eco_format!("invalid hexadecimal number: 0x{}", number),
|
|
||||||
_ => eco_format!("invalid number: {}", number),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if suffix.is_empty() {
|
let number_result = if is_float && number.parse::<f64>().is_err() {
|
||||||
return kind;
|
// The only invalid case should be when a float lacks digits after
|
||||||
|
// the exponent: e.g. `1.2e`, `2.3E-`, or `1EM`.
|
||||||
|
Err(eco_format!("invalid floating point number: {number}"))
|
||||||
|
} else if base == 10 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let (name, prefix) = match base {
|
||||||
|
2 => ("binary", "0b"),
|
||||||
|
8 => ("octal", "0o"),
|
||||||
|
16 => ("hexadecimal", "0x"),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
match i64::from_str_radix(number, base) {
|
||||||
|
Ok(_) if suffix.is_empty() => Ok(()),
|
||||||
|
Ok(value) => {
|
||||||
|
if suffix_result.is_ok() {
|
||||||
|
suffix_result = Err(eco_format!(
|
||||||
|
"try using a decimal number: {value}{suffix}"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
Err(eco_format!("{name} numbers cannot have a suffix"))
|
||||||
if !matches!(
|
|
||||||
suffix,
|
|
||||||
"pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%"
|
|
||||||
) {
|
|
||||||
return self.error(eco_format!("invalid number suffix: {}", suffix));
|
|
||||||
}
|
}
|
||||||
|
Err(_) => Err(eco_format!("invalid {name} number: {prefix}{number}")),
|
||||||
if base != 10 {
|
|
||||||
let kind = self.error(eco_format!("invalid base-{base} prefix"));
|
|
||||||
self.hint("numbers with a unit cannot have a base prefix");
|
|
||||||
return kind;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
SyntaxKind::Numeric
|
// Return our number or write an error with helpful hints.
|
||||||
|
match (number_result, suffix_result) {
|
||||||
|
// Valid numbers :D
|
||||||
|
(Ok(()), Ok(None)) if is_float => SyntaxKind::Float,
|
||||||
|
(Ok(()), Ok(None)) => SyntaxKind::Int,
|
||||||
|
(Ok(()), Ok(Some(()))) => SyntaxKind::Numeric,
|
||||||
|
// Invalid numbers :(
|
||||||
|
(Err(number_err), Err(suffix_err)) => {
|
||||||
|
let err = self.error(number_err);
|
||||||
|
self.hint(suffix_err);
|
||||||
|
err
|
||||||
|
}
|
||||||
|
(Ok(()), Err(msg)) | (Err(msg), Ok(_)) => self.error(msg),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn string(&mut self) -> SyntaxKind {
|
fn string(&mut self) -> SyntaxKind {
|
||||||
|
@ -107,11 +107,11 @@
|
|||||||
#123.E // this is a field access, so is fine syntactically
|
#123.E // this is a field access, so is fine syntactically
|
||||||
#0.e
|
#0.e
|
||||||
#1.E+020
|
#1.E+020
|
||||||
// Error: 2-10 invalid number: 123.456e
|
// Error: 2-10 invalid floating point number: 123.456e
|
||||||
#123.456e
|
#123.456e
|
||||||
// Error: 2-11 invalid number: 123.456e+
|
// Error: 2-11 invalid floating point number: 123.456e+
|
||||||
#123.456e+
|
#123.456e+
|
||||||
// Error: 2-6 invalid number: .1E-
|
// Error: 2-6 invalid floating point number: .1E-
|
||||||
#.1E-
|
#.1E-
|
||||||
// Error: 2-4 invalid number: 0e
|
// Error: 2-4 invalid floating point number: 0e
|
||||||
#0e
|
#0e
|
||||||
|
@ -75,11 +75,25 @@
|
|||||||
// Hint: 2-24 or use `length.abs.inches()` instead to ignore its em component
|
// Hint: 2-24 or use `length.abs.inches()` instead to ignore its em component
|
||||||
#(4.5em + 6in).inches()
|
#(4.5em + 6in).inches()
|
||||||
|
|
||||||
--- issue-5519-length-base ---
|
--- issue-5519-nondecimal-suffix ---
|
||||||
// Error: 2-9 invalid base-2 prefix
|
// Error: 2-9 binary numbers cannot have a suffix
|
||||||
// Hint: 2-9 numbers with a unit cannot have a base prefix
|
// Hint: 2-9 try using a decimal number: 4pt
|
||||||
#0b100pt
|
#0b100pt
|
||||||
|
|
||||||
|
--- nondecimal-suffix-edge-cases ---
|
||||||
|
// Error: 2-7 octal numbers cannot have a suffix
|
||||||
|
// Hint: 2-7 try using a decimal number: 50%
|
||||||
|
#0o62%
|
||||||
|
// Error: 2-8 hexadecimal numbers cannot have a suffix
|
||||||
|
// Hint: 2-8 try using a decimal number: 2748%
|
||||||
|
#0xabc%
|
||||||
|
// Error: 2-9 invalid hexadecimal number: 0xabcem
|
||||||
|
#0xabcem
|
||||||
|
// Error: 2-11 binary numbers cannot have a suffix
|
||||||
|
// Hint: 2-11 invalid number suffix: dag
|
||||||
|
#0b0101dag
|
||||||
|
|
||||||
|
|
||||||
--- number-syntax-edge-cases ---
|
--- number-syntax-edge-cases ---
|
||||||
// Test numeric syntax edge cases with suffixes and which spans of text are
|
// Test numeric syntax edge cases with suffixes and which spans of text are
|
||||||
// highlighted. Valid items are those not annotated with an error comment since
|
// highlighted. Valid items are those not annotated with an error comment since
|
||||||
@ -92,17 +106,20 @@
|
|||||||
#1.2E+0%
|
#1.2E+0%
|
||||||
#1.2e-0%
|
#1.2e-0%
|
||||||
#0.0e0deg
|
#0.0e0deg
|
||||||
#5in%
|
|
||||||
#0.%
|
#0.%
|
||||||
|
#5in%
|
||||||
// Error: 2-8 invalid number suffix: hello
|
// Error: 2-8 invalid number suffix: hello
|
||||||
#1hello
|
#1hello
|
||||||
// Error: 2-7 invalid number suffix: infr
|
// Error: 2-7 invalid number suffix: infr
|
||||||
#1infr
|
#1infr
|
||||||
// Error: 2-5 invalid number: 2E
|
// Error: 2-5 invalid floating point number: 2E
|
||||||
|
// Hint: 2-5 invalid number suffix: M
|
||||||
#2EM
|
#2EM
|
||||||
// Error: 2-8 invalid number: .1E-
|
// Error: 2-8 invalid floating point number: .1E-
|
||||||
#.1E-fr
|
#.1E-fr
|
||||||
// Error: 2-16 invalid number: 0.1E+
|
// Error: 2-16 invalid floating point number: 0.1E+
|
||||||
|
// Hint: 2-16 invalid number suffix: fr123e456
|
||||||
#0.1E+fr123e456
|
#0.1E+fr123e456
|
||||||
// Error: 2-11 invalid number: .1e-
|
// Error: 2-11 invalid floating point number: .1e-
|
||||||
|
// Hint: 2-11 invalid number suffix: fr123
|
||||||
#.1e-fr123.456
|
#.1e-fr123.456
|
||||||
|
Loading…
x
Reference in New Issue
Block a user