Skip to content

Commit 3735e85

Browse files
giacomocavalierilpil
authored andcommitted
fix purity for dict.each
1 parent ac98b23 commit 3735e85

4 files changed

Lines changed: 166 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@
162162
unreachable.
163163
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
164164

165+
- Fixed a bug where the Gleam standard library's `dict.each` function would
166+
incorrectly be assumed to be pure.
167+
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
168+
165169
- The compiler now correctly tracks the minimum required version for constant
166170
record updates to be `>= 1.14.0`.
167171
([Giacomo Cavalieri](https://github.com/giacomocavalieri))

compiler-core/src/analyse.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pub mod name;
55
mod tests;
66

77
use crate::{
8-
GLEAM_CORE_PACKAGE_NAME,
8+
GLEAM_CORE_PACKAGE_NAME, STDLIB_PACKAGE_NAME,
99
ast::{
1010
self, Arg, BitArrayOption, CustomType, DefinitionLocation, Function, GroupedDefinitions,
1111
Import, ModuleConstant, Publicity, RecordConstructor, RecordConstructorArg, SrcSpan,
@@ -632,13 +632,61 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
632632
.last()
633633
.map_or(prereg_return_type.clone(), |last| last.type_());
634634

635+
// `dict.do_fold` is a bit special: since it belongs to the stdlib
636+
// it is considered pure by default.
637+
// However, since it ends up calling its function argument its
638+
// purity should actually be `Impure`. We need to special case it
639+
// and set the value ourselves.
640+
//
641+
// You might wonder why `do_fold` needs this but other similar
642+
// functions like `list.each` don't need this special handling.
643+
// The key difference is `list.each` calls the higher order function
644+
// in its gleam body:
645+
//
646+
// ```gleam
647+
// fn each(list, fun) {
648+
// case list {
649+
// [] -> Nil
650+
// [first, ..rest] -> {
651+
// fun(first)
652+
// // ^^^ Here we're calling `fun`. It's happening in Gleam so
653+
// // the compiler can see this and understand that `each`
654+
// // is impure.
655+
// // You might argue the purity actually depends on the
656+
// // purity of `fun` itself. That's true! But it's a
657+
// // separate known problem. For the time being we always
658+
// // assume a function argument is impure.
659+
// each(rest, fun)
660+
// }
661+
// }
662+
// }
663+
// ```
664+
//
665+
// But since `do_fold` is an external the compiler can't know what
666+
// is going on with its function argument and keeps thinking it must
667+
// be pure
668+
//
669+
// ```gleam
670+
// @external(erlang, "", "")
671+
// fn do_fold(dict: Dict(k, v), fun: fn(k, v) -> a) -> Nil
672+
// ```
673+
//
674+
let purity = if expr_typer.environment.current_package == STDLIB_PACKAGE_NAME
675+
&& expr_typer.environment.current_module == "gleam/dict"
676+
&& name == "do_fold"
677+
{
678+
Purity::Impure
679+
} else {
680+
expr_typer.purity
681+
};
682+
635683
let type_ = fn_(arguments_types, return_type);
636684
Ok((
637685
type_,
638686
body,
639687
expr_typer.implementations,
640688
expr_typer.minimum_required_version,
641-
expr_typer.purity,
689+
purity,
642690
))
643691
});
644692

compiler-core/src/type_/expression.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
324324
};
325325

326326
let purity = if is_trusted_pure_module(environment) {
327-
// The standard library uses a lot of FFI, but as we are the maintainers we know that
328-
// it can be trusted to pure pure.
327+
// The standard library uses a lot of FFI, but as we are the
328+
// maintainers we know that it can be trusted to be pure.
329329
Purity::TrustedPure
330330
} else if uses_externals {
331331
Purity::Impure
@@ -5335,11 +5335,10 @@ fn invalid_with_annotated_type(constant: TypedConstant, new_type: Arc<Type>) ->
53355335
}
53365336
}
53375337

