Skip to content

Commit 4b9ab17

Browse files
kmr-srbhassem2002
authored andcommitted
Detect unhashable object types at the ASR level (lcompilers#2664)
* Detect unhashable types for `dict` key * Tests: Add error tests and update references * Create a function to check for hashable objects * Tests: Add error tests for `set` and update references * Tests: Update error tests and references * Check for unhashable types in type-annotations * Tests: Update tests and references * Fix indentation
1 parent 59e1dc1 commit 4b9ab17

40 files changed

Lines changed: 380 additions & 3 deletions

src/lpython/semantics/python_ast_to_asr.cpp

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,15 @@ class CommonVisitor : public AST::BaseVisitor<Struct> {
16191619
}
16201620
}
16211621

1622+
bool is_hashable(ASR::ttype_t* object_type) {
1623+
if (ASR::is_a<ASR::List_t>(*object_type)
1624+
|| ASR::is_a<ASR::Dict_t>(*object_type)
1625+
|| ASR::is_a<ASR::Set_t>(*object_type)) {
1626+
return false;
1627+
}
1628+
return true;
1629+
}
1630+
16221631
AST::expr_t* get_var_intent_and_annotation(AST::expr_t *annotation, ASR::intentType &intent) {
16231632
if (AST::is_a<AST::Subscript_t>(*annotation)) {
16241633
AST::Subscript_t *s = AST::down_cast<AST::Subscript_t>(annotation);
@@ -1730,6 +1739,17 @@ class CommonVisitor : public AST::BaseVisitor<Struct> {
17301739
if (AST::is_a<AST::Name_t>(*s->m_slice) || AST::is_a<AST::Subscript_t>(*s->m_slice)) {
17311740
ASR::ttype_t *type = ast_expr_to_asr_type(loc, *s->m_slice,
17321741
is_allocatable, is_const, raise_error, abi, is_argument);
1742+
if (!is_hashable(type)) {
1743+
diag.add(diag::Diagnostic(
1744+
"Unhashable type: '" + ASRUtils::type_to_str(type) + "'",
1745+
diag::Level::Error, diag::Stage::Semantic, {
1746+
diag::Label("Mutable type '" + ASRUtils::type_to_str(type)
1747+
+ "' cannot be stored in a set.",
1748+
{s->m_slice->base.loc})
1749+
})
1750+
);
1751+
throw SemanticAbort();
1752+
}
17331753
return ASRUtils::TYPE(ASR::make_Set_t(al, loc, type));
17341754
} else {
17351755
throw SemanticError("Only Name in Subscript supported for now in `set`"
@@ -1765,6 +1785,17 @@ class CommonVisitor : public AST::BaseVisitor<Struct> {
17651785
}
17661786
ASR::ttype_t *key_type = ast_expr_to_asr_type(loc, *t->m_elts[0],
17671787
is_allocatable, is_const, raise_error, abi, is_argument);
1788+
if (!is_hashable(key_type)) {
1789+
diag.add(diag::Diagnostic(
1790+
"Unhashable type: '" + ASRUtils::type_to_str(key_type) + "'",
1791+
diag::Level::Error, diag::Stage::Semantic, {
1792+
diag::Label("Mutable type '" + ASRUtils::type_to_str(key_type)
1793+
+ "' cannot become a key in dict. Hint: Use an immutable type for key.",
1794+
{t->m_elts[0]->base.loc})
1795+
})
1796+
);
1797+
throw SemanticAbort();
1798+
}
17681799
ASR::ttype_t *value_type = ast_expr_to_asr_type(loc, *t->m_elts[1],
17691800
is_allocatable, is_const, raise_error, abi, is_argument);
17701801
raise_error_when_dict_key_is_float_or_complex(key_type, loc);
@@ -3808,7 +3839,7 @@ class CommonVisitor : public AST::BaseVisitor<Struct> {
38083839
ai.m_step, type, nullptr);
38093840
return false;
38103841
} else if (ASR::is_a<ASR::Dict_t>(*type)) {
3811-
throw SemanticError("unhashable type in dict: 'slice'", loc);
3842+
throw SemanticError("Unhashable type in dict: 'slice'", loc);
38123843
}
38133844
} else if(AST::is_a<AST::Tuple_t>(*m_slice) &&
38143845
ASRUtils::is_array(type)) {
@@ -6134,6 +6165,17 @@ class BodyVisitor : public CommonVisitor<BodyVisitor> {
61346165
ASR::expr_t *key = ASRUtils::EXPR(tmp);
61356166
if (key_type == nullptr) {
61366167
key_type = ASRUtils::expr_type(key);
6168+
if (!is_hashable(key_type)) {
6169+
diag.add(diag::Diagnostic(
6170+
"Unhashable type: '" + ASRUtils::type_to_str(key_type) + "'",
6171+
diag::Level::Error, diag::Stage::Semantic, {
6172+
diag::Label("Mutable type '" + ASRUtils::type_to_str(key_type)
6173+
+ "' cannot become a key in dict. Hint: Use an immutable type for key.",
6174+
{key->base.loc})
6175+
})
6176+
);
6177+
throw SemanticAbort();
6178+
}
61376179
} else {
61386180
if (!ASRUtils::check_equal_type(ASRUtils::expr_type(key), key_type)) {
61396181
throw SemanticError("All dictionary keys must be of the same type",
@@ -6603,6 +6645,17 @@ class BodyVisitor : public CommonVisitor<BodyVisitor> {
66036645
ASR::expr_t *value = ASRUtils::EXPR(tmp);
66046646
if (type == nullptr) {
66056647
type = ASRUtils::expr_type(value);
6648+
if (!is_hashable(type)) {
6649+
diag.add(diag::Diagnostic(
6650+
"Unhashable type: '" + ASRUtils::type_to_str(type) + "'",
6651+
diag::Level::Error, diag::Stage::Semantic, {
6652+
diag::Label("Mutable type '" + ASRUtils::type_to_str(type)
6653+
+ "' cannot be stored in a set.",
6654+
{value->base.loc})
6655+
})
6656+
);
6657+
throw SemanticAbort();
6658+
}
66066659
} else {
66076660
if (!ASRUtils::check_equal_type(ASRUtils::expr_type(value), type)) {
66086661
throw SemanticError("All Set values must be of the same type for now",

tests/errors/test_dict_key1.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_dict_key1():
4+
my_dict: dict[list[i32], str] = {[1, 2]: "first", [3, 4]: "second"}
5+
6+
test_dict_key1()

tests/errors/test_dict_key2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_dict_key2():
4+
my_dict: dict[dict[i32, str], str] = {{1: "a", 2: "b"}: "first", {3: "c", 4: "d"}: "second"}
5+
6+
test_dict_key2()

tests/errors/test_dict_key3.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_dict_key3():
4+
my_dict: dict[set[str], str] = {{1, 2}: "first", {3, 4}: "second"}
5+
6+
test_dict_key3()

tests/errors/test_dict_key4.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def test_dict_key4():
2+
print({[1, 2]: "first", [3, 4]: "second"})
3+
4+
test_dict_key4()

tests/errors/test_dict_key5.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def test_dict_key5():
2+
print({{1: "a", 2: "b"}: "first", {3: "c", 4: "d"}: "second"})
3+
4+
test_dict_key5()

tests/errors/test_dict_key6.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def test_dict_key6():
2+
print({{1, 2}: "first", {3, 4}: "second"})
3+
4+
test_dict_key6()

tests/errors/test_set_object1.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_set_object1():
4+
my_set: set[list[i32]] = {[1, 2], [3, 4]}
5+
6+
test_set_object1()

tests/errors/test_set_object2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_set_object2():
4+
my_set: set[dict[i32, str]] = {{1: "a", 2: "b"}, {3: "c", 4: "d"}}
5+
6+
test_set_object2()

tests/errors/test_set_object3.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from lpython import i32
2+
3+
def test_set_object3():
4+
my_set: set[set[i32]] = {{1, 2}, {3, 4}}
5+
6+
test_set_object3()

0 commit comments

Comments
 (0)