-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of the Claim Deducer. (#6129)
This PR introduces a simple ExpressionDependencyAnalyzer. The ExpressionDependencyAnalyzer visitor will return a map representing derivations from input to output handle connection specs. Later, these can be converted to `AccessPath`s given a particular particle. So far, this PR implements fields access, object creation (i.e. the `new` keyword), binary operators, literals, `from` and `select` statements. CC: @piotrswigon @cromwellian Closes #6129 COPYBARA_INTEGRATE_REVIEW=#6129 from alxrsngrtn:claim-deducer 35d15ae PiperOrigin-RevId: 338714593
- Loading branch information
Showing
5 changed files
with
609 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
* Copyright 2020 Google LLC. | ||
* | ||
* This code may only be used under the BSD style license found at | ||
* http://polymer.github.io/LICENSE.txt | ||
* | ||
* Code distributed by Google as part of this project is also subject to an additional IP rights | ||
* grant found at | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
package arcs.core.analysis | ||
|
||
import arcs.core.data.AccessPath | ||
import arcs.core.data.expression.Expression | ||
|
||
/** Field [Identifier]. */ | ||
private typealias Identifier = String | ||
|
||
/** Lists of [Identifier]s imply an [AccessPath]. */ | ||
private typealias Path = List<Identifier> | ||
|
||
/** | ||
* [DependencyNode]s make up a directed-acyclic-graph that describes how input handle connections | ||
* map to output connections in particle specs with Paxel [Expression]s. | ||
* | ||
* - [DependencyNode.Input] represents an input handle connection and access path. | ||
* - [DependencyNode.DerivedFrom] indicates that an input has been modified in the Paxel expression. | ||
* - [DependencyNode.AssociationNode] connects fields to other nodes in the graph. These are used to | ||
* form left-hand-side / right-hand-side relations between handle connections. | ||
* | ||
* Example: | ||
* ``` | ||
* particle HousePets | ||
* input: reads PetData { cats: Number, dogs: Number, household: Text } | ||
* output: writes PetStats { | ||
* ratio: inline Domestication {trained: Number, total: Number}, | ||
* family: Text, | ||
* limit: Number | ||
* } = new PetStats { | ||
* ratio: new Domestication { | ||
* trained: input.cats, | ||
* total: input.cats + input.dogs, | ||
* }, | ||
* family: input.household, | ||
* limit: 5 | ||
* } | ||
* ``` | ||
* | ||
* This can be translated to: | ||
* | ||
* ``` | ||
* DependencyNode.AssociationNode( | ||
* "ratio" to DependencyNode.AssociationNode( | ||
* "trained" to DependencyNode.Input("input", "cats"), | ||
* "total" to DependencyNode.DerivedFrom( | ||
* DependencyNode.Input("input", "cats"), | ||
* DependencyNode.Input("input", "dogs") | ||
* ) | ||
* ), | ||
* "family" to DependencyNode.Input("input", "household"), | ||
* "limit" to DependencyNode.LITERAL | ||
* ) | ||
* ``` | ||
* | ||
* We can represent this DAG graphically: | ||
* | ||
* (root) | ||
* __________|______ | ||
* / | \ | ||
* / / _(ratio)_ | ||
* / / | \ | ||
* (limit) (family) (trained) (total) | ||
* | | | / | | ||
* | | | / | | ||
* x (household) (cats) (dogs) | ||
* | ||
* This internal representation, in turn, can be translated into the following claims: | ||
* | ||
* ``` | ||
* claim output.ratio.trained derives from input.cats | ||
* claim output.ratio.total derives from input.cats and derives from input.dogs | ||
* claim output.family derives from input.household | ||
* ``` | ||
*/ | ||
sealed class DependencyNode { | ||
|
||
/** An unmodified input (from a handle connection) used in a Paxel [Expression]. */ | ||
data class Input(val path: Path = emptyList()) : DependencyNode() { | ||
constructor(vararg fields: Identifier) : this(listOf(*fields)) | ||
} | ||
|
||
/** Represents derivation from a group of [Input]s in an [Expression]. */ | ||
data class DerivedFrom(val inputs: Set<Input> = emptySet()) : DependencyNode() { | ||
|
||
constructor(vararg paths: Path) : this(paths.map { Input(it) }.toSet()) | ||
|
||
/** Produce a new [DerivedFrom] with a flattened set of [Input]s. */ | ||
constructor(vararg nodes: DependencyNode) : this(flatten(*nodes)) | ||
|
||
companion object { | ||
/** Flatten nested sets of [DependencyNode]s.*/ | ||
private fun flatten(vararg nodes: DependencyNode): Set<Input> { | ||
return nodes.flatMap { node -> | ||
when (node) { | ||
is Input -> setOf(node) | ||
is DerivedFrom -> node.inputs | ||
else -> throw IllegalArgumentException( | ||
"Nodes must be a 'Input' or 'DerivedFrom'." | ||
) | ||
} | ||
}.toSet() | ||
} | ||
} | ||
} | ||
|
||
/** Associates [Identifier]s with [DependencyNode]s. */ | ||
data class AssociationNode( | ||
val associations: Map<Identifier, DependencyNode> = emptyMap() | ||
) : DependencyNode() { | ||
|
||
/** Construct an [AssociationNode] from associations of [Identifier]s to [DependencyNode]s. */ | ||
constructor(vararg pairs: Pair<Identifier, DependencyNode>) : this(pairs.toMap()) | ||
|
||
/** Replace the associations of an [AssociationNode] with new mappings. */ | ||
fun add(vararg other: Pair<Identifier, DependencyNode>): DependencyNode = AssociationNode( | ||
associations + other | ||
) | ||
|
||
/** Returns the [DependencyNode] associated with the input [Identifier]. */ | ||
fun lookup(key: Identifier): DependencyNode = requireNotNull(associations[key]) { | ||
"Identifier '$key' is not found in AssociationNode." | ||
} | ||
} | ||
|
||
companion object { | ||
/** A [DependencyNode] case to represent literals. */ | ||
val LITERAL = DerivedFrom() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright 2020 Google LLC. | ||
* | ||
* This code may only be used under the BSD style license found at | ||
* http://polymer.github.io/LICENSE.txt | ||
* | ||
* Code distributed by Google as part of this project is also subject to an additional IP rights | ||
* grant found at | ||
* http://polymer.github.io/PATENTS.txt | ||
*/ | ||
package arcs.core.analysis | ||
|
||
import arcs.core.data.Claim | ||
import arcs.core.data.ParticleSpec | ||
import arcs.core.data.expression.Expression | ||
|
||
private typealias Scope = DependencyNode.AssociationNode | ||
|
||
/** | ||
* A visitor that parses Paxel [Expression]s to produce data flow dependencies. | ||
* | ||
* For each [Expression], this visitor produces a [DependencyNode], which can be translated into a | ||
* set of [Claim] relationships. | ||
* | ||
* [DependencyNode]s are DAG structures that help map handle connections in a [ParticleSpec] to the | ||
* target [Expression]. | ||
*/ | ||
class ExpressionDependencyAnalyzer : Expression.Visitor<DependencyNode, Scope> { | ||
override fun <E, T> visit(expr: Expression.UnaryExpression<E, T>, ctx: Scope): DependencyNode = | ||
DependencyNode.DerivedFrom(expr.expr.accept(this, ctx)) | ||
|
||
override fun <L, R, T> visit(expr: Expression.BinaryExpression<L, R, T>, ctx: Scope) = | ||
DependencyNode.DerivedFrom( | ||
expr.left.accept(this, ctx), | ||
expr.right.accept(this, ctx) | ||
) | ||
|
||
override fun <T> visit(expr: Expression.FieldExpression<T>, ctx: Scope): DependencyNode { | ||
return when (val qualifier = expr.qualifier?.accept(this, ctx)) { | ||
null -> ctx.associations[expr.field] ?: DependencyNode.Input(expr.field) | ||
is DependencyNode.Input -> DependencyNode.Input( | ||
qualifier.path + expr.field | ||
) | ||
is DependencyNode.AssociationNode -> qualifier.lookup(expr.field) | ||
is DependencyNode.DerivedFrom -> throw UnsupportedOperationException( | ||
"Field access is not defined on a '${expr.qualifier}'." | ||
) | ||
} | ||
} | ||
|
||
override fun <T> visit(expr: Expression.QueryParameterExpression<T>, ctx: Scope): DependencyNode { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun visit(expr: Expression.NumberLiteralExpression, ctx: Scope) = DependencyNode.LITERAL | ||
|
||
override fun visit(expr: Expression.TextLiteralExpression, ctx: Scope) = DependencyNode.LITERAL | ||
|
||
override fun visit(expr: Expression.BooleanLiteralExpression, ctx: Scope) = DependencyNode.LITERAL | ||
|
||
override fun visit(expr: Expression.NullLiteralExpression, ctx: Scope) = DependencyNode.LITERAL | ||
|
||
override fun visit(expr: Expression.FromExpression, ctx: Scope): DependencyNode { | ||
val scope = (expr.qualifier?.accept(this, ctx) ?: ctx) as Scope | ||
return scope.add(expr.iterationVar to expr.source.accept(this, scope)) | ||
} | ||
|
||
override fun <T> visit(expr: Expression.SelectExpression<T>, ctx: Scope): DependencyNode { | ||
val qualifier = expr.qualifier.accept(this, ctx) as Scope | ||
return expr.expr.accept(this, qualifier) | ||
} | ||
|
||
override fun visit(expr: Expression.LetExpression, ctx: Scope): DependencyNode { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun <T> visit(expr: Expression.FunctionExpression<T>, ctx: Scope): DependencyNode { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun visit(expr: Expression.NewExpression, ctx: Scope) = DependencyNode.AssociationNode( | ||
expr.fields.associateBy({ it.first }, { it.second.accept(this, ctx) }) | ||
) | ||
|
||
override fun <T> visit(expr: Expression.OrderByExpression<T>, ctx: Scope): DependencyNode { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun visit(expr: Expression.WhereExpression, ctx: Scope): DependencyNode { | ||
TODO("Not yet implemented") | ||
} | ||
} | ||
|
||
/** Analyze data flow relationships in a Paxel [Expression]. */ | ||
fun <T> Expression<T>.analyze() = this.accept( | ||
ExpressionDependencyAnalyzer(), | ||
DependencyNode.AssociationNode() | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.