classification
Title: No ValueError for duplicate key value in mapping patern when lengths do not match
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.11, Python 3.10
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: brandtbucher Nosy List: brandtbucher, jack__d, xtreak
Priority: normal Keywords:

Created on 2021-07-16 20:32 by jack__d, last changed 2021-07-29 06:00 by brandtbucher. This issue is now closed.

Messages (5)
msg397662 - (view) Author: Jack DeVries (jack__d) * Date: 2021-07-16 20:31
Consider the following code:

class A:
    a = 'a'

# runs without error
match {'a': 1}:
    case {'a': 1, A.a: 1}:
        pass

# raises ValueError
match {'a': 1, 'b': 1}:
    case {'a': 1, A.a: 1}:
        pass

In both cases, the mapping pattern is the same (and both are not valid due to duplicate key values). However, the pattern is only evaluated in the second case. This is because a key-length optimization provides a shortcut around pattern evaluation. The docs gives users a hint that things like this might happen, which is a good thing:

> Users should generally never rely on a pattern being evaluated. Depending on > implementation, the interpreter may cache values or use other optimizations > which skip repeated evaluations. 

> https://docs.python.org/3.10/reference/compound_stmts.html#overview

However, I can't help but think that these ergonomics are strange. Consider if some other code is mutating the value of `A.a`. This could create some very strange and flaky bugs where the state of `A.a` can change and make the pattern invalid, but nonetheless not cause an exception until much later, or not at all.

There is mapping pattern validation code in the `match_keys` function in ceval.c. I haven't looked, but I assume there is some other runtime validation for other match case types. I propose factoring Exception-raising validation into a separate procedure that is called before any optimization jumps occur.

This trades speed for consistent behavior, and I'm interested to hear what others think!
msg397664 - (view) Author: Jack DeVries (jack__d) * Date: 2021-07-16 20:38
Another option if this problem is isolated to mapping patterns, we could introduce a new op-code: BUILD_MATCH_MAP, which is essentially a wrapper around BUILD_MAP that disallows duplicate key values.
msg397683 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2021-07-17 03:16
See also https://bugs.python.org/issue44589
msg398409 - (view) Author: Brandt Bucher (brandtbucher) * (Python committer) Date: 2021-07-28 17:46
Evaluating every key in every mapping pattern and checking them for duplicates every time a match block is entered would be prohibitively expensive, for negligible gain. The length check exists to avoid this step, replacing it with a much cheaper O(1) trapdoor.

Imagine the overhead of validating this match statement, for example:

https://github.com/brandtbucher/patmaperformance/blob/2fe8b79691d4f11b18d65b957ed36a85cb4761a4/bm_patma_holdem.py#L54-L95

> Consider if some other code is mutating the value of `A.a`. This could create some very strange and flaky bugs where the state of `A.a` can change and make the pattern invalid, but nonetheless not cause an exception until much later, or not at all.

That's okay with me; it's not the VM's job to check the runtime validity of branches that aren't taken.

> Another option if this problem is isolated to mapping patterns, we could introduce a new op-code: BUILD_MATCH_MAP, which is essentially a wrapper around BUILD_MAP that disallows duplicate key values.

I'm not sure what you mean by this. We don't call BUILD_MAP when evaluating mapping patterns... in fact, we don't build any dictionaries at all (except for collecting remaining items using **rest).

Unless others disagree, I'm probably going to close this as "not a bug".
msg398444 - (view) Author: Jack DeVries (jack__d) * Date: 2021-07-28 23:34
@brandtbucher yeah, you can close it, this was a silly idea.
History
Date User Action Args
2021-07-29 06:00:32brandtbuchersetstatus: open -> closed
resolution: not a bug
stage: resolved
2021-07-28 23:34:46jack__dsetmessages: + msg398444
2021-07-28 17:46:45brandtbuchersetassignee: brandtbucher
messages: + msg398409
2021-07-17 03:16:41xtreaksetnosy: + brandtbucher, xtreak
messages: + msg397683
2021-07-16 20:38:07jack__dsetmessages: + msg397664
2021-07-16 20:32:00jack__dcreate