Skip to content

Commit

Permalink
Fix multiple MatchErrors
Browse files Browse the repository at this point in the history
Resolves   #706.
  • Loading branch information
evhub committed Dec 30, 2022
1 parent d707bf5 commit bde2443
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 22 deletions.
6 changes: 4 additions & 2 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ Additionally, to import custom operators from other modules, Coconut supports th
from <module> import operator <op>
```

Note that custom operators will often need to be surrounded by whitespace (or parentheses when used as an operator function) to be parsed correctly.
Custom operators will often need to be surrounded by whitespace (or parentheses when used as an operator function) to be parsed correctly.

If a custom operator that is also a valid name is desired, you can use a backslash before the name to get back the name instead with Coconut's [keyword/variable disambiguation syntax](#handling-keywordvariable-name-overlap).

Expand Down Expand Up @@ -1967,7 +1967,7 @@ Coconut will perform automatic [tail call](https://en.wikipedia.org/wiki/Tail_ca
1. it must directly return (using either `return` or [assignment function notation](#assignment-functions)) a call to itself (tail recursion elimination, the most powerful optimization) or another function (tail call optimization),
2. it must not be a generator (uses `yield`) or an asynchronous function (uses `async`).

_Note: Tail call optimization (though not tail recursion elimination) will work even for 1) mutual recursion and 2) pattern-matching functions split across multiple definitions using [`addpattern`](#addpattern)._
Tail call optimization (though not tail recursion elimination) will work even for 1) mutual recursion and 2) pattern-matching functions split across multiple definitions using [`addpattern`](#addpattern).

If you are encountering a `RuntimeError` due to maximum recursion depth, it is highly recommended that you rewrite your function to meet either the criteria above for tail call optimization, or the corresponding criteria for [`recursive_iterator`](#recursive-iterator), either of which should prevent such errors.

Expand Down Expand Up @@ -2805,6 +2805,8 @@ A `MatchError` is raised when a [destructuring assignment](#destructuring-assign

Additionally, if you are using [view patterns](#match), you might need to raise your own `MatchError` (though you can also just use a destructuring assignment or pattern-matching function definition to do so). To raise your own `MatchError`, just `raise MatchError(pattern, value)` (both arguments are optional).

In some cases where there are multiple Coconut packages installed at the same time, there may be multiple `MatchError`s defined in different packages. Coconut can perform some magic under the hood to make sure that all these `MatchError`s will seamlessly interoperate, but only if all such packages are compiled in [`--package` mode rather than `--standalone` mode](#compilation-modes).

### Generic Built-In Functions

```{contents}
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ test-pypy3-verbose: clean
.PHONY: test-mypy
test-mypy: export COCONUT_USE_COLOR=TRUE
test-mypy: clean
python ./coconut/tests --strict --force --target sys --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition
python ./coconut/tests --strict --force --target sys --keep-lines --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition
python ./coconut/tests/dest/runner.py
python ./coconut/tests/dest/extras.py

# same as test-mypy but uses the universal target
.PHONY: test-mypy-univ
test-mypy-univ: export COCONUT_USE_COLOR=TRUE
test-mypy-univ: clean
python ./coconut/tests --strict --force --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition
python ./coconut/tests --strict --force --keep-lines --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition
python ./coconut/tests/dest/runner.py
python ./coconut/tests/dest/extras.py

Expand All @@ -159,7 +159,7 @@ test-verbose: clean
.PHONY: test-mypy-all
test-mypy-all: export COCONUT_USE_COLOR=TRUE
test-mypy-all: clean
python ./coconut/tests --strict --force --target sys --verbose --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition --check-untyped-defs
python ./coconut/tests --strict --force --target sys --verbose --keep-lines --mypy --follow-imports silent --ignore-missing-imports --allow-redefinition --check-untyped-defs
python ./coconut/tests/dest/runner.py
python ./coconut/tests/dest/extras.py

