Thoughts and practice on dynamic thermal renewal of Flutter
Thoughts and Practice on Flutter dynamic thermal renewal (II) —-Dart code conversion AST
Thoughts and Practice on Flutter dynamic thermal renewal (iii) —- analysis of AST Runtime
Thoughts and Practice on Flutter dynamic thermal renewal (IV) —- analysis of AST widgets
Thinking and practice of Flutter dynamic thermal update (v) —- call AST dynamic code
In the previous article “Thoughts and Practice on Flutter Dynamic Thermal Update (II) —-Dart Code Conversion AST”, Dart code is converted to AST. In this article, we will explore how to parse the generated AST data
1. What is the Runtime
The Runtime we define here is a container that dynamically runs the AST, starting with the way the AST parses. In the opening article “Thoughts and Practice of Flutter Dynamic Thermal Renewal”, we mentioned that the dynamic solution we implemented also follows the MVVM idea, decoupling UI and business. The AST parsing is divided into two parts: the UI CLASS AST parsing and the service class AST parsing. Build a Widget based on the STRUCTURE and properties of the UI described in the AST and then call setState to re-render it. This is because the Flutter declarative programming framework supports this operation. However, for business class AST, we cannot compile AST into binary code and then execute it during App running. Therefore, a feasible idea is dynamic parsing, which is also the IDEA of JIT, which runs while parsing. By parsing data nodes in AST, it maps to the corresponding syntax operation of Dart. The corresponding Dart code is then executed.
2. Implement the Runtime
In order to implement the Runtime, we need to deal with an important problem: variables. It is inevitable that there will be many variables in the code to store and exchange data, and the syntax rules of the language are nothing more than various operations on these variables. Therefore, how to organize variables in the Runtime is the first problem to be solved.
We know the variables stored in two places when the program is running, one is stack, the other is heap, we refer to the assembly language to operate variables, introduced the concept of a variable stack. All variables encountered during Runtime parsing are pushed into this variable stack. For example, when executing a code block, variables in the code block are first pushed onto the stack, and when the code block is executed, variables are pushed out of the stack. When we use variables, we start at the top of the stack and look down. When we find a matching variable name, we pull out the value of the variable and stop looking. This also solves the problem of variable scope.
Now that the variable problem is solved, the next step is relatively simple. Just keep parsing the AST Node, as shown in the figure below:
3. Code implementation
Using the example code from the previous article, let’s see how Runtime is implemented in code:
//demo_blog_code.dart
int incTen(int a) {
int b = a + 10;
return b;
}
Copy the code
Let’s examine the above sample code, which contains several syntactic elements:
- function
- Function parameters
- Variable declarations
- Variable assignment
- Additive expression
- Function return value
To correspond to Ast data nodes, we need to deal with the following nodes:
- FunctionDeclaration
- FunctionExpression
- SimpleFormalParameter
- BlockStatement
- VariableDeclarationList
- AssignmentExpression
- BinaryExpression
- ReturnStatement
The code that handles all nodes is not posted, and a representative code implementation of BinaryExpression is posted:
class BinaryExpression extends AstNode {
/ / / operator
/ / / * +
/ / / * -
/// * *
/ / / * /
/ / / *<
/ / / * >
/ / / *<=
/ / / * >=
/ / / * = =
/ / / * &&
/ / / * | |
/ / / * %
/ / / *<<
/ / / * |
/ / / * &
/ / / * >>
///
String operator;
/// left manipulation expression
Expression left;
/// right manipulate expression
Expression right;
BinaryExpression(this.operator.this.left, this.right, {Map ast})
: super(ast: ast);
factory BinaryExpression.fromAst(Map ast) {
if(ast ! =null &&
ast['type'] == astNodeNameValue(AstNodeName.BinaryExpression)) {
return BinaryExpression(ast['operator'], Expression.fromAst(ast['left']),
Expression.fromAst(ast['right']),
ast: ast);
}
return null;
}
@override
Map toAst() => _ast;
}
Copy the code
See the Git address at the end of this article for the complete code. BinaryExpression defines the syntax of operational expressions, such as four operations and logical operations. Once the Ast data node is defined, a method is required to parse the defined Ast node. Again, take BinaryExpression as an example:
dynamic _executeBinaryExpression(
BinaryExpression binaryExpression, AstVariableStack variableStack) {
// Get the value of the left operator
var leftValue = _executeBaseExpression(binaryExpression.left, variableStack);
// Get the value of the right operator
var rightValue =
_executeBaseExpression(binaryExpression.right, variableStack);
/ / operators
switch (binaryExpression.operator) {
case '+':
return leftValue + rightValue;
case The '-':
return leftValue - rightValue;
case The '*':
return leftValue * rightValue;
case '/':
return leftValue / rightValue;
case '<':
return leftValue < rightValue;
case '>':
return leftValue > rightValue;
case '< =':
return leftValue <= rightValue;
case '> =':
return leftValue >= rightValue;
case '= =':
return leftValue == rightValue;
case '&':
return leftValue && rightValue;
case '| |':
return leftValue || rightValue;
case The '%':
return leftValue % rightValue;
case '< <':
return leftValue << rightValue;
case '|':
return leftValue | rightValue;
case '&':
return leftValue & rightValue;
case '> >':
return leftValue >> rightValue;
default:
return null; }}Copy the code
Method returns the result of dynamically executing the BinaryExpression. The same is true for parsing other Ast nodes.
Finally, let’s look at the Runtime code implementation:
class AstRuntime {
/ / / Ast class definition
AstClass _astClass;
/ / / variable stack
AstVariableStack _variableStack;
AstRuntime(Map ast) {
if (ast['type'] == astNodeNameValue(AstNodeName.Program)) {
var body = ast['body'] as List; _variableStack = AstVariableStack(); _variableStack.blockIn(); body? .forEach((b) {if (b['type'] == astNodeNameValue(AstNodeName.ClassDeclaration)) {
/ / class
_astClass = AstClass.fromAst(b, variableStack: _variableStack);
} else if (b['type'] ==
astNodeNameValue(AstNodeName.FunctionDeclaration)) {
// Parse the global function
varfunc = AstFunction.fromAst(b); _variableStack.setFunctionInstance<AstFunction>(func.name, func); }}); }}/// Call the class method, noting that the argument list is in the same order as in the template code
Future callMethod(String methodName, {List params}) async {
if(_astClass ! =null) {
return _astClass.callMethod(methodName, params: params);
}
return Future.value();
}
/// Call the global function. Note that the argument list is in the same order as in the template code
Future callFunction(String functionName, {List params}) async {
var function =
_variableStack.getFunctionInstance<AstFunction>(functionName);
if(function ! =null) {
return function.invoke(params, variableStack: _variableStack);
}
returnFuture.value(); }}Copy the code
Let’s test the Runtime in command-line:
main(List<String> arguments) async {
exitCode = 0; // presume success
finalparser = ArgParser().. addFlag("file", negatable: false, abbr: 'f');
var argResults = parser.parse(arguments);
final paths = argResults.rest;
if (paths.isEmpty) {
stdout.writeln('No file found');
} else {
var ast = await generate(paths[0]);
var astRuntime = AstRuntime(ast);
var res = await astRuntime.callFunction('incTen', params: [100]);
stdout.writeln('Invoke incTec(100) result: $res'); }}Copy the code
We execute the above test code:
Output “110”, in line with the source code calculation logic.
See DynamicFlutter for a complete code implementation