Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a round of canonicalization panics found in fuzzing #7504

Merged
merged 6 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 85 additions & 28 deletions crates/compiler/can/src/def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2453,38 +2453,33 @@ fn canonicalize_pending_value_def<'a>(
}
}
IngestedFile(loc_pattern, opt_loc_ann, path_literal) => {
let relative_path =
if let ast::StrLiteral::PlainLine(ingested_path) = path_literal.value {
ingested_path
} else {
todo!(
"Only plain strings are supported. Other cases should be made impossible here"
);
};

let mut file_path: PathBuf = env.module_path.into();
// Remove the header file name and push the new path.
file_path.pop();
file_path.push(relative_path);
let expr = if let Some(relative_path) = extract_str_literal(env, path_literal) {
let mut file_path: PathBuf = env.module_path.into();
// Remove the header file name and push the new path.
file_path.pop();
file_path.push(relative_path);

let mut bytes = vec![];
let mut bytes = vec![];

let expr = match fs::File::open(&file_path)
.and_then(|mut file| file.read_to_end(&mut bytes))
{
Ok(_) => Expr::IngestedFile(file_path.into(), Arc::new(bytes), var_store.fresh()),
Err(e) => {
env.problems.push(Problem::FileProblem {
filename: file_path.to_path_buf(),
error: e.kind(),
});
match fs::File::open(&file_path).and_then(|mut file| file.read_to_end(&mut bytes)) {
Ok(_) => {
Expr::IngestedFile(file_path.into(), Arc::new(bytes), var_store.fresh())
}
Err(e) => {
env.problems.push(Problem::FileProblem {
filename: file_path.to_path_buf(),
error: e.kind(),
});

Expr::RuntimeError(RuntimeError::ReadIngestedFileError {
filename: file_path.to_path_buf(),
error: e.kind(),
region: path_literal.region,
})
Expr::RuntimeError(RuntimeError::ReadIngestedFileError {
filename: file_path.to_path_buf(),
error: e.kind(),
region: path_literal.region,
})
}
}
} else {
Expr::RuntimeError(RuntimeError::IngestedFilePathError(path_literal.region))
};

let loc_expr = Loc::at(path_literal.region, expr);
Expand Down Expand Up @@ -2539,6 +2534,68 @@ fn canonicalize_pending_value_def<'a>(
output
}

fn extract_str_literal<'a>(
env: &mut Env<'a>,
literal: Loc<ast::StrLiteral<'a>>,
) -> Option<&'a str> {
let relative_path = match literal.value {
ast::StrLiteral::PlainLine(ingested_path) => ingested_path,
ast::StrLiteral::Line(text) => {
let mut result_text = bumpalo::collections::String::new_in(env.arena);
if !extract_str_segments(env, text, &mut result_text) {
return None;
}
result_text.into_bump_str()
}
ast::StrLiteral::Block(lines) => {
let mut result_text = bumpalo::collections::String::new_in(env.arena);
for text in lines {
if !extract_str_segments(env, text, &mut result_text) {
return None;
}
}
result_text.into_bump_str()
}
};
Some(relative_path)
}

fn extract_str_segments<'a>(
env: &mut Env<'a>,
segments: &[ast::StrSegment<'a>],
result_text: &mut bumpalo::collections::String<'a>,
) -> bool {
for segment in segments.iter() {
match segment {
ast::StrSegment::Plaintext(t) => {
result_text.push_str(t);
}
ast::StrSegment::Unicode(t) => {
let hex_code: &str = t.value;
if let Some(c) = u32::from_str_radix(hex_code, 16)
.ok()
.and_then(char::from_u32)
{
result_text.push(c);
} else {
env.problem(Problem::InvalidUnicodeCodePt(t.region));
return false;
}
}
ast::StrSegment::EscapedChar(c) => {
result_text.push(c.unescape());
}
ast::StrSegment::Interpolated(e) => {
// TODO: maybe in the future we do want to allow building up the path with local bindings?
// This would require an interpreter tho; so for now we just disallow it.
env.problem(Problem::InterpolatedStringNotAllowed(e.region));
return false;
}
}
}
true
}

