A component is a modular part of a program that may encapsulate component elements, including relation declarations, type declarations, rules, facts, directives, and other components. A component has component declarations and component instantiations.
A component declaration starts with the keyword .comp
followed by the name of the
component and a block between curly brackets (i.e. { ... }
) containing the
component elements.
For example,
.comp MyComponent {
.type myType = number
.decl TheAnswer(x:myType) // component relation
TheAnswer(42). // component fact
}
declares a new component with the name MyComponent
. The component elements are a type myType
,
a relation TheAnswer
, and a fact.
We instantiate a component using the .init
keyword followed by a unique name of the instance,
the =
symbol, and the name of the component declaration.
In this example,
.init myInstance1 = MyComponent
.decl Test(x:number)
Test(x) :- myInstance1.TheAnswer(x).
.output Test
we instantiate the component MyComponent
with the name myInstance1
.
Internally, Soufflé flattens the component instantiation
creating for each component instantiation its own namespace.
The qualified names of component elements
are prepended using the name of the instantiation.
For the above example, Soufflé internally expands the component instantiation as follows:
.type myInstance1.myType = number
.decl myInstance1.TheAnswer(x:myType) // relation of myInstance1
myInstance1.TheAnswer(42). // fact of myInstance1
.decl Test(x:number)
Test(x) :- myInstance1.TheAnswer(x).
.output Test
You can display the expansion using the specifying the command-line
option --show=transformed-datalog
.
In the following example, we have two component instantiation of
MyComponent
:
.init myInstance1 = MyComponent
.init myInstance2 = MyComponent
myInstance2.TheAnswer(33).
.decl Test(x:number)
Test(x) :- myInstance1.TheAnswer(x).
Test(x) :- myInstance2.TheAnswer(x).
.output Test
producing internally the following logic program,
.type myInstance1.myType = number
.decl myInstance1.TheAnswer(x:myType) // relation of myInstance1
myInstance1.TheAnswer(42). // fact of myInstance1
.type myInstance2.myType = number
.decl myInstance2.TheAnswer(x:myType) // relation of myInstance1
myInstance2.TheAnswer(42). // fact of myInstance1
myInstance2.TheAnswer(33).
.decl Test(x:number)
Test(x) :- myInstance1.TheAnswer(x).
Test(x) :- myInstance2.TheAnswer(x).
.output Test
Note that the two relations TheAnswer
of both component instantiations are
disambiguated by the prefix myInstance1
and myInstance2
to avoid a name clash.
Suppose rules/facts are defined in a component for which there are no relation declarations. In that case, no prefix is prepended, and the resolution is deferred to the actual component instantiation.
For example, in the following example we have facts and rules defined whose relations reside outside of the component in which they are defined.
.decl Out(x:number)
.comp A {
.decl R(x:number)
.comp Count {
R(1). // fact accessing R outside of Count
R(x+1):- R(x), x<10. // rule accessing R outside of Count
}
.init myCount = Count // instantiate Count
Out(x) :- R(x). // rule accessing Out outside of A
}
.init myA = A
.output Out
Soufflé expands the code above as follows:
.decl Out(x:number)
.decl myA.R(x:number)
myA.R(1).
myA.R((x+1)) :- myA.R(x), x < 10.
Out(x) :-
myA.R(x).
.output Out
Prefices are prepended to relations in rules and facts if they are instantiated in the local scope.
Type Parameterisation
Components can be parametrized by unqualified type names. In the example,
.comp ParamComponent<myType> {
.decl TheAnswer(x:myType) // component relation
TheAnswer(42). // component fact
.output TheAnswer // component output directive
}
.init numberInstance = ParamComponent<number>
.init floatInstance = ParamComponent<float>
two different instances are generated whose relation’s attribute
are either of type number
or float
. Internally,
Soufflé produces the program showing the different attribute types
of the instantiated relations TheAnswer
.
.decl numberInstance.TheAnswer(x:number) // relation of numberInstance
numberInstance.TheAnswer(42). // fact of numberInstance
.output numberInstance.TheAnswer // output directive of numberInstance
.decl floatInstance.TheAnswer(x:float) // relation of floatInstance
floatInstance.TheAnswer(42). // fact of floatInstance
.output floatInstance.TheAnswer // output directive of floatInstance
Parameters can also be used to parameter a component name in a component instantiation. This mechanism is useful to model different behaviours in component since the component instantiation is controlled by the user of the component. For example,
.decl R(x:number)
.comp Case<Selector> {
.comp One {
R(1).
}
.comp Two {
R(2).
}
.init selection = Selector // instantiation depending on type parameter "Selector"
}
.init myCase = Case<One>
.output R
produces internally the following program
.decl R(x:number)
R(1).
.output R
since One
was passed on in the component instantiation of myCase
.
By changing the instantiation from .init myCase = Case<One>
to .init myCase = Case<Two>
,
the fact R(2).
would be issued for relation R
.
Inheritance
One component can inherit from multiple super components using inheritance. The component elements of the super-components are passed on to the sub-component. Using inheritance is helpful for larger programs and libraries and avoids code duplication.
In the following example,
.comp Base1 {
.type myNumber = number
.decl TheAnswer(x:myNumber)
TheAnswer(42).
}
.comp Base2 {
TheAnswer(41).
}
.comp Sub : Base1, Base2 { // inherit from Base1 and Base2
.decl WhatIsTheAnswer(n:myNumber)
WhatIsTheAnswer(n) :- TheAnswer(n).
.output WhatIsTheAnswer
}
.init mySub = Sub
the components Base1
and Base2
pass on all component elements to the component Sub
.
Soufflé produces internally the following code for the component instantiation mySub
:
.type mySub.myNumber = number
.decl mySub.TheAnswer(x:mySub.myNumber)
.decl mySub.WhatIsTheAnswer(n:mySub.myNumber)
mySub.TheAnswer(42).
mySub.TheAnswer(41).
mySub.WhatIsTheAnswer(n) :- mySub.TheAnswer(n).
.output mySub.WhatIsTheAnswer
Overridable Relations
If a relation is declared as overridable
in the super-component,
a subcomponent may override its associated rules and facts
from its super-component. The facts and rules of the super-component
are discarded of an overridable
relation
with the directive override
in the subcomponent.
For this example,
.comp Base {
.decl R(x:number) overridable
R(1).
R(x+1) :- R(x), x < 5.
.output R
}
.comp Sub : Base {
.override R
R(2).
R(x+1) :- R(x), x < 4.
}
.init mySub = Sub
the sub-components discards fact R(1).
and rule R(x+1) :- R(x), x < 5.
since the relation R
has been declared with the relation qualifier overrideable
and the sub-component has the .override R
directive, which let Soufflé drop the
facts and rules of its super component. Soufflé will produce internally the following
code for the sub-component instantiation mySub
:
.decl mySub.R(x:number)overridable
mySub.R(2).
mySub.R((x+1)) :-
mySub.R(x),
x < 4.
.output mySub.R
In the following, we show a use case for override in a more practical setup. For example, a more precise version of an existing analysis can be implemented by using the override semantics as follows,
.comp AbstractPointsto{
.decl HeapAllocationMerge(heap,mergeHeap) overridable
HeapAllocationMerge(heap,"<<string-constant>>") :-
StringConstant(heap).
// ...
}
.comp PrecisePointsto : AbstractPointsto{
.override HeapAllocationMerge
HeapAllocationMerge(heap,"<<string-constant>>") :-
StringConstant(heap),
!ClassNameStringConstant(heap),
!SimpleNameStringConstant(heap).
}
.init precise_pointsto = PrecisePointsto
In this example, PrecisePointsto inherits all the relations from AbstractPointsto, but only implements the HeapAllocationMerge relation differently. This feature avoids code duplications when we need several implementations of a generic analysis with minor variations and want to overwrite behaviour of the super component.
Type Parameterisation and Inheritance
Type parameters of super-components can be explicitly specified in sub-component declarations. For example,
.comp A<T> { .... }
.comp B<K> : A<K> { ... }
defines a sub-component B
with parameter K
that is used to instantiate
the super component with parameter K
for inheriting for B
.
The type parameter can also be used as the base class
.comp A<T> : T { ... }
building a selective inheritance based on the type parameter T
.
The instantiation of A<T>
defines which super component A
is inheriting.
With inheritance, complex component instantiatons are implementable. In the example below
.init reach = Reachability<Graph<number>> // syntax error
we would like to us a complex component parameter Graph<number>
,
but leads to syntax parameter because the parameter is not a
simple identifier. By introducing a new component, the above
instantiation can be rewritten as followes
.comp NumberGraph : Graph<number> { } // NumberGraph inherits
.init reach = Reachability<NumberGraph>
Syntax
In the following, we define the component model more formally using syntax diagrams and EBNF. The syntax diagrams were produced with Bottlecaps.
Component Declaration
A component declaration may consist of type declarations, relation declarations, rules, facts, directives, override directives, component declarations and initialisations.
component_decl ::=
'.comp' component_type ( ( ':' | ',' ) component_type )*
'{'
( type_decl | relation_decl | rule | fact | directive | '.override' IDENT | component_init | component_decl )*
'}'
Component Initialisation
A component initialisation has a name and the component declaration name with its parameters.
component_init ::= '.init' IDENT '=' component_type
Component Type
A component type is a signature of a component either used for initialisation, super-component references, or declaration of a component.
component_type ::= IDENT ( '<' IDENT ( ',' IDENT )* '>' )?