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