mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-30 23:31:12 +00:00
Merge branch 'parse-record-expr' into import-alias
This commit is contained in:
commit
a255805331
20 changed files with 1950 additions and 997 deletions
|
@ -737,10 +737,10 @@ pub fn canonicalize_expr<'a>(
|
||||||
use roc_problem::can::RuntimeError::*;
|
use roc_problem::can::RuntimeError::*;
|
||||||
(RuntimeError(MalformedClosure(region)), Output::default())
|
(RuntimeError(MalformedClosure(region)), Output::default())
|
||||||
}
|
}
|
||||||
ast::Expr::MalformedIdent(name) => {
|
ast::Expr::MalformedIdent(name, bad_ident) => {
|
||||||
use roc_problem::can::RuntimeError::*;
|
use roc_problem::can::RuntimeError::*;
|
||||||
|
|
||||||
let problem = MalformedIdentifier((*name).into(), region);
|
let problem = MalformedIdentifier((*name).into(), *bad_ident, region);
|
||||||
env.problem(Problem::RuntimeError(problem.clone()));
|
env.problem(Problem::RuntimeError(problem.clone()));
|
||||||
|
|
||||||
(RuntimeError(problem), Output::default())
|
(RuntimeError(problem), Output::default())
|
||||||
|
|
|
@ -88,8 +88,8 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Located<Expr<'a>>) -> &'a
|
||||||
| Nested(AccessorFunction(_))
|
| Nested(AccessorFunction(_))
|
||||||
| Var { .. }
|
| Var { .. }
|
||||||
| Nested(Var { .. })
|
| Nested(Var { .. })
|
||||||
| MalformedIdent(_)
|
| MalformedIdent(_, _)
|
||||||
| Nested(MalformedIdent(_))
|
| Nested(MalformedIdent(_, _))
|
||||||
| MalformedClosure
|
| MalformedClosure
|
||||||
| Nested(MalformedClosure)
|
| Nested(MalformedClosure)
|
||||||
| PrecedenceConflict(_, _, _, _)
|
| PrecedenceConflict(_, _, _, _)
|
||||||
|
|
|
@ -379,6 +379,11 @@ pub fn canonicalize_pattern<'a>(
|
||||||
malformed_pattern(env, problem, region)
|
malformed_pattern(env, problem, region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MalformedIdent(_str, problem) => {
|
||||||
|
let problem = MalformedPatternProblem::BadIdent(*problem);
|
||||||
|
malformed_pattern(env, problem, region)
|
||||||
|
}
|
||||||
|
|
||||||
QualifiedIdentifier { .. } => {
|
QualifiedIdentifier { .. } => {
|
||||||
let problem = MalformedPatternProblem::QualifiedIdentifier;
|
let problem = MalformedPatternProblem::QualifiedIdentifier;
|
||||||
malformed_pattern(env, problem, region)
|
malformed_pattern(env, problem, region)
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl<'a> Formattable<'a> for Expr<'a> {
|
||||||
| Access(_, _)
|
| Access(_, _)
|
||||||
| AccessorFunction(_)
|
| AccessorFunction(_)
|
||||||
| Var { .. }
|
| Var { .. }
|
||||||
| MalformedIdent(_)
|
| MalformedIdent(_, _)
|
||||||
| MalformedClosure
|
| MalformedClosure
|
||||||
| GlobalTag(_)
|
| GlobalTag(_)
|
||||||
| PrivateTag(_) => false,
|
| PrivateTag(_) => false,
|
||||||
|
@ -303,7 +303,7 @@ impl<'a> Formattable<'a> for Expr<'a> {
|
||||||
buf.push('.');
|
buf.push('.');
|
||||||
buf.push_str(key);
|
buf.push_str(key);
|
||||||
}
|
}
|
||||||
MalformedIdent(_) => {}
|
MalformedIdent(_, _) => {}
|
||||||
MalformedClosure => {}
|
MalformedClosure => {}
|
||||||
PrecedenceConflict(_, _, _, _) => {}
|
PrecedenceConflict(_, _, _, _) => {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ impl<'a> Formattable<'a> for Pattern<'a> {
|
||||||
| Pattern::StrLiteral(_)
|
| Pattern::StrLiteral(_)
|
||||||
| Pattern::Underscore(_)
|
| Pattern::Underscore(_)
|
||||||
| Pattern::Malformed(_)
|
| Pattern::Malformed(_)
|
||||||
|
| Pattern::MalformedIdent(_, _)
|
||||||
| Pattern::QualifiedIdentifier { .. } => false,
|
| Pattern::QualifiedIdentifier { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +158,7 @@ impl<'a> Formattable<'a> for Pattern<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Malformed
|
// Malformed
|
||||||
Malformed(string) => buf.push_str(string),
|
Malformed(string) | MalformedIdent(string, _) => buf.push_str(string),
|
||||||
QualifiedIdentifier { module_name, ident } => {
|
QualifiedIdentifier { module_name, ident } => {
|
||||||
if !module_name.is_empty() {
|
if !module_name.is_empty() {
|
||||||
buf.push_str(module_name);
|
buf.push_str(module_name);
|
||||||
|
|
|
@ -150,7 +150,7 @@ pub enum Expr<'a> {
|
||||||
Nested(&'a Expr<'a>),
|
Nested(&'a Expr<'a>),
|
||||||
|
|
||||||
// Problems
|
// Problems
|
||||||
MalformedIdent(&'a str),
|
MalformedIdent(&'a str, crate::ident::BadIdent),
|
||||||
MalformedClosure,
|
MalformedClosure,
|
||||||
// Both operators were non-associative, e.g. (True == False == False).
|
// Both operators were non-associative, e.g. (True == False == False).
|
||||||
// We should tell the author to disambiguate by grouping them with parens.
|
// We should tell the author to disambiguate by grouping them with parens.
|
||||||
|
@ -356,6 +356,7 @@ pub enum Pattern<'a> {
|
||||||
|
|
||||||
// Malformed
|
// Malformed
|
||||||
Malformed(&'a str),
|
Malformed(&'a str),
|
||||||
|
MalformedIdent(&'a str, crate::ident::BadIdent),
|
||||||
QualifiedIdentifier {
|
QualifiedIdentifier {
|
||||||
module_name: &'a str,
|
module_name: &'a str,
|
||||||
ident: &'a str,
|
ident: &'a str,
|
||||||
|
@ -411,7 +412,7 @@ impl<'a> Pattern<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ident::AccessorFunction(string) => Pattern::Malformed(string),
|
Ident::AccessorFunction(string) => Pattern::Malformed(string),
|
||||||
Ident::Malformed(string) => Pattern::Malformed(string),
|
Ident::Malformed(string, _problem) => Pattern::Malformed(string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -337,6 +337,28 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn space1_e<'a, E>(
|
||||||
|
min_indent: u16,
|
||||||
|
space_problem: fn(BadInputError, Row, Col) -> E,
|
||||||
|
indent_problem: fn(Row, Col) -> E,
|
||||||
|
no_parse_problem: fn(Row, Col) -> E,
|
||||||
|
) -> impl Parser<'a, &'a [CommentOrNewline<'a>], E>
|
||||||
|
where
|
||||||
|
E: 'a,
|
||||||
|
{
|
||||||
|
move |arena, state| match space0_e(min_indent, space_problem, indent_problem)
|
||||||
|
.parse(arena, state)
|
||||||
|
{
|
||||||
|
Ok((NoProgress, _, state)) => Err((
|
||||||
|
NoProgress,
|
||||||
|
no_parse_problem(state.line, state.column),
|
||||||
|
state,
|
||||||
|
)),
|
||||||
|
Ok((MadeProgress, spaces, state)) => Ok((MadeProgress, spaces, state)),
|
||||||
|
Err(bad) => Err(bad),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One or more (spaces/comments/newlines).
|
/// One or more (spaces/comments/newlines).
|
||||||
pub fn space1<'a>(min_indent: u16) -> impl Parser<'a, &'a [CommentOrNewline<'a>], SyntaxError<'a>> {
|
pub fn space1<'a>(min_indent: u16) -> impl Parser<'a, &'a [CommentOrNewline<'a>], SyntaxError<'a>> {
|
||||||
// TODO try benchmarking a short-circuit for the typical case: see if there is
|
// TODO try benchmarking a short-circuit for the typical case: see if there is
|
||||||
|
@ -434,6 +456,62 @@ pub fn spaces_exactly<'a>(spaces_expected: u16) -> impl Parser<'a, (), SyntaxErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn spaces_exactly_e<'a>(spaces_expected: u16) -> impl Parser<'a, (), parser::EExpr<'a>> {
|
||||||
|
use parser::EExpr;
|
||||||
|
|
||||||
|
move |arena: &'a Bump, state: State<'a>| {
|
||||||
|
if spaces_expected == 0 {
|
||||||
|
return Ok((NoProgress, (), state));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = state;
|
||||||
|
let mut spaces_seen: u16 = 0;
|
||||||
|
|
||||||
|
while !state.bytes.is_empty() {
|
||||||
|
match peek_utf8_char(&state) {
|
||||||
|
Ok((' ', _)) => {
|
||||||
|
spaces_seen += 1;
|
||||||
|
state = state.advance_spaces_e(arena, 1, EExpr::IndentStart)?;
|
||||||
|
if spaces_seen == spaces_expected {
|
||||||
|
return Ok((MadeProgress, (), state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
return Err((
|
||||||
|
NoProgress,
|
||||||
|
EExpr::IndentStart(state.line, state.column),
|
||||||
|
state,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(SyntaxError::BadUtf8) => {
|
||||||
|
// If we hit an invalid UTF-8 character, bail out immediately.
|
||||||
|
let progress = Progress::progress_when(spaces_seen != 0);
|
||||||
|
return Err((
|
||||||
|
progress,
|
||||||
|
EExpr::Space(BadInputError::BadUtf8, state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err((
|
||||||
|
NoProgress,
|
||||||
|
EExpr::IndentStart(state.line, state.column),
|
||||||
|
state,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err((
|
||||||
|
NoProgress,
|
||||||
|
EExpr::IndentStart(state.line, state.column),
|
||||||
|
state,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn spaces<'a>(
|
fn spaces<'a>(
|
||||||
require_at_least_one: bool,
|
require_at_least_one: bool,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,13 @@
|
||||||
use crate::ast::Attempting;
|
use crate::ast::Attempting;
|
||||||
use crate::keyword;
|
use crate::keyword;
|
||||||
use crate::parser::Progress::{self, *};
|
use crate::parser::Progress::{self, *};
|
||||||
use crate::parser::{peek_utf8_char, unexpected, ParseResult, Parser, State, SyntaxError};
|
use crate::parser::{
|
||||||
|
peek_utf8_char, unexpected, BadInputError, Col, EExpr, ParseResult, Parser, Row, State,
|
||||||
|
SyntaxError,
|
||||||
|
};
|
||||||
use bumpalo::collections::string::String;
|
use bumpalo::collections::string::String;
|
||||||
use bumpalo::collections::vec::Vec;
|
use bumpalo::collections::vec::Vec;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use roc_collections::all::arena_join;
|
|
||||||
use roc_region::all::Region;
|
use roc_region::all::Region;
|
||||||
|
|
||||||
/// The parser accepts all of these in any position where any one of them could
|
/// The parser accepts all of these in any position where any one of them could
|
||||||
|
@ -26,7 +28,7 @@ pub enum Ident<'a> {
|
||||||
/// .foo
|
/// .foo
|
||||||
AccessorFunction(&'a str),
|
AccessorFunction(&'a str),
|
||||||
/// .Foo or foo. or something like foo.Bar
|
/// .Foo or foo. or something like foo.Bar
|
||||||
Malformed(&'a str),
|
Malformed(&'a str, BadIdent),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Ident<'a> {
|
impl<'a> Ident<'a> {
|
||||||
|
@ -50,7 +52,7 @@ impl<'a> Ident<'a> {
|
||||||
len - 1
|
len - 1
|
||||||
}
|
}
|
||||||
AccessorFunction(string) => string.len(),
|
AccessorFunction(string) => string.len(),
|
||||||
Malformed(string) => string.len(),
|
Malformed(string, _) => string.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,274 +61,8 @@ impl<'a> Ident<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an identifier into a string.
|
|
||||||
///
|
|
||||||
/// This is separate from the `ident` Parser because string interpolation
|
|
||||||
/// wants to use it this way.
|
|
||||||
///
|
|
||||||
/// By design, this does not check for reserved keywords like "if", "else", etc.
|
|
||||||
/// Sometimes we may want to check for those later in the process, and give
|
|
||||||
/// more contextually-aware error messages than "unexpected `if`" or the like.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn parse_ident<'a>(
|
|
||||||
arena: &'a Bump,
|
|
||||||
mut state: State<'a>,
|
|
||||||
) -> ParseResult<'a, (Ident<'a>, Option<char>), SyntaxError<'a>> {
|
|
||||||
let mut part_buf = String::new_in(arena); // The current "part" (parts are dot-separated.)
|
|
||||||
let mut capitalized_parts: Vec<&'a str> = Vec::new_in(arena);
|
|
||||||
let mut noncapitalized_parts: Vec<&'a str> = Vec::new_in(arena);
|
|
||||||
let mut is_capitalized;
|
|
||||||
let is_accessor_fn;
|
|
||||||
let mut is_private_tag = false;
|
|
||||||
|
|
||||||
let start_bytes_len = state.bytes.len();
|
|
||||||
|
|
||||||
// Identifiers and accessor functions must start with either a letter or a dot.
|
|
||||||
// If this starts with neither, it must be something else!
|
|
||||||
match peek_utf8_char(&state) {
|
|
||||||
Ok((first_ch, bytes_parsed)) => {
|
|
||||||
if first_ch.is_alphabetic() {
|
|
||||||
part_buf.push(first_ch);
|
|
||||||
|
|
||||||
is_capitalized = first_ch.is_uppercase();
|
|
||||||
is_accessor_fn = false;
|
|
||||||
|
|
||||||
state = state.advance_without_indenting(bytes_parsed)?;
|
|
||||||
} else if first_ch == '.' {
|
|
||||||
is_capitalized = false;
|
|
||||||
is_accessor_fn = true;
|
|
||||||
|
|
||||||
state = state.advance_without_indenting(bytes_parsed)?;
|
|
||||||
} else if first_ch == '@' {
|
|
||||||
state = state.advance_without_indenting(bytes_parsed)?;
|
|
||||||
|
|
||||||
// '@' must always be followed by a capital letter!
|
|
||||||
match peek_utf8_char(&state) {
|
|
||||||
Ok((next_ch, next_bytes_parsed)) => {
|
|
||||||
if next_ch.is_uppercase() {
|
|
||||||
state = state.advance_without_indenting(next_bytes_parsed)?;
|
|
||||||
|
|
||||||
part_buf.push('@');
|
|
||||||
part_buf.push(next_ch);
|
|
||||||
|
|
||||||
is_private_tag = true;
|
|
||||||
is_capitalized = true;
|
|
||||||
is_accessor_fn = false;
|
|
||||||
} else {
|
|
||||||
return Err(unexpected(
|
|
||||||
bytes_parsed + next_bytes_parsed,
|
|
||||||
Attempting::Identifier,
|
|
||||||
state,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(reason) => {
|
|
||||||
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
|
||||||
return state.fail(arena, progress, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(unexpected(0, Attempting::Identifier, state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(reason) => {
|
|
||||||
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
|
||||||
return state.fail(arena, progress, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while !state.bytes.is_empty() {
|
|
||||||
match peek_utf8_char(&state) {
|
|
||||||
Ok((ch, bytes_parsed)) => {
|
|
||||||
// After the first character, only these are allowed:
|
|
||||||
//
|
|
||||||
// * Unicode alphabetic chars - you might name a variable `鹏` if that's clear to your readers
|
|
||||||
// * ASCII digits - e.g. `1` but not `¾`, both of which pass .is_numeric()
|
|
||||||
// * A dot ('.')
|
|
||||||
if ch.is_alphabetic() {
|
|
||||||
if part_buf.is_empty() {
|
|
||||||
// Capitalization is determined by the first character in the part.
|
|
||||||
is_capitalized = ch.is_uppercase();
|
|
||||||
}
|
|
||||||
|
|
||||||
part_buf.push(ch);
|
|
||||||
} else if ch.is_ascii_digit() {
|
|
||||||
// Parts may not start with numbers!
|
|
||||||
if part_buf.is_empty() {
|
|
||||||
return malformed(
|
|
||||||
Some(ch),
|
|
||||||
arena,
|
|
||||||
state,
|
|
||||||
capitalized_parts,
|
|
||||||
noncapitalized_parts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
part_buf.push(ch);
|
|
||||||
} else if ch == '.' {
|
|
||||||
// There are two posssible errors here:
|
|
||||||
//
|
|
||||||
// 1. Having two consecutive dots is an error.
|
|
||||||
// 2. Having capitalized parts after noncapitalized (e.g. `foo.Bar`) is an error.
|
|
||||||
if part_buf.is_empty() || (is_capitalized && !noncapitalized_parts.is_empty()) {
|
|
||||||
return malformed(
|
|
||||||
Some(ch),
|
|
||||||
arena,
|
|
||||||
state,
|
|
||||||
capitalized_parts,
|
|
||||||
noncapitalized_parts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_capitalized {
|
|
||||||
capitalized_parts.push(part_buf.into_bump_str());
|
|
||||||
} else {
|
|
||||||
noncapitalized_parts.push(part_buf.into_bump_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we've recorded the contents of the current buffer, reset it.
|
|
||||||
part_buf = String::new_in(arena);
|
|
||||||
} else {
|
|
||||||
// This must be the end of the identifier. We're done!
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.advance_without_indenting(bytes_parsed)?;
|
|
||||||
}
|
|
||||||
Err(reason) => {
|
|
||||||
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
|
||||||
return state.fail(arena, progress, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if part_buf.is_empty() {
|
|
||||||
// We probably had a trailing dot, e.g. `Foo.bar.` - this is malformed!
|
|
||||||
//
|
|
||||||
// This condition might also occur if we encounter a malformed accessor like `.|`
|
|
||||||
//
|
|
||||||
// If we made it this far and don't have a next_char, then necessarily
|
|
||||||
// we have consumed a '.' char previously.
|
|
||||||
return malformed(
|
|
||||||
Some('.'),
|
|
||||||
arena,
|
|
||||||
state,
|
|
||||||
capitalized_parts,
|
|
||||||
noncapitalized_parts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the final parts.
|
|
||||||
if is_capitalized {
|
|
||||||
capitalized_parts.push(part_buf.into_bump_str());
|
|
||||||
} else {
|
|
||||||
noncapitalized_parts.push(part_buf.into_bump_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
let answer = if is_accessor_fn {
|
|
||||||
// Handle accessor functions first because they have the strictest requirements.
|
|
||||||
// Accessor functions may have exactly 1 noncapitalized part, and no capitalzed parts.
|
|
||||||
if capitalized_parts.is_empty() && noncapitalized_parts.len() == 1 && !is_private_tag {
|
|
||||||
let value = noncapitalized_parts.iter().next().unwrap();
|
|
||||||
|
|
||||||
Ident::AccessorFunction(value)
|
|
||||||
} else {
|
|
||||||
return malformed(None, arena, state, capitalized_parts, noncapitalized_parts);
|
|
||||||
}
|
|
||||||
} else if noncapitalized_parts.is_empty() {
|
|
||||||
// We have capitalized parts only, so this must be a tag.
|
|
||||||
match capitalized_parts.first() {
|
|
||||||
Some(value) => {
|
|
||||||
if capitalized_parts.len() == 1 {
|
|
||||||
if is_private_tag {
|
|
||||||
Ident::PrivateTag(value)
|
|
||||||
} else {
|
|
||||||
Ident::GlobalTag(value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This is a qualified tag, which is not allowed!
|
|
||||||
return malformed(None, arena, state, capitalized_parts, noncapitalized_parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// We had neither capitalized nor noncapitalized parts,
|
|
||||||
// yet we made it this far. The only explanation is that this was
|
|
||||||
// a stray '.' drifting through the cosmos.
|
|
||||||
return Err(unexpected(1, Attempting::Identifier, state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if is_private_tag {
|
|
||||||
// This is qualified field access with an '@' in front, which does not make sense!
|
|
||||||
return malformed(None, arena, state, capitalized_parts, noncapitalized_parts);
|
|
||||||
} else {
|
|
||||||
// We have multiple noncapitalized parts, so this must be field access.
|
|
||||||
Ident::Access {
|
|
||||||
module_name: join_module_parts(arena, capitalized_parts.into_bump_slice()),
|
|
||||||
parts: noncapitalized_parts.into_bump_slice(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
|
||||||
debug_assert_eq!(progress, Progress::MadeProgress,);
|
|
||||||
Ok((Progress::MadeProgress, (answer, None), state))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn malformed<'a>(
|
|
||||||
opt_bad_char: Option<char>,
|
|
||||||
arena: &'a Bump,
|
|
||||||
mut state: State<'a>,
|
|
||||||
capitalized_parts: Vec<&'a str>,
|
|
||||||
noncapitalized_parts: Vec<&'a str>,
|
|
||||||
) -> ParseResult<'a, (Ident<'a>, Option<char>), SyntaxError<'a>> {
|
|
||||||
// Reconstruct the original string that we've been parsing.
|
|
||||||
let mut full_string = String::new_in(arena);
|
|
||||||
|
|
||||||
full_string
|
|
||||||
.push_str(arena_join(arena, &mut capitalized_parts.into_iter(), ".").into_bump_str());
|
|
||||||
full_string
|
|
||||||
.push_str(arena_join(arena, &mut noncapitalized_parts.into_iter(), ".").into_bump_str());
|
|
||||||
|
|
||||||
if let Some(bad_char) = opt_bad_char {
|
|
||||||
full_string.push(bad_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume the remaining chars in the identifier.
|
|
||||||
let mut next_char = None;
|
|
||||||
|
|
||||||
while !state.bytes.is_empty() {
|
|
||||||
match peek_utf8_char(&state) {
|
|
||||||
Ok((ch, bytes_parsed)) => {
|
|
||||||
// We can't use ch.is_alphanumeric() here because that passes for
|
|
||||||
// things that are "numeric" but not ASCII digits, like `¾`
|
|
||||||
if ch == '.' || ch.is_alphabetic() || ch.is_ascii_digit() {
|
|
||||||
full_string.push(ch);
|
|
||||||
} else {
|
|
||||||
next_char = Some(ch);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.advance_without_indenting(bytes_parsed)?;
|
|
||||||
}
|
|
||||||
Err(reason) => return state.fail(arena, MadeProgress, reason),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
MadeProgress,
|
|
||||||
(Ident::Malformed(full_string.into_bump_str()), next_char),
|
|
||||||
state,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ident<'a>() -> impl Parser<'a, Ident<'a>, SyntaxError<'a>> {
|
pub fn ident<'a>() -> impl Parser<'a, Ident<'a>, SyntaxError<'a>> {
|
||||||
move |arena: &'a Bump, state: State<'a>| {
|
crate::parser::specialize(|e, _, _| SyntaxError::Expr(e), parse_ident_help)
|
||||||
// Discard next_char; we don't need it.
|
|
||||||
let (progress, (string, _), state) = parse_ident(arena, state)?;
|
|
||||||
|
|
||||||
Ok((progress, string, state))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global_tag_or_ident<'a, F>(pred: F) -> impl Parser<'a, &'a str, SyntaxError<'a>>
|
pub fn global_tag_or_ident<'a, F>(pred: F) -> impl Parser<'a, &'a str, SyntaxError<'a>>
|
||||||
|
@ -435,3 +171,343 @@ pub fn join_module_parts<'a>(arena: &'a Bump, module_parts: &[&str]) -> &'a str
|
||||||
|
|
||||||
buf.into_bump_str()
|
buf.into_bump_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! advance_state {
|
||||||
|
($state:expr, $n:expr) => {
|
||||||
|
$state.advance_without_indenting_ee($n, |r, c| {
|
||||||
|
BadIdent::Space(crate::parser::BadInputError::LineTooLong, r, c)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_ident_help<'a>(
|
||||||
|
arena: &'a Bump,
|
||||||
|
state: State<'a>,
|
||||||
|
) -> ParseResult<'a, Ident<'a>, EExpr<'a>> {
|
||||||
|
let initial = state.clone();
|
||||||
|
|
||||||
|
match parse_ident_help_help(arena, state) {
|
||||||
|
Ok((progress, (ident, _), state)) => {
|
||||||
|
if let Ident::Access { module_name, parts } = ident {
|
||||||
|
if module_name.is_empty() {
|
||||||
|
if let Some(first) = parts.first() {
|
||||||
|
for keyword in crate::keyword::KEYWORDS.iter() {
|
||||||
|
if first == keyword {
|
||||||
|
return Err((
|
||||||
|
NoProgress,
|
||||||
|
EExpr::Start(initial.line, initial.column),
|
||||||
|
initial,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((progress, ident, state))
|
||||||
|
}
|
||||||
|
Err((NoProgress, _, state)) => {
|
||||||
|
Err((NoProgress, EExpr::Start(state.line, state.column), state))
|
||||||
|
}
|
||||||
|
Err((MadeProgress, fail, state)) => match fail {
|
||||||
|
BadIdent::Start(r, c) => Err((NoProgress, EExpr::Start(r, c), state)),
|
||||||
|
BadIdent::Space(e, r, c) => Err((NoProgress, EExpr::Space(e, r, c), state)),
|
||||||
|
_ => malformed_identifier(initial.bytes, fail, arena, state),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn malformed_identifier<'a>(
|
||||||
|
initial_bytes: &'a [u8],
|
||||||
|
problem: BadIdent,
|
||||||
|
_arena: &'a Bump,
|
||||||
|
mut state: State<'a>,
|
||||||
|
) -> ParseResult<'a, Ident<'a>, EExpr<'a>> {
|
||||||
|
// skip forward to the next non-identifier character
|
||||||
|
while !state.bytes.is_empty() {
|
||||||
|
match peek_utf8_char(&state) {
|
||||||
|
Ok((ch, bytes_parsed)) => {
|
||||||
|
// We can't use ch.is_alphanumeric() here because that passes for
|
||||||
|
// things that are "numeric" but not ASCII digits, like `¾`
|
||||||
|
if ch == '.' || ch == '_' || ch.is_alphabetic() || ch.is_ascii_digit() {
|
||||||
|
state = state.advance_without_indenting_ee(bytes_parsed, |r, c| {
|
||||||
|
EExpr::Space(crate::parser::BadInputError::LineTooLong, r, c)
|
||||||
|
})?;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_reason) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = &initial_bytes[..(initial_bytes.len() - state.bytes.len())];
|
||||||
|
|
||||||
|
let parsed_str = unsafe { std::str::from_utf8_unchecked(parsed) };
|
||||||
|
|
||||||
|
Ok((MadeProgress, Ident::Malformed(parsed_str, problem), state))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BadIdent {
|
||||||
|
Start(Row, Col),
|
||||||
|
Space(BadInputError, Row, Col),
|
||||||
|
Underscore(Row, Col),
|
||||||
|
QualifiedTag(Row, Col),
|
||||||
|
PrivateTagNotUppercase(Row, Col),
|
||||||
|
PartStartsWithNumber(Row, Col),
|
||||||
|
WeirdAccessor(Row, Col),
|
||||||
|
PrivateTagFieldAccess(Row, Col),
|
||||||
|
|
||||||
|
WeirdDotAccess(Row, Col),
|
||||||
|
WeirdDotQualified(Row, Col),
|
||||||
|
DoubleDot(Row, Col),
|
||||||
|
StrayDot(Row, Col),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an identifier into a string.
|
||||||
|
///
|
||||||
|
/// This is separate from the `ident` Parser because string interpolation
|
||||||
|
/// wants to use it this way.
|
||||||
|
pub fn parse_ident_help_help<'a>(
|
||||||
|
arena: &'a Bump,
|
||||||
|
mut state: State<'a>,
|
||||||
|
) -> ParseResult<'a, (Ident<'a>, Option<char>), BadIdent> {
|
||||||
|
let mut part_buf = String::new_in(arena); // The current "part" (parts are dot-separated.)
|
||||||
|
let mut capitalized_parts: Vec<&'a str> = Vec::new_in(arena);
|
||||||
|
let mut noncapitalized_parts: Vec<&'a str> = Vec::new_in(arena);
|
||||||
|
let mut is_capitalized;
|
||||||
|
let is_accessor_fn;
|
||||||
|
let mut is_private_tag = false;
|
||||||
|
|
||||||
|
// Identifiers and accessor functions must start with either a letter or a dot.
|
||||||
|
// If this starts with neither, it must be something else!
|
||||||
|
match peek_utf8_char(&state) {
|
||||||
|
Ok((first_ch, bytes_parsed)) => {
|
||||||
|
if first_ch.is_alphabetic() {
|
||||||
|
part_buf.push(first_ch);
|
||||||
|
|
||||||
|
is_capitalized = first_ch.is_uppercase();
|
||||||
|
is_accessor_fn = false;
|
||||||
|
|
||||||
|
state = advance_state!(state, bytes_parsed)?;
|
||||||
|
} else if first_ch == '.' {
|
||||||
|
is_capitalized = false;
|
||||||
|
is_accessor_fn = true;
|
||||||
|
|
||||||
|
state = advance_state!(state, bytes_parsed)?;
|
||||||
|
} else if first_ch == '@' {
|
||||||
|
state = advance_state!(state, bytes_parsed)?;
|
||||||
|
|
||||||
|
// '@' must always be followed by a capital letter!
|
||||||
|
match peek_utf8_char(&state) {
|
||||||
|
Ok((next_ch, next_bytes_parsed)) => {
|
||||||
|
if next_ch.is_uppercase() {
|
||||||
|
state = advance_state!(state, next_bytes_parsed)?;
|
||||||
|
|
||||||
|
part_buf.push('@');
|
||||||
|
part_buf.push(next_ch);
|
||||||
|
|
||||||
|
is_private_tag = true;
|
||||||
|
is_capitalized = true;
|
||||||
|
is_accessor_fn = false;
|
||||||
|
} else {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::PrivateTagNotUppercase(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_reason) => {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::PrivateTagNotUppercase(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err((NoProgress, BadIdent::Start(state.line, state.column), state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_reason) => {
|
||||||
|
return Err((NoProgress, BadIdent::Start(state.line, state.column), state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while !state.bytes.is_empty() {
|
||||||
|
match peek_utf8_char(&state) {
|
||||||
|
Ok((ch, bytes_parsed)) => {
|
||||||
|
// After the first character, only these are allowed:
|
||||||
|
//
|
||||||
|
// * Unicode alphabetic chars - you might name a variable `鹏` if that's clear to your readers
|
||||||
|
// * ASCII digits - e.g. `1` but not `¾`, both of which pass .is_numeric()
|
||||||
|
// * A dot ('.')
|
||||||
|
if ch.is_alphabetic() {
|
||||||
|
if part_buf.is_empty() {
|
||||||
|
// Capitalization is determined by the first character in the part.
|
||||||
|
is_capitalized = ch.is_uppercase();
|
||||||
|
}
|
||||||
|
|
||||||
|
part_buf.push(ch);
|
||||||
|
} else if ch.is_ascii_digit() {
|
||||||
|
// Parts may not start with numbers!
|
||||||
|
if part_buf.is_empty() {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::PartStartsWithNumber(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
part_buf.push(ch);
|
||||||
|
} else if ch == '.' {
|
||||||
|
// There are two posssible errors here:
|
||||||
|
//
|
||||||
|
// 1. Having two consecutive dots is an error.
|
||||||
|
// 2. Having capitalized parts after noncapitalized (e.g. `foo.Bar`) is an error.
|
||||||
|
if part_buf.is_empty() {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::DoubleDot(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_capitalized && !noncapitalized_parts.is_empty() {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::WeirdDotQualified(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_capitalized {
|
||||||
|
capitalized_parts.push(part_buf.into_bump_str());
|
||||||
|
} else {
|
||||||
|
noncapitalized_parts.push(part_buf.into_bump_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've recorded the contents of the current buffer, reset it.
|
||||||
|
part_buf = String::new_in(arena);
|
||||||
|
} else if ch == '_' {
|
||||||
|
// we don't allow underscores in the middle of an identifier
|
||||||
|
// but still parse them (and generate a malformed identifier)
|
||||||
|
// to give good error messages for this case
|
||||||
|
state = advance_state!(state, bytes_parsed)?;
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::Underscore(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// This must be the end of the identifier. We're done!
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = advance_state!(state, bytes_parsed)?;
|
||||||
|
}
|
||||||
|
Err(_reason) => {
|
||||||
|
//
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::Start(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if part_buf.is_empty() {
|
||||||
|
// We probably had a trailing dot, e.g. `Foo.bar.` - this is malformed!
|
||||||
|
//
|
||||||
|
// This condition might also occur if we encounter a malformed accessor like `.|`
|
||||||
|
//
|
||||||
|
// If we made it this far and don't have a next_char, then necessarily
|
||||||
|
// we have consumed a '.' char previously.
|
||||||
|
let fail = if noncapitalized_parts.is_empty() {
|
||||||
|
if capitalized_parts.is_empty() {
|
||||||
|
BadIdent::StrayDot(state.line, state.column)
|
||||||
|
} else {
|
||||||
|
BadIdent::WeirdDotQualified(state.line, state.column)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BadIdent::WeirdDotAccess(state.line, state.column)
|
||||||
|
};
|
||||||
|
|
||||||
|
return Err((MadeProgress, fail, state));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the final parts.
|
||||||
|
if is_capitalized {
|
||||||
|
capitalized_parts.push(part_buf.into_bump_str());
|
||||||
|
} else {
|
||||||
|
noncapitalized_parts.push(part_buf.into_bump_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer = if is_accessor_fn {
|
||||||
|
// Handle accessor functions first because they have the strictest requirements.
|
||||||
|
// Accessor functions may have exactly 1 noncapitalized part, and no capitalzed parts.
|
||||||
|
if capitalized_parts.is_empty() && noncapitalized_parts.len() == 1 && !is_private_tag {
|
||||||
|
let value = noncapitalized_parts.iter().next().unwrap();
|
||||||
|
|
||||||
|
Ident::AccessorFunction(value)
|
||||||
|
} else {
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::WeirdAccessor(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if noncapitalized_parts.is_empty() {
|
||||||
|
// We have capitalized parts only, so this must be a tag.
|
||||||
|
match capitalized_parts.first() {
|
||||||
|
Some(value) => {
|
||||||
|
if capitalized_parts.len() == 1 {
|
||||||
|
if is_private_tag {
|
||||||
|
Ident::PrivateTag(value)
|
||||||
|
} else {
|
||||||
|
Ident::GlobalTag(value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a qualified tag, which is not allowed!
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::QualifiedTag(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// We had neither capitalized nor noncapitalized parts,
|
||||||
|
// yet we made it this far. The only explanation is that this was
|
||||||
|
// a stray '.' drifting through the cosmos.
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::StrayDot(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if is_private_tag {
|
||||||
|
// This is qualified field access with an '@' in front, which does not make sense!
|
||||||
|
return Err((
|
||||||
|
MadeProgress,
|
||||||
|
BadIdent::PrivateTagFieldAccess(state.line, state.column),
|
||||||
|
state,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// We have multiple noncapitalized parts, so this must be field access.
|
||||||
|
Ident::Access {
|
||||||
|
module_name: join_module_parts(arena, capitalized_parts.into_bump_slice()),
|
||||||
|
parts: noncapitalized_parts.into_bump_slice(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((Progress::MadeProgress, (answer, None), state))
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
use crate::ast::{Base, Expr};
|
use crate::ast::Base;
|
||||||
use crate::parser::{parse_utf8, Number, ParseResult, Parser, Progress, State, SyntaxError};
|
use crate::parser::{parse_utf8, Number, ParseResult, Parser, Progress, State, SyntaxError};
|
||||||
use std::char;
|
use std::char;
|
||||||
use std::str::from_utf8_unchecked;
|
use std::str::from_utf8_unchecked;
|
||||||
|
|
||||||
pub fn number_literal<'a>() -> impl Parser<'a, Expr<'a>, Number> {
|
pub enum NumLiteral<'a> {
|
||||||
|
Float(&'a str),
|
||||||
|
Num(&'a str),
|
||||||
|
NonBase10Int {
|
||||||
|
string: &'a str,
|
||||||
|
base: Base,
|
||||||
|
is_negative: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, Number> {
|
||||||
move |_arena, state: State<'a>| {
|
move |_arena, state: State<'a>| {
|
||||||
match state.bytes.get(0) {
|
match state.bytes.get(0) {
|
||||||
Some(first_byte) if *first_byte == b'-' => {
|
Some(first_byte) if *first_byte == b'-' => {
|
||||||
|
@ -25,7 +35,7 @@ fn parse_number_base<'a>(
|
||||||
is_negated: bool,
|
is_negated: bool,
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
state: State<'a>,
|
state: State<'a>,
|
||||||
) -> ParseResult<'a, Expr<'a>, Number> {
|
) -> ParseResult<'a, NumLiteral<'a>, Number> {
|
||||||
match bytes.get(0..2) {
|
match bytes.get(0..2) {
|
||||||
Some(b"0b") => chomp_number_base(Base::Binary, is_negated, &bytes[2..], state),
|
Some(b"0b") => chomp_number_base(Base::Binary, is_negated, &bytes[2..], state),
|
||||||
Some(b"0o") => chomp_number_base(Base::Octal, is_negated, &bytes[2..], state),
|
Some(b"0o") => chomp_number_base(Base::Octal, is_negated, &bytes[2..], state),
|
||||||
|
@ -39,7 +49,7 @@ fn chomp_number_base<'a>(
|
||||||
is_negative: bool,
|
is_negative: bool,
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
state: State<'a>,
|
state: State<'a>,
|
||||||
) -> ParseResult<'a, Expr<'a>, Number> {
|
) -> ParseResult<'a, NumLiteral<'a>, Number> {
|
||||||
let (_is_float, chomped) = chomp_number(bytes);
|
let (_is_float, chomped) = chomp_number(bytes);
|
||||||
|
|
||||||
match parse_utf8(&bytes[0..chomped]) {
|
match parse_utf8(&bytes[0..chomped]) {
|
||||||
|
@ -48,7 +58,7 @@ fn chomp_number_base<'a>(
|
||||||
// all is well
|
// all is well
|
||||||
Ok((
|
Ok((
|
||||||
Progress::MadeProgress,
|
Progress::MadeProgress,
|
||||||
Expr::NonBase10Int {
|
NumLiteral::NonBase10Int {
|
||||||
is_negative,
|
is_negative,
|
||||||
string,
|
string,
|
||||||
base,
|
base,
|
||||||
|
@ -71,7 +81,7 @@ fn chomp_number_dec<'a>(
|
||||||
is_negative: bool,
|
is_negative: bool,
|
||||||
bytes: &'a [u8],
|
bytes: &'a [u8],
|
||||||
state: State<'a>,
|
state: State<'a>,
|
||||||
) -> ParseResult<'a, Expr<'a>, Number> {
|
) -> ParseResult<'a, NumLiteral<'a>, Number> {
|
||||||
let (is_float, chomped) = chomp_number(bytes);
|
let (is_float, chomped) = chomp_number(bytes);
|
||||||
|
|
||||||
if is_negative && chomped == 0 {
|
if is_negative && chomped == 0 {
|
||||||
|
@ -92,9 +102,9 @@ fn chomp_number_dec<'a>(
|
||||||
Ok((
|
Ok((
|
||||||
Progress::MadeProgress,
|
Progress::MadeProgress,
|
||||||
if is_float {
|
if is_float {
|
||||||
Expr::Float(string)
|
NumLiteral::Float(string)
|
||||||
} else {
|
} else {
|
||||||
Expr::Num(string)
|
NumLiteral::Num(string)
|
||||||
},
|
},
|
||||||
new,
|
new,
|
||||||
))
|
))
|
||||||
|
|
|
@ -389,11 +389,22 @@ pub enum EExpr<'a> {
|
||||||
|
|
||||||
Dot(Row, Col),
|
Dot(Row, Col),
|
||||||
Access(Row, Col),
|
Access(Row, Col),
|
||||||
|
UnaryNot(Row, Col),
|
||||||
|
UnaryNegate(Row, Col),
|
||||||
|
BinOp(roc_module::operator::BinOp, Row, Col),
|
||||||
|
|
||||||
Def(&'a SyntaxError<'a>, Row, Col),
|
Def(&'a SyntaxError<'a>, Row, Col),
|
||||||
|
Type(Type<'a>, Row, Col),
|
||||||
|
Pattern(&'a EPattern<'a>, Row, Col),
|
||||||
IndentDefBody(Row, Col),
|
IndentDefBody(Row, Col),
|
||||||
IndentEquals(Row, Col),
|
IndentEquals(Row, Col),
|
||||||
|
IndentAnnotation(Row, Col),
|
||||||
Equals(Row, Col),
|
Equals(Row, Col),
|
||||||
|
Colon(Row, Col),
|
||||||
|
Ident(Row, Col),
|
||||||
|
ElmStyleFunction(Region, Row, Col),
|
||||||
|
MalformedPattern(Row, Col),
|
||||||
|
QualifiedTag(Row, Col),
|
||||||
|
|
||||||
Syntax(&'a SyntaxError<'a>, Row, Col),
|
Syntax(&'a SyntaxError<'a>, Row, Col),
|
||||||
|
|
||||||
|
@ -469,8 +480,7 @@ pub enum EInParens<'a> {
|
||||||
End(Row, Col),
|
End(Row, Col),
|
||||||
Open(Row, Col),
|
Open(Row, Col),
|
||||||
///
|
///
|
||||||
// TODO remove
|
Expr(&'a EExpr<'a>, Row, Col),
|
||||||
Syntax(&'a SyntaxError<'a>, Row, Col),
|
|
||||||
|
|
||||||
///
|
///
|
||||||
Space(BadInputError, Row, Col),
|
Space(BadInputError, Row, Col),
|
||||||
|
@ -488,8 +498,7 @@ pub enum ELambda<'a> {
|
||||||
Arg(Row, Col),
|
Arg(Row, Col),
|
||||||
// TODO make EEXpr
|
// TODO make EEXpr
|
||||||
Pattern(EPattern<'a>, Row, Col),
|
Pattern(EPattern<'a>, Row, Col),
|
||||||
Syntax(&'a SyntaxError<'a>, Row, Col),
|
Body(&'a EExpr<'a>, Row, Col),
|
||||||
|
|
||||||
IndentArrow(Row, Col),
|
IndentArrow(Row, Col),
|
||||||
IndentBody(Row, Col),
|
IndentBody(Row, Col),
|
||||||
IndentArg(Row, Col),
|
IndentArg(Row, Col),
|
||||||
|
@ -516,9 +525,10 @@ pub enum When<'a> {
|
||||||
Pattern(EPattern<'a>, Row, Col),
|
Pattern(EPattern<'a>, Row, Col),
|
||||||
Arrow(Row, Col),
|
Arrow(Row, Col),
|
||||||
Bar(Row, Col),
|
Bar(Row, Col),
|
||||||
|
|
||||||
IfToken(Row, Col),
|
IfToken(Row, Col),
|
||||||
// TODO make EEXpr
|
IfGuard(&'a EExpr<'a>, Row, Col),
|
||||||
IfGuard(&'a SyntaxError<'a>, Row, Col),
|
|
||||||
Condition(&'a EExpr<'a>, Row, Col),
|
Condition(&'a EExpr<'a>, Row, Col),
|
||||||
Branch(&'a EExpr<'a>, Row, Col),
|
Branch(&'a EExpr<'a>, Row, Col),
|
||||||
Syntax(&'a SyntaxError<'a>, Row, Col),
|
Syntax(&'a SyntaxError<'a>, Row, Col),
|
||||||
|
@ -562,6 +572,7 @@ pub enum EPattern<'a> {
|
||||||
Space(BadInputError, Row, Col),
|
Space(BadInputError, Row, Col),
|
||||||
|
|
||||||
PInParens(PInParens<'a>, Row, Col),
|
PInParens(PInParens<'a>, Row, Col),
|
||||||
|
NumLiteral(Number, Row, Col),
|
||||||
|
|
||||||
IndentStart(Row, Col),
|
IndentStart(Row, Col),
|
||||||
IndentEnd(Row, Col),
|
IndentEnd(Row, Col),
|
||||||
|
@ -1958,6 +1969,44 @@ macro_rules! one_or_more {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! one_or_more_e {
|
||||||
|
($parser:expr, $to_error:expr) => {
|
||||||
|
move |arena, state: State<'a>| {
|
||||||
|
use bumpalo::collections::Vec;
|
||||||
|
|
||||||
|
match $parser.parse(arena, state) {
|
||||||
|
Ok((_, first_output, next_state)) => {
|
||||||
|
let mut state = next_state;
|
||||||
|
let mut buf = Vec::with_capacity_in(1, arena);
|
||||||
|
|
||||||
|
buf.push(first_output);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match $parser.parse(arena, state) {
|
||||||
|
Ok((_, next_output, next_state)) => {
|
||||||
|
state = next_state;
|
||||||
|
buf.push(next_output);
|
||||||
|
}
|
||||||
|
Err((NoProgress, _, old_state)) => {
|
||||||
|
return Ok((MadeProgress, buf, old_state));
|
||||||
|
}
|
||||||
|
Err((MadeProgress, fail, old_state)) => {
|
||||||
|
return Err((MadeProgress, fail, old_state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err((progress, _, new_state)) => Err((
|
||||||
|
progress,
|
||||||
|
$to_error(new_state.line, new_state.column),
|
||||||
|
new_state,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! debug {
|
macro_rules! debug {
|
||||||
($parser:expr) => {
|
($parser:expr) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::ast::Pattern;
|
use crate::ast::Pattern;
|
||||||
use crate::blankspace::{space0_around_ee, space0_before_e, space0_e};
|
use crate::blankspace::{space0_around_ee, space0_before_e, space0_e};
|
||||||
use crate::ident::{ident, lowercase_ident, Ident};
|
use crate::ident::{ident, lowercase_ident, Ident};
|
||||||
use crate::number_literal::number_literal;
|
|
||||||
use crate::parser::Progress::{self, *};
|
use crate::parser::Progress::{self, *};
|
||||||
use crate::parser::{
|
use crate::parser::{
|
||||||
backtrackable, optional, specialize, specialize_ref, word1, EPattern, PInParens, PRecord,
|
backtrackable, optional, specialize, specialize_ref, word1, EPattern, PInParens, PRecord,
|
||||||
|
@ -144,9 +143,23 @@ fn loc_pattern_in_parens_help<'a>(
|
||||||
|
|
||||||
fn number_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> {
|
fn number_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> {
|
||||||
specialize(
|
specialize(
|
||||||
|_, r, c| EPattern::Start(r, c),
|
EPattern::NumLiteral,
|
||||||
map_with_arena!(number_literal(), |arena, expr| {
|
map!(crate::number_literal::number_literal(), |literal| {
|
||||||
crate::expr::expr_to_pattern(arena, &expr).unwrap()
|
use crate::number_literal::NumLiteral::*;
|
||||||
|
|
||||||
|
match literal {
|
||||||
|
Num(s) => Pattern::NumLiteral(s),
|
||||||
|
Float(s) => Pattern::FloatLiteral(s),
|
||||||
|
NonBase10Int {
|
||||||
|
string,
|
||||||
|
base,
|
||||||
|
is_negative,
|
||||||
|
} => Pattern::NonBase10Literal {
|
||||||
|
string,
|
||||||
|
base,
|
||||||
|
is_negative,
|
||||||
|
},
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -267,12 +280,15 @@ fn loc_ident_pattern_help<'a>(
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
)),
|
)),
|
||||||
Ident::Malformed(malformed) => {
|
Ident::Malformed(malformed, problem) => {
|
||||||
debug_assert!(!malformed.is_empty());
|
debug_assert!(!malformed.is_empty());
|
||||||
|
|
||||||
Err((
|
Ok((
|
||||||
MadeProgress,
|
MadeProgress,
|
||||||
EPattern::Start(state.line, state.column),
|
Located {
|
||||||
|
region: loc_ident.region,
|
||||||
|
value: Pattern::MalformedIdent(malformed, problem),
|
||||||
|
},
|
||||||
state,
|
state,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ pub fn located<'a>(
|
||||||
specialize(|x, _, _| SyntaxError::Type(x), expression(min_indent))
|
specialize(|x, _, _| SyntaxError::Type(x), expression(min_indent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn located_help<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>>, Type<'a>> {
|
||||||
|
expression(min_indent)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn tag_union_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TTagUnion<'a>> {
|
fn tag_union_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TTagUnion<'a>> {
|
||||||
move |arena, state| {
|
move |arena, state| {
|
||||||
|
|
|
@ -975,22 +975,25 @@ mod test_parse {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn qualified_global_tag() {
|
fn qualified_global_tag() {
|
||||||
|
use roc_parse::ident::BadIdent;
|
||||||
|
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
let expected = Expr::MalformedIdent("One.Two.Whee");
|
let expected = Expr::MalformedIdent("One.Two.Whee", BadIdent::QualifiedTag(0, 12));
|
||||||
let actual = parse_expr_with(&arena, "One.Two.Whee");
|
let actual = parse_expr_with(&arena, "One.Two.Whee");
|
||||||
|
|
||||||
assert_eq!(Ok(expected), actual);
|
assert_eq!(Ok(expected), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO restore this test - it fails, but is not worth fixing right now.
|
#[test]
|
||||||
// #[test]
|
fn private_qualified_tag() {
|
||||||
// fn qualified_private_tag() {
|
use roc_parse::ident::BadIdent;
|
||||||
// let arena = Bump::new();
|
|
||||||
// let expected = Expr::MalformedIdent("One.Two.@Whee");
|
|
||||||
// let actual = parse_expr_with(&arena, "One.Two.@Whee");
|
|
||||||
|
|
||||||
// assert_eq!(Ok(expected), actual);
|
let arena = Bump::new();
|
||||||
// }
|
let expected = Expr::MalformedIdent("@One.Two.Whee", BadIdent::QualifiedTag(0, 13));
|
||||||
|
let actual = parse_expr_with(&arena, "@One.Two.Whee");
|
||||||
|
|
||||||
|
assert_eq!(Ok(expected), actual);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tag_pattern() {
|
fn tag_pattern() {
|
||||||
|
@ -1003,15 +1006,6 @@ mod test_parse {
|
||||||
assert_eq!(Ok(expected), actual);
|
assert_eq!(Ok(expected), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn private_qualified_tag() {
|
|
||||||
let arena = Bump::new();
|
|
||||||
let expected = Expr::MalformedIdent("@One.Two.Whee");
|
|
||||||
let actual = parse_expr_with(&arena, "@One.Two.Whee");
|
|
||||||
|
|
||||||
assert_eq!(Ok(expected), actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
// LISTS
|
// LISTS
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1501,15 +1495,26 @@ mod test_parse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
|
||||||
fn malformed_ident_due_to_underscore() {
|
fn malformed_ident_due_to_underscore() {
|
||||||
// This is a regression test against a bug where if you included an
|
// This is a regression test against a bug where if you included an
|
||||||
// underscore in an argument name, it would parse as three arguments
|
// underscore in an argument name, it would parse as three arguments
|
||||||
// (and would ignore the underscore as if it had been blank space).
|
// (and would ignore the underscore as if it had been blank space).
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
|
|
||||||
|
let pattern = Located::new(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
Pattern::MalformedIdent(&"the_answer", roc_parse::ident::BadIdent::Underscore(0, 5)),
|
||||||
|
);
|
||||||
|
let patterns = &[pattern];
|
||||||
|
let expr = Located::new(0, 0, 15, 17, Expr::Num("42"));
|
||||||
|
|
||||||
|
let expected = Closure(patterns, &expr);
|
||||||
let actual = parse_expr_with(&arena, "\\the_answer -> 42");
|
let actual = parse_expr_with(&arena, "\\the_answer -> 42");
|
||||||
|
|
||||||
assert_eq!(Ok(MalformedClosure), actual);
|
assert_eq!(Ok(expected), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -133,7 +133,7 @@ pub enum RuntimeError {
|
||||||
region: Region,
|
region: Region,
|
||||||
},
|
},
|
||||||
InvalidPrecedence(PrecedenceProblem, Region),
|
InvalidPrecedence(PrecedenceProblem, Region),
|
||||||
MalformedIdentifier(Box<str>, Region),
|
MalformedIdentifier(Box<str>, roc_parse::ident::BadIdent, Region),
|
||||||
MalformedClosure(Region),
|
MalformedClosure(Region),
|
||||||
InvalidRecordUpdate {
|
InvalidRecordUpdate {
|
||||||
region: Region,
|
region: Region,
|
||||||
|
@ -167,4 +167,5 @@ pub enum MalformedPatternProblem {
|
||||||
MalformedBase(Base),
|
MalformedBase(Base),
|
||||||
Unknown,
|
Unknown,
|
||||||
QualifiedIdentifier,
|
QualifiedIdentifier,
|
||||||
|
BadIdent(roc_parse::ident::BadIdent),
|
||||||
}
|
}
|
||||||
|
|
|
@ -344,6 +344,253 @@ pub fn can_problem<'b>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_bad_ident_expr_report<'b>(
|
||||||
|
alloc: &'b RocDocAllocator<'b>,
|
||||||
|
bad_ident: roc_parse::ident::BadIdent,
|
||||||
|
surroundings: Region,
|
||||||
|
) -> RocDocBuilder<'b> {
|
||||||
|
use roc_parse::ident::BadIdent::*;
|
||||||
|
|
||||||
|
match bad_ident {
|
||||||
|
Start(_, _) | Space(_, _, _) => unreachable!("these are handled in the parser"),
|
||||||
|
WeirdDotAccess(row, col) | StrayDot(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I trying to parse a record field accessor here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Something like "),
|
||||||
|
alloc.parser_suggestion(".name"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion(".height"),
|
||||||
|
alloc.reflow(" that accesses a value from a record."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
PartStartsWithNumber(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I trying to parse a record field access here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("So I expect to see a lowercase letter next, like "),
|
||||||
|
alloc.parser_suggestion(".name"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion(".height"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
WeirdAccessor(_row, _col) => alloc.stack(vec![
|
||||||
|
alloc.reflow("I am very confused by this field access"),
|
||||||
|
alloc.region(surroundings),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("It looks like a field access on an accessor. I parse"),
|
||||||
|
alloc.parser_suggestion(".client.name"),
|
||||||
|
alloc.reflow(" as "),
|
||||||
|
alloc.parser_suggestion("(.client).name"),
|
||||||
|
alloc.reflow(". Maybe use an anonymous function like "),
|
||||||
|
alloc.parser_suggestion("(\\r -> r.client.name)"),
|
||||||
|
alloc.reflow(" instead"),
|
||||||
|
alloc.reflow("?"),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
WeirdDotQualified(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a qualified name here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("I was expecting to see an identifier next, like "),
|
||||||
|
alloc.parser_suggestion("height"),
|
||||||
|
alloc.reflow(". A complete qualified name looks something like "),
|
||||||
|
alloc.parser_suggestion("Json.Decode.string"),
|
||||||
|
alloc.text("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
QualifiedTag(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a qualified name here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"This looks like a qualified tag name to me, "),
|
||||||
|
alloc.reflow(r"but tags cannot be qualified! "),
|
||||||
|
alloc.reflow(r"Maybe you wanted a qualified name, something like "),
|
||||||
|
alloc.parser_suggestion("Json.Decode.string"),
|
||||||
|
alloc.text("?"),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
PrivateTagNotUppercase(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a private tag here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"But after the "),
|
||||||
|
alloc.keyword("@"),
|
||||||
|
alloc.reflow(r" symbol I found a lowercase letter. "),
|
||||||
|
alloc.reflow(r"All tag names (global and private)"),
|
||||||
|
alloc.reflow(r" must start with an uppercase letter, like "),
|
||||||
|
alloc.parser_suggestion("@UUID"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion("@Secrets"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivateTagFieldAccess(_row, _col) => alloc.stack(vec![
|
||||||
|
alloc.reflow("I am very confused by this field access:"),
|
||||||
|
alloc.region(surroundings),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"It looks like a record field access on a private tag.")
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bad_ident_pattern_report<'b>(
|
||||||
|
alloc: &'b RocDocAllocator<'b>,
|
||||||
|
bad_ident: roc_parse::ident::BadIdent,
|
||||||
|
surroundings: Region,
|
||||||
|
) -> RocDocBuilder<'b> {
|
||||||
|
use roc_parse::ident::BadIdent::*;
|
||||||
|
|
||||||
|
match bad_ident {
|
||||||
|
Start(_, _) | Space(_, _, _) => unreachable!("these are handled in the parser"),
|
||||||
|
WeirdDotAccess(row, col) | StrayDot(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I trying to parse a record field accessor here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Something like "),
|
||||||
|
alloc.parser_suggestion(".name"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion(".height"),
|
||||||
|
alloc.reflow(" that accesses a value from a record."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
PartStartsWithNumber(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I trying to parse a record field access here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("So I expect to see a lowercase letter next, like "),
|
||||||
|
alloc.parser_suggestion(".name"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion(".height"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
WeirdAccessor(_row, _col) => alloc.stack(vec![
|
||||||
|
alloc.reflow("I am very confused by this field access"),
|
||||||
|
alloc.region(surroundings),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("It looks like a field access on an accessor. I parse"),
|
||||||
|
alloc.parser_suggestion(".client.name"),
|
||||||
|
alloc.reflow(" as "),
|
||||||
|
alloc.parser_suggestion("(.client).name"),
|
||||||
|
alloc.reflow(". Maybe use an anonymous function like "),
|
||||||
|
alloc.parser_suggestion("(\\r -> r.client.name)"),
|
||||||
|
alloc.reflow(" instead"),
|
||||||
|
alloc.reflow("?"),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
WeirdDotQualified(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a qualified name here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("I was expecting to see an identifier next, like "),
|
||||||
|
alloc.parser_suggestion("height"),
|
||||||
|
alloc.reflow(". A complete qualified name looks something like "),
|
||||||
|
alloc.parser_suggestion("Json.Decode.string"),
|
||||||
|
alloc.text("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
QualifiedTag(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a qualified name here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"This looks like a qualified tag name to me, "),
|
||||||
|
alloc.reflow(r"but tags cannot be qualified! "),
|
||||||
|
alloc.reflow(r"Maybe you wanted a qualified name, something like "),
|
||||||
|
alloc.parser_suggestion("Json.Decode.string"),
|
||||||
|
alloc.text("?"),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
PrivateTagNotUppercase(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse a private tag here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"But after the "),
|
||||||
|
alloc.keyword("@"),
|
||||||
|
alloc.reflow(r" symbol I found a lowercase letter. "),
|
||||||
|
alloc.reflow(r"All tag names (global and private)"),
|
||||||
|
alloc.reflow(r" must start with an uppercase letter, like "),
|
||||||
|
alloc.parser_suggestion("@UUID"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion("@Secrets"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivateTagFieldAccess(_row, _col) => alloc.stack(vec![
|
||||||
|
alloc.reflow("I am very confused by this field access:"),
|
||||||
|
alloc.region(surroundings),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"It looks like a record field access on a private tag.")
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Underscore(row, col) => {
|
||||||
|
let region = Region::from_row_col(row, col - 1);
|
||||||
|
|
||||||
|
alloc.stack(vec![
|
||||||
|
alloc.reflow("I am trying to parse an identifier here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![alloc.reflow(
|
||||||
|
r"Underscores are not allowed in identifiers. Use camelCase instead!",
|
||||||
|
)]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pretty_runtime_error<'b>(
|
fn pretty_runtime_error<'b>(
|
||||||
alloc: &'b RocDocAllocator<'b>,
|
alloc: &'b RocDocAllocator<'b>,
|
||||||
runtime_error: RuntimeError,
|
runtime_error: RuntimeError,
|
||||||
|
@ -432,6 +679,7 @@ fn pretty_runtime_error<'b>(
|
||||||
MalformedBase(Base::Binary) => " binary integer ",
|
MalformedBase(Base::Binary) => " binary integer ",
|
||||||
MalformedBase(Base::Octal) => " octal integer ",
|
MalformedBase(Base::Octal) => " octal integer ",
|
||||||
MalformedBase(Base::Decimal) => " integer ",
|
MalformedBase(Base::Decimal) => " integer ",
|
||||||
|
BadIdent(bad_ident) => return to_bad_ident_pattern_report(alloc, bad_ident, region),
|
||||||
Unknown => " ",
|
Unknown => " ",
|
||||||
QualifiedIdentifier => " qualified ",
|
QualifiedIdentifier => " qualified ",
|
||||||
};
|
};
|
||||||
|
@ -440,7 +688,7 @@ fn pretty_runtime_error<'b>(
|
||||||
MalformedInt | MalformedFloat | MalformedBase(_) => alloc
|
MalformedInt | MalformedFloat | MalformedBase(_) => alloc
|
||||||
.tip()
|
.tip()
|
||||||
.append(alloc.reflow("Learn more about number literals at TODO")),
|
.append(alloc.reflow("Learn more about number literals at TODO")),
|
||||||
Unknown => alloc.nil(),
|
Unknown | BadIdent(_) => alloc.nil(),
|
||||||
QualifiedIdentifier => alloc.tip().append(
|
QualifiedIdentifier => alloc.tip().append(
|
||||||
alloc.reflow("In patterns, only private and global tags can be qualified"),
|
alloc.reflow("In patterns, only private and global tags can be qualified"),
|
||||||
),
|
),
|
||||||
|
@ -482,15 +730,10 @@ fn pretty_runtime_error<'b>(
|
||||||
// do nothing, reported with PrecedenceProblem
|
// do nothing, reported with PrecedenceProblem
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
RuntimeError::MalformedIdentifier(box_str, region) => {
|
RuntimeError::MalformedIdentifier(_box_str, bad_ident, surroundings) => {
|
||||||
alloc.stack(vec![
|
to_bad_ident_expr_report(alloc, bad_ident, surroundings)
|
||||||
alloc.concat(vec![
|
|
||||||
alloc.reflow("The ")
|
|
||||||
.append(format!("`{}`", box_str))
|
|
||||||
.append(alloc.reflow(" identifier is malformed:")),
|
|
||||||
]),
|
|
||||||
alloc.region(region),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
RuntimeError::MalformedClosure(_) => todo!(""),
|
RuntimeError::MalformedClosure(_) => todo!(""),
|
||||||
RuntimeError::InvalidFloat(sign @ FloatErrorKind::PositiveInfinity, region, _raw_str)
|
RuntimeError::InvalidFloat(sign @ FloatErrorKind::PositiveInfinity, region, _raw_str)
|
||||||
|
|
|
@ -145,23 +145,32 @@ fn to_syntax_report<'a>(
|
||||||
}
|
}
|
||||||
Type(typ) => to_type_report(alloc, filename, &typ, 0, 0),
|
Type(typ) => to_type_report(alloc, filename, &typ, 0, 0),
|
||||||
Pattern(pat) => to_pattern_report(alloc, filename, &pat, 0, 0),
|
Pattern(pat) => to_pattern_report(alloc, filename, &pat, 0, 0),
|
||||||
Expr(expr) => to_expr_report(alloc, filename, Context::InDef, &expr, 0, 0),
|
Expr(expr) => to_expr_report(
|
||||||
|
alloc,
|
||||||
|
filename,
|
||||||
|
Context::InDef(start_row, start_col),
|
||||||
|
&expr,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Context {
|
enum Context {
|
||||||
InNode(Node, Row, Col, Box<Context>),
|
InNode(Node, Row, Col, Box<Context>),
|
||||||
InDef,
|
InDef(Row, Col),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Node {
|
enum Node {
|
||||||
WhenCondition,
|
WhenCondition,
|
||||||
WhenBranch,
|
WhenBranch,
|
||||||
|
WhenIfGuard,
|
||||||
IfCondition,
|
IfCondition,
|
||||||
IfThenBranch,
|
IfThenBranch,
|
||||||
IfElseBranch,
|
IfElseBranch,
|
||||||
ListElement,
|
ListElement,
|
||||||
|
InsideParens,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_expr_report<'a>(
|
fn to_expr_report<'a>(
|
||||||
|
@ -169,8 +178,8 @@ fn to_expr_report<'a>(
|
||||||
filename: PathBuf,
|
filename: PathBuf,
|
||||||
context: Context,
|
context: Context,
|
||||||
parse_problem: &roc_parse::parser::EExpr<'a>,
|
parse_problem: &roc_parse::parser::EExpr<'a>,
|
||||||
_start_row: Row,
|
start_row: Row,
|
||||||
_start_col: Col,
|
start_col: Col,
|
||||||
) -> Report<'a> {
|
) -> Report<'a> {
|
||||||
use roc_parse::parser::EExpr;
|
use roc_parse::parser::EExpr;
|
||||||
|
|
||||||
|
@ -184,6 +193,134 @@ fn to_expr_report<'a>(
|
||||||
EExpr::Str(string, row, col) => {
|
EExpr::Str(string, row, col) => {
|
||||||
to_str_report(alloc, filename, context, &string, *row, *col)
|
to_str_report(alloc, filename, context, &string, *row, *col)
|
||||||
}
|
}
|
||||||
|
EExpr::InParens(expr, row, col) => {
|
||||||
|
to_expr_in_parens_report(alloc, filename, context, &expr, *row, *col)
|
||||||
|
}
|
||||||
|
EExpr::Type(tipe, row, col) => to_type_report(alloc, filename, &tipe, *row, *col),
|
||||||
|
EExpr::Def(syntax, row, col) => to_syntax_report(alloc, filename, syntax, *row, *col),
|
||||||
|
|
||||||
|
EExpr::ElmStyleFunction(region, row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
|
let region = *region;
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I am in the middle of parsing a definition, but I got stuck here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Looks like you are trying to define a function. "),
|
||||||
|
alloc.reflow("In roc, functions are always written as a lambda, like "),
|
||||||
|
alloc.parser_suggestion("increment = \\n -> n + 1"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "ARGUMENTS BEFORE EQUALS".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EExpr::Ident(_row, _col) => unreachable!("another branch would be taken"),
|
||||||
|
|
||||||
|
EExpr::QualifiedTag(row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I am very confused by this identifier:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Are you trying to qualify a name? I am execting something like "),
|
||||||
|
alloc.parser_suggestion("Json.Decode.string"),
|
||||||
|
alloc.reflow(". Maybe you are trying to qualify a tag? Tags like "),
|
||||||
|
alloc.parser_suggestion("Err"),
|
||||||
|
alloc.reflow(" are globally scoped in roc, and cannot be qualified."),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "WEIRD IDENTIFIER".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EExpr::Start(row, col) => {
|
||||||
|
let (context_row, context_col, a_thing) = match context {
|
||||||
|
Context::InNode(node, r, c, _) => match node {
|
||||||
|
Node::WhenCondition | Node::WhenBranch | Node::WhenIfGuard => (
|
||||||
|
r,
|
||||||
|
c,
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.text("an "),
|
||||||
|
alloc.keyword("when"),
|
||||||
|
alloc.text(" expression"),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
Node::IfCondition | Node::IfThenBranch | Node::IfElseBranch => (
|
||||||
|
r,
|
||||||
|
c,
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.text("an "),
|
||||||
|
alloc.keyword("if"),
|
||||||
|
alloc.text(" expression"),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
Node::ListElement => (r, c, alloc.text("a list")),
|
||||||
|
Node::InsideParens => (r, c, alloc.text("some parentheses")),
|
||||||
|
},
|
||||||
|
Context::InDef(r, c) => (r, c, alloc.text("a definition")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let surroundings = Region::from_rows_cols(context_row, context_col, *row, *col);
|
||||||
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"I am partway through parsing "),
|
||||||
|
a_thing,
|
||||||
|
alloc.reflow(", but I got stuck here:"),
|
||||||
|
]),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("I was expecting to see an expression like "),
|
||||||
|
alloc.parser_suggestion("42"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.parser_suggestion("\"hello\""),
|
||||||
|
alloc.text("."),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "MISSING EXPRESSION".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EExpr::Colon(row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I am in the middle of parsing a definition, but I got stuck here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Looks like you are trying to define a function. "),
|
||||||
|
alloc.reflow("In roc, functions are always written as a lambda, like "),
|
||||||
|
alloc.parser_suggestion("increment = \\n -> n + 1"),
|
||||||
|
alloc.reflow("."),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "ARGUMENTS BEFORE EQUALS".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -334,7 +471,14 @@ fn to_lambda_report<'a>(
|
||||||
|
|
||||||
ELambda::Start(_row, _col) => unreachable!("another branch would have been taken"),
|
ELambda::Start(_row, _col) => unreachable!("another branch would have been taken"),
|
||||||
|
|
||||||
ELambda::Syntax(syntax, row, col) => to_syntax_report(alloc, filename, syntax, row, col),
|
ELambda::Body(expr, row, col) => to_expr_report(
|
||||||
|
alloc,
|
||||||
|
filename,
|
||||||
|
Context::InDef(start_row, start_col),
|
||||||
|
expr,
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
),
|
||||||
ELambda::Pattern(ref pattern, row, col) => {
|
ELambda::Pattern(ref pattern, row, col) => {
|
||||||
to_pattern_report(alloc, filename, pattern, row, col)
|
to_pattern_report(alloc, filename, pattern, row, col)
|
||||||
}
|
}
|
||||||
|
@ -538,6 +682,75 @@ fn to_str_report<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn to_expr_in_parens_report<'a>(
|
||||||
|
alloc: &'a RocDocAllocator<'a>,
|
||||||
|
filename: PathBuf,
|
||||||
|
context: Context,
|
||||||
|
parse_problem: &roc_parse::parser::EInParens<'a>,
|
||||||
|
start_row: Row,
|
||||||
|
start_col: Col,
|
||||||
|
) -> Report<'a> {
|
||||||
|
use roc_parse::parser::EInParens;
|
||||||
|
|
||||||
|
match *parse_problem {
|
||||||
|
EInParens::Space(error, row, col) => to_space_report(alloc, filename, &error, row, col),
|
||||||
|
EInParens::Expr(expr, row, col) => to_expr_report(
|
||||||
|
alloc,
|
||||||
|
filename,
|
||||||
|
Context::InNode(Node::InsideParens, start_row, start_col, Box::new(context)),
|
||||||
|
expr,
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
),
|
||||||
|
EInParens::End(row, col) | EInParens::IndentEnd(row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, row, col);
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc
|
||||||
|
.reflow("I am partway through parsing a record pattern, but I got stuck here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(
|
||||||
|
r"I was expecting to see a closing parenthesis next, so try adding a ",
|
||||||
|
),
|
||||||
|
alloc.parser_suggestion(")"),
|
||||||
|
alloc.reflow(" and see if that helps?"),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "UNFINISHED PARENTHESES".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EInParens::Open(row, col) | EInParens::IndentOpen(row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, row, col);
|
||||||
|
let region = Region::from_row_col(row, col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(
|
||||||
|
r"I just started parsing an expression in parentheses, but I got stuck here:",
|
||||||
|
),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow(r"An expression in parentheses looks like "),
|
||||||
|
alloc.parser_suggestion("(32)"),
|
||||||
|
alloc.reflow(r" or "),
|
||||||
|
alloc.parser_suggestion("(\"hello\")"),
|
||||||
|
alloc.reflow(" so I was expecting to see an expression next."),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "UNFINISHED PARENTHESES".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_list_report<'a>(
|
fn to_list_report<'a>(
|
||||||
alloc: &'a RocDocAllocator<'a>,
|
alloc: &'a RocDocAllocator<'a>,
|
||||||
|
@ -799,18 +1012,14 @@ fn to_when_report<'a>(
|
||||||
title: "IF GUARD NO CONDITION".to_string(),
|
title: "IF GUARD NO CONDITION".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => to_expr_report(
|
||||||
// to_expr_report(
|
alloc,
|
||||||
// alloc,
|
filename,
|
||||||
// filename,
|
Context::InNode(Node::WhenIfGuard, start_row, start_col, Box::new(context)),
|
||||||
// Context::InNode(Node::WhenIfGuard, start_row, start_col, Box::new(context)),
|
nested,
|
||||||
// expr,
|
row,
|
||||||
// row,
|
col,
|
||||||
// col,
|
),
|
||||||
// )
|
|
||||||
|
|
||||||
to_syntax_report(alloc, filename, nested, row, col)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
When::Arrow(row, col) => {
|
When::Arrow(row, col) => {
|
||||||
let surroundings = Region::from_rows_cols(start_row, start_col, row, col);
|
let surroundings = Region::from_rows_cols(start_row, start_col, row, col);
|
||||||
|
|
|
@ -3158,12 +3158,15 @@ mod test_reporting {
|
||||||
),
|
),
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
── PARSE PROBLEM ───────────────────────────────────────────────────────────────
|
── ARGUMENTS BEFORE EQUALS ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
Unexpected tokens in front of the `=` symbol:
|
I am in the middle of parsing a definition, but I got stuck here:
|
||||||
|
|
||||||
1│ f x y = x
|
1│ f x y = x
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
Looks like you are trying to define a function. In roc, functions are
|
||||||
|
always written as a lambda, like increment = \n -> n + 1.
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -4018,10 +4021,86 @@ mod test_reporting {
|
||||||
r#"
|
r#"
|
||||||
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
The `Foo.Bar` identifier is malformed:
|
I am trying to parse a qualified name here:
|
||||||
|
|
||||||
1│ Foo.Bar
|
1│ Foo.Bar
|
||||||
^^^^^^^
|
^
|
||||||
|
|
||||||
|
This looks like a qualified tag name to me, but tags cannot be
|
||||||
|
qualified! Maybe you wanted a qualified name, something like
|
||||||
|
Json.Decode.string?
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_ident_ends_with_dot() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Foo.Bar.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am trying to parse a qualified name here:
|
||||||
|
|
||||||
|
1│ Foo.Bar.
|
||||||
|
^
|
||||||
|
|
||||||
|
I was expecting to see an identifier next, like height. A complete
|
||||||
|
qualified name looks something like Json.Decode.string.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_access_ends_with_dot() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
foo.bar.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I trying to parse a record field accessor here:
|
||||||
|
|
||||||
|
1│ foo.bar.
|
||||||
|
^
|
||||||
|
|
||||||
|
Something like .name or .height that accesses a value from a record.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn qualified_private_tag() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
@Foo.Bar
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am trying to parse a qualified name here:
|
||||||
|
|
||||||
|
1│ @Foo.Bar
|
||||||
|
^
|
||||||
|
|
||||||
|
This looks like a qualified tag name to me, but tags cannot be
|
||||||
|
qualified! Maybe you wanted a qualified name, something like
|
||||||
|
Json.Decode.string?
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -4096,12 +4175,15 @@ mod test_reporting {
|
||||||
),
|
),
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
── PARSE PROBLEM ───────────────────────────────────────────────────────────────
|
── MISSING EXPRESSION ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Unexpected token :
|
I am partway through parsing a definition, but I got stuck here:
|
||||||
|
|
||||||
|
1│ main =
|
||||||
2│ 5 ** 3
|
2│ 5 ** 3
|
||||||
^
|
^
|
||||||
|
|
||||||
|
I was expecting to see an expression like 42 or "hello".
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -4822,12 +4904,15 @@ mod test_reporting {
|
||||||
),
|
),
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
── PARSE PROBLEM ───────────────────────────────────────────────────────────────
|
── MISSING EXPRESSION ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Unexpected token :
|
I am partway through parsing a definition, but I got stuck here:
|
||||||
|
|
||||||
|
1│ when Just 4 is
|
||||||
2│ Just 4 | ->
|
2│ Just 4 | ->
|
||||||
^
|
^
|
||||||
|
|
||||||
|
I was expecting to see an expression like 42 or "hello".
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
// indoc!(
|
// indoc!(
|
||||||
|
@ -5291,4 +5376,200 @@ mod test_reporting {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyword_record_field_access() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
foo = {}
|
||||||
|
|
||||||
|
foo.if
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── TYPE MISMATCH ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
This expression is used in an unexpected way:
|
||||||
|
|
||||||
|
3│ foo.if
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
This `foo` value is a:
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
But you are trying to use it as:
|
||||||
|
|
||||||
|
{ if : a }b
|
||||||
|
|
||||||
|
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keyword_qualified_import() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Num.if
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
The Num module does not expose a if value:
|
||||||
|
|
||||||
|
1│ Num.if
|
||||||
|
^^^^^^
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stray_dot_expr() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Num.add . 23
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I trying to parse a record field accessor here:
|
||||||
|
|
||||||
|
1│ Num.add . 23
|
||||||
|
^
|
||||||
|
|
||||||
|
Something like .name or .height that accesses a value from a record.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_tag_not_uppercase() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Num.add @foo 23
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am trying to parse a private tag here:
|
||||||
|
|
||||||
|
1│ Num.add @foo 23
|
||||||
|
^
|
||||||
|
|
||||||
|
But after the `@` symbol I found a lowercase letter. All tag names
|
||||||
|
(global and private) must start with an uppercase letter, like @UUID
|
||||||
|
or @Secrets.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_tag_field_access() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
@UUID.bar
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am very confused by this field access:
|
||||||
|
|
||||||
|
1│ @UUID.bar
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
It looks like a record field access on a private tag.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn weird_accessor() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
.foo.bar
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am very confused by this field access
|
||||||
|
|
||||||
|
1│ .foo.bar
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
It looks like a field access on an accessor. I parse.client.name as
|
||||||
|
(.client).name. Maybe use an anonymous function like
|
||||||
|
(\r -> r.client.name) instead?
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn part_starts_with_number() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
foo.100
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I trying to parse a record field access here:
|
||||||
|
|
||||||
|
1│ foo.100
|
||||||
|
^
|
||||||
|
|
||||||
|
So I expect to see a lowercase letter next, like .name or .height.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closure_underscore_ident() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
\the_answer -> 100
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── SYNTAX PROBLEM ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I am trying to parse an identifier here:
|
||||||
|
|
||||||
|
1│ \the_answer -> 100
|
||||||
|
^
|
||||||
|
|
||||||
|
Underscores are not allowed in identifiers. Use camelCase instead!
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -807,7 +807,7 @@ pub fn to_expr2<'a>(
|
||||||
// (RuntimeError(MalformedClosure(region)), Output::default())
|
// (RuntimeError(MalformedClosure(region)), Output::default())
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
MalformedIdent(_name) => {
|
MalformedIdent(_name, _problem) => {
|
||||||
// use roc_problem::can::RuntimeError::*;
|
// use roc_problem::can::RuntimeError::*;
|
||||||
//
|
//
|
||||||
// let problem = MalformedIdentifier((*name).into(), region);
|
// let problem = MalformedIdentifier((*name).into(), region);
|
||||||
|
|
|
@ -409,6 +409,11 @@ pub fn to_pattern2<'a>(
|
||||||
malformed_pattern(env, problem, region)
|
malformed_pattern(env, problem, region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MalformedIdent(_str, bad_ident) => {
|
||||||
|
let problem = MalformedPatternProblem::BadIdent(*bad_ident);
|
||||||
|
malformed_pattern(env, problem, region)
|
||||||
|
}
|
||||||
|
|
||||||
SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) | Nested(sub_pattern) => {
|
SpaceBefore(sub_pattern, _) | SpaceAfter(sub_pattern, _) | Nested(sub_pattern) => {
|
||||||
return to_pattern2(env, scope, pattern_type, sub_pattern, region)
|
return to_pattern2(env, scope, pattern_type, sub_pattern, region)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue