Skip to content

Commit

Permalink
Start with python match statement (#1801)
Browse files Browse the repository at this point in the history
* Start with python match statement

* fix bug, add test

* More testing

* Add implicit break

* Review feedback

* nullable MatchSingleton: comment and handling
  • Loading branch information
KuechA authored Nov 21, 2024
1 parent 37987af commit 2b0474a
Show file tree
Hide file tree
Showing 6 changed files with 498 additions and 26 deletions.
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
): 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 }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,134 @@ 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) {
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]. A `guard` is modeled with an `AND` BinaryOperator in the
* [CaseStatement.caseExpression]. Its `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
val guard = node.guard
statements +=
if (pattern is Python.AST.MatchAs && pattern.pattern == null) {
newDefaultStatement(rawNode = pattern)
} else if (guard != null) {
newCaseStatement(rawNode = node).apply {
this.caseExpression =
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 = guard)
}
}
} else {
newCaseStatement(rawNode = node).apply {
this.caseExpression = 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)
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

0 comments on commit 2b0474a

Please sign in to comment.