// TODO trim down these arguments!
#[allow(clippy::too_many_arguments)]
#[allow(clippy::cognitive_complexity)]
Expand Down
17 changes: 14 additions & 3 deletions crates/compiler/can/src/desugar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,17 @@ fn desugar_value_def<'a>(
desugar_loc_pattern(env, scope, loc_pattern),
desugar_expr(env, scope, loc_expr),
),
ann @ Annotation(_, _) => *ann,
Annotation(ann_pattern, ann_type) => {
Annotation(*desugar_loc_pattern(env, scope, ann_pattern), *ann_type)
}
AnnotatedBody {
ann_pattern,
ann_type,
lines_between,
body_pattern,
body_expr,
} => AnnotatedBody {
ann_pattern,
ann_pattern: desugar_loc_pattern(env, scope, ann_pattern),
ann_type,
lines_between,
body_pattern: desugar_loc_pattern(env, scope, body_pattern),
Expand Down Expand Up @@ -1001,7 +1003,16 @@ pub fn desugar_expr<'a>(
},
loc_args,
) => {
let result_expr = if loc_args.len() == 1 {
let result_expr = if loc_args.is_empty() {
env.problem(Problem::UnderAppliedTry {
region: loc_expr.region,
});
// Replace with a dummy expression to avoid cascading errors
env.arena.alloc(Loc {
value: Record(Collection::empty()),
region: loc_expr.region,
})
} else if loc_args.len() == 1 {
desugar_expr(env, scope, loc_args.items[0])
} else {
let function = desugar_expr(env, scope, loc_args.items.first().unwrap());
Expand Down
118 changes: 95 additions & 23 deletions crates/compiler/can/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,18 @@ pub fn canonicalize_expr<'a>(
}),
Output::default(),
),
Err(CanonicalizeRecordProblem::InvalidIgnoredValue {
field_name,
field_region,
record_region,
}) => (
Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidIgnoredValue {
field_name,
field_region,
record_region,
}),
Output::default(),
),
}
} else {
// only (optionally qualified) variables can be updated, not arbitrary expressions
Expand Down Expand Up @@ -1256,34 +1268,26 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::AccessorFunction(field) => (
RecordAccessor(StructAccessorData {
name: scope.gen_unique_symbol(),
function_var: var_store.fresh(),
record_var: var_store.fresh(),
ext_var: var_store.fresh(),
closure_var: var_store.fresh(),
field_var: var_store.fresh(),
field: match field {
Accessor::RecordField(field) => IndexOrField::Field((*field).into()),
Accessor::TupleIndex(index) => IndexOrField::Index(index.parse().unwrap()),
},
}),
Output::default(),
),
ast::Expr::AccessorFunction(field) => {
canonicalize_accessor_function(env, var_store, scope, field, region)
}
ast::Expr::TupleAccess(tuple_expr, field) => {
let (loc_expr, output) = canonicalize_expr(env, var_store, scope, region, tuple_expr);

(
let res = if let Ok(index) = field.parse() {
TupleAccess {
tuple_var: var_store.fresh(),
ext_var: var_store.fresh(),
elem_var: var_store.fresh(),
loc_expr: Box::new(loc_expr),
index: field.parse().unwrap(),
},
output,
)
index,
}
} else {
let error = roc_problem::can::RuntimeError::InvalidTupleIndex(region);
env.problem(Problem::RuntimeError(error.clone()));
Expr::RuntimeError(error)
};
(res, output)
}
ast::Expr::TrySuffix { .. } => internal_error!(
"a Expr::TrySuffix expression was not completely removed in desugar_value_def_suffixed"
Expand Down Expand Up @@ -1644,6 +1648,37 @@ pub fn canonicalize_expr<'a>(
)
}

fn canonicalize_accessor_function(
env: &mut Env<'_>,
var_store: &mut VarStore,
scope: &mut Scope,
field: &Accessor<'_>,
region: Region,
) -> (Expr, Output) {
(
Expr::RecordAccessor(StructAccessorData {
name: scope.gen_unique_symbol(),
function_var: var_store.fresh(),
record_var: var_store.fresh(),
ext_var: var_store.fresh(),
closure_var: var_store.fresh(),
field_var: var_store.fresh(),
field: match field {
Accessor::RecordField(field) => IndexOrField::Field((*field).into()),
Accessor::TupleIndex(index) => match index.parse() {
Ok(index) => IndexOrField::Index(index),
Err(_) => {
let error = roc_problem::can::RuntimeError::InvalidTupleIndex(region);
env.problem(Problem::RuntimeError(error.clone()));
return (Expr::RuntimeError(error), Output::default());
}
},
},
}),
Output::default(),
)
}

pub fn canonicalize_record<'a>(
env: &mut Env<'a>,
var_store: &mut VarStore,
Expand Down Expand Up @@ -1676,6 +1711,18 @@ pub fn canonicalize_record<'a>(
}),
Output::default(),
),
Err(CanonicalizeRecordProblem::InvalidIgnoredValue {
field_name,
field_region,
record_region,
}) => (
Expr::RuntimeError(roc_problem::can::RuntimeError::InvalidIgnoredValue {
field_name,
field_region,
record_region,
}),
Output::default(),
),
}
}
}
Expand Down Expand Up @@ -2016,6 +2063,11 @@ enum CanonicalizeRecordProblem {
field_region: Region,
record_region: Region,
},
InvalidIgnoredValue {
field_name: Lowercase,
field_region: Region,
record_region: Region,
},
}
fn canonicalize_fields<'a>(
env: &mut Env<'a>,
Expand Down Expand Up @@ -2064,6 +2116,21 @@ fn canonicalize_fields<'a>(
record_region: region,
});
}
Err(CanonicalizeFieldProblem::InvalidIgnoredValue {
field_name,
field_region,
}) => {
env.problems.push(Problem::InvalidIgnoredValue {
field_name: field_name.clone(),
field_region,
record_region: region,
});
return Err(CanonicalizeRecordProblem::InvalidIgnoredValue {
field_name,
field_region,
record_region: region,
});
}
}
}

Expand All @@ -2075,6 +2142,10 @@ enum CanonicalizeFieldProblem {
field_name: Lowercase,
field_region: Region,
},
InvalidIgnoredValue {
field_name: Lowercase,
field_region: Region,
},
}
fn canonicalize_field<'a>(
env: &mut Env<'a>,
Expand Down Expand Up @@ -2105,9 +2176,10 @@ fn canonicalize_field<'a>(
}),

// An ignored value, e.g. `{ _name: 123 }`
IgnoredValue(_, _, _) => {
internal_error!("Somehow an IgnoredValue record field was not desugared!");
}
IgnoredValue(name, _, value) => Err(CanonicalizeFieldProblem::InvalidIgnoredValue {
field_name: Lowercase::from(name.value),
field_region: Region::span_across(&name.region, &value.region),
}),

// A label with no value, e.g. `{ name }` (this is sugar for { name: name })
LabelOnly(_) => {
Expand Down
Loading