Expand Down
29 changes: 20 additions & 9 deletions coconut/compiler/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ def base_pycondition(target, ver, if_lt=None, if_ge=None, indent=None, newline=F
return out


def make_py_str(str_contents, target_startswith, after_py_str_defined=False):
"""Get code that effectively wraps the given code in py_str."""
return (
repr(str_contents) if target_startswith == "3"
else "b" + repr(str_contents) if target_startswith == "2"
else "py_str(" + repr(str_contents) + ")" if after_py_str_defined
else "str(" + repr(str_contents) + ")"
)


# -----------------------------------------------------------------------------------------------------------------------
# FORMAT DICTIONARY:
# -----------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -198,6 +208,8 @@ def process_header_args(which, target, use_hash, no_tco, strict, no_wrap):
typing_line="# type: ignore\n" if which == "__coconut__" else "",
VERSION_STR=VERSION_STR,
module_docstring='"""Built-in Coconut utilities."""\n\n' if which == "__coconut__" else "",
__coconut__=make_py_str("__coconut__", target_startswith),
_coconut_cached_module=make_py_str("_coconut_cached_module", target_startswith),
object="" if target_startswith == "3" else "(object)",
comma_object="" if target_startswith == "3" else ", object",
comma_slash=", /" if target_info >= (3, 8) else "",
Expand Down Expand Up @@ -655,15 +667,18 @@ def getheader(which, target, use_hash, no_tco, strict, no_wrap):
elif target_info >= (3, 5):
header += "from __future__ import generator_stop\n"

header += "import sys as _coconut_sys\n"

if which.startswith("package"):
levels_up = int(which[len("package:"):])
coconut_file_dir = "_coconut_os.path.dirname(_coconut_os.path.abspath(__file__))"
for _ in range(levels_up):
coconut_file_dir = "_coconut_os.path.dirname(" + coconut_file_dir + ")"
return header + '''import sys as _coconut_sys, os as _coconut_os
return header + '''import os as _coconut_os
_coconut_file_dir = {coconut_file_dir}
_coconut_cached_module = _coconut_sys.modules.get({__coconut__})
if _coconut_cached_module is not None and _coconut_os.path.dirname(_coconut_cached_module.__file__) != _coconut_file_dir: # type: ignore
_coconut_sys.modules[{_coconut_cached_module}] = _coconut_cached_module
del _coconut_sys.modules[{__coconut__}]
_coconut_sys.path.insert(0, _coconut_file_dir)
_coconut_module_name = _coconut_os.path.splitext(_coconut_os.path.basename(_coconut_file_dir))[0]
Expand All @@ -685,23 +700,19 @@ def getheader(which, target, use_hash, no_tco, strict, no_wrap):
_coconut_sys.path.pop(0)
'''.format(
coconut_file_dir=coconut_file_dir,
__coconut__=(
'"__coconut__"' if target_startswith == "3"
else 'b"__coconut__"' if target_startswith == "2"
else 'str("__coconut__")'
),
**format_dict
) + section("Compiled Coconut")

if which == "sys":
return header + '''import sys as _coconut_sys
from coconut.__coconut__ import *
return header + '''from coconut.__coconut__ import *
from coconut.__coconut__ import {underscore_imports}
'''.format(**format_dict)

# __coconut__, code, file

header += "import sys as _coconut_sys\n"
header += '''_coconut_cached_module = _coconut_sys.modules.get({_coconut_cached_module}, _coconut_sys.modules.get({__coconut__}))
_coconut_base_MatchError = Exception if _coconut_cached_module is None else getattr(_coconut_cached_module, "MatchError", Exception)
'''.format(**format_dict)

if target_info >= (3, 7):
header += PY37_HEADER
Expand Down
29 changes: 24 additions & 5 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ class _coconut_base_hashable{object}:
def __setstate__(self, setvars):{COMMENT.fixes_unpickling_with_slots}
for k, v in setvars.items():
_coconut.setattr(self, k, v)
class MatchError(_coconut_base_hashable, Exception):
"""Pattern-matching error. Has attributes .pattern, .value, and .message."""
__slots__ = ("pattern", "value", "_message")
class MatchError(_coconut_base_hashable, _coconut_base_MatchError):
"""Pattern-matching error. Has attributes .pattern, .value, and .message."""{COMMENT.no_slots_to_allow_setattr_below}
max_val_repr_len = 500
def __init__(self, pattern=None, value=None):
self.pattern = pattern
Expand All @@ -74,7 +73,14 @@ class MatchError(_coconut_base_hashable, Exception):
self.message
return Exception.__unicode__(self)
def __reduce__(self):
return (self.__class__, (self.pattern, self.value))
return (self.__class__, (self.pattern, self.value), {lbrace}"_message": self._message{rbrace})
if _coconut_base_MatchError is not Exception:
for _coconut_MatchError_k in dir(MatchError):
try:
setattr(_coconut_base_MatchError, _coconut_MatchError_k, getattr(MatchError, _coconut_MatchError_k))
except (AttributeError, TypeError):
pass
MatchError = _coconut_base_MatchError
class _coconut_tail_call{object}:
__slots__ = ("func", "args", "kwargs")
def __init__(self, _coconut_func, *args, **kwargs):
Expand Down Expand Up @@ -1515,7 +1521,20 @@ def safe_call(_coconut_f{comma_slash}, *args, **kwargs):
except _coconut.Exception as err:
return _coconut_Expected(error=err)
class Expected(_coconut.collections.namedtuple("Expected", ("result", "error")){comma_object}):
"""TODO"""
"""Coconut's Expected built-in is a Coconut data that represents a value
that may or may not be an error, similar to Haskell's Either.

Effectively equivalent to:
data Expected[T](result: T?, error: Exception?):
def __new__(cls, result: T?=None, error: Exception?=None) -> Expected[T]:
if result is not None and error is not None:
raise ValueError("Expected cannot have both a result and an error")
return makedata(cls, result, error)
def __bool__(self) -> bool:
return self.error is None
def __fmap__[U](self, func: T -> U) -> Expected[U]:
return self.__class__(func(self.result)) if self else self
"""
_coconut_is_data = True
__slots__ = ()
def __add__(self, other): return _coconut.NotImplemented
Expand Down
2 changes: 1 addition & 1 deletion coconut/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
VERSION = "2.1.1"
VERSION_NAME = "The Spanish Inquisition"
# False for release, int >= 1 for develop
DEVELOP = 36
DEVELOP = 37
ALPHA = False # for pre releases rather than post releases

# -----------------------------------------------------------------------------------------------------------------------
Expand Down
17 changes: 16 additions & 1 deletion coconut/tests/src/cocotest/agnostic/main.coco
Original file line number Diff line number Diff line change
Expand Up @@ -1420,11 +1420,24 @@ def mypy_test() -> bool:
assert reveal_locals() is None
return True

def package_test(outer_MatchError) -> bool:
from __coconut__ import MatchError as coconut_MatchError
assert MatchError is coconut_MatchError, (MatchError, coconut_MatchError)
assert MatchError() `isinstance` outer_MatchError, (MatchError, outer_MatchError)
assert outer_MatchError() `isinstance` MatchError, (outer_MatchError, MatchError)
assert_raises((raise)$(outer_MatchError), MatchError)
assert_raises((raise)$(MatchError), outer_MatchError)
def raises_outer_MatchError(obj=None):
raise outer_MatchError("raises_outer_MatchError")
match raises_outer_MatchError -> None in 10:
assert False
return True

def tco_func() = tco_func()

def print_dot() = print(".", end="", flush=True)

def run_main(test_easter_eggs=False) -> bool:
def run_main(outer_MatchError, test_easter_eggs=False) -> bool:
"""Asserts arguments and executes tests."""
using_tco = "_coconut_tco" in globals() or "_coconut_tco" in locals()

Expand Down Expand Up @@ -1462,6 +1475,8 @@ def run_main(test_easter_eggs=False) -> bool:
if using_tco:
assert hasattr(tco_func, "_coconut_tco_func")
assert tco_test() is True
if outer_MatchError.__module__ != "__main__":
assert package_test(outer_MatchError) is True

print_dot() # ......
if sys.version_info < (3,):
Expand Down
2 changes: 2 additions & 0 deletions coconut/tests/src/cocotest/agnostic/suite.coco
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,8 @@ forward 2""") == 900
assert cycle_slide("123", slide=1)$[:9] |> "".join == "123231312"
assert "1234" |> windowsof_$(2) |> map$("".join) |> list == ["12", "23", "34"] == "1234" |> windowsof$(2) |> map$("".join) |> list
assert try_divide(1, 2) |> fmap$(.+1) == Expected(1.5)
assert sum_evens(0, 5) == 6 == sum_evens(1, 6)
assert sum_evens(7, 3) == 0 == sum_evens(4, 4)

# must come at end
assert fibs_calls[0] == 1
Expand Down
6 changes: 6 additions & 0 deletions coconut/tests/src/cocotest/agnostic/util.coco
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,12 @@ dict_zip = (
..> collectby$(.[0], value_func=.[1])
)

sum_evens = (
range
..> filter$((.%2) ..> (.==0))
..> sum
)


# n-ary reduction
def binary_reduce(binop, it) = (
Expand Down
5 changes: 4 additions & 1 deletion coconut/tests/src/runner.coco
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ from cocotest.main import run_main
def main() -> bool:
print(".", end="", flush=True) # .
assert cocotest.__doc__
assert run_main(test_easter_eggs="--test-easter-eggs" in sys.argv) is True
assert run_main(
outer_MatchError=MatchError,
test_easter_eggs="--test-easter-eggs" in sys.argv,
) is True
return True


Expand Down

0 comments on commit bde2443

Please sign in to comment.