Update home, add code generation help authored by René Schöne's avatar René Schöne
## Terms/Concepts
# Terms/Concepts
- **Definition type**: they define when to read from and write to mqtt, when to update tokens, and when to call or invalidate attributes. there will be a small DSL to define those
- **Connection**: on instance/AST-level, an mqtt-topic can be associated to a source or target of a definition
- **Transformation**: a method or attribute modeling a math. function y = f(x)
- **Operator**: the atomar building blocks of a transformation, e.g., projection of an object, or a boolean operator
# Ros2Rag Really Generating
Sources:
- UpdateDefinition `read Joint.CurrentPosition using ParseLinkState, LinkStateToIntPosition ;`
- DependencyDefinition `RobotArm._AppropriateSpeed canDependOn Joint.CurrentPosition as dependency1 ;`
- MappingDefinition
```
ParseLinkState maps byte[] bytes to panda.Linkstate.PandaLinkState {:
return panda.Linkstate.PandaLinkState.parseFrom(bytes);
:}
```
## UpdateDefinition
```java=
abstract UpdateDefinition ::= <AlwaysApply:Boolean> ;
rel UpdateDefinition.Mapping* -> MappingDefinition;
abstract TokenUpdateDefinition : UpdateDefinition;
rel TokenUpdateDefinition.Token -> TokenComponent;
ReadFromMqttDefinition : TokenUpdateDefinition;
WriteToMqttDefinition : TokenUpdateDefinition;
```
## ReadFromMqttDefinition
Example:
- `read Joint.CurrentPosition using ParseLinkState, LinkStateToIntPosition ;`
Idea:
- create connectMethod to setup new mqtt connection
Input:
- AlwaysApply:Boolean
- Mapping*:List of MappingDefinition
- Token:TokenComponent
Calculated:
- parentType:TypeDecl == Token.containingTypeDecl
Code:
```java=
public void ${parentType}.connect${Token.getName()}(String topic) {
_mqttUpdater().newConnection(topic, message -> {
// TODO: iterate over mapping, calling method, store result in fresh variable of matching type, maybe surround with try/catch?
// $if not AlwaysApply:
// $if Token.isPrimitiveType:
if (get${Token.getName()}() == ${lastResult}) { return; }
// $else (use equals)
if (get${Token.getName()}().equals(${lastResult})) { return; }
// $fi
// $fi
set${Token.getName()}(${lastResult});
}
}
```
Warnings/Errors to check for:
- the token must be resolvable within the parent type
- the Token must not be a TokenNTA (i.e., check for `!Token.getNTA()`)
- type of first mapping must be `byte[]`
- type of last mapping must be type of the Token
- types of mappings must match (modulo inheritance)
- for all type checks, there are three cases regarding the two types to check against:
- 1) both are nonterminal types, check with grammar
- 2) both are known classes, check with `Class.forName()` and subclass-checking-methods
- 3) otherwise issue warning, that types could not be matched
## WriteToMqttDefinition
Example:
- `write RobotArm.AppropriateSpeed using CreateSpeedMessage, SerializeRobotConfig ;`
Idea:
- create write topic variable (type String) in parentType, initially `null`
- create last value variable (type _effectiveWriteType_ == type of last mapping), initially `null`
- create connect method to setup topic and update lastValue, and maybe write initial value (see parameter `boolean writeCurrentValue`)
- create update method to reset token NTA, apply mappings, check if equal (return `false`), and call write method otherwise (return `true`)
- create write method to publish the last value
Input:
- AlwaysApply:Boolean
- Mapping*:List of MappingDefinition
- Token:TokenComponent
Calculated:
- parentType:TypeDecl
Attributes on this used in generation process:
```java=
writeTopic() = _topic_${Token.getName()}
syn String WriteToMqttDefinition.lastValue() = "_lastValue_" + getToken().getName();
connectMethod() = connect${Token.getName()}
updateMethod() = _update_${Token.getName()}
writeMethod() = _write_${Token.getName()}_lastValue
tokenResetMethod = get${Token.getName()}_reset()
```
Code:
```java=
private String ${parentType()}.${writeTopic()} = null;
private byte[] ${parentType()}.${lastValue()} = null;
// connect method
public void ${parentType()}.${connectMethod()}(String topic, boolean writeCurrentValue) {
${writeTopic()} = topic;
${updateMethod()}();
if (writeCurrentValue) {
${writeMethod()}();
}
}
// update method
protected boolean ${parentType}.${updateMethod()}() {
${tokenResetMethod()}();
${typeOfFirstMappingParameterName} ${firstMappingParameterName} = get${Token.getName()}();
// TODO: iterate over mapping, calling method, store result in fresh variable of matching type, maybe surround with try/catch?
// $if not getAlwaysApply():
if (java.util.Arrays.equals(${lastResult}, ${lastValue()})) return false;
// $fi
${lastValue()} = ${lastResult};
return true;
}
// write method
protected void ${parentType}.${writeMethod()}() {
_mqttUpdater().publish(${writeTopic()}, ${lastValue()});
}
```
Open questions:
- should update method be `public`? (to explicitly update value from outside)
- should write method be `public`? (to explicitly write value from outside)
- are there any effects when comparing `byte[]` (when not alwaysApply)
Warnings/Errors to check for:
- Token must be a TokenNTA (i.e., check for `Token.getNTA()`)
- type of first mapping must be type of Token
- type of last mapping must be `byte[]`
- types of mappings must match (modulo inheritance)
- for all type checks, there are three cases regarding the two types to check against:
- 1) both are nonterminal types, check with grammar
- 2) both are known classes, check with `Class.forName()` and subclass-checking-methods
- 3) otherwise issue warning, that types could not be matched
- no more than one write mapping for each TokenComponent
# MappingDefinition
```=
MappingDefinition ::= <ID> FromType:MappingDefinitionType <FromVariableName> ToType:MappingDefinitionType <Content> ;
abstract MappingDefinitionType ::= ;
JavaMappingDefinitionType : MappingDefinitionType ::= Type:JavaTypeUse ;
JavaArrayMappingDefinitionType : MappingDefinitionType ::= Type:JavaTypeUse ;
```
Example:
```java=
ParseLinkState maps byte[] bytes to panda.Linkstate.PandaLinkState {:
return panda.Linkstate.PandaLinkState.parseFrom(bytes);
:}
```
Idea: create static method within `ASTNode` accepting one parameter, and return their result
Input:
- ID:String
- FromType (Type or array of Type)
- FromVariableName:String
- ToType (Type or array of Type)
- Content
Code:
```java=
protected static ${ToType.printJavaType()} ASTNode.${ID}(${FromType.printJavaType()} ${FromVariableName}) {
${Content}
}
```
# DependencyDefinition
```=
DependencyDefinition ::= <ID> ;
rel DependencyDefinition.Source -> TokenComponent ;
rel DependencyDefinition.Target -> TokenComponent ;
```
Example:
- `RobotArm._AppropriateSpeed canDependOn Joint.CurrentPosition as dependency1 ;` (read Target canDependOn Source)
Idea:
- create new token with name _hiddenTokenName_ = `_internal` + _originalTokenName_
- create virtual getter delegating to now hidden token
- create virtual setter with observer notification
- create (internal) relation between parents of source and target
- create dependency method for convenience
Input:
- ID:String
- Source:TokenComponent
- Target:TokenComponent
Calculated:
- sourceParent:TypeDecl
- targetParent:TypeDecl
- targetUpdateDefinition:WriteToMqttDefinition
Attributes on this used in generation process:
```java=
internalRelationPrefix() = "_internal_" + ${ID};
internalTokenName() = "_internal" + ${Source.getName()};
```
Code:
```java=
// relation (to be added in grammar)
rel ${targetParent}.${internalRelationPrefix}Source* <-> ${sourceParent}.${internalRelationPrefix}Target* ;
// dependency method
public void ${targetParent}.add${ID}(${sourceParent} source) {
this.add${internalRelationPrefix}Source(source);
}
// virtual setter
public ${sourceParent.getName()} ${sourceParent.getName()}.set${Source.getName()}(${Source.getType()} value) {
set${internalTokenName()}(value);
for (${targetParent.getName()} target : get${internalRelationPrefix}Targets()) {
if (target.${targetUpdateDefinition().updateMethod()}()) {
target.${targetUpdateDefinition().writeMethod()}();
}
}
return this;
}
// virtual getter
public ${Source.getJavaTypeUse().generateAbstractGrammar(sb)} ${sourceParent.getName()}.get${Source.getName()}() {
return get${internalTokenName()};
}
// refine name of TokenComponent when writing out abstract grammar
refine BackendAbstractGrammar public void TokenComponent.generateAbstractGrammar(StringBuilder sb) {
// TODO, check if TokenComponent is in Source of a dependency definition
// if yes, then somehow change getName() to return internalTokenName()
// if not, return refined()
}
```
Things to pay attention to:
- `JavaTypeUse().generateAbstractGrammar(sb)` adds the type to a given StringBuilder
Open questions:
- What is the best way to replace/change the Source-TokenComponent
- Replacing the TokenComponent in the original Program is probably not a good idea, because there may be references to the original TokenComponent, which then has no parent (and this may lead to NullPointerExceptions etc.)
- Setting a new name in the original TokenComponent defeats the purpose, because other definitions would use the internal token instead of the created virtual getter and setter
- Refining just `TokenComponent.generateAbstractGrammar` from the aspect `BackendAbstractGrammar` seems fine, but may lead to duplicate code
Warnings/Errors to check for:
- There must **not** be a write update definition for the source token
- This would create conflicting setter
- [name=rs] [time=May 6th, 2020] This could actually be wrong, as write definition do not create a setter
- There **must be** a write update definition for the target token
- Otherwise there are missing update and write methods used in the virtual setter