Merge remote-tracking branch 'origin/main' into upgrade-llvm-zig

This commit is contained in:
Brendan Hansknecht 2024-12-11 16:38:34 -08:00
commit 0a573ca557
No known key found for this signature in database
GPG key ID: 0EA784685083E75B
818 changed files with 15185 additions and 4951 deletions

View file

@ -29,6 +29,11 @@ jobs:
- name: roc format check on builtins
run: cargo run --locked --release format --check crates/compiler/builtins/roc
- name: run fuzz tests
run: |
cargo +nightly-2024-02-03 install --locked cargo-fuzz
cd crates/compiler/test_syntax/fuzz && cargo +nightly-2024-02-03 fuzz run -j4 fuzz_expr --sanitizer=none -- -dict=dict.txt -max_total_time=60
- name: ensure there are no unused dependencies
run: cargo +nightly-2024-02-03 udeps --all-targets

View file

@ -0,0 +1,21 @@
name: Notify zulip high priority issues
on:
issues:
types: [labeled]
jobs:
notify:
runs-on: ubuntu-22.04
if: github.event.label.name == 'P-high'
steps:
- name: Send a stream message
uses: zulip/github-actions-zulip/send-message@v1
with:
api-key: ${{ secrets.ZULIP_API_KEY_ISSUE_BOT }}
email: "high-priority-issues-bot@roc.zulipchat.com"
organization-url: "https://roc.zulipchat.com"
to: "contributing"
type: "stream"
topic: "High Priority Issues Bot"
content: '[${{ github.event.issue.title }}](${{ github.event.issue.html_url }})'

16
Cargo.lock generated
View file

@ -85,15 +85,16 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.4"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
@ -1363,6 +1364,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.9.0"
@ -2580,6 +2587,7 @@ dependencies = [
"roc_module",
"roc_parse",
"roc_region",
"soa",
]
[[package]]
@ -2704,6 +2712,7 @@ dependencies = [
"parking_lot",
"roc_can",
"roc_collections",
"roc_error_macros",
"roc_fmt",
"roc_load",
"roc_module",
@ -3982,13 +3991,16 @@ dependencies = [
"bumpalo",
"indoc",
"pretty_assertions",
"roc_can",
"roc_collections",
"roc_error_macros",
"roc_fmt",
"roc_module",
"roc_parse",
"roc_region",
"roc_test_utils",
"roc_test_utils_dir",
"roc_types",
"walkdir",
]

View file

@ -7,6 +7,7 @@ use roc_error_macros::{internal_error, user_error};
use roc_fmt::def::fmt_defs;
use roc_fmt::header::fmt_header;
use roc_fmt::Buf;
use roc_fmt::MigrationFlags;
use roc_parse::ast::{FullAst, SpacesBefore};
use roc_parse::header::parse_module_defs;
use roc_parse::normalize::Normalize;
@ -63,14 +64,18 @@ fn is_roc_file(path: &Path) -> bool {
matches!(path.extension().and_then(OsStr::to_str), Some("roc"))
}
pub fn format_files(files: std::vec::Vec<PathBuf>, mode: FormatMode) -> Result<(), String> {
pub fn format_files(
files: std::vec::Vec<PathBuf>,
mode: FormatMode,
flags: MigrationFlags,
) -> Result<(), String> {
let arena = Bump::new();
let mut files_to_reformat = Vec::new(); // to track which files failed `roc format --check`
for file in flatten_directories(files) {
let src = std::fs::read_to_string(&file).unwrap();
match format_src(&arena, &src) {
match format_src(&arena, &src, flags) {
Ok(buf) => {
match mode {
FormatMode::CheckOnly => {
@ -183,11 +188,11 @@ pub enum FormatProblem {
},
}
pub fn format_src(arena: &Bump, src: &str) -> Result<String, FormatProblem> {
pub fn format_src(arena: &Bump, src: &str, flags: MigrationFlags) -> Result<String, FormatProblem> {
let ast = arena.alloc(parse_all(arena, src).unwrap_or_else(|e| {
user_error!("Unexpected parse failure when parsing this formatting:\n\n{:?}\n\nParse error was:\n\n{:?}\n\n", src, e)
}));
let mut buf = Buf::new_in(arena);
let mut buf = Buf::new_in(arena, flags);
fmt_all(&mut buf, ast);
let reparsed_ast = match arena.alloc(parse_all(arena, buf.as_str())) {
@ -208,7 +213,9 @@ pub fn format_src(arena: &Bump, src: &str) -> Result<String, FormatProblem> {
// the PartialEq implementation is returning `false` even when the Debug-formatted impl is exactly the same.
// I don't have the patience to debug this right now, so let's leave it for another day...
// TODO: fix PartialEq impl on ast types
if format!("{ast_normalized:?}") != format!("{reparsed_ast_normalized:?}") {
if !flags.at_least_one_active()
&& format!("{ast_normalized:?}") != format!("{reparsed_ast_normalized:?}")
{
return Err(FormatProblem::ReformattingChangedAst {
formatted_src: buf.as_str().to_string(),
ast_before: format!("{ast_normalized:#?}\n"),
@ -217,7 +224,7 @@ pub fn format_src(arena: &Bump, src: &str) -> Result<String, FormatProblem> {
}
// Now verify that the resultant formatting is _stable_ - i.e. that it doesn't change again if re-formatted
let mut reformatted_buf = Buf::new_in(arena);
let mut reformatted_buf = Buf::new_in(arena, flags);
fmt_all(&mut reformatted_buf, reparsed_ast);
@ -298,8 +305,9 @@ main =
fn test_single_file_needs_reformatting() {
let dir = tempdir().unwrap();
let file_path = setup_test_file(dir.path(), "test1.roc", UNFORMATTED_ROC);
let flags = MigrationFlags::new(false);
let result = format_files(vec![file_path.clone()], FormatMode::CheckOnly);
let result = format_files(vec![file_path.clone()], FormatMode::CheckOnly, flags);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
@ -317,8 +325,9 @@ main =
let dir = tempdir().unwrap();
let file1 = setup_test_file(dir.path(), "test1.roc", UNFORMATTED_ROC);
let file2 = setup_test_file(dir.path(), "test2.roc", UNFORMATTED_ROC);
let flags = MigrationFlags::new(false);
let result = format_files(vec![file1, file2], FormatMode::CheckOnly);
let result = format_files(vec![file1, file2], FormatMode::CheckOnly, flags);
assert!(result.is_err());
let error_message = result.unwrap_err();
assert!(error_message.contains("test1.roc") && error_message.contains("test2.roc"));
@ -330,8 +339,9 @@ main =
fn test_no_files_need_reformatting() {
let dir = tempdir().unwrap();
let file_path = setup_test_file(dir.path(), "formatted.roc", FORMATTED_ROC);
let flags = MigrationFlags::new(false);
let result = format_files(vec![file_path], FormatMode::CheckOnly);
let result = format_files(vec![file_path], FormatMode::CheckOnly, flags);
assert!(result.is_ok());
cleanup_temp_dir(dir);
@ -343,10 +353,12 @@ main =
let file_formatted = setup_test_file(dir.path(), "formatted.roc", FORMATTED_ROC);
let file1_unformated = setup_test_file(dir.path(), "test1.roc", UNFORMATTED_ROC);
let file2_unformated = setup_test_file(dir.path(), "test2.roc", UNFORMATTED_ROC);
let flags = MigrationFlags::new(false);
let result = format_files(
vec![file_formatted, file1_unformated, file2_unformated],
FormatMode::CheckOnly,
flags,
);
assert!(result.is_err());
let error_message = result.unwrap_err();

View file

@ -81,7 +81,6 @@ pub const FLAG_OUTPUT: &str = "output";
pub const FLAG_FUZZ: &str = "fuzz";
pub const FLAG_MAIN: &str = "main";
pub const ROC_FILE: &str = "ROC_FILE";
pub const ROC_DIR: &str = "ROC_DIR";
pub const GLUE_DIR: &str = "GLUE_DIR";
pub const GLUE_SPEC: &str = "GLUE_SPEC";
pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES";
@ -89,6 +88,7 @@ pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP";
pub const FLAG_PP_HOST: &str = "host";
pub const FLAG_PP_PLATFORM: &str = "platform";
pub const FLAG_PP_DYLIB: &str = "lib";
pub const FLAG_MIGRATE: &str = "migrate";
pub const VERSION: &str = env!("ROC_VERSION");
const DEFAULT_GENERATED_DOCS_DIR: &str = "generated-docs";
@ -344,6 +344,13 @@ pub fn build_app() -> Command {
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(FLAG_MIGRATE)
.long(FLAG_MIGRATE)
.help("Will change syntax to match the latest preferred style. This can cause changes to variable names and more.")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(FLAG_STDIN)
.long(FLAG_STDIN)
@ -864,24 +871,26 @@ pub fn build(
// so we don't want to spend time freeing these values
let arena = ManuallyDrop::new(Bump::new());
let opt_level = if let BuildConfig::BuildAndRunIfNoErrors = config {
OptLevel::Development
} else {
opt_level_from_flags(matches)
};
let opt_level = opt_level_from_flags(matches);
// Note: This allows using `--dev` with `--optimize`.
// This means frontend optimizations and dev backend.
let code_gen_backend = if matches.get_flag(FLAG_DEV) {
let should_run_expects = matches!(opt_level, OptLevel::Development | OptLevel::Normal) &&
// TODO: once expect is decoupled from roc launching the executable, remove this part of the conditional.
matches!(
config,
BuildConfig::BuildAndRun | BuildConfig::BuildAndRunIfNoErrors
);
let code_gen_backend = if matches!(opt_level, OptLevel::Development) {
if matches!(target.architecture(), Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
CodeGenBackend::Assembly(AssemblyBackendMode::Binary)
}
} else {
let backend_mode = match opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
let backend_mode = if should_run_expects {
LlvmBackendMode::BinaryWithExpect
} else {
LlvmBackendMode::Binary
};
CodeGenBackend::Llvm(backend_mode)
@ -1019,7 +1028,7 @@ pub fn build(
roc_run(
&arena,
path,
opt_level,
should_run_expects,
target,
args,
bytes,
@ -1062,7 +1071,7 @@ pub fn build(
roc_run(
&arena,
path,
opt_level,
should_run_expects,
target,
args,
bytes,
@ -1081,7 +1090,7 @@ pub fn build(
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump,
script_path: &Path,
opt_level: OptLevel,
should_run_expects: bool,
target: Target,
args: I,
binary_bytes: &[u8],
@ -1123,7 +1132,7 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
_ => roc_run_native(
arena,
script_path,
opt_level,
should_run_expects,
args,
binary_bytes,
expect_metadata,
@ -1191,7 +1200,7 @@ fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump,
script_path: &Path,
opt_level: OptLevel,
should_run_expects: bool,
args: I,
binary_bytes: &[u8],
expect_metadata: ExpectMetadata,
@ -1213,11 +1222,10 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
.chain([std::ptr::null()])
.collect_in(arena);
match opt_level {
OptLevel::Development => roc_dev_native(arena, executable, argv, envp, expect_metadata),
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => unsafe {
roc_run_native_fast(executable, &argv, &envp);
},
if should_run_expects {
roc_dev_native(arena, executable, argv, envp, expect_metadata);
} else {
unsafe { roc_run_native_fast(executable, &argv, &envp) };
}
Ok(1)
@ -1455,7 +1463,7 @@ fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result<Executab
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
script_path: &Path,
opt_level: OptLevel,
should_run_expects: bool,
args: I,
binary_bytes: &[u8],
_expect_metadata: ExpectMetadata,
@ -1480,16 +1488,13 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
.chain([std::ptr::null()])
.collect_in(arena);
match opt_level {
OptLevel::Development => {
if should_run_expects {
// roc_run_native_debug(executable, &argv, &envp, expectations, interns)
internal_error!("running `expect`s does not currently work on windows")
}
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => {
internal_error!("running `expect`s does not currently work on windows");
} else {
roc_run_native_fast(executable, &argv, &envp);
}
}
}
Ok(1)
}

View file

@ -5,12 +5,14 @@ use roc_build::program::{check_file, CodeGenBackend};
use roc_cli::{
build_app, format_files, format_src, test, BuildConfig, FormatMode, CMD_BUILD, CMD_CHECK,
CMD_DEV, CMD_DOCS, CMD_FORMAT, CMD_GLUE, CMD_PREPROCESS_HOST, CMD_REPL, CMD_RUN, CMD_TEST,
CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_DEV, FLAG_LIB, FLAG_MAIN, FLAG_NO_COLOR,
FLAG_NO_HEADER, FLAG_NO_LINK, FLAG_OUTPUT, FLAG_PP_DYLIB, FLAG_PP_HOST, FLAG_PP_PLATFORM,
FLAG_STDIN, FLAG_STDOUT, FLAG_TARGET, FLAG_TIME, GLUE_DIR, GLUE_SPEC, ROC_FILE, VERSION,
CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_DEV, FLAG_LIB, FLAG_MAIN, FLAG_MIGRATE,
FLAG_NO_COLOR, FLAG_NO_HEADER, FLAG_NO_LINK, FLAG_OUTPUT, FLAG_PP_DYLIB, FLAG_PP_HOST,
FLAG_PP_PLATFORM, FLAG_STDIN, FLAG_STDOUT, FLAG_TARGET, FLAG_TIME, GLUE_DIR, GLUE_SPEC,
ROC_FILE, VERSION,
};
use roc_docs::generate_docs_html;
use roc_error_macros::user_error;
use roc_fmt::MigrationFlags;
use roc_gen_dev::AssemblyBackendMode;
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{LoadingProblem, Threading};
@ -247,6 +249,8 @@ fn main() -> io::Result<()> {
) {
Ok((problems, total_time)) => {
problems.print_error_warning_count(total_time);
println!(".\n");
exit_code = problems.exit_code();
}
@ -310,6 +314,7 @@ fn main() -> io::Result<()> {
Some((CMD_FORMAT, matches)) => {
let from_stdin = matches.get_flag(FLAG_STDIN);
let to_stdout = matches.get_flag(FLAG_STDOUT);
let migrate = matches.get_flag(FLAG_MIGRATE);
let format_mode = if to_stdout {
FormatMode::WriteToStdout
} else {
@ -318,6 +323,7 @@ fn main() -> io::Result<()> {
false => FormatMode::WriteToFile,
}
};
let flags = MigrationFlags::new(migrate);
if from_stdin && matches!(format_mode, FormatMode::WriteToFile) {
eprintln!("When using the --stdin flag, either the --check or the --stdout flag must also be specified. (Otherwise, it's unclear what filename to write to!)");
@ -370,7 +376,7 @@ fn main() -> io::Result<()> {
std::process::exit(1);
});
match format_src(&arena, src) {
match format_src(&arena, src, flags) {
Ok(formatted_src) => {
match format_mode {
FormatMode::CheckOnly => {
@ -402,7 +408,7 @@ fn main() -> io::Result<()> {
}
}
} else {
match format_files(roc_files, format_mode) {
match format_files(roc_files, format_mode, flags) {
Ok(()) => 0,
Err(message) => {
eprintln!("{message}");

View file

@ -346,7 +346,7 @@ mod cli_tests {
);
let expected_out =
"0 error and 0 warning found in <ignored for test> ms\n0 error and 0 warning found in <ignored for test> ms\n";
"0 errors and 0 warnings found in <ignored for test> ms.\n\n0 errors and 0 warnings found in <ignored for test> ms.\n\n";
cli_build.run().assert_clean_stdout(expected_out);
}
@ -1454,6 +1454,20 @@ mod cli_tests {
.run()
.assert_clean_success();
}
#[test]
fn module_params_effectful_param() {
let cli_check = ExecCli::new(
CMD_CHECK,
file_from_root(
"crates/cli/tests/test-projects/module_params",
"effect_module.roc",
),
);
let cli_check_out = cli_check.run();
cli_check_out.assert_clean_success();
}
}
#[cfg(feature = "wasm32-cli-run")]

View file

@ -1,7 +1,7 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 429
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TOO MANY ARGS in tests/test-projects/module_params/arity_mismatch.roc ───────
@ -36,8 +36,6 @@ make partial application explicit.
────────────────────────────────────────────────────────────────────────────────
3 error and 0 warning found in <ignored for test> ms
.
3 errors and 0 warnings found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -1,7 +1,7 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 445
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TYPE MISMATCH in tests/test-projects/module_params/BadAnn.roc ───────────────
@ -41,8 +41,6 @@ Tip: It looks like it takes too many arguments. I'm seeing 1 extra.
────────────────────────────────────────────────────────────────────────────────
2 error and 1 warning found in <ignored for test> ms
.
2 errors and 1 warning found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -1,15 +1,16 @@
---
source: crates/cli/tests/cli_tests.rs
assertion_line: 476
expression: cli_dev_out.normalize_stdout_and_stderr()
snapshot_kind: text
---
── TYPE MISMATCH in tests/test-projects/module_params/unexpected_fn.roc ────────
This argument to this string interpolation has an unexpected type:
11│ $(Api.getPost)
^^^^^^^^^^^
10│ """
11│> $(Api.getPost)
12│ """
The argument is an anonymous function of type:
@ -21,8 +22,6 @@ But this string interpolation needs its argument to be:
────────────────────────────────────────────────────────────────────────────────
1 error and 0 warning found in <ignored for test> ms
.
1 error and 0 warnings found in <ignored for test> ms.
You can run <ignored for tests>

View file

@ -29,7 +29,6 @@ extern "C" {
#[link_name = "roc__mainForHost_0_caller"]
fn call_Fx(flags: *const u8, closure_data: *const u8, output: *mut u8);
#[allow(dead_code)]
#[link_name = "roc__mainForHost_0_size"]
fn size_Fx() -> i64;

View file

@ -0,0 +1,3 @@
module { stdout! } -> [log!]
log! = \msg, level -> stdout! "$(level):$(msg)"

View file

@ -4,7 +4,7 @@ extern crate roc_load;
extern crate roc_module;
extern crate tempfile;
use roc_command_utils::{cargo, root_dir};
use roc_command_utils::root_dir;
use std::env;
use std::path::PathBuf;
@ -36,60 +36,6 @@ pub fn path_to_binary(binary_name: &str) -> PathBuf {
path
}
// If we don't already have a /target/release/roc, build it!
pub fn build_roc_bin_cached() -> PathBuf {
let roc_binary_path = path_to_roc_binary();
if !roc_binary_path.exists() {
build_roc_bin(&[]);
}
roc_binary_path
}
pub fn build_roc_bin(extra_args: &[&str]) -> PathBuf {
let roc_binary_path = path_to_roc_binary();
// Remove the /target/release/roc part
let root_project_dir = roc_binary_path
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap();
// cargo build --bin roc
// (with --release iff the test is being built with --release)
let mut args = if cfg!(debug_assertions) {
vec!["build", "--bin", "roc"]
} else {
vec!["build", "--release", "--bin", "roc"]
};
args.extend(extra_args);
let mut cargo_cmd = cargo();
cargo_cmd.current_dir(root_project_dir).args(&args);
let cargo_cmd_str = format!("{cargo_cmd:?}");
let cargo_output = cargo_cmd.output().unwrap();
if !cargo_output.status.success() {
panic!(
"The following cargo command failed:\n\n {}\n\n stdout was:\n\n {}\n\n stderr was:\n\n {}\n",
cargo_cmd_str,
String::from_utf8(cargo_output.stdout).unwrap(),
String::from_utf8(cargo_output.stderr).unwrap()
);
}
roc_binary_path
}
#[allow(dead_code)]
pub fn dir_from_root(dir_name: &str) -> PathBuf {
let mut path = root_dir();

View file

@ -1203,12 +1203,6 @@ fn recursive_variant_types<'a>(
Ok(result)
}
#[allow(dead_code)]
fn worst_case_type(context: &mut impl TypeContext) -> Result<TypeId> {
let cell = context.add_heap_cell_type();
context.add_bag_type(cell)
}
fn expr_spec<'a>(
builder: &mut FuncDefBuilder,
interner: &STLayoutInterner<'a>,

View file

@ -25,22 +25,6 @@ pub fn target_triple_str(target: Target) -> &'static str {
}
}
pub fn target_zig_str(target: Target) -> &'static str {
// Zig has its own architecture mappings, defined here:
// https://github.com/ziglang/zig/blob/master/tools/process_headers.zig
//
// and an open proposal to unify them with the more typical "target triples":
// https://github.com/ziglang/zig/issues/4911
match target {
Target::LinuxArm64 => "aarch64-linux-gnu",
Target::LinuxX32 => "i386-linux-gnu",
Target::LinuxX64 => "x86_64-linux-gnu",
Target::MacArm64 => "aarch64-macos-none",
Target::MacX64 => "x86_64-macos-none",
_ => internal_error!("TODO gracefully handle unsupported target: {:?}", target),
}
}
pub fn init_arch(target: Target) {
match target.architecture() {
Architecture::X86_64 | Architecture::X86_32

View file

@ -3,8 +3,8 @@ use roc_error_macros::internal_error;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;
use std::str;
use std::{env, path::PathBuf, process::Command};
#[cfg(target_os = "macos")]
use tempfile::tempdir;
@ -82,17 +82,6 @@ fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) {
run_command(zig_cmd, 0);
}
pub fn get_lib_dir() -> PathBuf {
// Currently we have the OUT_DIR variable which points to `/target/debug/build/roc_builtins-*/out/`.
// So we just need to add "/bitcode" to that.
let dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
// create dir if it does not exist
fs::create_dir_all(&dir).expect("Failed to make $OUT_DIR/ dir.");
dir
}
fn run_command(mut command: Command, flaky_fail_counter: usize) {
let command_str = pretty_command_string(&command);
let command_str = command_str.to_string_lossy();

View file

@ -343,12 +343,11 @@ pub const RocDec = extern struct {
}
}
const unsigned_answer: i128 = mul_and_decimalize(self_u128, other_u128);
const unsigned_answer = mul_and_decimalize(self_u128, other_u128);
if (is_answer_negative) {
return .{ .value = RocDec{ .num = -unsigned_answer }, .has_overflowed = false };
return .{ .value = RocDec{ .num = -unsigned_answer.value }, .has_overflowed = unsigned_answer.has_overflowed };
} else {
return .{ .value = RocDec{ .num = unsigned_answer }, .has_overflowed = false };
return .{ .value = RocDec{ .num = unsigned_answer.value }, .has_overflowed = unsigned_answer.has_overflowed };
}
}
@ -435,8 +434,16 @@ pub const RocDec = extern struct {
pub fn mulSaturated(self: RocDec, other: RocDec) RocDec {
const answer = RocDec.mulWithOverflow(self, other);
if (answer.has_overflowed) {
if (answer.value.num < 0) {
return RocDec.max;
} else {
return RocDec.min;
}
} else {
return answer.value;
}
}
pub fn div(self: RocDec, other: RocDec) RocDec {
const numerator_i128 = self.num;
@ -597,7 +604,7 @@ inline fn count_trailing_zeros_base10(input: i128) u6 {
return count;
}
fn mul_and_decimalize(a: u128, b: u128) i128 {
fn mul_and_decimalize(a: u128, b: u128) WithOverflow(i128) {
const answer_u256 = mul_u128(a, b);
var lhs_hi = answer_u256.hi;
@ -715,14 +722,15 @@ fn mul_and_decimalize(a: u128, b: u128) i128 {
overflowed = overflowed | answer[1];
const d = answer[0];
if (overflowed == 1) {
roc_panic("Decimal multiplication overflow!", 0);
}
// Final 512bit value is d, c, b, a
// need to left shift 321 times
// 315 - 256 is 59. So left shift d, c 59 times.
return @as(i128, @intCast(c >> 59 | (d << (128 - 59))));
// need to right shift 315 times
// 315 - 256 is 59. So shift c right 59 times.
// d takes up the higher space above c, so shift it left by (128 - 59 = 69).
// Since d is being shift left 69 times, all of those 69 bits (+1 for the sign bit)
// must be zero. Otherwise, we have an overflow.
const d_high_bits = d >> 58;
return .{ .value = @as(i128, @intCast(c >> 59 | (d << (128 - 59)))), .has_overflowed = overflowed | d_high_bits != 0 };
}
// Multiply two 128-bit ints and divide the result by 10^DECIMAL_PLACES

View file

@ -132,10 +132,12 @@ comptime {
num.exportAddWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_with_overflow.");
num.exportAddOrPanic(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_or_panic.");
num.exportAddSaturatedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_saturated.");
num.exportAddWrappedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".add_wrapped.");
num.exportSubWithOverflow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_with_overflow.");
num.exportSubOrPanic(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_or_panic.");
num.exportSubSaturatedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_saturated.");
num.exportSubWrappedInt(T, ROC_BUILTINS ++ "." ++ NUM ++ ".sub_wrapped.");
num.exportMulWithOverflow(T, WIDEINTS[i], ROC_BUILTINS ++ "." ++ NUM ++ ".mul_with_overflow.");
num.exportMulOrPanic(T, WIDEINTS[i], ROC_BUILTINS ++ "." ++ NUM ++ ".mul_or_panic.");

View file

@ -110,7 +110,21 @@ pub fn exportNumToFloatCast(comptime T: type, comptime F: type, comptime name: [
pub fn exportPow(comptime T: type, comptime name: []const u8) void {
const f = struct {
fn func(base: T, exp: T) callconv(.C) T {
switch (@typeInfo(T)) {
// std.math.pow can handle ints via powi, but it turns any errors to unreachable
// we want to catch overflow and report a proper error to the user
.Int => {
if (std.math.powi(T, base, exp)) |value| {
return value;
} else |err| switch (err) {
error.Overflow => roc_panic("Integer raised to power overflowed!", 0),
error.Underflow => return 0,
}
},
else => {
return std.math.pow(T, base, exp);
},
}
}
}.func;
@export(f, .{ .name = name ++ @typeName(T), .linkage = .strong });
@ -361,6 +375,15 @@ pub fn exportAddSaturatedInt(comptime T: type, comptime name: []const u8) void {
@export(f, .{ .name = name ++ @typeName(T), .linkage = .strong });
}
pub fn exportAddWrappedInt(comptime T: type, comptime name: []const u8) void {
comptime var f = struct {
fn func(self: T, other: T) callconv(.C) T {
return self +% other;
}
}.func;
@export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong });
}
pub fn exportAddOrPanic(comptime T: type, comptime name: []const u8) void {
const f = struct {
fn func(self: T, other: T) callconv(.C) T {
@ -418,6 +441,15 @@ pub fn exportSubSaturatedInt(comptime T: type, comptime name: []const u8) void {
@export(f, .{ .name = name ++ @typeName(T), .linkage = .strong });
}
pub fn exportSubWrappedInt(comptime T: type, comptime name: []const u8) void {
comptime var f = struct {
fn func(self: T, other: T) callconv(.C) T {
return self -% other;
}
}.func;
@export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong });
}
pub fn exportSubOrPanic(comptime T: type, comptime name: []const u8) void {
const f = struct {
fn func(self: T, other: T) callconv(.C) T {

View file

@ -73,19 +73,29 @@ comptime {
}
}
fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque {
// We store an extra usize which is the size of the full allocation.
const full_size = size + @sizeOf(usize);
var raw_ptr = (std.testing.allocator.alloc(u8, full_size) catch unreachable).ptr;
@as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0] = full_size;
raw_ptr += @sizeOf(usize);
const ptr = @as(?*anyopaque, @ptrCast(raw_ptr));
fn testing_roc_alloc(size: usize, nominal_alignment: u32) callconv(.C) ?*anyopaque {
const real_alignment = 16;
if (nominal_alignment > real_alignment) {
@panic("alignments larger than that of usize are not currently supported");
}
// We store an extra usize which is the size of the data plus the size of the size, directly before the data.
// We need enough clocks of the alignment size to fit this (usually this will be one)
const size_of_size = @sizeOf(usize);
const alignments_needed = size_of_size / real_alignment + comptime if (size_of_size % real_alignment == 0) 0 else 1;
const extra_bytes = alignments_needed * size_of_size;
const full_size = size + extra_bytes;
const whole_ptr = (std.testing.allocator.alignedAlloc(u8, real_alignment, full_size) catch unreachable).ptr;
const written_to_size = size + size_of_size;
@as([*]align(real_alignment) usize, @ptrCast(whole_ptr))[extra_bytes - size_of_size] = written_to_size;
const data_ptr = @as(?*anyopaque, @ptrCast(whole_ptr + extra_bytes));
if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("+ alloc {*}: {} bytes\n", .{ ptr, size });
std.debug.print("+ alloc {*}: {} bytes\n", .{ data_ptr, size });
}
return ptr;
return data_ptr;
}
fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u32) callconv(.C) ?*anyopaque {
@ -106,9 +116,16 @@ fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u
}
fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void {
const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize);
const full_size = @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0];
const slice = raw_ptr[0..full_size];
const alignment = 16;
const size_of_size = @sizeOf(usize);
const alignments_needed = size_of_size / alignment + comptime if (size_of_size % alignment == 0) 0 else 1;
const extra_bytes = alignments_needed * size_of_size;
const byte_array = @as([*]u8, @ptrCast(c_ptr)) - extra_bytes;
const allocation_ptr = @as([*]align(alignment) u8, @alignCast(byte_array));
const offset_from_allocation_to_size = extra_bytes - size_of_size;
const size_of_data_and_size = @as([*]usize, @alignCast(@ptrCast(allocation_ptr)))[offset_from_allocation_to_size];
const full_size = size_of_data_and_size + offset_from_allocation_to_size;
const slice = allocation_ptr[0..full_size];
if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) {
std.debug.print("💀 dealloc {*}\n", .{slice.ptr});

View file

@ -13,11 +13,6 @@ impl IntrinsicName {
}
}
#[repr(u8)]
pub enum DecWidth {
Dec,
}
#[repr(u8)]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub enum FloatWidth {
@ -171,14 +166,6 @@ impl IntWidth {
}
}
impl Index<DecWidth> for IntrinsicName {
type Output = str;
fn index(&self, _: DecWidth) -> &Self::Output {
self.options[0]
}
}
impl Index<FloatWidth> for IntrinsicName {
type Output = str;
@ -299,12 +286,14 @@ pub const INT_TO_FLOAT_CAST_F64: IntrinsicName =
pub const NUM_ADD_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_or_panic");
pub const NUM_ADD_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_saturated");
pub const NUM_ADD_WRAP_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_wrapped");
pub const NUM_ADD_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.add_with_overflow");
pub const NUM_ADD_CHECKED_FLOAT: IntrinsicName =
float_intrinsic!("roc_builtins.num.add_with_overflow");
pub const NUM_SUB_OR_PANIC_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_or_panic");
pub const NUM_SUB_SATURATED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_saturated");
pub const NUM_SUB_WRAP_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_wrapped");
pub const NUM_SUB_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.sub_with_overflow");
pub const NUM_SUB_CHECKED_FLOAT: IntrinsicName =
float_intrinsic!("roc_builtins.num.sub_with_overflow");

View file

@ -144,7 +144,6 @@ pub struct IntroducedVariables {
pub able: VecSet<AbleVariable>,
/// Extension variables which should be inferred in output position.
pub infer_ext_in_output: Vec<Variable>,
pub host_exposed_aliases: VecMap<Symbol, Variable>,
}
impl IntroducedVariables {
@ -156,7 +155,6 @@ impl IntroducedVariables {
.chain(self.named.iter().map(|nv| &nv.variable))
.chain(self.able.iter().map(|av| &av.variable))
.chain(self.infer_ext_in_output.iter())
.chain(self.host_exposed_aliases.values())
.all(|&v| v != var));
}
@ -205,17 +203,10 @@ impl IntroducedVariables {
self.lambda_sets.push(var);
}
pub fn insert_host_exposed_alias(&mut self, symbol: Symbol, var: Variable) {
self.debug_assert_not_already_present(var);
self.host_exposed_aliases.insert(symbol, var);
}
pub fn union(&mut self, other: &Self) {
self.wildcards.extend(other.wildcards.iter().copied());
self.lambda_sets.extend(other.lambda_sets.iter().copied());
self.inferred.extend(other.inferred.iter().copied());
self.host_exposed_aliases
.extend(other.host_exposed_aliases.iter().map(|(k, v)| (*k, *v)));
self.named.extend(other.named.iter().cloned());
self.able.extend(other.able.iter().cloned());
@ -227,7 +218,6 @@ impl IntroducedVariables {
self.wildcards.extend(other.wildcards);
self.lambda_sets.extend(other.lambda_sets);
self.inferred.extend(other.inferred);
self.host_exposed_aliases.extend(other.host_exposed_aliases);
self.named.extend(other.named);
self.able.extend(other.able);
@ -897,7 +887,9 @@ fn can_annotation_help(
"tuples should never be implicitly inferred open"
);
debug_assert!(!elems.is_empty()); // We don't allow empty tuples
if elems.is_empty() {
env.problem(roc_problem::can::Problem::EmptyTupleType(region));
}
let elem_types = can_assigned_tuple_elems(
env,

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use crate::abilities::SpecializationId;
use crate::exhaustive::{ExhaustiveContext, SketchedRows};
use crate::expected::{Expected, PExpected};
use crate::expr::TryKind;
use roc_collections::soa::{index_push_new, slice_extend_new};
use roc_module::ident::{IdentSuffix, TagName};
use roc_module::symbol::{ModuleId, Symbol};
@ -31,6 +32,7 @@ pub struct Constraints {
pub cycles: Vec<Cycle>,
pub fx_call_constraints: Vec<FxCallConstraint>,
pub fx_suffix_constraints: Vec<FxSuffixConstraint>,
pub try_target_constraints: Vec<TryTargetConstraint>,
}
impl std::fmt::Debug for Constraints {
@ -87,6 +89,7 @@ impl Constraints {
let cycles = Vec::new();
let fx_call_constraints = Vec::with_capacity(16);
let fx_suffix_constraints = Vec::new();
let result_type_constraints = Vec::new();
categories.extend([
Category::Record,
@ -103,7 +106,6 @@ impl Constraints {
Category::List,
Category::Str,
Category::Character,
Category::Return,
]);
pattern_categories.extend([
@ -138,6 +140,7 @@ impl Constraints {
cycles,
fx_call_constraints,
fx_suffix_constraints,
try_target_constraints: result_type_constraints,
}
}
@ -635,6 +638,27 @@ impl Constraints {
Constraint::FlexToPure(fx_var)
}
pub fn try_target(
&mut self,
result_type_index: TypeOrVar,
ok_payload_var: Variable,
err_payload_var: Variable,
region: Region,
kind: TryKind,
) -> Constraint {
let constraint = TryTargetConstraint {
target_type_index: result_type_index,
ok_payload_var,
err_payload_var,
region,
kind,
};
let constraint_index = index_push_new(&mut self.try_target_constraints, constraint);
Constraint::TryTarget(constraint_index)
}
pub fn contains_save_the_environment(&self, constraint: &Constraint) -> bool {
match constraint {
Constraint::SaveTheEnvironment => true,
@ -660,6 +684,7 @@ impl Constraints {
| Constraint::Lookup(..)
| Constraint::Pattern(..)
| Constraint::ExpectEffectful(..)
| Constraint::TryTarget(_)
| Constraint::FxCall(_)
| Constraint::FxSuffix(_)
| Constraint::FlexToPure(_)
@ -843,6 +868,8 @@ pub enum Constraint {
FlexToPure(Variable),
/// Expect statement or ignored def to be effectful
ExpectEffectful(Variable, ExpectEffectfulReason, Region),
/// Expect value to be some kind of Result
TryTarget(Index<TryTargetConstraint>),
/// Used for things that always unify, e.g. blanks and runtime errors
True,
SaveTheEnvironment,
@ -909,6 +936,15 @@ pub struct IncludesTag {
pub region: Region,
}
#[derive(Debug, Clone)]
pub struct TryTargetConstraint {
pub target_type_index: TypeOrVar,
pub ok_payload_var: Variable,
pub err_payload_var: Variable,
pub region: Region,
pub kind: TryKind,
}
#[derive(Debug, Clone, Copy)]
pub struct Cycle {
pub def_names: Slice<(Symbol, Region)>,
@ -1000,6 +1036,9 @@ impl std::fmt::Debug for Constraint {
Self::FlexToPure(arg0) => {
write!(f, "FlexToPure({arg0:?})")
}
Self::TryTarget(arg0) => {
write!(f, "ExpectResultType({arg0:?})")
}
Self::True => write!(f, "True"),
Self::SaveTheEnvironment => write!(f, "SaveTheEnvironment"),
Self::Let(arg0, arg1) => f.debug_tuple("Let").field(arg0).field(arg1).finish(),

View file

@ -217,7 +217,6 @@ pub fn deep_copy_type_vars_into_expr(
deep_copy_expr_top(subs, var, expr)
}
#[allow(unused)] // TODO to be removed when this is used for the derivers
pub fn deep_copy_expr_across_subs(
source: &mut Subs,
target: &mut Subs,
@ -479,7 +478,7 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
fx_type: sub!(*fx_type),
early_returns: early_returns
.iter()
.map(|(var, region)| (sub!(*var), *region))
.map(|(var, region, type_)| (sub!(*var), *region, *type_))
.collect(),
name: *name,
captured_symbols: captured_symbols
@ -718,6 +717,24 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
symbol: *symbol,
},
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
} => Try {
result_expr: Box::new(result_expr.map(|e| go_help!(e))),
result_var: sub!(*result_var),
return_var: sub!(*return_var),
ok_payload_var: sub!(*ok_payload_var),
err_payload_var: sub!(*err_payload_var),
err_ext_var: sub!(*err_ext_var),
kind: *kind,
},
RuntimeError(err) => RuntimeError(err.clone()),
}
}

View file

@ -1,6 +1,5 @@
mod pretty_print;
pub use pretty_print::pretty_print_declarations;
pub use pretty_print::pretty_print_def;
pub use pretty_print::pretty_write_declarations;
pub use pretty_print::Ctx as PPCtx;

View file

@ -19,14 +19,6 @@ pub struct Ctx<'a> {
pub print_lambda_names: bool,
}
pub fn pretty_print_declarations(c: &Ctx, declarations: &Declarations) -> String {
let f = Arena::new();
print_declarations_help(c, &f, declarations)
.1
.pretty(80)
.to_string()
}
pub fn pretty_write_declarations(
writer: &mut impl std::io::Write,
c: &Ctx,
@ -452,6 +444,7 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
),
Dbg { .. } => todo!(),
Expect { .. } => todo!(),
Try { .. } => todo!(),
Return { .. } => todo!(),
RuntimeError(_) => todo!(),
}

View file

@ -340,45 +340,12 @@ impl PendingTypeDef<'_> {
pub enum Declaration {
Declare(Def),
DeclareRec(Vec<Def>, IllegalCycleMark),
Builtin(Def),
Expects(ExpectsOrDbgs),
/// If we know a cycle is illegal during canonicalization.
/// Otherwise we will try to detect this during solving; see [`IllegalCycleMark`].
InvalidCycle(Vec<CycleEntry>),
}
impl Declaration {
pub fn def_count(&self) -> usize {
use Declaration::*;
match self {
Declare(_) => 1,
DeclareRec(defs, _) => defs.len(),
InvalidCycle { .. } => 0,
Builtin(_) => 0,
Expects(_) => 0,
}
}
pub fn region(&self) -> Region {
match self {
Declaration::Declare(def) => def.region(),
Declaration::DeclareRec(defs, _) => Region::span_across(
&defs.first().unwrap().region(),
&defs.last().unwrap().region(),
),
Declaration::Builtin(def) => def.region(),
Declaration::InvalidCycle(cycles) => Region::span_across(
&cycles.first().unwrap().expr_region,
&cycles.last().unwrap().expr_region,
),
Declaration::Expects(expects) => Region::span_across(
expects.regions.first().unwrap(),
expects.regions.last().unwrap(),
),
}
}
}
/// Returns a topologically sorted sequence of alias/opaque names
fn sort_type_defs_before_introduction(
referenced_symbols: VecMap<Symbol, Vec<Symbol>>,
@ -2805,10 +2772,6 @@ fn decl_to_let(decl: Declaration, loc_ret: Loc<Expr>) -> Loc<Expr> {
Declaration::InvalidCycle(entries) => {
Loc::at_zero(Expr::RuntimeError(RuntimeError::CircularDef(entries)))
}
Declaration::Builtin(_) => {
// Builtins should only be added to top-level decls, not to let-exprs!
unreachable!()
}
Declaration::Expects(expects) => {
let mut loc_ret = loc_ret;

View file

@ -11,8 +11,8 @@ use roc_module::called_via::{BinOp, CalledVia};
use roc_module::ident::ModuleName;
use roc_parse::ast::Expr::{self, *};
use roc_parse::ast::{
is_expr_suffixed, AssignedField, Collection, Defs, ModuleImportParams, Pattern, StrLiteral,
StrSegment, TypeAnnotation, ValueDef, WhenBranch,
is_expr_suffixed, AssignedField, Collection, Defs, ModuleImportParams, Pattern, ResultTryKind,
StrLiteral, StrSegment, TryTarget, TypeAnnotation, ValueDef, WhenBranch,
};
use roc_problem::can::Problem;
use roc_region::all::{Loc, Region};
@ -44,7 +44,10 @@ fn new_op_call_expr<'a>(
match right_without_spaces {
Try => {
let desugared_left = desugar_expr(env, scope, left);
return desugar_try_expr(env, scope, desugared_left);
return Loc::at(
region,
Expr::LowLevelTry(desugared_left, ResultTryKind::KeywordPrefix),
);
}
Apply(&Loc { value: Try, .. }, arguments, _called_via) => {
let try_fn = desugar_expr(env, scope, arguments.first().unwrap());
@ -58,13 +61,73 @@ fn new_op_call_expr<'a>(
.map(|a| desugar_expr(env, scope, a)),
);
return desugar_try_expr(
env,
scope,
return Loc::at(
region,
Expr::LowLevelTry(
env.arena.alloc(Loc::at(
right.region,
region,
Expr::Apply(try_fn, args.into_bump_slice(), CalledVia::Try),
)),
ResultTryKind::KeywordPrefix,
),
);
}
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
} => {
let loc_fn = env.arena.alloc(Loc::at(right.region, **fn_expr));
let function = desugar_expr(env, scope, loc_fn);
return Loc::at(
region,
LowLevelTry(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
function,
env.arena.alloc([desugar_expr(env, scope, left)]),
CalledVia::Try,
),
)),
ResultTryKind::OperatorSuffix,
),
);
}
Apply(
&Loc {
value:
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
},
region: fn_region,
},
loc_args,
_called_via,
) => {
let loc_fn = env.arena.alloc(Loc::at(fn_region, *fn_expr));
let function = desugar_expr(env, scope, loc_fn);
let mut desugared_args = Vec::with_capacity_in(loc_args.len() + 1, env.arena);
desugared_args.push(desugar_expr(env, scope, left));
for loc_arg in &loc_args[..] {
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
return Loc::at(
region,
LowLevelTry(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
function,
desugared_args.into_bump_slice(),
CalledVia::Try,
),
)),
ResultTryKind::OperatorSuffix,
),
);
}
_ => {}
@ -207,7 +270,9 @@ fn desugar_value_def<'a>(
let desugared_expr = desugar_expr(env, scope, stmt_expr);
if !is_expr_suffixed(&desugared_expr.value) {
if !is_expr_suffixed(&desugared_expr.value)
&& !matches!(&desugared_expr.value, LowLevelTry(_, _))
{
env.problems.push(Problem::StmtAfterExpr(stmt_expr.region));
return ValueDef::StmtAfterExpr;
@ -457,16 +522,23 @@ pub fn desugar_expr<'a>(
env.arena.alloc(Loc { region, value })
}
// desugar the sub_expression, but leave the TrySuffix as this will
// be unwrapped later in desugar_value_def_suffixed
TrySuffix {
expr: sub_expr,
target,
} => {
let intermediate = env.arena.alloc(Loc::at(loc_expr.region, **sub_expr));
let new_sub_loc_expr = desugar_expr(env, scope, intermediate);
match target {
TryTarget::Result => env.arena.alloc(Loc::at(
loc_expr.region,
LowLevelTry(new_sub_loc_expr, ResultTryKind::OperatorSuffix),
)),
TryTarget::Task => {
let new_sub_expr = env.arena.alloc(new_sub_loc_expr.value);
// desugar the sub_expression, but leave the TrySuffix as this will
// be unwrapped later in desugar_value_def_suffixed
env.arena.alloc(Loc::at(
loc_expr.region,
TrySuffix {
@ -475,6 +547,8 @@ pub fn desugar_expr<'a>(
},
))
}
}
}
RecordAccess(sub_expr, paths) => {
let region = loc_expr.region;
let loc_sub_expr = Loc {
@ -957,13 +1031,52 @@ pub fn desugar_expr<'a>(
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
let args_region =
Region::span_across(&loc_args[0].region, &loc_args[loc_args.len() - 1].region);
env.arena.alloc(Loc::at(
loc_expr.region,
args_region,
Expr::Apply(function, desugared_args.into_bump_slice(), CalledVia::Try),
))
};
env.arena.alloc(desugar_try_expr(env, scope, result_expr))
env.arena.alloc(Loc::at(
loc_expr.region,
Expr::LowLevelTry(result_expr, ResultTryKind::KeywordPrefix),
))
}
Apply(
Loc {
value:
TrySuffix {
target: TryTarget::Result,
expr: fn_expr,
},
region: fn_region,
},
loc_args,
_called_via,
) => {
let loc_fn = env.arena.alloc(Loc::at(*fn_region, **fn_expr));
let function = desugar_expr(env, scope, loc_fn);
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), env.arena);
for loc_arg in &loc_args[..] {
desugared_args.push(desugar_expr(env, scope, loc_arg));
}
let args_region =
Region::span_across(&loc_args[0].region, &loc_args[loc_args.len() - 1].region);
let result_expr = env.arena.alloc(Loc::at(
args_region,
Expr::Apply(function, desugared_args.into_bump_slice(), CalledVia::Try),
));
env.arena.alloc(Loc::at(
loc_expr.region,
Expr::LowLevelTry(result_expr, ResultTryKind::OperatorSuffix),
))
}
Apply(loc_fn, loc_args, called_via) => {
let mut desugared_args = Vec::with_capacity_in(loc_args.len(), env.arena);
@ -1104,10 +1217,21 @@ pub fn desugar_expr<'a>(
// Allow naked dbg, necessary for piping values into dbg with the `Pizza` binop
loc_expr
}
DbgStmt(condition, continuation) => {
DbgStmt {
first: condition,
extra_args,
continuation,
} => {
let desugared_condition = &*env.arena.alloc(desugar_expr(env, scope, condition));
let desugared_continuation = &*env.arena.alloc(desugar_expr(env, scope, continuation));
if let Some(last) = extra_args.last() {
let args_region = Region::span_across(&condition.region, &last.region);
env.problem(Problem::OverAppliedDbg {
region: args_region,
});
}
env.arena.alloc(Loc {
value: *desugar_dbg_stmt(env, desugared_condition, desugared_continuation),
region: loc_expr.region,
@ -1123,77 +1247,11 @@ pub fn desugar_expr<'a>(
})
}
// note this only exists after desugaring
LowLevelDbg(_, _, _) => loc_expr,
// note these only exist after desugaring
LowLevelDbg(_, _, _) | LowLevelTry(_, _) => loc_expr,
}
}
pub fn desugar_try_expr<'a>(
env: &mut Env<'a>,
scope: &mut Scope,
result_expr: &'a Loc<Expr<'a>>,
) -> Loc<Expr<'a>> {
let region = result_expr.region;
let ok_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let err_symbol = env.arena.alloc(scope.gen_unique_symbol_name().to_string());
let ok_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Ok"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: ok_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Var {
module_name: "",
ident: ok_symbol,
},
),
guard: None,
});
let err_branch = env.arena.alloc(WhenBranch {
patterns: env.arena.alloc([Loc::at(
region,
Pattern::Apply(
env.arena.alloc(Loc::at(region, Pattern::Tag("Err"))),
env.arena
.alloc([Loc::at(region, Pattern::Identifier { ident: err_symbol })]),
),
)]),
value: Loc::at(
region,
Expr::Return(
env.arena.alloc(Loc::at(
region,
Expr::Apply(
env.arena.alloc(Loc::at(region, Expr::Tag("Err"))),
&*env.arena.alloc([&*env.arena.alloc(Loc::at(
region,
Expr::Var {
module_name: "",
ident: err_symbol,
},
))]),
CalledVia::Try,
),
)),
None,
),
),
guard: None,
});
Loc::at(
region,
Expr::When(result_expr, &*env.arena.alloc([&*ok_branch, &*err_branch])),
)
}
fn desugar_str_segments<'a>(
env: &mut Env<'a>,
scope: &mut Scope,

View file

@ -34,9 +34,6 @@ pub struct Env<'a> {
/// Symbols of values/functions which were referenced by qualified lookups.
pub qualified_value_lookups: VecSet<Symbol>,
/// Symbols of types which were referenced by qualified lookups.
pub qualified_type_lookups: VecSet<Symbol>,
pub top_level_symbols: VecSet<Symbol>,
pub home_params_record: Option<(Symbol, Variable)>,
@ -77,7 +74,6 @@ impl<'a> Env<'a> {
problems: Vec::new(),
closures: MutMap::default(),
qualified_value_lookups: VecSet::default(),
qualified_type_lookups: VecSet::default(),
tailcallable_symbol: None,
top_level_symbols: VecSet::default(),
home_params_record: None,
@ -152,9 +148,7 @@ impl<'a> Env<'a> {
Some(ident_id) => {
let symbol = Symbol::new(module.id, ident_id);
if is_type_name {
self.qualified_type_lookups.insert(symbol);
} else {
if !is_type_name {
self.qualified_value_lookups.insert(symbol);
}
@ -183,9 +177,7 @@ impl<'a> Env<'a> {
Some(ident_id) => {
let symbol = Symbol::new(module.id, ident_id);
if is_type_name {
self.qualified_type_lookups.insert(symbol);
} else {
if !is_type_name {
self.qualified_value_lookups.insert(symbol);
}

View file

@ -1,7 +1,6 @@
use crate::abilities::SpecializationId;
use crate::annotation::{freshen_opaque_def, IntroducedVariables};
use crate::builtins::builtin_defs_map;
use crate::def::{can_defs_with_return, Annotation, Def, DefKind};
use crate::def::{can_defs_with_return, Annotation, Def};
use crate::env::Env;
use crate::num::{
finish_parsing_base, finish_parsing_float, finish_parsing_num, float_expr_from_result,
@ -19,14 +18,16 @@ use roc_module::called_via::CalledVia;
use roc_module::ident::{ForeignSymbol, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::symbol::{IdentId, ModuleId, Symbol};
use roc_parse::ast::{self, Defs, PrecedenceConflict, StrLiteral};
use roc_parse::ast::{self, Defs, PrecedenceConflict, ResultTryKind, StrLiteral};
use roc_parse::ident::Accessor;
use roc_parse::pattern::PatternType::*;
use roc_problem::can::{PrecedenceProblem, Problem, RuntimeError};
use roc_region::all::{Loc, Region};
use roc_types::num::SingleQuoteBound;
use roc_types::subs::{ExhaustiveMark, IllegalCycleMark, RedundantMark, VarStore, Variable};
use roc_types::types::{Alias, Category, IndexOrField, LambdaSet, OptAbleVar, Type};
use roc_types::types::{
Alias, Category, EarlyReturnKind, IndexOrField, LambdaSet, OptAbleVar, Type,
};
use soa::Index;
use std::fmt::{Debug, Display};
use std::path::PathBuf;
@ -328,6 +329,16 @@ pub enum Expr {
symbol: Symbol,
},
Try {
result_expr: Box<Loc<Expr>>,
result_var: Variable,
return_var: Variable,
ok_payload_var: Variable,
err_payload_var: Variable,
err_ext_var: Variable,
kind: TryKind,
},
Return {
return_value: Box<Loc<Expr>>,
return_var: Variable,
@ -337,6 +348,12 @@ pub enum Expr {
RuntimeError(RuntimeError),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TryKind {
KeywordPrefix,
OperatorSuffix,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExpectLookup {
pub symbol: Symbol,
@ -344,14 +361,6 @@ pub struct ExpectLookup {
pub ability_info: Option<SpecializationId>,
}
#[derive(Clone, Copy, Debug)]
pub struct DbgLookup {
pub symbol: Symbol,
pub var: Variable,
pub region: Region,
pub ability_info: Option<SpecializationId>,
}
impl Expr {
pub fn category(&self) -> Category {
match self {
@ -403,14 +412,115 @@ impl Expr {
}
Self::Expect { .. } => Category::Expect,
Self::Crash { .. } => Category::Crash,
Self::Return { .. } => Category::Return,
Self::Return { .. } => Category::Return(EarlyReturnKind::Return),
Self::Dbg { .. } => Category::Expect,
Self::Try { .. } => Category::TrySuccess,
// these nodes place no constraints on the expression's type
Self::RuntimeError(..) => Category::Unknown,
}
}
pub fn contains_any_early_returns(&self) -> bool {
match self {
Self::Num { .. }
| Self::Int { .. }
| Self::Float { .. }
| Self::Str { .. }
| Self::IngestedFile { .. }
| Self::SingleQuote { .. }
| Self::Var { .. }
| Self::AbilityMember { .. }
| Self::ParamsVar { .. }
| Self::Closure(..)
| Self::EmptyRecord
| Self::RecordAccessor(_)
| Self::ZeroArgumentTag { .. }
| Self::OpaqueWrapFunction(_)
| Self::RuntimeError(..) => false,
Self::Return { .. } | Self::Try { .. } => true,
Self::List { loc_elems, .. } => loc_elems
.iter()
.any(|elem| elem.value.contains_any_early_returns()),
Self::When {
loc_cond, branches, ..
} => {
loc_cond.value.contains_any_early_returns()
|| branches.iter().any(|branch| {
branch
.guard
.as_ref()
.is_some_and(|guard| guard.value.contains_any_early_returns())
|| branch.value.value.contains_any_early_returns()
})
}
Self::If {
branches,
final_else,
..
} => {
final_else.value.contains_any_early_returns()
|| branches.iter().any(|(cond, then)| {
cond.value.contains_any_early_returns()
|| then.value.contains_any_early_returns()
})
}
Self::LetRec(defs, expr, _cycle_mark) => {
expr.value.contains_any_early_returns()
|| defs
.iter()
.any(|def| def.loc_expr.value.contains_any_early_returns())
}
Self::LetNonRec(def, expr) => {
def.loc_expr.value.contains_any_early_returns()
|| expr.value.contains_any_early_returns()
}
Self::Call(_func, args, _called_via) => args
.iter()
.any(|(_var, arg_expr)| arg_expr.value.contains_any_early_returns()),
Self::RunLowLevel { args, .. } | Self::ForeignCall { args, .. } => args
.iter()
.any(|(_var, arg_expr)| arg_expr.contains_any_early_returns()),
Self::Tuple { elems, .. } => elems
.iter()
.any(|(_var, loc_elem)| loc_elem.value.contains_any_early_returns()),
Self::Record { fields, .. } => fields
.iter()
.any(|(_field_name, field)| field.loc_expr.value.contains_any_early_returns()),
Self::RecordAccess { loc_expr, .. } => loc_expr.value.contains_any_early_returns(),
Self::TupleAccess { loc_expr, .. } => loc_expr.value.contains_any_early_returns(),
Self::RecordUpdate { updates, .. } => {
updates.iter().any(|(_field_name, field_update)| {
field_update.loc_expr.value.contains_any_early_returns()
})
}
Self::ImportParams(_module_id, _region, params) => params
.as_ref()
.is_some_and(|(_var, p)| p.contains_any_early_returns()),
Self::Tag { arguments, .. } => arguments
.iter()
.any(|(_var, arg)| arg.value.contains_any_early_returns()),
Self::OpaqueRef { argument, .. } => argument.1.value.contains_any_early_returns(),
Self::Crash { msg, .. } => msg.value.contains_any_early_returns(),
Self::Dbg {
loc_message,
loc_continuation,
..
} => {
loc_message.value.contains_any_early_returns()
|| loc_continuation.value.contains_any_early_returns()
}
Self::Expect {
loc_condition,
loc_continuation,
..
} => {
loc_condition.value.contains_any_early_returns()
|| loc_continuation.value.contains_any_early_returns()
}
}
}
}
/// Stores exhaustiveness-checking metadata for a closure argument that may
@ -445,7 +555,7 @@ pub struct ClosureData {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub name: Symbol,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub recursive: Recursive,
@ -650,36 +760,6 @@ pub struct WhenBranch {
pub redundant: RedundantMark,
}
impl WhenBranch {
pub fn pattern_region(&self) -> Region {
Region::span_across(
&self
.patterns
.first()
.expect("when branch has no pattern?")
.pattern
.region,
&self
.patterns
.last()
.expect("when branch has no pattern?")
.pattern
.region,
)
}
}
impl WhenBranch {
pub fn region(&self) -> Region {
Region::across_all(
self.patterns
.iter()
.map(|p| &p.pattern.region)
.chain([self.value.region].iter()),
)
}
}
pub fn canonicalize_expr<'a>(
env: &mut Env<'a>,
var_store: &mut VarStore,
@ -1245,7 +1325,7 @@ pub fn canonicalize_expr<'a>(
(loc_expr.value, output)
}
ast::Expr::DbgStmt(_, _) => {
ast::Expr::DbgStmt { .. } => {
internal_error!("DbgStmt should have been desugared by now")
}
ast::Expr::LowLevelDbg((source_location, source), message, continuation) => {
@ -1285,6 +1365,32 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::LowLevelTry(loc_expr, kind) => {
let (loc_result_expr, output) =
canonicalize_expr(env, var_store, scope, loc_expr.region, &loc_expr.value);
let return_var = var_store.fresh();
scope
.early_returns
.push((return_var, loc_expr.region, EarlyReturnKind::Try));
(
Try {
result_expr: Box::new(loc_result_expr),
result_var: var_store.fresh(),
return_var,
ok_payload_var: var_store.fresh(),
err_payload_var: var_store.fresh(),
err_ext_var: var_store.fresh(),
kind: match kind {
ResultTryKind::KeywordPrefix => TryKind::KeywordPrefix,
ResultTryKind::OperatorSuffix => TryKind::OperatorSuffix,
},
},
output,
)
}
ast::Expr::Return(return_expr, after_return) => {
let mut output = Output::default();
@ -1309,7 +1415,9 @@ pub fn canonicalize_expr<'a>(
let return_var = var_store.fresh();
scope.early_returns.push((return_var, return_expr.region));
scope
.early_returns
.push((return_var, return_expr.region, EarlyReturnKind::Return));
(
Return {
@ -2071,446 +2179,6 @@ fn lookup_to_expr(
}
}
/// Currently uses the heuristic of "only inline if it's a builtin"
pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr {
use Expr::*;
match expr {
// Num stores the `a` variable in `Num a`. Not the same as the variable
// stored in Int and Float below, which is strictly for better error messages
other @ Num(..)
| other @ Int(..)
| other @ Float(..)
| other @ Str { .. }
| other @ IngestedFile(..)
| other @ SingleQuote(..)
| other @ RuntimeError(_)
| other @ EmptyRecord
| other @ RecordAccessor { .. }
| other @ RecordUpdate { .. }
| other @ Var(..)
| other @ ParamsVar { .. }
| other @ AbilityMember(..)
| other @ RunLowLevel { .. }
| other @ ForeignCall { .. }
| other @ OpaqueWrapFunction(_)
| other @ Crash { .. }
| other @ Return { .. } => other,
List {
elem_var,
loc_elems,
} => {
let mut new_elems = Vec::with_capacity(loc_elems.len());
for loc_elem in loc_elems {
let value = inline_calls(var_store, loc_elem.value);
new_elems.push(Loc {
value,
region: loc_elem.region,
});
}
List {
elem_var,
loc_elems: new_elems,
}
}
// Branching
When {
cond_var,
expr_var,
region,
loc_cond,
branches,
branches_cond_var,
exhaustive,
} => {
let loc_cond = Box::new(Loc {
region: loc_cond.region,
value: inline_calls(var_store, loc_cond.value),
});
let mut new_branches = Vec::with_capacity(branches.len());
for branch in branches {
let value = Loc {
value: inline_calls(var_store, branch.value.value),
region: branch.value.region,
};
let guard = match branch.guard {
Some(loc_expr) => Some(Loc {
region: loc_expr.region,
value: inline_calls(var_store, loc_expr.value),
}),
None => None,
};
let new_branch = WhenBranch {
patterns: branch.patterns,
value,
guard,
redundant: RedundantMark::new(var_store),
};
new_branches.push(new_branch);
}
When {
cond_var,
expr_var,
region,
loc_cond,
branches: new_branches,
branches_cond_var,
exhaustive,
}
}
If {
cond_var,
branch_var,
branches,
final_else,
} => {
let mut new_branches = Vec::with_capacity(branches.len());
for (loc_cond, loc_expr) in branches {
let loc_cond = Loc {
value: inline_calls(var_store, loc_cond.value),
region: loc_cond.region,
};
let loc_expr = Loc {
value: inline_calls(var_store, loc_expr.value),
region: loc_expr.region,
};
new_branches.push((loc_cond, loc_expr));
}
let final_else = Box::new(Loc {
region: final_else.region,
value: inline_calls(var_store, final_else.value),
});
If {
cond_var,
branch_var,
branches: new_branches,
final_else,
}
}
Expect {
loc_condition,
loc_continuation,
lookups_in_cond,
} => {
let loc_condition = Loc {
region: loc_condition.region,
value: inline_calls(var_store, loc_condition.value),
};
let loc_continuation = Loc {
region: loc_continuation.region,
value: inline_calls(var_store, loc_continuation.value),
};
Expect {
loc_condition: Box::new(loc_condition),
loc_continuation: Box::new(loc_continuation),
lookups_in_cond,
}
}
Dbg {
source_location,
source,
loc_message,
loc_continuation,
variable,
symbol,
} => {
let loc_message = Loc {
region: loc_message.region,
value: inline_calls(var_store, loc_message.value),
};
let loc_continuation = Loc {
region: loc_continuation.region,
value: inline_calls(var_store, loc_continuation.value),
};
Dbg {
source_location,
source,
loc_message: Box::new(loc_message),
loc_continuation: Box::new(loc_continuation),
variable,
symbol,
}
}
LetRec(defs, loc_expr, mark) => {
let mut new_defs = Vec::with_capacity(defs.len());
for def in defs {
new_defs.push(Def {
loc_pattern: def.loc_pattern,
loc_expr: Loc {
region: def.loc_expr.region,
value: inline_calls(var_store, def.loc_expr.value),
},
expr_var: def.expr_var,
pattern_vars: def.pattern_vars,
annotation: def.annotation,
kind: def.kind,
});
}
let loc_expr = Loc {
region: loc_expr.region,
value: inline_calls(var_store, loc_expr.value),
};
LetRec(new_defs, Box::new(loc_expr), mark)
}
LetNonRec(def, loc_expr) => {
let def = Def {
loc_pattern: def.loc_pattern,
loc_expr: Loc {
region: def.loc_expr.region,
value: inline_calls(var_store, def.loc_expr.value),
},
expr_var: def.expr_var,
pattern_vars: def.pattern_vars,
annotation: def.annotation,
kind: def.kind,
};
let loc_expr = Loc {
region: loc_expr.region,
value: inline_calls(var_store, loc_expr.value),
};
LetNonRec(Box::new(def), Box::new(loc_expr))
}
Closure(ClosureData {
function_type,
closure_type,
return_type,
fx_type,
early_returns,
recursive,
name,
captured_symbols,
arguments,
loc_body,
}) => {
let loc_expr = *loc_body;
let loc_expr = Loc {
value: inline_calls(var_store, loc_expr.value),
region: loc_expr.region,
};
Closure(ClosureData {
function_type,
closure_type,
return_type,
fx_type,
early_returns,
recursive,
name,
captured_symbols,
arguments,
loc_body: Box::new(loc_expr),
})
}
Record { record_var, fields } => {
todo!(
"Inlining for Record with record_var {:?} and fields {:?}",
record_var,
fields
);
}
ImportParams(module_id, region, Some((var, expr))) => ImportParams(
module_id,
region,
Some((var, Box::new(inline_calls(var_store, *expr)))),
),
ImportParams(module_id, region, None) => ImportParams(module_id, region, None),
RecordAccess {
record_var,
ext_var,
field_var,
loc_expr,
field,
} => {
todo!("Inlining for RecordAccess with record_var {:?}, ext_var {:?}, field_var {:?}, loc_expr {:?}, field {:?}", record_var, ext_var, field_var, loc_expr, field);
}
Tuple { tuple_var, elems } => {
todo!(
"Inlining for Tuple with tuple_var {:?} and elems {:?}",
tuple_var,
elems
);
}
TupleAccess {
tuple_var,
ext_var,
elem_var,
loc_expr,
index,
} => {
todo!("Inlining for TupleAccess with tuple_var {:?}, ext_var {:?}, elem_var {:?}, loc_expr {:?}, index {:?}", tuple_var, ext_var, elem_var, loc_expr, index);
}
Tag {
tag_union_var: variant_var,
ext_var,
name,
arguments,
} => {
todo!(
"Inlining for Tag with variant_var {:?}, ext_var {:?}, name {:?}, arguments {:?}",
variant_var,
ext_var,
name,
arguments
);
}
OpaqueRef {
opaque_var,
name,
argument,
specialized_def_type,
type_arguments,
lambda_set_variables,
} => {
let (var, loc_expr) = *argument;
let argument = Box::new((
var,
loc_expr.map_owned(|expr| inline_calls(var_store, expr)),
));
OpaqueRef {
opaque_var,
name,
argument,
specialized_def_type,
type_arguments,
lambda_set_variables,
}
}
ZeroArgumentTag {
closure_name,
variant_var,
ext_var,
name,
} => {
todo!(
"Inlining for ZeroArgumentTag with closure_name {:?}, variant_var {:?}, ext_var {:?}, name {:?}",
closure_name,
variant_var,
ext_var,
name,
);
}
Call(boxed_tuple, args, called_via) => {
let (fn_var, loc_expr, closure_var, expr_var, fx_var) = *boxed_tuple;
match loc_expr.value {
Var(symbol, _) if symbol.is_builtin() => {
// NOTE: This assumes builtins are not effectful!
match builtin_defs_map(symbol, var_store) {
Some(Def {
loc_expr:
Loc {
value:
Closure(ClosureData {
recursive,
arguments: params,
loc_body: boxed_body,
..
}),
..
},
..
}) => {
debug_assert_eq!(recursive, Recursive::NotRecursive);
// Since this is a canonicalized Expr, we should have
// already detected any arity mismatches and replaced this
// with a RuntimeError if there was a mismatch.
debug_assert_eq!(params.len(), args.len());
// Start with the function's body as the answer.
let mut loc_answer = *boxed_body;
// Wrap the body in one LetNonRec for each argument,
// such that at the end we have all the arguments in
// scope with the values the caller provided.
for (
(_param_var, _exhaustive_mark, loc_pattern),
(expr_var, loc_expr),
) in params.iter().cloned().zip(args.into_iter()).rev()
{
// TODO get the correct vars into here.
// Not sure if param_var should be involved.
let pattern_vars = SendMap::default();
let def = Def {
loc_pattern,
loc_expr,
expr_var,
pattern_vars,
annotation: None,
kind: DefKind::Let,
};
loc_answer = Loc {
region: Region::zero(),
value: LetNonRec(Box::new(def), Box::new(loc_answer)),
};
}
loc_answer.value
}
Some(_) => {
internal_error!("Tried to inline a non-function");
}
None => {
internal_error!(
"Tried to inline a builtin that wasn't registered: {:?}",
symbol
);
}
}
}
_ => {
// For now, we only inline calls to builtins. Leave this alone!
Call(
Box::new((fn_var, loc_expr, closure_var, expr_var, fx_var)),
args,
called_via,
)
}
}
}
}
}
fn flatten_str_literal<'a>(
env: &mut Env<'a>,
var_store: &mut VarStore,
@ -2545,8 +2213,9 @@ pub fn is_valid_interpolation(expr: &ast::Expr<'_>) -> bool {
| ast::Expr::MalformedIdent(_, _)
| ast::Expr::Tag(_)
| ast::Expr::OpaqueRef(_) => true,
ast::Expr::LowLevelTry(loc_expr, _) => is_valid_interpolation(&loc_expr.value),
// Newlines are disallowed inside interpolation, and these all require newlines
ast::Expr::DbgStmt(_, _)
ast::Expr::DbgStmt { .. }
| ast::Expr::LowLevelDbg(_, _, _)
| ast::Expr::Return(_, _)
| ast::Expr::When(_, _)
@ -3230,7 +2899,7 @@ impl Declarations {
pub fn expects(&self) -> ExpectCollector {
let mut collector = ExpectCollector {
expects: VecMap::default(),
dbgs: VecMap::default(),
has_dbgs: false,
};
let var = Variable::EMPTY_RECORD;
@ -3301,7 +2970,7 @@ pub struct FunctionDef {
pub closure_type: Variable,
pub return_type: Variable,
pub fx_type: Variable,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
pub captured_symbols: Vec<(Symbol, Variable)>,
pub arguments: Vec<(Variable, AnnotatedMark, Loc<Pattern>)>,
}
@ -3443,6 +3112,9 @@ pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec<ExpectLookup> {
// Intentionally ignore the lookups in the nested `expect` condition itself,
// because they couldn't possibly influence the outcome of this `expect`!
}
Expr::Try { result_expr, .. } => {
stack.push(&result_expr.value);
}
Expr::Return { return_value, .. } => {
stack.push(&return_value.value);
}
@ -3552,7 +3224,7 @@ pub fn toplevel_expect_to_inline_expect_pure(mut loc_expr: Loc<Expr>) -> Loc<Exp
pub struct ExpectCollector {
pub expects: VecMap<Region, Vec<ExpectLookup>>,
pub dbgs: VecMap<Symbol, DbgLookup>,
pub has_dbgs: bool,
}
impl crate::traverse::Visitor for ExpectCollector {
@ -3566,20 +3238,8 @@ impl crate::traverse::Visitor for ExpectCollector {
self.expects
.insert(loc_condition.region, lookups_in_cond.to_vec());
}
Expr::Dbg {
loc_message,
variable,
symbol,
..
} => {
let lookup = DbgLookup {
symbol: *symbol,
var: *variable,
region: loc_message.region,
ability_info: None,
};
self.dbgs.insert(*symbol, lookup);
Expr::Dbg { .. } => {
self.has_dbgs = true;
}
_ => (),
}

View file

@ -24,7 +24,6 @@ pub mod num;
pub mod pattern;
pub mod procedure;
pub mod scope;
pub mod string;
pub mod suffixed;
pub mod traverse;

View file

@ -5,9 +5,7 @@ use crate::annotation::{canonicalize_annotation, AnnotationFor};
use crate::def::{canonicalize_defs, report_unused_imports, Def, DefKind};
use crate::desugar::desugar_record_destructures;
use crate::env::{Env, FxMode};
use crate::expr::{
ClosureData, DbgLookup, Declarations, ExpectLookup, Expr, Output, PendingDerives,
};
use crate::expr::{ClosureData, Declarations, ExpectLookup, Expr, Output, PendingDerives};
use crate::pattern::{
canonicalize_record_destructs, BindingsFromPattern, Pattern, PermitShadows, RecordDestruct,
};
@ -137,7 +135,7 @@ pub struct Module {
pub rigid_variables: RigidVariables,
pub abilities_store: PendingAbilitiesStore,
pub loc_expects: VecMap<Region, Vec<ExpectLookup>>,
pub loc_dbgs: VecMap<Symbol, DbgLookup>,
pub has_dbgs: bool,
pub module_params: Option<ModuleParams>,
}
@ -188,7 +186,7 @@ pub struct ModuleOutput {
pub pending_derives: PendingDerives,
pub scope: Scope,
pub loc_expects: VecMap<Region, Vec<ExpectLookup>>,
pub loc_dbgs: VecMap<Symbol, DbgLookup>,
pub has_dbgs: bool,
}
fn has_no_implementation(expr: &Expr) -> bool {
@ -371,9 +369,10 @@ pub fn canonicalize_module_defs<'a>(
PatternType::TopLevelDef,
);
for (_early_return_var, early_return_region) in &scope.early_returns {
for (_early_return_var, early_return_region, early_return_kind) in &scope.early_returns {
env.problem(Problem::ReturnOutsideOfFunction {
region: *early_return_region,
return_kind: *early_return_kind,
});
}
@ -763,7 +762,7 @@ pub fn canonicalize_module_defs<'a>(
symbols_from_requires,
pending_derives,
loc_expects: collected.expects,
loc_dbgs: collected.dbgs,
has_dbgs: collected.has_dbgs,
exposed_symbols,
}
}
@ -964,6 +963,14 @@ fn fix_values_captured_in_closure_expr(
);
}
Try { result_expr, .. } => {
fix_values_captured_in_closure_expr(
&mut result_expr.value,
no_capture_symbols,
closure_captures,
);
}
Return { return_value, .. } => {
fix_values_captured_in_closure_expr(
&mut return_value.value,

View file

@ -9,7 +9,7 @@ use crate::scope::{PendingAbilitiesInScope, Scope};
use roc_exhaustive::ListArity;
use roc_module::ident::{Ident, Lowercase, TagName};
use roc_module::symbol::Symbol;
use roc_parse::ast::{self, StrLiteral, StrSegment};
use roc_parse::ast::{self, ExtractSpaces, StrLiteral, StrSegment};
use roc_parse::pattern::PatternType;
use roc_problem::can::{MalformedPatternProblem, Problem, RuntimeError, ShadowKind};
use roc_region::all::{Loc, Region};
@ -273,6 +273,14 @@ pub fn canonicalize_def_header_pattern<'a>(
region,
) {
Ok((symbol, shadowing_ability_member)) => {
if name.contains("__") {
env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern(
MalformedPatternProblem::BadIdent(
roc_parse::ident::BadIdent::TooManyUnderscores(region.start()),
),
region,
)));
}
let can_pattern = match shadowing_ability_member {
// A fresh identifier.
None => {
@ -468,7 +476,14 @@ pub fn canonicalize_pattern<'a>(
Pattern::OpaqueNotInScope(Loc::at(tag.region, name.into()))
}
},
_ => unreachable!("Other patterns cannot be applied"),
_ => {
env.problem(Problem::RuntimeError(RuntimeError::MalformedPattern(
MalformedPatternProblem::CantApplyPattern,
tag.region,
)));
Pattern::UnsupportedPattern(region)
}
}
}
@ -809,7 +824,7 @@ pub fn canonicalize_record_destructs<'a>(
let mut opt_erroneous = None;
for loc_pattern in patterns.iter() {
match loc_pattern.value {
match loc_pattern.value.extract_spaces().item {
Identifier { ident: label } => {
match scope.introduce(label.into(), region) {
Ok(symbol) => {

View file

@ -1,42 +1,5 @@
use crate::pattern::Pattern;
use crate::{expr::Expr, scope::SymbolLookup};
use crate::scope::SymbolLookup;
use roc_module::symbol::{ModuleId, Symbol};
use roc_region::all::{Loc, Region};
use roc_types::subs::Variable;
#[derive(Clone, Debug)]
pub struct Procedure {
pub name: Option<Box<str>>,
pub is_self_tail_recursive: bool,
pub definition: Region,
pub args: Vec<Loc<Pattern>>,
pub body: Loc<Expr>,
pub references: References,
pub var: Variable,
pub ret_var: Variable,
}
impl Procedure {
pub fn new(
definition: Region,
args: Vec<Loc<Pattern>>,
body: Loc<Expr>,
references: References,
var: Variable,
ret_var: Variable,
) -> Procedure {
Procedure {
name: None,
is_self_tail_recursive: false,
definition,
args,
body,
references,
var,
ret_var,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
struct ReferencesBitflags(u8);

View file

@ -5,7 +5,7 @@ use roc_module::symbol::{IdentId, IdentIds, ModuleId, ModuleIds, Symbol};
use roc_problem::can::{RuntimeError, ScopeModuleSource};
use roc_region::all::{Loc, Region};
use roc_types::subs::Variable;
use roc_types::types::{Alias, AliasKind, AliasVar, Type};
use roc_types::types::{Alias, AliasKind, AliasVar, EarlyReturnKind, Type};
use crate::abilities::PendingAbilitiesStore;
@ -49,7 +49,7 @@ pub struct Scope {
/// We won't intern them because they're only used during canonicalization for error reporting.
ignored_locals: VecMap<String, Region>,
pub early_returns: Vec<(Variable, Region)>,
pub early_returns: Vec<(Variable, Region, EarlyReturnKind)>,
}
impl Scope {

View file

@ -1,446 +0,0 @@
// use bumpalo::collections::string::String;
// use bumpalo::collections::vec::Vec;
use bumpalo::Bump;
use roc_error_macros::internal_error;
use roc_parse::ast::Expr;
// use roc_parse::ast::{Attempting, Expr};
// use roc_parse::ident;
// use roc_parse::parser::{unexpected, unexpected_eof, Fail, Parser, State};
// use roc_parse::problems::{Problem, Problems};
// use roc_region::all::{Loc, Region};
use roc_region::all::Region;
// use std::char;
// use std::iter::Peekable;
pub fn canonical_string_literal<'a>(_arena: &Bump, _raw: &'a str, _region: Region) -> Expr<'a> {
internal_error!("TODO restore canonicalization");
}
// let mut problems = std::vec::Vec::new();
// // Stores the accumulated string characters
// let mut buf = String::new_in(arena);
// // This caches the total string length of interpolated_pairs. Every
// // time we add a new pair to interpolated_pairs, we increment this
// // by the sum of whatever we parsed in order to obtain that pair.
// let mut buf_col_offset: usize = 0;
// // Stores interpolated identifiers, if any.
// let mut interpolated_pairs = Vec::new_in(arena);
// let mut chars = raw.chars();
// while let Some(ch) = chars.next() {
// match ch {
// // If it's a backslash, escape things.
// '\\' => match chars.next() {
// Some(next_ch) => {
// if let Some(ident) = handle_escaped_char(
// arena,
// &state,
// next_ch,
// &mut chars,
// &mut buf,
// &mut problems,
// )? {
// let expr = Expr::Var(ident);
// // +2 for `$(` and then another +1 for `)` at the end
// let parsed_length = buf.len() + 2 + ident.len() + 1;
// // Casting should always succeed in this section, because
// // if this string literal overflowed our maximum
// // line length, that would have already happened back
// // in the parsing step, and we never would have reached
// // this code. Still, debug_assert that they won't!
// debug_assert!(buf_col_offset <= u16::MAX as usize);
// debug_assert!(ident.len() <= u16::MAX as usize);
// debug_assert!((parsed_length - ident.len() - 1) <= u16::MAX as usize);
// let start_line = state.line;
// // Subtract ident length and another 1 for the `)`
// let start_col = state.column
// + buf_col_offset as u16
// + (parsed_length - ident.len() - 1) as u16;
// let ident_region = Region {
// start_line,
// start_col,
// end_line: start_line,
// end_col: start_col + ident.len() as u16 - 1,
// };
// let loc_expr = Loc {
// region: ident_region,
// value: expr,
// };
// // Push the accumulated string into the pairs list,
// // along with the ident that came after it.
// interpolated_pairs.push((buf.into_bump_str(), loc_expr));
// // Reset the buffer so we start working on a new string.
// buf = String::new_in(arena);
// // Advance the cached offset of how many chars we've parsed,
// // so the next time we see an interpolated ident, we can
// // correctly calculate its region.
// buf_col_offset += parsed_length;
// }
// }
// None => {
// problems.push(loc_char(Problem::TrailingBackslash, &state, buf.len()));
// }
// },
// '\t' => {
// // Tabs are syntax errors.
// problems.push(loc_char(Problem::Tab, &state, buf.len()));
// }
// '\r' => {
// // Carriage returns aren't allowed in string literals.
// problems.push(loc_char(Problem::CarriageReturn, &state, buf.len()));
// }
// normal_char => buf.push(normal_char),
// }
// }
// // We ran out of characters; this is the end of the string!
// if problems.is_empty() {
// let final_str = buf.into_bump_str();
// if interpolated_pairs.is_empty() {
// Expr::Str(final_str)
// } else {
// let tuple_ref = arena.alloc((interpolated_pairs.into_bump_slice(), final_str));
// Expr::InterpolatedStr(tuple_ref)
// }
// } else {
// Expr::MalformedStr(problems.into_boxed_slice())
// }
// }
// fn loc_char<'a, V>(value: V, state: &State<'a>, buf_len: usize) -> Located<V> {
// let start_line = state.line;
// let start_col = state.column + buf_len as u16;
// let end_line = start_line;
// // All invalid chars should have a length of 1
// let end_col = state.column + 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// Loc { region, value }
// }
// fn loc_escaped_char<'a, V>(value: V, state: &State<'a>, buf_len: usize) -> Located<V> {
// let start_line = state.line;
// let start_col = state.column + buf_len as u16;
// let end_line = start_line;
// // escapes should all be 2 chars long
// let end_col = state.column + 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// Loc { region, value }
// }
// fn loc_escaped_unicode<'a, V>(
// value: V,
// state: &State<'a>,
// buf_len: usize,
// hex_str_len: usize,
// ) -> Located<V> {
// let start_line = state.line;
// // +1 due to the `"` which precedes buf.
// let start_col = state.column + buf_len as u16 + 1;
// let end_line = start_line;
// // +3 due to the `\u{` and another + 1 due to the `}`
// // -1 to prevent overshooting because end col is inclusive.
// let end_col = start_col + 3 + hex_str_len as u16 + 1 - 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// Loc { region, value }
// }
// #[inline(always)]
// fn handle_escaped_char<'a, I>(
// arena: &'a Bump,
// state: &State<'a>,
// ch: char,
// chars: &mut Peekable<I>,
// buf: &mut String<'a>,
// problems: &mut Problems,
// ) -> Result<Option<&'a str>, (Fail, State<'a>)>
// where
// I: Iterator<Item = char>,
// {
// match ch {
// '\\' => buf.push('\\'),
// '"' => buf.push('"'),
// 't' => buf.push('\t'),
// 'n' => buf.push('\n'),
// 'r' => buf.push('\r'),
// '0' => buf.push('\0'), // We explicitly support null characters, as we
// // can't be sure we won't receive them from Rust.
// 'u' => handle_escaped_unicode(arena, &state, chars, buf, problems)?,
// '(' => {
// let ident = parse_interpolated_ident(arena, state, chars)?;
// return Ok(Some(ident));
// }
// '\t' => {
// // Report and continue.
// // Tabs are syntax errors, but maybe the rest of the string is fine!
// problems.push(loc_escaped_char(Problem::Tab, &state, buf.len()));
// }
// '\r' => {
// // Report and continue.
// // Carriage returns aren't allowed in string literals,
// // but maybe the rest of the string is fine!
// problems.push(loc_escaped_char(Problem::CarriageReturn, &state, buf.len()));
// }
// '\n' => {
// // Report and bail out.
// // We can't safely assume where the string was supposed to end.
// problems.push(loc_escaped_char(
// Problem::NewlineInLiteral,
// &state,
// buf.len(),
// ));
// return Err(unexpected_eof(
// buf.len(),
// Attempting::UnicodeEscape,
// state.clone(),
// ));
// }
// _ => {
// // Report and continue.
// // An unsupported escaped char (e.g. \q) shouldn't halt parsing.
// problems.push(loc_escaped_char(
// Problem::UnsupportedEscapedChar,
// &state,
// buf.len(),
// ));
// }
// }
// Ok(None)
// }
// #[inline(always)]
// fn handle_escaped_unicode<'a, I>(
// arena: &'a Bump,
// state: &State<'a>,
// chars: &mut Peekable<I>,
// buf: &mut String<'a>,
// problems: &mut Problems,
// ) -> Result<(), (Fail, State<'a>)>
// where
// I: Iterator<Item = char>,
// {
// // \u{00A0} is how you specify a Unicode code point,
// // so we should always see a '{' next.
// if chars.next() != Some('{') {
// let start_line = state.line;
// // +1 due to the `"` which precedes buf
// let start_col = state.column + 1 + buf.len() as u16;
// let end_line = start_line;
// // All we parsed was `\u`, so end on the column after `\`'s column.
// let end_col = start_col + 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// problems.push(Loc {
// region,
// value: Problem::NoUnicodeDigits,
// });
// // The rest of the string literal might be fine. Keep parsing!
// return Ok(());
// }
// // Record the point in the string literal where we started parsing `\u`
// let start_of_unicode = buf.len();
// // Stores the accumulated unicode digits
// let mut hex_str = String::new_in(arena);
// while let Some(hex_char) = chars.next() {
// match hex_char {
// '}' => {
// // Done! Validate and add it to the buffer.
// match u32::from_str_radix(&hex_str, 16) {
// Ok(code_pt) => {
// if code_pt > 0x10FFFF {
// let start_line = state.line;
// // +1 due to the `"` which precedes buf
// // +3 due to the `\u{` which precedes the hex digits
// let start_col = state.column + 1 + buf.len() as u16 + 3;
// let end_line = start_line;
// // We want to underline only the number. That's the error!
// // -1 because we want to end on the last digit, not
// // overshoot it.
// let end_col = start_col + hex_str.len() as u16 - 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// problems.push(Loc {
// region,
// value: Problem::UnicodeCodePtTooLarge,
// });
// } else {
// // If it all checked out, add it to
// // the main buffer.
// match char::from_u32(code_pt) {
// Some(ch) => buf.push(ch),
// None => {
// problems.push(loc_escaped_unicode(
// Problem::InvalidUnicodeCodePt,
// &state,
// start_of_unicode,
// hex_str.len(),
// ));
// }
// }
// }
// }
// Err(_) => {
// let problem = if hex_str.is_empty() {
// Problem::NoUnicodeDigits
// } else {
// Problem::NonHexCharsInUnicodeCodePt
// };
// problems.push(loc_escaped_unicode(
// problem,
// &state,
// start_of_unicode,
// hex_str.len(),
// ));
// }
// }
// // We are now done processing the unicode portion of the string,
// // so exit the loop without further advancing the iterator.
// return Ok(());
// }
// '\t' => {
// // Report and continue.
// // Tabs are syntax errors, but maybe the rest of the string is fine!
// problems.push(loc_escaped_unicode(
// Problem::Tab,
// &state,
// start_of_unicode,
// hex_str.len(),
// ));
// }
// '\r' => {
// // Report and continue.
// // Carriage returns aren't allowed in string literals,
// // but maybe the rest of the string is fine!
// problems.push(loc_escaped_unicode(
// Problem::CarriageReturn,
// &state,
// start_of_unicode,
// hex_str.len(),
// ));
// }
// '\n' => {
// // Report and bail out.
// // We can't safely assume where the string was supposed to end.
// problems.push(loc_escaped_unicode(
// Problem::NewlineInLiteral,
// &state,
// start_of_unicode,
// hex_str.len(),
// ));
// return Err(unexpected_eof(
// buf.len(),
// Attempting::UnicodeEscape,
// state.clone(),
// ));
// }
// normal_char => hex_str.push(normal_char),
// }
// // If we're about to hit the end of the string, and we didn't already
// // complete parsing a valid unicode escape sequence, this is a malformed
// // escape sequence - it wasn't terminated!
// if chars.peek() == Some(&'"') {
// // Record a problem and exit the loop early, so the string literal
// // parsing logic can consume the quote and do its job as normal.
// let start_line = state.line;
// // +1 due to the `"` which precedes buf.
// let start_col = state.column + buf.len() as u16 + 1;
// let end_line = start_line;
// // +3 due to the `\u{`
// // -1 to prevent overshooting because end col is inclusive.
// let end_col = start_col + 3 + hex_str.len() as u16 - 1;
// let region = Region {
// start_line,
// start_col,
// end_line,
// end_col,
// };
// problems.push(Loc {
// region,
// value: Problem::MalformedEscapedUnicode,
// });
// return Ok(());
// }
// }
// Ok(())
// }
// #[inline(always)]
// fn parse_interpolated_ident<'a, I>(
// arena: &'a Bump,
// state: &State<'a>,
// chars: &mut Peekable<I>,
// ) -> Result<&'a str, (Fail, State<'a>)>
// where
// I: Iterator<Item = char>,
// {
// // This will return Err on invalid identifiers like "if"
// let ((string, next_char), state) = ident::parse_into(arena, chars, state.clone())?;
// // Make sure we got a closing ) to end the interpolation.
// match next_char {
// Some(')') => Ok(string),
// Some(ch) => Err(unexpected(ch, 0, state, Attempting::InterpolatedString)),
// None => Err(unexpected_eof(0, Attempting::InterpolatedString, state)),
// }
// }

View file

@ -164,7 +164,7 @@ pub fn unwrap_suffixed_expression<'a>(
// USEFUL TO SEE THE UNWRAPPING
// OF AST NODES AS THEY DESCEND
// if is_expr_suffixed(&loc_expr.value) {
// dbg!(&maybe_def_pat, &loc_expr, &unwrapped_expression);
// eprintln!("{:?}, {:?}, {:?}", &maybe_def_pat, &loc_expr, &unwrapped_expression);
// }
unwrapped_expression

View file

@ -22,10 +22,6 @@ pub enum DeclarationInfo<'a> {
pattern: Pattern,
annotation: Option<&'a Annotation>,
},
Return {
loc_expr: &'a Loc<Expr>,
expr_var: Variable,
},
Expectation {
loc_condition: &'a Loc<Expr>,
},
@ -54,7 +50,6 @@ impl<'a> DeclarationInfo<'a> {
loc_expr,
..
} => Region::span_across(&loc_symbol.region, &loc_expr.region),
Return { loc_expr, .. } => loc_expr.region,
Expectation { loc_condition } => loc_condition.region,
Function {
loc_symbol,
@ -72,7 +67,6 @@ impl<'a> DeclarationInfo<'a> {
fn var(&self) -> Variable {
match self {
DeclarationInfo::Value { expr_var, .. } => *expr_var,
DeclarationInfo::Return { expr_var, .. } => *expr_var,
DeclarationInfo::Expectation { .. } => Variable::BOOL,
DeclarationInfo::Function { expr_var, .. } => *expr_var,
DeclarationInfo::Destructure { expr_var, .. } => *expr_var,
@ -191,9 +185,6 @@ pub fn walk_decl<V: Visitor>(visitor: &mut V, decl: DeclarationInfo<'_>) {
Expectation { loc_condition } => {
visitor.visit_expr(&loc_condition.value, loc_condition.region, Variable::BOOL);
}
Return { loc_expr, expr_var } => {
visitor.visit_expr(&loc_expr.value, loc_expr.region, expr_var);
}
Function {
loc_symbol,
loc_body,
@ -400,6 +391,17 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
Variable::NULL,
);
}
Expr::Try {
result_expr,
result_var,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
kind: _,
} => {
visitor.visit_expr(&result_expr.value, result_expr.region, *result_var);
}
Expr::Return {
return_value,
return_var,

View file

@ -3,7 +3,6 @@ extern crate bumpalo;
use self::bumpalo::Bump;
use roc_can::desugar;
use roc_can::env::Env;
use roc_can::expr::Output;
use roc_can::expr::{canonicalize_expr, Expr};
use roc_can::scope::Scope;
use roc_collections::all::MutMap;
@ -26,12 +25,8 @@ pub fn can_expr(expr_str: &str) -> CanExprOut {
pub struct CanExprOut {
pub loc_expr: Loc<Expr>,
pub output: Output,
pub problems: Vec<Problem>,
pub home: ModuleId,
pub interns: Interns,
pub var_store: VarStore,
pub var: Variable,
}
#[allow(dead_code)]
@ -43,7 +38,6 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut
});
let mut var_store = VarStore::default();
let var = var_store.fresh();
let qualified_module_ids = PackageModuleIds::default();
let mut scope = Scope::new(
@ -86,7 +80,7 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut
roc_types::types::AliasKind::Structural,
);
let (loc_expr, output) = canonicalize_expr(
let (loc_expr, _) = canonicalize_expr(
&mut env,
&mut var_store,
&mut scope,
@ -104,12 +98,8 @@ pub fn can_expr_with(arena: &Bump, home: ModuleId, expr_str: &str) -> CanExprOut
CanExprOut {
loc_expr,
output,
problems: env.problems,
home: env.home,
var_store,
interns,
var,
}
}

View file

@ -143,14 +143,37 @@ Defs {
ident: "i",
},
],
@113-189 Defs(
Defs {
tags: [
EitherIndex(2147483648),
],
regions: [
@132-172,
],
space_before: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
space_after: [
Slice<roc_parse::ast::CommentOrNewline> { start: 0, length: 0 },
],
spaces: [],
type_defs: [],
value_defs: [
Body(
@113-117 Identifier {
ident: "newi",
},
@132-172 LowLevelTry(
@132-172 Apply(
@132-172 Var {
module_name: "Result",
ident: "try",
@169-172 Var {
module_name: "",
ident: "inc",
},
[
@132-152 LowLevelTry(
@132-152 Apply(
@132-152 Var {
@149-152 Var {
module_name: "",
ident: "inc",
},
@ -160,43 +183,18 @@ Defs {
ident: "i",
},
],
BinOp(
Pizza,
Try,
),
OperatorSuffix,
),
@132-172 Closure(
[
@132-152 Identifier {
ident: "#!0_arg",
},
],
@132-172 Apply(
@132-172 Var {
module_name: "Result",
ident: "try",
},
[
@132-172 Apply(
@132-172 Var {
module_name: "",
ident: "inc",
},
[
@132-152 Var {
module_name: "",
ident: "#!0_arg",
},
],
BinOp(
Pizza,
Try,
),
OperatorSuffix,
),
),
@132-172 Closure(
[
@113-117 Identifier {
ident: "newi",
},
],
},
@182-189 Apply(
@182-184 Tag(
"Ok",
@ -210,13 +208,6 @@ Defs {
Space,
),
),
],
QuestionSuffix,
),
),
],
QuestionSuffix,
),
),
),
Body(

View file

@ -802,6 +802,106 @@ mod test_can {
}
}
#[test]
fn question_suffix_simple() {
let src = indoc!(
r#"
(Str.toU64 "123")?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Space, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_after_function() {
let src = indoc!(
r#"
Str.toU64? "123"
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_pipe() {
let src = indoc!(
r#"
"123" |> Str.toU64?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123")
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
}
#[test]
fn question_suffix_pipe_nested() {
let src = indoc!(
r#"
"123" |> Str.toU64? (Ok 123)?
"#
);
let arena = Bump::new();
let out = can_expr_with(&arena, test_home(), src);
assert_eq!(out.problems, Vec::new());
// Assert that we desugar to:
//
// Try(Str.toU64 "123" Try(Ok 123))
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 2);
assert_str_value(&cond_args[0].1.value, "123");
let ok_tag = assert_try_expr(&cond_args[1].1.value);
let tag_args = assert_tag_application(ok_tag, "Ok");
assert_eq!(tag_args.len(), 1);
assert_num_value(&tag_args[0].1.value, 123);
}
#[test]
fn try_desugar_plain_prefix() {
let src = indoc!(
@ -816,64 +916,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -890,64 +939,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Try, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -964,64 +962,13 @@ mod test_can {
// Assert that we desugar to:
//
// when Str.toU64 "123" is
// Ok `0` -> `0`
// Err `1` -> return Err `1`
// Try(Str.toU64 "123")
let (cond_expr, branches) = assert_when(&out.loc_expr.value);
let cond_expr = assert_try_expr(&out.loc_expr.value);
let cond_args = assert_func_call(cond_expr, "toU64", CalledVia::Space, &out.interns);
assert_eq!(cond_args.len(), 1);
assert_str_value(&cond_args[0].1.value, "123");
assert_eq!(branches.len(), 2);
assert_eq!(branches[0].patterns.len(), 1);
assert!(!branches[0].patterns[0].degenerate);
match &branches[0].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Ok");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "0", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
assert!(&branches[0].guard.is_none());
assert_var_usage(&branches[0].value.value, "0", &out.interns);
assert_eq!(branches[1].patterns.len(), 1);
assert!(!branches[1].patterns[0].degenerate);
match &branches[1].patterns[0].pattern.value {
Pattern::AppliedTag {
tag_name,
arguments,
..
} => {
assert_eq!(tag_name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_pattern_name(&arguments[0].1.value, "1", &out.interns);
}
other => panic!("First argument was not an applied tag: {:?}", other),
}
match &branches[1].value.value {
Expr::Return { return_value, .. } => match &return_value.value {
Expr::Tag {
name, arguments, ..
} => {
assert_eq!(name.0.to_string(), "Err");
assert_eq!(arguments.len(), 1);
assert_var_usage(&arguments[0].1.value, "1", &out.interns);
}
other_inner => panic!("Expr was not a Tag: {:?}", other_inner),
},
other_outer => panic!("Expr was not a Return: {:?}", other_outer),
}
}
#[test]
@ -1126,6 +1073,16 @@ mod test_can {
}
}
fn assert_tag_application(expr: &Expr, tag_name: &str) -> Vec<(Variable, Loc<Expr>)> {
match expr {
Expr::LetNonRec(_, loc_expr) => assert_tag_application(&loc_expr.value, tag_name),
Expr::Tag {
name, arguments, ..
} if *name == tag_name.into() => arguments.clone(),
_ => panic!("Expr was not a Tag named {tag_name:?}: {expr:?}",),
}
}
fn assert_pattern_name(pattern: &Pattern, name: &str, interns: &roc_module::symbol::Interns) {
match pattern {
Pattern::Identifier(sym) => assert_eq!(sym.as_str(interns), name),
@ -1142,6 +1099,13 @@ mod test_can {
}
}
fn assert_try_expr(expr: &Expr) -> &Expr {
match expr {
Expr::Try { result_expr, .. } => &result_expr.value,
_ => panic!("Expr was not a Try: {:?}", expr),
}
}
// TAIL CALLS
fn get_closure(expr: &Expr, i: usize) -> roc_can::expr::Recursive {
match expr {

View file

@ -3,10 +3,6 @@ use std::collections::HashMap;
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Serialize;
#[derive(Serialize, JsonSchema, Debug)]
#[serde(tag = "type")]
pub enum Constraint {}
#[derive(Serialize, JsonSchema, Debug, PartialEq)]
pub struct Variable(pub u32);
@ -173,12 +169,6 @@ pub enum NumericRangeKind {
#[derive(Serialize, JsonSchema, Debug)]
pub struct Rank(pub u32);
#[derive(Serialize, JsonSchema, Debug)]
pub struct Descriptor {
pub content: Content,
pub rank: Rank,
}
#[derive(Serialize, JsonSchema, Debug)]
pub struct Symbol(
// TODO: should this be module ID + symbol?

View file

@ -50,7 +50,6 @@ impl ReferenceMatrix {
//
// Thank you, Samuel!
impl ReferenceMatrix {
#[allow(dead_code)]
pub fn topological_sort_into_groups(&self) -> TopologicalSort {
if self.length == 0 {
return TopologicalSort::Groups { groups: Vec::new() };

View file

@ -30,8 +30,8 @@ use roc_region::all::{Loc, Region};
use roc_types::subs::{IllegalCycleMark, Variable};
use roc_types::types::Type::{self, *};
use roc_types::types::{
AliasKind, AnnotationSource, Category, IndexOrField, OptAbleType, PReason, Reason, RecordField,
TypeExtension, TypeTag, Types,
AliasKind, AnnotationSource, Category, EarlyReturnKind, IndexOrField, OptAbleType, PReason,
Reason, RecordField, TypeExtension, TypeTag, Types,
};
use soa::{Index, Slice};
@ -152,7 +152,7 @@ fn constrain_untyped_closure(
closure_var: Variable,
ret_var: Variable,
fx_var: Variable,
early_returns: &[(Variable, Region)],
early_returns: &[(Variable, Region, EarlyReturnKind)],
arguments: &[(Variable, AnnotatedMark, Loc<Pattern>)],
loc_body_expr: &Loc<Expr>,
captured_symbols: &[(Symbol, Variable)],
@ -184,30 +184,16 @@ fn constrain_untyped_closure(
));
let returns_constraint = env.with_fx_expectation(fx_var, None, |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_index,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_index,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
constraints.and_constraint(return_constraints)
ret_var,
false,
)
});
// make sure the captured symbols are sorted!
@ -259,6 +245,51 @@ fn constrain_untyped_closure(
constraints.exists_many(vars, cons)
}
pub fn constrain_function_return(
types: &mut Types,
constraints: &mut Constraints,
env: &mut Env,
body_expr: &Loc<Expr>,
early_returns: &[(Variable, Region, EarlyReturnKind)],
return_type_expected: ExpectedTypeIndex,
ret_var: Variable,
should_attach_res_constraints: bool,
) -> Constraint {
let return_con = constrain_expr(
types,
constraints,
env,
body_expr.region,
&body_expr.value,
return_type_expected,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
let mut return_type_vars = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
return_type_vars.push(ret_var);
for (early_return_variable, early_return_region, early_return_kind) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return(*early_return_kind),
*early_return_region,
);
return_constraints.push(early_return_con);
return_type_vars.push(*early_return_variable);
}
let returns_constraint = constraints.exists_many(return_type_vars, return_constraints);
if should_attach_res_constraints {
attach_resolution_constraints(constraints, env, returns_constraint)
} else {
returns_constraint
}
}
pub fn constrain_expr(
types: &mut Types,
constraints: &mut Constraints,
@ -576,7 +607,6 @@ pub fn constrain_expr(
let mut arg_cons = Vec::with_capacity(loc_args.len());
for (index, (arg_var, loc_arg)) in loc_args.iter().enumerate() {
let region = loc_arg.region;
let arg_type = Variable(*arg_var);
let arg_type_index = constraints.push_variable(*arg_var);
@ -833,6 +863,80 @@ pub fn constrain_expr(
constraints.exists_many([*variable], [message_con, continuation_con])
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind,
} => {
let result_var_index = constraints.push_variable(*result_var);
let result_expected_type = constraints.push_expected_type(ForReason(
Reason::TryResult,
result_var_index,
result_expr.region,
));
let result_constraint = constrain_expr(
types,
constraints,
env,
result_expr.region,
&result_expr.value,
result_expected_type,
);
let try_target_constraint = constraints.try_target(
result_var_index,
*ok_payload_var,
*err_payload_var,
result_expr.region,
*kind,
);
let return_type_index = constraints.push_variable(*return_var);
let expected_return_value = constraints.push_expected_type(ForReason(
Reason::TryResult,
return_type_index,
result_expr.region,
));
let try_failure_type_index = {
let typ = types.from_old_type(&Type::TagUnion(
vec![("Err".into(), vec![Type::Variable(*err_payload_var)])],
TypeExtension::from_non_annotation_type(Type::Variable(*err_ext_var)),
));
constraints.push_type(types, typ)
};
let try_failure_constraint = constraints.equal_types(
try_failure_type_index,
expected_return_value,
Category::TryFailure,
region,
);
let ok_type_index = constraints.push_variable(*ok_payload_var);
let try_success_constraint =
constraints.equal_types(ok_type_index, expected, Category::TrySuccess, region);
constraints.exists_many(
[
*return_var,
*result_var,
*ok_payload_var,
*err_payload_var,
*err_ext_var,
],
[
result_constraint,
try_target_constraint,
try_failure_constraint,
try_success_constraint,
],
)
}
If {
cond_var,
branch_var,
@ -1018,8 +1122,7 @@ pub fn constrain_expr(
Region::span_across(&loc_cond.region, &branches.last().unwrap().value.region)
};
let branch_expr_reason =
|expected: &Expected<TypeOrVar>, index, branch_region| match expected {
let branch_expr_reason = |expected: &Expected<TypeOrVar>, index| match expected {
FromAnnotation(name, arity, ann_source, _typ) => {
// NOTE deviation from elm.
//
@ -1036,7 +1139,7 @@ pub fn constrain_expr(
)
}
_ => ForReason(Reason::WhenBranch { index }, body_type_index, branch_region),
_ => ForReason(Reason::WhenBranch { index }, body_type_index, region),
};
// Our goal is to constrain and introduce variables in all pattern when branch patterns before
@ -1091,11 +1194,7 @@ pub fn constrain_expr(
region,
when_branch,
expected_pattern,
branch_expr_reason(
&constraints[expected],
HumanIndex::zero_based(index),
when_branch.value.region,
),
branch_expr_reason(&constraints[expected], HumanIndex::zero_based(index)),
);
pattern_vars.extend(new_pattern_vars);
@ -1438,14 +1537,16 @@ pub fn constrain_expr(
return_value.region,
));
constrain_expr(
let return_con = constrain_expr(
types,
constraints,
env,
return_value.region,
&return_value.value,
expected_return_value,
)
);
constraints.exists([*return_var], return_con)
}
Tag {
tag_union_var: variant_var,
@ -2072,43 +2173,19 @@ fn constrain_function_def(
constraints.push_type(types, fn_type)
};
let returns_constraint = {
let return_con = constrain_expr(
let returns_constraint =
env.with_fx_expectation(function_def.fx_type, Some(annotation.region), |env| {
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
&function_def.early_returns,
return_type_annotation_expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns {
let early_return_type_expected =
constraints.push_expected_type(Expected::ForReason(
Reason::FunctionOutput,
ret_type_index,
*early_return_region,
));
vars.push(*early_return_variable);
let early_return_con = constraints.equal_types_var(
*early_return_variable,
early_return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
};
function_def.return_type,
true,
)
});
vars.push(expr_var);
@ -2457,7 +2534,7 @@ fn constrain_when_branch_help(
types,
constraints,
env,
region,
when_branch.value.region,
&when_branch.value.value,
expr_expected,
);
@ -2964,32 +3041,16 @@ fn constrain_typed_def(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type,
);
let mut return_constraints = Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);
@ -3435,12 +3496,18 @@ fn constrain_let_def(
)
});
// Ignored def must be effectful, otherwise it's dead code
let effectful_constraint = Constraint::ExpectEffectful(
let effectful_constraint = if def.loc_expr.value.contains_any_early_returns() {
// If the statement has early returns, it doesn't need to be effectful to
// potentially affect the output of the containing function
Constraint::True
} else {
// If there are no early returns, it must be effectful or else it's dead code
Constraint::ExpectEffectful(
fx_var,
ExpectEffectfulReason::Ignored,
def.loc_pattern.region,
);
)
};
let enclosing_fx_constraint = constraints.fx_call(
fx_var,
@ -3527,9 +3594,14 @@ fn constrain_stmt_def(
generalizable,
);
// Stmt expr must be effectful, otherwise it's dead code
let effectful_constraint =
Constraint::ExpectEffectful(fx_var, ExpectEffectfulReason::Stmt, region);
let effectful_constraint = if def.loc_expr.value.contains_any_early_returns() {
// If the statement has early returns, it doesn't need to be effectful to
// potentially affect the output of the containing function
Constraint::True
} else {
// If there are no early returns, it must be effectful or else it's dead code
Constraint::ExpectEffectful(fx_var, ExpectEffectfulReason::Stmt, region)
};
let fx_call_kind = match fn_name {
None => FxCallKind::Stmt,
@ -4012,35 +4084,22 @@ fn constraint_recursive_function(
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let expected = constraints.push_expected_type(NoExpectation(ret_type_index));
let return_con = constrain_expr(
let expected = constraints.push_expected_type(ForReason(
Reason::FunctionOutput,
ret_type_index,
region,
));
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
&function_def.early_returns,
expected,
);
let mut return_constraints =
Vec::with_capacity(function_def.early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in &function_def.early_returns
{
let early_return_con = constraints.equal_types_var(
*early_return_variable,
expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint = constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(expr_var);
@ -4393,6 +4452,7 @@ fn is_generalizable_expr(mut expr: &Expr) -> bool {
| RecordUpdate { .. }
| Expect { .. }
| Dbg { .. }
| Try { .. }
| Return { .. }
| RuntimeError(..)
| ZeroArgumentTag { .. }
@ -4584,37 +4644,20 @@ fn rec_defs_help(
};
let returns_constraint =
env.with_fx_expectation(fx_var, Some(annotation.region), |env| {
let return_type_expected =
constraints.push_expected_type(NoExpectation(ret_type_index));
let return_type_expected = constraints.push_expected_type(
ForReason(Reason::FunctionOutput, ret_type_index, region),
);
let return_con = constrain_expr(
constrain_function_return(
types,
constraints,
env,
loc_body_expr.region,
&loc_body_expr.value,
loc_body_expr,
early_returns,
return_type_expected,
);
let mut return_constraints =
Vec::with_capacity(early_returns.len() + 1);
return_constraints.push(return_con);
for (early_return_variable, early_return_region) in early_returns {
let early_return_con = constraints.equal_types_var(
*early_return_variable,
return_type_expected,
Category::Return,
*early_return_region,
);
return_constraints.push(early_return_con);
}
let returns_constraint =
constraints.and_constraint(return_constraints);
attach_resolution_constraints(constraints, env, returns_constraint)
ret_var,
true,
)
});
vars.push(*fn_var);

View file

@ -15,3 +15,4 @@ roc_region.workspace = true
roc_error_macros.workspace = true
bumpalo.workspace = true
soa.workspace = true

View file

@ -1,11 +1,15 @@
use crate::{
collection::{fmt_collection, Braces},
expr::merge_spaces_conservative,
pattern::pattern_lift_spaces_after,
spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT},
Buf,
};
use bumpalo::{collections::Vec, Bump};
use roc_parse::ast::{
AbilityImpls, AssignedField, Collection, Expr, ExtractSpaces, FunctionArrow,
ImplementsAbilities, ImplementsAbility, ImplementsClause, Tag, TypeAnnotation, TypeHeader,
AbilityImpls, AssignedField, Collection, CommentOrNewline, Expr, ExtractSpaces, FunctionArrow,
ImplementsAbilities, ImplementsAbility, ImplementsClause, Spaceable, Spaces, SpacesAfter,
SpacesBefore, Tag, TypeAnnotation, TypeHeader,
};
use roc_parse::ident::UppercaseIdent;
use roc_region::all::Loc;
@ -35,10 +39,12 @@ use roc_region::all::Loc;
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Parens {
NotNeeded,
InCollection,
InFunctionType,
InApply,
InOperator,
InAsPattern,
InApplyLastArg,
}
/// In an AST node, do we show newlines around it
@ -148,7 +154,10 @@ impl<'a> Formattable for TypeAnnotation<'a> {
true
}
Wildcard | Inferred | BoundVariable(_) | Malformed(_) => false,
TypeAnnotation::Wildcard
| TypeAnnotation::Inferred
| BoundVariable(_)
| Malformed(_) => false,
Function(args, _arrow, result) => {
result.value.is_multiline()
|| args.iter().any(|loc_arg| loc_arg.value.is_multiline())
@ -165,8 +174,7 @@ impl<'a> Formattable for TypeAnnotation<'a> {
Some(ann) if ann.value.is_multiline() => return true,
_ => {}
}
fields.items.iter().any(|field| field.value.is_multiline())
is_collection_multiline(fields)
}
Record { fields, ext } => {
@ -175,7 +183,7 @@ impl<'a> Formattable for TypeAnnotation<'a> {
_ => {}
}
fields.items.iter().any(|field| field.value.is_multiline())
is_collection_multiline(fields)
}
TagUnion { tags, ext } => {
@ -184,18 +192,42 @@ impl<'a> Formattable for TypeAnnotation<'a> {
_ => {}
}
tags.iter().any(|tag| tag.value.is_multiline())
!tags.final_comments().is_empty() || tags.iter().any(|tag| tag.value.is_multiline())
}
}
}
fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) {
use roc_parse::ast::TypeAnnotation::*;
fmt_ty_ann(self, buf, indent, parens, newlines, false);
}
}
let self_is_multiline = self.is_multiline();
fn fmt_ty_ann(
me: &TypeAnnotation<'_>,
buf: &mut Buf<'_>,
match self {
Function(args, arrow, ret) => {
indent: u16,
parens: Parens,
newlines: Newlines,
newline_at_top: bool,
) {
let me = ann_lift_spaces(buf.text.bump(), me);
let self_is_multiline = me.item.is_multiline();
if !me.before.is_empty() {
buf.ensure_ends_with_newline();
fmt_comments_only(buf, me.before.iter(), NewlineAt::Bottom, indent);
}
if newline_at_top {
buf.ensure_ends_with_newline();
}
match &me.item {
TypeAnnotation::SpaceBefore(_ann, _spaces) | TypeAnnotation::SpaceAfter(_ann, _spaces) => {
unreachable!()
}
TypeAnnotation::Function(args, arrow, ret) => {
let needs_parens = parens != Parens::NotNeeded;
buf.indent(indent);
@ -204,29 +236,27 @@ impl<'a> Formattable for TypeAnnotation<'a> {
buf.push('(')
}
let mut it = args.iter().enumerate().peekable();
while let Some((index, argument)) = it.next() {
for (index, argument) in args.iter().enumerate() {
let is_first = index == 0;
let is_multiline = &argument.value.is_multiline();
if !is_first && !is_multiline && self_is_multiline {
buf.newline();
}
argument.value.format_with_options(
buf,
Parens::InFunctionType,
Newlines::Yes,
indent,
);
if it.peek().is_some() {
if !is_first {
buf.indent(indent);
buf.push_str(",");
if !self_is_multiline {
buf.spaces(1);
}
}
let newline_at_top = !is_first && self_is_multiline;
fmt_ty_ann(
&argument.value,
buf,
indent,
Parens::InFunctionType,
Newlines::Yes,
newline_at_top,
);
}
if self_is_multiline {
@ -250,7 +280,7 @@ impl<'a> Formattable for TypeAnnotation<'a> {
buf.push(')')
}
}
Apply(pkg, name, arguments) => {
TypeAnnotation::Apply(pkg, name, arguments) => {
buf.indent(indent);
let write_parens = parens == Parens::InApply && !arguments.is_empty();
@ -271,7 +301,7 @@ impl<'a> Formattable for TypeAnnotation<'a> {
.map(|a| {
a.is_multiline()
&& (!a.extract_spaces().before.is_empty()
|| !is_outdentable(&a.value))
|| !ty_is_outdentable(&a.value))
})
.unwrap_or_default();
@ -286,12 +316,8 @@ impl<'a> Formattable for TypeAnnotation<'a> {
let arg = arg.extract_spaces();
fmt_spaces(buf, arg.before.iter(), arg_indent);
buf.ensure_ends_with_newline();
arg.item.format_with_options(
buf,
Parens::InApply,
Newlines::Yes,
arg_indent,
);
arg.item
.format_with_options(buf, Parens::InApply, Newlines::Yes, arg_indent);
fmt_spaces(buf, arg.after.iter(), arg_indent);
} else {
buf.spaces(1);
@ -303,44 +329,35 @@ impl<'a> Formattable for TypeAnnotation<'a> {
buf.push(')')
}
}
BoundVariable(v) => {
TypeAnnotation::BoundVariable(v) => {
buf.indent(indent);
buf.push_str(v)
}
Wildcard => {
TypeAnnotation::Wildcard => {
buf.indent(indent);
buf.push('*')
}
Inferred => {
TypeAnnotation::Inferred => {
buf.indent(indent);
buf.push('_')
}
TagUnion { tags, ext } => {
TypeAnnotation::TagUnion { tags, ext } => {
fmt_collection(buf, indent, Braces::Square, *tags, newlines);
if let Some(loc_ext_ann) = *ext {
loc_ext_ann.value.format(buf, indent);
}
fmt_ext(ext, buf, indent);
}
Tuple { elems: fields, ext } => {
fmt_collection(buf, indent, Braces::Round, *fields, newlines);
if let Some(loc_ext_ann) = *ext {
loc_ext_ann.value.format(buf, indent);
}
TypeAnnotation::Tuple { elems: fields, ext } => {
fmt_ty_collection(buf, indent, Braces::Round, *fields, newlines);
fmt_ext(ext, buf, indent);
}
Record { fields, ext } => {
TypeAnnotation::Record { fields, ext } => {
fmt_collection(buf, indent, Braces::Curly, *fields, newlines);
if let Some(loc_ext_ann) = *ext {
loc_ext_ann.value.format(buf, indent);
}
fmt_ext(ext, buf, indent);
}
As(lhs, _spaces, TypeHeader { name, vars }) => {
TypeAnnotation::As(lhs, _spaces, TypeHeader { name, vars }) => {
// TODO use _spaces?
lhs.value
.format_with_options(buf, Parens::InFunctionType, Newlines::No, indent);
@ -355,7 +372,7 @@ impl<'a> Formattable for TypeAnnotation<'a> {
}
}
Where(annot, implements_clauses) => {
TypeAnnotation::Where(annot, implements_clauses) => {
annot.format_with_options(buf, parens, newlines, indent);
if implements_clauses
.iter()
@ -376,31 +393,137 @@ impl<'a> Formattable for TypeAnnotation<'a> {
has.format_with_options(buf, parens, newlines, indent);
}
}
SpaceBefore(ann, spaces) => {
buf.ensure_ends_with_newline();
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
ann.format_with_options(buf, parens, newlines, indent)
}
SpaceAfter(ann, spaces) => {
ann.format_with_options(buf, parens, newlines, indent);
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
}
Malformed(raw) => {
TypeAnnotation::Malformed(raw) => {
buf.indent(indent);
buf.push_str(raw)
}
}
if !me.after.is_empty() {
fmt_comments_only(buf, me.after.iter(), NewlineAt::Bottom, indent);
}
}
fn is_outdentable(ann: &TypeAnnotation) -> bool {
matches!(
ann.extract_spaces().item,
TypeAnnotation::Tuple { .. } | TypeAnnotation::Record { .. }
fn lower<'a, 'b: 'a>(
arena: &'b Bump,
lifted: Spaces<'b, TypeAnnotation<'b>>,
) -> TypeAnnotation<'b> {
if lifted.before.is_empty() && lifted.after.is_empty() {
return lifted.item;
}
if lifted.before.is_empty() {
return TypeAnnotation::SpaceAfter(arena.alloc(lifted.item), lifted.after);
}
if lifted.after.is_empty() {
return TypeAnnotation::SpaceBefore(arena.alloc(lifted.item), lifted.before);
}
TypeAnnotation::SpaceBefore(
arena.alloc(TypeAnnotation::SpaceAfter(
arena.alloc(lifted.item),
lifted.after,
)),
lifted.before,
)
}
fn fmt_ty_collection(
buf: &mut Buf<'_>,
indent: u16,
braces: Braces,
items: Collection<'_, Loc<TypeAnnotation<'_>>>,
newlines: Newlines,
) {
let arena = buf.text.bump();
let mut new_items: Vec<'_, NodeSpaces<'_, Node<'_>>> =
Vec::with_capacity_in(items.len(), arena);
let mut last_after: &[CommentOrNewline<'_>] = &[];
for (i, item) in items.items.iter().enumerate() {
let func_ty_risky = i > 0;
let lifted = ann_lift_to_node(
if func_ty_risky {
Parens::InCollection
} else {
Parens::NotNeeded
},
arena,
&item.value,
);
let before = merge_spaces_conservative(arena, last_after, lifted.before);
last_after = lifted.after;
new_items.push(NodeSpaces {
before,
item: lifted.item,
after: &[],
});
}
let final_comments = merge_spaces_conservative(arena, last_after, items.final_comments());
let new_items =
Collection::with_items_and_comments(arena, new_items.into_bump_slice(), final_comments);
fmt_collection(buf, indent, braces, new_items, newlines)
}
fn fmt_ext(ext: &Option<&Loc<TypeAnnotation<'_>>>, buf: &mut Buf<'_>, indent: u16) {
if let Some(loc_ext_ann) = *ext {
let me = ann_lift_spaces(buf.text.bump(), &loc_ext_ann.value);
let parens_needed = !me.before.is_empty() || ext_needs_parens(me.item);
if parens_needed {
// We need to make sure to not have whitespace before the ext of a type,
// since that would make it parse as something else.
buf.push('(');
loc_ext_ann.value.format(buf, indent + INDENT);
buf.indent(indent);
buf.push(')');
} else {
loc_ext_ann.value.format(buf, indent + INDENT);
}
}
}
fn ext_needs_parens(item: TypeAnnotation<'_>) -> bool {
match item {
TypeAnnotation::Record { .. }
| TypeAnnotation::TagUnion { .. }
| TypeAnnotation::Tuple { .. }
| TypeAnnotation::BoundVariable(..)
| TypeAnnotation::Wildcard
| TypeAnnotation::Inferred => false,
TypeAnnotation::Apply(_module, _func, args) => !args.is_empty(),
_ => true,
}
}
pub fn ty_is_outdentable(mut rhs: &TypeAnnotation) -> bool {
loop {
match rhs {
TypeAnnotation::SpaceBefore(sub_def, spaces) => {
let is_only_newlines = spaces.iter().all(|s| s.is_newline());
if !is_only_newlines || !sub_def.is_multiline() {
return false;
}
rhs = sub_def;
}
TypeAnnotation::SpaceAfter(sub_def, _) => {
rhs = sub_def;
}
TypeAnnotation::Where(ann, _clauses) => {
if !ann.is_multiline() {
return false;
}
rhs = &ann.value;
}
TypeAnnotation::Record { .. }
| TypeAnnotation::TagUnion { .. }
| TypeAnnotation::Tuple { .. } => return rhs.is_multiline(),
_ => return false,
}
}
}
/// Fields are subtly different on the type and term level:
///
/// > type: { x : Int, y : Bool }
@ -444,6 +567,7 @@ fn is_multiline_assigned_field_help<T: Formattable>(afield: &AssignedField<'_, T
fn format_assigned_field_help<T>(
zelf: &AssignedField<T>,
buf: &mut Buf,
indent: u16,
separator_spaces: usize,
is_multiline: bool,
@ -466,6 +590,7 @@ fn format_assigned_field_help<T>(
}
buf.spaces(separator_spaces);
buf.indent(indent);
buf.push(':');
buf.spaces(1);
ann.value.format(buf, indent);
@ -473,9 +598,9 @@ fn format_assigned_field_help<T>(
OptionalValue(name, spaces, ann) => {
if is_multiline {
buf.newline();
buf.indent(indent);
}
buf.indent(indent);
buf.push_str(name.value);
if !spaces.is_empty() {
@ -483,6 +608,7 @@ fn format_assigned_field_help<T>(
}
buf.spaces(separator_spaces);
buf.indent(indent);
buf.push('?');
buf.spaces(1);
ann.value.format(buf, indent);
@ -501,6 +627,7 @@ fn format_assigned_field_help<T>(
}
buf.spaces(separator_spaces);
buf.indent(indent);
buf.push(':');
buf.spaces(1);
ann.value.format(buf, indent);
@ -508,9 +635,9 @@ fn format_assigned_field_help<T>(
LabelOnly(name) => {
if is_multiline {
buf.newline();
buf.indent(indent);
}
buf.indent(indent);
buf.push_str(name.value);
}
AssignedField::SpaceBefore(sub_field, spaces) => {
@ -539,6 +666,7 @@ impl<'a> Formattable for Tag<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let is_multiline = self.is_multiline();
@ -704,3 +832,432 @@ pub fn except_last<T>(items: &[T]) -> impl Iterator<Item = &T> {
items[..items.len() - 1].iter()
}
}
pub fn ann_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
ann: &TypeAnnotation<'b>,
) -> Spaces<'a, TypeAnnotation<'a>> {
match ann {
TypeAnnotation::Apply(module, func, args) => {
if args.is_empty() {
return Spaces {
item: *ann,
before: &[],
after: &[],
};
}
let mut new_args = Vec::with_capacity_in(args.len(), arena);
if !args.is_empty() {
for arg in args.iter().take(args.len() - 1) {
let lifted = ann_lift_spaces(arena, &arg.value);
new_args.push(Loc::at(arg.region, lower(arena, lifted)));
}
}
let after = if let Some(last) = args.last() {
let lifted = ann_lift_spaces(arena, &last.value);
if lifted.before.is_empty() {
new_args.push(Loc::at(last.region, lifted.item));
} else {
new_args.push(Loc::at(
last.region,
TypeAnnotation::SpaceBefore(arena.alloc(lifted.item), lifted.before),
));
}
lifted.after
} else {
&[]
};
Spaces {
before: &[],
item: TypeAnnotation::Apply(module, func, new_args.into_bump_slice()),
after,
}
}
TypeAnnotation::Function(args, purity, res) => {
let new_args = arena.alloc_slice_copy(args);
let before = if let Some(first) = new_args.first_mut() {
let lifted = ann_lift_spaces_before(arena, &first.value);
first.value = lifted.item;
lifted.before
} else {
&[]
};
let new_res = ann_lift_spaces_after(arena, &res.value);
let new_ann = TypeAnnotation::Function(
new_args,
*purity,
arena.alloc(Loc::at_zero(new_res.item)),
);
Spaces {
before,
item: new_ann,
after: new_res.after,
}
}
TypeAnnotation::SpaceBefore(expr, spaces) => {
let mut inner = ann_lift_spaces(arena, expr);
inner.before = merge_spaces_conservative(arena, spaces, inner.before);
inner
}
TypeAnnotation::SpaceAfter(expr, spaces) => {
let mut inner = ann_lift_spaces(arena, expr);
inner.after = merge_spaces_conservative(arena, inner.after, spaces);
inner
}
TypeAnnotation::Tuple { elems, ext } => {
if let Some(ext) = ext {
let lifted = ann_lift_spaces_after(arena, &ext.value);
Spaces {
before: &[],
item: TypeAnnotation::Tuple {
elems: *elems,
ext: Some(arena.alloc(Loc::at_zero(lifted.item))),
},
after: lifted.after,
}
} else {
Spaces {
before: &[],
item: *ann,
after: &[],
}
}
}
TypeAnnotation::Record { fields, ext } => {
if let Some(ext) = ext {
let lifted = ann_lift_spaces_after(arena, &ext.value);
Spaces {
before: &[],
item: TypeAnnotation::Record {
fields: *fields,
ext: Some(arena.alloc(Loc::at_zero(lifted.item))),
},
after: lifted.after,
}
} else {
Spaces {
before: &[],
item: *ann,
after: &[],
}
}
}
TypeAnnotation::TagUnion { ext, tags } => {
if let Some(ext) = ext {
let lifted = ann_lift_spaces_after(arena, &ext.value);
Spaces {
before: &[],
item: TypeAnnotation::TagUnion {
ext: Some(arena.alloc(Loc::at_zero(lifted.item))),
tags: *tags,
},
after: lifted.after,
}
} else {
Spaces {
before: &[],
item: *ann,
after: &[],
}
}
}
TypeAnnotation::BoundVariable(_)
| TypeAnnotation::Inferred
| TypeAnnotation::Wildcard
| TypeAnnotation::Malformed(_) => Spaces {
before: &[],
item: *ann,
after: &[],
},
TypeAnnotation::Where(inner, clauses) => {
let new_inner = ann_lift_spaces_before(arena, &inner.value);
let new_clauses = arena.alloc_slice_copy(clauses);
let after = if let Some(last) = new_clauses.last_mut() {
let lifted = implements_clause_lift_spaces_after(arena, &last.value);
last.value = lifted.item;
lifted.after
} else {
&[]
};
Spaces {
before: new_inner.before,
item: TypeAnnotation::Where(arena.alloc(Loc::at_zero(new_inner.item)), new_clauses),
after,
}
}
TypeAnnotation::As(ann, comments, type_header) => {
let new_ann = ann_lift_spaces_before(arena, &ann.value);
let new_header = type_head_lift_spaces_after(arena, type_header);
Spaces {
before: new_ann.before,
item: TypeAnnotation::As(
arena.alloc(Loc::at_zero(new_ann.item)),
comments,
new_header.item,
),
after: new_header.after,
}
}
}
}
fn implements_clause_lift_spaces_after<'a, 'b: 'a>(
arena: &'a Bump,
value: &ImplementsClause<'b>,
) -> SpacesAfter<'a, ImplementsClause<'a>> {
let new_abilities = arena.alloc_slice_copy(value.abilities);
let after = if let Some(last) = new_abilities.last_mut() {
let lifted = ann_lift_spaces_after(arena, &last.value);
last.value = lifted.item;
lifted.after
} else {
&[]
};
SpacesAfter {
item: ImplementsClause {
var: value.var,
abilities: new_abilities,
},
after,
}
}
pub fn ann_lift_spaces_before<'a, 'b: 'a>(
arena: &'a Bump,
ann: &TypeAnnotation<'b>,
) -> SpacesBefore<'a, TypeAnnotation<'a>> {
let lifted = ann_lift_spaces(arena, ann);
SpacesBefore {
before: lifted.before,
item: lifted.item.maybe_after(arena, lifted.after),
}
}
pub fn ann_lift_spaces_after<'a, 'b: 'a>(
arena: &'a Bump,
ann: &TypeAnnotation<'b>,
) -> SpacesAfter<'a, TypeAnnotation<'a>> {
let lifted = ann_lift_spaces(arena, ann);
SpacesAfter {
item: lifted.item.maybe_before(arena, lifted.before),
after: lifted.after,
}
}
pub fn type_head_lift_spaces_after<'a, 'b: 'a>(
arena: &'a Bump,
header: &TypeHeader<'b>,
) -> SpacesAfter<'a, TypeHeader<'a>> {
let new_vars = arena.alloc_slice_copy(header.vars);
let after = if let Some(last) = new_vars.last_mut() {
let lifted = pattern_lift_spaces_after(arena, &last.value);
last.value = lifted.item;
lifted.after
} else {
&[]
};
SpacesAfter {
item: TypeHeader {
name: header.name,
vars: new_vars,
},
after,
}
}
type Sp<'a> = &'a [CommentOrNewline<'a>];
#[derive(Copy, Clone, Debug)]
enum Node<'a> {
DelimitedSequence(Braces, &'a [(Sp<'a>, Node<'a>)], Sp<'a>),
TypeAnnotation(TypeAnnotation<'a>),
}
impl<'a> Formattable for Node<'a> {
fn is_multiline(&self) -> bool {
match self {
Node::DelimitedSequence(_braces, lefts, right) => {
if !right.is_empty() {
return true;
}
for (sp, l) in *lefts {
if l.is_multiline() || !sp.is_empty() {
return true;
}
}
false
}
Node::TypeAnnotation(type_annotation) => type_annotation.is_multiline(),
}
}
fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) {
match self {
Node::DelimitedSequence(braces, lefts, right) => {
buf.indent(indent);
buf.push(braces.start());
for (sp, l) in *lefts {
if !sp.is_empty() {
fmt_spaces(buf, sp.iter(), indent);
}
l.format_with_options(buf, parens, newlines, indent);
}
if !right.is_empty() {
fmt_spaces(buf, right.iter(), indent);
}
buf.indent(indent);
buf.push(braces.end());
}
Node::TypeAnnotation(type_annotation) => {
type_annotation.format_with_options(buf, parens, newlines, indent);
}
}
}
}
fn ann_lift_to_node<'a, 'b: 'a>(
parens: Parens,
arena: &'a Bump,
ann: &TypeAnnotation<'b>,
) -> Spaces<'a, Node<'a>> {
match ann {
TypeAnnotation::Apply(module, func, args) => {
if args.is_empty() {
return Spaces {
item: Node::TypeAnnotation(*ann),
before: &[],
after: &[],
};
}
let mut new_args = Vec::with_capacity_in(args.len(), arena);
if !args.is_empty() {
for arg in args.iter().take(args.len() - 1) {
let lifted = ann_lift_spaces(arena, &arg.value);
new_args.push(Loc::at(arg.region, lower(arena, lifted)));
}
}
let after = if let Some(last) = args.last() {
let lifted = ann_lift_spaces(arena, &last.value);
if lifted.before.is_empty() {
new_args.push(Loc::at(last.region, lifted.item));
} else {
new_args.push(Loc::at(
last.region,
TypeAnnotation::SpaceBefore(arena.alloc(lifted.item), lifted.before),
));
}
lifted.after
} else {
&[]
};
Spaces {
before: &[],
item: Node::TypeAnnotation(TypeAnnotation::Apply(
module,
func,
new_args.into_bump_slice(),
)),
after,
}
}
TypeAnnotation::SpaceBefore(expr, spaces) => {
let mut inner = ann_lift_to_node(parens, arena, expr);
inner.before = merge_spaces_conservative(arena, spaces, inner.before);
inner
}
TypeAnnotation::SpaceAfter(expr, spaces) => {
let mut inner = ann_lift_to_node(parens, arena, expr);
inner.after = merge_spaces_conservative(arena, inner.after, spaces);
inner
}
TypeAnnotation::Function(args, purity, res) => {
let new_args = arena.alloc_slice_copy(args);
let before = if let Some(first) = new_args.first_mut() {
let lifted = ann_lift_spaces_before(arena, &first.value);
first.value = lifted.item;
lifted.before
} else {
&[]
};
let new_res = ann_lift_spaces_after(arena, &res.value);
let new_ann = TypeAnnotation::Function(
new_args,
*purity,
arena.alloc(Loc::at_zero(new_res.item)),
);
let inner = Spaces {
before,
item: Node::TypeAnnotation(new_ann),
after: new_res.after,
};
if parens == Parens::InCollection {
let node = Node::DelimitedSequence(
Braces::Round,
arena.alloc_slice_copy(&[(inner.before, inner.item)]),
inner.after,
);
Spaces {
before: &[],
item: node,
after: &[],
}
} else {
inner
}
}
_ => Spaces {
before: &[],
item: Node::TypeAnnotation(*ann),
after: &[],
},
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NodeSpaces<'a, T> {
pub before: &'a [CommentOrNewline<'a>],
pub item: T,
pub after: &'a [CommentOrNewline<'a>],
}
impl<'a, T: Copy> ExtractSpaces<'a> for NodeSpaces<'a, T> {
type Item = T;
fn extract_spaces(&self) -> Spaces<'a, T> {
Spaces {
before: self.before,
item: self.item,
after: self.after,
}
}
}
impl<'a, V: Formattable> Formattable for NodeSpaces<'a, V> {
fn is_multiline(&self) -> bool {
!self.before.is_empty() || !self.after.is_empty() || self.item.is_multiline()
}
fn format_with_options(
&self,
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
fmt_spaces(buf, self.before.iter(), indent);
self.item.format_with_options(buf, parens, newlines, indent);
fmt_spaces(buf, self.after.iter(), indent);
}
}

View file

@ -1,11 +1,13 @@
use roc_parse::ast::{Collection, CommentOrNewline, ExtractSpaces};
use roc_parse::{
ast::{Collection, CommentOrNewline, ExtractSpaces},
expr::merge_spaces,
};
use crate::{
annotation::{is_collection_multiline, Formattable, Newlines},
spaces::{fmt_comments_only, NewlineAt, INDENT},
Buf,
};
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Braces {
Round,
@ -13,26 +15,36 @@ pub enum Braces {
Curly,
}
pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
impl Braces {
pub fn start(self) -> char {
match self {
Braces::Round => '(',
Braces::Curly => '{',
Braces::Square => '[',
}
}
pub fn end(self) -> char {
match self {
Braces::Round => ')',
Braces::Curly => '}',
Braces::Square => ']',
}
}
}
pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable + std::fmt::Debug>(
buf: &mut Buf<'buf>,
indent: u16,
braces: Braces,
items: Collection<'a, T>,
newline: Newlines,
) where
<T as ExtractSpaces<'a>>::Item: Formattable,
<T as ExtractSpaces<'a>>::Item: Formattable + std::fmt::Debug,
{
let start = match braces {
Braces::Round => '(',
Braces::Curly => '{',
Braces::Square => '[',
};
let end = match braces {
Braces::Round => ')',
Braces::Curly => '}',
Braces::Square => ']',
};
let start = braces.start();
let end = braces.end();
if is_collection_multiline(&items) {
let braces_indent = indent;
@ -43,10 +55,21 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.indent(braces_indent);
buf.push(start);
let mut last_after: &[CommentOrNewline<'_>] = &[];
for (index, item) in items.iter().enumerate() {
let is_first_item = index == 0;
let item = item.extract_spaces();
let is_only_newlines = item.before.iter().all(|s| s.is_newline());
let last_after_was_only_newlines = last_after.iter().all(|s| s.is_newline());
if !last_after.is_empty() {
if last_after.iter().any(|s| s.is_newline()) {
buf.newline();
}
fmt_comments_only(buf, last_after.iter(), NewlineAt::None, item_indent);
}
if item.before.is_empty() || is_only_newlines {
buf.ensure_ends_with_newline();
@ -64,13 +87,14 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
if item
.before
.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
&& last_after_was_only_newlines
{
// If there's a comment, and it's not on the first item,
// and it's preceded by at least one blank line, maintain 1 blank line.
// (We already ensured that it ends in a newline, so this will turn that
// into a blank line.)
buf.newline();
buf.ensure_ends_with_blank_line();
}
}
@ -91,32 +115,30 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.indent(item_indent);
buf.push(',');
if !item.after.is_empty() {
if item.after.iter().any(|s| s.is_newline()) {
last_after = item.after;
}
let final_comments = if !last_after.is_empty() {
if last_after.iter().any(|s| s.is_newline()) {
buf.newline();
}
fmt_comments_only(buf, item.after.iter(), NewlineAt::None, item_indent);
}
}
merge_spaces(buf.text.bump(), last_after, items.final_comments())
} else {
if items.final_comments().iter().any(|s| s.is_newline()) {
buf.newline();
}
if items
.final_comments()
.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
items.final_comments()
};
if has_comments(final_comments)
&& final_comments.starts_with(&[CommentOrNewline::Newline, CommentOrNewline::Newline])
{
buf.newline();
buf.ensure_ends_with_blank_line();
}
fmt_comments_only(
buf,
items.final_comments().iter(),
NewlineAt::None,
item_indent,
);
fmt_comments_only(buf, final_comments.iter(), NewlineAt::None, item_indent);
buf.ensure_ends_with_newline();
buf.indent(braces_indent);
@ -144,3 +166,13 @@ pub fn fmt_collection<'a, 'buf, T: ExtractSpaces<'a> + Formattable>(
buf.push(end);
}
fn has_comments(spaces: &[CommentOrNewline<'_>]) -> bool {
for space in spaces {
match space {
CommentOrNewline::Newline => {}
CommentOrNewline::LineComment(_) | CommentOrNewline::DocComment(_) => return true,
}
}
false
}

View file

@ -1,15 +1,27 @@
use crate::annotation::{is_collection_multiline, Formattable, Newlines, Parens};
use crate::annotation::{
ann_lift_spaces, ann_lift_spaces_after, is_collection_multiline, ty_is_outdentable,
Formattable, Newlines, Parens,
};
use crate::collection::{fmt_collection, Braces};
use crate::expr::fmt_str_literal;
use crate::pattern::fmt_pattern;
use crate::spaces::{fmt_default_newline, fmt_default_spaces, fmt_spaces, INDENT};
use crate::expr::{
expr_lift_and_lower, expr_lift_spaces, expr_lift_spaces_after, expr_lift_spaces_before,
fmt_str_literal, is_str_multiline, sub_expr_requests_parens,
};
use crate::pattern::{fmt_pattern, pattern_lift_spaces};
use crate::pattern::{pattern_lift_spaces_before, starts_with_inline_comment};
use crate::spaces::{
fmt_comments_only, fmt_default_newline, fmt_default_spaces, fmt_spaces, NewlineAt, INDENT,
};
use crate::Buf;
use bumpalo::Bump;
use roc_error_macros::internal_error;
use roc_parse::ast::{
AbilityMember, Defs, Expr, ExtractSpaces, ImportAlias, ImportAsKeyword, ImportExposingKeyword,
ImportedModuleName, IngestedFileAnnotation, IngestedFileImport, ModuleImport,
ModuleImportParams, Pattern, Spaces, StrLiteral, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
AbilityMember, CommentOrNewline, Defs, Expr, ExtractSpaces, ImportAlias, ImportAsKeyword,
ImportExposingKeyword, ImportedModuleName, IngestedFileAnnotation, IngestedFileImport,
ModuleImport, ModuleImportParams, Pattern, Spaceable, Spaces, SpacesAfter, SpacesBefore,
StrLiteral, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
};
use roc_parse::expr::merge_spaces;
use roc_parse::header::Keyword;
use roc_region::all::Loc;
@ -25,21 +37,28 @@ impl<'a> Formattable for Defs<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let mut prev_spaces = true;
let arena = buf.text.bump();
for (index, def) in self.defs().enumerate() {
let spaces_before = &self.spaces[self.space_before[index].indices()];
let spaces_after = &self.spaces[self.space_after[index].indices()];
let def = def_lift_spaces(buf.text.bump(), def);
let spaces_before = merge_spaces(arena, spaces_before, def.before);
let spaces_after = merge_spaces(arena, def.after, spaces_after);
if prev_spaces {
fmt_spaces(buf, spaces_before.iter(), indent);
} else {
fmt_default_newline(buf, spaces_before, indent);
}
match def {
match def.item {
Ok(type_def) => type_def.format(buf, indent),
Err(value_def) => value_def.format(buf, indent),
}
@ -51,6 +70,339 @@ impl<'a> Formattable for Defs<'a> {
}
}
pub fn def_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
def: Result<&'a TypeDef<'b>, &'a ValueDef<'b>>,
) -> Spaces<'a, Result<TypeDef<'a>, ValueDef<'a>>> {
match def {
Ok(td) => {
let td = tydef_lift_spaces(arena, *td);
Spaces {
before: td.before,
item: Ok(td.item),
after: td.after,
}
}
Err(vd) => {
let vd = valdef_lift_spaces(arena, *vd);
Spaces {
before: vd.before,
item: Err(vd.item),
after: vd.after,
}
}
}
}
fn lift_spaces_after<'a, 'b: 'a, T: 'b + ExtractSpaces<'a> + Spaceable<'a>>(
arena: &'a Bump,
item: T,
) -> SpacesAfter<'a, <T as ExtractSpaces<'a>>::Item>
where
<T as ExtractSpaces<'a>>::Item: Spaceable<'a>,
{
let spaces = item.extract_spaces();
SpacesAfter {
item: spaces.item.maybe_before(arena, spaces.before),
after: spaces.after,
}
}
pub fn tydef_lift_spaces<'a, 'b: 'a>(arena: &'a Bump, def: TypeDef<'b>) -> Spaces<'a, TypeDef<'a>> {
match def {
TypeDef::Alias { header, ann } => {
let ann_lifted = ann_lift_spaces_after(arena, &ann.value);
Spaces {
before: &[],
item: TypeDef::Alias {
header,
ann: Loc::at(ann.region, ann_lifted.item),
},
after: ann_lifted.after,
}
}
TypeDef::Opaque {
header,
typ,
derived,
} => {
if let Some(derived) = derived {
let derived_lifted = lift_spaces_after(arena, derived.value);
Spaces {
before: &[],
item: TypeDef::Opaque {
header,
typ,
derived: Some(Loc::at(derived.region, derived_lifted.item)),
},
after: derived_lifted.after,
}
} else {
let typ_lifted = ann_lift_spaces_after(arena, &typ.value);
Spaces {
before: &[],
item: TypeDef::Opaque {
header,
typ: Loc::at(typ.region, typ_lifted.item),
derived,
},
after: typ_lifted.after,
}
}
}
TypeDef::Ability {
header: _,
loc_implements: _,
members: _,
} => {
// TODO: if the fuzzer ever generates examples where it's important to lift spaces from the members,
// we'll need to implement this. I'm not sure that's possible, though.
Spaces {
before: &[],
item: def,
after: &[],
}
}
}
}
pub fn valdef_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
def: ValueDef<'b>,
) -> Spaces<'a, ValueDef<'a>> {
match def {
ValueDef::Annotation(pat, ann) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
let ann_lifted = ann_lift_spaces_after(arena, &ann.value);
Spaces {
before: pat_lifted.before,
item: ValueDef::Annotation(
Loc::at(pat.region, pat_lifted.item),
Loc::at(ann.region, ann_lifted.item),
),
after: ann_lifted.after,
}
}
ValueDef::Body(pat, expr) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
let expr_lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: pat_lifted.before,
item: ValueDef::Body(
arena.alloc(Loc::at(pat.region, pat_lifted.item)),
arena.alloc(Loc::at(expr.region, expr_lifted.item)),
),
after: expr_lifted.after,
}
}
ValueDef::AnnotatedBody {
ann_pattern,
ann_type,
lines_between,
body_pattern,
body_expr,
} => {
let ann_pattern_lifted = pattern_lift_spaces_before(arena, &ann_pattern.value);
let ann_type_lifted = ann_lift_spaces_after(arena, &ann_type.value);
let body_pattern_lifted = pattern_lift_spaces_before(arena, &body_pattern.value);
let body_expr_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &body_expr.value);
let lines_between = merge_spaces(
arena,
ann_type_lifted.after,
merge_spaces(arena, lines_between, body_pattern_lifted.before),
);
Spaces {
before: ann_pattern_lifted.before,
item: ValueDef::AnnotatedBody {
ann_pattern: arena.alloc(Loc::at(ann_pattern.region, ann_pattern_lifted.item)),
ann_type: arena.alloc(Loc::at(ann_type.region, ann_type_lifted.item)),
lines_between,
body_pattern: arena
.alloc(Loc::at(body_pattern.region, body_pattern_lifted.item)),
body_expr: arena.alloc(Loc::at(body_expr.region, body_expr_lifted.item)),
},
after: body_expr_lifted.after,
}
}
ValueDef::Dbg {
condition,
preceding_comment,
} => {
let condition_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &condition.value);
Spaces {
before: &[],
item: ValueDef::Dbg {
condition: arena.alloc(Loc::at(condition.region, condition_lifted.item)),
preceding_comment,
},
after: condition_lifted.after,
}
}
ValueDef::Expect {
condition,
preceding_comment,
} => {
let condition_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &condition.value);
Spaces {
before: &[],
item: ValueDef::Expect {
condition: arena.alloc(Loc::at(condition.region, condition_lifted.item)),
preceding_comment,
},
after: condition_lifted.after,
}
}
ValueDef::ModuleImport(module_import) => {
// Module imports begin with 'import', and end with either a ImportAlias or Collection
// No spaces in sight!
Spaces {
before: &[],
item: ValueDef::ModuleImport(module_import),
after: &[],
}
}
ValueDef::IngestedFileImport(mut ingested_file_import) => {
// Ingested file imports begin with 'import', but can end with a TypeAnnotation, which can have spaces
let after = if let Some(ann) = &mut ingested_file_import.annotation {
let lifted = ann_lift_spaces_after(arena, &ann.annotation.value);
ann.annotation.value = lifted.item;
lifted.after
} else {
&[]
};
Spaces {
before: &[],
item: ValueDef::IngestedFileImport(ingested_file_import),
after,
}
}
ValueDef::Stmt(expr) => {
let expr_lifted = expr_lift_spaces(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: expr_lifted.before,
item: ValueDef::Stmt(arena.alloc(Loc::at(expr.region, expr_lifted.item))),
after: expr_lifted.after,
}
}
ValueDef::StmtAfterExpr => Spaces {
before: &[],
item: ValueDef::StmtAfterExpr,
after: &[],
},
}
}
pub fn valdef_lift_spaces_before<'a, 'b: 'a>(
arena: &'a Bump,
def: ValueDef<'b>,
) -> SpacesBefore<'a, ValueDef<'a>> {
match def {
ValueDef::Annotation(pat, ann) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
SpacesBefore {
before: pat_lifted.before,
item: ValueDef::Annotation(Loc::at(pat.region, pat_lifted.item), ann),
}
}
ValueDef::Body(pat, expr) => {
let pat_lifted = pattern_lift_spaces_before(arena, &pat.value);
SpacesBefore {
before: pat_lifted.before,
item: ValueDef::Body(arena.alloc(Loc::at(pat.region, pat_lifted.item)), expr),
}
}
ValueDef::AnnotatedBody {
ann_pattern,
ann_type,
lines_between,
body_pattern,
body_expr,
} => {
let ann_pattern_lifted = pattern_lift_spaces_before(arena, &ann_pattern.value);
let ann_type_lifted = ann_lift_spaces_after(arena, &ann_type.value);
let body_pattern_lifted = pattern_lift_spaces_before(arena, &body_pattern.value);
let lines_between = merge_spaces(
arena,
ann_type_lifted.after,
merge_spaces(arena, lines_between, body_pattern_lifted.before),
);
SpacesBefore {
before: ann_pattern_lifted.before,
item: ValueDef::AnnotatedBody {
ann_pattern: arena.alloc(Loc::at(ann_pattern.region, ann_pattern_lifted.item)),
ann_type: arena.alloc(Loc::at(ann_type.region, ann_type_lifted.item)),
lines_between,
body_pattern: arena
.alloc(Loc::at(body_pattern.region, body_pattern_lifted.item)),
body_expr,
},
}
}
ValueDef::Dbg {
condition,
preceding_comment,
} => SpacesBefore {
before: &[],
item: ValueDef::Dbg {
condition,
preceding_comment,
},
},
ValueDef::Expect {
condition,
preceding_comment,
} => SpacesBefore {
before: &[],
item: ValueDef::Expect {
condition,
preceding_comment,
},
},
ValueDef::ModuleImport(module_import) => {
// Module imports always start with 'import', no spaces
SpacesBefore {
before: &[],
item: ValueDef::ModuleImport(module_import),
}
}
ValueDef::IngestedFileImport(ingested_file_import) => {
// Similarly, ingested file imports always start with 'import', no spaces
SpacesBefore {
before: &[],
item: ValueDef::IngestedFileImport(ingested_file_import),
}
}
ValueDef::Stmt(expr) => {
let expr_lifted = expr_lift_spaces_before(Parens::NotNeeded, arena, &expr.value);
SpacesBefore {
before: expr_lifted.before,
item: ValueDef::Stmt(arena.alloc(Loc::at(expr.region, expr_lifted.item))),
}
}
ValueDef::StmtAfterExpr => SpacesBefore {
before: &[],
item: ValueDef::StmtAfterExpr,
},
}
}
impl<'a> Formattable for TypeDef<'a> {
fn is_multiline(&self) -> bool {
use roc_parse::ast::TypeDef::*;
@ -66,34 +418,23 @@ impl<'a> Formattable for TypeDef<'a> {
use roc_parse::ast::TypeDef::*;
match self {
Alias {
header: TypeHeader { name, vars },
ann,
} => {
Alias { header, ann } => {
header.format(buf, indent);
buf.indent(indent);
buf.push_str(name.value);
for var in *vars {
buf.spaces(1);
let need_parens = matches!(var.value, Pattern::Apply(..));
if need_parens {
buf.push_str("(");
}
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
if need_parens {
buf.push_str(")");
}
}
buf.push_str(" :");
buf.spaces(1);
ann.format(buf, indent)
let ann = ann_lift_spaces(buf.text.bump(), &ann.value);
let inner_indent = if ty_is_outdentable(&ann.item) {
indent
} else {
indent + INDENT
};
fmt_comments_only(buf, ann.before.iter(), NewlineAt::Bottom, inner_indent);
ann.item.format(buf, inner_indent);
fmt_spaces(buf, ann.after.iter(), indent);
}
Opaque {
header,
@ -113,7 +454,15 @@ impl<'a> Formattable for TypeDef<'a> {
let make_multiline = ann.is_multiline() || has_abilities_multiline;
fmt_general_def(header, buf, indent, ":=", &ann.value, newlines);
fmt_general_def(
header,
Parens::NotNeeded,
buf,
indent,
":=",
&ann.value,
newlines,
);
if let Some(has_abilities) = has_abilities {
buf.spaces(1);
@ -127,23 +476,17 @@ impl<'a> Formattable for TypeDef<'a> {
}
}
Ability {
header: TypeHeader { name, vars },
header,
loc_implements: _,
members,
} => {
buf.indent(indent);
buf.push_str(name.value);
for var in *vars {
buf.spaces(1);
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
}
header.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
buf.spaces(1);
buf.push_str(roc_parse::keyword::IMPLEMENTS);
buf.spaces(1);
if !self.is_multiline() {
debug_assert_eq!(members.len(), 1);
buf.spaces(1);
members[0].format_with_options(
buf,
Parens::NotNeeded,
@ -175,15 +518,68 @@ impl<'a> Formattable for TypeHeader<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
buf.push_str(self.name.value);
let vars_indent = if self.vars.iter().any(|v| v.is_multiline()) {
indent + INDENT
} else {
indent
};
let mut last_after: &[CommentOrNewline<'_>] = &[];
let mut last_multiline = false;
for var in self.vars.iter() {
let var = pattern_lift_spaces(buf.text.bump(), &var.value);
let before = if !last_after.is_empty() {
merge_spaces(buf.text.bump(), last_after, var.before)
} else {
var.before
};
if !before.is_empty() {
if !var.item.is_multiline() {
fmt_comments_only(buf, before.iter(), NewlineAt::Bottom, vars_indent)
} else {
fmt_spaces(buf, before.iter(), vars_indent);
}
}
buf.ensure_ends_with_whitespace();
last_after = var.after;
last_multiline = var.item.is_multiline();
let need_parens = matches!(var.item, Pattern::Apply(..));
if need_parens {
buf.push_str("(");
}
fmt_pattern(buf, &var.item, vars_indent, Parens::NotNeeded);
buf.indent(vars_indent);
if need_parens {
buf.push_str(")");
}
}
if !last_after.is_empty() {
if starts_with_inline_comment(last_after.iter()) {
buf.spaces(1);
fmt_pattern(buf, &var.value, indent, Parens::NotNeeded);
buf.indent(indent);
}
if !last_multiline {
fmt_comments_only(buf, last_after.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, last_after.iter(), indent);
}
}
}
}
@ -213,6 +609,7 @@ impl<'a> Formattable for ModuleImport<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -228,6 +625,7 @@ impl<'a> Formattable for ModuleImport<'a> {
let indent = if !before_name.is_empty()
|| (params.is_multiline() && exposed.is_some())
|| params.map(|p| !p.before.is_empty()).unwrap_or(false)
|| alias.is_multiline()
|| exposed.map_or(false, |e| e.keyword.is_multiline())
{
@ -280,6 +678,7 @@ impl<'a> Formattable for IngestedFileImport<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -315,6 +714,7 @@ impl<'a> Formattable for ImportedModuleName<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
@ -394,6 +794,7 @@ impl<'a> Formattable for IngestedFileAnnotation<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
@ -431,8 +832,14 @@ impl<'a> Formattable for ValueDef<'a> {
use roc_parse::ast::ValueDef::*;
match self {
Annotation(loc_pattern, loc_annotation) => {
let pat_parens = if ann_pattern_needs_parens(&loc_pattern.value) {
Parens::InApply
} else {
Parens::NotNeeded
};
fmt_general_def(
loc_pattern,
pat_parens,
buf,
indent,
":",
@ -441,7 +848,7 @@ impl<'a> Formattable for ValueDef<'a> {
);
}
Body(loc_pattern, loc_expr) => {
fmt_body(buf, &loc_pattern.value, &loc_expr.value, indent);
fmt_body(buf, true, &loc_pattern.value, &loc_expr.value, indent);
}
Dbg { condition, .. } => fmt_dbg_in_def(buf, condition, self.is_multiline(), indent),
Expect { condition, .. } => fmt_expect(buf, condition, self.is_multiline(), indent),
@ -452,12 +859,25 @@ impl<'a> Formattable for ValueDef<'a> {
body_pattern,
body_expr,
} => {
fmt_general_def(ann_pattern, buf, indent, ":", &ann_type.value, newlines);
let pat_parens = if ann_pattern_needs_parens(&ann_pattern.value) {
Parens::InApply
} else {
Parens::NotNeeded
};
fmt_general_def(
ann_pattern,
pat_parens,
buf,
indent,
":",
&ann_type.value,
newlines,
);
fmt_annotated_body_comment(buf, indent, lines_between);
buf.newline();
fmt_body(buf, &body_pattern.value, &body_expr.value, indent);
fmt_body(buf, false, &body_pattern.value, &body_expr.value, indent);
}
ModuleImport(module_import) => module_import.format(buf, indent),
IngestedFileImport(ingested_file_import) => ingested_file_import.format(buf, indent),
@ -467,15 +887,27 @@ impl<'a> Formattable for ValueDef<'a> {
}
}
fn ann_pattern_needs_parens(value: &Pattern<'_>) -> bool {
match value.extract_spaces().item {
Pattern::Tag(_) => true,
Pattern::Apply(func, _args) if matches!(func.extract_spaces().item, Pattern::Tag(..)) => {
true
}
_ => false,
}
}
fn fmt_general_def<L: Formattable>(
lhs: L,
lhs_parens: Parens,
buf: &mut Buf,
indent: u16,
sep: &str,
rhs: &TypeAnnotation,
newlines: Newlines,
) {
lhs.format(buf, indent);
lhs.format_with_options(buf, lhs_parens, Newlines::Yes, indent);
buf.indent(indent);
if rhs.is_multiline() {
@ -483,20 +915,25 @@ fn fmt_general_def<L: Formattable>(
buf.push_str(sep);
buf.spaces(1);
let should_outdent = should_outdent(rhs);
let rhs_lifted = ann_lift_spaces(buf.text.bump(), rhs);
if should_outdent {
match rhs {
TypeAnnotation::SpaceBefore(sub_def, _) => {
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
_ => {
rhs.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
}
if ty_is_outdentable(&rhs_lifted.item) && rhs_lifted.before.iter().all(|s| s.is_newline()) {
rhs_lifted
.item
.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
} else {
rhs.format_with_options(buf, Parens::NotNeeded, newlines, indent + INDENT);
buf.ensure_ends_with_newline();
fmt_comments_only(
buf,
rhs_lifted.before.iter(),
NewlineAt::Bottom,
indent + INDENT,
);
rhs_lifted
.item
.format_with_options(buf, Parens::NotNeeded, newlines, indent + INDENT);
}
fmt_comments_only(buf, rhs_lifted.after.iter(), NewlineAt::Bottom, indent);
} else {
buf.spaces(1);
buf.push_str(sep);
@ -505,28 +942,6 @@ fn fmt_general_def<L: Formattable>(
}
}
fn should_outdent(mut rhs: &TypeAnnotation) -> bool {
loop {
match rhs {
TypeAnnotation::SpaceBefore(sub_def, spaces) => {
let is_only_newlines = spaces.iter().all(|s| s.is_newline());
if !is_only_newlines || !sub_def.is_multiline() {
return false;
}
rhs = sub_def;
}
TypeAnnotation::Where(ann, _clauses) => {
if !ann.is_multiline() {
return false;
}
rhs = &ann.value;
}
TypeAnnotation::Record { .. } | TypeAnnotation::TagUnion { .. } => return true,
_ => return false,
}
}
}
fn fmt_dbg_in_def<'a>(buf: &mut Buf, condition: &'a Loc<Expr<'a>>, _: bool, indent: u16) {
buf.ensure_ends_with_newline();
buf.indent(indent);
@ -553,14 +968,6 @@ fn fmt_expect<'a>(buf: &mut Buf, condition: &'a Loc<Expr<'a>>, is_multiline: boo
condition.format(buf, return_indent);
}
pub fn fmt_value_def(buf: &mut Buf, def: &roc_parse::ast::ValueDef, indent: u16) {
def.format(buf, indent);
}
pub fn fmt_type_def(buf: &mut Buf, def: &roc_parse::ast::TypeDef, indent: u16) {
def.format(buf, indent);
}
pub fn fmt_defs(buf: &mut Buf, defs: &Defs, indent: u16) {
defs.format(buf, indent);
}
@ -608,21 +1015,44 @@ pub fn fmt_annotated_body_comment<'a>(
}
}
pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>, indent: u16) {
pub fn fmt_body<'a>(
buf: &mut Buf,
allow_simplify_empty_record_destructure: bool,
pattern: &'a Pattern<'a>,
body: &'a Expr<'a>,
indent: u16,
) {
let pattern_extracted = pattern.extract_spaces();
// Check if this is an assignment into the unit value
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = pattern {
collection.is_empty()
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = pattern_extracted.item
{
allow_simplify_empty_record_destructure
&& collection.is_empty()
&& pattern_extracted.before.iter().all(|s| s.is_newline())
&& pattern_extracted.after.iter().all(|s| s.is_newline())
&& !matches!(body.extract_spaces().item, Expr::Defs(..))
} else {
false
};
// Don't format the `{} =` for defs with this pattern
if !is_unit_assignment {
pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
buf.indent(indent);
buf.push_str(" =");
if is_unit_assignment {
return body.format_with_options(buf, Parens::NotNeeded, Newlines::No, indent);
}
pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
if pattern.is_multiline() {
buf.ensure_ends_with_newline();
buf.indent(indent);
} else {
buf.spaces(1);
}
buf.push_str("=");
let body = expr_lift_and_lower(Parens::NotNeeded, buf.text.bump(), body);
if body.is_multiline() {
match body {
Expr::SpaceBefore(sub_def, spaces) => {
@ -634,7 +1064,10 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
_ => false,
};
if should_outdent {
if is_unit_assignment {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
} else if should_outdent {
buf.spaces(1);
sub_def.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
} else {
@ -646,6 +1079,32 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
);
}
}
Expr::Apply(
Loc {
value: Expr::Str(StrLiteral::Block(..)),
..
},
..,
) => {
buf.spaces(1);
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::Str(s) => {
if is_str_multiline(&s) {
buf.ensure_ends_with_newline();
} else {
buf.spaces(1);
}
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
_ if starts_with_block_string_literal(&body) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::When(..) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::Defs(..) | Expr::BinOps(_, _) | Expr::Backpassing(..) => {
// Binop chains always get a newline. Otherwise you can have things like:
//
@ -662,9 +1121,15 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
buf.newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
Expr::When(..) | Expr::Str(StrLiteral::Block(_)) => {
buf.ensure_ends_with_newline();
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
Expr::ParensAround(&Expr::SpaceBefore(sub_def, _)) => {
let needs_indent = !sub_expr_requests_parens(sub_def);
let indent = if needs_indent {
indent + INDENT
} else {
indent
};
buf.spaces(1);
body.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
}
_ => {
buf.spaces(1);
@ -677,6 +1142,18 @@ pub fn fmt_body<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, body: &'a Expr<'a>,
}
}
pub fn starts_with_block_string_literal(expr: &Expr<'_>) -> bool {
match expr {
Expr::Str(s) => is_str_multiline(s),
Expr::SpaceAfter(inner, _) | Expr::SpaceBefore(inner, _) => {
starts_with_block_string_literal(inner)
}
Expr::Apply(inner, _, _) => starts_with_block_string_literal(&inner.value),
Expr::TrySuffix { target: _, expr } => starts_with_block_string_literal(expr),
_ => false,
}
}
impl<'a> Formattable for AbilityMember<'a> {
fn is_multiline(&self) -> bool {
self.name.value.is_multiline() || self.typ.is_multiline()
@ -687,6 +1164,7 @@ impl<'a> Formattable for AbilityMember<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Spaces { before, item, .. } = self.name.value.extract_spaces();

File diff suppressed because it is too large Load diff

View file

@ -84,6 +84,7 @@ impl<V: Formattable> Formattable for Option<V> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
if let Some(v) = self {
@ -109,6 +110,7 @@ impl<'a> Formattable for ProvidesTo<'a> {
buf: &mut Buf,
_parens: crate::annotation::Parens,
_newlines: Newlines,
indent: u16,
) {
self.provides_keyword.format(buf, indent);
@ -128,6 +130,7 @@ impl<'a> Formattable for PlatformRequires<'a> {
buf: &mut Buf,
_parens: crate::annotation::Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_requires(buf, self, indent);
@ -144,6 +147,7 @@ impl<'a, V: Formattable> Formattable for Spaces<'a, V> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
fmt_default_spaces(buf, self.before, indent);
@ -280,6 +284,7 @@ impl<'a> Formattable for TypedIdent<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
buf.indent(indent);
@ -316,6 +321,7 @@ impl<'a, T: Formattable> Formattable for Spaced<'a, T> {
buf: &mut Buf,
parens: crate::annotation::Parens,
newlines: Newlines,
indent: u16,
) {
match self {
@ -337,6 +343,7 @@ impl<'a, T: Formattable> Formattable for Spaced<'a, T> {
fn fmt_imports<'a>(
buf: &mut Buf,
loc_entries: Collection<'a, Loc<Spaced<'a, ImportsEntry<'a>>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No)
@ -346,6 +353,7 @@ fn fmt_provides<'a>(
buf: &mut Buf,
loc_exposed_names: Collection<'a, Loc<Spaced<'a, ExposedName<'a>>>>,
loc_provided_types: Option<Collection<'a, Loc<Spaced<'a, UppercaseIdent<'a>>>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_exposed_names, Newlines::No);
@ -367,27 +375,12 @@ fn fmt_to(buf: &mut Buf, to: To, indent: u16) {
fn fmt_exposes<N: Formattable + Copy + core::fmt::Debug>(
buf: &mut Buf,
loc_entries: Collection<'_, Loc<Spaced<'_, N>>>,
indent: u16,
) {
fmt_collection(buf, indent, Braces::Square, loc_entries, Newlines::No)
}
pub trait FormatName {
fn format(&self, buf: &mut Buf);
}
impl<'a> FormatName for &'a str {
fn format(&self, buf: &mut Buf) {
buf.push_str(self)
}
}
impl<'a> FormatName for ModuleName<'a> {
fn format(&self, buf: &mut Buf) {
buf.push_str(self.as_str());
}
}
impl<'a> Formattable for ModuleName<'a> {
fn is_multiline(&self) -> bool {
false
@ -421,12 +414,6 @@ impl<'a> Formattable for ExposedName<'a> {
}
}
impl<'a> FormatName for ExposedName<'a> {
fn format(&self, buf: &mut Buf) {
buf.push_str(self.as_str());
}
}
fn fmt_packages<'a>(
buf: &mut Buf,
loc_entries: Collection<'a, Loc<Spaced<'a, PackageEntry<'a>>>>,
@ -445,6 +432,7 @@ impl<'a> Formattable for PackageEntry<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_packages_entry(buf, self, indent);
@ -461,6 +449,7 @@ impl<'a> Formattable for ImportsEntry<'a> {
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
fmt_imports_entry(buf, self, indent);

View file

@ -18,18 +18,41 @@ pub struct Buf<'a> {
spaces_to_flush: usize,
newlines_to_flush: usize,
beginning_of_line: bool,
line_indent: u16,
flags: MigrationFlags,
}
#[derive(Debug, Copy, Clone)]
pub struct MigrationFlags {
pub(crate) snakify: bool,
}
impl MigrationFlags {
pub fn new(snakify: bool) -> Self {
MigrationFlags { snakify }
}
pub fn at_least_one_active(&self) -> bool {
self.snakify
}
}
impl<'a> Buf<'a> {
pub fn new_in(arena: &'a Bump) -> Buf<'a> {
pub fn new_in(arena: &'a Bump, flags: MigrationFlags) -> Buf<'a> {
Buf {
text: String::new_in(arena),
line_indent: 0,
spaces_to_flush: 0,
newlines_to_flush: 0,
beginning_of_line: true,
flags,
}
}
pub fn flags(&self) -> MigrationFlags {
self.flags
}
pub fn as_str(&'a self) -> &'a str {
self.text.as_str()
}
@ -40,11 +63,18 @@ impl<'a> Buf<'a> {
pub fn indent(&mut self, indent: u16) {
if self.beginning_of_line {
self.line_indent = indent;
self.spaces_to_flush = indent as usize;
}
self.beginning_of_line = false;
}
pub fn cur_line_indent(&self) -> u16 {
debug_assert!(!self.beginning_of_line, "cur_line_indent before indent");
self.line_indent
}
#[track_caller]
pub fn push(&mut self, ch: char) {
debug_assert!(!self.beginning_of_line);
debug_assert!(
@ -61,6 +91,7 @@ impl<'a> Buf<'a> {
self.text.push(ch);
}
#[track_caller]
pub fn push_str_allow_spaces(&mut self, s: &str) {
debug_assert!(
!self.beginning_of_line,
@ -73,6 +104,7 @@ impl<'a> Buf<'a> {
self.text.push_str(s);
}
#[track_caller]
pub fn push_str(&mut self, s: &str) {
debug_assert!(
!self.beginning_of_line,
@ -129,6 +161,12 @@ impl<'a> Buf<'a> {
}
}
pub fn ensure_ends_with_whitespace(&mut self) {
if !self.text.is_empty() && self.newlines_to_flush == 0 && self.spaces_to_flush == 0 {
self.spaces_to_flush = 1;
}
}
fn flush_spaces(&mut self) {
for _ in 0..self.newlines_to_flush {
self.text.push('\n');

View file

@ -1,8 +1,15 @@
use crate::annotation::{Formattable, Newlines, Parens};
use crate::expr::{fmt_str_literal, format_sq_literal, is_str_multiline};
use crate::expr::{
expr_is_multiline, expr_lift_spaces_after, fmt_str_literal, format_sq_literal, is_str_multiline,
};
use crate::spaces::{fmt_comments_only, fmt_spaces, NewlineAt, INDENT};
use crate::Buf;
use roc_parse::ast::{Base, CommentOrNewline, Pattern, PatternAs};
use bumpalo::Bump;
use roc_parse::ast::{
Base, CommentOrNewline, Pattern, PatternAs, Spaceable, Spaces, SpacesAfter, SpacesBefore,
};
use roc_parse::expr::merge_spaces;
use roc_region::all::Loc;
pub fn fmt_pattern<'a>(buf: &mut Buf, pattern: &'a Pattern<'a>, indent: u16, parens: Parens) {
pattern.format_with_options(buf, parens, Newlines::No, indent);
@ -54,7 +61,7 @@ impl<'a> Formattable for Pattern<'a> {
Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()),
Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(),
Pattern::OptionalField(_, expr) => expr.is_multiline(),
Pattern::OptionalField(_, expr) => expr_is_multiline(&expr.value, true),
Pattern::As(pattern, pattern_as) => pattern.is_multiline() || pattern_as.is_multiline(),
Pattern::ListRest(opt_pattern_as) => match opt_pattern_as {
@ -86,25 +93,82 @@ impl<'a> Formattable for Pattern<'a> {
}
}
fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) {
use self::Pattern::*;
match self {
Identifier { ident: string } => {
buf.indent(indent);
buf.push_str(string);
fn format_with_options(&self, buf: &mut Buf, parens: Parens, _newlines: Newlines, indent: u16) {
fmt_pattern_inner(self, buf, parens, indent, self.is_multiline(), false);
}
Tag(name) | OpaqueRef(name) => {
}
fn fmt_pattern_inner(
pat: &Pattern<'_>,
buf: &mut Buf,
parens: Parens,
indent: u16,
outer_is_multiline: bool,
force_newline_at_start: bool,
) -> bool {
let me = pattern_lift_spaces(buf.text.bump(), pat);
let mut was_multiline = me.item.is_multiline();
if !me.before.is_empty() {
if !outer_is_multiline {
was_multiline |= me.before.iter().any(|s| s.is_comment());
fmt_comments_only(buf, me.before.iter(), NewlineAt::Bottom, indent)
} else {
was_multiline |= true;
fmt_spaces(buf, me.before.iter(), indent);
}
}
if force_newline_at_start {
buf.ensure_ends_with_newline();
}
let is_multiline = me.item.is_multiline();
fmt_pattern_only(me, buf, indent, parens, is_multiline);
if !me.after.is_empty() {
if starts_with_inline_comment(me.after.iter()) {
buf.spaces(1);
}
if !outer_is_multiline {
was_multiline |= me.before.iter().any(|s| s.is_comment());
fmt_comments_only(buf, me.after.iter(), NewlineAt::Bottom, indent)
} else {
was_multiline |= true;
fmt_spaces(buf, me.after.iter(), indent);
}
}
was_multiline
}
fn fmt_pattern_only(
me: Spaces<'_, Pattern<'_>>,
buf: &mut Buf<'_>,
indent: u16,
parens: Parens,
is_multiline: bool,
) {
match me.item {
Pattern::Identifier { ident: string } => {
buf.indent(indent);
snakify_camel_ident(buf, string);
}
Pattern::Tag(name) | Pattern::OpaqueRef(name) => {
buf.indent(indent);
buf.push_str(name);
}
Apply(loc_pattern, loc_arg_patterns) => {
Pattern::Apply(loc_pattern, loc_arg_patterns) => {
buf.indent(indent);
// Sometimes, an Apply pattern needs parens around it.
// In particular when an Apply's argument is itself an Apply (> 0) arguments
let parens = !loc_arg_patterns.is_empty() && (parens == Parens::InApply);
let indent_more = if self.is_multiline() {
let indent_more = if is_multiline {
indent + INDENT
} else {
indent
@ -114,18 +178,46 @@ impl<'a> Formattable for Pattern<'a> {
buf.push('(');
}
loc_pattern.format_with_options(buf, Parens::InApply, Newlines::No, indent);
let pat = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
if !pat.before.is_empty() {
if !is_multiline {
fmt_comments_only(buf, pat.before.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, pat.before.iter(), indent);
}
}
fmt_pattern_inner(&pat.item, buf, Parens::InApply, indent, is_multiline, false);
if !pat.after.is_empty() {
if !is_multiline {
fmt_comments_only(buf, pat.after.iter(), NewlineAt::Bottom, indent_more)
} else {
fmt_spaces(buf, pat.after.iter(), indent_more);
}
}
let mut add_newlines = false;
for loc_arg in loc_arg_patterns.iter() {
buf.spaces(1);
loc_arg.format_with_options(buf, Parens::InApply, Newlines::No, indent_more);
let was_multiline = fmt_pattern_inner(
&loc_arg.value,
buf,
Parens::InApply,
indent_more,
is_multiline,
add_newlines,
);
add_newlines |= was_multiline;
}
if parens {
buf.push(')');
}
}
RecordDestructure(loc_patterns) => {
Pattern::RecordDestructure(loc_patterns) => {
buf.indent(indent);
buf.push_str("{");
@ -133,40 +225,81 @@ impl<'a> Formattable for Pattern<'a> {
buf.spaces(1);
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
let item = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
if !item.before.is_empty() {
if !is_multiline {
fmt_comments_only(buf, item.before.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, item.before.iter(), indent);
}
}
fmt_pattern_inner(
&item.item,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
let is_multiline = item.item.is_multiline();
if it.peek().is_some() {
buf.push_str(",");
}
if !item.after.is_empty() {
if starts_with_inline_comment(item.after.iter()) {
buf.spaces(1);
}
if !is_multiline {
fmt_comments_only(buf, item.after.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, item.after.iter(), indent);
}
}
if it.peek().is_some() {
buf.ensure_ends_with_whitespace();
}
}
buf.spaces(1);
}
buf.indent(indent);
buf.push_str("}");
}
RequiredField(name, loc_pattern) => {
Pattern::RequiredField(name, loc_pattern) => {
buf.indent(indent);
buf.push_str(name);
snakify_camel_ident(buf, name);
buf.push_str(":");
buf.spaces(1);
loc_pattern.format(buf, indent);
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
}
OptionalField(name, loc_pattern) => {
Pattern::OptionalField(name, loc_pattern) => {
buf.indent(indent);
buf.push_str(name);
snakify_camel_ident(buf, name);
buf.push_str(" ?");
buf.spaces(1);
loc_pattern.format(buf, indent);
}
&NumLiteral(string) => {
Pattern::NumLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
&NonBase10Literal {
Pattern::NonBase10Literal {
base,
string,
is_negative,
@ -185,53 +318,71 @@ impl<'a> Formattable for Pattern<'a> {
buf.push_str(string);
}
&FloatLiteral(string) => {
Pattern::FloatLiteral(string) => {
buf.indent(indent);
buf.push_str(string);
}
StrLiteral(literal) => fmt_str_literal(buf, *literal, indent),
SingleQuote(string) => {
Pattern::StrLiteral(literal) => fmt_str_literal(buf, literal, indent),
Pattern::SingleQuote(string) => {
buf.indent(indent);
format_sq_literal(buf, string);
}
Underscore(name) => {
Pattern::Underscore(name) => {
buf.indent(indent);
buf.push('_');
buf.push_str(name);
}
Tuple(loc_patterns) => {
Pattern::Tuple(loc_patterns) => {
buf.indent(indent);
buf.push_str("(");
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
if it.peek().is_some() {
buf.indent(indent);
buf.push_str(",");
buf.spaces(1);
}
}
buf.indent(indent);
buf.push_str(")");
}
List(loc_patterns) => {
Pattern::List(loc_patterns) => {
buf.indent(indent);
buf.push_str("[");
let mut it = loc_patterns.iter().peekable();
while let Some(loc_pattern) = it.next() {
loc_pattern.format(buf, indent);
fmt_pattern_inner(
&loc_pattern.value,
buf,
Parens::NotNeeded,
indent,
is_multiline,
false,
);
if it.peek().is_some() {
buf.indent(indent);
buf.push_str(",");
buf.spaces(1);
}
}
buf.indent(indent);
buf.push_str("]");
}
ListRest(opt_pattern_as) => {
Pattern::ListRest(opt_pattern_as) => {
buf.indent(indent);
buf.push_str("..");
@ -243,10 +394,11 @@ impl<'a> Formattable for Pattern<'a> {
}
}
As(pattern, pattern_as) => {
let needs_parens = parens == Parens::InAsPattern;
Pattern::As(pattern, pattern_as) => {
let needs_parens = parens == Parens::InAsPattern || parens == Parens::InApply;
if needs_parens {
buf.indent(indent);
buf.push('(');
}
@ -255,53 +407,33 @@ impl<'a> Formattable for Pattern<'a> {
pattern_as.format(buf, indent + INDENT);
if needs_parens {
buf.indent(indent);
buf.push(')');
}
}
// Space
SpaceBefore(sub_pattern, spaces) => {
if !sub_pattern.is_multiline() {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, spaces.iter(), indent);
}
sub_pattern.format_with_options(buf, parens, newlines, indent);
}
SpaceAfter(sub_pattern, spaces) => {
sub_pattern.format_with_options(buf, parens, newlines, indent);
if starts_with_inline_comment(spaces.iter()) {
buf.spaces(1);
}
if !sub_pattern.is_multiline() {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent)
} else {
fmt_spaces(buf, spaces.iter(), indent);
}
Pattern::SpaceBefore(..) | Pattern::SpaceAfter(..) => {
unreachable!("handled by lift_spaces")
}
// Malformed
Malformed(string) | MalformedIdent(string, _) => {
Pattern::Malformed(string) | Pattern::MalformedIdent(string, _) => {
buf.indent(indent);
buf.push_str(string);
}
QualifiedIdentifier { module_name, ident } => {
Pattern::QualifiedIdentifier { module_name, ident } => {
buf.indent(indent);
if !module_name.is_empty() {
buf.push_str(module_name);
buf.push('.');
}
buf.push_str(ident);
}
snakify_camel_ident(buf, ident);
}
}
}
fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a>>>(
pub fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a>>>(
spaces: I,
) -> bool {
matches!(
@ -309,3 +441,192 @@ fn starts_with_inline_comment<'a, I: IntoIterator<Item = &'a CommentOrNewline<'a
Some(CommentOrNewline::LineComment(_))
)
}
pub fn pattern_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> Spaces<'a, Pattern<'a>> {
match pat {
Pattern::Apply(func, args) => {
let func_lifted = pattern_lift_spaces(arena, &func.value);
let args = arena.alloc_slice_copy(args);
let (before, func, after) = if let Some(last) = args.last_mut() {
let last_lifted = pattern_lift_spaces(arena, &last.value);
if last_lifted.before.is_empty() {
*last = Loc::at(last.region, last_lifted.item)
} else {
*last = Loc::at(
last.region,
Pattern::SpaceBefore(arena.alloc(last_lifted.item), last_lifted.before),
);
}
let f = if func_lifted.after.is_empty() {
func_lifted.item
} else {
Pattern::SpaceAfter(arena.alloc(func_lifted.item), func_lifted.after)
};
(
func_lifted.before,
Loc::at(func.region, f),
last_lifted.after,
)
} else {
(
func_lifted.before,
Loc::at(func.region, func_lifted.item),
func_lifted.after,
)
};
Spaces {
before,
item: Pattern::Apply(arena.alloc(func), args),
after,
}
}
Pattern::OptionalField(name, expr) => {
let lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &expr.value);
Spaces {
before: &[],
item: Pattern::OptionalField(name, arena.alloc(Loc::at(expr.region, lifted.item))),
after: lifted.after,
}
}
Pattern::RequiredField(name, pat) => {
let lifted = pattern_lift_spaces_after(arena, &pat.value);
Spaces {
before: &[],
item: Pattern::RequiredField(name, arena.alloc(Loc::at(pat.region, lifted.item))),
after: lifted.after,
}
}
Pattern::SpaceBefore(expr, spaces) => {
let mut inner = pattern_lift_spaces(arena, expr);
inner.before = merge_spaces(arena, spaces, inner.before);
inner
}
Pattern::SpaceAfter(expr, spaces) => {
let mut inner = pattern_lift_spaces(arena, expr);
inner.after = merge_spaces(arena, inner.after, spaces);
inner
}
_ => Spaces {
before: &[],
item: *pat,
after: &[],
},
}
}
pub fn pattern_lift_spaces_before<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> SpacesBefore<'a, Pattern<'a>> {
let lifted = pattern_lift_spaces(arena, pat);
SpacesBefore {
before: lifted.before,
item: lifted.item.maybe_after(arena, lifted.after),
}
}
pub fn pattern_lift_spaces_after<'a, 'b: 'a>(
arena: &'a Bump,
pat: &Pattern<'b>,
) -> SpacesAfter<'a, Pattern<'a>> {
let lifted = pattern_lift_spaces(arena, pat);
SpacesAfter {
item: lifted.item.maybe_before(arena, lifted.before),
after: lifted.after,
}
}
/// Convert camelCase identifier to snake case
fn snakify_camel_ident(buf: &mut Buf, string: &str) {
let chars: Vec<char> = string.chars().collect();
if !buf.flags().snakify || (string.contains('_') && !string.ends_with('_')) {
buf.push_str(string);
return;
}
let mut index = 0;
let len = chars.len();
while index < len {
let prev = if index == 0 {
None
} else {
Some(chars[index - 1])
};
let c = chars[index];
let next = chars.get(index + 1);
let boundary = match (prev, c, next) {
// LUU, LUN, and LUL (simplified to LU_)
(Some(p), curr, _) if !p.is_ascii_uppercase() && curr.is_ascii_uppercase() => true,
// UUL
(Some(p), curr, Some(n))
if p.is_ascii_uppercase()
&& curr.is_ascii_uppercase()
&& n.is_ascii_lowercase() =>
{
true
}
_ => false,
};
// those are boundary transitions - should push _ and curr
if boundary {
buf.push('_');
}
buf.push(c.to_ascii_lowercase());
index += 1;
}
}
#[cfg(test)]
mod snakify_test {
use bumpalo::Bump;
use super::snakify_camel_ident;
use crate::{Buf, MigrationFlags};
fn check_snakify(arena: &Bump, original: &str) -> String {
let flags = MigrationFlags::new(true);
let mut buf = Buf::new_in(arena, flags);
buf.indent(0);
snakify_camel_ident(&mut buf, original);
buf.text.to_string()
}
#[test]
fn test_snakify_camel_ident() {
let arena = Bump::new();
assert_eq!(check_snakify(&arena, "A"), "a");
assert_eq!(check_snakify(&arena, "Ba"), "ba");
assert_eq!(check_snakify(&arena, "aB"), "a_b");
assert_eq!(check_snakify(&arena, "aBa"), "a_ba");
assert_eq!(check_snakify(&arena, "mBB"), "m_bb");
assert_eq!(check_snakify(&arena, "NbA"), "nb_a");
assert_eq!(check_snakify(&arena, "doIT"), "do_it");
assert_eq!(check_snakify(&arena, "ROC"), "roc");
assert_eq!(
check_snakify(&arena, "someHTTPRequest"),
"some_http_request"
);
assert_eq!(check_snakify(&arena, "usingXML"), "using_xml");
assert_eq!(check_snakify(&arena, "some123"), "some123");
assert_eq!(
check_snakify(&arena, "theHTTPStatus404"),
"the_http_status404"
);
assert_eq!(
check_snakify(&arena, "inThe99thPercentile"),
"in_the99th_percentile"
);
assert_eq!(
check_snakify(&arena, "all400SeriesErrorCodes"),
"all400_series_error_codes",
);
assert_eq!(check_snakify(&arena, "number4Yellow"), "number4_yellow");
assert_eq!(check_snakify(&arena, "useCases4Cobol"), "use_cases4_cobol");
assert_eq!(check_snakify(&arena, "c3PO"), "c3_po")
}
}

View file

@ -20,6 +20,43 @@ pub fn fmt_default_newline(buf: &mut Buf, spaces: &[CommentOrNewline], indent: u
}
}
pub enum SpacesNewlineMode {
Normal,
SkipNewlinesAtStart,
SkipNewlinesAtEnd,
SkipNewlinesAtBoth,
}
pub fn fmt_spaces_with_newline_mode(
buf: &mut Buf<'_>,
mut spaces: &[CommentOrNewline<'_>],
indent: u16,
mode: SpacesNewlineMode,
) {
if matches!(
mode,
SpacesNewlineMode::SkipNewlinesAtStart | SpacesNewlineMode::SkipNewlinesAtBoth
) {
let skip_count = spaces
.iter()
.take_while(|s| *s == &CommentOrNewline::Newline)
.count();
spaces = &spaces[skip_count..];
}
if matches!(
mode,
SpacesNewlineMode::SkipNewlinesAtEnd | SpacesNewlineMode::SkipNewlinesAtBoth
) {
let skip_count = spaces
.iter()
.rev()
.take_while(|s| *s == &CommentOrNewline::Newline)
.count();
spaces = &spaces[..spaces.len() - skip_count];
}
fmt_spaces(buf, spaces.iter(), indent);
}
/// Like fmt_spaces, but disallows two consecutive newlines.
pub fn fmt_spaces_no_blank_lines<'a, 'buf, I>(buf: &mut Buf<'buf>, spaces: I, indent: u16)
where

View file

@ -1388,8 +1388,12 @@ impl<
src2: &Symbol,
layout: &InLayout<'a>,
) {
match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Builtin::Int(quadword_and_smaller!())) => {
let int_width = match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width,
other => internal_error!("NumAddWrap is not defined for {other:?}"),
};
match int_width {
quadword_and_smaller!() => {
let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
let src1_reg = self
.storage_manager
@ -1399,29 +1403,10 @@ impl<
.load_to_general_reg(&mut self.buf, src2);
ASM::add_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2);
ASM::add_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
IntWidth::I128 | IntWidth::U128 => {
let intrinsic = bitcode::NUM_ADD_WRAP_INT[int_width].to_string();
self.build_fn_call(dst, intrinsic, &[*src1, *src2], &[*layout, *layout], layout);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2);
ASM::add_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::DEC => self.build_fn_call(
dst,
bitcode::DEC_ADD_SATURATED.to_string(),
&[*src1, *src2],
&[Layout::DEC, Layout::DEC],
&Layout::DEC,
),
other => unreachable!("NumAddWrap for layout {other:?}"),
}
}
@ -1432,28 +1417,20 @@ impl<
src2: Symbol,
layout: InLayout<'a>,
) {
match self.layout_interner.get_repr(layout) {
LayoutRepr::Builtin(Builtin::Int(width @ quadword_and_smaller!())) => {
let intrinsic = bitcode::NUM_ADD_SATURATED_INT[width].to_string();
match self.interner().get_repr(layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => {
let intrinsic = bitcode::NUM_ADD_SATURATED_INT[int_width].to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2);
ASM::add_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2);
ASM::add_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg);
LayoutRepr::Builtin(Builtin::Float(_)) => {
// saturated add is just normal add
self.build_num_add(&dst, &src1, &src2, &layout)
}
LayoutRepr::Builtin(Builtin::Decimal) => {
let intrinsic = bitcode::DEC_ADD_SATURATED.to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
x => todo!("NumAddSaturated: layout, {:?}", x),
other => internal_error!("NumAddSaturated is not defined for {other:?}"),
}
}
@ -1481,6 +1458,30 @@ impl<
)
}
fn build_num_sub_saturated(
&mut self,
dst: Symbol,
src1: Symbol,
src2: Symbol,
layout: InLayout<'a>,
) {
match self.interner().get_repr(layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => {
let intrinsic = bitcode::NUM_SUB_SATURATED_INT[int_width].to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
LayoutRepr::Builtin(Builtin::Float(_)) => {
// saturated sub is just normal sub
self.build_num_sub(&dst, &src1, &src2, &layout)
}
LayoutRepr::Builtin(Builtin::Decimal) => {
let intrinsic = bitcode::DEC_SUB_SATURATED.to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
other => internal_error!("NumSubSaturated is not defined for {other:?}"),
}
}
fn build_num_sub_checked(
&mut self,
dst: &Symbol,
@ -1547,12 +1548,12 @@ impl<
src2: &Symbol,
layout: &InLayout<'a>,
) {
use Builtin::Int;
match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Int(
IntWidth::I64 | IntWidth::I32 | IntWidth::I16 | IntWidth::I8,
)) => {
let int_width = match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width,
other => internal_error!("NumMulWrap is not defined for {other:?}"),
};
match int_width {
IntWidth::I64 | IntWidth::I32 | IntWidth::I16 | IntWidth::I8 => {
let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
let src1_reg = self
.storage_manager
@ -1562,9 +1563,7 @@ impl<
.load_to_general_reg(&mut self.buf, src2);
ASM::imul_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::Builtin(Int(
IntWidth::U64 | IntWidth::U32 | IntWidth::U16 | IntWidth::U8,
)) => {
IntWidth::U64 | IntWidth::U32 | IntWidth::U16 | IntWidth::U8 => {
let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
let src1_reg = self
.storage_manager
@ -1581,34 +1580,10 @@ impl<
src2_reg,
);
}
LayoutRepr::Builtin(Builtin::Int(IntWidth::I128 | IntWidth::U128)) => {
let int_width = match *layout {
Layout::I128 => IntWidth::I128,
Layout::U128 => IntWidth::U128,
_ => unreachable!(),
};
self.build_fn_call(
dst,
bitcode::NUM_MUL_WRAP_INT[int_width].to_string(),
&[*src1, *src2],
&[*layout, *layout],
layout,
);
IntWidth::I128 | IntWidth::U128 => {
let intrinsic = bitcode::NUM_MUL_WRAP_INT[int_width].to_string();
self.build_fn_call(dst, intrinsic, &[*src1, *src2], &[*layout, *layout], layout);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2);
ASM::mul_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, src2);
ASM::mul_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
x => todo!("NumMulWrap: layout, {:?}", x),
}
}
@ -1619,28 +1594,20 @@ impl<
src2: Symbol,
layout: InLayout<'a>,
) {
match self.layout_interner.get_repr(layout) {
LayoutRepr::Builtin(Builtin::Int(width @ quadword_and_smaller!())) => {
let intrinsic = bitcode::NUM_MUL_SATURATED_INT[width].to_string();
match self.interner().get_repr(layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => {
let intrinsic = bitcode::NUM_MUL_SATURATED_INT[int_width].to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2);
ASM::mul_freg64_freg64_freg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => {
let dst_reg = self.storage_manager.claim_float_reg(&mut self.buf, &dst);
let src1_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src1);
let src2_reg = self.storage_manager.load_to_float_reg(&mut self.buf, &src2);
ASM::mul_freg32_freg32_freg32(&mut self.buf, dst_reg, src1_reg, src2_reg);
LayoutRepr::Builtin(Builtin::Float(_)) => {
// saturated mul is just normal mul
self.build_num_mul(&dst, &src1, &src2, &layout)
}
LayoutRepr::Builtin(Builtin::Decimal) => {
let intrinsic = bitcode::DEC_MUL_SATURATED.to_string();
self.build_fn_call(&dst, intrinsic, &[src1, src2], &[layout, layout], &layout);
}
x => todo!("NumMulSaturated: layout, {:?}", x),
other => internal_error!("NumMulSaturated is not defined for {other:?}"),
}
}
@ -1866,8 +1833,12 @@ impl<
src2: &Symbol,
layout: &InLayout<'a>,
) {
match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Builtin::Int(quadword_and_smaller!())) => {
let int_width = match self.layout_interner.get_repr(*layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => int_width,
other => internal_error!("NumSubWrap is not defined for {other:?}"),
};
match int_width {
quadword_and_smaller!() => {
let dst_reg = self.storage_manager.claim_general_reg(&mut self.buf, dst);
let src1_reg = self
.storage_manager
@ -1877,7 +1848,10 @@ impl<
.load_to_general_reg(&mut self.buf, src2);
ASM::sub_reg64_reg64_reg64(&mut self.buf, dst_reg, src1_reg, src2_reg);
}
x => todo!("NumSubWrap: layout, {:?}", x),
IntWidth::I128 | IntWidth::U128 => {
let intrinsic = bitcode::NUM_SUB_WRAP_INT[int_width].to_string();
self.build_fn_call(dst, intrinsic, &[*src1, *src2], &[*layout, *layout], layout);
}
}
}

View file

@ -78,7 +78,6 @@ pub struct Env<'a> {
// These relocations likely will need a length.
// They may even need more definition, but this should be at least good enough for how we will use elf.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Relocation {
LocalData {
offset: u64,
@ -1005,7 +1004,7 @@ trait Backend<'a> {
}
}
/// build_run_low_level builds the low level opertation and outputs to the specified symbol.
/// build_run_low_level builds the low level operation and outputs to the specified symbol.
/// The builder must keep track of the symbol because it may be referred to later.
fn build_run_low_level(
&mut self,
@ -1068,9 +1067,6 @@ trait Backend<'a> {
LowLevel::NumAddChecked => {
self.build_num_add_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout)
}
LowLevel::NumSubChecked => {
self.build_num_sub_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout)
}
LowLevel::NumAcos => self.build_fn_call(
sym,
bitcode::NUM_ACOS[FloatWidth::F64].to_string(),
@ -1239,27 +1235,12 @@ trait Backend<'a> {
);
self.build_num_sub_wrap(sym, &args[0], &args[1], ret_layout)
}
LowLevel::NumSubSaturated => match self.interner().get_repr(*ret_layout) {
LayoutRepr::Builtin(Builtin::Int(int_width)) => self.build_fn_call(
sym,
bitcode::NUM_SUB_SATURATED_INT[int_width].to_string(),
args,
arg_layouts,
ret_layout,
),
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F32)) => {
self.build_num_sub(sym, &args[0], &args[1], ret_layout)
LowLevel::NumSubSaturated => {
self.build_num_sub_saturated(*sym, args[0], args[1], *ret_layout)
}
LayoutRepr::Builtin(Builtin::Float(FloatWidth::F64)) => {
// saturated sub is just normal sub
self.build_num_sub(sym, &args[0], &args[1], ret_layout)
LowLevel::NumSubChecked => {
self.build_num_sub_checked(sym, &args[0], &args[1], &arg_layouts[0], ret_layout)
}
LayoutRepr::Builtin(Builtin::Decimal) => {
// self.load_args_and_call_zig(backend, bitcode::DEC_SUB_SATURATED)
todo!()
}
_ => internal_error!("invalid return type"),
},
LowLevel::NumBitwiseAnd => {
if let LayoutRepr::Builtin(Builtin::Int(int_width)) =
self.interner().get_repr(*ret_layout)
@ -2392,16 +2373,6 @@ trait Backend<'a> {
layout: &InLayout<'a>,
);
/// build_num_sub_checked stores the sum of src1 and src2 into dst.
fn build_num_sub_checked(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
num_layout: &InLayout<'a>,
return_layout: &InLayout<'a>,
);
/// build_num_mul stores `src1 * src2` into dst.
fn build_num_mul(&mut self, dst: &Symbol, src1: &Symbol, src2: &Symbol, layout: &InLayout<'a>);
@ -2460,6 +2431,25 @@ trait Backend<'a> {
layout: &InLayout<'a>,
);
/// build_num_sub_saturated stores the `src1 - src2` difference into dst.
fn build_num_sub_saturated(
&mut self,
dst: Symbol,
src1: Symbol,
src2: Symbol,
layout: InLayout<'a>,
);
/// build_num_sub_checked stores the difference of src1 and src2 into dst.
fn build_num_sub_checked(
&mut self,
dst: &Symbol,
src1: &Symbol,
src2: &Symbol,
num_layout: &InLayout<'a>,
return_layout: &InLayout<'a>,
);
/// stores the `src1 & src2` into dst.
fn build_int_bitwise_and(
&mut self,

View file

@ -19,7 +19,7 @@ macro_rules! run_jit_function_raw {
let result = main();
if !$errors.is_empty() {
dbg!(&$errors);
eprintln!("{:?}", &$errors);
assert_eq!(
$errors,

View file

@ -12,8 +12,8 @@ use crate::llvm::refcounting::{
use inkwell::attributes::{Attribute, AttributeLoc};
use inkwell::types::{BasicType, BasicTypeEnum, StructType};
use inkwell::values::{
BasicValueEnum, CallSiteValue, FunctionValue, InstructionValue, IntValue, PointerValue,
StructValue,
BasicMetadataValueEnum, BasicValue, BasicValueEnum, CallSiteValue, FunctionValue,
InstructionValue, IntValue, PointerValue, StructValue,
};
use inkwell::AddressSpace;
use roc_error_macros::internal_error;
@ -47,6 +47,28 @@ pub fn call_bitcode_fn<'ctx>(
.build_bit_cast(ret, env.context.i128_type(), "return_i128")
.unwrap();
}
} else if env.target == roc_target::Target::MacArm64 {
// Essentially the same happens on macos arm64, but with array type [2xi64].
let i64_type = env.context.i64_type();
let arr_type = i64_type.array_type(2);
if ret.get_type() == arr_type.into() {
let alloca = env
.builder
.build_array_alloca(i64_type, i64_type.const_int(2, false), "dec_alloca")
.unwrap_or_else(|err| internal_error!("failed to build array alloca: {err}"));
let instruction = alloca.as_instruction_value().unwrap_or_else(|| {
internal_error!("failed to convert pointer to instruction value ({alloca:?})");
});
instruction.set_alignment(16).unwrap_or_else(|err| {
internal_error!(
"failed to set 16-byte alignment on instruction ({instruction:?}): {err}"
);
});
env.builder.new_build_store(alloca, ret);
return env
.builder
.new_build_load(env.context.i128_type(), alloca, "return_i128");
}
}
ret
@ -696,13 +718,18 @@ pub fn build_compare_wrapper<'a, 'ctx>(
"load_opaque",
);
let closure_data =
let closure_data: BasicMetadataValueEnum =
if layout_interner.is_passed_by_reference(closure_data_repr) {
closure_cast.into()
} else {
env.builder
.new_build_load(closure_type, closure_cast, "load_opaque");
.new_build_load(closure_type, closure_cast, "load_opaque")
.into()
};
env.arena
.alloc([value1.into(), value2.into(), closure_data.into()])
as &[_]
.alloc([value1.into(), value2.into(), closure_data])
as &[BasicMetadataValueEnum]
}
};

View file

@ -685,7 +685,7 @@ macro_rules! debug_info_init {
pub enum LlvmBackendMode {
/// Assumes primitives (roc_alloc, roc_panic, etc) are provided by the host
Binary,
BinaryDev,
BinaryWithExpect,
/// Creates a test wrapper around the main roc function to catch and report panics.
/// Provides a testing implementation of primitives (roc_alloc, roc_panic, etc)
BinaryGlue,
@ -698,7 +698,7 @@ impl LlvmBackendMode {
pub(crate) fn has_host(self) -> bool {
match self {
LlvmBackendMode::Binary => true,
LlvmBackendMode::BinaryDev => true,
LlvmBackendMode::BinaryWithExpect => true,
LlvmBackendMode::BinaryGlue => false,
LlvmBackendMode::GenTest => false,
LlvmBackendMode::WasmGenTest => true,
@ -710,7 +710,7 @@ impl LlvmBackendMode {
fn returns_roc_result(self) -> bool {
match self {
LlvmBackendMode::Binary => false,
LlvmBackendMode::BinaryDev => false,
LlvmBackendMode::BinaryWithExpect => false,
LlvmBackendMode::BinaryGlue => true,
LlvmBackendMode::GenTest => true,
LlvmBackendMode::WasmGenTest => true,
@ -721,7 +721,7 @@ impl LlvmBackendMode {
pub(crate) fn runs_expects(self) -> bool {
match self {
LlvmBackendMode::Binary => false,
LlvmBackendMode::BinaryDev => true,
LlvmBackendMode::BinaryWithExpect => true,
LlvmBackendMode::BinaryGlue => false,
LlvmBackendMode::GenTest => false,
LlvmBackendMode::WasmGenTest => false,
@ -3470,12 +3470,10 @@ pub(crate) fn build_exp_stmt<'a, 'ctx>(
variable: _,
remainder,
} => {
if env.mode.runs_expects() {
let location = build_string_literal(env, source_location);
let source = build_string_literal(env, source);
let message = scope.load_symbol(symbol);
env.call_dbg(env, location, source, message);
}
build_exp_stmt(
env,
@ -3531,7 +3529,7 @@ pub(crate) fn build_exp_stmt<'a, 'ctx>(
variables,
);
if let LlvmBackendMode::BinaryDev = env.mode {
if let LlvmBackendMode::BinaryWithExpect = env.mode {
crate::llvm::expect::notify_parent_expect(env, &shared_memory);
}
@ -4809,7 +4807,9 @@ fn expose_function_to_host_help_c_abi<'a, 'ctx>(
)
}
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => {}
LlvmBackendMode::Binary
| LlvmBackendMode::BinaryWithExpect
| LlvmBackendMode::BinaryGlue => {}
}
// a generic version that writes the result into a passed *u8 pointer
@ -4862,13 +4862,13 @@ fn expose_function_to_host_help_c_abi<'a, 'ctx>(
roc_call_result_type(env, roc_function.get_type().get_return_type().unwrap()).into()
}
LlvmBackendMode::Binary | LlvmBackendMode::BinaryDev | LlvmBackendMode::BinaryGlue => {
basic_type_from_layout(
LlvmBackendMode::Binary
| LlvmBackendMode::BinaryWithExpect
| LlvmBackendMode::BinaryGlue => basic_type_from_layout(
env,
layout_interner,
layout_interner.get_repr(return_layout),
)
}
),
};
let size: BasicValueEnum = return_type.size_of().unwrap().into();
@ -5657,7 +5657,7 @@ fn build_procedures_help<'a>(
use LlvmBackendMode::*;
match env.mode {
GenTest | WasmGenTest | CliTest => { /* no host, or exposing types is not supported */ }
Binary | BinaryDev | BinaryGlue => {
Binary | BinaryWithExpect | BinaryGlue => {
for (proc_name, alias_name, hels) in host_exposed_lambda_sets.iter() {
let ident_string = proc_name.name().as_unsuffixed_str(&env.interns);
let fn_name: String = format!("{}_{}", ident_string, hels.id.0);

View file

@ -29,7 +29,7 @@ pub(crate) struct SharedMemoryPointer<'ctx>(PointerValue<'ctx>);
impl<'ctx> SharedMemoryPointer<'ctx> {
pub(crate) fn get<'a, 'env>(env: &Env<'a, 'ctx, 'env>) -> Self {
let start_function = if let LlvmBackendMode::BinaryDev = env.mode {
let start_function = if let LlvmBackendMode::BinaryWithExpect = env.mode {
bitcode::UTILS_EXPECT_FAILED_START_SHARED_FILE
} else {
bitcode::UTILS_EXPECT_FAILED_START_SHARED_BUFFER

View file

@ -1137,15 +1137,37 @@ pub(crate) fn run_low_level<'a, 'ctx>(
)
}
NumIntCast => {
arguments!(arg);
arguments_with_layouts!((arg, arg_layout));
let to = basic_type_from_layout(env, layout_interner, layout_interner.get_repr(layout))
.into_int_type();
let to_signed = intwidth_from_layout(layout).is_signed();
let from_signed = intwidth_from_layout(arg_layout).is_signed();
let extend = intwidth_from_layout(layout).stack_size()
> intwidth_from_layout(arg_layout).stack_size();
//Examples given with sizes of 32, 16, and 8
let result = match (from_signed, to_signed, extend) {
//I16 -> I32
(true, true, true) => {
env.builder
.new_build_int_cast_sign_flag(arg.into_int_value(), to, to_signed, "inc_cast")
.into()
.build_int_s_extend(arg.into_int_value(), to, "inc_cast")
}
//U16 -> X32
(false, _, true) => {
env.builder
.build_int_z_extend(arg.into_int_value(), to, "inc_cast")
},
//I16 -> U32
(true,false,true)
//Any case where it is not an extension, also perhaps warn here?
| (_, _, false) => {
Ok(env.builder
.new_build_int_cast_sign_flag(arg.into_int_value(), to, to_signed, "inc_cast"))
}
};
let Ok(value) = result else { todo!() };
value.into()
}
NumToFloatCast => {
arguments_with_layouts!((arg, arg_layout));
@ -1748,7 +1770,7 @@ fn build_float_binop<'ctx>(
};
match op {
NumAdd => bd.new_build_float_add(lhs, rhs, "add_float").into(),
NumAdd | NumAddSaturated => bd.new_build_float_add(lhs, rhs, "add_float").into(),
NumAddChecked => {
let context = env.context;
@ -1775,7 +1797,7 @@ fn build_float_binop<'ctx>(
struct_value.into()
}
NumAddWrap => unreachable!("wrapping addition is not defined on floats"),
NumSub => bd.new_build_float_sub(lhs, rhs, "sub_float").into(),
NumSub | NumSubSaturated => bd.new_build_float_sub(lhs, rhs, "sub_float").into(),
NumSubChecked => {
let context = env.context;
@ -1802,8 +1824,7 @@ fn build_float_binop<'ctx>(
struct_value.into()
}
NumSubWrap => unreachable!("wrapping subtraction is not defined on floats"),
NumMul => bd.new_build_float_mul(lhs, rhs, "mul_float").into(),
NumMulSaturated => bd.new_build_float_mul(lhs, rhs, "mul_float").into(),
NumMul | NumMulSaturated => bd.new_build_float_mul(lhs, rhs, "mul_float").into(),
NumMulChecked => {
let context = env.context;
@ -1845,7 +1866,7 @@ fn build_float_binop<'ctx>(
&bitcode::NUM_POW[float_width],
),
_ => {
unreachable!("Unrecognized int binary operation: {:?}", op);
unreachable!("Unrecognized float binary operation: {:?}", op);
}
}
}
@ -2276,6 +2297,9 @@ fn build_dec_binop<'a, 'ctx>(
rhs,
"Decimal multiplication overflowed",
),
NumAddSaturated => dec_binary_op(env, bitcode::DEC_ADD_SATURATED, lhs, rhs),
NumSubSaturated => dec_binary_op(env, bitcode::DEC_SUB_SATURATED, lhs, rhs),
NumMulSaturated => dec_binary_op(env, bitcode::DEC_MUL_SATURATED, lhs, rhs),
NumDivFrac => dec_binop_with_unchecked(env, bitcode::DEC_DIV, lhs, rhs),
NumLt => call_bitcode_fn(env, &[lhs, rhs], &bitcode::NUM_LESS_THAN[IntWidth::I128]),

View file

@ -46,8 +46,6 @@ macro_rules! instruction_memargs {
#[derive(Debug)]
pub struct CodeBuilder<'a> {
pub arena: &'a Bump,
/// The main container for the instructions
code: Vec<'a, u8>,
@ -81,7 +79,6 @@ pub struct CodeBuilder<'a> {
impl<'a> CodeBuilder<'a> {
pub fn new(arena: &'a Bump) -> Self {
CodeBuilder {
arena,
code: Vec::with_capacity_in(1024, arena),
insertions: Vec::with_capacity_in(32, arena),
insert_bytes: Vec::with_capacity_in(64, arena),

View file

@ -38,8 +38,6 @@ const PTR_SIZE: u32 = {
const PTR_TYPE: ValueType = ValueType::I32;
pub const MEMORY_NAME: &str = "memory";
pub const BUILTINS_IMPORT_MODULE_NAME: &str = "env";
pub const STACK_POINTER_NAME: &str = "__stack_pointer";
pub struct Env<'a> {
pub arena: &'a Bump,

View file

@ -940,8 +940,7 @@ impl<'a> LowLevelCall<'a> {
NumAddWrap => match self.ret_layout_raw {
LayoutRepr::Builtin(Builtin::Int(width)) => match width {
IntWidth::I128 | IntWidth::U128 => {
// TODO: don't panic
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_OR_PANIC_INT[width])
self.load_args_and_call_zig(backend, &bitcode::NUM_ADD_WRAP_INT[width])
}
IntWidth::I64 | IntWidth::U64 => {
self.load_args(backend);
@ -1031,8 +1030,7 @@ impl<'a> LowLevelCall<'a> {
NumSubWrap => match self.ret_layout_raw {
LayoutRepr::Builtin(Builtin::Int(width)) => match width {
IntWidth::I128 | IntWidth::U128 => {
// TODO: don't panic
self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_OR_PANIC_INT[width])
self.load_args_and_call_zig(backend, &bitcode::NUM_SUB_WRAP_INT[width])
}
IntWidth::I64 | IntWidth::U64 => {
self.load_args(backend);
@ -1829,24 +1827,12 @@ impl<'a> LowLevelCall<'a> {
_ => panic_ret_type(),
}
}
NumPowInt => {
self.load_args(backend);
let base_type = CodeGenNumType::for_symbol(backend, self.arguments[0]);
let exponent_type = CodeGenNumType::for_symbol(backend, self.arguments[1]);
let ret_type = CodeGenNumType::from(self.ret_layout);
debug_assert!(base_type == exponent_type);
debug_assert!(exponent_type == ret_type);
let width = match ret_type {
CodeGenNumType::I32 => IntWidth::I32,
CodeGenNumType::I64 => IntWidth::I64,
CodeGenNumType::I128 => todo!("{:?} for I128", self.lowlevel),
_ => internal_error!("Invalid return type for pow: {:?}", ret_type),
};
NumPowInt => match self.ret_layout_raw {
LayoutRepr::Builtin(Builtin::Int(width)) => {
self.load_args_and_call_zig(backend, &bitcode::NUM_POW_INT[width])
}
_ => panic_ret_type(),
},
NumIsNan => num_is_nan(backend, self.arguments[0]),
NumIsInfinite => num_is_infinite(backend, self.arguments[0]),

View file

@ -1203,7 +1203,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \x -> g x
^^^
@ -1212,7 +1213,7 @@ mod test_reporting {
List List a
But you are trying to use it as:
But I expected the function to have return type:
List a
@ -1239,7 +1240,8 @@ mod test_reporting {
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
7 g = \x -> f [x]
^^^^^
@ -1248,7 +1250,7 @@ mod test_reporting {
List List b
But you are trying to use it as:
But I expected the function to have return type:
List b
@ -6019,25 +6021,6 @@ All branches in an `if` must have the same type!
"
);
test_report!(
closure_underscore_ident,
indoc!(
r"
\the_answer -> 100
"
),
@r"
NAMING PROBLEM in /code/proj/Main.roc
I am trying to parse an identifier here:
4 \the_answer -> 100
^
Underscores are not allowed in identifiers. Use camelCase instead!
"
);
test_report!(
#[ignore]
double_binop,
@ -10258,16 +10241,17 @@ All branches in an `if` must have the same type!
@r"
TYPE MISMATCH in /code/proj/Main.roc
This expression is used in an unexpected way:
This returns something that's incompatible with the return type of the
enclosing function:
5 f = \_ -> if Bool.true then {} else f {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is a value of type:
It a value of type:
{}
But you are trying to use it as:
But I expected the function to have return type:
* -> Str
"
@ -10416,9 +10400,10 @@ All branches in an `if` must have the same type!
10 olist : OList
11 olist =
12> when alist is
13> Nil -> @OList Nil
14> Cons _ lst -> lst
12 when alist is
13 Nil -> @OList Nil
14 Cons _ lst -> lst
^^^
This `lst` value is a:
@ -10449,6 +10434,7 @@ All branches in an `if` must have the same type!
This 2nd argument to `map` has an unexpected type:
4 A := U8
5 List.map [1u16, 2u16, 3u16] @A
^^
@ -10697,26 +10683,26 @@ All branches in an `if` must have the same type!
);
test_report!(
underscore_in_middle_of_identifier,
call_with_declared_identifier_with_more_than_one_underscore,
indoc!(
r"
f = \x, y, z -> x + y + z
f__arg = \x, y, z -> x + y + z
\a, _b -> f a var_name 1
\a, b -> f__arg a b 1
"
),
|golden| pretty_assertions::assert_eq!(
golden,
indoc!(
r"
SYNTAX PROBLEM in /code/proj/Main.roc
r"── NAMING PROBLEM in /code/proj/Main.roc ───────────────────────────────────────
Underscores are not allowed in identifier names:
I am trying to parse an identifier here:
6 \a, _b -> f a var_name 1
^^^^^^^^
4 f__arg = \x, y, z -> x + y + z
^^^^^^
I recommend using camelCase. It's the standard style in Roc code!
Snake case is allowed here, but only a single consecutive underscore
should be used.
"
),
)
@ -14556,7 +14542,7 @@ All branches in an `if` must have the same type!
@r###"
RETURN OUTSIDE OF FUNCTION in /code/proj/Main.roc
This `return` statement doesn't belong to a function:
This `return` doesn't belong to a function:
7 return x
^^^^^^^^
@ -14634,11 +14620,11 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r###"
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6> return "abc"
@ -14652,9 +14638,103 @@ All branches in an `if` must have the same type!
But I expected the function to have return type:
Num *
"#
);
test_report!(
try_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
validateNum = \num ->
if num > 5 then
Ok {}
else
Err TooBig
main! = \{} ->
Effect.putLine! "hello"
# this returns {}, so it's ignored
try validateNum 10
# this returns a value, so we are incorrectly
# dropping the parsed value
try List.get [1, 2, 3] 5
Ok {}
"#
),
@r###"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
19 try List.get [1, 2, 3] 5
^^^^^^^^^^^^^^^^^^^^^^^^
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"###
);
test_report!(
return_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# this outputs {}, so it's ignored
if 7 > 5 then
{}
else
return Err TooBig
# this outputs a value, so we are incorrectly
# dropping the parsed value
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
16> when List.get [1, 2, 3] 5 is
17> Ok item -> item
18> Err err ->
19> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
mismatch_only_early_returns,
indoc!(
@ -14668,11 +14748,11 @@ All branches in an `if` must have the same type!
myFunction 3
"#
),
@r###"
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This `return` statement doesn't match the return type of its enclosing
function:
This returns something that's incompatible with the return type of the
enclosing function:
5 if x == 5 then
6 return "abc"
@ -14687,9 +14767,181 @@ All branches in an `if` must have the same type!
But I expected the function to have return type:
Str
"#
);
test_report!(
try_with_ignored_output,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# not ignored, warning
try List.get [1, 2, 3] 5
# ignored, OK
_ = try List.get [1, 2, 3] 5
_ignored = try List.get [1, 2, 3] 5
Ok {}
"#
),
@r###"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
9 try List.get [1, 2, 3] 5
^^^^^^^^^^^^^^^^^^^^^^^^
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"###
);
test_report!(
return_with_ignored_output,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
# not ignored, warning
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# ignored, OK
_ =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
# also ignored, also OK
_ignored =
when List.get [1, 2, 3] 5 is
Ok item -> item
Err err ->
return Err err
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this expression is ignored:
9> when List.get [1, 2, 3] 5 is
10> Ok item -> item
11> Err err ->
12> return Err err
Standalone statements are required to produce an empty record, but the
type of this one is:
Num *
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
"#
);
test_report!(
no_early_return_in_bare_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
Num.toStr 123
Ok {}
"#
),
@r#"
IGNORED RESULT in /code/proj/Main.roc
The result of this call to `Num.toStr` is ignored:
8 Num.toStr 123
^^^^^^^^^
Standalone statements are required to produce an empty record, but the
type of this one is:
Str
If you still want to ignore it, assign it to `_`, like this:
_ = File.delete! "data.json"
LEFTOVER STATEMENT in /code/proj/Main.roc
This statement does not produce any effects:
8 Num.toStr 123
^^^^^^^^^^^^^
Standalone statements are only useful if they call effectful
functions.
Did you forget to use its result? If not, feel free to remove it.
"#
);
test_report!(
no_early_return_in_ignored_statement,
indoc!(
r#"
app [main!] { pf: platform "../../../../../crates/cli/tests/test-projects/test-platform-effects-zig/main.roc" }
import pf.Effect
main! = \{} ->
Effect.putLine! "hello"
_ignored = Num.toStr 123
Ok {}
"#
),
@r"
UNNECESSARY DEFINITION in /code/proj/Main.roc
This assignment doesn't introduce any new variables:
8 _ignored = Num.toStr 123
^^^^^^^^
Since it doesn't call any effectful functions, this assignment cannot
affect the program's behavior. If you don't need to use the value on
the right-hand side, consider removing the assignment.
"
);
test_report!(
mismatch_early_return_annotated_function,
indoc!(
@ -14726,6 +14978,225 @@ All branches in an `if` must have the same type!
"###
);
test_report!(
function_with_early_return_generalizes,
indoc!(
r#"
parseItemsWith = \parser ->
when List.mapTry ["123", "456"] parser is
Ok ok -> Ok ok
Err err ->
return Err err
u64Nums = parseItemsWith Str.toU64
u8Nums = parseItemsWith Str.toU8
"$(Inspect.toStr u64Nums) $(Inspect.toStr u8Nums)"
"#
),
@"" // no errors
);
test_report!(
keyword_try_with_non_result_target,
indoc!(
r#"
invalidTry = \{} ->
nonResult = "abc"
x = try nonResult
Ok (x * 2)
invalidTry {}
"#
),
@r"
INVALID TRY TARGET in /code/proj/Main.roc
This expression cannot be used as a `try` target:
6 x = try nonResult
^^^^^^^^^
I expected a Result, but it actually has type:
Str
Hint: Did you forget to wrap the value with an `Ok` or an `Err` tag?
"
);
test_report!(
question_try_with_non_result_target,
indoc!(
r#"
invalidTry = \{} ->
nonResult = "abc"
x = nonResult?
Ok (x * 2)
invalidTry {}
"#
),
@r"
INVALID TRY TARGET in /code/proj/Main.roc
This expression cannot be tried with the `?` operator:
6 x = nonResult?
^^^^^^^^^^
I expected a Result, but it actually has type:
Str
Hint: Did you forget to wrap the value with an `Ok` or an `Err` tag?
"
);
test_report!(
incompatible_try_errs,
indoc!(
r#"
incompatibleTrys = \{} ->
x = try Err 123
y = try Err "abc"
Ok (x + y)
incompatibleTrys {}
"#
),
@r#"
TYPE MISMATCH in /code/proj/Main.roc
This returns something that's incompatible with the return type of the
enclosing function:
5 x = try Err 123
6
7> y = try Err "abc"
8
9 Ok (x + y)
This returns an `Err` of type:
[Err Str, ]
But I expected the function to have return type:
[Err (Num *), ]a
"#
);
test_report!(
keyword_try_prefix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> try Result.mapErr ErrWrapper
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to this function has an unexpected type:
9> readFile filePath
10 |> try Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But this function needs its 1st argument to be:
Result ok a
"
);
test_report!(
keyword_try_suffix_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> Result.mapErr ErrWrapper
|> try
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to |> has an unexpected type:
9> readFile filePath
10 |> Result.mapErr ErrWrapper
This `readFile` call produces:
Str
But |> needs its 1st argument to be:
Result ok a
"
);
test_report!(
question_try_in_pipe,
indoc!(
r#"
readFile : Str -> Str
getFileContents : Str -> Result Str _
getFileContents = \filePath ->
contents =
readFile filePath
|> Result.mapErr? ErrWrapper
contents
getFileContents "file.txt"
"#
),
@r"
TYPE MISMATCH in /code/proj/Main.roc
This 1st argument to this function has an unexpected type:
9> readFile filePath
10 |> Result.mapErr? ErrWrapper
This `readFile` call produces:
Str
But this function needs its 1st argument to be:
Result ok a
"
);
test_report!(
leftover_statement,
indoc!(

View file

@ -16,7 +16,7 @@ use roc_builtins::roc::module_source;
use roc_can::abilities::{AbilitiesStore, PendingAbilitiesStore, ResolvedImpl};
use roc_can::constraint::{Constraint as ConstraintSoa, Constraints, TypeOrVar};
use roc_can::env::FxMode;
use roc_can::expr::{DbgLookup, Declarations, ExpectLookup, PendingDerives};
use roc_can::expr::{Declarations, ExpectLookup, PendingDerives};
use roc_can::module::{
canonicalize_module_defs, ExposedByModule, ExposedForModule, ExposedModuleTypes, Module,
ModuleParams, ResolvedImplementations, TypeState,
@ -571,7 +571,6 @@ pub struct ExpectMetadata<'a> {
}
type LocExpects = VecMap<Region, Vec<ExpectLookup>>;
type LocDbgs = VecMap<Symbol, DbgLookup>;
/// A message sent out _from_ a worker thread,
/// representing a result of work done, or a request for further work
@ -591,7 +590,7 @@ enum Msg<'a> {
module_timing: ModuleTiming,
abilities_store: AbilitiesStore,
loc_expects: LocExpects,
loc_dbgs: LocDbgs,
has_dbgs: bool,
#[cfg(debug_assertions)]
checkmate: Option<roc_checkmate::Collector>,
@ -661,7 +660,7 @@ struct CanAndCon {
module_docs: Option<ModuleDocumentation>,
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
enum PlatformPath<'a> {
NotSpecified,
Valid(To<'a>),
@ -881,7 +880,6 @@ impl std::fmt::Display for ModuleTiming {
/// A message sent _to_ a worker thread, describing the work to be done
#[derive(Debug)]
#[allow(dead_code)]
enum BuildTask<'a> {
LoadModule {
module_name: PQModuleName<'a>,
@ -1145,7 +1143,6 @@ impl<'a> LoadStart<'a> {
// Load the root module synchronously; we can't proceed until we have its id.
let root_start_time = Instant::now();
let load_result = load_filename(
arena,
filename.clone(),
@ -1290,7 +1287,6 @@ fn handle_root_type<'a>(
if let (Some(main_path), Some(cache_dir)) = (main_path.clone(), cache_dir) {
let mut messages = Vec::with_capacity(4);
messages.push(header_output.msg);
load_packages_from_main(
arena,
src_dir.clone(),
@ -1466,8 +1462,6 @@ pub fn load<'a>(
) -> Result<LoadResult<'a>, LoadingProblem<'a>> {
enum Threads {
Single,
#[allow(dead_code)]
Many(usize),
}
@ -2249,7 +2243,9 @@ fn update<'a>(
// If we're building an app module, and this was the platform
// specified in its header's `to` field, record it as our platform.
if state.opt_platform_shorthand == Some(config_shorthand) {
if state.opt_platform_shorthand == Some(config_shorthand)
|| state.platform_path == PlatformPath::RootIsModule
{
debug_assert!(state.platform_data.is_none());
state.platform_data = Some(PlatformData {
@ -2269,18 +2265,18 @@ fn update<'a>(
state.fx_mode = FxMode::PurityInference;
}
}
Builtin { .. } | Module { .. } => {
Builtin { .. } => {
if header.is_root_module {
debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified));
state.platform_path = PlatformPath::RootIsModule;
}
}
Hosted { exposes, .. } => {
Hosted { exposes, .. } | Module { exposes, .. } => {
if header.is_root_module {
debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified));
state.platform_path = PlatformPath::RootIsHosted;
}
// WARNING: This will be bypassed if we export a record of effectful functions. This is a temporary hacky method
if exposes
.iter()
.any(|exposed| exposed.value.is_effectful_fn())
@ -2343,7 +2339,6 @@ fn update<'a>(
extend_module_with_builtin_import(parsed, ModuleId::INSPECT);
extend_module_with_builtin_import(parsed, ModuleId::TASK);
}
state
.module_cache
.imports
@ -2449,7 +2444,7 @@ fn update<'a>(
mut module_timing,
abilities_store,
loc_expects,
loc_dbgs,
has_dbgs,
#[cfg(debug_assertions)]
checkmate,
@ -2466,7 +2461,7 @@ fn update<'a>(
.exposes
.insert(module_id, solved_module.exposed_vars_by_symbol.clone());
let should_include_expects = (!loc_expects.is_empty() || !loc_dbgs.is_empty()) && {
let should_include_expects = (!loc_expects.is_empty() || has_dbgs) && {
let modules = state.arc_modules.lock();
modules
.package_eq(module_id, state.root_id)
@ -2478,7 +2473,6 @@ fn update<'a>(
Some(Expectations {
expectations: loc_expects,
dbgs: loc_dbgs,
subs: solved_subs.clone().into_inner(),
path: path.to_owned(),
ident_ids: ident_ids.clone(),
@ -3685,8 +3679,6 @@ fn load_module<'a>(
#[derive(Debug)]
enum ShorthandPath {
/// e.g. "/home/rtfeldman/.cache/roc/0.1.0/oUkxSOI9zFGtSoIaMB40QPdrXphr1p1780eiui2iO9Mz"
#[allow(dead_code)]
// wasm warns FromHttpsUrl is unused, but errors if it is removed ¯\_(ツ)_/¯
FromHttpsUrl {
/// e.g. "/home/rtfeldman/.cache/roc/0.1.0/oUkxSOI9zFGtSoIaMB40QPdrXphr1p1780eiui2iO9Mz"
root_module_dir: PathBuf,
@ -4830,7 +4822,7 @@ fn run_solve<'a>(
let mut module = module;
let loc_expects = std::mem::take(&mut module.loc_expects);
let loc_dbgs = std::mem::take(&mut module.loc_dbgs);
let has_dbgs = module.has_dbgs;
let module = module;
let solve_result = {
@ -4945,7 +4937,7 @@ fn run_solve<'a>(
module_timing,
abilities_store,
loc_expects,
loc_dbgs,
has_dbgs,
#[cfg(debug_assertions)]
checkmate,
@ -5257,7 +5249,7 @@ fn canonicalize_and_constrain<'a>(
rigid_variables: module_output.rigid_variables,
abilities_store: module_output.scope.abilities_store,
loc_expects: module_output.loc_expects,
loc_dbgs: module_output.loc_dbgs,
has_dbgs: module_output.has_dbgs,
module_params: module_output.module_params,
};

View file

@ -1,6 +1,6 @@
use crate::docs::ModuleDocumentation;
use roc_can::constraint::{Constraint as ConstraintSoa, Constraints};
use roc_can::expr::{DbgLookup, ExpectLookup};
use roc_can::expr::ExpectLookup;
use roc_can::{
abilities::AbilitiesStore,
expr::{Declarations, PendingDerives},
@ -223,7 +223,6 @@ pub struct Expectations {
pub subs: roc_types::subs::Subs,
pub path: PathBuf,
pub expectations: VecMap<Region, Vec<ExpectLookup>>,
pub dbgs: VecMap<Symbol, DbgLookup>,
pub ident_ids: IdentIds,
}

View file

@ -373,6 +373,17 @@ impl<'a> LowerParams<'a> {
expr_stack.push(&mut loc_message.value);
expr_stack.push(&mut loc_continuation.value);
}
Try {
result_expr,
result_var: _,
return_var: _,
ok_payload_var: _,
err_payload_var: _,
err_ext_var: _,
kind: _,
} => {
expr_stack.push(&mut result_expr.value);
}
Return {
return_value,
return_var: _,

View file

@ -104,7 +104,8 @@ pub fn remove_module_param_arguments(
| TypeError::FxInTopLevel(_, _)
| TypeError::ExpectedEffectful(_, _)
| TypeError::UnsuffixedEffectfulFunction(_, _)
| TypeError::SuffixedPureFunction(_, _) => {}
| TypeError::SuffixedPureFunction(_, _)
| TypeError::InvalidTryTarget(_, _, _) => {}
}
}
}
@ -188,7 +189,8 @@ fn remove_for_reason(
| Reason::CrashArg
| Reason::ImportParams(_)
| Reason::Stmt(_)
| Reason::FunctionOutput => {}
| Reason::FunctionOutput
| Reason::TryResult => {}
}
}

View file

@ -198,18 +198,6 @@ pub(crate) fn infer_borrow_signatures<'a>(
borrow_signatures
}
#[allow(unused)]
fn infer_borrow_signature<'a>(
arena: &'a Bump,
interner: &impl LayoutInterner<'a>,
borrow_signatures: &'a mut BorrowSignatures<'a>,
proc: &Proc<'a>,
) -> BorrowSignature {
let mut state = State::new(arena, interner, borrow_signatures, proc);
state.inspect_stmt(interner, borrow_signatures, &proc.body);
state.borrow_signature
}
struct State<'state, 'arena> {
/// Argument symbols with a layout of `List *` or `Str`, i.e. the layouts
/// for which borrow inference might decide to pass as borrowed
@ -235,29 +223,6 @@ fn layout_to_ownership<'a>(
}
impl<'state, 'a> State<'state, 'a> {
fn new(
arena: &'a Bump,
interner: &impl LayoutInterner<'a>,
borrow_signatures: &mut BorrowSignatures<'a>,
proc: &Proc<'a>,
) -> Self {
let key = (proc.name.name(), proc.proc_layout(arena));
// initialize the borrow signature based on the layout if first time
let borrow_signature = borrow_signatures
.procs
.entry(key)
.or_insert_with(|| BorrowSignature::from_layouts(interner, key.1.arguments.iter()));
Self {
args: proc.args,
borrow_signature: *borrow_signature,
join_point_stack: Vec::new_in(arena),
join_points: MutMap::default(),
modified: false,
}
}
/// Mark the given argument symbol as Owned if the symbol participates in borrow inference
///
/// Currently argument symbols participate if `layout_to_ownership` returns `Borrowed` for their layout.

View file

@ -11,7 +11,7 @@ use crate::layout::{
use bumpalo::collections::{CollectIn, Vec};
use bumpalo::Bump;
use roc_can::abilities::SpecializationId;
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup};
use roc_can::expr::{AnnotatedMark, ClosureData, ExpectLookup, WhenBranch, WhenBranchPattern};
use roc_can::module::ExposedByModule;
use roc_collections::all::{default_hasher, BumpMap, BumpMapDefault, MutMap};
use roc_collections::VecMap;
@ -2930,7 +2930,7 @@ fn pattern_to_when_help(
body: Loc<roc_can::expr::Expr>,
symbol: Symbol,
) -> (Symbol, Loc<roc_can::expr::Expr>) {
use roc_can::expr::{Expr, WhenBranch, WhenBranchPattern};
use roc_can::expr::Expr;
let wrapped_body = Expr::When {
cond_var: pattern_var,
@ -5871,6 +5871,86 @@ pub fn with_hole<'a>(
}
}
}
Try {
result_expr,
result_var,
return_var,
ok_payload_var,
err_payload_var,
err_ext_var,
kind: _,
} => {
let ok_symbol = env.unique_symbol();
let err_symbol = env.unique_symbol();
let ok_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: Variable::EMPTY_TAG_UNION,
tag_name: "Ok".into(),
arguments: vec![(
ok_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(ok_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Var(ok_symbol, ok_payload_var)),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let err_branch = WhenBranch {
patterns: vec![WhenBranchPattern {
pattern: Loc::at_zero(roc_can::pattern::Pattern::AppliedTag {
whole_var: result_var,
ext_var: err_ext_var,
tag_name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(roc_can::pattern::Pattern::Identifier(err_symbol)),
)],
}),
degenerate: false,
}],
value: Loc::at_zero(Return {
return_var,
return_value: Box::new(Loc::at_zero(Tag {
tag_union_var: return_var,
ext_var: err_ext_var,
name: "Err".into(),
arguments: vec![(
err_payload_var,
Loc::at_zero(Var(err_symbol, err_payload_var)),
)],
})),
}),
guard: None,
redundant: RedundantMark::known_non_redundant(),
};
let result_region = result_expr.region;
let when_expr = When {
loc_cond: result_expr,
cond_var: result_var,
expr_var: ok_payload_var,
region: result_region,
branches: vec![ok_branch, err_branch],
branches_cond_var: result_var,
exhaustive: ExhaustiveMark::known_exhaustive(),
};
with_hole(
env,
when_expr,
variable,
procs,
layout_cache,
assigned,
hole,
)
}
Return {
return_value,
return_var,

View file

@ -28,12 +28,26 @@ pub struct Spaces<'a, T> {
pub after: &'a [CommentOrNewline<'a>],
}
impl<'a, T: Copy> ExtractSpaces<'a> for Spaces<'a, T> {
type Item = T;
fn extract_spaces(&self) -> Spaces<'a, T> {
*self
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct SpacesBefore<'a, T> {
pub before: &'a [CommentOrNewline<'a>],
pub item: T,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct SpacesAfter<'a, T> {
pub after: &'a [CommentOrNewline<'a>],
pub item: T,
}
#[derive(Copy, Clone, PartialEq)]
pub enum Spaced<'a, T> {
Item(T),
@ -401,6 +415,12 @@ pub enum TryTarget {
Result,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResultTryKind {
KeywordPrefix,
OperatorSuffix,
}
/// A parsed expression. This uses lifetimes extensively for two reasons:
///
/// 1. It uses Bump::alloc for all allocations, which returns a reference.
@ -491,13 +511,19 @@ pub enum Expr<'a> {
Backpassing(&'a [Loc<Pattern<'a>>], &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
Dbg,
DbgStmt(&'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
DbgStmt {
first: &'a Loc<Expr<'a>>,
extra_args: &'a [&'a Loc<Expr<'a>>],
continuation: &'a Loc<Expr<'a>>,
},
/// The `try` keyword that performs early return on errors
Try,
// This form of try is a desugared Result unwrapper
LowLevelTry(&'a Loc<Expr<'a>>, ResultTryKind),
// This form of debug is a desugared call to roc_dbg
LowLevelDbg(&'a (&'a str, &'a str), &'a Loc<Expr<'a>>, &'a Loc<Expr<'a>>),
// Application
/// To apply by name, do Apply(Var(...), ...)
@ -671,9 +697,18 @@ pub fn is_expr_suffixed(expr: &Expr) -> bool {
Expr::OpaqueRef(_) => false,
Expr::Backpassing(_, _, _) => false, // TODO: we might want to check this?
Expr::Dbg => false,
Expr::DbgStmt(a, b) => is_expr_suffixed(&a.value) || is_expr_suffixed(&b.value),
Expr::DbgStmt {
first,
extra_args,
continuation,
} => {
is_expr_suffixed(&first.value)
|| extra_args.iter().any(|a| is_expr_suffixed(&a.value))
|| is_expr_suffixed(&continuation.value)
}
Expr::LowLevelDbg(_, a, b) => is_expr_suffixed(&a.value) || is_expr_suffixed(&b.value),
Expr::Try => false,
Expr::LowLevelTry(loc_expr, _) => is_expr_suffixed(&loc_expr.value),
Expr::UnaryOp(a, _) => is_expr_suffixed(&a.value),
Expr::When(cond, branches) => {
is_expr_suffixed(&cond.value) || branches.iter().any(|x| is_when_branch_suffixed(x))
@ -861,7 +896,7 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> {
}
}
fn push_pending_from_expr(&mut self, expr: &'b Expr<'a>) {
pub fn push_pending_from_expr(&mut self, expr: &'b Expr<'a>) {
let mut expr_stack = vec![expr];
use Expr::*;
@ -930,16 +965,26 @@ impl<'a, 'b> RecursiveValueDefIter<'a, 'b> {
expr_stack.push(&a.value);
expr_stack.push(&b.value);
}
DbgStmt(condition, cont) => {
DbgStmt {
first,
extra_args,
continuation,
} => {
expr_stack.reserve(2);
expr_stack.push(&condition.value);
expr_stack.push(&cont.value);
expr_stack.push(&first.value);
for arg in extra_args.iter() {
expr_stack.push(&arg.value);
}
expr_stack.push(&continuation.value);
}
LowLevelDbg(_, condition, cont) => {
expr_stack.reserve(2);
expr_stack.push(&condition.value);
expr_stack.push(&cont.value);
}
LowLevelTry(loc_expr, _) => {
expr_stack.push(&loc_expr.value);
}
Return(return_value, after_return) => {
if let Some(after_return) = after_return {
expr_stack.reserve(2);
@ -2311,6 +2356,7 @@ impl_extract_spaces!(Tag);
impl_extract_spaces!(AssignedField<T>);
impl_extract_spaces!(TypeAnnotation);
impl_extract_spaces!(ImplementsAbility);
impl_extract_spaces!(ImplementsAbilities);
impl<'a, T: Copy> ExtractSpaces<'a> for Spaced<'a, T> {
type Item = T;
@ -2476,9 +2522,10 @@ impl<'a> Malformed for Expr<'a> {
Defs(defs, body) => defs.is_malformed() || body.is_malformed(),
Backpassing(args, call, body) => args.iter().any(|arg| arg.is_malformed()) || call.is_malformed() || body.is_malformed(),
Dbg => false,
DbgStmt(condition, continuation) => condition.is_malformed() || continuation.is_malformed(),
DbgStmt { first, extra_args, continuation } => first.is_malformed() || extra_args.iter().any(|a| a.is_malformed()) || continuation.is_malformed(),
LowLevelDbg(_, condition, continuation) => condition.is_malformed() || continuation.is_malformed(),
Try => false,
LowLevelTry(loc_expr, _) => loc_expr.is_malformed(),
Return(return_value, after_return) => return_value.is_malformed() || after_return.is_some_and(|ar| ar.is_malformed()),
Apply(func, args, _) => func.is_malformed() || args.iter().any(|arg| arg.is_malformed()),
BinOps(firsts, last) => firsts.iter().any(|(expr, _)| expr.is_malformed()) || last.is_malformed(),

View file

@ -182,26 +182,25 @@ fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, Suffix<'a>>, EExpr
/// pattern later
fn loc_term_or_underscore_or_conditional<'a>(
options: ExprParseOptions,
allow_conditional: bool,
) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| {
if allow_conditional {
match loc_conditional(options).parse(arena, state.clone(), min_indent) {
Ok((_, expr, state)) => return Ok((MadeProgress, expr, state)),
Err((MadeProgress, e)) => return Err((MadeProgress, e)),
Err((NoProgress, _)) => {}
}
}
loc_term_or_underscore(options).parse(arena, state, min_indent)
}
}
fn loc_conditional<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
one_of!(
loc_expr_in_parens_etc_help(),
loc(specialize_err(EExpr::If, if_expr_help(options))),
loc(specialize_err(EExpr::When, when::when_expr_help(options))),
loc(specialize_err(EExpr::Str, string_like_literal_help())),
loc(specialize_err(
EExpr::Number,
positive_number_literal_help()
)),
loc(specialize_err(EExpr::Closure, closure_help(options))),
loc(crash_kw()),
loc(specialize_err(EExpr::Dbg, dbg_kw())),
loc(try_kw()),
loc(underscore_expression()),
loc(record_literal_help()),
loc(specialize_err(EExpr::List, list_literal_help())),
ident_seq(),
)
.trace("term_or_underscore_or_conditional")
}
/// In some contexts we want to parse the `_` as an expression, so it can then be turned into a
@ -217,6 +216,7 @@ fn loc_term_or_underscore<'a>(
positive_number_literal_help()
)),
loc(specialize_err(EExpr::Closure, closure_help(options))),
loc(crash_kw()),
loc(specialize_err(EExpr::Dbg, dbg_kw())),
loc(try_kw()),
loc(underscore_expression()),
@ -294,12 +294,21 @@ fn crash_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
fn loc_possibly_negative_or_negated_term<'a>(
options: ExprParseOptions,
allow_negate: bool,
allow_conditional: bool,
) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
let parse_unary_negate = move |arena, state: State<'a>, min_indent: u32| {
let initial = state.clone();
let (_, (loc_op, loc_expr), state) =
and(loc(unary_negate()), loc_term(options)).parse(arena, state, min_indent)?;
if !allow_negate {
return Err((NoProgress, EExpr::UnaryNegate(state.pos())));
}
let (_, (loc_op, loc_expr), state) = and(
loc(unary_negate()),
loc_possibly_negative_or_negated_term(options, true, false),
)
.parse(arena, state, min_indent)?;
let loc_expr = numeric_negate_expression(arena, initial, loc_op, loc_expr, &[]);
@ -307,20 +316,26 @@ fn loc_possibly_negative_or_negated_term<'a>(
};
one_of![
parse_unary_negate,
parse_unary_negate.trace("negate_expr"),
// this will parse negative numbers, which the unary negate thing up top doesn't (for now)
loc(specialize_err(EExpr::Number, number_literal_help())),
// loc(specialize_err(EExpr::Number, number_literal_help())),
loc(map_with_arena(
and(
loc(byte(b'!', EExpr::Start)),
space0_before_e(loc_term(options), EExpr::IndentStart)
loc(unary_not()).trace("not"),
space0_before_e(
loc_possibly_negative_or_negated_term(options, true, false),
EExpr::IndentStart
)
.trace("not_expr")
),
|arena: &'a Bump, (loc_op, loc_expr): (Loc<_>, _)| {
Expr::UnaryOp(arena.alloc(loc_expr), Loc::at(loc_op.region, UnaryOp::Not))
}
)),
loc_term_or_underscore_or_conditional(options)
))
.trace("not_expr"),
loc_term_or_underscore_or_conditional(options, allow_conditional)
]
.trace("loc_possibly_negative_or_negated_term")
}
fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> {
@ -328,18 +343,22 @@ fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> {
}
fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| {
move |_arena: &'a Bump, state: State<'a>, min_indent: u32| {
// a minus is unary iff
//
// - it is preceded by whitespace (spaces, newlines, comments)
// - it is not followed by whitespace
let followed_by_whitespace = state
// - it is not followed by >, making ->
let followed_by_illegal_char = state
.bytes()
.get(1)
.map(|c| c.is_ascii_whitespace() || *c == b'#')
.map(|c| c.is_ascii_whitespace() || *c == b'#' || *c == b'>')
.unwrap_or(false);
if state.bytes().starts_with(b"-") && !followed_by_whitespace {
if state.bytes().starts_with(b"-")
&& !followed_by_illegal_char
&& state.column() >= min_indent
{
// the negate is only unary if it is not followed by whitespace
let state = state.advance(1);
Ok((MadeProgress, (), state))
@ -350,6 +369,19 @@ fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
}
}
fn unary_not<'a>() -> impl Parser<'a, (), EExpr<'a>> {
move |_arena: &'a Bump, state: State<'a>, min_indent: u32| {
let followed_by_equals = state.bytes().get(1).map(|c| *c == b'=').unwrap_or(false);
if state.bytes().starts_with(b"!") && !followed_by_equals && state.column() >= min_indent {
let state = state.advance(1);
Ok((MadeProgress, (), state))
} else {
Err((NoProgress, EExpr::UnaryNot(state.pos())))
}
}
}
/// Entry point for parsing an expression.
fn expr_start<'a>(options: ExprParseOptions) -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>> {
one_of![
@ -378,10 +410,11 @@ fn parse_expr_operator_chain<'a>(
) -> Result<(Progress, Expr<'a>, State<'a>), (Progress, EExpr<'a>)> {
let line_indent = state.line_indent();
let (_, expr, state) =
loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent)?;
let (_, expr, state) = loc_possibly_negative_or_negated_term(options, true, true)
.parse(arena, state, min_indent)?;
let mut initial_state = state.clone();
let mut end = state.pos();
let (spaces_before_op, state) =
match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
@ -402,10 +435,13 @@ fn parse_expr_operator_chain<'a>(
let call_min_indent = line_indent + 1;
loop {
let allow_negate = state.pos() > end;
let parser = skip_first(
crate::blankspace::check_indent(EExpr::IndentEnd),
loc_term_or_underscore(options),
);
loc_possibly_negative_or_negated_term(options, allow_negate, false),
)
.trace("term_or_underscore");
end = state.pos();
match parser.parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => return Err((MadeProgress, f)),
Err((NoProgress, _)) => {
@ -459,7 +495,7 @@ fn parse_expr_after_apply<'a>(
before_op: State<'a>,
initial_state: State<'a>,
) -> Result<(Progress, Expr<'a>, State<'a>), (Progress, EExpr<'a>)> {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), min_indent) {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -572,11 +608,11 @@ fn parse_stmt_operator_chain<'a>(
) -> Result<(Progress, Stmt<'a>, State<'a>), (Progress, EExpr<'a>)> {
let line_indent = state.line_indent();
let (_, expr, state) =
loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent)?;
let (_, expr, state) = loc_possibly_negative_or_negated_term(options, true, true)
.parse(arena, state, min_indent)?;
let mut initial_state = state.clone();
let end = state.pos();
let mut end = state.pos();
let (spaces_before_op, state) =
match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
@ -597,10 +633,12 @@ fn parse_stmt_operator_chain<'a>(
let call_min_indent = line_indent + 1;
loop {
let allow_negate = state.pos() > end;
let parser = skip_first(
crate::blankspace::check_indent(EExpr::IndentEnd),
loc_term_or_underscore(options),
loc_possibly_negative_or_negated_term(options, allow_negate, false),
);
end = state.pos();
match parser.parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => return Err((MadeProgress, f)),
Ok((
@ -845,13 +883,13 @@ fn numeric_negate_expression<'a, T>(
let region = Region::new(start, expr.region.end());
let new_expr = match expr.value {
Expr::Num(string) => {
Expr::Num(string) if !string.starts_with('-') => {
let new_string =
unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) };
Expr::Num(new_string)
}
Expr::Float(string) => {
Expr::Float(string) if !string.starts_with('-') => {
let new_string =
unsafe { std::str::from_utf8_unchecked(&state.bytes()[..string.len() + 1]) };
@ -860,11 +898,11 @@ fn numeric_negate_expression<'a, T>(
Expr::NonBase10Int {
string,
base,
is_negative,
is_negative: false,
} => {
// don't include the minus sign here; it will not be parsed right
Expr::NonBase10Int {
is_negative: !is_negative,
is_negative: true,
string,
base,
}
@ -1151,9 +1189,7 @@ fn parse_stmt_alias_or_opaque<'a>(
AliasOrOpaque::Alias => {
let (_, signature, state) = alias_signature().parse(arena, state, min_indent)?;
// TODO: this code used to be broken and it dropped the spaces after the operator.
// The formatter is not expecting this, so let's keep it as is for now.
// let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let header = TypeHeader {
name: Loc::at(expr.region, name),
@ -1172,9 +1208,7 @@ fn parse_stmt_alias_or_opaque<'a>(
let (_, (signature, derived), state) =
opaque_signature().parse(arena, state, indented_more)?;
// TODO: this code used to be broken and it dropped the spaces after the operator.
// The formatter is not expecting this, so let's keep it as is for now.
// let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let signature = signature.map(|v| v.maybe_before(arena, spaces_after_operator));
let header = TypeHeader {
name: Loc::at(expr.region, name),
@ -1560,10 +1594,10 @@ fn parse_after_binop<'a>(
mut expr_state: ExprState<'a>,
loc_op: Loc<BinOp>,
) -> ParseResult<'a, Expr<'a>, EExpr<'a>> {
match loc_possibly_negative_or_negated_term(options).parse(
match loc_possibly_negative_or_negated_term(options, true, true).parse(
arena,
state.clone(),
call_min_indent,
min_indent,
) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, mut new_expr, state)) => {
@ -1855,7 +1889,7 @@ fn parse_expr_end<'a>(
Err((NoProgress, _)) => {
let before_op = state.clone();
// try an operator
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), min_indent) {
match loc(bin_op(check_for_defs)).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -1899,7 +1933,7 @@ fn parse_stmt_after_apply<'a>(
initial_state: State<'a>,
) -> ParseResult<'a, Stmt<'a>, EExpr<'a>> {
let before_op = state.clone();
match loc(operator()).parse(arena, state.clone(), min_indent) {
match loc(operator()).parse(arena, state.clone(), call_min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena);
@ -2172,8 +2206,9 @@ fn expr_to_pattern_help<'a>(arena: &'a Bump, expr: &Expr<'a>) -> Result<Pattern<
| Expr::If { .. }
| Expr::When(_, _)
| Expr::Dbg
| Expr::DbgStmt(_, _)
| Expr::DbgStmt { .. }
| Expr::LowLevelDbg(_, _, _)
| Expr::LowLevelTry(_, _)
| Expr::Return(_, _)
| Expr::MalformedSuffixed(..)
| Expr::PrecedenceConflict { .. }
@ -2589,7 +2624,8 @@ fn if_branch<'a>() -> impl Parser<'a, (Loc<Expr<'a>>, Loc<Expr<'a>>), EIf<'a>> {
specialize_err_ref(EIf::Condition, loc_expr(true)),
EIf::IndentCondition,
EIf::IndentThenToken,
),
)
.trace("if_condition"),
parser::keyword(keyword::THEN, EIf::Then),
),
map_with_arena(
@ -2607,6 +2643,7 @@ fn if_branch<'a>() -> impl Parser<'a, (Loc<Expr<'a>>, Loc<Expr<'a>>), EIf<'a>> {
),
parser::keyword(keyword::ELSE, EIf::Else),
)
.trace("if_branch")
}
fn expect_help<'a>(
@ -2686,18 +2723,19 @@ fn try_kw<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
fn import<'a>() -> impl Parser<'a, ValueDef<'a>, EImport<'a>> {
skip_second(
skip_first(
indented_seq_skip_first(
parser::keyword(keyword::IMPORT, EImport::Import),
increment_min_indent(one_of!(import_body(), import_ingested_file_body())),
one_of!(import_body(), import_ingested_file_body()),
),
require_newline_or_eof(EImport::EndNewline),
)
}
fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<'a>> {
move |arena: &'a Bump, state, min_indent| {
let (_, _, state) =
parser::keyword(keyword::IF, EIf::If).parse(arena, state, min_indent)?;
(move |arena: &'a Bump, state, min_indent| {
let (_, _, state) = parser::keyword(keyword::IF, EIf::If)
.trace("if_kw")
.parse(arena, state, min_indent)?;
let if_indent = state.line_indent();
@ -2706,8 +2744,9 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
let mut loop_state = state;
let state_final_else = loop {
let (_, (cond, then_branch), state) =
if_branch().parse(arena, loop_state, min_indent)?;
let (_, (cond, then_branch), state) = if_branch()
.parse(arena, loop_state, min_indent)
.map_err(|(_p, err)| (MadeProgress, err))?;
branches.push((cond, then_branch));
@ -2727,8 +2766,12 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
}
};
let has_newline_next = require_newline_or_eof(EExpr::IndentEnd)
.parse(arena, state_final_else.clone(), min_indent)
.is_ok();
let else_indent = state_final_else.line_indent();
let indented_else = else_indent > if_indent;
let indented_else = else_indent > if_indent && has_newline_next;
let min_indent = if !indented_else {
else_indent + 1
@ -2760,7 +2803,8 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
};
Ok((MadeProgress, expr, state))
}
})
.trace("if")
}
/// Parse a block of statements (parser combinator version of `parse_block`)
@ -3086,16 +3130,13 @@ fn stmts_to_defs<'a>(
_,
) = e
{
if args.len() != 1 {
// TODO: this should be done in can, not parsing!
return Err(EExpr::Dbg(
EExpect::DbgArity(sp_stmt.item.region.start()),
sp_stmt.item.region.start(),
));
}
let condition = &args[0];
let rest = stmts_to_expr(&stmts[i + 1..], arena)?;
let e = Expr::DbgStmt(condition, arena.alloc(rest));
let e = Expr::DbgStmt {
first: condition,
extra_args: &args[1..],
continuation: arena.alloc(rest),
};
let e = if sp_stmt.before.is_empty() {
e
@ -3160,7 +3201,7 @@ fn stmts_to_defs<'a>(
)),
) = (td, stmts.get(i + 1).map(|s| (s.before, s.item.value)))
{
if spaces_middle.len() <= 1
if (spaces_middle.len() <= 1 && !ends_with_spaces_conservative(&ann_type.value))
|| header
.vars
.first()
@ -3211,7 +3252,11 @@ fn stmts_to_defs<'a>(
if exprify_dbg {
let e = if i + 1 < stmts.len() {
let rest = stmts_to_expr(&stmts[i + 1..], arena)?;
Expr::DbgStmt(arena.alloc(condition), arena.alloc(rest))
Expr::DbgStmt {
first: arena.alloc(condition),
extra_args: &[],
continuation: arena.alloc(rest),
}
} else {
Expr::Apply(
arena.alloc(Loc {
@ -3244,7 +3289,8 @@ fn stmts_to_defs<'a>(
)),
) = (vd, stmts.get(i + 1).map(|s| (s.before, s.item.value)))
{
if spaces_middle.len() <= 1 || ann_pattern.value.equivalent(&loc_pattern.value)
if (spaces_middle.len() <= 1 && !ends_with_spaces_conservative(&ann_type.value))
|| ann_pattern.value.equivalent(&loc_pattern.value)
{
let region = Region::span_across(&loc_pattern.region, &loc_def_expr.region);
@ -3277,6 +3323,64 @@ fn stmts_to_defs<'a>(
Ok((defs, last_expr))
}
fn ends_with_spaces_conservative(ty: &TypeAnnotation<'_>) -> bool {
match ty {
TypeAnnotation::Function(_, _, res) => ends_with_spaces_conservative(&res.value),
TypeAnnotation::Apply(_, _, args) => args
.last()
.map_or(false, |a| ends_with_spaces_conservative(&a.value)),
TypeAnnotation::As(_, _, type_header) => type_header
.vars
.last()
.map_or(false, |v| pat_ends_with_spaces_conservative(&v.value)),
TypeAnnotation::Record { fields: _, ext }
| TypeAnnotation::Tuple { elems: _, ext }
| TypeAnnotation::TagUnion { ext, tags: _ } => {
ext.map_or(false, |e| ends_with_spaces_conservative(&e.value))
}
TypeAnnotation::BoundVariable(_) | TypeAnnotation::Inferred | TypeAnnotation::Wildcard => {
false
}
TypeAnnotation::Where(_, clauses) => clauses.last().map_or(false, |c| {
c.value
.abilities
.last()
.map_or(false, |a| ends_with_spaces_conservative(&a.value))
}),
TypeAnnotation::SpaceBefore(inner, _) => ends_with_spaces_conservative(inner),
TypeAnnotation::SpaceAfter(_, _) => true,
TypeAnnotation::Malformed(_) => false,
}
}
fn pat_ends_with_spaces_conservative(pat: &Pattern<'_>) -> bool {
match pat {
Pattern::Identifier { .. }
| Pattern::QualifiedIdentifier { .. }
| Pattern::Tag(_)
| Pattern::NumLiteral(_)
| Pattern::FloatLiteral(_)
| Pattern::StrLiteral(_)
| Pattern::Underscore(_)
| Pattern::SingleQuote(_)
| Pattern::Tuple(_)
| Pattern::List(_)
| Pattern::NonBase10Literal { .. }
| Pattern::ListRest(_)
| Pattern::As(_, _)
| Pattern::OpaqueRef(_) => false,
Pattern::Apply(_, args) => args
.last()
.map_or(false, |a| pat_ends_with_spaces_conservative(&a.value)),
Pattern::RecordDestructure(_) => false,
Pattern::RequiredField(_, _) => unreachable!(),
Pattern::OptionalField(_, _) => unreachable!(),
Pattern::SpaceBefore(inner, _) => pat_ends_with_spaces_conservative(inner),
Pattern::SpaceAfter(_, _) => true,
Pattern::Malformed(_) | Pattern::MalformedIdent(_, _) => false,
}
}
/// Given a type alias and a value definition, join them into a AnnotatedBody
pub fn join_alias_to_body<'a>(
arena: &'a Bump,
@ -3753,26 +3857,6 @@ fn positive_number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> {
)
}
fn number_literal_help<'a>() -> impl Parser<'a, Expr<'a>, ENumber> {
map(crate::number_literal::number_literal(), |literal| {
use crate::number_literal::NumLiteral::*;
match literal {
Num(s) => Expr::Num(s),
Float(s) => Expr::Float(s),
NonBase10Int {
string,
base,
is_negative,
} => Expr::NonBase10Int {
string,
base,
is_negative,
},
}
})
}
const BINOP_CHAR_SET: &[u8] = b"+-/*=.<>:&|^?%!";
const BINOP_CHAR_MASK: [bool; 125] = {
@ -3799,9 +3883,9 @@ enum OperatorOrDef {
}
fn bin_op<'a>(check_for_defs: bool) -> impl Parser<'a, BinOp, EExpr<'a>> {
move |_, state: State<'a>, _m| {
move |_, state: State<'a>, min_indent| {
let start = state.pos();
let (_, op, state) = operator_help(EExpr::Start, EExpr::BadOperator, state)?;
let (_, op, state) = operator_help(EExpr::Start, EExpr::BadOperator, state, min_indent)?;
let err_progress = if check_for_defs {
MadeProgress
} else {
@ -3822,7 +3906,8 @@ fn bin_op<'a>(check_for_defs: bool) -> impl Parser<'a, BinOp, EExpr<'a>> {
}
fn operator<'a>() -> impl Parser<'a, OperatorOrDef, EExpr<'a>> {
(move |_, state, _m| operator_help(EExpr::Start, EExpr::BadOperator, state)).trace("operator")
(move |_, state, min_indent| operator_help(EExpr::Start, EExpr::BadOperator, state, min_indent))
.trace("operator")
}
#[inline(always)]
@ -3830,6 +3915,7 @@ fn operator_help<'a, F, G, E>(
to_expectation: F,
to_error: G,
mut state: State<'a>,
min_indent: u32,
) -> ParseResult<'a, OperatorOrDef, E>
where
F: Fn(Position) -> E,
@ -3855,7 +3941,21 @@ where
match chomped {
"" => Err((NoProgress, to_expectation(state.pos()))),
"+" => good!(OperatorOrDef::BinOp(BinOp::Plus), 1),
"-" => good!(OperatorOrDef::BinOp(BinOp::Minus), 1),
"-" => {
// A unary minus must only match if we are at the correct indent level; indent level doesn't
// matter for the rest of the operators.
// Note that a unary minus is distinguished by not having a space after it
let has_whitespace = matches!(
state.bytes().get(1),
Some(b' ' | b'#' | b'\n' | b'\r' | b'\t') | None
);
if !has_whitespace && state.column() < min_indent {
return Err((NoProgress, to_expectation(state.pos())));
}
good!(OperatorOrDef::BinOp(BinOp::Minus), 1)
}
"*" => good!(OperatorOrDef::BinOp(BinOp::Star), 1),
"/" => good!(OperatorOrDef::BinOp(BinOp::Slash), 1),
"%" => good!(OperatorOrDef::BinOp(BinOp::Percent), 1),
@ -3883,6 +3983,10 @@ where
}
"<-" => good!(OperatorOrDef::Backpassing, 2),
"!" => Err((NoProgress, to_error("!", state.pos()))),
"&" => {
// makes no progress, so it does not interfere with record updaters / `&foo`
Err((NoProgress, to_error("&", state.pos())))
}
_ => bad_made_progress!(chomped),
}
}

View file

@ -231,6 +231,7 @@ pub enum BadIdent {
UnderscoreAlone(Position),
UnderscoreInMiddle(Position),
TooManyUnderscores(Position),
UnderscoreAtStart {
position: Position,
/// If this variable was already declared in a pattern (e.g. \_x -> _x),
@ -252,11 +253,21 @@ fn is_alnum(ch: char) -> bool {
}
fn chomp_lowercase_part(buffer: &[u8]) -> Result<&str, Progress> {
chomp_part(char::is_lowercase, is_alnum, true, buffer)
chomp_part(
char::is_lowercase,
is_plausible_ident_continue,
true,
buffer,
)
}
fn chomp_uppercase_part(buffer: &[u8]) -> Result<&str, Progress> {
chomp_part(char::is_uppercase, is_alnum, false, buffer)
chomp_part(
char::is_uppercase,
is_plausible_ident_continue,
false,
buffer,
)
}
fn chomp_anycase_part(buffer: &[u8]) -> Result<&str, Progress> {
@ -265,7 +276,12 @@ fn chomp_anycase_part(buffer: &[u8]) -> Result<&str, Progress> {
let allow_bang =
char::from_utf8_slice_start(buffer).map_or(false, |(leading, _)| leading.is_lowercase());
chomp_part(char::is_alphabetic, is_alnum, allow_bang, buffer)
chomp_part(
char::is_alphabetic,
is_plausible_ident_continue,
allow_bang,
buffer,
)
}
fn chomp_integer_part(buffer: &[u8]) -> Result<&str, Progress> {
@ -429,9 +445,16 @@ fn chomp_opaque_ref(buffer: &[u8], pos: Position) -> Result<&str, BadIdent> {
Err(bad_ident(pos.bump_column(width as u32)))
} else {
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..width]) };
if value.contains('_') {
// we don't allow underscores in the middle of a type identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err(BadIdent::UnderscoreInMiddle(pos.bump_column(width as u32)))
} else {
Ok(value)
}
}
}
Err(_) => Err(bad_ident(pos.bump_column(1))),
}
}
@ -486,7 +509,7 @@ fn chomp_identifier_chain<'a>(
}
while let Ok((ch, width)) = char::from_utf8_slice_start(&buffer[chomped..]) {
if ch.is_alphabetic() || ch.is_ascii_digit() {
if ch.is_alphabetic() || ch.is_ascii_digit() || ch == '_' {
chomped += width;
} else if ch == '!' && !first_is_uppercase {
chomped += width;
@ -556,19 +579,20 @@ fn chomp_identifier_chain<'a>(
BadIdent::WeirdDotAccess(pos.bump_column(chomped as u32 + width)),
)),
}
} else if let Ok(('_', _)) = char::from_utf8_slice_start(&buffer[chomped..]) {
// we don't allow underscores in the middle of an identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err((
chomped as u32 + 1,
BadIdent::UnderscoreInMiddle(pos.bump_column(chomped as u32 + 1)),
))
} else if first_is_uppercase {
// just one segment, starting with an uppercase letter; that's a tag
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) };
if value.contains('_') {
// we don't allow underscores in the middle of a tag identifier
// but still parse them (and generate a malformed identifier)
// to give good error messages for this case
Err((
chomped as u32,
BadIdent::UnderscoreInMiddle(pos.bump_column(chomped as u32)),
))
} else {
Ok((chomped as u32, Ident::Tag(value)))
}
} else {
// just one segment, starting with a lowercase letter; that's a normal identifier
let value = unsafe { std::str::from_utf8_unchecked(&buffer[..chomped]) };
@ -689,3 +713,121 @@ fn chomp_access_chain<'a>(buffer: &'a [u8], parts: &mut Vec<'a, Accessor<'a>>) -
Ok(chomped as u32)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_ident_parses<'a>(arena: &'a Bump, ident: &str, expected: Ident<'a>) {
let s = State::new(ident.as_bytes());
let (_, id, _) = parse_ident(arena, s, 0).unwrap();
assert_eq!(id, expected);
}
fn assert_ident_parses_tag(arena: &Bump, ident: &str) {
assert_ident_parses(arena, ident, Ident::Tag(ident));
}
fn assert_ident_parses_opaque(arena: &Bump, ident: &str) {
assert_ident_parses(arena, ident, Ident::OpaqueRef(ident));
}
fn assert_ident_parses_simple_access(arena: &Bump, ident: &str) {
assert_ident_parses(
arena,
ident,
Ident::Access {
module_name: "",
parts: arena.alloc([Accessor::RecordField(ident)]),
},
);
}
fn assert_ident_parses_malformed(arena: &Bump, ident: &str, pos: Position) {
assert_ident_parses(
arena,
ident,
Ident::Malformed(ident, BadIdent::UnderscoreInMiddle(pos)),
);
}
#[test]
fn test_parse_ident_lowercase_camel() {
let arena = Bump::new();
assert_ident_parses_simple_access(&arena, "hello");
assert_ident_parses_simple_access(&arena, "hello23");
assert_ident_parses_simple_access(&arena, "helloWorld");
assert_ident_parses_simple_access(&arena, "helloWorld23");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag_");
assert_ident_parses_simple_access(&arena, "helloworldthisisquiteatag_");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag23");
assert_ident_parses_simple_access(&arena, "helloWorldThisIsQuiteATag23_");
assert_ident_parses_simple_access(&arena, "helloworldthisisquiteatag23_");
}
#[test]
fn test_parse_ident_lowercase_snake() {
let arena = Bump::new();
assert_ident_parses_simple_access(&arena, "hello_world");
assert_ident_parses_simple_access(&arena, "hello_world23");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var_");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var23");
assert_ident_parses_simple_access(&arena, "hello_world_this_is_quite_a_var23_");
}
#[test]
fn test_parse_tag_camel() {
let arena = Bump::new();
assert_ident_parses_tag(&arena, "Hello");
assert_ident_parses_tag(&arena, "Hello23");
assert_ident_parses_tag(&arena, "HelloWorld");
assert_ident_parses_tag(&arena, "HelloWorld23");
assert_ident_parses_tag(&arena, "HelloWorldThisIsQuiteATag");
assert_ident_parses_tag(&arena, "HelloWorldThisIsQuiteATag23");
}
#[test]
fn test_parse_tag_snake_is_malformed() {
let arena = Bump::new();
assert_ident_parses_malformed(&arena, "Hello_World", Position { offset: 11 });
assert_ident_parses_malformed(&arena, "Hello_World23", Position { offset: 13 });
assert_ident_parses_malformed(
&arena,
"Hello_World_This_Is_Quite_A_Tag",
Position { offset: 31 },
);
assert_ident_parses_malformed(
&arena,
"Hello_World_This_Is_Quite_A_Tag23",
Position { offset: 33 },
);
}
#[test]
fn test_parse_opaque_ref_camel() {
let arena = Bump::new();
assert_ident_parses_opaque(&arena, "@Hello");
assert_ident_parses_opaque(&arena, "@Hello23");
assert_ident_parses_opaque(&arena, "@HelloWorld");
assert_ident_parses_opaque(&arena, "@HelloWorld23");
assert_ident_parses_opaque(&arena, "@HelloWorldThisIsQuiteARef");
assert_ident_parses_opaque(&arena, "@HelloWorldThisIsQuiteARef23");
}
#[test]
fn test_parse_opaque_ref_snake_is_malformed() {
let arena = Bump::new();
assert_ident_parses_malformed(&arena, "@Hello_World", Position { offset: 12 });
assert_ident_parses_malformed(&arena, "@Hello_World23", Position { offset: 14 });
assert_ident_parses_malformed(
&arena,
"@Hello_World_This_Is_Quite_A_Ref",
Position { offset: 32 },
);
assert_ident_parses_malformed(
&arena,
"@Hello_World_This_Is_Quite_A_Ref23",
Position { offset: 34 },
);
}
}

View file

@ -396,10 +396,22 @@ impl<'a> Normalize<'a> for ValueDef<'a> {
match *self {
Annotation(a, b) => Annotation(a.normalize(arena), b.normalize(arena)),
Body(a, b) => Body(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Body(a, b) => {
let a = a.normalize(arena);
let b = b.normalize(arena);
let is_unit_assignment = if let Pattern::RecordDestructure(collection) = a.value {
collection.is_empty()
} else {
false
};
if is_unit_assignment {
Stmt(arena.alloc(b))
} else {
Body(arena.alloc(a), arena.alloc(b))
}
}
AnnotatedBody {
ann_pattern,
ann_type,
@ -560,26 +572,6 @@ impl<'a> Normalize<'a> for StrLiteral<'a> {
match *self {
StrLiteral::PlainLine(t) => StrLiteral::PlainLine(t),
StrLiteral::Line(t) => {
let mut needs_merge = false;
let mut last_was_mergable = false;
for segment in t.iter() {
let mergable = matches!(
segment,
StrSegment::Plaintext(_)
| StrSegment::Unicode(_)
| StrSegment::EscapedChar(_)
);
if mergable && last_was_mergable {
needs_merge = true;
break;
}
last_was_mergable = mergable;
}
if !needs_merge {
return StrLiteral::Line(t.normalize(arena));
}
let mut new_segments = Vec::new_in(arena);
let mut last_text = String::new_in(arena);
@ -713,39 +705,29 @@ impl<'a> Normalize<'a> for Expr<'a> {
arena.alloc(b.normalize(arena)),
),
Expr::Crash => Expr::Crash,
Expr::Defs(a, b) => {
let mut defs = a.clone();
defs.space_before = vec![Default::default(); defs.len()];
defs.space_after = vec![Default::default(); defs.len()];
defs.regions = vec![Region::zero(); defs.len()];
defs.spaces.clear();
for type_def in defs.type_defs.iter_mut() {
*type_def = type_def.normalize(arena);
}
for value_def in defs.value_defs.iter_mut() {
*value_def = value_def.normalize(arena);
}
Expr::Defs(arena.alloc(defs), arena.alloc(b.normalize(arena)))
}
Expr::Defs(a, b) => fold_defs(arena, a.defs(), b.value.normalize(arena)),
Expr::Backpassing(a, b, c) => Expr::Backpassing(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
arena.alloc(c.normalize(arena)),
),
Expr::Dbg => Expr::Dbg,
Expr::DbgStmt(a, b) => Expr::DbgStmt(
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Expr::DbgStmt {
first,
extra_args,
continuation,
} => Expr::DbgStmt {
first: arena.alloc(first.normalize(arena)),
extra_args: extra_args.normalize(arena),
continuation: arena.alloc(continuation.normalize(arena)),
},
Expr::LowLevelDbg(x, a, b) => Expr::LowLevelDbg(
x,
arena.alloc(a.normalize(arena)),
arena.alloc(b.normalize(arena)),
),
Expr::Try => Expr::Try,
Expr::LowLevelTry(a, kind) => Expr::LowLevelTry(arena.alloc(a.normalize(arena)), kind),
Expr::Return(a, b) => Expr::Return(
arena.alloc(a.normalize(arena)),
b.map(|loc_b| &*arena.alloc(loc_b.normalize(arena))),
@ -755,7 +737,22 @@ impl<'a> Normalize<'a> for Expr<'a> {
}
Expr::BinOps(a, b) => Expr::BinOps(a.normalize(arena), arena.alloc(b.normalize(arena))),
Expr::UnaryOp(a, b) => {
Expr::UnaryOp(arena.alloc(a.normalize(arena)), b.normalize(arena))
let a = a.normalize(arena);
match (a.value, b.value) {
(Expr::Num(text), UnaryOp::Negate) if !text.starts_with('-') => {
let mut res = String::new_in(arena);
res.push('-');
res.push_str(text);
Expr::Num(res.into_bump_str())
}
(Expr::Float(text), UnaryOp::Negate) if !text.starts_with('-') => {
let mut res = String::new_in(arena);
res.push('-');
res.push_str(text);
Expr::Float(res.into_bump_str())
}
_ => Expr::UnaryOp(arena.alloc(a), b.normalize(arena)),
}
}
Expr::If {
if_thens,
@ -776,7 +773,7 @@ impl<'a> Normalize<'a> for Expr<'a> {
Expr::PrecedenceConflict(a) => Expr::PrecedenceConflict(a),
Expr::SpaceBefore(a, _) => a.normalize(arena),
Expr::SpaceAfter(a, _) => a.normalize(arena),
Expr::SingleQuote(a) => Expr::Num(a),
Expr::SingleQuote(a) => Expr::SingleQuote(a),
Expr::EmptyRecordBuilder(a) => {
Expr::EmptyRecordBuilder(arena.alloc(a.normalize(arena)))
}
@ -791,6 +788,61 @@ impl<'a> Normalize<'a> for Expr<'a> {
}
}
fn fold_defs<'a>(
arena: &'a Bump,
mut defs: impl Iterator<Item = Result<&'a TypeDef<'a>, &'a ValueDef<'a>>>,
final_expr: Expr<'a>,
) -> Expr<'a> {
let mut new_defs = Defs::default();
while let Some(def) = defs.next() {
match def {
Ok(td) => {
let td = td.normalize(arena);
new_defs.push_type_def(td, Region::zero(), &[], &[]);
}
Err(vd) => {
let vd = vd.normalize(arena);
match vd {
ValueDef::Stmt(&Loc {
value:
Expr::Apply(
&Loc {
value: Expr::Dbg, ..
},
args,
_,
),
..
}) => {
let rest = fold_defs(arena, defs, final_expr);
let new_final = Expr::DbgStmt {
first: args[0],
extra_args: &args[1..],
continuation: arena.alloc(Loc::at_zero(rest)),
};
if new_defs.is_empty() {
return new_final;
}
return Expr::Defs(
arena.alloc(new_defs),
arena.alloc(Loc::at_zero(new_final)),
);
}
_ => {
new_defs.push_value_def(vd, Region::zero(), &[], &[]);
}
}
}
}
}
if new_defs.is_empty() {
return final_expr;
}
Expr::Defs(arena.alloc(new_defs), arena.alloc(Loc::at_zero(final_expr)))
}
fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent {
match ident {
BadIdent::Start(_) => BadIdent::Start(Position::zero()),
@ -804,6 +856,7 @@ fn remove_spaces_bad_ident(ident: BadIdent) -> BadIdent {
position: Position::zero(),
declaration_region,
},
BadIdent::TooManyUnderscores(_) => BadIdent::TooManyUnderscores(Position::zero()),
BadIdent::QualifiedTag(_) => BadIdent::QualifiedTag(Position::zero()),
BadIdent::WeirdAccessor(_) => BadIdent::WeirdAccessor(Position::zero()),
BadIdent::WeirdDotAccess(_) => BadIdent::WeirdDotAccess(Position::zero()),
@ -847,7 +900,7 @@ impl<'a> Normalize<'a> for Pattern<'a> {
is_negative,
},
Pattern::FloatLiteral(a) => Pattern::FloatLiteral(a),
Pattern::StrLiteral(a) => Pattern::StrLiteral(a),
Pattern::StrLiteral(a) => Pattern::StrLiteral(a.normalize(arena)),
Pattern::Underscore(a) => Pattern::Underscore(a),
Pattern::Malformed(a) => Pattern::Malformed(a),
Pattern::MalformedIdent(a, b) => Pattern::MalformedIdent(a, remove_spaces_bad_ident(b)),
@ -1465,7 +1518,6 @@ impl<'a> Normalize<'a> for EExpect<'a> {
EExpect::Continuation(arena.alloc(inner_err.normalize(arena)), Position::zero())
}
EExpect::IndentCondition(_) => EExpect::IndentCondition(Position::zero()),
EExpect::DbgArity(_) => EExpect::DbgArity(Position::zero()),
}
}
}

View file

@ -513,7 +513,6 @@ pub enum EExpect<'a> {
Condition(&'a EExpr<'a>, Position),
Continuation(&'a EExpr<'a>, Position),
IndentCondition(Position),
DbgArity(Position),
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -836,7 +835,7 @@ where
}
// This should be enough for anyone. Right? RIGHT?
let indent_text = "| ; : ! ".repeat(20);
let indent_text = "| ; : ! ".repeat(100);
let cur_indent = INDENT.with(|i| *i.borrow());
@ -1060,7 +1059,7 @@ where
Some(
b' ' | b'#' | b'\n' | b'\r' | b'\t' | b',' | b'(' | b')' | b'[' | b']' | b'{'
| b'}' | b'"' | b'\'' | b'/' | b'\\' | b'+' | b'*' | b'%' | b'^' | b'&' | b'|'
| b'<' | b'>' | b'=' | b'!' | b'~' | b'`' | b';' | b':' | b'?' | b'.',
| b'<' | b'>' | b'=' | b'!' | b'~' | b'`' | b';' | b':' | b'?' | b'.' | b'@' | b'-',
) => {
state = state.advance(width);
Ok((MadeProgress, (), state))
@ -1669,6 +1668,21 @@ where
}
}
/// Creates a parser that fails if the next byte is the given byte.
pub fn error_on_byte<'a, T, E, F>(byte_to_match: u8, to_error: F) -> impl Parser<'a, T, E>
where
T: 'a,
E: 'a,
F: Fn(Position) -> E,
{
debug_assert_ne!(byte_to_match, b'\n');
move |_arena: &'a Bump, state: State<'a>, _min_indent: u32| match state.bytes().first() {
Some(x) if *x == byte_to_match => Err((MadeProgress, to_error(state.pos()))),
_ => Err((NoProgress, to_error(state.pos()))),
}
}
/// Runs two parsers in succession. If both parsers succeed, the output is a tuple of both outputs.
/// Both parsers must have the same error type.
///

View file

@ -181,6 +181,7 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
match one_byte {
b'"' if !is_single_quote => {
preceded_by_dollar = false;
if segment_parsed_bytes == 1 && segments.is_empty() {
// special case of the empty string
if is_multiline {
@ -318,6 +319,7 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
));
}
b'\n' => {
preceded_by_dollar = false;
if is_multiline {
let without_newline = &state.bytes()[0..(segment_parsed_bytes - 1)];
let with_newline = &state.bytes()[0..segment_parsed_bytes];
@ -456,7 +458,8 @@ pub fn parse_str_like_literal<'a>() -> impl Parser<'a, StrLikeLiteral<'a>, EStri
let (_progress, loc_expr, new_state) = skip_second(
specialize_err_ref(
EString::Format,
loc(allocated(reset_min_indent(expr::expr_help()))),
loc(allocated(reset_min_indent(expr::expr_help())))
.trace("str_interpolation"),
),
byte(b')', EString::FormatEnd),
)

View file

@ -10,9 +10,9 @@ use crate::expr::record_field;
use crate::ident::{lowercase_ident, lowercase_ident_keyword_e};
use crate::keyword;
use crate::parser::{
absolute_column_min_indent, and, collection_trailing_sep_e, either, increment_min_indent,
indented_seq, loc, map, map_with_arena, skip_first, skip_second, succeed, then, zero_or_more,
ERecord, ETypeAbilityImpl,
absolute_column_min_indent, and, collection_trailing_sep_e, either, error_on_byte,
increment_min_indent, indented_seq, loc, map, map_with_arena, skip_first, skip_second, succeed,
then, zero_or_more, ERecord, ETypeAbilityImpl,
};
use crate::parser::{
allocated, backtrackable, byte, fail, optional, specialize_err, specialize_err_ref, two_bytes,
@ -97,7 +97,7 @@ fn check_type_alias<'a>(
fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>> {
then(
space0_before_e(term(false), EType::TAsIndentStart),
space0_before_e(term_or_apply_with_as(false), EType::TAsIndentStart),
// TODO: introduce a better combinator for this.
// `check_type_alias` doesn't need to modify the state or progress, but it needs to access `state.pos()`
|arena, state, progress, output| {
@ -111,9 +111,9 @@ fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>>
)
}
fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
map_with_arena(
and(
fn term_fragment<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
one_of!(
loc_wildcard(),
loc_inferred(),
@ -126,8 +126,27 @@ fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>
EType::TTagUnion,
tag_union_type(stop_at_surface_has)
)),
loc(applied_type(stop_at_surface_has)),
loc(parse_type_variable(stop_at_surface_has)),
)
}
fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
one_of!(
term_fragment(stop_at_surface_has),
loc(specialize_err(EType::TApply, concrete_type())),
fail(EType::TStart),
)
.trace("type_annotation:term")
}
fn term_or_apply_with_as<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
map_with_arena(
and(
one_of!(
term_fragment(stop_at_surface_has),
loc(applied_type(stop_at_surface_has)),
fail(EType::TStart),
),
// Inline alias notation, e.g. [Nil, Cons a (List a)] as List a
@ -161,7 +180,7 @@ fn term<'a>(stop_at_surface_has: bool) -> impl Parser<'a, Loc<TypeAnnotation<'a>
}
},
)
.trace("type_annotation:term")
.trace("type_annotation:term_or_apply_with_as")
}
/// The `*` type variable, e.g. in (List *) Wildcard,
@ -579,23 +598,32 @@ fn expression<'a>(
stop_at_surface_has: bool,
) -> impl Parser<'a, Loc<TypeAnnotation<'a>>, EType<'a>> {
(move |arena, state: State<'a>, min_indent: u32| {
let (p1, first, state) = space0_before_e(term(stop_at_surface_has), EType::TIndentStart)
let (p1, first, state) = space0_before_e(
term_or_apply_with_as(stop_at_surface_has),
EType::TIndentStart,
)
.parse(arena, state, min_indent)?;
let result = and(
zero_or_more(skip_first(
byte(b',', EType::TFunctionArgument),
let (p2, rest, rest_state) = zero_or_more(skip_first(
backtrackable(byte(b',', EType::TFunctionArgument)),
one_of![
space0_around_ee(
term(stop_at_surface_has),
EType::TIndentStart,
EType::TIndentEnd
map_with_arena(
and(
backtrackable(space0_e(EType::TIndentStart)),
and(
term_or_apply_with_as(stop_at_surface_has),
space0_e(EType::TIndentEnd)
),
fail(EType::TFunctionArgument)
),
comma_args_help,
),
error_on_byte(b',', EType::TFunctionArgument)
],
))
.trace("type_annotation:expression:rest_args"),
and(
.trace("type_annotation:expression:rest_args")
.parse(arena, state.clone(), min_indent)?;
let result = and(
space0_e(EType::TIndentStart),
one_of![
map(two_bytes(b'-', b'>', EType::TStart), |_| {
@ -606,14 +634,15 @@ fn expression<'a>(
}),
],
)
.trace("type_annotation:expression:arrow"),
)
.parse(arena, state.clone(), min_indent);
.trace("type_annotation:expression:arrow")
.parse(arena, rest_state, min_indent);
let (progress, annot, state) = match result {
Ok((p2, (rest, (space_before_arrow, arrow)), state)) => {
let (p3, return_type, state) =
space0_before_e(term(stop_at_surface_has), EType::TIndentStart)
Ok((p3, (space_before_arrow, arrow), state)) => {
let (p4, return_type, state) = space0_before_e(
term_or_apply_with_as(stop_at_surface_has),
EType::TIndentStart,
)
.parse(arena, state, min_indent)?;
let region = Region::span_across(&first.region, &return_type.region);
@ -636,7 +665,7 @@ fn expression<'a>(
region,
value: TypeAnnotation::Function(output, arrow, arena.alloc(return_type)),
};
let progress = p1.or(p2).or(p3);
let progress = p1.or(p2).or(p3).or(p4);
(progress, result, state)
}
Err(err) => {
@ -694,6 +723,36 @@ fn expression<'a>(
.trace("type_annotation:expression")
}
fn comma_args_help<'a>(
arena: &'a Bump,
(spaces_before, (loc_val, spaces_after)): (
&'a [CommentOrNewline<'a>],
(Loc<TypeAnnotation<'a>>, &'a [CommentOrNewline<'a>]),
),
) -> Loc<TypeAnnotation<'a>> {
if spaces_before.is_empty() {
if spaces_after.is_empty() {
loc_val
} else {
arena
.alloc(loc_val.value)
.with_spaces_after(spaces_after, loc_val.region)
}
} else if spaces_after.is_empty() {
arena
.alloc(loc_val.value)
.with_spaces_before(spaces_before, loc_val.region)
} else {
let wrapped_expr = arena
.alloc(loc_val.value)
.with_spaces_after(spaces_after, loc_val.region);
arena
.alloc(wrapped_expr.value)
.with_spaces_before(spaces_before, wrapped_expr.region)
}
}
/// Parse a basic type annotation that's a combination of variables
/// (which are lowercase and unqualified, e.g. `a` in `List a`),
/// type applications (which are uppercase and optionally qualified, e.g.

View file

@ -372,10 +372,10 @@ mod test_parse {
let parsed = parse_module_defs(arena, state, ast::Defs::default());
match parsed {
Ok(_) => {
// dbg!(_state);
// eprintln!("{:?}", _state);
}
Err(_) => {
// dbg!(_fail, _state);
// eprintln!("{:?}, {:?}", _fail, _state);
panic!("Failed to parse!");
}
}

View file

@ -8,7 +8,7 @@ use roc_module::symbol::{ModuleId, Symbol};
use roc_parse::ast::Base;
use roc_parse::pattern::PatternType;
use roc_region::all::{Loc, Region};
use roc_types::types::AliasKind;
use roc_types::types::{AliasKind, EarlyReturnKind};
use crate::Severity;
@ -244,6 +244,7 @@ pub enum Problem {
},
ReturnOutsideOfFunction {
region: Region,
return_kind: EarlyReturnKind,
},
StatementsAfterReturn {
region: Region,
@ -254,6 +255,7 @@ pub enum Problem {
StmtAfterExpr(Region),
UnsuffixedEffectfulRecordField(Region),
SuffixedPureRecordField(Region),
EmptyTupleType(Region),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -342,6 +344,7 @@ impl Problem {
Problem::UnsuffixedEffectfulRecordField(_) | Problem::SuffixedPureRecordField(..) => {
Warning
}
Problem::EmptyTupleType(_) => Warning,
}
}
@ -454,6 +457,7 @@ impl Problem {
| Problem::RuntimeError(RuntimeError::ReadIngestedFileError { region, .. })
| Problem::InvalidAliasRigid { region, .. }
| Problem::InvalidInterpolation(region)
| Problem::EmptyTupleType(region)
| Problem::InvalidHexadecimal(region)
| Problem::InvalidUnicodeCodePt(region)
| Problem::NestedDatatype {
@ -504,7 +508,7 @@ impl Problem {
| Problem::OverAppliedDbg { region }
| Problem::UnappliedDbg { region }
| Problem::DefsOnlyUsedInRecursion(_, region)
| Problem::ReturnOutsideOfFunction { region }
| Problem::ReturnOutsideOfFunction { region, .. }
| Problem::StatementsAfterReturn { region }
| Problem::ReturnAtEndOfFunction { region }
| Problem::UnsuffixedEffectfulRecordField(region)
@ -763,8 +767,8 @@ impl RuntimeError {
record: _,
field: region,
}
| RuntimeError::ReadIngestedFileError { region, .. } => *region,
RuntimeError::InvalidUnicodeCodePt(region) => *region,
| RuntimeError::ReadIngestedFileError { region, .. }
| RuntimeError::InvalidUnicodeCodePt(region) => *region,
RuntimeError::UnresolvedTypeVar | RuntimeError::ErroneousType => Region::zero(),
RuntimeError::LookupNotInScope { loc_name, .. } => loc_name.region,
RuntimeError::OpaqueNotDefined { usage, .. } => usage.region,
@ -790,4 +794,5 @@ pub enum MalformedPatternProblem {
EmptySingleQuote,
MultipleCharsInSingleQuote,
DuplicateListRestPattern,
CantApplyPattern,
}

View file

@ -16,6 +16,7 @@ use roc_can::abilities::{AbilitiesStore, MemberSpecializationInfo};
use roc_can::constraint::Constraint::{self, *};
use roc_can::constraint::{
Cycle, FxCallConstraint, FxSuffixConstraint, FxSuffixKind, LetConstraint, OpportunisticResolve,
TryTargetConstraint,
};
use roc_can::expected::{Expected, PExpected};
use roc_can::module::ModuleParams;
@ -908,6 +909,96 @@ fn solve(
}
}
}
TryTarget(index) => {
let try_target_constraint = &env.constraints.try_target_constraints[index.index()];
let TryTargetConstraint {
target_type_index,
ok_payload_var,
err_payload_var,
region,
kind,
} = try_target_constraint;
let target_actual = either_type_index_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
*target_type_index,
);
let wanted_result_ty = can_types.from_old_type(&Type::TagUnion(
vec![
("Ok".into(), vec![Type::Variable(*ok_payload_var)]),
("Err".into(), vec![Type::Variable(*err_payload_var)]),
],
TypeExtension::Closed,
));
let wanted_result_var = type_to_var(
env,
rank,
problems,
abilities_store,
obligation_cache,
&mut can_types,
aliases,
wanted_result_ty,
);
match unify(
&mut env.uenv(),
target_actual,
wanted_result_var,
UnificationMode::EQ,
Polarity::OF_VALUE,
) {
Success {
vars,
must_implement_ability,
lambda_sets_to_specialize,
extra_metadata: _,
} => {
env.introduce(rank, &vars);
if !must_implement_ability.is_empty() {
let new_problems = obligation_cache.check_obligations(
env.subs,
abilities_store,
must_implement_ability,
AbilityImplError::BadExpr(
*region,
Category::TryTarget,
target_actual,
),
);
problems.extend(new_problems);
}
compact_lambdas_and_check_obligations(
env,
problems,
abilities_store,
obligation_cache,
awaiting_specializations,
lambda_sets_to_specialize,
);
state
}
Failure(vars, actual_type, _expected_type, _bad_impls) => {
env.introduce(rank, &vars);
let problem = TypeError::InvalidTryTarget(*region, actual_type, *kind);
problems.push(problem);
state
}
}
}
Let(index, pool_slice) => {
let let_con = &env.constraints.let_constraints[index.index()];

View file

@ -2,6 +2,7 @@
use std::{path::PathBuf, str::Utf8Error};
use roc_can::constraint::{ExpectEffectfulReason, FxSuffixKind};
use roc_can::expr::TryKind;
use roc_can::{
constraint::FxCallKind,
expected::{Expected, PExpected},
@ -48,6 +49,7 @@ pub enum TypeError {
ExpectedEffectful(Region, ExpectEffectfulReason),
UnsuffixedEffectfulFunction(Region, FxSuffixKind),
SuffixedPureFunction(Region, FxSuffixKind),
InvalidTryTarget(Region, ErrorType, TryKind),
}
impl TypeError {
@ -77,6 +79,7 @@ impl TypeError {
TypeError::FxInTopLevel(_, _) => Warning,
TypeError::UnsuffixedEffectfulFunction(_, _) => Warning,
TypeError::SuffixedPureFunction(_, _) => Warning,
TypeError::InvalidTryTarget(_, _, _) => RuntimeError,
}
}
@ -97,7 +100,8 @@ impl TypeError {
| TypeError::FxInTopLevel(region, _)
| TypeError::ExpectedEffectful(region, _)
| TypeError::UnsuffixedEffectfulFunction(region, _)
| TypeError::SuffixedPureFunction(region, _) => Some(*region),
| TypeError::SuffixedPureFunction(region, _)
| TypeError::InvalidTryTarget(region, _, _) => Some(*region),
TypeError::UnfulfilledAbility(ab, ..) => ab.region(),
TypeError::Exhaustive(e) => Some(e.region()),
TypeError::CircularDef(c) => c.first().map(|ce| ce.symbol_region),

View file

@ -84,7 +84,7 @@ impl<'a, 'c, 'd, 'i, 's, 't, P: Push<Problem>> Env<'a, 'c, 'd, 'i, 's, 't, P> {
}
}
pub fn to_mono_expr(&mut self, can_expr: &Expr) -> Option<MonoExpr> {
pub fn to_mono_expr(&mut self, can_expr: &Expr) -> MonoExpr {
let problems = &mut self.problems;
let mono_types = &mut self.mono_types;
let mut mono_from_var = |var| {
@ -103,7 +103,7 @@ impl<'a, 'c, 'd, 'i, 's, 't, P: Push<Problem>> Env<'a, 'c, 'd, 'i, 's, 't, P> {
macro_rules! compiler_bug {
($problem:expr) => {{
problems.push($problem);
Some(MonoExpr::CompilerBug($problem))
MonoExpr::CompilerBug($problem)
}};
}
@ -112,56 +112,48 @@ impl<'a, 'c, 'd, 'i, 's, 't, P: Push<Problem>> Env<'a, 'c, 'd, 'i, 's, 't, P> {
match self.subs.get_content_without_compacting(*var) {
Content::FlexVar(_) => {
// Plain decimal number literals like `4.2` can still have an unbound var.
Some(MonoExpr::Number(Number::Dec(*val)))
}
_ => match mono_from_var(*var) {
Some(mono_id) => match mono_types.get(mono_id) {
MonoType::Primitive(primitive) => {
Some(to_frac(*primitive, *val, problems))
MonoExpr::Number(Number::Dec(*val))
}
_ => {
let mono_type = mono_from_var(*var);
match mono_types.get(mono_type) {
MonoType::Primitive(primitive) => to_frac(*primitive, *val, problems),
other => {
compiler_bug!(Problem::NumSpecializedToWrongType(Some(*other)))
}
},
None => {
compiler_bug!(Problem::NumSpecializedToWrongType(None))
}
},
}
}
}
Expr::Num(var, _, int_value, _) | Expr::Int(var, _, _, int_value, _) => {
let mono_type = mono_from_var(*var);
// Number literals and int literals both specify integer numbers, so to_num() can work on both.
match mono_from_var(*var) {
Some(mono_id) => match mono_types.get(mono_id) {
MonoType::Primitive(primitive) => {
Some(to_num(*primitive, *int_value, problems))
}
match mono_types.get(mono_type) {
MonoType::Primitive(primitive) => to_num(*primitive, *int_value, problems),
other => compiler_bug!(Problem::NumSpecializedToWrongType(Some(*other))),
},
None => compiler_bug!(Problem::NumSpecializedToWrongType(None)),
}
}
Expr::SingleQuote(var, _, char, _) => match mono_from_var(*var) {
Expr::SingleQuote(var, _, char, _) => {
let mono_type = mono_from_var(*var);
// Single-quote characters monomorphize to an integer.
// TODO if we store these using the same representation as other ints (e.g. Expr::Int,
// TODO [mono2] if we store these using the same representation as other ints (e.g. Expr::Int,
// or keeping a separate value but storing an IntValue instead of a char), then
// even though we verify them differently, we can combine this branch with Num and Int.
Some(mono_id) => match mono_types.get(mono_id) {
MonoType::Primitive(primitive) => {
Some(char_to_int(*primitive, *char, problems))
}
match mono_types.get(mono_type) {
MonoType::Primitive(primitive) => char_to_int(*primitive, *char, problems),
other => compiler_bug!(Problem::CharSpecializedToWrongType(Some(*other))),
},
None => compiler_bug!(Problem::CharSpecializedToWrongType(None)),
},
Expr::Str(contents) => Some(MonoExpr::Str(self.string_interns.get_id(
}
}
Expr::Str(contents) => MonoExpr::Str(self.string_interns.get_id(
self.arena,
// TODO should be able to remove this alloc_str() once canonical Expr stores an arena-allocated string.
self.arena.alloc_str(contents),
))),
)),
Expr::EmptyRecord => {
// Empty records are zero-sized and should be discarded.
None
MonoExpr::Unit
}
Expr::Record {
record_var: _,
@ -172,10 +164,10 @@ impl<'a, 'c, 'd, 'i, 's, 't, P: Push<Problem>> Env<'a, 'c, 'd, 'i, 's, 't, P> {
// Check for records with 0-1 fields before sorting or reserving a slice of IDs (which might be unnecessary).
// We'll check again after discarding zero-sized fields, because we might end up with 0 or 1 fields remaining.
if fields.len() <= 1 {
return fields
.into_iter()
.next()
.and_then(|(_, field)| self.to_mono_expr(&field.loc_expr.value));
return match fields.into_iter().next() {
Some((_, field)) => self.to_mono_expr(&field.loc_expr.value),
None => MonoExpr::Unit,
};
}
// Sort the fields alphabetically by name.
@ -189,21 +181,18 @@ impl<'a, 'c, 'd, 'i, 's, 't, P: Push<Problem>> Env<'a, 'c, 'd, 'i, 's, 't, P> {
let mut buf: Vec<(MonoExpr, Region)> =
Vec::with_capacity_in(fields.len(), self.arena);
buf.extend(
// flat_map these so we discard all the fields that monomorphized to None
fields.into_iter().flat_map(|(_name, field)| {
self.to_mono_expr(&field.loc_expr.value)
.map(|mono_expr| (mono_expr, field.loc_expr.region))
}),
);
buf.extend(fields.into_iter().map(|(_name, field)| {
(
self.to_mono_expr(&field.loc_expr.value),
field.loc_expr.region,
)
}));
// If we ended up with exactly 1 field, return it unwrapped.
if buf.len() == 1 {
return buf.pop().map(|(expr, _region)| expr);
}
let slice = unsafe {
NonEmptySlice::from_slice_unchecked(self.mono_exprs.extend(buf.into_iter()))
};
NonEmptySlice::from_slice(self.mono_exprs.extend(buf.iter().copied()))
.map(MonoExpr::Struct)
MonoExpr::Struct(slice)
}
// Expr::Call((fn_var, fn_expr, capture_var, ret_var), args, called_via) => {
// let opt_ret_type = mono_from_var(*var);

View file

@ -129,14 +129,18 @@ impl MonoExprs {
})
}
pub fn extend(
&mut self,
exprs: impl Iterator<Item = (MonoExpr, Region)> + Clone,
) -> Slice<MonoExpr> {
pub fn extend(&mut self, exprs: impl Iterator<Item = (MonoExpr, Region)>) -> Slice<MonoExpr> {
let start = self.exprs.len();
self.exprs.extend(exprs.clone().map(|(expr, _region)| expr));
self.regions.extend(exprs.map(|(_expr, region)| region));
let (size_hint, _) = exprs.size_hint();
self.exprs.reserve(size_hint);
self.regions.reserve(size_hint);
for (expr, region) in exprs {
self.exprs.push(expr);
self.regions.push(region);
}
let len = self.exprs.len() - start;
@ -279,6 +283,8 @@ pub enum MonoExpr {
recursive: Recursive,
},
Unit,
/// A record literal or a tuple literal.
/// These have already been sorted alphabetically.
Struct(NonEmptySlice<MonoExpr>),

View file

@ -129,36 +129,20 @@ impl MonoTypes {
pub(crate) fn add_function(
&mut self,
ret: Option<MonoTypeId>,
ret: MonoTypeId,
args: impl IntoIterator<Item = MonoTypeId>,
) -> MonoTypeId {
let mono_type = match ret {
Some(ret) => {
let ret_then_args = {
let start = self.ids.len();
self.ids.push(ret);
self.ids.extend(args);
// Safety: we definitely have at least 2 elements in here, even if the iterator is empty.
let length =
unsafe { NonZeroU16::new_unchecked((self.ids.len() - start) as u16) };
let length = unsafe { NonZeroU16::new_unchecked((self.ids.len() - start) as u16) };
NonEmptySlice::new(start as u32, length)
};
MonoType::Func { ret_then_args }
}
None => {
let args = {
let start = self.ids.len();
self.ids.extend(args);
let length = (self.ids.len() - start) as u16;
Slice::new(start as u32, length)
};
MonoType::VoidFunc { args }
}
};
let mono_type = MonoType::Func { ret_then_args };
let index = self.entries.len();
self.entries.push(mono_type);
@ -275,16 +259,6 @@ pub enum MonoType {
Func {
ret_then_args: NonEmptySlice<MonoTypeId>,
},
/// A function that does not have a return value (e.g. the return value was {}, which
/// got eliminated), and which has 0 or more arguments.
/// This has to be its own variant because the other function variant uses one slice to
/// store the return value followed by the arguments. Without this separate variant,
/// the other one couldn't distinguish between a function with a return value and 0 arguments,
/// and a function with 1 argument but no return value.
VoidFunc {
args: Slice<MonoTypeId>,
},
// This last slot is tentatively reserved for Dict, because in the past we've discussed wanting to
// implement Dict in Zig (for performance) instead of on top of List, like it is as of this writing.
//

View file

@ -71,7 +71,7 @@ impl MonoTypeCache {
problems: &mut impl Push<Problem>,
debug_info: &mut Option<DebugInfo>,
var: Variable,
) -> Option<MonoTypeId> {
) -> MonoTypeId {
let mut env = Env {
arena,
cache: self,
@ -135,7 +135,7 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
mono_args.extend(
arg_vars
.into_iter()
.flat_map(|var_index| self.lower_var(subs, subs[var_index])),
.map(|var_index| self.lower_var(subs, subs[var_index])),
);
// TODO [mono2] populate debuginfo as appropriate
@ -143,7 +143,7 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
self.mono_types.add_function(func, mono_args)
}
fn lower_var(&mut self, subs: &Subs, var: Variable) -> Option<MonoTypeId> {
fn lower_var(&mut self, subs: &Subs, var: Variable) -> MonoTypeId {
let root_var = subs.get_root_key_without_compacting(var);
// TODO: we could replace this cache by having Subs store a Content::Monomorphic(MonoTypeId)
@ -151,11 +151,11 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
// for sure, and the lookups should be faster because they're O(1) but don't require hashing.
// Kinda creates a cyclic dep though.
if let Some(mono_id) = self.cache.inner.get(&root_var) {
return Some(*mono_id);
return *mono_id;
}
// Convert the Content to a MonoType, often by passing an iterator. None of these iterators introduce allocations.
let mono_id = match dbg!(*subs.get_content_without_compacting(root_var)) {
let mono_id = match *subs.get_content_without_compacting(root_var) {
Content::Structure(flat_type) => match flat_type {
FlatType::Apply(symbol, args) => {
if symbol.is_builtin() {
@ -291,7 +291,7 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
}
_ => {
// TODO [mono2] record in debuginfo the alias name for whatever we're lowering.
self.lower_var(subs, real)?
self.lower_var(subs, real)
}
}
}
@ -313,10 +313,7 @@ impl<'a, 'c, 'd, 'e, 'f, 'm, 'p, P: Push<Problem>> Env<'a, 'c, 'd, 'e, 'f, 'm, '
};
// This var is now known to be monomorphic, so we don't repeat this work again later.
// (We don't insert entries for Unit values.)
self.cache.inner.insert(root_var, mono_id);
Some(mono_id)
mono_id
}
}
@ -452,12 +449,11 @@ fn number_args_to_mono_id(
// Unroll aliases in this loop, as many aliases as we encounter.
loop {
match content {
Content::Structure(flat_type) => {
if let FlatType::Apply(outer_symbol, args) = flat_type {
Content::Structure(FlatType::Apply(outer_symbol, args)) => {
return num_num_args_to_mono_id(*outer_symbol, *args, subs, problems);
} else {
break;
}
Content::Structure(_) => {
break;
}
Content::FlexVar(_) => {
// Num *

View file

@ -86,9 +86,8 @@ fn specialize_expr<'a>(
assert_eq!(0, home_decls.expressions.len());
let region = Region::zero();
let mono_expr_id = env
.to_mono_expr(&main_expr)
.map(|mono_expr| mono_exprs.add(mono_expr, region));
let mono_expr = env.to_mono_expr(&main_expr);
let mono_expr_id = mono_exprs.add(mono_expr, region);
SpecializedExprOut {
mono_expr_id,
@ -100,13 +99,13 @@ fn specialize_expr<'a>(
}
#[track_caller]
pub fn expect_no_expr(input: impl AsRef<str>) {
pub fn expect_unit(input: impl AsRef<str>) {
let arena = Bump::new();
let mut interns = Interns::new();
let out = specialize_expr(&arena, input.as_ref(), &mut interns);
let actual = out.mono_expr_id.map(|id| out.mono_exprs.get_expr(id));
let actual = out.mono_exprs.get_expr(out.mono_expr_id);
assert_eq!(None, actual, "This input expr should have specialized to being dicarded as zero-sized, but it didn't: {:?}", input.as_ref());
assert_eq!(MonoExpr::Unit, *actual, "This input expr should have specialized to being dicarded as zero-sized, but it didn't: {:?}", input.as_ref());
}
#[track_caller]
@ -165,6 +164,9 @@ fn dbg_mono_expr_help<'a>(
write!(buf, "])").unwrap();
}
MonoExpr::Unit => {
write!(buf, "{{}}").unwrap();
}
// MonoExpr::List { elem_type, elems } => todo!(),
// MonoExpr::Lookup(symbol, mono_type_id) => todo!(),
// MonoExpr::ParameterizedLookup {
@ -270,11 +272,8 @@ pub fn expect_mono_expr_custom<T: PartialEq + core::fmt::Debug>(
let arena = Bump::new();
let mut string_interns = Interns::new();
let out = specialize_expr(&arena, input.as_ref(), &mut string_interns);
let mono_expr_id = out
.mono_expr_id
.expect("This input expr should not have been discarded as zero-sized, but it was discarded: {input:?}");
let actual_expr = out.mono_exprs.get_expr(mono_expr_id); // Must run first, to populate string interns!
let actual_expr = out.mono_exprs.get_expr(out.mono_expr_id); // Must run first, to populate string interns!
let actual = to_actual(&arena, &out.mono_exprs, &string_interns, actual_expr);
let expected = to_expected(&arena, &out.mono_exprs, &string_interns);

View file

@ -10,16 +10,16 @@ mod specialize_structs {
use crate::helpers::expect_mono_expr_str;
use super::helpers::{expect_mono_expr_with_interns, expect_no_expr};
use super::helpers::{expect_mono_expr_with_interns, expect_unit};
#[test]
fn empty_record() {
expect_no_expr("{}");
expect_unit("{}");
}
#[test]
fn one_field_with_empty_record() {
expect_no_expr("{ discardedField: {} }");
expect_unit("{ discardedField: {} }");
}
#[test]
@ -31,16 +31,6 @@ mod specialize_structs {
});
}
#[test]
fn one_field_after_dropping_zero_sized() {
let string = "foo";
let expected =
format!("{{ discarded: {{}}, discardedToo: \"{string}\", alsoDiscarded: {{}} }}");
expect_mono_expr_with_interns(expected, |arena, interns| {
MonoExpr::Str(interns.try_get_id(arena, string).unwrap())
});
}
#[test]
fn two_fields() {
let one = 42;
@ -51,4 +41,15 @@ mod specialize_structs {
format!("Struct([Number(I8({one})), Number(I8({two}))])"),
);
}
#[test]
fn two_fields_one_unit() {
let one = 42;
let two = "{}";
expect_mono_expr_str(
format!("{{ one: {one}, two: {two} }}"),
format!("Struct([Number(I8({one})), {{}}])"),
);
}
}

View file

@ -1926,6 +1926,13 @@ fn pow_int() {
assert_evals_to!("Num.powInt 2 3", 8, i64);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
#[should_panic(expected = r#"Roc failed with message: "Integer raised to power overflowed!"#)]
fn pow_int_overflow() {
assert_evals_to!("Num.powInt 2u8 8", 0, u8);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn atan() {
@ -1959,16 +1966,6 @@ fn int_add_checked_err() {
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn int_add_wrap() {
assert_evals_to!(
"Num.addWrap 9_223_372_036_854_775_807 1",
std::i64::MIN,
i64
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn float_add_checked_pass() {
@ -2001,21 +1998,129 @@ fn float_add_overflow() {
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
#[should_panic(expected = r#"Roc failed with message: "Integer subtraction overflowed!"#)]
fn int_sub_overflow() {
assert_evals_to!("-9_223_372_036_854_775_808 - 1", 0, i64);
fn add_wrap() {
assert_evals_to!("Num.addWrap 255u8 10u8", 9u8, u8);
assert_evals_to!("Num.addWrap 127i8 10i8", -119i8, i8);
assert_evals_to!("Num.addWrap -127i8 -10i8", 119i8, i8);
assert_evals_to!("Num.addWrap 65535u16 10", 9u16, u16);
assert_evals_to!("Num.addWrap 32767i16 10", -32759i16, i16);
assert_evals_to!("Num.addWrap -32767i16 -10", 32759i16, i16);
assert_evals_to!("Num.addWrap 4294967295u32 10", 9u32, u32);
assert_evals_to!("Num.addWrap 2147483647i32 10", -2147483639i32, i32);
assert_evals_to!("Num.addWrap -2147483647i32 -10", 2147483639i32, i32);
assert_evals_to!("Num.addWrap 18446744073709551615u64 10", 9u64, u64);
assert_evals_to!(
"Num.addWrap 9223372036854775807i64 10",
-9223372036854775799i64,
i64
);
assert_evals_to!(
"Num.addWrap -9223372036854775807i64 -10",
9223372036854775799i64,
i64
);
assert_evals_to!(
"Num.addWrap 340282366920938463463374607431768211455u128 10",
U128::from(9u128),
U128
);
assert_evals_to!(
"Num.addWrap 170141183460469231731687303715884105727i128 10",
I128::from(-170141183460469231731687303715884105719i128),
I128
);
assert_evals_to!(
"Num.addWrap -170141183460469231731687303715884105727i128 -10",
I128::from(170141183460469231731687303715884105719i128),
I128
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn int_sub_wrap() {
fn add_saturated() {
assert_evals_to!("Num.addSaturated 200u8 200u8", 255u8, u8);
assert_evals_to!("Num.addSaturated 100i8 100i8", 127i8, i8);
assert_evals_to!("Num.addSaturated -100i8 -100i8", -128i8, i8);
assert_evals_to!("Num.addSaturated 40000u16 40000u16", 65535u16, u16);
assert_evals_to!("Num.addSaturated 20000i16 20000i16", 32767i16, i16);
assert_evals_to!("Num.addSaturated -20000i16 -20000i16", -32768i16, i16);
assert_evals_to!(
"Num.subWrap -9_223_372_036_854_775_808 1",
std::i64::MAX,
"Num.addSaturated 3000000000u32 3000000000u32",
4294967295u32,
u32
);
assert_evals_to!(
"Num.addSaturated 2000000000i32 2000000000i32",
2147483647i32,
i32
);
assert_evals_to!(
"Num.addSaturated -2000000000i32 -2000000000i32",
-2147483648i32,
i32
);
assert_evals_to!(
"Num.addSaturated 10000000000000000000u64 10000000000000000000u64",
18446744073709551615u64,
u64
);
assert_evals_to!(
"Num.addSaturated 5000000000000000000i64 5000000000000000000i64 ",
9223372036854775807i64,
i64
);
assert_evals_to!(
"Num.addSaturated -5000000000000000000i64 -5000000000000000000i64 ",
-9223372036854775808i64,
i64
);
assert_evals_to!(
"Num.addSaturated 200000000000000000000000000000000000000u128 200000000000000000000000000000000000000u128",
U128::from(340282366920938463463374607431768211455u128),
U128
);
assert_evals_to!(
"Num.addSaturated 100000000000000000000000000000000000000i128 100000000000000000000000000000000000000i128",
I128::from(170141183460469231731687303715884105727i128),
I128
);
assert_evals_to!(
"Num.addSaturated -100000000000000000000000000000000000000i128 -100000000000000000000000000000000000000i128",
I128::from(-170141183460469231731687303715884105728i128),
I128
);
assert_evals_to!(
"Num.addSaturated Num.maxF32 Num.maxF32",
std::f32::INFINITY,
f32
);
assert_evals_to!(
"Num.addSaturated Num.minF32 Num.minF32",
std::f32::NEG_INFINITY,
f32
);
assert_evals_to!(
"Num.addSaturated Num.maxF64 Num.maxF64",
std::f64::INFINITY,
f64
);
assert_evals_to!(
"Num.addSaturated Num.minF64 Num.minF64",
std::f64::NEG_INFINITY,
f64
);
assert_evals_to!("Num.subWrap -128i8 1", std::i8::MAX, i8);
assert_evals_to!(
"Num.addSaturated 170_141_183_460_469_231_731dec 1",
RocDec::from_str("170141183460469231731.687303715884105727").unwrap(),
RocDec
);
assert_evals_to!(
"Num.addSaturated -170_141_183_460_469_231_731dec -1",
RocDec::from_str("-170141183460469231731.687303715884105728").unwrap(),
RocDec
);
}
#[test]
@ -2060,6 +2165,128 @@ fn float_sub_checked() {
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
#[should_panic(expected = r#"Roc failed with message: "Integer subtraction overflowed!"#)]
fn int_sub_overflow() {
assert_evals_to!("-9_223_372_036_854_775_808 - 1", 0, i64);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn sub_wrap() {
assert_evals_to!("Num.subWrap 1u8 10u8", 247u8, u8);
assert_evals_to!("Num.subWrap 127i8 -10i8", -119i8, i8);
assert_evals_to!("Num.subWrap -127i8 10i8", 119i8, i8);
assert_evals_to!("Num.subWrap 1u16 10", 65527u16, u16);
assert_evals_to!("Num.subWrap 32767i16 -10", -32759i16, i16);
assert_evals_to!("Num.subWrap -32767i16 10", 32759i16, i16);
assert_evals_to!("Num.subWrap 1u32 10", 4294967287u32, u32);
assert_evals_to!("Num.subWrap 2147483647i32 -10", -2147483639i32, i32);
assert_evals_to!("Num.subWrap -2147483647i32 10", 2147483639i32, i32);
assert_evals_to!("Num.subWrap 1u64 10", 18446744073709551607u64, u64);
assert_evals_to!(
"Num.subWrap 9223372036854775807i64 -10",
-9223372036854775799i64,
i64
);
assert_evals_to!(
"Num.subWrap -9223372036854775807i64 10",
9223372036854775799i64,
i64
);
assert_evals_to!(
"Num.subWrap 1u128 10",
U128::from(340282366920938463463374607431768211447u128),
U128
);
assert_evals_to!(
"Num.subWrap 170141183460469231731687303715884105727i128 -10",
I128::from(-170141183460469231731687303715884105719i128),
I128
);
assert_evals_to!(
"Num.subWrap -170141183460469231731687303715884105727i128 10",
I128::from(170141183460469231731687303715884105719i128),
I128
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn sub_saturated() {
assert_evals_to!("Num.subSaturated 1u8 10u8", 0u8, u8);
assert_evals_to!("Num.subSaturated 100i8 -100i8", 127i8, i8);
assert_evals_to!("Num.subSaturated -100i8 100i8", -128i8, i8);
assert_evals_to!("Num.subSaturated 1u16 10u16", 0u16, u16);
assert_evals_to!("Num.subSaturated 20000i16 -20000i16", 32767i16, i16);
assert_evals_to!("Num.subSaturated -20000i16 20000i16", -32768i16, i16);
assert_evals_to!("Num.subSaturated 1u32 10u32", 0u32, u32);
assert_evals_to!(
"Num.subSaturated 2000000000i32 -2000000000i32",
2147483647i32,
i32
);
assert_evals_to!(
"Num.subSaturated -2000000000i32 2000000000i32",
-2147483648i32,
i32
);
assert_evals_to!("Num.subSaturated 1u64 10u64", 0u64, u64);
assert_evals_to!(
"Num.subSaturated 5000000000000000000i64 -5000000000000000000i64 ",
9223372036854775807i64,
i64
);
assert_evals_to!(
"Num.subSaturated -5000000000000000000i64 5000000000000000000i64 ",
-9223372036854775808i64,
i64
);
assert_evals_to!("Num.subSaturated 1u128 10", U128::from(0u128), U128);
assert_evals_to!(
"Num.subSaturated 100000000000000000000000000000000000000i128 -100000000000000000000000000000000000000i128",
I128::from(170141183460469231731687303715884105727i128),
I128
);
assert_evals_to!(
"Num.subSaturated -100000000000000000000000000000000000000i128 100000000000000000000000000000000000000i128",
I128::from(-170141183460469231731687303715884105728i128),
I128
);
assert_evals_to!(
"Num.subSaturated Num.maxF32 -Num.maxF32",
std::f32::INFINITY,
f32
);
assert_evals_to!(
"Num.subSaturated Num.minF32 -Num.minF32",
std::f32::NEG_INFINITY,
f32
);
assert_evals_to!(
"Num.subSaturated Num.maxF64 -Num.maxF64",
std::f64::INFINITY,
f64
);
assert_evals_to!(
"Num.subSaturated Num.minF64 -Num.minF64",
std::f64::NEG_INFINITY,
f64
);
assert_evals_to!(
"Num.subSaturated 170_141_183_460_469_231_731dec -1",
RocDec::from_str("170141183460469231731.687303715884105727").unwrap(),
RocDec
);
assert_evals_to!(
"Num.subSaturated -170_141_183_460_469_231_731dec 1",
RocDec::from_str("-170141183460469231731.687303715884105728").unwrap(),
RocDec
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
#[should_panic(expected = r#"Roc failed with message: "Integer multiplication overflowed!"#)]
@ -2086,18 +2313,6 @@ fn float_negative_mul_overflow() {
assert_evals_to!("-1.7976931348623157e308f64 * 2", -f64::INFINITY, f64);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn int_mul_wrap_i64() {
assert_evals_to!("Num.mulWrap Num.maxI64 2", -2, i64);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn int_mul_wrap_i128() {
assert_evals_to!("Num.mulWrap Num.maxI128 2", I128::from(-2), I128);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn int_mul_checked() {
@ -2130,6 +2345,103 @@ fn float_mul_checked() {
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn mul_wrap() {
assert_evals_to!("Num.mulWrap 255u8 2", 254u8, u8);
assert_evals_to!("Num.mulWrap 127i8 2", -2i8, i8);
assert_evals_to!("Num.mulWrap -127i8 2", 2i8, i8);
assert_evals_to!("Num.mulWrap 65535u16 2", 65534u16, u16);
assert_evals_to!("Num.mulWrap 32767i16 2", -2i16, i16);
assert_evals_to!("Num.mulWrap -32767i16 2", 2i16, i16);
assert_evals_to!("Num.mulWrap 4294967295u32 2", 4294967294u32, u32);
assert_evals_to!("Num.mulWrap 2147483647i32 2", -2i32, i32);
assert_evals_to!("Num.mulWrap -2147483647i32 2", 2i32, i32);
assert_evals_to!(
"Num.mulWrap 18446744073709551615u64 2",
18446744073709551614u64,
u64
);
assert_evals_to!("Num.mulWrap 9223372036854775807i64 2", -2i64, i64);
assert_evals_to!("Num.mulWrap -9223372036854775807i64 2", 2i64, i64);
assert_evals_to!(
"Num.mulWrap 340282366920938463463374607431768211455u128 2",
U128::from(340282366920938463463374607431768211454u128),
U128
);
assert_evals_to!(
"Num.mulWrap 170141183460469231731687303715884105727i128 2",
I128::from(-2i128),
I128
);
assert_evals_to!(
"Num.mulWrap -170141183460469231731687303715884105727i128 2",
I128::from(2i128),
I128
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn mul_saturated() {
assert_evals_to!("Num.mulSaturated 200u8 2", 255u8, u8);
assert_evals_to!("Num.mulSaturated 100i8 2", 127i8, i8);
assert_evals_to!("Num.mulSaturated -100i8 2", -128i8, i8);
assert_evals_to!("Num.mulSaturated 40000u16 2", 65535u16, u16);
assert_evals_to!("Num.mulSaturated 20000i16 2", 32767i16, i16);
assert_evals_to!("Num.mulSaturated -20000i16 2", -32768i16, i16);
assert_evals_to!("Num.mulSaturated 3000000000u32 2", 4294967295u32, u32);
assert_evals_to!("Num.mulSaturated 2000000000i32 2", 2147483647i32, i32);
assert_evals_to!("Num.mulSaturated -2000000000i32 2", -2147483648i32, i32);
assert_evals_to!(
"Num.mulSaturated 10000000000000000000u64 2",
18446744073709551615u64,
u64
);
assert_evals_to!(
"Num.mulSaturated 5000000000000000000i64 2",
9223372036854775807i64,
i64
);
assert_evals_to!(
"Num.mulSaturated -5000000000000000000i64 2",
-9223372036854775808i64,
i64
);
assert_evals_to!(
"Num.mulSaturated 200000000000000000000000000000000000000u128 2",
U128::from(340282366920938463463374607431768211455u128),
U128
);
assert_evals_to!(
"Num.mulSaturated 100000000000000000000000000000000000000i128 2",
I128::from(170141183460469231731687303715884105727i128),
I128
);
assert_evals_to!(
"Num.mulSaturated -100000000000000000000000000000000000000i128 2",
I128::from(-170141183460469231731687303715884105728i128),
I128
);
assert_evals_to!("Num.mulSaturated Num.maxF32 2", std::f32::INFINITY, f32);
assert_evals_to!("Num.mulSaturated Num.minF32 2", std::f32::NEG_INFINITY, f32);
assert_evals_to!("Num.mulSaturated Num.maxF64 2", std::f64::INFINITY, f64);
assert_evals_to!("Num.mulSaturated Num.minF64 2", std::f64::NEG_INFINITY, f64);
// TODO: This doesn't work anywhere? It returns -1.374607431768211456 : Dec ?
/*
assert_evals_to!(
"Num.mulSaturated 170_141_183_460_469_231_731dec 2",
RocDec::from_str("170141183460469231731.687303715884105727").unwrap(),
RocDec
);
assert_evals_to!(
"Num.mulSaturated -170_141_183_460_469_231_731dec 2",
RocDec::from_str("-170141183460469231731.687303715884105728").unwrap(),
RocDec
);
*/
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn shift_left_by() {
@ -2992,167 +3304,6 @@ fn u8_mul_greater_than_i8() {
u8
)
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn add_saturated() {
assert_evals_to!(
indoc!(
r"
x : U8
x = 200
y : U8
y = 200
Num.addSaturated x y
"
),
255,
u8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = 100
y : I8
y = 100
Num.addSaturated x y
"
),
127,
i8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = -100
y : I8
y = -100
Num.addSaturated x y
"
),
-128,
i8
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn sub_saturated() {
assert_evals_to!(
indoc!(
r"
x : U8
x = 10
y : U8
y = 20
Num.subSaturated x y
"
),
0,
u8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = -100
y : I8
y = 100
Num.subSaturated x y
"
),
-128,
i8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = 100
y : I8
y = -100
Num.subSaturated x y
"
),
127,
i8
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn mul_saturated() {
assert_evals_to!(
indoc!(
r"
x : U8
x = 20
y : U8
y = 20
Num.mulSaturated x y
"
),
255,
u8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = -20
y : I8
y = -20
Num.mulSaturated x y
"
),
127,
i8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = 20
y : I8
y = -20
Num.mulSaturated x y
"
),
-128,
i8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = -20
y : I8
y = 20
Num.mulSaturated x y
"
),
-128,
i8
);
assert_evals_to!(
indoc!(
r"
x : I8
x = 20
y : I8
y = 20
Num.mulSaturated x y
"
),
127,
i8
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm", feature = "gen-dev"))]
fn monomorphized_ints() {
@ -3950,3 +4101,12 @@ fn infinity_f32() {
fn infinity_f64() {
assert_evals_to!(r"Num.infinityF64", f64::INFINITY, f64);
}
#[allow(clippy::non_minimal_cfg)]
#[test]
#[cfg(any(feature = "gen-llvm"))]
fn cast_signed_unsigned() {
assert_evals_to!(r"Num.toI16 255u8", 255, i16);
assert_evals_to!(r"Num.toU16 127i8", 127, u16);
assert_evals_to!(r"Num.toU8 127i8", 127, u8);
assert_evals_to!(r"Num.toI8 127u8", 127, i8);
}

View file

@ -340,8 +340,6 @@ pub(crate) fn asm_evals_to<T, U, F>(
let result = crate::helpers::dev::run_test_main::<T>(&lib);
if !errors.is_empty() {
dbg!(&errors);
assert_eq!(
errors,
std::vec::Vec::new(),

View file

@ -255,7 +255,7 @@ fn create_llvm_module<'a>(
};
let (main_fn_name, main_fn) = match config.mode {
LlvmBackendMode::Binary => unreachable!(),
LlvmBackendMode::BinaryDev => unreachable!(),
LlvmBackendMode::BinaryWithExpect => unreachable!(),
LlvmBackendMode::BinaryGlue => unreachable!(),
LlvmBackendMode::CliTest => unreachable!(),
LlvmBackendMode::WasmGenTest => roc_gen_llvm::llvm::build::build_wasm_test_wrapper(

View file

@ -24,25 +24,25 @@ procedure Str.66 (Str.191):
let Str.247 : [C {}, C U64] = TagId(0) Str.248;
ret Str.247;
procedure Test.3 (Test.4):
joinpoint Test.14 Test.5:
let Test.12 : [C {}, C U64] = TagId(1) Test.5;
ret Test.12;
procedure Test.1 (Test.2):
joinpoint Test.11 Test.3:
let Test.7 : [C {}, C U64] = TagId(1) Test.3;
ret Test.7;
in
let Test.13 : [C {}, C U64] = CallByName Str.26 Test.4;
let Test.18 : U8 = 1i64;
let Test.19 : U8 = GetTagId Test.13;
let Test.20 : Int1 = lowlevel Eq Test.18 Test.19;
if Test.20 then
let Test.6 : U64 = UnionAtIndex (Id 1) (Index 0) Test.13;
jump Test.14 Test.6;
let Test.10 : [C {}, C U64] = CallByName Str.26 Test.2;
let Test.15 : U8 = 1i64;
let Test.16 : U8 = GetTagId Test.10;
let Test.17 : Int1 = lowlevel Eq Test.15 Test.16;
if Test.17 then
let Test.8 : U64 = UnionAtIndex (Id 1) (Index 0) Test.10;
jump Test.11 Test.8;
else
let Test.7 : {} = UnionAtIndex (Id 0) (Index 0) Test.13;
let Test.17 : [C {}, C U64] = TagId(0) Test.7;
ret Test.17;
let Test.9 : {} = UnionAtIndex (Id 0) (Index 0) Test.10;
let Test.14 : [C {}, C U64] = TagId(0) Test.9;
ret Test.14;
procedure Test.0 ():
let Test.11 : Str = "123";
let Test.10 : [C {}, C U64] = CallByName Test.3 Test.11;
dec Test.11;
ret Test.10;
let Test.6 : Str = "123";
let Test.5 : [C {}, C U64] = CallByName Test.1 Test.6;
dec Test.6;
ret Test.5;

View file

@ -12,13 +12,16 @@ version.workspace = true
[dependencies]
bumpalo.workspace = true
roc_can.workspace = true
roc_collections.workspace = true
roc_error_macros.workspace = true
roc_fmt.workspace = true
roc_module.workspace = true
roc_parse.workspace = true
roc_region.workspace = true
roc_test_utils.workspace = true
roc_test_utils_dir.workspace = true
roc_types.workspace = true
[dev-dependencies]
indoc.workspace = true

View file

@ -10,9 +10,12 @@ edition = "2021"
cargo-fuzz = true
[dependencies]
test_syntax.workspace = true
roc_parse.workspace = true
bumpalo = { workspace = true, features = ["collections"] }
# WARNING! Do not update these dependencies to use the dot workspace syntax.
# this crate is intentionally not a part of the overall workspace, so making changes here
# is neither necessary nor desired.
test_syntax = { path = "../../test_syntax" }
roc_parse = { path = "../../parse" }
bumpalo = { version = "3.12.0", features = ["collections"] }
libfuzzer-sys = "0.4"
# Prevent this from interfering with workspaces

View file

@ -0,0 +1,25 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
(pkgs.rustup.override {
targets = ["x86_64-unknown-linux-gnu"]; # Adjust for your desired target
})
];
shellHook = ''
# Ensure rustup is initialized
export RUSTUP_HOME="$PWD/.rustup"
export CARGO_HOME="$PWD/.cargo"
mkdir -p $RUSTUP_HOME $CARGO_HOME
# Install Rust nightly
rustup install nightly
rustup default nightly
# Add rustup and cargo to PATH
export PATH="$CARGO_HOME/bin:$PATH"
echo "Rust nightly and Cargo are available on PATH."
'';
}

View file

@ -11,7 +11,7 @@ fuzz_target!(|data: &[u8]| {
let ast = input.parse_in(&arena);
if let Ok(ast) = ast {
if !ast.is_malformed() {
input.check_invariants(|_| (), true);
input.check_invariants(|_| (), true, None);
}
}
}

View file

@ -1,6 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use bumpalo::Bump;
use libfuzzer_sys::fuzz_target;
use test_syntax::test_helpers::Input;
fuzz_target!(|data: &[u8]| {

View file

@ -102,6 +102,12 @@ fn round_trip_once(input: Input<'_>) -> Option<String> {
return Some("Different ast".to_string());
}
let reformatted = reparsed_ast.format();
if output != reformatted {
return Some("Formatting not stable".to_string());
}
None
}

View file

@ -1,14 +1,33 @@
use std::path::Path;
use bumpalo::Bump;
use roc_fmt::{annotation::Formattable, header::fmt_header};
use roc_can::desugar;
use roc_can::env::Env;
use roc_can::expr::canonicalize_expr;
use roc_can::scope::Scope;
use roc_error_macros::set_panic_not_exit;
use roc_fmt::{annotation::Formattable, header::fmt_header, MigrationFlags};
use roc_module::ident::QualifiedModuleName;
use roc_module::symbol::{IdentIds, Interns, ModuleIds, PackageModuleIds, Symbol};
use roc_parse::ast::RecursiveValueDefIter;
use roc_parse::ast::ValueDef;
use roc_parse::header::parse_module_defs;
use roc_parse::parser::Parser;
use roc_parse::parser::SyntaxError;
use roc_parse::state::State;
use roc_parse::test_helpers::parse_loc_with;
use roc_parse::{ast::Malformed, normalize::Normalize};
use roc_parse::{
ast::{Defs, Expr, FullAst, Header, Malformed, SpacesBefore},
header::parse_module_defs,
normalize::Normalize,
parser::{Parser, SyntaxError},
state::State,
test_helpers::{parse_defs_with, parse_expr_with, parse_header_with},
ast::{Defs, Expr, FullAst, Header, SpacesBefore},
test_helpers::{parse_defs_with, parse_header_with},
};
use roc_region::all::Loc;
use roc_region::all::Region;
use roc_test_utils::assert_multiline_str_eq;
use roc_types::{
subs::{VarStore, Variable},
types::{AliasVar, Type},
};
use roc_fmt::Buf;
@ -74,7 +93,7 @@ pub enum Output<'a> {
ModuleDefs(Defs<'a>),
Expr(Expr<'a>),
Expr(Loc<Expr<'a>>),
Full(FullAst<'a>),
}
@ -82,7 +101,8 @@ pub enum Output<'a> {
impl<'a> Output<'a> {
pub fn format(&self) -> InputOwned {
let arena = Bump::new();
let mut buf = Buf::new_in(&arena);
let flags = MigrationFlags::new(false);
let mut buf = Buf::new_in(&arena, flags);
match self {
Output::Header(header) => {
fmt_header(&mut buf, header);
@ -115,6 +135,107 @@ impl<'a> Output<'a> {
Output::Full { .. } => format!("{self:#?}\n"),
}
}
pub fn canonicalize(&self, arena: &Bump, src: &str) {
set_panic_not_exit(true); // can has a bunch of internal_error! calls
match self {
Output::Header(_) => {}
Output::ModuleDefs(_) => {
// TODO: canonicalize module defs
}
Output::Full(_) => {
// TODO: canonicalize full ast
}
Output::Expr(loc_expr) => {
let mut var_store = VarStore::default();
let mut imported: Vec<(QualifiedModuleName, Region)> = vec![];
let empty_defs = Defs::default();
let mut it = RecursiveValueDefIter::new(&empty_defs);
it.push_pending_from_expr(&loc_expr.value);
for (def, region) in it {
if let ValueDef::ModuleImport(import) = def {
imported.push((import.name.value.into(), *region));
}
}
let mut module_ids = ModuleIds::default();
let mut qualified_module_ids = PackageModuleIds::default();
let mut dep_idents = IdentIds::exposed_builtins(0);
// Make sure the module_ids has ModuleIds for all our deps,
// then record those ModuleIds in can_module_ids for later.
// For each of our imports, add an entry to deps_by_name
//
// e.g. for `import pf.Foo exposing [bar]`, add `Foo` to deps_by_name
//
// Also build a list of imported_values_to_expose (like `bar` above.)
for (qualified_module_name, _region) in imported.into_iter() {
let pq_module_name = qualified_module_name.into_pq_module_name(None);
let module_id = qualified_module_ids.get_or_insert(&pq_module_name);
dep_idents.get_or_insert(module_id);
}
let home = module_ids.get_or_insert(&"Test".into());
let mut scope = Scope::new(
home,
"TestPath".into(),
IdentIds::default(),
Default::default(),
);
let mut env = Env::new(
arena,
src,
home,
Path::new("Test.roc"),
&dep_idents,
&qualified_module_ids,
None,
roc_can::env::FxMode::PurityInference,
);
// Desugar operators (convert them to Apply calls, taking into account
// operator precedence and associativity rules), before doing other canonicalization.
//
// If we did this *during* canonicalization, then each time we
// visited a BinOp node we'd recursively try to apply this to each of its nested
// operators, and then again on *their* nested operators, ultimately applying the
// rules multiple times unnecessarily.
let loc_expr = desugar::desugar_expr(&mut env, &mut scope, loc_expr);
scope.add_alias(
Symbol::NUM_INT,
Region::zero(),
vec![Loc::at_zero(AliasVar::unbound(
"a".into(),
Variable::EMPTY_RECORD,
))],
vec![],
Type::EmptyRec,
roc_types::types::AliasKind::Structural,
);
let (_loc_expr, _output) = canonicalize_expr(
&mut env,
&mut var_store,
&mut scope,
Region::zero(),
&loc_expr.value,
);
let mut all_ident_ids = IdentIds::exposed_builtins(1);
all_ident_ids.insert(home, scope.locals.ident_ids);
let _interns = Interns {
module_ids: env.qualified_module_ids.clone().into_module_ids(),
all_ident_ids,
};
}
}
}
}
impl<'a> Malformed for Output<'a> {
@ -162,7 +283,7 @@ impl<'a> Input<'a> {
}
Input::Expr(input) => {
let expr = parse_expr_with(arena, input)?;
let expr = parse_loc_with(arena, input).map_err(|e| e.problem)?;
Ok(Output::Expr(expr))
}
@ -196,6 +317,7 @@ impl<'a> Input<'a> {
&self,
handle_formatted_output: impl Fn(Input),
check_idempotency: bool,
canonicalize_mode: Option<bool>,
) {
let arena = Bump::new();
@ -239,7 +361,7 @@ impl<'a> Input<'a> {
self.as_str(),
output.as_ref().as_str(),
actual,
reparsed_ast_normalized
reparsed_ast
);
}
@ -258,5 +380,27 @@ impl<'a> Input<'a> {
assert_multiline_str_eq!(output.as_ref().as_str(), reformatted.as_ref().as_str());
}
}
if let Some(expect_panic) = canonicalize_mode {
if expect_panic {
let text = self.as_str();
let res = std::panic::catch_unwind(|| {
let new_arena = Bump::new();
actual.canonicalize(&new_arena, text);
});
assert!(
res.is_err(),
"Canonicalize was expected to panic, but it did not. \
If you're running test_snapshots, you may need to remove this test from \
the list of tests that are expected to panic (great!), \
in `fn expect_canonicalize_panics`"
);
} else {
// TODO grab the output here and assert things about it.
// For now we just make sure that it doesn't crash on this input
actual.canonicalize(&arena, self.as_str());
}
}
}
}

View file

@ -1 +1 @@
Expr(When(IfGuard(Start(@28), @27), @0), @0)
Expr(When(IfGuard(Start(@27), @27), @0), @0)

View file

@ -0,0 +1 @@
Expr(If(Condition(Start(@2), @2), @0), @0)

Some files were not shown because too many files have changed in this diff Show more