Initialization order should honour dependencies

Issue #28 closed
Anders Melander created an issue

I propose that the unit initialization order be changed to follow the Delphi model where the interface dependency tree defines the order.

Consider the following three units, the first one being the main script module:

unit Unit1;

interface

uses
  Unit2;

implementation

initialization
  PrintLn(CurrentSourceCodeLocation.File);
finalization
end;
unit Unit2;

interface

uses
  Unit3;

implementation

initialization
  PrintLn(CurrentSourceCodeLocation.File);
finalization
end;
unit Unit3;

interface

implementation

initialization
  PrintLn(CurrentSourceCodeLocation.File);
finalization
end;

The output of this script should be:

Unit3
Unit2
*MainModule*

but currently is:

*MainModule*
Unit2
Unit3

Comments (11)

  1. Anders Melander reporter

    The current initialization order is just the order the units were added to the unit list (TdwsCompiler.FProg.UnitMains).

    The terms used in TdwsCompiler.SetupInitializationFinalization suggests that the list were, at some point, meant to be ordered by some criteria, but this never happens. Maybe the (unused) TUnitMainSymbol.InitializationRank property had something to do with that.

    I'm now using the following hack to ensure that the initialization order is as I would expect it to (Added stuff marked with {*}):

    function TdwsCompiler.HandleExplicitDependency(const unitName : UnicodeString) : TUnitSymbol;
    var
       HasBumpedUnit: boolean; {*}
    
      procedure BumpUnit(RefUnit: TUnitMainSymbol); {*}
      var
        i: integer;
      begin
        if (HasBumpedUnit) or (RefUnit = nil) then
          exit;
        HasBumpedUnit := True;
        // Move unit to end of unit list so it will be initialized after the units it references
        i := FProg.UnitMains.IndexOf(RefUnit);
        ASSERT(i <> -1);
        if (i <> FProg.UnitMains.Count-1) then
        begin
          FProg.UnitMains.Extract(i);
          FProg.UnitMains.Add(RefUnit);
        end;
      end;
    ...
    begin
       HasBumpedUnit := False; {*}
      ...
       i:=FUnits.IndexOfName(unitName);
       if i<0 then begin
          if Assigned(FOnNeedUnit) then
             unitResolved:=FOnNeedUnit(unitName, unitSource);
          if unitResolved<>nil then
             FUnits.Add(unitResolved)
          else begin
             if unitSource='' then
                unitSource:=GetScriptSource(unitName+'.pas');
             if unitSource<>'' then begin
                srcUnit:=TSourceUnit.Create(unitName, FProg.Root.RootTable, FProg.UnitMains);
                unitResolved:=srcUnit;
                FUnits.Add(unitResolved);
                oldContext:=FSourceContextMap.SuspendContext;
                SwitchTokenizerToUnit(srcUnit, unitSource);
                FSourceContextMap.ResumeContext(oldContext);
                BumpUnit(srcUnit.Symbol); {*}
             end;
          end;
    ...
    end;
    
    function TdwsCompiler.Compile(const aCodeText : UnicodeString; aConf : TdwsConfiguration;
      const mainFileName : String = '') : IdwsProgram;
    
      procedure BumpUnit(RefUnit: TUnitMainSymbol); {*}
      var
        i: integer;
      begin
        if (RefUnit = nil) then
          exit;
        // Move unit to end of unit list so it will be initialized after the units it references
        i := FProg.UnitMains.IndexOf(RefUnit);
        ASSERT(i <> -1);
        if (i <> FProg.UnitMains.Count-1) then
        begin
          FProg.UnitMains.Extract(i);
          FProg.UnitMains.Add(RefUnit);
        end;
      end;
    ...
    begin
    ...
          // Start compilation
          FProg.Expr:=ReadScript(sourceFile, stMain);
    
          ReadScriptImplementations;
    
          BumpUnit(FCurrentUnitSymbol); {*}
    ...
    end;
    

    TdwsCompiler.RecompileInContext needs the same treatment but I've not touched that (yet).

  2. Anders Melander reporter
    • changed status to open

    If you change the uses clause of Unit1 to this:

    uses
      Unit3,
      Unit2;
    

    then the initialization order should still be:

    Unit3
    Unit2
    *MainModule*

    since unit2 uses unit3.

  3. Eric Grange repo owner

    Hmm, I guess there also needs to be test cases for circular references and a few dependency graphs.

    What is Delphi ordering behavior for unit references specified in the implementation?

  4. Anders Melander reporter

    As far as I know it's undefined (or at least undocumented), but I believe my fix hack handles it too.

    There are already checks for circular references. Search for CPE_UnitCircularReference.

  5. Eric Grange repo owner

    Circular references in implementations are allowed, it's only in the interfaces they are not allowed.

    Just made a few checks in Delphi, the initialization order does not seem to be affected whether the uses is in interface or implementation and it handles circular references "correctly".

    F.i is Unit1 -> ( Unit2 <-> Unit 3 ) -> Unit4

    1 & 4 will be in the appropriate position, 2 & 3 (circular dependency) do not have a well defined order (2 or 3 comes ahead, depending, which is ok)

  6. Anders Melander reporter

    Impressive. I'll give it a spin.

    Note that it would probably be best to test both the original:

    uses
      Unit2;
    

    and the new:

    uses
      Unit3,
      Unit2;
    

    since they are exercising two different problems.

  7. Log in to comment