diff --git a/src/can/symbol.rs b/src/can/symbol.rs index 29064f96ba..1709c1bd37 100644 --- a/src/can/symbol.rs +++ b/src/can/symbol.rs @@ -1,4 +1,5 @@ -use ident::VariantName; +use ident::{UnqualifiedIdent, VariantName}; +use module::ModuleName; /// A globally unique identifier, used for both vars and variants. /// It will be used directly in code gen. @@ -26,6 +27,13 @@ impl Symbol { } } + pub fn from_module<'a>( + module_name: &'a ModuleName<'a>, + ident: &'a UnqualifiedIdent<'a>, + ) -> Symbol { + Symbol(format!("{}.{}", module_name.as_str(), ident.as_str()).into()) + } + pub fn into_boxed_str(self) -> Box { self.0 } diff --git a/src/fmt.rs b/src/fmt.rs deleted file mode 100644 index 88a3e20ad8..0000000000 --- a/src/fmt.rs +++ /dev/null @@ -1,85 +0,0 @@ -use parse::ast::{AssignedField, Expr, Pattern}; - -pub fn is_multiline_expr<'a>(expr: &'a Expr<'a>) -> bool { - use parse::ast::Expr::*; - // TODO cache these answers using a Map, so - // we don't have to traverse subexpressions repeatedly - - match expr { - // Return whether these spaces contain any Newlines - SpaceBefore(_, spaces) | SpaceAfter(_, spaces) => { - spaces.iter().any(|space| space.contains_newline()) - } - - // These expressions never have newlines - Float(_) - | Int(_) - | HexInt(_) - | OctalInt(_) - | BinaryInt(_) - | Str(_) - | Field(_, _) - | QualifiedField(_, _) - | AccessorFunction(_) - | Var(_, _) - | MalformedIdent(_) - | MalformedClosure - | Variant(_, _) => false, - - // These expressions always have newlines - Defs(_, _) | Case(_, _) => true, - - List(elems) => elems - .iter() - .any(|loc_expr| is_multiline_expr(&loc_expr.value)), - - BlockStr(lines) => lines.len() > 1, - Apply(loc_expr, args, _) => { - is_multiline_expr(&loc_expr.value) - || args.iter().any(|loc_arg| is_multiline_expr(&loc_arg.value)) - } - - If((loc_cond, loc_if_true, loc_if_false)) => { - is_multiline_expr(&loc_cond.value) - || is_multiline_expr(&loc_if_true.value) - || is_multiline_expr(&loc_if_false.value) - } - - BinOp((loc_left, _, loc_right)) => { - is_multiline_expr(&loc_left.value) || is_multiline_expr(&loc_right.value) - } - - UnaryOp(loc_subexpr, _) | PrecedenceConflict(_, _, loc_subexpr) => { - is_multiline_expr(&loc_subexpr.value) - } - - ParensAround(subexpr) => is_multiline_expr(&subexpr), - - Closure(loc_patterns, loc_body) => { - // check the body first because it's more likely to be multiline - is_multiline_expr(&loc_body.value) - || loc_patterns - .iter() - .any(|loc_pattern| is_multiline_pattern(&loc_pattern.value)) - } - - Record(loc_fields) => loc_fields - .iter() - .any(|loc_field| is_multiline_field(&loc_field.value)), - } -} - -pub fn is_multiline_field<'a, Val>(field: &'a AssignedField<'a, Val>) -> bool { - use self::AssignedField::*; - - match field { - LabeledValue(_, spaces, _) => !spaces.is_empty(), - LabelOnly(_) => false, - AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => true, - Malformed(text) => text.chars().any(|c| c == '\n'), - } -} - -pub fn is_multiline_pattern<'a>(_pattern: &'a Pattern<'a>) -> bool { - panic!("TODO return iff there are any newlines") -} diff --git a/src/fmt/def.rs b/src/fmt/def.rs new file mode 100644 index 0000000000..ced594907d --- /dev/null +++ b/src/fmt/def.rs @@ -0,0 +1,28 @@ +use fmt::expr::fmt_expr; +use fmt::pattern::fmt_pattern; +use fmt::spaces::fmt_spaces; + +use bumpalo::collections::String; +use parse::ast::Def; + +pub fn fmt_def<'a>(buf: &mut String<'a>, def: &'a Def<'a>, indent: u16) { + match def { + Def::Annotation(_, _) => panic!("TODO have format_def support Annotation"), + Def::Body(loc_pattern, loc_expr) => { + fmt_pattern(buf, &loc_pattern.value, indent, true); + buf.push_str(" = "); + fmt_expr(buf, &loc_expr.value, indent, false); + } + Def::CustomType(_, _) => panic!("TODO have format_def support CustomType"), + Def::TypeAlias(_, _) => panic!("TODO have format_def support TypeAlias"), + Def::SpaceBefore(sub_def, spaces) => { + fmt_spaces(buf, spaces.iter(), indent); + fmt_def(buf, sub_def, indent); + } + Def::SpaceAfter(sub_def, spaces) => { + fmt_def(buf, sub_def, indent); + + fmt_spaces(buf, spaces.iter(), indent); + } + } +} diff --git a/src/fmt/expr.rs b/src/fmt/expr.rs new file mode 100644 index 0000000000..d7b4461bf0 --- /dev/null +++ b/src/fmt/expr.rs @@ -0,0 +1,359 @@ +use fmt::def::fmt_def; +use fmt::pattern::fmt_pattern; +use fmt::spaces::{add_spaces, fmt_comments_only, fmt_spaces, newline, INDENT}; + +use bumpalo::collections::String; +use parse::ast::{AssignedField, Expr, Pattern}; + +pub fn fmt_expr<'a>( + buf: &mut String<'a>, + expr: &'a Expr<'a>, + indent: u16, + apply_needs_parens: bool, +) { + use self::Expr::*; + + match expr { + SpaceBefore(sub_expr, spaces) => { + fmt_spaces(buf, spaces.iter(), indent); + fmt_expr(buf, sub_expr, indent, apply_needs_parens); + } + SpaceAfter(sub_expr, spaces) => { + fmt_expr(buf, sub_expr, indent, apply_needs_parens); + fmt_spaces(buf, spaces.iter(), indent); + } + ParensAround(sub_expr) => { + buf.push('('); + fmt_expr(buf, sub_expr, indent, false); + buf.push(')'); + } + Str(string) => { + buf.push('"'); + buf.push_str(string); + buf.push('"'); + } + Var(module_parts, name) => { + for part in module_parts.iter() { + buf.push_str(part); + buf.push('.'); + } + + buf.push_str(name); + } + Apply(loc_expr, loc_args, _) => { + if apply_needs_parens { + buf.push('('); + } + + fmt_expr(buf, &loc_expr.value, indent, true); + + for loc_arg in loc_args { + buf.push(' '); + + fmt_expr(buf, &loc_arg.value, indent, true); + } + + if apply_needs_parens { + buf.push(')'); + } + } + BlockStr(lines) => { + buf.push_str("\"\"\""); + for line in lines.iter() { + buf.push_str(line); + } + buf.push_str("\"\"\""); + } + Int(string) => buf.push_str(string), + Float(string) => buf.push_str(string), + HexInt(string) => { + buf.push('0'); + buf.push('x'); + buf.push_str(string); + } + BinaryInt(string) => { + buf.push('0'); + buf.push('b'); + buf.push_str(string); + } + OctalInt(string) => { + buf.push('0'); + buf.push('o'); + buf.push_str(string); + } + Record(loc_fields) => { + buf.push('{'); + + let is_multiline = loc_fields + .iter() + .any(|loc_field| is_multiline_field(&loc_field.value)); + + let mut iter = loc_fields.iter().peekable(); + let field_indent = if is_multiline { + indent + INDENT + } else { + if !loc_fields.is_empty() { + buf.push(' '); + } + + indent + }; + + while let Some(field) = iter.next() { + fmt_field( + buf, + &field.value, + is_multiline, + field_indent, + apply_needs_parens, + ); + + if iter.peek().is_some() { + buf.push(','); + + if !is_multiline { + buf.push(' '); + } + } + } + + if is_multiline { + buf.push('\n'); + } else if !loc_fields.is_empty() { + buf.push(' '); + } + + buf.push('}'); + } + Closure(loc_patterns, loc_ret) => { + buf.push('\\'); + + for loc_pattern in loc_patterns.iter() { + fmt_pattern(buf, &loc_pattern.value, indent, true); + + buf.push(' '); + } + + let is_multiline = is_multiline_expr(&loc_ret.value); + + // If the body is multiline, go down a line and indent. + let indent = if is_multiline { + indent + INDENT + } else { + indent + }; + + buf.push_str("->"); + + let newline_is_next = match &loc_ret.value { + SpaceBefore(_, _) => true, + _ => false, + }; + + if !newline_is_next { + // Push a space after the "->" preceding this. + buf.push(' '); + } + + fmt_expr(buf, &loc_ret.value, indent, false); + } + Defs(defs, ret) => { + // It should theoretically be impossible to *parse* an empty defs list. + // (Canonicalization can remove defs later, but that hasn't happened yet!) + debug_assert!(!defs.is_empty()); + + // The first def is located last in the list, because it gets added there + // with .push() for efficiency. (The order of parsed defs doesn't + // matter because canonicalization sorts them anyway.) + // The other defs in the list are in their usual order. + if let Some(loc_first_def) = defs.last() { + let other_spaced_defs = &defs[0..defs.len() - 1]; + + fmt_def(buf, &loc_first_def.value, indent); + + for loc_def in other_spaced_defs.iter() { + fmt_def(buf, &loc_def.value, indent); + } + } + + // Even if there were no defs, which theoretically should never happen, + // still print the return value. + fmt_expr(buf, &ret.value, indent, false); + } + If((loc_condition, loc_then, loc_else)) => { + buf.push_str("if "); + fmt_expr(buf, &loc_condition.value, indent, false); + buf.push_str(" then "); + fmt_expr(buf, &loc_then.value, indent, false); + buf.push_str(" else "); + fmt_expr(buf, &loc_else.value, indent, false); + } + Case(loc_condition, branches) => { + buf.push_str("case "); + fmt_expr(buf, &loc_condition.value, indent, false); + buf.push_str(" when\n"); + + let mut it = branches.iter().peekable(); + while let Some((pattern, expr)) = it.next() { + add_spaces(buf, indent + INDENT); + + match pattern.value { + Pattern::SpaceBefore(nested, spaces) => { + fmt_comments_only(buf, spaces.iter(), indent + INDENT); + fmt_pattern(buf, nested, indent + INDENT, false); + } + _ => { + fmt_pattern(buf, &pattern.value, indent + INDENT, false); + } + } + + buf.push_str(" ->\n"); + + add_spaces(buf, indent + (INDENT * 2)); + match expr.value { + Expr::SpaceBefore(nested, spaces) => { + fmt_comments_only(buf, spaces.iter(), indent + (INDENT * 2)); + fmt_expr(buf, &nested, indent + (INDENT * 2), false); + } + _ => { + fmt_expr(buf, &expr.value, indent + (INDENT * 2), false); + } + } + + if it.peek().is_some() { + buf.push('\n'); + buf.push('\n'); + } + } + } + other => panic!("TODO implement Display for AST variant {:?}", other), + } +} + +pub fn fmt_field<'a>( + buf: &mut String<'a>, + assigned_field: &'a AssignedField<'a, Expr<'a>>, + is_multiline: bool, + indent: u16, + apply_needs_parens: bool, +) { + use self::AssignedField::*; + + match assigned_field { + LabeledValue(name, spaces, value) => { + if is_multiline { + newline(buf, indent); + } + + buf.push_str(name.value); + + if !spaces.is_empty() { + fmt_spaces(buf, spaces.iter(), indent); + } + + buf.push(':'); + buf.push(' '); + fmt_expr(buf, &value.value, indent, apply_needs_parens); + } + LabelOnly(name) => { + if is_multiline { + newline(buf, indent); + } + + buf.push_str(name.value); + } + AssignedField::SpaceBefore(sub_expr, spaces) => { + fmt_comments_only(buf, spaces.iter(), indent); + fmt_field(buf, sub_expr, is_multiline, indent, apply_needs_parens); + } + AssignedField::SpaceAfter(sub_expr, spaces) => { + fmt_field(buf, sub_expr, is_multiline, indent, apply_needs_parens); + fmt_comments_only(buf, spaces.iter(), indent); + } + Malformed(string) => buf.push_str(string), + } +} + +pub fn is_multiline_expr<'a>(expr: &'a Expr<'a>) -> bool { + use parse::ast::Expr::*; + // TODO cache these answers using a Map, so + // we don't have to traverse subexpressions repeatedly + + match expr { + // Return whether these spaces contain any Newlines + SpaceBefore(_, spaces) | SpaceAfter(_, spaces) => { + spaces.iter().any(|space| space.contains_newline()) + } + + // These expressions never have newlines + Float(_) + | Int(_) + | HexInt(_) + | OctalInt(_) + | BinaryInt(_) + | Str(_) + | Field(_, _) + | QualifiedField(_, _) + | AccessorFunction(_) + | Var(_, _) + | MalformedIdent(_) + | MalformedClosure + | Variant(_, _) => false, + + // These expressions always have newlines + Defs(_, _) | Case(_, _) => true, + + List(elems) => elems + .iter() + .any(|loc_expr| is_multiline_expr(&loc_expr.value)), + + BlockStr(lines) => lines.len() > 1, + Apply(loc_expr, args, _) => { + is_multiline_expr(&loc_expr.value) + || args.iter().any(|loc_arg| is_multiline_expr(&loc_arg.value)) + } + + If((loc_cond, loc_if_true, loc_if_false)) => { + is_multiline_expr(&loc_cond.value) + || is_multiline_expr(&loc_if_true.value) + || is_multiline_expr(&loc_if_false.value) + } + + BinOp((loc_left, _, loc_right)) => { + is_multiline_expr(&loc_left.value) || is_multiline_expr(&loc_right.value) + } + + UnaryOp(loc_subexpr, _) | PrecedenceConflict(_, _, loc_subexpr) => { + is_multiline_expr(&loc_subexpr.value) + } + + ParensAround(subexpr) => is_multiline_expr(&subexpr), + + Closure(loc_patterns, loc_body) => { + // check the body first because it's more likely to be multiline + is_multiline_expr(&loc_body.value) + || loc_patterns + .iter() + .any(|loc_pattern| is_multiline_pattern(&loc_pattern.value)) + } + + Record(loc_fields) => loc_fields + .iter() + .any(|loc_field| is_multiline_field(&loc_field.value)), + } +} + +pub fn is_multiline_field<'a, Val>(field: &'a AssignedField<'a, Val>) -> bool { + use self::AssignedField::*; + + match field { + LabeledValue(_, spaces, _) => !spaces.is_empty(), + LabelOnly(_) => false, + AssignedField::SpaceBefore(_, _) | AssignedField::SpaceAfter(_, _) => true, + Malformed(text) => text.chars().any(|c| c == '\n'), + } +} + +pub fn is_multiline_pattern<'a>(_pattern: &'a Pattern<'a>) -> bool { + panic!("TODO return iff there are any newlines") +} diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs new file mode 100644 index 0000000000..42b247c34d --- /dev/null +++ b/src/fmt/mod.rs @@ -0,0 +1,5 @@ +pub mod def; +pub mod expr; +pub mod module; +pub mod pattern; +pub mod spaces; diff --git a/src/fmt/module.rs b/src/fmt/module.rs new file mode 100644 index 0000000000..9d58e023a6 --- /dev/null +++ b/src/fmt/module.rs @@ -0,0 +1,123 @@ +use bumpalo::collections::{String, Vec}; +use fmt::def::fmt_def; +use fmt::spaces::{fmt_spaces, INDENT}; +use parse::ast::{AppHeader, ExposesEntry, ImportsEntry, InterfaceHeader, Module}; +use region::Located; + +pub fn fmt_module<'a>(buf: &mut String<'a>, module: &'a Module<'a>) { + match module { + Module::Interface { header, defs } => { + fmt_interface_header(buf, header); + + for loc_def in defs { + fmt_def(buf, &loc_def.value, 0); + } + } + Module::App { header, defs } => { + fmt_app_header(buf, header); + for loc_def in defs { + fmt_def(buf, &loc_def.value, 0); + } + } + } +} + +pub fn fmt_interface_header<'a>(buf: &mut String<'a>, header: &'a InterfaceHeader<'a>) { + buf.push_str("interface"); + + // module name + if header.after_interface.is_empty() { + buf.push(' '); + } else { + fmt_spaces(buf, header.after_interface.iter(), INDENT); + } + + buf.push_str(header.name.value.as_str()); + + // exposes + if header.before_exposes.is_empty() { + buf.push(' '); + } else { + fmt_spaces(buf, header.before_exposes.iter(), INDENT); + } + + buf.push_str("exposes"); + + if header.after_exposes.is_empty() { + buf.push(' '); + } else { + fmt_spaces(buf, header.after_exposes.iter(), INDENT); + } + + fmt_exposes(buf, &header.exposes); + + // imports + if header.before_imports.is_empty() { + buf.push(' '); + } else { + fmt_spaces(buf, header.before_imports.iter(), INDENT); + } + + buf.push_str("imports"); + + if header.after_imports.is_empty() { + buf.push(' '); + } else { + fmt_spaces(buf, header.after_imports.iter(), INDENT); + } + + fmt_imports(buf, &header.imports); +} + +pub fn fmt_app_header<'a>(buf: &mut String<'a>, header: &'a AppHeader<'a>) { + buf.push_str("app"); + + // imports + fmt_spaces(buf, header.before_imports.iter(), INDENT); + fmt_imports(buf, &header.imports); + fmt_spaces(buf, header.after_imports.iter(), INDENT); +} + +fn fmt_imports<'a>(buf: &mut String<'a>, loc_entries: &'a Vec<'a, Located>>) { + buf.push('['); + + if !loc_entries.is_empty() { + buf.push(' '); + } + + for loc_entry in loc_entries { + fmt_imports_entry(buf, &loc_entry.value); + } + + if !loc_entries.is_empty() { + buf.push(' '); + } + + buf.push(']'); +} + +fn fmt_imports_entry<'a>(buf: &mut String<'a>, entry: &'a ImportsEntry<'a>) { + panic!("TODO fmt import entry"); +} + +fn fmt_exposes<'a>(buf: &mut String<'a>, loc_entries: &'a Vec<'a, Located>>) { + buf.push('['); + + if !loc_entries.is_empty() { + buf.push(' '); + } + + for loc_entry in loc_entries { + fmt_exposes_entry(buf, &loc_entry.value); + } + + if !loc_entries.is_empty() { + buf.push(' '); + } + + buf.push(']'); +} + +fn fmt_exposes_entry<'a>(buf: &mut String<'a>, entry: &'a ExposesEntry<'a>) { + panic!("TODO fmt import entry"); +} diff --git a/src/fmt/pattern.rs b/src/fmt/pattern.rs new file mode 100644 index 0000000000..4f5e202e35 --- /dev/null +++ b/src/fmt/pattern.rs @@ -0,0 +1,99 @@ +use fmt::spaces::fmt_spaces; + +use bumpalo::collections::String; +use parse::ast::Pattern; + +pub fn fmt_pattern<'a>( + buf: &mut String<'a>, + pattern: &'a Pattern<'a>, + indent: u16, + apply_needs_parens: bool, +) { + use self::Pattern::*; + + match pattern { + Identifier(string) => buf.push_str(string), + Variant(module_parts, name) => { + for part in module_parts.iter() { + buf.push_str(part); + buf.push('.'); + } + + buf.push_str(name); + } + Apply(loc_pattern, loc_arg_patterns) => { + if apply_needs_parens { + buf.push('('); + } + + fmt_pattern(buf, &loc_pattern.value, indent, true); + + for loc_arg in loc_arg_patterns.iter() { + buf.push(' '); + fmt_pattern(buf, &loc_arg.value, indent, true); + } + + if apply_needs_parens { + buf.push(')'); + } + } + RecordDestructure(loc_patterns) => { + buf.push_str("{ "); + + let mut is_first = true; + + for loc_pattern in loc_patterns { + if is_first { + is_first = false; + } else { + buf.push_str(", "); + } + + fmt_pattern(buf, &loc_pattern.value, indent, true); + } + + buf.push_str(" }"); + } + + RecordField(name, loc_pattern) => { + buf.push_str(name); + buf.push_str(": "); + fmt_pattern(buf, &loc_pattern.value, indent, true); + } + + IntLiteral(string) => buf.push_str(string), + HexIntLiteral(string) => buf.push_str(string), + OctalIntLiteral(string) => buf.push_str(string), + BinaryIntLiteral(string) => buf.push_str(string), + FloatLiteral(string) => buf.push_str(string), + StrLiteral(string) => buf.push_str(string), + BlockStrLiteral(lines) => { + for line in *lines { + buf.push_str(line) + } + } + EmptyRecordLiteral => buf.push_str("{}"), + Underscore => buf.push('_'), + + // Space + SpaceBefore(sub_pattern, spaces) => { + fmt_spaces(buf, spaces.iter(), indent); + fmt_pattern(buf, sub_pattern, indent, apply_needs_parens); + } + SpaceAfter(sub_pattern, spaces) => { + fmt_pattern(buf, sub_pattern, indent, apply_needs_parens); + fmt_spaces(buf, spaces.iter(), indent); + } + + // Malformed + Malformed(string) => buf.push_str(string), + QualifiedIdentifier(maybe_qualified) => { + for part in maybe_qualified.module_parts.iter() { + buf.push_str(part); + buf.push('.'); + } + + buf.push_str(maybe_qualified.value); + } + } +} diff --git a/src/fmt/spaces.rs b/src/fmt/spaces.rs new file mode 100644 index 0000000000..1c15914f0d --- /dev/null +++ b/src/fmt/spaces.rs @@ -0,0 +1,76 @@ +use bumpalo::collections::String; +use parse::ast::CommentOrNewline; + +/// The number of spaces to indent. +pub const INDENT: u16 = 4; + +pub fn newline<'a>(buf: &mut String<'a>, indent: u16) { + buf.push('\n'); + + add_spaces(buf, indent); +} + +pub fn add_spaces<'a>(buf: &mut String<'a>, spaces: u16) { + for _ in 0..spaces { + buf.push(' '); + } +} + +pub fn fmt_spaces<'a, I>(buf: &mut String<'a>, spaces: I, indent: u16) +where + I: Iterator>, +{ + use self::CommentOrNewline::*; + + let mut consecutive_newlines = 0; + let mut iter = spaces.peekable(); + + while let Some(space) = iter.next() { + match space { + Newline => { + // Only ever print two newlines back to back. + // (Two newlines renders as one blank line.) + if consecutive_newlines < 2 { + if iter.peek() == Some(&&Newline) { + buf.push('\n'); + } else { + newline(buf, indent); + } + + // Don't bother incrementing it if we're already over the limit. + // There's no upside, and it might eventually overflow, + consecutive_newlines += 1; + } + } + LineComment(comment) => { + buf.push('#'); + buf.push_str(comment); + + newline(buf, indent); + + // Reset to 1 because we just printed a \n + consecutive_newlines = 1; + } + } + } +} + +/// Like format_spaces, but remove newlines and keep only comments. +pub fn fmt_comments_only<'a, I>(buf: &mut String<'a>, spaces: I, indent: u16) +where + I: Iterator>, +{ + use self::CommentOrNewline::*; + + for space in spaces { + match space { + Newline => {} + LineComment(comment) => { + buf.push('#'); + buf.push_str(comment); + + newline(buf, indent); + } + } + } +} diff --git a/src/ident.rs b/src/ident.rs index 00534f0179..288b2b7a69 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -1,5 +1,34 @@ use std::fmt::{self, Display, Formatter}; +/// 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 +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct UnqualifiedIdent<'a>(&'a str); + +impl<'a> Into<&'a str> for UnqualifiedIdent<'a> { + fn into(self) -> &'a str { + self.0 + } +} + +impl<'a> UnqualifiedIdent<'a> { + pub fn new(name: &'a str) -> Self { + // Unqualified idents must always start with a lowercase character. + debug_assert!(name + .chars() + .next() + .expect("UnqualifiedIdent was empty") + .is_alphabetic()); + + UnqualifiedIdent(name) + } + + pub fn as_str(&'a self) -> &'a str { + self.0 + } +} + /// 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 diff --git a/src/lib.rs b/src/lib.rs index 11ea64e27f..458f48b8de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,15 +18,15 @@ pub mod string; pub mod constrain; pub mod ena; pub mod fmt; +pub mod gen; pub mod infer; +pub mod module; pub mod pretty_print_types; pub mod solve; pub mod subs; pub mod types; pub mod unify; -pub mod gen; - extern crate bumpalo; extern crate fraction; extern crate fxhash; diff --git a/src/module.rs b/src/module.rs new file mode 100644 index 0000000000..2eefded726 --- /dev/null +++ b/src/module.rs @@ -0,0 +1,94 @@ +use bumpalo::collections::Vec; +use ident::UnqualifiedIdent; +use parse::ast::{CommentOrNewline, Def}; +use region::Loc; +use std::path::PathBuf; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub struct ModuleName<'a>(&'a str); + +impl<'a> Into<&'a str> for ModuleName<'a> { + fn into(self) -> &'a str { + self.0 + } +} + +impl<'a> ModuleName<'a> { + pub fn new(name: &'a str) -> Self { + ModuleName(name) + } + + pub fn as_str(&'a self) -> &'a str { + self.0 + } + + pub fn add_to_path(&'a self, filename: &'a mut PathBuf) { + // Convert dots in module name to directories + for part in self.0.split(".") { + filename.push(part); + } + + // End with .roc + filename.push(".roc"); + } +} + +pub enum Exposing { + Ident, + TypeAndVariants, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Module<'a> { + Interface { + header: InterfaceHeader<'a>, + defs: Vec<'a, Def<'a>>, + }, + App { + header: AppHeader<'a>, + defs: Vec<'a, Def<'a>>, + }, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct InterfaceHeader<'a> { + pub name: Loc>, + pub exposes: Vec<'a, Loc>>, + pub imports: Vec<'a, (ModuleName<'a>, Vec<'a, Loc>>)>, + + // Potential comments and newlines - these will typically all be empty. + pub after_interface: &'a [CommentOrNewline<'a>], + pub before_exposes: &'a [CommentOrNewline<'a>], + pub after_exposes: &'a [CommentOrNewline<'a>], + pub before_imports: &'a [CommentOrNewline<'a>], + pub after_imports: &'a [CommentOrNewline<'a>], +} + +#[derive(Clone, Debug, PartialEq)] +pub struct AppHeader<'a> { + pub imports: Vec<'a, (ModuleName<'a>, Loc>)>, + + // Potential comments and newlines - these will typically all be empty. + pub before_imports: &'a [CommentOrNewline<'a>], + pub after_imports: &'a [CommentOrNewline<'a>], +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Exposes<'a> { + /// e.g. `Task` + Ident(UnqualifiedIdent<'a>), + + // Spaces + SpaceBefore(&'a Exposes<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Exposes<'a>, &'a [CommentOrNewline<'a>]), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Imports<'a> { + /// e.g. `Task` or `Task.{ Task, after }` + Ident(UnqualifiedIdent<'a>, Vec<'a, UnqualifiedIdent<'a>>), + + // Spaces + SpaceBefore(&'a Imports<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a Imports<'a>, &'a [CommentOrNewline<'a>]), +} diff --git a/src/parse/ast.rs b/src/parse/ast.rs index 96e117d925..f957d9d703 100644 --- a/src/parse/ast.rs +++ b/src/parse/ast.rs @@ -1,32 +1,30 @@ use bumpalo::collections::String; use bumpalo::collections::Vec; use bumpalo::Bump; -use fmt::{self, is_multiline_expr}; +use ident::UnqualifiedIdent; +use module::ModuleName; use operator::CalledVia; use operator::{BinOp, UnaryOp}; use parse::ident::Ident; use region::{Loc, Region}; -/// The number of spaces to indent. -const INDENT: u16 = 4; - #[derive(Clone, Debug, PartialEq)] pub enum Module<'a> { Interface { header: InterfaceHeader<'a>, - defs: Vec<'a, Def<'a>>, + defs: Vec<'a, Loc>>, }, App { header: AppHeader<'a>, - defs: Vec<'a, Def<'a>>, + defs: Vec<'a, Loc>>, }, } #[derive(Clone, Debug, PartialEq)] pub struct InterfaceHeader<'a> { - pub name: Loc<(&'a [&'a str], &'a str)>, - pub exposes: Vec<'a, Loc>>, - pub imports: Vec<'a, Loc>>, + pub name: Loc>, + pub exposes: Vec<'a, Loc>>, + pub imports: Vec<'a, Loc>>, // Potential comments and newlines - these will typically all be empty. pub after_interface: &'a [CommentOrNewline<'a>], @@ -38,21 +36,31 @@ pub struct InterfaceHeader<'a> { #[derive(Clone, Debug, PartialEq)] pub struct AppHeader<'a> { - pub imports: Vec<'a, Loc>>, + pub imports: Vec<'a, Loc>>, // Potential comments and newlines - these will typically all be empty. - pub after_app: &'a [CommentOrNewline<'a>], pub before_imports: &'a [CommentOrNewline<'a>], pub after_imports: &'a [CommentOrNewline<'a>], } #[derive(Clone, Debug, PartialEq)] -pub enum HeaderEntry<'a> { - Val(&'a str), - TypeOnly(&'a str), - TypeAndVariants(&'a str), - SpaceBefore(&'a HeaderEntry<'a>, &'a [CommentOrNewline<'a>]), - SpaceAfter(&'a HeaderEntry<'a>, &'a [CommentOrNewline<'a>]), +pub enum ExposesEntry<'a> { + /// e.g. `Task` + Ident(UnqualifiedIdent<'a>), + + // Spaces + SpaceBefore(&'a ExposesEntry<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a ExposesEntry<'a>, &'a [CommentOrNewline<'a>]), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ImportsEntry<'a> { + /// e.g. `Task` or `Task.{ Task, after }` + Module(ModuleName<'a>, Vec<'a, Loc>>), + + // Spaces + SpaceBefore(&'a ImportsEntry<'a>, &'a [CommentOrNewline<'a>]), + SpaceAfter(&'a ImportsEntry<'a>, &'a [CommentOrNewline<'a>]), } /// An optional qualifier (the `Foo.Bar` in `Foo.Bar.baz`). @@ -392,12 +400,21 @@ impl<'a> Spaceable<'a> for TypeAnnotation<'a> { } } -impl<'a> Spaceable<'a> for HeaderEntry<'a> { +impl<'a> Spaceable<'a> for ExposesEntry<'a> { fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - HeaderEntry::SpaceBefore(self, spaces) + ExposesEntry::SpaceBefore(self, spaces) } fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { - HeaderEntry::SpaceAfter(self, spaces) + ExposesEntry::SpaceAfter(self, spaces) + } +} + +impl<'a> Spaceable<'a> for ImportsEntry<'a> { + fn before(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImportsEntry::SpaceBefore(self, spaces) + } + fn after(&'a self, spaces: &'a [CommentOrNewline<'a>]) -> Self { + ImportsEntry::SpaceAfter(self, spaces) } } @@ -521,509 +538,3 @@ impl<'a> Expr<'a> { } } } - -pub fn format<'a>( - arena: &'a Bump, - expr: &'a Expr<'a>, - indent: u16, - apply_needs_parens: bool, -) -> String<'a> { - use self::Expr::*; - - let mut buf = String::new_in(arena); - - match expr { - SpaceBefore(sub_expr, spaces) => { - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - buf.push_str(&format(arena, sub_expr, indent, apply_needs_parens)); - } - SpaceAfter(sub_expr, spaces) => { - buf.push_str(&format(arena, sub_expr, indent, apply_needs_parens)); - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - } - ParensAround(sub_expr) => { - buf.push('('); - buf.push_str(&format(arena, sub_expr, indent, false)); - buf.push(')'); - } - Str(string) => { - buf.push('"'); - buf.push_str(string); - buf.push('"'); - } - Var(module_parts, name) => { - for part in module_parts.iter() { - buf.push_str(part); - buf.push('.'); - } - - buf.push_str(name); - } - Apply(loc_expr, loc_args, _) => { - if apply_needs_parens { - buf.push('('); - } - - buf.push_str(&format(arena, &loc_expr.value, indent, true)); - - for loc_arg in loc_args { - buf.push(' '); - - buf.push_str(&format(arena, &loc_arg.value, indent, true)); - } - - if apply_needs_parens { - buf.push(')'); - } - } - BlockStr(lines) => { - buf.push_str("\"\"\""); - for line in lines.iter() { - buf.push_str(line); - } - buf.push_str("\"\"\""); - } - Int(string) => buf.push_str(string), - Float(string) => buf.push_str(string), - HexInt(string) => { - buf.push('0'); - buf.push('x'); - buf.push_str(string); - } - BinaryInt(string) => { - buf.push('0'); - buf.push('b'); - buf.push_str(string); - } - OctalInt(string) => { - buf.push('0'); - buf.push('o'); - buf.push_str(string); - } - Record(loc_fields) => { - buf.push('{'); - - let is_multiline = loc_fields - .iter() - .any(|loc_field| fmt::is_multiline_field(&loc_field.value)); - - let mut iter = loc_fields.iter().peekable(); - let field_indent = if is_multiline { - indent + INDENT - } else { - if !loc_fields.is_empty() { - buf.push(' '); - } - - indent - }; - - while let Some(field) = iter.next() { - buf.push_str(&format_field( - arena, - &field.value, - is_multiline, - field_indent, - apply_needs_parens, - )); - - if iter.peek().is_some() { - buf.push(','); - - if !is_multiline { - buf.push(' '); - } - } - } - - if is_multiline { - buf.push('\n'); - } else if !loc_fields.is_empty() { - buf.push(' '); - } - - buf.push('}'); - } - Closure(loc_patterns, loc_ret) => { - buf.push('\\'); - - for loc_pattern in loc_patterns.iter() { - buf.push_str(&format_pattern(arena, &loc_pattern.value, indent, true)); - - buf.push(' '); - } - - let is_multiline = is_multiline_expr(&loc_ret.value); - - // If the body is multiline, go down a line and indent. - let indent = if is_multiline { - indent + INDENT - } else { - indent - }; - - buf.push_str("->"); - - let newline_is_next = match &loc_ret.value { - SpaceBefore(_, _) => true, - _ => false, - }; - - if !newline_is_next { - // Push a space after the "->" preceding this. - buf.push(' '); - } - - buf.push_str(&format(arena, &loc_ret.value, indent, false)); - } - Defs(defs, ret) => { - // The first def is actually at the end of the list, because - // it gets added there with .push() for efficiency. (The order of parsed defs doesn't - // matter because canonicalization sorts them anyway.) The other - // defs in the list are in their usual order. - let loc_first_def = defs.last().unwrap_or_else(|| { - panic!("Tried to format Defs which somehow had an empty list of defs!") - }); - let other_spaced_defs = &defs[0..defs.len() - 1]; - - buf.push_str(&format_def(arena, &loc_first_def.value, indent)); - - for loc_def in other_spaced_defs.iter() { - buf.push_str(&format_def(arena, &loc_def.value, indent)); - } - - buf.push_str(&format(arena, &ret.value, indent, false)); - } - If((loc_condition, loc_then, loc_else)) => { - buf.push_str("if "); - buf.push_str(&format(arena, &loc_condition.value, indent, false)); - buf.push_str(" then "); - buf.push_str(&format(arena, &loc_then.value, indent, false)); - buf.push_str(" else "); - buf.push_str(&format(arena, &loc_else.value, indent, false)); - } - Case(loc_condition, branches) => { - buf.push_str("case "); - buf.push_str(&format(arena, &loc_condition.value, indent, false)); - buf.push_str(" when\n"); - - let mut it = branches.iter().peekable(); - while let Some((pattern, expr)) = it.next() { - add_spaces(&mut buf, indent + INDENT); - - match pattern.value { - Pattern::SpaceBefore(nested, spaces) => { - buf.push_str(&format_comments_only(arena, spaces.iter(), indent + INDENT)); - buf.push_str(&format_pattern(arena, nested, indent + INDENT, false)); - } - _ => { - buf.push_str(&format_pattern( - arena, - &pattern.value, - indent + INDENT, - false, - )); - } - } - - buf.push_str(" ->\n"); - - add_spaces(&mut buf, indent + (INDENT * 2)); - match expr.value { - Expr::SpaceBefore(nested, spaces) => { - buf.push_str(&format_comments_only( - arena, - spaces.iter(), - indent + (INDENT * 2), - )); - buf.push_str(&format(arena, &nested, indent + (INDENT * 2), false)); - } - _ => { - buf.push_str(&format(arena, &expr.value, indent + (INDENT * 2), false)); - } - } - - if it.peek().is_some() { - buf.push('\n'); - buf.push('\n'); - } - } - } - other => panic!("TODO implement Display for AST variant {:?}", other), - } - - buf -} - -pub fn format_def<'a>(arena: &'a Bump, def: &'a Def<'a>, indent: u16) -> String<'a> { - let mut buf = String::new_in(arena); - - match def { - Def::Annotation(_, _) => panic!("TODO have format_def support Annotation"), - Def::Body(loc_pattern, loc_expr) => { - buf.push_str(&format_pattern(arena, &loc_pattern.value, indent, true)); - buf.push_str(" = "); - buf.push_str(&format(arena, &loc_expr.value, indent, false)); - } - Def::CustomType(_, _) => panic!("TODO have format_def support CustomType"), - Def::TypeAlias(_, _) => panic!("TODO have format_def support TypeAlias"), - Def::SpaceBefore(sub_def, spaces) => { - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - buf.push_str(&format_def(arena, sub_def, indent)); - } - Def::SpaceAfter(sub_def, spaces) => { - buf.push_str(&format_def(arena, sub_def, indent)); - - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - } - } - - buf -} - -fn format_pattern<'a>( - arena: &'a Bump, - pattern: &'a Pattern<'a>, - indent: u16, - apply_needs_parens: bool, -) -> String<'a> { - use self::Pattern::*; - - let mut buf = String::new_in(arena); - - match pattern { - Identifier(string) => buf.push_str(string), - Variant(module_parts, name) => { - for part in module_parts.iter() { - buf.push_str(part); - buf.push('.'); - } - - buf.push_str(name); - } - Apply(loc_pattern, loc_arg_patterns) => { - if apply_needs_parens { - buf.push('('); - } - - buf.push_str(&format_pattern(arena, &loc_pattern.value, indent, true)); - - for loc_arg in loc_arg_patterns.iter() { - buf.push(' '); - buf.push_str(&format_pattern(arena, &loc_arg.value, indent, true)); - } - - if apply_needs_parens { - buf.push(')'); - } - } - RecordDestructure(loc_patterns) => { - buf.push_str("{ "); - - let mut is_first = true; - - for loc_pattern in loc_patterns { - if is_first { - is_first = false; - } else { - buf.push_str(", "); - } - - buf.push_str(&format_pattern(arena, &loc_pattern.value, indent, true)); - } - - buf.push_str(" }"); - } - - RecordField(name, loc_pattern) => { - buf.push_str(name); - buf.push_str(": "); - buf.push_str(&format_pattern(arena, &loc_pattern.value, indent, true)); - } - - IntLiteral(string) => buf.push_str(string), - HexIntLiteral(string) => buf.push_str(string), - OctalIntLiteral(string) => buf.push_str(string), - BinaryIntLiteral(string) => buf.push_str(string), - FloatLiteral(string) => buf.push_str(string), - StrLiteral(string) => buf.push_str(string), - BlockStrLiteral(lines) => { - for line in *lines { - buf.push_str(line) - } - } - EmptyRecordLiteral => buf.push_str("{}"), - Underscore => buf.push('_'), - - // Space - SpaceBefore(sub_pattern, spaces) => { - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - buf.push_str(&format_pattern( - arena, - sub_pattern, - indent, - apply_needs_parens, - )); - } - SpaceAfter(sub_pattern, spaces) => { - buf.push_str(&format_pattern( - arena, - sub_pattern, - indent, - apply_needs_parens, - )); - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - } - - // Malformed - Malformed(string) => buf.push_str(string), - QualifiedIdentifier(maybe_qualified) => { - for part in maybe_qualified.module_parts.iter() { - buf.push_str(part); - buf.push('.'); - } - - buf.push_str(maybe_qualified.value); - } - } - - buf -} - -fn format_spaces<'a, I>(arena: &'a Bump, spaces: I, indent: u16) -> String<'a> -where - I: Iterator>, -{ - use self::CommentOrNewline::*; - - let mut buf = String::new_in(arena); - let mut consecutive_newlines = 0; - let mut iter = spaces.peekable(); - - while let Some(space) = iter.next() { - match space { - Newline => { - // Only ever print two newlines back to back. - // (Two newlines renders as one blank line.) - if consecutive_newlines < 2 { - if iter.peek() == Some(&&Newline) { - buf.push('\n'); - } else { - newline(&mut buf, indent); - } - - // Don't bother incrementing it if we're already over the limit. - // There's no upside, and it might eventually overflow, - consecutive_newlines += 1; - } - } - LineComment(comment) => { - buf.push('#'); - buf.push_str(comment); - - newline(&mut buf, indent); - - // Reset to 1 because we just printed a \n - consecutive_newlines = 1; - } - } - } - - buf -} - -/// Like format_spaces, but remove newlines and keep only comments. -fn format_comments_only<'a, I>(arena: &'a Bump, spaces: I, indent: u16) -> String<'a> -where - I: Iterator>, -{ - use self::CommentOrNewline::*; - - let mut buf = String::new_in(arena); - - for space in spaces { - match space { - Newline => {} - LineComment(comment) => { - buf.push('#'); - buf.push_str(comment); - - newline(&mut buf, indent); - } - } - } - - buf -} - -pub fn format_field<'a>( - arena: &'a Bump, - assigned_field: &'a AssignedField<'a, Expr<'a>>, - is_multiline: bool, - indent: u16, - apply_needs_parens: bool, -) -> String<'a> { - use self::AssignedField::*; - - let mut buf = String::new_in(arena); - - match assigned_field { - LabeledValue(name, spaces, value) => { - if is_multiline { - newline(&mut buf, indent); - } - - buf.push_str(name.value); - - if !spaces.is_empty() { - buf.push_str(&format_spaces(arena, spaces.iter(), indent)); - } - - buf.push(':'); - buf.push(' '); - buf.push_str(&format(arena, &value.value, indent, apply_needs_parens)); - } - LabelOnly(name) => { - if is_multiline { - newline(&mut buf, indent); - } - - buf.push_str(name.value); - } - AssignedField::SpaceBefore(sub_expr, spaces) => { - buf.push_str(&format_comments_only(arena, spaces.iter(), indent)); - buf.push_str(&format_field( - arena, - sub_expr, - is_multiline, - indent, - apply_needs_parens, - )); - } - AssignedField::SpaceAfter(sub_expr, spaces) => { - buf.push_str(&format_field( - arena, - sub_expr, - is_multiline, - indent, - apply_needs_parens, - )); - buf.push_str(&format_comments_only(arena, spaces.iter(), indent)); - } - Malformed(string) => buf.push_str(string), - } - - buf -} - -fn newline<'a>(buf: &mut String<'a>, indent: u16) { - buf.push('\n'); - - add_spaces(buf, indent); -} - -fn add_spaces<'a>(buf: &mut String<'a>, spaces: u16) { - for _ in 0..spaces { - buf.push(' '); - } -} diff --git a/src/parse/ident.rs b/src/parse/ident.rs index 2d73204e35..bf413182d6 100644 --- a/src/parse/ident.rs +++ b/src/parse/ident.rs @@ -2,6 +2,7 @@ use bumpalo::collections::string::String; use bumpalo::collections::vec::Vec; use bumpalo::Bump; use collections::arena_join; +use ident::UnqualifiedIdent; use parse::ast::{Attempting, MaybeQualified}; use parse::parser::{unexpected, unexpected_eof, ParseResult, Parser, State}; @@ -348,6 +349,13 @@ pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str> { variant_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()), + UnqualifiedIdent::new + ) +} + // TESTS // fn test_parse<'a>(input: &'a str) -> Result, Fail> { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 834a51aaf5..db42e615fa 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -16,10 +16,7 @@ use bumpalo::collections::Vec; use bumpalo::Bump; use operator::{BinOp, CalledVia, UnaryOp}; use parse; -use parse::ast::{ - AppHeader, AssignedField, Attempting, CommentOrNewline, Def, Expr, HeaderEntry, - InterfaceHeader, MaybeQualified, Module, Pattern, Spaceable, -}; +use parse::ast::{AssignedField, Attempting, Def, Expr, MaybeQualified, Pattern, Spaceable}; use parse::blankspace::{ space0, space0_after, space0_around, space0_before, space1, space1_after, space1_around, space1_before, @@ -35,107 +32,6 @@ use parse::parser::{ use parse::record::record; use region::{Located, Region}; -pub fn module<'a>() -> impl Parser<'a, Module<'a>> { - one_of2(interface_module(), app_module()) -} - -#[inline(always)] -fn interface_module<'a>() -> impl Parser<'a, Module<'a>> { - map!(and!(interface_header(), module_defs()), |(header, defs)| { - Module::Interface { header, defs } - }) -} - -#[inline(always)] -fn app_module<'a>() -> impl Parser<'a, Module<'a>> { - map!(and!(app_header(), module_defs()), |(header, defs)| { - Module::App { header, defs } - }) -} - -#[inline(always)] -fn interface_header<'a>() -> impl Parser<'a, InterfaceHeader<'a>> { - parser::map( - and!( - skip_first(string("interface"), and!(space1(1), loc!(ident()))), - and!(mod_header_list("exposes"), mod_header_list("imports")) - ), - |( - (after_interface, loc_name_ident), - ( - ((before_exposes, after_exposes), exposes), - ((before_imports, after_imports), imports), - ), - )| { - match loc_name_ident.value { - Ident::Variant(info) => { - let name = Located { - value: (info.module_parts, info.value), - region: loc_name_ident.region, - }; - - InterfaceHeader { - name, - exposes, - imports, - after_interface, - before_exposes, - after_exposes, - before_imports, - after_imports, - } - } - _ => panic!("TODO handle malformed module header"), - } - }, - ) -} - -#[inline(always)] -fn app_header<'a>() -> impl Parser<'a, AppHeader<'a>> { - move |_, _| { - panic!("TODO parse app header"); - } -} - -#[inline(always)] -fn module_defs<'a>() -> impl Parser<'a, Vec<'a, Def<'a>>> { - move |_, _| { - panic!("TODO parse defs"); - } -} - -/// Either "imports" or "exposes" - they both work the same way. -#[inline(always)] -fn mod_header_list<'a>( - kw: &'static str, -) -> impl Parser< - 'a, - ( - (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), - Vec<'a, Located>>, - ), -> { - and!( - and!(skip_second(space1(1), string(kw)), space1(1)), - collection(char('['), loc!(mod_header_entry()), char(','), char(']'), 1) - ) -} - -#[inline(always)] -fn mod_header_entry<'a>() -> impl Parser<'a, HeaderEntry<'a>> { - one_of2( - map!(unqualified_ident(), |ident| HeaderEntry::Val(ident)), - map!( - and!(unqualified_variant(), optional(string("..."))), - |(ident, opt_ellipsis)| match opt_ellipsis { - None => HeaderEntry::TypeOnly(ident), - Some(()) => HeaderEntry::TypeAndVariants(ident), - } - ), - ) -} - pub fn expr<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> { // Recursive parsers must not directly invoke functions which return (impl Parser), // as this causes rustc to stack overflow. Thus, parse_expr must be a diff --git a/src/parse/module.rs b/src/parse/module.rs index d0786782fe..b3c9b1c71a 100644 --- a/src/parse/module.rs +++ b/src/parse/module.rs @@ -1,16 +1,200 @@ -use ident::Ident; -use parse::ast::{Expr, Pattern}; +use bumpalo::collections::{String, Vec}; +use module::ModuleName; +use parse::ast::{ + AppHeader, Attempting, CommentOrNewline, Def, ExposesEntry, ImportsEntry, InterfaceHeader, + Module, +}; +use parse::blankspace::{space1, space1_around}; +use parse::collection::collection; +use parse::ident::unqualified_ident; +use parse::parse; +use parse::parser::{ + self, char, loc, one_of2, optional, skip_first, skip_second, string, unexpected, + unexpected_eof, Parser, State, +}; +use region::Located; -pub struct Module<'a> { - pub name: Ident, - pub exposes: Vec, - pub uses: Vec, - pub decls: Vec>, +pub fn module<'a>() -> impl Parser<'a, Module<'a>> { + one_of2(interface_module(), app_module()) } -#[derive(Clone, Debug, PartialEq)] -pub enum Decl<'a> { - Def(Pattern<'a>, Expr<'a>, Expr<'a>), - // TODO Alias - // TODO SumType +#[inline(always)] +pub fn interface_module<'a>() -> impl Parser<'a, Module<'a>> { + map!(and!(interface_header(), module_defs()), |(header, defs)| { + Module::Interface { header, defs } + }) +} + +#[inline(always)] +pub fn app_module<'a>() -> impl Parser<'a, Module<'a>> { + map!(and!(app_header(), module_defs()), |(header, defs)| { + Module::App { header, defs } + }) +} + +#[inline(always)] +pub fn interface_header<'a>() -> impl Parser<'a, InterfaceHeader<'a>> { + parser::map( + and!( + skip_first(string("interface"), and!(space1(1), loc!(module_name()))), + and!(exposes(), imports()) + ), + |( + (after_interface, name), + ( + ((before_exposes, after_exposes), exposes), + ((before_imports, after_imports), imports), + ), + )| { + InterfaceHeader { + name, + exposes, + imports, + after_interface, + before_exposes, + after_exposes, + before_imports, + after_imports, + } + }, + ) +} + +#[inline(always)] +pub fn module_name<'a>() -> impl Parser<'a, ModuleName<'a>> { + move |arena, state: State<'a>| { + let mut chars = state.input.chars(); + + let first_letter = match chars.next() { + Some(first_char) => { + // Module names must all be uppercase + if first_char.is_uppercase() { + first_char + } else { + return Err(unexpected( + first_char, + 0, + state, + Attempting::RecordFieldLabel, + )); + } + } + None => { + return Err(unexpected_eof(0, Attempting::Identifier, state)); + } + }; + + let mut buf = String::with_capacity_in(1, arena); + + buf.push(first_letter); + + while let Some(ch) = chars.next() { + // After the first character, only these are allowed: + // + // * Unicode alphabetic chars - you might include `鹏` if that's clear to your readers + // * ASCII digits - e.g. `1` but not `¾`, both of which pass .is_numeric() + // * A '.' separating module parts + if ch.is_alphabetic() || ch.is_ascii_digit() { + buf.push(ch); + } else if ch == '.' { + match chars.next() { + Some(next) => { + if next.is_uppercase() { + // If we hit another uppercase letter, keep going! + buf.push(next); + } else { + // We have finished parsing the module name. + // + // There may be an identifier after this '.', + // e.g. "baz" in `Foo.Bar.baz` + break; + } + } + None => { + // A module name can't end with a '.' + return Err(unexpected_eof(0, Attempting::Identifier, state)); + } + } + } else { + // This is the end of the module name. We're done! + break; + } + } + + let chars_parsed = buf.len(); + + Ok(( + ModuleName::new(buf.into_bump_str()), + state.advance_without_indenting(chars_parsed)?, + )) + } +} + +#[inline(always)] +fn app_header<'a>() -> impl Parser<'a, AppHeader<'a>> { + move |_, _| { + panic!("TODO parse app header"); + } +} + +#[inline(always)] +fn module_defs<'a>() -> impl Parser<'a, Vec<'a, Located>>> { + zero_or_more!(space1_around(loc(parse::def(0)), 0)) +} + +#[inline(always)] +fn exposes<'a>() -> impl Parser< + 'a, + ( + (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), + Vec<'a, Located>>, + ), +> { + and!( + and!(skip_second(space1(1), string("exposes")), space1(1)), + collection(char('['), loc!(exposes_entry()), char(','), char(']'), 1) + ) +} + +#[inline(always)] +fn imports<'a>() -> impl Parser< + 'a, + ( + (&'a [CommentOrNewline<'a>], &'a [CommentOrNewline<'a>]), + Vec<'a, Located>>, + ), +> { + and!( + and!(skip_second(space1(1), string("imports")), space1(1)), + collection(char('['), loc!(imports_entry()), char(','), char(']'), 1) + ) +} + +#[inline(always)] +fn exposes_entry<'a>() -> impl Parser<'a, ExposesEntry<'a>> { + map!(unqualified_ident(), ExposesEntry::Ident) +} + +#[inline(always)] +fn imports_entry<'a>() -> impl Parser<'a, ImportsEntry<'a>> { + map_with_arena!( + and!( + // e.g. `Task` + module_name(), + // e.g. `.{ Task, after}` + optional(skip_first( + char('.'), + collection(char('{'), loc!(exposes_entry()), char(','), char('}'), 1) + )) + ), + |arena, + (module_name, opt_values): ( + ModuleName<'a>, + Option>>> + )| { + let exposed_values = opt_values.unwrap_or_else(|| Vec::new_in(arena)); + + ImportsEntry::Module(module_name, exposed_values) + } + ) } diff --git a/tests/test_format.rs b/tests/test_format.rs index 70361ce362..8e127a5815 100644 --- a/tests/test_format.rs +++ b/tests/test_format.rs @@ -8,10 +8,14 @@ extern crate roc; #[cfg(test)] mod test_format { + use bumpalo::collections::String; use bumpalo::Bump; + use roc::fmt::expr::fmt_expr; + use roc::fmt::module::fmt_module; use roc::parse; - use roc::parse::ast::{format, Attempting, Expr}; + use roc::parse::ast::{Attempting, Expr}; use roc::parse::blankspace::space0_before; + use roc::parse::module::module; use roc::parse::parser::{Fail, Parser, State}; fn parse_with<'a>(arena: &'a Bump, input: &'a str) -> Result, Fail> { @@ -24,26 +28,53 @@ mod test_format { .map_err(|(fail, _)| fail) } - fn assert_formats_to(input: &str, expected: &str) { + fn expr_formats_to(input: &str, expected: &str) { let arena = Bump::new(); let input = input.trim_end(); let expected = expected.trim_end(); match parse_with(&arena, input) { - Ok(actual) => assert_eq!(format(&arena, &actual, 0, false), expected), + Ok(actual) => { + let mut buf = String::new_in(&arena); + + fmt_expr(&mut buf, &actual, 0, false); + + assert_eq!(buf, expected) + }, Err(error) => panic!("Unexpected parse failure when parsing this for formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", input, error) } } - fn assert_formats_same(input: &str) { - assert_formats_to(input, input); + fn expr_formats_same(input: &str) { + expr_formats_to(input, input); + } + + fn module_formats_to(src: &str, expected: &str) { + let arena = Bump::new(); + let src = src.trim_end(); + let expected = expected.trim_end(); + + match module().parse(&arena, State::new(&src, Attempting::Module)) { + Ok((actual, _)) => { + let mut buf = String::new_in(&arena); + + fmt_module(&mut buf, &actual); + + assert_eq!(buf, expected) + }, + Err(error) => panic!("Unexpected parse failure when parsing this for formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, error) + }; + } + + fn module_formats_same(input: &str) { + module_formats_to(input, input); } // STRING LITERALS #[test] fn empty_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" "" "# @@ -52,7 +83,7 @@ mod test_format { #[test] fn basic_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" "blah" "# @@ -61,7 +92,7 @@ mod test_format { #[test] fn escaped_unicode_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" "unicode: \u{A00A}!" "# @@ -70,7 +101,7 @@ mod test_format { #[test] fn escaped_quote_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" "\"" "# @@ -79,7 +110,7 @@ mod test_format { #[test] fn empty_block_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" """""" "# @@ -88,7 +119,7 @@ mod test_format { #[test] fn basic_block_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" """blah""" "# @@ -97,7 +128,7 @@ mod test_format { #[test] fn newlines_block_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" """blah spam @@ -108,7 +139,7 @@ mod test_format { #[test] fn quotes_block_string() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" """ @@ -121,7 +152,7 @@ mod test_format { #[test] fn zero() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" 0 "# @@ -130,7 +161,7 @@ mod test_format { #[test] fn zero_point_zero() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" 0.0 "# @@ -139,7 +170,7 @@ mod test_format { #[test] fn int_with_underscores() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" 1_23_456 "# @@ -148,7 +179,7 @@ mod test_format { #[test] fn float_with_underscores() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" 1_23_456.7_89_10 "# @@ -157,7 +188,7 @@ mod test_format { #[test] fn multi_arg_closure() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" \a b c -> a b c "# @@ -168,7 +199,7 @@ mod test_format { #[test] fn single_def() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" x = 5 @@ -179,7 +210,7 @@ mod test_format { #[test] fn two_defs() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" x = 5 y = 10 @@ -191,7 +222,7 @@ mod test_format { #[test] fn parenthetical_def() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" (UserId userId) = 5 y = 10 @@ -203,7 +234,7 @@ mod test_format { #[test] fn record_destructuring() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" { x, y } = 5 { x: 5 } = { x: 5 } @@ -215,7 +246,7 @@ mod test_format { // #[test] // fn record_field_destructuring() { - // assert_formats_same(indoc!( + // expr_formats_same(indoc!( // r#" // case foo of // { x: 5 } -> 42 @@ -225,7 +256,7 @@ mod test_format { #[test] fn def_closure() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" identity = \a -> a @@ -238,22 +269,22 @@ mod test_format { #[test] fn empty_record() { - assert_formats_same("{}"); + expr_formats_same("{}"); } #[test] fn one_field() { - assert_formats_same("{ x: 4 }"); + expr_formats_same("{ x: 4 }"); } #[test] fn two_fields() { - assert_formats_same("{ x: 4, y: 42 }"); + expr_formats_same("{ x: 4, y: 42 }"); } #[test] fn two_fields_newline() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" { x: 4, @@ -265,7 +296,7 @@ mod test_format { #[test] fn two_fields_center_newline() { - assert_formats_to( + expr_formats_to( indoc!( r#" { x: 4, @@ -286,7 +317,7 @@ mod test_format { #[test] fn one_unnamed_field() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" foo = 4 @@ -299,13 +330,13 @@ mod test_format { #[test] fn single_line_if() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" if foo bar then a b c else d e f "# )); - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" if foo (a b c) then a b c else d e f "# @@ -316,7 +347,7 @@ mod test_format { #[test] fn integer_case() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" case b when 1 -> @@ -330,7 +361,7 @@ mod test_format { #[test] fn case_with_comments() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" case b when # look at cases @@ -351,7 +382,7 @@ mod test_format { #[test] fn nested_case() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" case b when _ -> @@ -364,7 +395,7 @@ mod test_format { #[test] fn case_with_moving_comments() { - assert_formats_to( + expr_formats_to( indoc!( r#" case b when @@ -395,7 +426,7 @@ mod test_format { #[test] fn multiple_blank_lines_collapse_to_one() { - assert_formats_to( + expr_formats_to( indoc!( r#" x = 5 @@ -423,7 +454,7 @@ mod test_format { #[test] fn def_returning_closure() { - assert_formats_same(indoc!( + expr_formats_same(indoc!( r#" f = \x -> x g = \x -> x @@ -436,4 +467,26 @@ mod test_format { "# )); } + + // MODULES + + #[test] + fn empty_interface() { + module_formats_same(indoc!( + r#" + interface Foo exposes [] imports [] + "# + )); + } + + #[test] + fn interface_exposing() { + module_formats_same(indoc!( + r#" + interface Foo + exposes [] + imports [] + "# + )); + } } diff --git a/tests/test_parse.rs b/tests/test_parse.rs index bbd37ed258..8a3ad117d9 100644 --- a/tests/test_parse.rs +++ b/tests/test_parse.rs @@ -17,14 +17,16 @@ mod test_parse { use bumpalo::collections::vec::Vec; use bumpalo::{self, Bump}; use helpers::parse_with; + use roc::module::ModuleName; use roc::operator::BinOp::*; use roc::operator::CalledVia; use roc::operator::UnaryOp; use roc::parse::ast::CommentOrNewline::*; use roc::parse::ast::Expr::{self, *}; use roc::parse::ast::Pattern::{self, *}; - use roc::parse::ast::{Attempting, Def, Spaceable}; - use roc::parse::parser::{Fail, FailReason}; + use roc::parse::ast::{Attempting, Def, InterfaceHeader, Spaceable}; + use roc::parse::module::interface_header; + use roc::parse::parser::{Fail, FailReason, Parser, State}; use roc::region::{Located, Region}; use std::{f64, i64}; @@ -1105,6 +1107,37 @@ mod test_parse { assert_eq!(Ok(expected), actual); } + // MODULE + + #[test] + fn empty_module() { + let arena = Bump::new(); + let exposes = Vec::new_in(&arena); + let imports = Vec::new_in(&arena); + let module_name = ModuleName::new("Foo"); + let expected = InterfaceHeader { + name: Located::new(0, 0, 10, 13, module_name), + exposes, + imports, + + after_interface: &[], + before_exposes: &[], + after_exposes: &[], + before_imports: &[], + after_imports: &[], + }; + let src = indoc!( + r#" + interface Foo exposes [] imports [] + "# + ); + let actual = interface_header() + .parse(&arena, State::new(&src, Attempting::Module)) + .map(|tuple| tuple.0); + + assert_eq!(Ok(expected), actual); + } + // TODO test hex/oct/binary parsing // // TODO test for \t \r and \n in string literals *outside* unicode escape sequence!