Skip to content

Instantly share code, notes, and snippets.

@lrhn
Created April 30, 2024 15:25
Show Gist options
  • Save lrhn/47d4161c4743a09659732952b21591f7 to your computer and use it in GitHub Desktop.
Save lrhn/47d4161c4743a09659732952b21591f7 to your computer and use it in GitHub Desktop.
Dart Augmenting non-redirecting generative constructors.

Constructor (non-redirecting generative) augmentation

class C {
  var int x, y;
  C(int x, [this.y = 0]) : x = ++x {
    print((++x, ++y));
  }
}
augment class C {
  augment C(int z, [int w = 0]) : assert(z == 0) {
    int x = 42;  
    print((z, w));
    augmented();
    print((z, w));
    augmented();
    print((z, w));
  }
}

What should this do? Reasonably?!

Things to not support:

  • Calling augmented() sees or changes local variables in the caller.
  • Calling augmented() changes parameter variables in the caller’s parameter list.

Let’s try this for a constructor invocation of a constructor defined by an augmentation stack and with and argument list args:

  • This invokes the constructor that is the result of augmentation.

    • First it evaluates all instance variable initializers in some order. (Traditionally source order inside a single declaration.)

      • First recurse on the parent augmentation stack, if any.
      • Then evaluate initializers in the current declaration.
    • Then bind actuals to formals.

      • If different parameter lists can have different names, and some may be mutable, we should have a binding per constructor in the chain. (It’s then an optimization to get rid of any that are unnecessary.)

      • So for each parameter list in the stack, depth first, bind the actual arguments to the parameters.

        • (We should have already made it an error if there are corresponding parameters in different lists where

          • one is an initializing formal and the other an effective super-parameter.
          • both are initializing formals, but for different variables.

          so that we don’t need to worry about that.)

        • If at least one of the corresponding parameters is an initializing formal, then that variable is initialized once to the value.

        • (Don’t know what to say about super. parameters. Probably something.)

      • This introduces a parameter scope for each constructor declaration, and initializes some variables.

    • Evaluate the initializer lists of the constructors.

      • Depth first again, evaluate the initializer lists in the corresponding constructor’s parameter scope.
      • When back, all initializers (and asserts) have been evaluated, which can have changed some of the parameter scopes, if they contain mutable variables.
    • Invoke the super-constructor.

      • Search back through the stack until finding a super-invocation. Update it with all super parameters from that declaration and forwards in the augmentation stack. (Overriding the super invocation also clears the meaning of all earlier super. parameters, making the ineffective, which is one of the things to say about super parameters.)
      • Invoke the super-constructor in that way.
    • When we come back, all superclass constructor bodies have been executed.

    • To execute the body of this class, we “execute the body of an augmentation stack” as follows:

      • If the stack is empty, there is no body. Execution completes normally.
      • Otherwise, let t be the top declaration of the stack and rest be the rest.
      • If t has a body:
        • Execute that body in its constructor declaration’s corresponding body scope (differs from parameter scope in that initializing formal and super parameters variables are not there).
        • If that body encounters an augmented(), execute the body of rest. The result has static type void (and will be null).
      • Otherwise, if the top had no body, execute the body of rest.

Let’s try that for C(0, 43).

  • First it creates an instance of C.

  • Then it invokes the augmentation stack C and augment C.

  • There are no field initializers.

  • Then we bind actuals to formals:

    • For C: local var x = 0, initializing parameter initializes field y and introduces local final y = 43
    • For augment C, local var z = 0 and local var w = 43.
  • Then evaluate initializer lists, depth first:

    • x = ++x in scope of C, which initializes x variable to 1 and updates environment to var x = 1, final y = 43.
    • assert(z == 0) in scope of augment C, which succeds.
  • Invoke super constructor. There are none, we end up calling Object, which does nothing, and we come right back.

  • Execute body:

        int x = 42;  
        print((z, w));
        augmented();
        print((z, w));
        augmented();
        print((z, w));

    Introduces body scope extending parameter scope of augment C with x = 42.

    Prints (0, 43).

    Executes the body of C in its own body scope (var x = 1;). Prints (2, 44) and updates variable of parameter/body scope to var x = 2, and instance variable y to 44.

    Prints (0, 43) again.

    Executes the body of C in its own body scope (var x = 2;). Prints (3, 45) and updates variable of parameter/body scope to var x = 3, and instance variable y to 45.

    Prints (0, 43) again.

    All changes to variables are local to the lexical scope where the local variable is declared.

    The variables are consistent across the parameter list evaluation, initializer list evaluation and body execution for each declaration. They keep their names and values, which is important for mutable variables, and do not get mixed together with other variables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment