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

Start with python match statement #1801

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import org.neo4j.ogm.annotation.Relationship

/**
* Represents a Java or C++ switch statement of the `switch (selector) {...}` that can include case
* and default statements. Break statements break out of the switch and labeled breaks in JAva are
* and default statements. Break statements break out of the switch and labeled breaks in Java are
* handled properly.
*/
class SwitchStatement : Statement(), BranchingNode {
Expand All @@ -51,7 +51,7 @@ class SwitchStatement : Statement(), BranchingNode {

@Relationship(value = "SELECTOR_DECLARATION")
var selectorDeclarationEdge = astOptionalEdgeOf<Declaration>()
/** C++ allows to use a declaration instead of a expression as selector */
/** C++ allows to use a declaration instead of an expression as selector */
var selectorDeclaration by unwrapping(SwitchStatement::selectorDeclarationEdge)

@Relationship(value = "STATEMENT") var statementEdge = astOptionalEdgeOf<Statement>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,19 +237,24 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
* where the first element in [nodes] is the lhs of the root of the tree of binary operators.
* The last operands are further down the tree.
*/
private fun joinListWithBinOp(
internal fun joinListWithBinOp(
operatorCode: String,
nodes: List<Expression>,
rawNode: Python.AST.AST? = null
rawNode: Python.AST.AST? = null,
isImplicit: Boolean = true
maximiliankaul marked this conversation as resolved.
Show resolved Hide resolved
): BinaryOperator {
val lastTwo = newBinaryOperator(operatorCode, rawNode = rawNode)
lastTwo.rhs = nodes.last()
lastTwo.lhs = nodes[nodes.size - 2]
val lastTwo =
newBinaryOperator(operatorCode = operatorCode, rawNode = rawNode).apply {
rhs = nodes.last()
lhs = nodes[nodes.size - 2]
this.isImplicit = isImplicit
}
return nodes.subList(0, nodes.size - 2).foldRight(lastTwo) { newVal, start ->
val nextValue = newBinaryOperator(operatorCode)
nextValue.rhs = start
nextValue.lhs = newVal
nextValue
newBinaryOperator(operatorCode = operatorCode, rawNode = rawNode).apply {
rhs = start
lhs = newVal
this.isImplicit = isImplicit
}
}
}

Expand Down Expand Up @@ -297,18 +302,12 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
rawNode = node
)
} else {
// Start with the last two operands, then keep prepending the previous ones until the
// list is finished.
val lastTwo = newBinaryOperator(op, rawNode = node)
lastTwo.rhs = handle(node.values.last())
lastTwo.lhs = handle(node.values[node.values.size - 2])
return node.values.subList(0, node.values.size - 2).foldRight(lastTwo) { newVal, start
->
val nextValue = newBinaryOperator(op, rawNode = node)
nextValue.rhs = start
nextValue.lhs = handle(newVal)
nextValue
}
joinListWithBinOp(
operatorCode = op,
nodes = node.values.map(::handle),
rawNode = node,
isImplicit = true
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,12 @@ interface Python {
* ```
*/
class MatchSingleton(pyObject: PyObject) : BasePattern(pyObject) {
val value: Any by lazy { "value" of pyObject }
/**
* [value] is not optional. We have to make it nullable though because the value will be
* set to `null` if the case matches on `None`. This is known behavior of jep (similar
* to literals/constants).
*/
val value: Any? by lazy { "value" of pyObject }
KuechA marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,135 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
is Python.AST.Global -> handleGlobal(node)
is Python.AST.Nonlocal -> handleNonLocal(node)
is Python.AST.Raise -> handleRaise(node)
is Python.AST.Match,
is Python.AST.Match -> handleMatch(node)
is Python.AST.TryStar ->
newProblemExpression(
"The statement of class ${node.javaClass} is not supported yet",
problem = "The statement of class ${node.javaClass} is not supported yet",
rawNode = node
)
}
}

/**
* Translates a pattern which can be used by a `match_case`. There are various options available
* and all of them are translated to traditional comparisons and logical expressions which could
* also be seen in the condition of an if-statement.
*/
private fun handlePattern(node: Python.AST.BasePattern, subject: String): Expression {
return when (node) {
is Python.AST.MatchValue ->
newBinaryOperator(operatorCode = "==", rawNode = node).implicit().apply {
this.lhs = newReference(name = subject)
this.rhs = frontend.expressionHandler.handle(ctx = node.value)
}
is Python.AST.MatchSingleton ->
newBinaryOperator(operatorCode = "===", rawNode = node).implicit().apply {
this.lhs = newReference(name = subject)
this.rhs =
when (val value = node.value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like this should be an extra function (at least if we use this logic somewhere else, too). Have you seen easyConstant in the ExpressionHandler?.
We only expect True, False or None here according to the doc.

is Python.AST.BaseExpr -> frontend.expressionHandler.handle(ctx = value)
null -> newLiteral(value = null, rawNode = node)
else ->
newProblemExpression(
problem =
"Can't handle ${value::class} in value of Python.AST.MatchSingleton yet"
)
}
}
is Python.AST.MatchOr ->
frontend.expressionHandler.joinListWithBinOp(
operatorCode = "or",
nodes = node.patterns.map { handlePattern(node = it, subject = subject) },
rawNode = node,
isImplicit = false
)
is Python.AST.MatchSequence,
is Python.AST.MatchMapping,
is Python.AST.MatchClass,
is Python.AST.MatchStar,
is Python.AST.MatchAs ->
newProblemExpression(
problem = "Cannot handle of type ${node::class} yet",
rawNode = node
)
else ->
newProblemExpression(
problem = "Cannot handle of type ${node::class} yet",
rawNode = node
)
}
}

/**
* Translates a [`match_case`](https://docs.python.org/3/library/ast.html#ast.match_case) to a
* [Block] which holds the [CaseStatement] and then all other statements of the
* [Python.AST.match_case.body].
*
* The [CaseStatement] is generated by the [Python.AST.match_case.pattern] and, if available,
* [Python.AST.match_case.guard]. If there's a `guard` present, we model the
Copy link
Contributor

Choose a reason for hiding this comment

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

"If there's a guard present, we model the... " this is a bit confusing. There is also a CaseStmt if there is no guard, isn't there?

* [CaseStatement.caseExpression] as an `AND` BinaryOperator, where the `lhs` is the normal
* pattern and the `rhs` is the guard. This is in line with
* [PEP 634](https://peps.python.org/pep-0634/).
*/
private fun handleMatchCase(node: Python.AST.match_case, subject: String): List<Statement> {
val statements = mutableListOf<Statement>()
// First, we add the CaseStatement. A `MatchAs` without a `pattern` implies
// it's a default statement.
// We have to handle this here since we do not want to generate the CaseStatement in this
// case.
val pattern = node.pattern
statements +=
if (
pattern is Python.AST.MatchAs &&
pattern.pattern == null
) {
newDefaultStatement(rawNode = pattern)
} else {
newCaseStatement(rawNode = node).apply {
this.caseExpression =
node.guard?.let {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this style. I think a simple if / else if / else at the top level would be easier to read.

newBinaryOperator(operatorCode = "and")
.implicit(
code = frontend.codeOf(astNode = node),
location = frontend.locationOf(astNode = node)
)
.apply {
this.lhs = handlePattern(node = node.pattern, subject = subject)
this.rhs = frontend.expressionHandler.handle(ctx = it)
}
} ?: handlePattern(node = node.pattern, subject = subject)
}
}
// Now, we add the remaining body.
statements += node.body.map(::handle)
// Currently, the EOG pass requires a break statement to work as expected. For this reason,
// we insert an implicit break statement at the end of the block.
statements +=
newBreakStatement()
.implicit(
code = frontend.codeOf(astNode = node),
location = frontend.locationOf(astNode = node)
)
return statements
}

/**
* Translates a Python [`Match`](https://docs.python.org/3/library/ast.html#ast.Match) into a
* [SwitchStatement].
*/
private fun handleMatch(node: Python.AST.Match): SwitchStatement =
newSwitchStatement(rawNode = node).apply {
val subject = frontend.expressionHandler.handle(ctx = node.subject)
maximiliankaul marked this conversation as resolved.
Show resolved Hide resolved
this.selector = subject

this.statement =
node.cases.fold(initial = newBlock().implicit()) { block, case ->
block.statements +=
handleMatchCase(node = case, subject = subject.name.localName)
block
}
}

/**
* Translates a Python [`Raise`](https://docs.python.org/3/library/ast.html#ast.Raise) into a
* [ThrowExpression].
Expand Down
Loading
Loading