From 1d18f4cc85b79182f557209b5fc5d5c75fa0babb Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 2 Dec 2019 18:32:05 -0500 Subject: [PATCH] Use private/global tags and Access over Field --- src/can/env.rs | 13 ++-- src/can/expr.rs | 7 ++- src/can/mod.rs | 42 ++++--------- src/can/operator.rs | 27 ++++++--- src/can/pattern.rs | 74 ++++++----------------- src/can/problem.rs | 4 +- src/can/procedure.rs | 7 --- src/can/symbol.rs | 14 ++--- src/fmt/expr.rs | 6 +- src/fmt/pattern.rs | 7 +-- src/ident.rs | 18 ------ src/parse/ast.rs | 72 +++++++++++----------- src/parse/ident.rs | 121 +++++++++++++++++++++++-------------- src/parse/mod.rs | 108 +++++++++++++++++++++++---------- tests/helpers/mod.rs | 10 +-- tests/test_canonicalize.rs | 32 +++------- tests/test_parse.rs | 85 ++++++++++++++++++++------ 17 files changed, 333 insertions(+), 314 deletions(-) diff --git a/src/can/env.rs b/src/can/env.rs index da4b290a58..ac09e02c4c 100644 --- a/src/can/env.rs +++ b/src/can/env.rs @@ -1,21 +1,17 @@ use crate::can::problem::Problem; use crate::can::procedure::References; use crate::can::symbol::Symbol; -use crate::collections::{ImMap, MutMap}; -use crate::region::Located; +use crate::collections::MutMap; /// The canonicalization environment for a particular module. pub struct Env { - /// The module's path. Unqualified references to identifiers and variant names are assumed - /// to be relative to this path. + /// The module's path. Private tags and unqualified references to identifiers + /// are assumed to be relative to this path. pub home: Box, /// Problems we've encountered along the way, which will be reported to the user at the end. pub problems: Vec, - /// Variants either declared in this module, or imported. - pub variants: ImMap>>, - /// Closures pub closures: MutMap, @@ -24,10 +20,9 @@ pub struct Env { } impl Env { - pub fn new(home: Box, declared_variants: ImMap>>) -> Env { + pub fn new(home: Box) -> Env { Env { home, - variants: declared_variants, problems: Vec::new(), closures: MutMap::default(), tailcallable_symbol: None, diff --git a/src/can/expr.rs b/src/can/expr.rs index 594c569e4c..6de41c7c8a 100644 --- a/src/can/expr.rs +++ b/src/can/expr.rs @@ -22,8 +22,9 @@ pub enum Expr { FunctionPointer(Variable, Symbol), /// Look up exactly one field on a record, e.g. (expr).foo. - /// Canonicalization will convert chains to single-access, e.g. foo.bar.baz to (foo.bar).baz. - Field(Box>, Box), + Access(Box>, Box), + + Tag(Box, Vec), // Pattern Matching /// Case is guaranteed to be exhaustive at this point. (If it wasn't, then @@ -40,6 +41,8 @@ pub enum Expr { Box>, ), + /// This is *only* for calling functions, not for tag application. + /// The Tag variant contains any applied values inside it. Call(Box, Vec>, CalledVia), Closure(Symbol, Recursive, Vec>, Box>), diff --git a/src/can/mod.rs b/src/can/mod.rs index d6acde075a..4c7cdb937c 100644 --- a/src/can/mod.rs +++ b/src/can/mod.rs @@ -86,7 +86,6 @@ fn canonicalize_def<'a>( let variable = var_store.fresh(); let expected = Expected::NoExpectation(Type::Variable(variable)); let declared_idents = ImMap::default(); // TODO FIXME infer this from scope arg - let declared_variants = ImMap::default(); // TODO get rid of this let name: Box = "TODOfixme".into(); // Desugar operators (convert them to Apply calls, taking into account @@ -102,7 +101,7 @@ fn canonicalize_def<'a>( // scope_prefix will be "Main.foo$" and its first closure will be named "Main.foo$0" let scope_prefix = format!("{}.{}$", home, name).into(); let mut scope = Scope::new(scope_prefix, declared_idents.clone()); - let mut env = Env::new(home, declared_variants.clone()); + let mut env = Env::new(home); let (loc_expr, _) = canonicalize_expr( &ImMap::default(), &mut env, @@ -165,7 +164,6 @@ pub fn canonicalize_declaration<'a>( region: Region, loc_expr: Located>, declared_idents: &ImMap, - declared_variants: &ImMap>>, expected: Expected, ) -> (Located, Output, Vec) { // Desugar operators (convert them to Apply calls, taking into account @@ -181,7 +179,7 @@ pub fn canonicalize_declaration<'a>( // scope_prefix will be "Main.foo$" and its first closure will be named "Main.foo$0" let scope_prefix = format!("{}.{}$", home, name).into(); let mut scope = Scope::new(scope_prefix, declared_idents.clone()); - let mut env = Env::new(home, declared_variants.clone()); + let mut env = Env::new(home); let (loc_expr, output) = canonicalize_expr( &ImMap::default(), &mut env, @@ -846,11 +844,11 @@ fn canonicalize_expr( (expr, output) } - ast::Expr::Field(_, _) - | ast::Expr::QualifiedField(_, _) + ast::Expr::Access(_, _) | ast::Expr::AccessorFunction(_) | ast::Expr::If(_) - | ast::Expr::Variant(_, _) + | ast::Expr::GlobalTag(_) + | ast::Expr::PrivateTag(_) | ast::Expr::MalformedIdent(_) | ast::Expr::MalformedClosure | ast::Expr::PrecedenceConflict(_, _, _) => { @@ -1267,17 +1265,9 @@ fn add_idents_from_pattern<'a>( // Ignore the newline/comment info; it doesn't matter in canonicalization. add_idents_from_pattern(region, pattern, scope, answer) } - Variant(_, _) - | IntLiteral(_) - | HexIntLiteral(_) - | OctalIntLiteral(_) - | BinaryIntLiteral(_) - | FloatLiteral(_) - | StrLiteral(_) - | BlockStrLiteral(_) - | EmptyRecordLiteral - | Malformed(_) - | Underscore => (), + GlobalTag(_) | PrivateTag(_) | IntLiteral(_) | HexIntLiteral(_) | OctalIntLiteral(_) + | BinaryIntLiteral(_) | FloatLiteral(_) | StrLiteral(_) | BlockStrLiteral(_) + | EmptyRecordLiteral | Malformed(_) | Underscore => (), } } @@ -1309,17 +1299,9 @@ fn remove_idents(pattern: &ast::Pattern, idents: &mut ImMap {} + GlobalTag(_) | PrivateTag(_) | IntLiteral(_) | HexIntLiteral(_) | BinaryIntLiteral(_) + | OctalIntLiteral(_) | FloatLiteral(_) | StrLiteral(_) | BlockStrLiteral(_) + | EmptyRecordLiteral | Malformed(_) | Underscore => {} } } @@ -1638,7 +1620,7 @@ fn can_defs<'a>( // see below: a closure needs a fresh References! let mut is_closure = false; - // First, make sure we are actually assigning an identifier instead of (for example) a variant. + // First, make sure we are actually assigning an identifier instead of (for example) a tag. // // If we're assigning (UserId userId) = ... then this is certainly not a closure declaration, // which also implies it's not a self tail call! diff --git a/src/can/operator.rs b/src/can/operator.rs index bab913cdb4..23071089e1 100644 --- a/src/can/operator.rs +++ b/src/can/operator.rs @@ -49,8 +49,6 @@ pub fn desugar<'a>(arena: &'a Bump, loc_expr: &'a Located>) -> &'a Loca | Nested(Str(_)) | BlockStr(_) | Nested(BlockStr(_)) - | QualifiedField(_, _) - | Nested(QualifiedField(_, _)) | AccessorFunction(_) | Nested(AccessorFunction(_)) | Var(_, _) @@ -61,13 +59,24 @@ pub fn desugar<'a>(arena: &'a Bump, loc_expr: &'a Located>) -> &'a Loca | Nested(MalformedClosure) | PrecedenceConflict(_, _, _) | Nested(PrecedenceConflict(_, _, _)) - | Variant(_, _) - | Nested(Variant(_, _)) => loc_expr, + | GlobalTag(_) + | Nested(GlobalTag(_)) + | PrivateTag(_) + | Nested(PrivateTag(_)) => loc_expr, - Field(sub_expr, paths) | Nested(Field(sub_expr, paths)) => arena.alloc(Located { - region: loc_expr.region, - value: Field(desugar(arena, sub_expr), paths.clone()), - }), + Access(sub_expr, paths) | Nested(Access(sub_expr, paths)) => { + let region = loc_expr.region; + let loc_sub_expr = Located { + region, + value: Nested(sub_expr), + }; + let value = Access( + &desugar(arena, arena.alloc(loc_sub_expr)).value, + paths.clone(), + ); + + arena.alloc(Located { region, value }) + } List(elems) | Nested(List(elems)) => { let mut new_elems = Vec::with_capacity_in(elems.len(), arena); @@ -221,7 +230,7 @@ pub fn desugar<'a>(arena: &'a Bump, loc_expr: &'a Located>) -> &'a Loca branches.push(&*arena.alloc(( Located { - value: Pattern::Variant(&[], "False"), + value: Pattern::GlobalTag("False"), region: pattern_region, }, Located { diff --git a/src/can/pattern.rs b/src/can/pattern.rs index f2118026ba..6a09928325 100644 --- a/src/can/pattern.rs +++ b/src/can/pattern.rs @@ -7,7 +7,7 @@ use crate::can::problem::Problem; use crate::can::scope::Scope; use crate::can::symbol::Symbol; use crate::collections::ImMap; -use crate::ident::{Ident, VariantName}; +use crate::ident::Ident; use crate::parse::ast; use crate::region::{Located, Region}; use crate::subs::VarStore; @@ -19,8 +19,9 @@ use crate::types::{Constraint, PExpected, PatternCategory, Type}; #[derive(Clone, Debug, PartialEq)] pub enum Pattern { Identifier(Variable, Symbol), - Variant(Variable, Symbol), - AppliedVariant(Variable, Symbol, Vec>), + Tag(Variable, Symbol), + /// TODO replace regular Tag with this + AppliedTag(Variable, Symbol, Vec>), IntLiteral(i64), FloatLiteral(f64), ExactString(Box), @@ -29,7 +30,6 @@ pub enum Pattern { // Runtime Exceptions Shadowed(Located), - UnrecognizedVariant(Located), // Example: (5 = 1 + 2) is an unsupported pattern in an assignment; Int patterns aren't allowed in assignments! UnsupportedPattern(Region), } @@ -118,9 +118,10 @@ pub fn canonicalize_pattern<'a>( let symbol_and_region = (symbol.clone(), region); // Add this to both scope.idents *and* shadowable_idents. - // The latter is relevant when recursively canonicalizing Variant patterns, - // which can bring multiple new idents into scope. For example, it's important - // that we catch (Blah foo foo) as being an example of shadowing. + // The latter is relevant when recursively canonicalizing + // tag application patterns, which can bring multiple + // new idents into scope. For example, it's important that + // we catch (Blah foo foo) as being an example of shadowing. scope .idents .insert(new_ident.clone(), symbol_and_region.clone()); @@ -132,56 +133,18 @@ pub fn canonicalize_pattern<'a>( } } } + &GlobalTag(name) => { + // Canonicalize the tag's name. + let symbol = Symbol::from_global_tag(name); - // &AppliedVariant((ref loc_name, ref loc_args)) => { - // // Canonicalize the variant's arguments. - // let mut can_args: Vec> = Vec::new(); - - // for loc_arg in loc_args { - // let loc_can_arg = - // canonicalize_pattern(env, subs, constraints, scope, pattern_type, &loc_arg, shadowable_idents); - - // can_args.push(loc_can_arg); - // } - - // // Canonicalize the variant's name. - // let symbol = Symbol::from_variant(&loc_name.value, &env.home); - - // if env.variants.contains_key(&symbol) { - // // No problems; the qualified variant name was in scope! - // Pattern::AppliedVariant(symbol, can_args) - // } else { - // // We couldn't find the variant name in scope. NAMING PROBLEM! - // env.problem(Problem::UnrecognizedVariant(loc_name.clone())); - - // Pattern::UnrecognizedVariant(loc_name.clone()) - // } - // } - &Variant(module_parts, name) => { - // Canonicalize the variant's name. - let variant = if module_parts.is_empty() { - VariantName::Unqualified(name.to_string()) - } else { - VariantName::Qualified(module_parts.to_vec().join("."), name.to_string()) - }; - - let symbol = Symbol::from_variant(&variant, &env.home); - - if env.variants.contains_key(&symbol) { - // No problems; the qualified variant name was in scope! - Pattern::Variant(var_store.fresh(), symbol) - } else { - let loc_name = Located { - region, - value: variant, - }; - // We couldn't find the variant name in scope. NAMING PROBLEM! - env.problem(Problem::UnrecognizedVariant(loc_name.clone())); - - Pattern::UnrecognizedVariant(loc_name.clone()) - } + Pattern::Tag(var_store.fresh(), symbol) } + &PrivateTag(name) => { + // Canonicalize the tag's name. + let symbol = Symbol::from_private_tag(&env.home, name); + Pattern::Tag(var_store.fresh(), symbol) + } &FloatLiteral(ref string) => match pattern_type { CaseBranch => { let float = finish_parsing_float(string) @@ -361,7 +324,8 @@ fn add_constraints<'a>( add_constraints(pattern, scope, region, expected, state) } - Variant(_, _) + GlobalTag(_) + | PrivateTag(_) | Apply(_, _) | RecordDestructure(_) | RecordField(_, _) diff --git a/src/can/problem.rs b/src/can/problem.rs index ff30931762..06be2f4aa3 100644 --- a/src/can/problem.rs +++ b/src/can/problem.rs @@ -1,5 +1,5 @@ use crate::can::pattern::PatternType; -use crate::ident::{Ident, VariantName}; +use crate::ident::Ident; use crate::operator::BinOp; use crate::region::{Located, Region}; @@ -9,7 +9,6 @@ pub enum Problem { Shadowing(Located), UnrecognizedFunctionName(Located), UnrecognizedConstant(Located), - UnrecognizedVariant(Located), UnusedAssignment(Located), UnusedArgument(Located), PrecedenceProblem(PrecedenceProblem), @@ -29,7 +28,6 @@ pub enum RuntimeError { InvalidPrecedence(PrecedenceProblem, Region), UnrecognizedFunctionName(Located), UnrecognizedConstant(Located), - UnrecognizedVariant(Located), FloatOutsideRange(Box), IntOutsideRange(Box), InvalidHex(std::num::ParseIntError, Box), diff --git a/src/can/procedure.rs b/src/can/procedure.rs index 8bc4ee2298..888832f953 100644 --- a/src/can/procedure.rs +++ b/src/can/procedure.rs @@ -46,7 +46,6 @@ impl Procedure { pub struct References { pub locals: ImSet, pub globals: ImSet, - pub variants: ImSet, pub calls: ImSet, } @@ -55,7 +54,6 @@ impl References { References { locals: ImSet::default(), globals: ImSet::default(), - variants: ImSet::default(), calls: ImSet::default(), } } @@ -63,7 +61,6 @@ impl References { pub fn union(mut self, other: References) -> Self { self.locals = self.locals.union(other.locals); self.globals = self.globals.union(other.globals); - self.variants = self.variants.union(other.variants); self.calls = self.calls.union(other.calls); self @@ -72,8 +69,4 @@ impl References { pub fn has_local(&self, symbol: &Symbol) -> bool { self.locals.contains(symbol) } - - pub fn has_variant(&self, symbol: &Symbol) -> bool { - self.variants.contains(symbol) - } } diff --git a/src/can/symbol.rs b/src/can/symbol.rs index bdd321360b..6e113d73ef 100644 --- a/src/can/symbol.rs +++ b/src/can/symbol.rs @@ -1,8 +1,8 @@ -use crate::ident::{UnqualifiedIdent, VariantName}; +use crate::ident::UnqualifiedIdent; use crate::module::ModuleName; use std::fmt; -/// A globally unique identifier, used for both vars and variants. +/// A globally unique identifier, used for both vars and tags. /// It will be used directly in code gen. #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Symbol(Box); @@ -33,12 +33,12 @@ impl Symbol { }) } - pub fn from_variant(variant_name: &VariantName, home: &str) -> Symbol { - match variant_name { - VariantName::Unqualified(name) => Symbol::new(home, name), + pub fn from_global_tag(tag_name: &str) -> Symbol { + Symbol(tag_name.into()) + } - VariantName::Qualified(path, name) => Symbol::new(path, name), - } + pub fn from_private_tag(home: &str, tag_name: &str) -> Symbol { + Symbol(format!("{}.{}", home, tag_name).into()) } pub fn from_module<'a>( diff --git a/src/fmt/expr.rs b/src/fmt/expr.rs index 75bae8a69f..082224a3fd 100644 --- a/src/fmt/expr.rs +++ b/src/fmt/expr.rs @@ -251,13 +251,13 @@ pub fn is_multiline_expr<'a>(expr: &'a Expr<'a>) -> bool { | OctalInt(_) | BinaryInt(_) | Str(_) - | Field(_, _) - | QualifiedField(_, _) + | Access(_, _) | AccessorFunction(_) | Var(_, _) | MalformedIdent(_) | MalformedClosure - | Variant(_, _) => false, + | GlobalTag(_) + | PrivateTag(_) => false, // These expressions always have newlines Defs(_, _) | Case(_, _) => true, diff --git a/src/fmt/pattern.rs b/src/fmt/pattern.rs index 76f67af4ce..41f74b0642 100644 --- a/src/fmt/pattern.rs +++ b/src/fmt/pattern.rs @@ -12,12 +12,7 @@ pub fn fmt_pattern<'a>( match pattern { Identifier(string) => buf.push_str(string), - Variant(module_parts, name) => { - for part in module_parts.iter() { - buf.push_str(part); - buf.push('.'); - } - + GlobalTag(name) | PrivateTag(name) => { buf.push_str(name); } Apply(loc_pattern, loc_arg_patterns) => { diff --git a/src/ident.rs b/src/ident.rs index a07e058d3d..87a5888083 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -40,15 +40,6 @@ impl<'a> UnqualifiedIdent<'a> { } } -/// A variant name, possibly fully-qualified with a module name -/// e.g. (Result.Ok) -/// Parameterized on a phantom marker for whether it has been canonicalized -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum VariantName { - Unqualified(String), - Qualified(String, String), -} - /// An identifier, possibly fully-qualified with a module name /// e.g. (Http.Request from http) /// Parameterized on a phantom marker for whether it has been canonicalized @@ -89,12 +80,3 @@ impl Display for Ident { } } } - -impl Display for VariantName { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - VariantName::Unqualified(name) => write!(f, "{}", name), - VariantName::Qualified(path, name) => write!(f, "{}.{}", path, name), - } - } -} diff --git a/src/parse/ast.rs b/src/parse/ast.rs index 2133492679..6dc25d089a 100644 --- a/src/parse/ast.rs +++ b/src/parse/ast.rs @@ -127,13 +127,10 @@ pub enum Expr<'a> { // String Literals Str(&'a str), BlockStr(&'a [&'a str]), - /// e.g. `(expr).foo.bar` - we rule out nested lookups in canonicalization, - /// but we want to keep the nesting here to give a nicer error message. - Field(&'a Loc>, Vec<'a, &'a str>), - /// e.g. `Foo.Bar.baz.qux` - QualifiedField(&'a [&'a str], &'a [&'a str]), + /// Look up exactly one field on a record, e.g. (expr).foo. + Access(&'a Expr<'a>, UnqualifiedIdent<'a>), /// e.g. `.foo` - AccessorFunction(&'a str), + AccessorFunction(UnqualifiedIdent<'a>), // Collection Literals List(Vec<'a, &'a Loc>>), @@ -141,7 +138,10 @@ pub enum Expr<'a> { // Lookups Var(&'a [&'a str], &'a str), - Variant(&'a [&'a str], &'a str), + + // Tags + GlobalTag(&'a str), + PrivateTag(&'a str), // Pattern Matching Closure(&'a Vec<'a, Loc>>, &'a Loc>), @@ -150,7 +150,7 @@ pub enum Expr<'a> { // Application /// To apply by name, do Apply(Var(...), ...) - /// To apply a variant by name, do Apply(Variant(...), ...) + /// To apply a tag by name, do Apply(Tag(...), ...) Apply(&'a Loc>, Vec<'a, &'a Loc>>, CalledVia), BinOp(&'a (Loc>, Loc, Loc>)), UnaryOp(&'a Loc>, Loc), @@ -272,8 +272,8 @@ pub enum Pattern<'a> { // Identifier Identifier(&'a str), - // Variant, optionally qualified - Variant(&'a [&'a str], &'a str), + GlobalTag(&'a str), + PrivateTag(&'a str), Apply(&'a Loc>, &'a [Loc>]), /// This is Loc rather than Loc so we can record comments /// around the destructured names, e.g. { x ### x does stuff ###, y } @@ -310,39 +310,35 @@ pub enum Pattern<'a> { impl<'a> Pattern<'a> { pub fn from_ident(arena: &'a Bump, ident: Ident<'a>) -> Pattern<'a> { match ident { - Ident::Var(maybe_qualified) => { - if maybe_qualified.module_parts.is_empty() { - Pattern::Identifier(maybe_qualified.value) + Ident::GlobalTag(string) => Pattern::GlobalTag(string), + Ident::PrivateTag(string) => Pattern::PrivateTag(string), + Ident::Access(maybe_qualified) => { + if maybe_qualified.value.len() == 1 { + Pattern::Identifier(maybe_qualified.value.iter().next().unwrap()) } else { - Pattern::Variant(maybe_qualified.module_parts, maybe_qualified.value) - } - } - Ident::Variant(maybe_qualified) => { - Pattern::Variant(maybe_qualified.module_parts, maybe_qualified.value) - } - Ident::Field(maybe_qualified) => { - let mut buf = String::with_capacity_in( - maybe_qualified.module_parts.len() + maybe_qualified.value.len(), - arena, - ); + let mut buf = String::with_capacity_in( + maybe_qualified.module_parts.len() + maybe_qualified.value.len(), + arena, + ); - for part in maybe_qualified.module_parts.iter() { - buf.push_str(part); - buf.push('.'); - } - - let mut iter = maybe_qualified.value.iter().peekable(); - - while let Some(part) = iter.next() { - buf.push_str(part); - - // If there are more fields to come, add a "." - if iter.peek().is_some() { + for part in maybe_qualified.module_parts.iter() { + buf.push_str(part); buf.push('.'); } - } - Pattern::Malformed(buf.into_bump_str()) + let mut iter = maybe_qualified.value.iter().peekable(); + + while let Some(part) = iter.next() { + buf.push_str(part); + + // If there are more fields to come, add a "." + if iter.peek().is_some() { + buf.push('.'); + } + } + + Pattern::Malformed(buf.into_bump_str()) + } } Ident::AccessorFunction(string) => Pattern::Malformed(string), Ident::Malformed(string) => Pattern::Malformed(string), diff --git a/src/parse/ident.rs b/src/parse/ident.rs index e066550b3a..9c8eba891c 100644 --- a/src/parse/ident.rs +++ b/src/parse/ident.rs @@ -8,16 +8,16 @@ use bumpalo::Bump; /// The parser accepts all of these in any position where any one of them could /// appear. This way, canonicalization can give more helpful error messages like -/// "you can't redefine this variant!" if you wrote `Foo = ...` or +/// "you can't redefine this tag!" if you wrote `Foo = ...` or /// "you can only define unqualified constants" if you wrote `Foo.bar = ...` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Ident<'a> { - /// foo or Bar.Baz.foo - Var(MaybeQualified<'a, &'a str>), - /// Foo or Bar.Baz.Foo - Variant(MaybeQualified<'a, &'a str>), - /// foo.bar or Foo.Bar.baz.qux - Field(MaybeQualified<'a, &'a [&'a str]>), + /// Foo or Bar + GlobalTag(&'a str), + /// @Foo or @Bar + PrivateTag(&'a str), + /// foo or foo.bar or Foo.Bar.baz.qux + Access(MaybeQualified<'a, &'a [&'a str]>), /// .foo AccessorFunction(&'a str), /// .Foo or foo. or something like foo.Bar @@ -29,9 +29,8 @@ impl<'a> Ident<'a> { use self::Ident::*; match self { - Var(string) => string.len(), - Variant(string) => string.len(), - Field(string) => string.len(), + GlobalTag(string) | PrivateTag(string) => string.len(), + Access(string) => string.len(), AccessorFunction(string) => string.len(), Malformed(string) => string.len(), } @@ -64,12 +63,31 @@ where 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 chars.next() { Some(ch) => { - if ch.is_alphabetic() { + if ch == '@' { + // '@' must always be followed by a capital letter! + match chars.next() { + Some(ch) if ch.is_uppercase() => { + part_buf.push('@'); + part_buf.push(ch); + + is_private_tag = true; + is_capitalized = true; + is_accessor_fn = false; + } + Some(ch) => { + return Err(unexpected(ch, 0, state, Attempting::Identifier)); + } + None => { + return Err(unexpected_eof(0, Attempting::Identifier, state)); + } + } + } else if ch.is_alphabetic() { part_buf.push(ch); is_capitalized = ch.is_uppercase(); @@ -178,7 +196,7 @@ where 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 { + if capitalized_parts.is_empty() && noncapitalized_parts.len() == 1 && !is_private_tag { let value = noncapitalized_parts.iter().next().unwrap(); Ident::AccessorFunction(value) @@ -193,38 +211,51 @@ where ); } } else { - match noncapitalized_parts.len() { - 0 => { - // We have capitalized parts only, so this must be a variant. - match capitalized_parts.pop() { - Some(value) => Ident::Variant(MaybeQualified { - module_parts: capitalized_parts.into_bump_slice(), - value, - }), - 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, state, Attempting::Identifier)); + 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, + chars, + 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, state, Attempting::Identifier)); + } } - 1 => { - // We have exactly one noncapitalized part, so this must be a var. - let value = noncapitalized_parts.iter().next().unwrap(); - - Ident::Var(MaybeQualified { - module_parts: capitalized_parts.into_bump_slice(), - value, - }) - } - _ => { - // We have multiple noncapitalized parts, so this must be a field. - Ident::Field(MaybeQualified { - module_parts: capitalized_parts.into_bump_slice(), - value: noncapitalized_parts.into_bump_slice(), - }) - } + } else if is_private_tag { + // This is qualified field access with an '@' in front, which does not make sense! + return malformed( + None, + arena, + state, + chars, + capitalized_parts, + noncapitalized_parts, + ); + } else { + // We have multiple noncapitalized parts, so this must be field access. + Ident::Access(MaybeQualified { + module_parts: capitalized_parts.into_bump_slice(), + value: noncapitalized_parts.into_bump_slice(), + }) } }; @@ -288,14 +319,14 @@ pub fn ident<'a>() -> impl Parser<'a, Ident<'a>> { } } -pub fn variant_or_ident<'a, F>(pred: F) -> impl Parser<'a, &'a str> +pub fn global_tag_or_ident<'a, F>(pred: F) -> impl Parser<'a, &'a str> where F: Fn(char) -> bool, { move |arena, state: State<'a>| { let mut chars = state.input.chars(); - // pred will determine if this is a variant or ident (based on capitalization) + // pred will determine if this is a tag or ident (based on capitalization) let first_letter = match chars.next() { Some(first_char) => { if pred(first_char) { @@ -346,12 +377,12 @@ where /// * A record field, e.g. "email" in `.email` or in `email:` /// * A named pattern match, e.g. "foo" in `foo =` or `foo ->` or `\foo ->` pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str> { - variant_or_ident(|first_char| first_char.is_lowercase()) + global_tag_or_ident(|first_char| first_char.is_lowercase()) } pub fn unqualified_ident<'a>() -> impl Parser<'a, UnqualifiedIdent<'a>> { map!( - variant_or_ident(|first_char| first_char.is_alphabetic()), + global_tag_or_ident(|first_char| first_char.is_alphabetic()), UnqualifiedIdent::new ) } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 461a2f174d..f1154e63e8 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -10,13 +10,14 @@ pub mod problems; pub mod string_literal; pub mod type_annotation; +use crate::ident::UnqualifiedIdent; use crate::operator::{BinOp, CalledVia, UnaryOp}; use crate::parse::ast::{AssignedField, Attempting, Def, Expr, MaybeQualified, Pattern, Spaceable}; use crate::parse::blankspace::{ space0, space0_after, space0_around, space0_before, space1, space1_after, space1_around, space1_before, }; -use crate::parse::ident::{ident, lowercase_ident, variant_or_ident, Ident}; +use crate::parse::ident::{global_tag_or_ident, ident, lowercase_ident, Ident}; use crate::parse::number_literal::number_literal; use crate::parse::parser::{ allocated, char, not, not_followed_by, optional, string, then, unexpected, unexpected_eof, @@ -216,13 +217,24 @@ pub fn loc_parenthetical_expr<'a>(min_indent: u16) -> impl Parser<'a, Located Ok(( - Located { - region: loc_expr_with_extras.region, - value: Expr::Field(arena.alloc(loc_expr), fields), - }, - state, - )), + Some(Either::Second(Either::First(fields))) => { + let mut value = loc_expr.value; + + for field in fields { + // Wrap the previous answer in the new one, so we end up + // with a nested Expr. That way, `foo.bar.baz` gets represented + // in the AST as if it had been written (foo.bar).baz all along. + value = Expr::Access(arena.alloc(value), UnqualifiedIdent::new(field)); + } + + Ok(( + Located { + region: loc_expr.region, + value, + }, + state, + )) + } None => Ok((loc_expr, state)), } }, @@ -243,7 +255,8 @@ fn expr_to_pattern<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result, })) } } - Expr::Variant(module_parts, value) => Ok(Pattern::Variant(module_parts, value)), + Expr::GlobalTag(value) => Ok(Pattern::GlobalTag(value)), + Expr::PrivateTag(value) => Ok(Pattern::PrivateTag(value)), Expr::Apply(loc_val, loc_args, _) => { let region = loc_val.region; let value = expr_to_pattern(arena, &loc_val.value)?; @@ -298,7 +311,7 @@ fn expr_to_pattern<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result, // These would not have parsed as patterns Expr::BlockStr(_) | Expr::AccessorFunction(_) - | Expr::Field(_, _) + | Expr::Access(_, _) | Expr::List(_) | Expr::Closure(_, _) | Expr::BinOp(_) @@ -307,8 +320,7 @@ fn expr_to_pattern<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result, | Expr::Case(_, _) | Expr::MalformedClosure | Expr::PrecedenceConflict(_, _, _) - | Expr::UnaryOp(_, _) - | Expr::QualifiedField(_, _) => Err(Fail { + | Expr::UnaryOp(_, _) => Err(Fail { attempting: Attempting::Def, reason: FailReason::InvalidPattern, }), @@ -655,9 +667,10 @@ fn parse_closure_param<'a>( char(')') ), // The least common, but still allowed, e.g. \Foo -> ... - loc!(map!(unqualified_variant(), |name| { - Pattern::Variant(&[], name) - })) + loc!(one_of!( + map!(private_tag(), Pattern::PrivateTag), + map!(global_tag(), Pattern::GlobalTag) + )) ) .parse(arena, state) } @@ -665,7 +678,7 @@ fn parse_closure_param<'a>( fn pattern<'a>(min_indent: u16) -> impl Parser<'a, Pattern<'a>> { one_of!( underscore_pattern(), - variant_pattern(), + tag_pattern(), ident_pattern(), record_destructure(min_indent), string_pattern(), @@ -708,8 +721,11 @@ fn record_destructure<'a>(min_indent: u16) -> impl Parser<'a, Pattern<'a>> { ) } -fn variant_pattern<'a>() -> impl Parser<'a, Pattern<'a>> { - map!(unqualified_variant(), |name| Pattern::Variant(&[], name)) +fn tag_pattern<'a>() -> impl Parser<'a, Pattern<'a>> { + one_of!( + map!(private_tag(), Pattern::PrivateTag), + map!(global_tag(), Pattern::GlobalTag) + ) } fn ident_pattern<'a>() -> impl Parser<'a, Pattern<'a>> { @@ -958,7 +974,7 @@ pub fn ident_etc<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { Some(Either::First(loc_args)) => { let loc_expr = Located { region: loc_ident.region, - value: ident_to_expr(loc_ident.value), + value: ident_to_expr(arena, loc_ident.value), }; let mut allocated_args = Vec::with_capacity_in(loc_args.len(), arena); @@ -999,7 +1015,7 @@ pub fn ident_etc<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { None => { let ident = loc_ident.value.clone(); - Ok((ident_to_expr(ident), state)) + Ok((ident_to_expr(arena, ident), state)) } } }, @@ -1007,8 +1023,8 @@ pub fn ident_etc<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { } pub fn ident_without_apply<'a>() -> impl Parser<'a, Expr<'a>> { - then(loc!(ident()), move |_arena, state, loc_ident| { - Ok((ident_to_expr(loc_ident.value), state)) + then(loc!(ident()), move |arena, state, loc_ident| { + Ok((ident_to_expr(arena, loc_ident.value), state)) }) } @@ -1047,12 +1063,34 @@ pub fn case_with_indent<'a>() -> impl Parser<'a, u16> { } } -fn ident_to_expr(src: Ident<'_>) -> Expr<'_> { +fn ident_to_expr<'a>(arena: &'a Bump, src: Ident<'a>) -> Expr<'a> { match src { - Ident::Var(info) => Expr::Var(info.module_parts, info.value), - Ident::Variant(info) => Expr::Variant(info.module_parts, info.value), - Ident::Field(info) => Expr::QualifiedField(info.module_parts, info.value), - Ident::AccessorFunction(string) => Expr::AccessorFunction(string), + Ident::GlobalTag(string) => Expr::GlobalTag(string), + Ident::PrivateTag(string) => Expr::PrivateTag(string), + Ident::Access(info) => { + let mut iter = info.value.into_iter(); + + // The first value in the iterator is the variable name, + // e.g. `foo` in `foo.bar.baz` + let mut answer = match iter.next() { + Some(var) => Expr::Var(info.module_parts, var), + None => { + panic!("Parsed an Ident::Access with no parts"); + } + }; + + // The remaining items in the iterator are record field accesses, + // e.g. `bar` in `foo.bar.baz`, followed by `baz` + for field in iter { + // Wrap the previous answer in the new one, so we end up + // with a nested Expr. That way, `foo.bar.baz` gets represented + // in the AST as if it had been written (foo.bar).baz all along. + answer = Expr::Access(arena.alloc(answer), UnqualifiedIdent::new(field)); + } + + answer + } + Ident::AccessorFunction(string) => Expr::AccessorFunction(UnqualifiedIdent::new(string)), Ident::Malformed(string) => Expr::MalformedIdent(string), } } @@ -1162,11 +1200,17 @@ pub fn record_literal<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { ) } -/// This is mainly for matching variants in closure params, e.g. \Foo -> ... -/// -/// TODO: this should absolutely support qualified variants. Need to change it and rename it! -fn unqualified_variant<'a>() -> impl Parser<'a, &'a str> { - variant_or_ident(|first_char| first_char.is_uppercase()) +/// This is mainly for matching tags in closure params, e.g. \@Foo -> ... +fn private_tag<'a>() -> impl Parser<'a, &'a str> { + skip_first!(char('@'), global_tag()) +} + +/// This is mainly for matching tags in closure params, e.g. \Foo -> ... +fn global_tag<'a>() -> impl Parser<'a, &'a str> { + skip_first!( + char('@'), + global_tag_or_ident(|first_char| first_char.is_uppercase()) + ) } pub fn string_literal<'a>() -> impl Parser<'a, Expr<'a>> { diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs index 9e7dafd341..dd5c2c8e9f 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -36,13 +36,7 @@ pub fn parse_loc_with<'a>(arena: &'a Bump, input: &'a str) -> Result (Expr, Output, Vec, VarStore, Variable) { - can_expr_with( - &Bump::new(), - "blah", - expr_str, - &ImMap::default(), - &ImMap::default(), - ) + can_expr_with(&Bump::new(), "blah", expr_str, &ImMap::default()) } #[allow(dead_code)] @@ -51,7 +45,6 @@ pub fn can_expr_with( name: &str, expr_str: &str, declared_idents: &ImMap, - declared_variants: &ImMap>>, ) -> (Expr, Output, Vec, VarStore, Variable) { let loc_expr = parse_loc_with(&arena, expr_str).unwrap_or_else(|_| { panic!( @@ -72,7 +65,6 @@ pub fn can_expr_with( Region::zero(), loc_expr, declared_idents, - declared_variants, expected, ); diff --git a/tests/test_canonicalize.rs b/tests/test_canonicalize.rs index 01de8980a6..21a6c45f36 100644 --- a/tests/test_canonicalize.rs +++ b/tests/test_canonicalize.rs @@ -48,7 +48,6 @@ mod test_canonicalize { struct Out<'a> { locals: Vec<&'a str>, globals: Vec<&'a str>, - variants: Vec<&'a str>, calls: Vec<&'a str>, tail_call: Option<&'a str>, } @@ -58,7 +57,6 @@ mod test_canonicalize { let references = References { locals: vec_to_set(self.locals), globals: vec_to_set(self.globals), - variants: vec_to_set(self.variants), calls: vec_to_set(self.calls), }; @@ -78,8 +76,7 @@ mod test_canonicalize { fn assert_can(input: &str, expected: Expr) { let arena = Bump::new(); - let (actual, _, _, _, _) = - can_expr_with(&arena, "Blah", input, &ImMap::default(), &ImMap::default()); + let (actual, _, _, _, _) = can_expr_with(&arena, "Blah", input, &ImMap::default()); assert_eq!(expected, actual); } @@ -141,7 +138,7 @@ mod test_canonicalize { "# ); let (_actual, mut output, problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); assert_eq!(problems, vec![]); @@ -153,7 +150,6 @@ mod test_canonicalize { Out { locals: vec!["func"], globals: vec![], - variants: vec![], calls: vec!["func"], tail_call: None } @@ -177,7 +173,6 @@ mod test_canonicalize { // references: References { // locals: vec_to_set(vec![]), // globals: vec_to_set(vec![]), - // variants: vec_to_set(vec![]), // calls: vec_to_set(vec![]), // } // } @@ -199,7 +194,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (_actual, mut output, problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); assert_eq!(problems, vec![]); @@ -211,7 +206,6 @@ mod test_canonicalize { Out { locals: vec!["identity", "apply"], globals: vec![], - variants: vec![], calls: vec!["f", "apply"], tail_call: None } @@ -257,7 +251,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (actual, _output, _problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::TailRecursive); @@ -283,7 +277,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (actual, _output, _problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::TailRecursive); @@ -300,7 +294,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (actual, _output, _problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::TailRecursive); @@ -320,7 +314,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (actual, _output, _problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::Recursive); @@ -346,7 +340,7 @@ mod test_canonicalize { ); let arena = Bump::new(); let (actual, _output, _problems, _var_store, _vars) = - can_expr_with(&arena, "Blah", src, &ImMap::default(), &ImMap::default()); + can_expr_with(&arena, "Blah", src, &ImMap::default()); let detected = get_closure(&actual, 0); assert_eq!(detected, Recursive::Recursive); @@ -381,7 +375,6 @@ mod test_canonicalize { // Out { // locals: vec!["func", "local"], // globals: vec![], - // variants: vec![], // calls: vec!["func"], // tail_call: None // } @@ -415,7 +408,6 @@ mod test_canonicalize { // Out { // locals: vec!["local"], // globals: vec![], - // variants: vec![], // calls: vec![], // tail_call: None // } @@ -450,7 +442,6 @@ mod test_canonicalize { // Out { // locals: vec![], // globals: vec![], - // variants: vec![], // calls: vec![], // tail_call: None // } @@ -481,7 +472,6 @@ mod test_canonicalize { // Out { // locals: vec!["a", "b"], // globals: vec![], - // variants: vec![], // calls: vec![], // tail_call: None // } @@ -512,7 +502,6 @@ mod test_canonicalize { // Out { // locals: vec!["c"], // globals: vec![], - // variants: vec![], // calls: vec![], // tail_call: None // } @@ -541,7 +530,6 @@ mod test_canonicalize { // Out { // locals: vec!["fibonacci"], // globals: vec![], - // variants: vec![], // calls: vec!["fibonacci"], // tail_call: None // } @@ -576,7 +564,6 @@ mod test_canonicalize { // Out { // locals: vec!["factorial", "factorialHelp"], // globals: vec![], - // variants: vec![], // calls: vec!["factorial", "factorialHelp"], // tail_call: None // } @@ -604,7 +591,6 @@ mod test_canonicalize { // Out { // locals: vec!["a", "b"], // globals: vec![], - // variants: vec![], // calls: vec![], // tail_call: None // } @@ -634,7 +620,6 @@ mod test_canonicalize { // Out { // locals: vec!["increment", "x", "y", "z"], // globals: vec![], - // variants: vec![], // calls: vec!["increment"], // tail_call: None // } @@ -675,7 +660,6 @@ mod test_canonicalize { // Out { // locals: vec!["func1", "func2", "x", "y", "z"], // globals: vec![], - // variants: vec![], // calls: vec!["func1", "func2"], // tail_call: None // } diff --git a/tests/test_parse.rs b/tests/test_parse.rs index 1b71e94085..3128b0a3d8 100644 --- a/tests/test_parse.rs +++ b/tests/test_parse.rs @@ -17,6 +17,7 @@ mod test_parse { use crate::helpers::parse_with; use bumpalo::collections::vec::Vec; use bumpalo::{self, Bump}; + use roc::ident::UnqualifiedIdent; use roc::module::ModuleName; use roc::operator::BinOp::*; use roc::operator::CalledVia; @@ -467,25 +468,51 @@ mod test_parse { // VARIANT #[test] - fn basic_variant() { + fn basic_global_tag() { let arena = Bump::new(); - let module_parts = Vec::new_in(&arena).into_bump_slice(); - let expected = Expr::Variant(module_parts, "Whee"); + let expected = Expr::GlobalTag("Whee"); let actual = parse_with(&arena, "Whee"); assert_eq!(Ok(expected), actual); } #[test] - fn qualified_variant() { + fn basic_private_tag() { let arena = Bump::new(); - let module_parts = bumpalo::vec![in &arena; "One", "Two"].into_bump_slice(); - let expected = Expr::Variant(module_parts, "Whee"); + let expected = Expr::PrivateTag("@Whee"); + let actual = parse_with(&arena, "@Whee"); + + assert_eq!(Ok(expected), actual); + } + + #[test] + fn qualified_global_tag() { + let arena = Bump::new(); + let expected = Expr::MalformedIdent("One.Two.Whee"); let actual = parse_with(&arena, "One.Two.Whee"); assert_eq!(Ok(expected), actual); } + // TODO restore this test - it fails, but is not worth fixing right now. + // #[test] + // fn qualified_private_tag() { + // let arena = Bump::new(); + // let expected = Expr::MalformedIdent("One.Two.@Whee"); + // let actual = parse_with(&arena, "One.Two.@Whee"); + + // assert_eq!(Ok(expected), actual); + // } + + #[test] + fn private_qualified_tag() { + let arena = Bump::new(); + let expected = Expr::MalformedIdent("@One.Two.Whee"); + let actual = parse_with(&arena, "@One.Two.Whee"); + + assert_eq!(Ok(expected), actual); + } + // LISTS #[test] @@ -520,13 +547,25 @@ mod test_parse { // FIELD ACCESS + #[test] + fn basic_field() { + let arena = Bump::new(); + let module_parts = Vec::new_in(&arena).into_bump_slice(); + let field = UnqualifiedIdent::new("field"); + let var = Var(module_parts, "rec"); + let expected = Access(arena.alloc(var), field); + let actual = parse_with(&arena, "rec.field"); + + assert_eq!(Ok(expected), actual); + } + #[test] fn parenthetical_basic_field() { let arena = Bump::new(); let module_parts = Vec::new_in(&arena).into_bump_slice(); - let fields = bumpalo::vec![in &arena; "field"]; + let field = UnqualifiedIdent::new("field"); let paren_var = ParensAround(arena.alloc(Var(module_parts, "rec"))); - let expected = Field(arena.alloc(Located::new(0, 0, 1, 4, paren_var)), fields); + let expected = Access(arena.alloc(paren_var), field); let actual = parse_with(&arena, "(rec).field"); assert_eq!(Ok(expected), actual); @@ -536,21 +575,27 @@ mod test_parse { fn parenthetical_field_qualified_var() { let arena = Bump::new(); let module_parts = bumpalo::vec![in &arena; "One", "Two"].into_bump_slice(); - let fields = bumpalo::vec![in &arena; "field"]; + let field = UnqualifiedIdent::new("field"); let paren_var = ParensAround(arena.alloc(Var(module_parts, "rec"))); - let expected = Field(arena.alloc(Located::new(0, 0, 1, 12, paren_var)), fields); + let expected = Access(arena.alloc(paren_var), field); let actual = parse_with(&arena, "(One.Two.rec).field"); assert_eq!(Ok(expected), actual); } #[test] - fn basic_field() { + fn multiple_fields() { let arena = Bump::new(); let module_parts = Vec::new_in(&arena).into_bump_slice(); - let fields = bumpalo::vec![in &arena; "rec", "field"].into_bump_slice(); - let expected = QualifiedField(module_parts, fields); - let actual = parse_with(&arena, "rec.field"); + let abc = UnqualifiedIdent::new("abc"); + let def = UnqualifiedIdent::new("def"); + let ghi = UnqualifiedIdent::new("ghi"); + let var = Var(module_parts, "rec"); + let expected = Access( + arena.alloc(Access(arena.alloc(Access(arena.alloc(var), abc)), def)), + ghi, + ); + let actual = parse_with(&arena, "rec.abc.def.ghi"); assert_eq!(Ok(expected), actual); } @@ -559,9 +604,15 @@ mod test_parse { fn qualified_field() { let arena = Bump::new(); let module_parts = bumpalo::vec![in &arena; "One", "Two"].into_bump_slice(); - let fields = bumpalo::vec![in &arena; "rec", "field"].into_bump_slice(); - let expected = QualifiedField(module_parts, fields); - let actual = parse_with(&arena, "One.Two.rec.field"); + let abc = UnqualifiedIdent::new("abc"); + let def = UnqualifiedIdent::new("def"); + let ghi = UnqualifiedIdent::new("ghi"); + let var = Var(module_parts, "rec"); + let expected = Access( + arena.alloc(Access(arena.alloc(Access(arena.alloc(var), abc)), def)), + ghi, + ); + let actual = parse_with(&arena, "One.Two.rec.abc.def.ghi"); assert_eq!(Ok(expected), actual); }