mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 03:42:17 +00:00
Move uitests to their own crate
This commit is contained in:
parent
630a8e32d4
commit
2f43aad8d0
65 changed files with 507 additions and 9 deletions
33
crates/compiler/uitest/Cargo.toml
Normal file
33
crates/compiler/uitest/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "uitest"
|
||||
description = "Integration tests for the solver."
|
||||
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[[test]]
|
||||
name = "uitest"
|
||||
path = "tests/uitest.rs"
|
||||
harness = false
|
||||
|
||||
[dev-dependencies]
|
||||
roc_builtins = { path = "../builtins" }
|
||||
roc_derive = { path = "../derive", features = ["debug-derived-symbols"] }
|
||||
roc_load = { path = "../load" }
|
||||
roc_parse = { path = "../parse" }
|
||||
roc_problem = { path = "../problem" }
|
||||
roc_reporting = { path = "../../reporting" }
|
||||
roc_solve = { path = "../solve" }
|
||||
roc_target = { path = "../roc_target" }
|
||||
test_solve_helpers = { path = "../test_solve_helpers" }
|
||||
|
||||
bumpalo.workspace = true
|
||||
indoc.workspace = true
|
||||
insta.workspace = true
|
||||
lazy_static.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
regex.workspace = true
|
||||
tempfile.workspace = true
|
||||
libtest-mimic.workspace = true
|
448
crates/compiler/uitest/src/uitest.rs
Normal file
448
crates/compiler/uitest/src/uitest.rs
Normal file
|
@ -0,0 +1,448 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use libtest_mimic::{run, Arguments, Failed, Trial};
|
||||
use regex::Regex;
|
||||
use test_solve_helpers::{
|
||||
infer_queries, Elaboration, InferOptions, InferredProgram, InferredQuery, MUTLILINE_MARKER,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args = Arguments::from_args();
|
||||
|
||||
let test_files = collect_uitest_files()?;
|
||||
let tests = test_files
|
||||
.into_iter()
|
||||
.map(into_test)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
run(&args, tests).exit()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref UITEST_PATH: PathBuf = PathBuf::from(std::env!("ROC_WORKSPACE_DIR"))
|
||||
.join("crates")
|
||||
.join("compiler")
|
||||
.join("uitest")
|
||||
.join("tests");
|
||||
|
||||
/// # +opt infer:<opt>
|
||||
static ref RE_OPT_INFER: Regex =
|
||||
Regex::new(r#"# \+opt infer:(?P<opt>.*)"#).unwrap();
|
||||
|
||||
/// # +opt print:<opt>
|
||||
static ref RE_OPT_PRINT: Regex =
|
||||
Regex::new(r#"# \+opt print:(?P<opt>.*)"#).unwrap();
|
||||
}
|
||||
|
||||
fn collect_uitest_files() -> io::Result<Vec<PathBuf>> {
|
||||
let mut tests = Vec::with_capacity(200);
|
||||
|
||||
let mut dirs_to_visit = vec![UITEST_PATH.clone()];
|
||||
while let Some(dir) = dirs_to_visit.pop() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let entry_type = entry.file_type()?;
|
||||
if entry_type.is_dir() {
|
||||
dirs_to_visit.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.extension() == Some(OsStr::new("txt")) {
|
||||
tests.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tests)
|
||||
}
|
||||
|
||||
fn into_test(path: PathBuf) -> io::Result<Trial> {
|
||||
let name = path
|
||||
.strip_prefix(UITEST_PATH.as_path().parent().unwrap())
|
||||
.expect("collected path does not have uitest prefix")
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
let trial = Trial::test(name, move || run_test(path));
|
||||
Ok(trial)
|
||||
}
|
||||
|
||||
fn run_test(path: PathBuf) -> Result<(), Failed> {
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let TestCase {
|
||||
infer_options,
|
||||
print_options,
|
||||
source,
|
||||
} = TestCase::parse(data)?;
|
||||
|
||||
let inferred_program = infer_queries(&source, infer_options)?;
|
||||
|
||||
{
|
||||
let mut fd = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
|
||||
assemble_query_output(&mut fd, &source, inferred_program, print_options)?;
|
||||
}
|
||||
|
||||
check_for_changes(&path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const EMIT_HEADER: &str = "# -emit:";
|
||||
|
||||
struct TestCase {
|
||||
infer_options: InferOptions,
|
||||
print_options: PrintOptions,
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PrintOptions {
|
||||
can_decls: bool,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
fn parse(mut data: String) -> Result<Self, Failed> {
|
||||
// Drop anything following `# -emit:` header lines; that's the output.
|
||||
if let Some(drop_at) = data.find(EMIT_HEADER) {
|
||||
data.truncate(drop_at);
|
||||
data.truncate(data.trim_end().len());
|
||||
}
|
||||
|
||||
Ok(TestCase {
|
||||
infer_options: Self::parse_infer_options(&data)?,
|
||||
print_options: Self::parse_print_options(&data)?,
|
||||
source: data,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_infer_options(data: &str) -> Result<InferOptions, Failed> {
|
||||
let mut infer_opts = InferOptions {
|
||||
no_promote: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let found_infer_opts = RE_OPT_INFER.captures_iter(data);
|
||||
for infer_opt in found_infer_opts {
|
||||
let opt = infer_opt.name("opt").unwrap().as_str();
|
||||
match opt {
|
||||
"allow_errors" => infer_opts.allow_errors = true,
|
||||
"print_only_under_alias" => infer_opts.print_only_under_alias = true,
|
||||
other => return Err(format!("unknown infer option: {other}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(infer_opts)
|
||||
}
|
||||
|
||||
fn parse_print_options(data: &str) -> Result<PrintOptions, Failed> {
|
||||
let mut print_opts = PrintOptions::default();
|
||||
|
||||
let found_infer_opts = RE_OPT_PRINT.captures_iter(data);
|
||||
for infer_opt in found_infer_opts {
|
||||
let opt = infer_opt.name("opt").unwrap().as_str();
|
||||
match opt {
|
||||
"can_decls" => print_opts.can_decls = true,
|
||||
other => return Err(format!("unknown print option: {other}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(print_opts)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_changes(path: &Path) -> Result<(), Failed> {
|
||||
Command::new("git").args(["add", "-N"]).arg(path).output()?;
|
||||
|
||||
let has_changes = Command::new("git")
|
||||
.args(["diff", "--color=always"])
|
||||
.arg(path)
|
||||
.output()?;
|
||||
|
||||
if !has_changes.stdout.is_empty() {
|
||||
return Err(format!(
|
||||
"{}\nOutput has changed. If it looks okay, `git` add the file.",
|
||||
std::str::from_utf8(&has_changes.stdout)?
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assemble the output for a test, with queries elaborated in-line.
|
||||
fn assemble_query_output(
|
||||
writer: &mut impl io::Write,
|
||||
source: &str,
|
||||
inferred_program: InferredProgram,
|
||||
print_options: PrintOptions,
|
||||
) -> io::Result<()> {
|
||||
// Reverse the queries so that we can pop them off the end as we pass through the lines.
|
||||
let (queries, program) = inferred_program.decompose();
|
||||
let mut sorted_queries = queries.into_sorted();
|
||||
sorted_queries.reverse();
|
||||
|
||||
let mut reflow = Reflow::new_unindented(writer);
|
||||
write_source_with_answers(&mut reflow, source, sorted_queries, 0)?;
|
||||
|
||||
// Finish up with any remaining print options we were asked to provide.
|
||||
let PrintOptions { can_decls } = print_options;
|
||||
if can_decls {
|
||||
writeln!(writer, "\n{EMIT_HEADER}can_decls")?;
|
||||
program.write_can_decls(writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_source_with_answers<W: io::Write>(
|
||||
reflow: &mut Reflow<'_, W>,
|
||||
source: &str,
|
||||
mut sorted_queries: Vec<InferredQuery>,
|
||||
offset_line: usize,
|
||||
) -> io::Result<()> {
|
||||
for (i, line) in source.lines().enumerate() {
|
||||
let i = i + offset_line;
|
||||
|
||||
let mut is_query_line = false;
|
||||
|
||||
// Write all elaborated query lines if applicable.
|
||||
while matches!(
|
||||
sorted_queries.last(),
|
||||
Some(InferredQuery {
|
||||
source_line_column,
|
||||
..
|
||||
}) if source_line_column.line == i as _
|
||||
) {
|
||||
let inferred = sorted_queries.pop().unwrap();
|
||||
|
||||
reflow.scoped(|reflow| reconstruct_comment_line(reflow, inferred))?;
|
||||
|
||||
reflow.write("\n")?;
|
||||
|
||||
is_query_line = true;
|
||||
}
|
||||
|
||||
// If this was previously a multi-line query output line, skip it, since we already wrote
|
||||
// the new output above.
|
||||
if line.contains(MUTLILINE_MARKER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, write the Roc source line.
|
||||
if !is_query_line {
|
||||
reflow.write(line.trim_end())?;
|
||||
reflow.write("\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_queries = sorted_queries.into_iter().peekable();
|
||||
while let Some(sorted_query) = sorted_queries.next() {
|
||||
reflow.scoped(|reflow| reconstruct_comment_line(reflow, sorted_query))?;
|
||||
|
||||
// Only write a newline if we're not yet at the end of the source.
|
||||
// Otherwise, a newline will be written for us after exiting the reconstruction of the
|
||||
// comment line, since this must happen in the reconsutrction of a multi-line query.
|
||||
if sorted_queries.peek().is_some() {
|
||||
reflow.write("\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconstruct_comment_line<W: io::Write>(
|
||||
reflow: &mut Reflow<'_, W>,
|
||||
inferred: InferredQuery,
|
||||
) -> io::Result<()> {
|
||||
let InferredQuery {
|
||||
comment_column,
|
||||
source_line_column,
|
||||
source,
|
||||
elaboration,
|
||||
} = inferred;
|
||||
|
||||
reflow.add_layer(comment_column as _, source_line_column.column as _);
|
||||
reflow.write_and_bump(&format!("{source} "))?;
|
||||
|
||||
match elaboration {
|
||||
Elaboration::Specialization {
|
||||
specialized_name,
|
||||
typ,
|
||||
} => {
|
||||
reflow.write_and_bump(&format!("{specialized_name}: "))?;
|
||||
reflow.write(&typ)
|
||||
}
|
||||
Elaboration::Source { source: _, typ } => reflow.write(&typ),
|
||||
Elaboration::Instantiation {
|
||||
typ,
|
||||
source,
|
||||
offset_line,
|
||||
queries_in_instantiation,
|
||||
} => {
|
||||
reflow.write(&typ)?;
|
||||
|
||||
// Write the source on new line, but at the reflow column the comment is aligned at.
|
||||
reflow.set_content(source_line_column.column as _);
|
||||
reflow.write("\n")?;
|
||||
|
||||
let queries = queries_in_instantiation.into_sorted();
|
||||
|
||||
write_source_with_answers(reflow, source.trim_end(), queries, offset_line as _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Reflow<'a, W: io::Write> {
|
||||
writer: &'a mut W,
|
||||
state: ReflowState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ReflowState {
|
||||
/// true if the first line of the elaboration comment has been written.
|
||||
top_line_written: bool,
|
||||
/// Number of `content columns` prefixes written.
|
||||
/// If this equals the number of content columns, the whole prefix for a line has been written.
|
||||
content_prefixes_written: usize,
|
||||
/// The column at which to insert the comment prefix "#".
|
||||
comment_column: usize,
|
||||
/// The columns at which content occurs.
|
||||
/// If the stack is >1, then
|
||||
/// - at the first content column, the [MUTLILINE_MARKER] may be written as appropriate
|
||||
/// - for subsequent columns, spaces are inserted until the column is reached.
|
||||
content_columns: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> std::ops::Deref for Reflow<'a, W> {
|
||||
type Target = ReflowState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> std::ops::DerefMut for Reflow<'a, W> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> Reflow<'a, W> {
|
||||
fn new_unindented(writer: &'a mut W) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
state: ReflowState {
|
||||
top_line_written: false,
|
||||
content_prefixes_written: 0,
|
||||
comment_column: 0,
|
||||
content_columns: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn scoped<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
|
||||
let state = self.state.clone();
|
||||
let result = f(self);
|
||||
self.state = state;
|
||||
result
|
||||
}
|
||||
|
||||
fn add_layer(&mut self, comment_column: usize, content_column: usize) {
|
||||
if self.comment_column == 0 {
|
||||
// If the comment column is not yet set, this is the top-level and we should update the
|
||||
// state; otherwise, we already have a comment column, only add to the content-ful
|
||||
// layer.
|
||||
self.comment_column = comment_column;
|
||||
}
|
||||
self.content_columns.push(content_column);
|
||||
}
|
||||
|
||||
fn set_content(&mut self, content_column: usize) {
|
||||
let latest_column = self
|
||||
.content_columns
|
||||
.last_mut()
|
||||
.expect("cannot set content before adding a layer");
|
||||
*latest_column = content_column;
|
||||
}
|
||||
|
||||
fn write(&mut self, content: &str) -> io::Result<()> {
|
||||
for (i, content_line) in content.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
// new line
|
||||
writeln!(self.writer)?;
|
||||
self.content_prefixes_written = 0;
|
||||
}
|
||||
|
||||
// If the content columns are empty, this is top-level and we
|
||||
// have no prefix to write.
|
||||
if self.content_prefixes_written != self.content_columns.len() {
|
||||
if self.content_prefixes_written == 0 {
|
||||
self.write_n_spaces(self.comment_column)?;
|
||||
write!(self.writer, "#")?;
|
||||
|
||||
// For the first column content - write spaces up to the column, and then if we are
|
||||
// in a multiline context, add the multi-line marker.
|
||||
{
|
||||
self.write_n_spaces(self.content_columns[0] - self.comment_column - 1)?;
|
||||
|
||||
if self.top_line_written {
|
||||
write!(self.writer, "{MUTLILINE_MARKER} ")?;
|
||||
}
|
||||
}
|
||||
|
||||
self.content_prefixes_written = 1;
|
||||
}
|
||||
|
||||
// For all remaining content columns, fill them in with spaces.
|
||||
let remaining_content_columns = self
|
||||
.content_columns
|
||||
.iter()
|
||||
.skip(self.content_prefixes_written);
|
||||
self.write_n_spaces(remaining_content_columns.sum())?;
|
||||
|
||||
self.content_prefixes_written = self.content_columns.len();
|
||||
self.top_line_written = true;
|
||||
}
|
||||
|
||||
write!(self.writer, "{content_line}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_and_bump(&mut self, content: &str) -> io::Result<()> {
|
||||
assert!(
|
||||
content.lines().count() == 1,
|
||||
"cannot bump with multi-line content"
|
||||
);
|
||||
|
||||
self.write(content)?;
|
||||
|
||||
let column = self
|
||||
.content_columns
|
||||
.last_mut()
|
||||
.expect("cannot write_and_bump before adding layer");
|
||||
*column += content.len();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_n_spaces(&mut self, n: usize) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
write!(self.writer, " ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
F a : a | a has Hash
|
||||
|
||||
main : F a -> F a
|
||||
#^^^^{-1} a -[[main(0)]]-> a | a has Hash
|
|
@ -0,0 +1,7 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
F a : a | a has Hash & Eq & Decoding
|
||||
|
||||
main : F a -> F a
|
||||
#^^^^{-1} a -[[main(0)]]-> a | a has Hash & Decoding & Eq
|
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
f : x -> x | x has Hash
|
||||
g : x -> x | x has Decoding & Encoding
|
||||
|
||||
main : x -> x | x has Hash & Decoding & Encoding
|
||||
main = \x -> x |> f |> g
|
||||
#^^^^{-1} x -[[main(0)]]-> x | x has Hash & Encoding & Decoding
|
|
@ -0,0 +1,12 @@
|
|||
app "test" provides [top] to "./platform"
|
||||
|
||||
MDict u := (List u) | u has Hash & Eq
|
||||
|
||||
bot : MDict k -> MDict k
|
||||
bot = \@MDict data ->
|
||||
when {} is
|
||||
{} -> @MDict data
|
||||
|
||||
top : MDict v -> MDict v
|
||||
top = \x -> bot x
|
||||
#^^^{-1} MDict v -[[top(0)]]-> MDict v | v has Hash & Eq
|
|
@ -0,0 +1,9 @@
|
|||
app "test" provides [isEqQ] to "./platform"
|
||||
|
||||
Q := [ F (Str -> Str), G ] has [Eq { isEq: isEqQ }]
|
||||
|
||||
isEqQ = \@Q q1, @Q q2 -> when T q1 q2 is
|
||||
#^^^^^{-1} Q, Q -[[isEqQ(0)]]-> Bool
|
||||
T (F _) (F _) -> Bool.true
|
||||
T G G -> Bool.true
|
||||
_ -> Bool.false
|
|
@ -0,0 +1,10 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
Q := ({} -> Str) has [Eq {isEq: isEqQ}]
|
||||
|
||||
isEqQ = \@Q f1, @Q f2 -> (f1 {} == f2 {})
|
||||
#^^^^^{-1} ({} -[[]]-> Str), ({} -[[]]-> Str) -[[isEqQ(2)]]-> [False, True]
|
||||
|
||||
main = isEqQ (@Q \{} -> "a") (@Q \{} -> "a")
|
||||
# ^^^^^ ({} -[[6, 7]]-> Str), ({} -[[6, 7]]-> Str) -[[isEqQ(2)]]-> [False, True]
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main : Decoder Bool _
|
||||
main = Decode.custom \bytes, fmt ->
|
||||
Decode.decodeWith bytes Decode.decoder fmt
|
||||
# ^^^^^^^^^^^^^^ Decoding#Decode.decoder(4): Decoder Bool fmt | fmt has DecoderFormatting
|
|
@ -0,0 +1,4 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main = Bool.isEq Bool.true Bool.false
|
||||
# ^^^^^^^^^ Eq#Bool.isEq(9): Bool, Bool -[[Bool.structuralEq(11)]]-> Bool
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
\h -> Hash.hash h Bool.true
|
||||
# ^^^^^^^^^ Hash#Hash.hash(1): a, Bool -[[Hash.hashBool(9)]]-> a | a has Hasher
|
|
@ -0,0 +1,4 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main = Encode.toEncoder Bool.true
|
||||
# ^^^^^^^^^^^^^^^^ Encoding#Encode.toEncoder(2): Bool -[[] + fmt:Encode.bool(17):1]-> Encoder fmt | fmt has EncoderFormatting
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
n : Num *
|
||||
|
||||
main = n == 1.
|
||||
# ^ Dec
|
|
@ -0,0 +1,9 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
N := U8 has [Decoding]
|
||||
|
||||
main : Decoder N _
|
||||
main = Decode.custom \bytes, fmt ->
|
||||
Decode.decodeWith bytes Decode.decoder fmt
|
||||
# ^^^^^^^^^^^^^^ N#Decode.decoder(3): List U8, fmt -[[7]]-> { rest : List U8, result : [Err [TooShort], Ok U8] } | fmt has DecoderFormatting
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
N := U8 has [Encoding]
|
||||
|
||||
main = Encode.toEncoder (@N 15)
|
||||
# ^^^^^^^^^^^^^^^^ N#Encode.toEncoder(3): N -[[#N_toEncoder(3)]]-> Encoder fmt | fmt has EncoderFormatting
|
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
Trivial := {} has [Eq {isEq}]
|
||||
|
||||
isEq = \@Trivial {}, @Trivial {} -> Bool.true
|
||||
|
||||
main = Bool.isEq (@Trivial {}) (@Trivial {})
|
||||
# ^^^^^^^^^ Trivial#Bool.isEq(2): Trivial, Trivial -[[isEq(2)]]-> Bool
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
N := U8 has [Eq]
|
||||
|
||||
main = Bool.isEq (@N 15) (@N 23)
|
||||
# ^^^^^^^^^ N#Bool.isEq(3): N, N -[[#N_isEq(3)]]-> Bool
|
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
Noop := {} has [Hash {hash}]
|
||||
|
||||
hash = \hasher, @Noop {} -> hasher
|
||||
|
||||
main = \hasher -> hash hasher (@Noop {})
|
||||
#^^^^{-1} hasher -[[main(0)]]-> hasher | hasher has Hasher
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
N := U8 has [Hash]
|
||||
|
||||
main = \hasher, @N n -> Hash.hash hasher (@N n)
|
||||
# ^^^^^^^^^ N#Hash.hash(3): a, N -[[#N_hash(3)]]-> a | a has Hasher
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
\h -> Hash.hash h 7
|
||||
# ^^^^^^^^^ Hash#Hash.hash(1): a, I64 -[[Hash.hashI64(13)]]-> a | a has Hasher
|
|
@ -0,0 +1,12 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
s1 : Set U8
|
||||
s1 = Set.empty {}
|
||||
|
||||
s2 : Set Str
|
||||
s2 = Set.empty {}
|
||||
|
||||
Bool.isEq s1 s1 && Bool.isEq s2 s2
|
||||
# ^^^^^^^^^ Set#Bool.isEq(17): Set Str, Set Str -[[Set.isEq(17)]]-> Bool
|
||||
# ^^^^^^^^^ Set#Bool.isEq(17): Set U8, Set U8 -[[Set.isEq(17)]]-> Bool
|
8
crates/compiler/uitest/tests/constrain_dbg_flex_var.txt
Normal file
8
crates/compiler/uitest/tests/constrain_dbg_flex_var.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
polyDbg = \x ->
|
||||
#^^^^^^^{-1} a -[[polyDbg(1)]]-> a
|
||||
dbg x
|
||||
x
|
||||
|
||||
main = polyDbg ""
|
|
@ -0,0 +1,8 @@
|
|||
# +opt infer:allow_errors
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
\x -> when x is
|
||||
#^ [A [B]w_a [C]w_b]
|
||||
A B _ -> ""
|
||||
A _ C -> ""
|
|
@ -0,0 +1,7 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
\x -> when x is
|
||||
#^ { a : [A { b : [B]w_a }*]w_b }*
|
||||
{ a: A { b: B } } -> ""
|
||||
_ -> ""
|
|
@ -0,0 +1,7 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
walkHelp : {} -> [Continue {}, Break []]
|
||||
|
||||
main = when walkHelp {} is
|
||||
# ^^^^^^^^^^^ [Break [], Continue {}]
|
||||
Continue {} -> {}
|
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
x : Result Str []
|
||||
x = Ok "abc"
|
||||
|
||||
main = when x is
|
||||
#^^^^{-1} Str
|
||||
Ok s -> s
|
|
@ -0,0 +1,9 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
x : Result Str []
|
||||
x = Ok "abc"
|
||||
|
||||
Ok str = x
|
||||
|
||||
main = str
|
||||
# ^^^ Str
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [x] to "./platform"
|
||||
|
||||
x : Result Str [] -> Str
|
||||
x = \Ok s -> s
|
||||
#^{-1} Result Str [] -[[x(0)]]-> Str
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [f] to "./platform"
|
||||
|
||||
f : _ -> {}
|
||||
f = \_ -> f {}
|
||||
#^{-1} {} -[[f(0)]]-> {}
|
6
crates/compiler/uitest/tests/infer_contextual_crash.txt
Normal file
6
crates/compiler/uitest/tests/infer_contextual_crash.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [getInfallible] to "./platform"
|
||||
|
||||
getInfallible = \result -> when result is
|
||||
#^^^^^^^^^^^^^{-1} [Ok a]w_b -[[getInfallible(0)]]-> a
|
||||
Ok x -> x
|
||||
_ -> crash "turns out this was fallible"
|
|
@ -0,0 +1,30 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
X has
|
||||
consume : a -> {} | a has X
|
||||
|
||||
O := {} has [X {consume: consumeO}]
|
||||
|
||||
consumeO = \@O {} -> {}
|
||||
|
||||
P := {} has [X {consume: consumeP}]
|
||||
|
||||
consumeP = \@P {} -> {}
|
||||
|
||||
caller = \x -> consume x
|
||||
# ^ a | a has X
|
||||
# ^^^^^^^ X#consume(2): a -[[] + a:consume(2):1]-> {} | a has X
|
||||
|
||||
main = {
|
||||
a: caller (@O {}),
|
||||
# ^^^^^^{inst} O -[[caller(7)]]-> {}
|
||||
# │ caller = \x -> consume x
|
||||
# │ ^ O
|
||||
# │ ^^^^^^^ X#consume(2): O -[[consumeO(5)]]-> {}
|
||||
|
||||
b: caller (@P {}),
|
||||
# ^^^^^^{inst} P -[[caller(7)]]-> {}
|
||||
# │ caller = \x -> consume x
|
||||
# │ ^ P
|
||||
# │ ^^^^^^^ X#consume(2): P -[[consumeP(6)]]-> {}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
# +opt print:can_decls
|
||||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
Parser a : {} -> a
|
||||
|
||||
v1 : {}
|
||||
v1 = {}
|
||||
|
||||
v2 : Str
|
||||
v2 = ""
|
||||
|
||||
apply : Parser (a -> Str), a -> Parser Str
|
||||
apply = \fnParser, valParser ->
|
||||
\{} ->
|
||||
(fnParser {}) (valParser)
|
||||
|
||||
map : a, (a -> Str) -> Parser Str
|
||||
map = \simpleParser, transform ->
|
||||
apply (\{} -> transform) simpleParser
|
||||
|
||||
parseInput = \{} ->
|
||||
when [ map v1 (\{} -> ""), map v2 (\s -> s) ] is
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ List (({} -[[9 (({} -[[12 (Str -[[14]]-> Str)]]-> (Str -[[14]]-> Str))) Str, 9 (({} -[[12 ({} -[[13]]-> Str)]]-> ({} -[[13]]-> Str))) {}]]-> Str))
|
||||
_ -> ""
|
||||
|
||||
main = parseInput {} == ""
|
||||
|
||||
# -emit:can_decls
|
||||
v1 = {}
|
||||
|
||||
v2 = ""
|
||||
|
||||
apply = \fnParser, valParser -> \{} -[9]-> (fnParser {}) valParser
|
||||
|
||||
map = \simpleParser, transform -> apply \{} -[12]-> transform simpleParser
|
||||
|
||||
parseInput = \{} ->
|
||||
when [
|
||||
map v1 \{} -[13]-> "",
|
||||
map v2 \s -[14]-> s,
|
||||
] is
|
||||
_ -> ""
|
||||
|
||||
main = Bool.isEq (parseInput {}) ""
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
g = if Bool.true then A else B
|
||||
|
||||
main = g ""
|
||||
# ^ Str -[[2, 3]]-> [A Str, B Str]w_a
|
|
@ -0,0 +1,16 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
x = Bool.true
|
||||
y = Bool.false
|
||||
|
||||
a = "foo"
|
||||
b = "bar"
|
||||
|
||||
foo = \{} -> if x then a else bar {}
|
||||
#^^^{-1} {} -[[foo(5) Bool Bool Str Str]]-> Str
|
||||
|
||||
bar = \{} -> if y then b else foo {}
|
||||
#^^^{-1} {} -[[bar(6) Bool Bool Str Str]]-> Str
|
||||
|
||||
bar {}
|
|
@ -0,0 +1,22 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
compose = \f, g ->
|
||||
closCompose = \x -> g (f x)
|
||||
closCompose
|
||||
|
||||
const = \x ->
|
||||
closConst = \_ -> x
|
||||
closConst
|
||||
|
||||
list = []
|
||||
|
||||
res : Str -> Str
|
||||
res = List.walk list (const "z") (\c1, c2 -> compose c1 c2)
|
||||
# ^^^^^^^ (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str), (Str -[[]]-> Str) -[[compose(1)]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str)
|
||||
# ^^^^^ Str -[[const(2)]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str)
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^ (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str), (Str -[[]]-> Str) -[[11]]-> (Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str)
|
||||
#^^^{-1} Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str
|
||||
|
||||
res "hello"
|
||||
#^^^{-1} Str -[[closCompose(7) (Str -a-> Str) (Str -[[]]-> Str), closConst(10) Str] as a]-> Str
|
|
@ -0,0 +1,14 @@
|
|||
app "test" provides [f] to "./platform"
|
||||
|
||||
thenDo = \x, callback ->
|
||||
callback x
|
||||
|
||||
f = \{} ->
|
||||
code = 10u16
|
||||
|
||||
bf = \{} ->
|
||||
#^^{-1} {} -[[bf(5) U16]]-> *
|
||||
thenDo code \_ -> bf {}
|
||||
# ^^^^^^^^^^^ U16 -[[6 U16]]-> *
|
||||
|
||||
bf {}
|
|
@ -0,0 +1,11 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
x = "abc"
|
||||
|
||||
getX = \{} -> x
|
||||
|
||||
h = \{} -> (getX {})
|
||||
#^{-1} {}* -[[h(3) Str]]-> Str
|
||||
|
||||
h {}
|
|
@ -0,0 +1,11 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
h = \{} -> (getX {})
|
||||
#^{-1} {}* -[[h(1) Str]]-> Str
|
||||
|
||||
getX = \{} -> x
|
||||
|
||||
x = "abc"
|
||||
|
||||
h {}
|
1
crates/compiler/uitest/tests/oiop/README.md
Normal file
1
crates/compiler/uitest/tests/oiop/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Tests for open-in-output-position semantics.
|
|
@ -0,0 +1,15 @@
|
|||
app "test" provides [accum] to "./platform"
|
||||
|
||||
Q : [Green, Blue]
|
||||
|
||||
f : Q -> Q
|
||||
f = \q -> when q is
|
||||
#^{-1} Q -[[f(2)]]-> Q
|
||||
Green -> Green
|
||||
Blue -> Blue
|
||||
|
||||
accum = \q -> when q is
|
||||
#^^^^^{-1} [A, B, C] -[[accum(0)]]-> [Blue, Green, Orange, Yellow]*
|
||||
A -> f Green
|
||||
B -> Yellow
|
||||
C -> Orange
|
14
crates/compiler/uitest/tests/pattern/as/does_not_narrow.txt
Normal file
14
crates/compiler/uitest/tests/pattern/as/does_not_narrow.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
input : [A Str, B Str]
|
||||
input = A "foo"
|
||||
|
||||
drop : a -> {}
|
||||
drop = \_ -> {}
|
||||
|
||||
main = when input is
|
||||
# ^^^^^ [A Str, B Str]
|
||||
A _ as a -> drop a
|
||||
# ^ [A Str, B Str]
|
||||
B _ as b -> drop b
|
||||
# ^ [A Str, B Str]
|
10
crates/compiler/uitest/tests/pattern/as/list.txt
Normal file
10
crates/compiler/uitest/tests/pattern/as/list.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
input : List Str
|
||||
input = [ "foo", "bar" ]
|
||||
|
||||
main = when input is
|
||||
# ^^^^^ List Str
|
||||
[ _first, .. as rest ] -> 1 + List.len rest
|
||||
# ^^^^ List Str
|
||||
[] -> 0
|
|
@ -0,0 +1,7 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main = when A "foo" is
|
||||
A _ as a -> a
|
||||
# ^ [A Str]w_a
|
||||
b -> b
|
||||
# ^ [A Str]w_a
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
Pair x _ = Pair 0 1
|
||||
|
||||
main = x
|
||||
# ^ Num w_a
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [x] to "./platform"
|
||||
|
||||
x : U16
|
||||
x = '.'
|
||||
#^{-1} U16
|
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [x] to "./platform"
|
||||
|
||||
x : U32
|
||||
x = '.'
|
||||
#^{-1} U32
|
5
crates/compiler/uitest/tests/ranged/check_char_as_u8.txt
Normal file
5
crates/compiler/uitest/tests/ranged/check_char_as_u8.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
app "test" provides [x] to "./platform"
|
||||
|
||||
x : U8
|
||||
x = '.'
|
||||
#^{-1} U8
|
|
@ -0,0 +1,10 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
f : U16 -> _
|
||||
f = \c ->
|
||||
when c is
|
||||
'.' -> 'A'
|
||||
c1 -> c1
|
||||
|
||||
main = f
|
||||
# ^ U16 -[[f(1)]]-> U16
|
|
@ -0,0 +1,10 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
f : U32 -> _
|
||||
f = \c ->
|
||||
when c is
|
||||
'.' -> 'A'
|
||||
c1 -> c1
|
||||
|
||||
main = f
|
||||
# ^ U32 -[[f(1)]]-> U32
|
|
@ -0,0 +1,10 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
f : U8 -> _
|
||||
f = \c ->
|
||||
when c is
|
||||
'.' -> 'A'
|
||||
c1 -> c1
|
||||
|
||||
main = f
|
||||
# ^ U8 -[[f(1)]]-> U8
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
f : { x ? Str, y ? Str } -> {}
|
||||
|
||||
main = f {x : ""}
|
||||
# ^ { x : Str, y ? Str } -[[f(1)]]-> {}
|
|
@ -0,0 +1,8 @@
|
|||
app "test" provides [b] to "./platform"
|
||||
|
||||
O := {} -> {}
|
||||
|
||||
a = @O \{} -> ((\@O f -> f {}) b)
|
||||
|
||||
b = a
|
||||
#^{-1} O
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [fx] to "./platform"
|
||||
|
||||
after : ({} -> a), ({} -> b) -> ({} -> b)
|
||||
|
||||
fx = after (\{} -> {}) \{} -> if Bool.true then fx {} else {}
|
||||
#^^{-1} {} -[[]]-> {}
|
|
@ -0,0 +1,9 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
after : ({} -> a), ({} -> b) -> ({} -> b)
|
||||
|
||||
fx = after (\{} -> {}) \{} -> if Bool.true then fx {} else {}
|
||||
|
||||
fx
|
||||
# ^^ {} -[[]]-> {}
|
|
@ -0,0 +1,13 @@
|
|||
app "test" provides [doIt] to "./platform"
|
||||
|
||||
Effect : [
|
||||
DoIt {} ({} -> Effect),
|
||||
]
|
||||
|
||||
Task := ({} -> Effect) -> Effect
|
||||
|
||||
doIt : {} -> Task
|
||||
doIt = \{} ->
|
||||
#^^^^{-1} {} -[[doIt(0)]]-> Task
|
||||
@Task \toNext ->
|
||||
DoIt {} \{} -> (toNext {})
|
|
@ -0,0 +1,11 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [job] to "./platform"
|
||||
|
||||
F : [Bar, FromG G]
|
||||
G : [G {lst : List F}]
|
||||
|
||||
job : { lst : List F } -> G
|
||||
job = \config -> G config
|
||||
#^^^{-1} { lst : List [Bar, FromG ([G { lst : List [Bar, FromG a] }] as a)] } -[[job(0)]]-> [G { lst : List [Bar, FromG a] }] as a
|
||||
# ^^^^^^^^ [G { lst : List [Bar, FromG a] }] as a
|
||||
# ^^^^^^ { lst : List [Bar, FromG ([G { lst : List [Bar, FromG a] }] as a)] }
|
|
@ -0,0 +1,6 @@
|
|||
app "test" provides [translateStatic] to "./platform"
|
||||
|
||||
translateStatic : _ -> _
|
||||
translateStatic = \Element c ->
|
||||
#^^^^^^^^^^^^^^^{-1} [Element (List a)] as a -[[translateStatic(0)]]-> [Element (List b)]* as b
|
||||
Element (List.map c translateStatic)
|
|
@ -0,0 +1,25 @@
|
|||
# +opt infer:print_only_under_alias
|
||||
app "test" provides [main] to "./platform"
|
||||
|
||||
Input := [
|
||||
FromJob Job
|
||||
]
|
||||
|
||||
Job := [
|
||||
Job (List Input)
|
||||
]
|
||||
|
||||
job : List Input -> Job
|
||||
job = \inputs ->
|
||||
@Job (Job inputs)
|
||||
|
||||
helloWorld : Job
|
||||
helloWorld =
|
||||
@Job ( Job [ @Input (FromJob greeting) ] )
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^ [FromJob ([Job (List [FromJob a])] as a)]
|
||||
|
||||
greeting : Job
|
||||
greeting =
|
||||
job []
|
||||
|
||||
main = (\_ -> "Which platform am I running on now?\n") helloWorld
|
448
crates/compiler/uitest/tests/uitest.rs
Normal file
448
crates/compiler/uitest/tests/uitest.rs
Normal file
|
@ -0,0 +1,448 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use libtest_mimic::{run, Arguments, Failed, Trial};
|
||||
use regex::Regex;
|
||||
use test_solve_helpers::{
|
||||
infer_queries, Elaboration, InferOptions, InferredProgram, InferredQuery, MUTLILINE_MARKER,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args = Arguments::from_args();
|
||||
|
||||
let test_files = collect_uitest_files()?;
|
||||
let tests = test_files
|
||||
.into_iter()
|
||||
.map(into_test)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
run(&args, tests).exit()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref UITEST_PATH: PathBuf = PathBuf::from(std::env!("ROC_WORKSPACE_DIR"))
|
||||
.join("crates")
|
||||
.join("compiler")
|
||||
.join("uitest")
|
||||
.join("tests");
|
||||
|
||||
/// # +opt infer:<opt>
|
||||
static ref RE_OPT_INFER: Regex =
|
||||
Regex::new(r#"# \+opt infer:(?P<opt>.*)"#).unwrap();
|
||||
|
||||
/// # +opt print:<opt>
|
||||
static ref RE_OPT_PRINT: Regex =
|
||||
Regex::new(r#"# \+opt print:(?P<opt>.*)"#).unwrap();
|
||||
}
|
||||
|
||||
fn collect_uitest_files() -> io::Result<Vec<PathBuf>> {
|
||||
let mut tests = Vec::with_capacity(200);
|
||||
|
||||
let mut dirs_to_visit = vec![UITEST_PATH.clone()];
|
||||
while let Some(dir) = dirs_to_visit.pop() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let entry_type = entry.file_type()?;
|
||||
if entry_type.is_dir() {
|
||||
dirs_to_visit.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.extension() == Some(OsStr::new("txt")) {
|
||||
tests.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tests)
|
||||
}
|
||||
|
||||
fn into_test(path: PathBuf) -> io::Result<Trial> {
|
||||
let name = path
|
||||
.strip_prefix(UITEST_PATH.as_path())
|
||||
.expect("collected path does not have uitest prefix")
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
let trial = Trial::test(name, move || run_test(path));
|
||||
Ok(trial)
|
||||
}
|
||||
|
||||
fn run_test(path: PathBuf) -> Result<(), Failed> {
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let TestCase {
|
||||
infer_options,
|
||||
print_options,
|
||||
source,
|
||||
} = TestCase::parse(data)?;
|
||||
|
||||
let inferred_program = infer_queries(&source, infer_options)?;
|
||||
|
||||
{
|
||||
let mut fd = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
|
||||
assemble_query_output(&mut fd, &source, inferred_program, print_options)?;
|
||||
}
|
||||
|
||||
check_for_changes(&path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const EMIT_HEADER: &str = "# -emit:";
|
||||
|
||||
struct TestCase {
|
||||
infer_options: InferOptions,
|
||||
print_options: PrintOptions,
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PrintOptions {
|
||||
can_decls: bool,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
fn parse(mut data: String) -> Result<Self, Failed> {
|
||||
// Drop anything following `# -emit:` header lines; that's the output.
|
||||
if let Some(drop_at) = data.find(EMIT_HEADER) {
|
||||
data.truncate(drop_at);
|
||||
data.truncate(data.trim_end().len());
|
||||
}
|
||||
|
||||
Ok(TestCase {
|
||||
infer_options: Self::parse_infer_options(&data)?,
|
||||
print_options: Self::parse_print_options(&data)?,
|
||||
source: data,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_infer_options(data: &str) -> Result<InferOptions, Failed> {
|
||||
let mut infer_opts = InferOptions {
|
||||
no_promote: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let found_infer_opts = RE_OPT_INFER.captures_iter(data);
|
||||
for infer_opt in found_infer_opts {
|
||||
let opt = infer_opt.name("opt").unwrap().as_str();
|
||||
match opt {
|
||||
"allow_errors" => infer_opts.allow_errors = true,
|
||||
"print_only_under_alias" => infer_opts.print_only_under_alias = true,
|
||||
other => return Err(format!("unknown infer option: {other}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(infer_opts)
|
||||
}
|
||||
|
||||
fn parse_print_options(data: &str) -> Result<PrintOptions, Failed> {
|
||||
let mut print_opts = PrintOptions::default();
|
||||
|
||||
let found_infer_opts = RE_OPT_PRINT.captures_iter(data);
|
||||
for infer_opt in found_infer_opts {
|
||||
let opt = infer_opt.name("opt").unwrap().as_str();
|
||||
match opt {
|
||||
"can_decls" => print_opts.can_decls = true,
|
||||
other => return Err(format!("unknown print option: {other}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(print_opts)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_changes(path: &Path) -> Result<(), Failed> {
|
||||
Command::new("git").args(["add", "-N"]).arg(path).output()?;
|
||||
|
||||
let has_changes = Command::new("git")
|
||||
.args(["diff", "--color=always"])
|
||||
.arg(path)
|
||||
.output()?;
|
||||
|
||||
if !has_changes.stdout.is_empty() {
|
||||
return Err(format!(
|
||||
"{}\nOutput has changed. If it looks okay, `git` add the file.",
|
||||
std::str::from_utf8(&has_changes.stdout)?
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assemble the output for a test, with queries elaborated in-line.
|
||||
fn assemble_query_output(
|
||||
writer: &mut impl io::Write,
|
||||
source: &str,
|
||||
inferred_program: InferredProgram,
|
||||
print_options: PrintOptions,
|
||||
) -> io::Result<()> {
|
||||
// Reverse the queries so that we can pop them off the end as we pass through the lines.
|
||||
let (queries, program) = inferred_program.decompose();
|
||||
let mut sorted_queries = queries.into_sorted();
|
||||
sorted_queries.reverse();
|
||||
|
||||
let mut reflow = Reflow::new_unindented(writer);
|
||||
write_source_with_answers(&mut reflow, source, sorted_queries, 0)?;
|
||||
|
||||
// Finish up with any remaining print options we were asked to provide.
|
||||
let PrintOptions { can_decls } = print_options;
|
||||
if can_decls {
|
||||
writeln!(writer, "\n{EMIT_HEADER}can_decls")?;
|
||||
program.write_can_decls(writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_source_with_answers<W: io::Write>(
|
||||
reflow: &mut Reflow<'_, W>,
|
||||
source: &str,
|
||||
mut sorted_queries: Vec<InferredQuery>,
|
||||
offset_line: usize,
|
||||
) -> io::Result<()> {
|
||||
for (i, line) in source.lines().enumerate() {
|
||||
let i = i + offset_line;
|
||||
|
||||
let mut is_query_line = false;
|
||||
|
||||
// Write all elaborated query lines if applicable.
|
||||
while matches!(
|
||||
sorted_queries.last(),
|
||||
Some(InferredQuery {
|
||||
source_line_column,
|
||||
..
|
||||
}) if source_line_column.line == i as _
|
||||
) {
|
||||
let inferred = sorted_queries.pop().unwrap();
|
||||
|
||||
reflow.scoped(|reflow| reconstruct_comment_line(reflow, inferred))?;
|
||||
|
||||
reflow.write("\n")?;
|
||||
|
||||
is_query_line = true;
|
||||
}
|
||||
|
||||
// If this was previously a multi-line query output line, skip it, since we already wrote
|
||||
// the new output above.
|
||||
if line.contains(MUTLILINE_MARKER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, write the Roc source line.
|
||||
if !is_query_line {
|
||||
reflow.write(line.trim_end())?;
|
||||
reflow.write("\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_queries = sorted_queries.into_iter().peekable();
|
||||
while let Some(sorted_query) = sorted_queries.next() {
|
||||
reflow.scoped(|reflow| reconstruct_comment_line(reflow, sorted_query))?;
|
||||
|
||||
// Only write a newline if we're not yet at the end of the source.
|
||||
// Otherwise, a newline will be written for us after exiting the reconstruction of the
|
||||
// comment line, since this must happen in the reconsutrction of a multi-line query.
|
||||
if sorted_queries.peek().is_some() {
|
||||
reflow.write("\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconstruct_comment_line<W: io::Write>(
|
||||
reflow: &mut Reflow<'_, W>,
|
||||
inferred: InferredQuery,
|
||||
) -> io::Result<()> {
|
||||
let InferredQuery {
|
||||
comment_column,
|
||||
source_line_column,
|
||||
source,
|
||||
elaboration,
|
||||
} = inferred;
|
||||
|
||||
reflow.add_layer(comment_column as _, source_line_column.column as _);
|
||||
reflow.write_and_bump(&format!("{source} "))?;
|
||||
|
||||
match elaboration {
|
||||
Elaboration::Specialization {
|
||||
specialized_name,
|
||||
typ,
|
||||
} => {
|
||||
reflow.write_and_bump(&format!("{specialized_name}: "))?;
|
||||
reflow.write(&typ)
|
||||
}
|
||||
Elaboration::Source { source: _, typ } => reflow.write(&typ),
|
||||
Elaboration::Instantiation {
|
||||
typ,
|
||||
source,
|
||||
offset_line,
|
||||
queries_in_instantiation,
|
||||
} => {
|
||||
reflow.write(&typ)?;
|
||||
|
||||
// Write the source on new line, but at the reflow column the comment is aligned at.
|
||||
reflow.set_content(source_line_column.column as _);
|
||||
reflow.write("\n")?;
|
||||
|
||||
let queries = queries_in_instantiation.into_sorted();
|
||||
|
||||
write_source_with_answers(reflow, source.trim_end(), queries, offset_line as _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Reflow<'a, W: io::Write> {
|
||||
writer: &'a mut W,
|
||||
state: ReflowState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ReflowState {
|
||||
/// true if the first line of the elaboration comment has been written.
|
||||
top_line_written: bool,
|
||||
/// Number of `content columns` prefixes written.
|
||||
/// If this equals the number of content columns, the whole prefix for a line has been written.
|
||||
content_prefixes_written: usize,
|
||||
/// The column at which to insert the comment prefix "#".
|
||||
comment_column: usize,
|
||||
/// The columns at which content occurs.
|
||||
/// If the stack is >1, then
|
||||
/// - at the first content column, the [MUTLILINE_MARKER] may be written as appropriate
|
||||
/// - for subsequent columns, spaces are inserted until the column is reached.
|
||||
content_columns: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> std::ops::Deref for Reflow<'a, W> {
|
||||
type Target = ReflowState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> std::ops::DerefMut for Reflow<'a, W> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: io::Write> Reflow<'a, W> {
|
||||
fn new_unindented(writer: &'a mut W) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
state: ReflowState {
|
||||
top_line_written: false,
|
||||
content_prefixes_written: 0,
|
||||
comment_column: 0,
|
||||
content_columns: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn scoped<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
|
||||
let state = self.state.clone();
|
||||
let result = f(self);
|
||||
self.state = state;
|
||||
result
|
||||
}
|
||||
|
||||
fn add_layer(&mut self, comment_column: usize, content_column: usize) {
|
||||
if self.comment_column == 0 {
|
||||
// If the comment column is not yet set, this is the top-level and we should update the
|
||||
// state; otherwise, we already have a comment column, only add to the content-ful
|
||||
// layer.
|
||||
self.comment_column = comment_column;
|
||||
}
|
||||
self.content_columns.push(content_column);
|
||||
}
|
||||
|
||||
fn set_content(&mut self, content_column: usize) {
|
||||
let latest_column = self
|
||||
.content_columns
|
||||
.last_mut()
|
||||
.expect("cannot set content before adding a layer");
|
||||
*latest_column = content_column;
|
||||
}
|
||||
|
||||
fn write(&mut self, content: &str) -> io::Result<()> {
|
||||
for (i, content_line) in content.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
// new line
|
||||
writeln!(self.writer)?;
|
||||
self.content_prefixes_written = 0;
|
||||
}
|
||||
|
||||
// If the content columns are empty, this is top-level and we
|
||||
// have no prefix to write.
|
||||
if self.content_prefixes_written != self.content_columns.len() {
|
||||
if self.content_prefixes_written == 0 {
|
||||
self.write_n_spaces(self.comment_column)?;
|
||||
write!(self.writer, "#")?;
|
||||
|
||||
// For the first column content - write spaces up to the column, and then if we are
|
||||
// in a multiline context, add the multi-line marker.
|
||||
{
|
||||
self.write_n_spaces(self.content_columns[0] - self.comment_column - 1)?;
|
||||
|
||||
if self.top_line_written {
|
||||
write!(self.writer, "{MUTLILINE_MARKER} ")?;
|
||||
}
|
||||
}
|
||||
|
||||
self.content_prefixes_written = 1;
|
||||
}
|
||||
|
||||
// For all remaining content columns, fill them in with spaces.
|
||||
let remaining_content_columns = self
|
||||
.content_columns
|
||||
.iter()
|
||||
.skip(self.content_prefixes_written);
|
||||
self.write_n_spaces(remaining_content_columns.sum())?;
|
||||
|
||||
self.content_prefixes_written = self.content_columns.len();
|
||||
self.top_line_written = true;
|
||||
}
|
||||
|
||||
write!(self.writer, "{content_line}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_and_bump(&mut self, content: &str) -> io::Result<()> {
|
||||
assert!(
|
||||
content.lines().count() == 1,
|
||||
"cannot bump with multi-line content"
|
||||
);
|
||||
|
||||
self.write(content)?;
|
||||
|
||||
let column = self
|
||||
.content_columns
|
||||
.last_mut()
|
||||
.expect("cannot write_and_bump before adding layer");
|
||||
*column += content.len();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_n_spaces(&mut self, n: usize) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
write!(self.writer, " ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main =
|
||||
#^^^^{-1} (a -[[]]-> b) -[[main(0)]]-> (a -[[y(2) (a -[[]]-> b)]]-> b)
|
||||
\x ->
|
||||
y = \z -> x z
|
||||
y
|
4
crates/compiler/uitest/tests/weaken/weakened_list.txt
Normal file
4
crates/compiler/uitest/tests/weaken/weakened_list.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main = []
|
||||
#^^^^{-1} List w_a
|
|
@ -0,0 +1,12 @@
|
|||
app "test" provides [main] to "./platform"
|
||||
|
||||
main = \{} -> when Red is
|
||||
#^^^^{-1} {}* -[[main(0)]]-> { y : [Green, Red]a, z : [Green, Red]a }
|
||||
x ->
|
||||
y : [Red]_
|
||||
y = x
|
||||
|
||||
z : [Red, Green]_
|
||||
z = x
|
||||
|
||||
{y, z}
|
Loading…
Add table
Add a link
Reference in a new issue