Skip to content

Commit

Permalink
Initial implementation of the Claim Deducer. (#6129)
Browse files Browse the repository at this point in the history
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
alxmrs authored and arcs-c3po committed Oct 23, 2020
1 parent 0dc7a5b commit a894550
Show file tree
Hide file tree
Showing 5 changed files with 609 additions and 0 deletions.
1 change: 1 addition & 0 deletions java/arcs/core/analysis/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ arcs_kt_library(
deps = [
"//java/arcs/core/data",
"//java/arcs/core/data:schema_fields",
"//java/arcs/core/data/expression",
"//java/arcs/core/policy",
"//java/arcs/core/type",
"//java/arcs/core/util",
Expand Down
139 changes: 139 additions & 0 deletions java/arcs/core/analysis/DependencyNode.kt
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()
}
}
98 changes: 98 additions & 0 deletions java/arcs/core/analysis/ExpressionDependencyAnalyzer.kt
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()
)
1 change: 1 addition & 0 deletions javatests/arcs/core/analysis/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ arcs_kt_jvm_test_suite(
"//java/arcs/core/analysis",
"//java/arcs/core/data",
"//java/arcs/core/data:schema_fields",
"//java/arcs/core/data/expression",
"//java/arcs/core/data/proto",
"//java/arcs/core/data/proto:manifest_java_proto_lite",
"//java/arcs/core/data/proto:policy_java_proto_lite",
Expand Down
Loading

0 comments on commit a894550

Please sign in to comment.