5338-
/// Returns `true` if the current function is one that the Gleam core team
5338+
/// Returns `true` if the current module is one that the Gleam core team
53395339
/// maintains and we know it to be pure.
53405340
/// Used in purity tracking.
53415341
fn is_trusted_pure_module(environment: &Environment<'_>) -> bool {
5342-
// We only t
53435342
if environment.current_package != STDLIB_PACKAGE_NAME {
53445343
return false;
53455344
}

compiler-core/src/type_/tests/warnings.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3622,6 +3622,115 @@ pub fn main() {
36223622
);
36233623
}
36243624

3625+
#[test]
3626+
fn stdlib_list_each_is_not_marked_as_pure() {
3627+
assert_no_warnings!(
3628+
(
3629+
"gleam",
3630+
"gleam/list",
3631+
r#"
3632+
pub fn each(list, f) {
3633+
case list {
3634+
[] -> Nil
3635+
[first, ..rest] -> {
3636+
f(first)
3637+
each(rest, f)
3638+
}
3639+
}
3640+
}
3641+
"#
3642+
),
3643+
"
3644+
import gleam/list
3645+
pub fn main() {
3646+
list.each([1, 2, 3, 4], fn(x) { echo x })
3647+
Nil
3648+
}
3649+
"
3650+
);
3651+
}
3652+
3653+
#[test]
3654+
fn dict_each_function_is_not_marked_as_pure() {
3655+
assert_no_warnings!(
3656+
(
3657+
"gleam_stdlib",
3658+
"gleam/dict",
3659+
r#"
3660+
pub type Dict(key, value)
3661+
3662+
@external(erlang, "maps", "new")
3663+
@external(javascript, "../dict.mjs", "make")
3664+
pub fn new() -> Dict(k, v)
3665+
3666+
pub fn each(dict: Dict(k, v), fun: fn(k, v) -> a) -> Nil {
3667+
fold(dict, Nil, fn(nil, k, v) {
3668+
fun(k, v)
3669+
nil
3670+
})
3671+
}
3672+
3673+
@external(javascript, "../dict.mjs", "fold")
3674+
pub fn fold(
3675+
over dict: Dict(k, v),
3676+
from initial: acc,
3677+
with fun: fn(acc, k, v) -> acc,
3678+
) -> acc {
3679+
let fun = fn(key, value, acc) { fun(acc, key, value) }
3680+
do_fold(fun, initial, dict)
3681+
}
3682+
3683+
@external(erlang, "maps", "fold")
3684+
fn do_fold(fun: fn(k, v, acc) -> acc, initial: acc, dict: Dict(k, v)) -> acc
3685+
"#
3686+
),
3687+
"
3688+
import gleam/dict
3689+
pub fn main() {
3690+
dict.each(dict.new(), fn(_, _) { echo 1 })
3691+
Nil
3692+
}
3693+
"
3694+
);
3695+
}
3696+
3697+
#[test]
3698+
fn dict_fold_function_is_not_marked_as_pure() {
3699+
assert_no_warnings!(
3700+
(
3701+
"gleam_stdlib",
3702+
"gleam/dict",
3703+
r#"
3704+
pub type Dict(key, value)
3705+
3706+
@external(erlang, "maps", "new")
3707+
@external(javascript, "../dict.mjs", "make")
3708+
pub fn new() -> Dict(k, v)
3709+
3710+
@external(javascript, "../dict.mjs", "fold")
3711+
pub fn fold(
3712+
over dict: Dict(k, v),
3713+
from initial: acc,
3714+
with fun: fn(acc, k, v) -> acc,
3715+
) -> acc {
3716+
let fun = fn(key, value, acc) { fun(acc, key, value) }
3717+
do_fold(fun, initial, dict)
3718+
}
3719+
3720+
@external(erlang, "maps", "fold")
3721+
fn do_fold(fun: fn(k, v, acc) -> acc, initial: acc, dict: Dict(k, v)) -> acc
3722+
"#
3723+
),
3724+
"
3725+
import gleam/dict
3726+
pub fn main() {
3727+
dict.fold(dict.new(), Nil, fn(_, _, _) { Nil })
3728+
Nil
3729+
}
3730+
"
3731+
);
3732+
}
3733+
36253734
#[test]
36263735
fn calling_local_variable_not_marked_as_pure() {
36273736
assert_no_warnings!(

0 commit comments

Comments
 (0)