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

MatchOr and guard statements #158

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
77da54b
wrote a basic test that calls visit_Match
juliawgraham Dec 4, 2022
c213d0f
added extra space to publish branch
juliawgraham Dec 4, 2022
73425a6
implementation for MatchValue
Yuqi07 Dec 4, 2022
adb9a50
partial implementation for MatchAs, only implemented for the wildcard…
Yuqi07 Dec 4, 2022
f64490e
changed test names for matchas tests
Yuqi07 Dec 4, 2022
4aa1195
1. merged visit match_case to visit Match; 2. added wildcards error h…
Yuqi07 Dec 7, 2022
607c359
1. removed unused i; 2. added error handling for multiple statements …
Yuqi07 Dec 7, 2022
33f7439
created a function visit_MatchOr, still needs to be implemented
erica-w-fu Dec 7, 2022
a57b6a8
updated matchcase to accept inequalities
juliawgraham Dec 9, 2022
e1d90f9
added support for and/or in guard
juliawgraham Dec 9, 2022
64a281c
matchor implemented with tests
Lucybean-hi Dec 9, 2022
4ab48e8
Merge branch 'integration' into matchand
erica-w-fu Dec 9, 2022
2dbcf03
added requirements for python version 3.10 for all test cases with match
erica-w-fu Dec 9, 2022
62a4633
updated formatting using flake/black
juliawgraham Dec 9, 2022
fc0372a
Merge branch 'integration' into matchand
erica-w-fu Dec 9, 2022
ad11197
Merge pull request #2 from erica-w-fu/matchand
erica-w-fu Dec 9, 2022
5b6e676
updated syntax according to flake/black (after merging match_case gua…
juliawgraham Dec 9, 2022
0a55289
Merge pull request #1 from google/main
juliawgraham Dec 11, 2022
f300ee7
Merge branch 'integration' into updatedMain
juliawgraham Dec 11, 2022
c047696
Moved matchor / match guard test cases into function_codegen_match_test
juliawgraham Dec 11, 2022
3b78dd7
Merge pull request #3 from erica-w-fu/updatedMain
juliawgraham Dec 11, 2022
fc4eca9
updated everything so it's consistent with main
Yuqi07 Dec 11, 2022
1a1cefe
resolved all comments on our latest PR
Yuqi07 Dec 11, 2022
befd6dc
fixed all CI errors, now all tests passed
Yuqi07 Dec 11, 2022
efd60f8
Update src/latexify/codegen/function_codegen.py
Lucybean-hi Dec 11, 2022
7b67262
Update src/latexify/codegen/function_codegen.py
Lucybean-hi Dec 11, 2022
33327a3
Update src/latexify/codegen/function_codegen.py
Lucybean-hi Dec 11, 2022
587c50b
Update src/latexify/codegen/function_codegen.py
Lucybean-hi Dec 11, 2022
ee82180
addressed all comments
Yuqi07 Dec 11, 2022
4db2356
fixed flake errors
Yuqi07 Dec 11, 2022
ccb0c5d
Added spaces to conditionals in tests
juliawgraham Dec 11, 2022
2d5807e
Added more spaces to conditionals in tests
juliawgraham Dec 11, 2022
fd2ddee
fixed style errors and removed guard
Yuqi07 Dec 15, 2022
0daeb72
used _match_subject_stack as suggested
Yuqi07 Dec 15, 2022
3cf0b0e
Merge branch 'main' into integration
Yuqi07 Dec 15, 2022
2019d4b
fixed unit tests, adding cdot back
Yuqi07 Dec 15, 2022
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
125 changes: 91 additions & 34 deletions src/latexify/codegen/function_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,42 +141,99 @@ def visit_If(self, node: ast.If) -> str:

def visit_Match(self, node: ast.Match) -> str:
"""Visit a Match node"""
if not (
len(node.cases) >= 2
and isinstance(node.cases[-1].pattern, ast.MatchAs)
and node.cases[-1].pattern.name is None
):
raise exceptions.LatexifySyntaxError(
"Match statement must contain the wildcard."
)

subject_latex = self._expression_codegen.visit(node.subject)
case_latexes: list[str] = []

for i, case in enumerate(node.cases):
if len(case.body) != 1 or not isinstance(case.body[0], ast.Return):
raise exceptions.LatexifyNotSupportedError(
"Match cases must contain exactly 1 return statement."
)

if i < len(node.cases) - 1:
body_latex = self.visit(case.body[0])
cond_latex = self.visit(case.pattern)
case_latexes.append(
body_latex + r", & \mathrm{if} \ " + subject_latex + cond_latex
latex = r"\left\{ \begin{array}{ll} "
subject_latex = self.visit(node.subject)
for i, match_case in enumerate(node.cases):
if len(match_case.body) != 1:
raise exceptions.LatexifySyntaxError(
"Multiple statements are not supported in Match nodes."
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
)
true_latex = self.visit(match_case.body[0])
cond_latex = self.visit(match_case.pattern)

if i < len(node.cases) - 1: # no wildcard cases
if match_case.guard:
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
cond_latex = self.visit(match_case.guard)
subject_latex = "" # getting variable from cond_latex
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
if not cond_latex:
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
raise exceptions.LatexifySyntaxError(
"Match subtrees must contain only one wildcard at the end."
)
latex += true_latex + r", & \mathrm{if} \ " + cond_latex + r" \\ "
else:
case_latexes.append(
self.visit(node.cases[-1].body[0]) + r", & \mathrm{otherwise}"
)

return (
r"\left\{ \begin{array}{ll} "
+ r" \\ ".join(case_latexes)
+ r" \end{array} \right."
)
if cond_latex:
raise exceptions.LatexifySyntaxError(
"Match subtrees must contain only one wildcard at the end."
)
latex += true_latex + r", & \mathrm{otherwise}"
latex += r"\end{array} \right."
latex_final = latex.replace("subject_name", subject_latex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird hack, might cause issues if there's an actual subject_name in the $\LaTeX$ code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is something we considered as well and would love to hear suggestions.

Since each of the visit_Match... functions cannot access the subject_latex variable(each of those functions can only have the ast node and pattern as the input), we had to find a workaround. We tried to use python format and % to insert the subject_latex but the use of % and {} in latex made this very difficult. Do you have any suggestions for how to better implement this (or possible a better name than "subject_name" to replace)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I see the dilemma, bit perplexing lol. Not entirely sure how to solve but I'll give it some thought. Thanks for tackling match statements btw! Really cool feature :)

Copy link
Contributor

@ZibingZhang ZibingZhang Dec 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what @odashi thinks of this solution, but you could create a stack of _match_subject_stack: list[str] = [], and at the very beginning of visit_Match do something like

subject_latex = self._expression_codegen.visit(node.subject)
self._match_subject_stack.append(subject_latex)

and then right before you return do something like

latex = (
    r"\left\{ \begin{array}{ll} "
    + r" \\ ".join(case_latexes)
    + r" \end{array} \right."
)
        
self._match_subject_stack.pop()
return latex

and you would also have

    def visit_MatchValue(self, node: ast.MatchValue) -> str:
        """Visit a MatchValue node."""
        latex = self._expression_codegen.visit(node.value)
        return self._match_subject_stack[-1] + " = " + latex

A stack is only required if nested match statements is supported, which it seems like it is, although I have no idea if it's valid $\LaTeX$ output? In any case this situation I think necessitates a test for nested match statements (assuming they're supported), in order to test that in visit_MatchValue, the LHS takes the appropriate _subject_latex, i.e. the inner one should take the inner subject while the outer one takes the outer.

edit: _subject_latexes -> _match_subject_stack

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the @ZibingZhang 's solution is desirable at this point.

One minor point is that the private member _subject_latexes sounds weird for every other visitors, more specific name (like _match_subject_stack) would be better.

return latex_final

def visit_MatchValue(self, node: ast.MatchValue) -> str:
"""Visit a MatchValue node"""
latex = self._expression_codegen.visit(node.value)
return " = " + latex
latex = self.visit(node.value)

erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
return "subject_name = " + latex

def visit_MatchAs(self, node: ast.MatchAs) -> str:
"""Visit a MatchAs node"""
"""If MatchAs is a wildcard, return 'otherwise' case, else throw error"""
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
if not (node.pattern):
Lucybean-hi marked this conversation as resolved.
Show resolved Hide resolved
return ""
else:
raise exceptions.LatexifySyntaxError(
"Nonempty as-patterns are not supported in MatchAs nodes."
)

def visit_MatchOr(self, node: ast.MatchOr) -> str:
"""Visit a MatchOr node"""
latex = ""
for i, pattern in enumerate(node.patterns):
if i != 0:
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
latex += r" \lor " + self.visit(pattern)
else:
latex += self.visit(pattern)
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
return latex

def _reduce_stop_parameter(self, node: ast.BinOp) -> str:
erica-w-fu marked this conversation as resolved.
Show resolved Hide resolved
# ast.Constant class is added in Python 3.8
# ast.Num is the relevant node type in previous versions
if sys.version_info.minor < 8:
if isinstance(node.right, ast.Num):
if isinstance(node.op, ast.Add):
if node.right.n == 1:
upper = "{" + self.visit(node.left) + "}"
else:
reduced_constant = ast.Num(node.right.n - 1)
new_node = ast.BinOp(node.left, node.op, reduced_constant)
upper = "{" + self.visit(new_node) + "}"
else:
if node.right.n == -1:
upper = "{" + self.visit(node.left) + "}"
else:
reduced_constant = ast.Num(node.right.n + 1)
new_node = ast.BinOp(node.left, node.op, reduced_constant)
upper = "{" + self.visit(new_node) + "}"
else:
upper = "{" + self.visit(node) + "}"
else:
if isinstance(node.right, ast.Constant):
if isinstance(node.op, ast.Add):
if node.right.value == 1:
upper = "{" + self.visit(node.left) + "}"
else:
reduced_constant = ast.Constant(node.right.value - 1)
new_node = ast.BinOp(node.left, node.op, reduced_constant)
upper = "{" + self.visit(new_node) + "}"
else:
if node.right.value == -1:
upper = "{" + self.visit(node.left) + "}"
else:
reduced_constant = ast.Constant(node.right.value + 1)
new_node = ast.BinOp(node.left, node.op, reduced_constant)
upper = "{" + self.visit(new_node) + "}"
else:
upper = "{" + self.visit(node) + "}"

return upper
109 changes: 109 additions & 0 deletions src/latexify/codegen/function_codegen_match_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,112 @@ def test_matchvalue_mutliple_statements() -> None:
match=r"^Match cases must contain exactly 1 return statement\.$",
):
function_codegen.FunctionCodegen().visit(tree)


@test_utils.require_at_least(10)
def test_matchcase_with_guard() -> None:
tree = ast.parse(
textwrap.dedent(
"""
match x:
case x if x>0:
Lucybean-hi marked this conversation as resolved.
Show resolved Hide resolved
return 1
case _:
return 2
"""
)
).body[0]

assert (
FunctionCodegen().visit(tree)
== r"\left\{ \begin{array}{ll} "
+ r"{1}, & \mathrm{if} \ {x > {0}} \\ "
+ r"{2}, & \mathrm{otherwise}\end{array} \right."
)


@test_utils.require_at_least(10)
def test_matchcase_with_and_guard() -> None:
tree = ast.parse(
textwrap.dedent(
"""
match x:
case x if x>0 and x<=10:
juliawgraham marked this conversation as resolved.
Show resolved Hide resolved
return 1
case _:
return 2
"""
)
).body[0]

assert (
FunctionCodegen().visit(tree)
== r"\left\{ \begin{array}{ll} {1}, & \mathrm{if} "
+ r"\ {{x > {0}} \land {x \le {10}}} \\ "
+ r"{2}, & \mathrm{otherwise}\end{array} \right."
)


@test_utils.require_at_least(10)
def test_matchcase_with_or_guard() -> None:
tree = ast.parse(
textwrap.dedent(
"""
match x:
case x if x>0 or x<=10:
juliawgraham marked this conversation as resolved.
Show resolved Hide resolved
return 1
case _:
return 2
"""
)
).body[0]

assert (
FunctionCodegen().visit(tree)
== r"\left\{ \begin{array}{ll} {1}, "
+ r"& \mathrm{if} \ {{x > {0}} \lor {x \le {10}}} \\ "
+ r"{2}, & \mathrm{otherwise}\end{array} \right."
)


@test_utils.require_at_least(10)
def test_matchcase_with_multiple_guards() -> None:
tree = ast.parse(
textwrap.dedent(
"""
match x:
case x if 0 < x <= 10:
return 1
case _:
return 2
"""
)
).body[0]

assert (
FunctionCodegen().visit(tree)
== r"\left\{ \begin{array}{ll} {1}, "
+ r"& \mathrm{if} \ {{0} < x \le {10}} \\ {2}, "
+ r"& \mathrm{otherwise}\end{array} \right."
)


@test_utils.require_at_least(10)
def test_matchor() -> None:
tree = ast.parse(
textwrap.dedent(
"""
match x:
case 0 | 1:
return 1
case _:
return 2
"""
)
).body[0]

assert (
FunctionCodegen().visit(tree)
== r"\left\{ \begin{array}{ll} {1}, & \mathrm{if} \ x = {0} \lor x = {1} \\"
+ r" {2}, & \mathrm{otherwise}\end{array} \right."
)