{**************************************************************************************************}
{                                                                                                  }
{ Delphi language Preprocessor (dpp32)                                                             }
{                                                                                                  }
{ The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); }
{ you may not use this file except in compliance with the License. You may obtain a copy of the    }
{ License at http://www.mozilla.org/MPL/                                                           }
{                                                                                                  }
{ Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF   }
{ ANY KIND, either express or implied. See the License for the specific language governing rights  }
{ and limitations under the License.                                                               }
{                                                                                                  }
{ The Original Code is dpp_Macros.pas                                                              }
{                                                                                                  }
{ The Initial Developer of the Original Code is Andreas Hausladen                                  }
{ Portions created by these individuals are Copyright (C) of these individuals.                    }
{                                                                                                  }
{ You may retrieve the latest version of this file at the Projects home page, located at           }
{ http://www.sourceforge.net/projects/dpp32                                                        }
{                                                                                                  }
{**************************************************************************************************}
unit dpp_Macros;
(*******************************************************************************
* Preprocessor for Delphi and Kylix compiler (dcc32, dcc)
* =======================================================
*
* This preprocessor make it possible for Delphi developers to use the power of
* macros in Object Pascal code. The macros acts like the C/C++ preprocessor
* macros with the exception of conditional compilation.
*
*
* Macro Syntax:
* {$define macroname}          // registers a macro
* {$undef macroname}           // unregisters a the macro
* {MACROINCLUDE mymacros.mac}  // does only include the macros from the file
*
* Examples:
* {$define macroname1   replacement text }
* {$define macroname2(arg1, arg2) WriteLn(#arg1); ReadLn(arg2 ## _Var) }
* {$undef macroname2}
*
*
*
* The macros in the interface sections of the USES units are not imported. For
* macro imports use MACROINCLUDE.
*
* For every unit in the uses-statement a MACROINCLUDE is automatically generated
* e.g. uses MyUnit; -> MyUnit.macros is included if it exists. For the current
* unit also the corresponding .macros file is also included. The .macros file
* must exist in the same directory as the unit itself and it cannot contain any
* source code. Only macro declarations ($define, $undef, MACROINCLUDE) are
* allowed. Other code is ignored.
*
* The preprocessor follows the compiler directive {$I filename} and
* {$INCLUDE filename}. For every included file a copy of the preprocessed files
* is generated to support differences in macro defined before the include
* directive.
*
*
* ISSUES:
*   - The compiler has a line limit of 1024 chars. For larger macros a unique
*     include file can be necessary. Splitting the macro in more lines will
*     break the error message lines. Prehaps the preprocessor can modify the
*     original file by inserting "{MACROPLACE}" comment lines after the long
*     line.
*
*   - No conditional expressions (Delphi 6+) are supported by the preprocessor.
*     All macros in such an expression are parsed and replaced. In order to
*     support this feature an expression parser is necessary. Furthermore
*     we must collect all *const* expressions. But how can we get the consts
*     from a compiled unit (.dcu).
*
*******************************************************************************)
{.$define HASHTABLE}
interface

uses
  Types, SysUtils, Classes, Contnrs, dpp_PascalParser, dpp_Utils;

const
  MaxFileRecursion = 30; // files can be opened at once

type
  TWarningEvent = procedure(Sender: TObject; const Filename, Msg: string;
    LineNum: Integer) of object;
  TErrorEvent = procedure(Sender: TObject; const Filename, Msg: string;
    LineNum: Integer) of object;
  TPredefineMacrosEvent = procedure(Sender: TObject) of object;
  TDefaultConditionalsEvent = procedure(Sender: TObject) of object;
  TBuiltInMacroEvent = function(Sender: TObject; Token: PTokenInfo;
    var Replacement: string; var IsBuiltIn: Boolean): string of object;

  TMacroCompare = function(const S1, S2: string): Integer;

  TParseType = (
    ptUnit,            // collect and replace macros in the whole file
    ptInclude,         // collect and replace macros in the whole file but use some special file handling
    ptInterfaceMacros  // collect macros from the interface-section of the file (only for MACROINCLUDE)
  );

 { TPascalParserEx make it easier to pass NoReplaceMacros across the
   TMacro.methods. }
  TPascalParserEx = class(TPascalParser)
  public
    NoReplaceMacros: Boolean;
  end;

  TMacros = class;
  TMacroList = class;

  IMacroFileSys = interface
  ['{F3CD3F56-F849-4C9E-BD57-3D76DE6E0C64}']
    { Called before the file is read. The file exists. }
    procedure BeforeFile(const FileName: string; IsIncludeFile: Boolean);
    { Called after the file was stored depending on Modified. Filename is a
      full qualified file name. The file and the new file exist. }
    procedure AfterFile(const FileName, NewFileName: string; IsIncludeFile,
      Modified: Boolean);
    { LoadFile must return the file content in *Content*. Filename is a
      full qualified file name. The file exists. }
    procedure LoadFile(const Filename: string; out Content: string;
      IsIncludeFile: Boolean);
    { SaveFile is called for saving the file's content. Filename is the original
      filename and NewFilename is the preprocessor's new file name. All file
      names are full qualified file names. The file exists but the new file
      doesn't. }
    procedure SaveFile(const Filename: string; var NewFilename: string;
      const Content: string; IsIncludeFile: Boolean);
    { FindFile is called if the file name is not a full qualified file
      name. Return the full qualified file name or '' if the file does not
      exist. }
    function FindFile(const Filename: string; IsIncludeFile: Boolean): string;
    { FileExists must return True if the given File exists. Filename is a
      full qualified file name. }
    function FileExists(const Filename: string): Boolean;

    { LinesMoved is called for macro replacements using more than one line.
      LineNum   : line where the macro is.
      AddedLines: number of inserted lines }
    procedure LinesMoved(const Filename: string; LineNum, AddedLines: Integer);
  end;

  TMacroItem = class(TObject)
  private
    FMacroList: TMacroList;
    FName: string;
    FReplacement: string;
    FHasBrackets: Boolean;
    FArguments: TStringDynArray;
    FInterfaceMacro: Boolean; // if TRUE the $ifdef/$ifndef must be modified
  public
    constructor Create(AMacroList: TMacroList);
    function Parse(const MacroNameArgReplacement: string; out ErrorMsg: string;
      AInterfaceMacro: Boolean): Boolean;
    function IsEqual(Item: TMacroItem): Boolean;
    procedure Assign(Item: TMacroItem);
    function IndexOfArg(const ArgName: string): Integer;

    property Name: string read FName;
    property Replacement: string read FReplacement;
    property HasBrackets: Boolean read FHasBrackets write FHasBrackets;
    property Arguments: TStringDynArray read FArguments;
    property MacroList: TMacroList read FMacroList;
    property InterfaceMacro: Boolean read FInterfaceMacro write FInterfaceMacro;
  end;

  TMacroList = class(TObjectList)
  private
    FMacros: TMacros;
    FHashTable: TRedirectTable;
    function GetItems(Index: Integer): TMacroItem;
  protected
    function IndexOfMacro(const Name: string): Integer;
  public
    constructor Create(Macros: TMacros);
    procedure Assign(MacroList: TMacroList);
    procedure Clear; override;

    function RegisterMacro(const Macro: string; AInterfaceMacro: Boolean): TMacroItem;  // in: 'test(x) x*x' // DEVINFO: Macro must be TrimLeft()
    procedure UnregisterMacro(const Name: string);            // in: 'test'        // DEVINFO: Macro must be Trim()
    function IsMacroRegistered(const Name: string): Boolean;                       // DEVINFO: Macro must be Trim()

    function FindMacro(const Name: string): TMacroItem;

    property Items[Index: Integer]: TMacroItem read GetItems;
  end;

  TMacros = class(TMacroList)
  private
    FCaseSensitive: Boolean;
    FConditionalParse: Boolean;
    FErrorMsg: string;

    FUnits: TStrings;        // contains all units (interface and implementation <uses>); "MacroFileExists:=Boolean(Objects[])"
    FIncludeFiles: TStrings; // contains all include files. Integer(Objects[]): how often the file is used

    FMacroMacroRecursion: TList; // used for in macro replacement see TMacros.ReplaceMacro()
    FFileRecursion: Integer;  // number of open include files

    FConditionals: TStrings; // $define, $ifdef, $ifndef - conditional compilation
    FCompilerOptions: TStrings; // $ifopt - conditional compilation
    FConditionalParseCode: TBooleanList; // .LastItem=True: parse code; .LastItem=False: ignore code and macros

    FAppType: string;
    FFileSys: IMacroFileSys;

    FCompare: TMacroCompare;
    FOnError: TErrorEvent;
    FOnWarning: TWarningEvent;
    FOnPredefineMacros: TPredefineMacrosEvent;
    FOnBuiltInMacro: TBuiltInMacroEvent;
    FOnDefaultConditionals: TDefaultConditionalsEvent;

    procedure SetCaseSensitive(const Value: Boolean);
  protected
    procedure Warning(const Msg, FileName: string; LineNum: Integer); overload;
    procedure Warning(const Msg: string; Token: PTokenInfo); overload;
    procedure Error(const Msg, FileName: string; LineNum: Integer); overload;
    procedure Error(const Msg: string; Token: PTokenInfo); overload;
    procedure PredefineMacros;
    function BuiltInMacro(Token: PTokenInfo; var Replacement: string): Boolean;
    procedure DefaultConditionals;

    function ParseUnitMacroFile(UnitIndex: Integer): Integer;
    function ParseFile(Filename: string; ParseType: TParseType;
      TestFileExistence: Boolean): string; // returns new filename (.i.pas, .i1.*, .i2.*, ...)
    function ParseString(var Text: string; const Filename: string;
      StartLineNum: Integer; ParseType: TParseType): Boolean;

    function NextToken(Parser: TPascalParserEx; out Token: PTokenInfo): Boolean; overload;
    function NextToken(Parser: TPascalParserEx): PTokenInfo; overload;
    function ParseConditionals(var Line: string; const Filename: string;
      StartLineNum: Integer): Boolean;
    function ParseComment(Token: PTokenInfo): Boolean;
    procedure ParseUsesIdent(Parser: TPascalParserEx);

    function GetReplacement(Item: TMacroItem; const Args: TStringDynArray;
      const Filename: string; StartLineNum: Integer): string;
    procedure ReplaceMacro(Parser: TPascalParserEx; Item: TMacroItem);

    function RegisterMacroByToken(const Macro: string; Token: PTokenInfo): TMacroItem;
  public
    constructor Create(AFileSys: IMacroFileSys);
    destructor Destroy; override;

    procedure Define(const Condition: string); // defines a condition (FConditionals)
    procedure Undefine(const Condition: string); // undefines a condition (FConditionals)
    procedure SetOption(const Option: string; Value: Boolean);
    function IsDefined(const Condition: string): Boolean; // return TRUE if the condition is defined (FConditionals)

    function Parse(const FileName: string; OnlyThisFile: Boolean): Boolean;

    property CaseSensitive: Boolean read FCaseSensitive write SetCaseSensitive default True;
    property ConditionalParse: Boolean read FConditionalParse write FConditionalParse default False;
    property ErrorMsg: string read FErrorMsg;

    property OnWarning: TWarningEvent read FOnWarning write FOnWarning;
    property OnError: TErrorEvent read FOnError write FOnError;
    property OnPredefineMacros: TPredefineMacrosEvent read FOnPredefineMacros write FOnPredefineMacros;
    property OnDefaultConditionals: TDefaultConditionalsEvent read FOnDefaultConditionals write FOnDefaultConditionals;
    property OnBuiltInMacro: TBuiltInMacroEvent read FOnBuiltInMacro write FOnBuiltInMacro;
  end;

implementation

resourcestring
 // error messages
  SMacroArgumentsSyntaxError = 'No valid macro name/argument: %s';
  SMacroNotEnoughArguments = 'Not enough arguments for %s';
  SMacroSytaxError = 'Syntax error: %s';
  SEmptyMacroArgument = 'Macro argument is empty.';
  SNoFurtherToken = 'Unexpected file/macro end.';
  SCanOnlyMakeStringFromArguments = 'Can only use # on macro arguments.';
  SCombineError = 'Wrong usage for ##.';
  SNoArgumentSpecified = 'No argument specified for macro.';
  SMacroRedefinitionNotIdentical = 'Redeclaration of "%s" is not identical.';
  SToManyRecursions = 'To many file recursions.';
  SFindFile = 'File "%s" not found.';
  SConditionalSyntaxError = 'Syntax error in conditional directive.';

const
  SMacroStartString = '$DEFINE ';
  SUnmacroStartString = '$UNDEF ';
  SMacroIncludeString = 'MACROINCLUDE ';
  SMacroIncludeFileExt = '.macros';

  SBuiltInStartChars = '__'; // do not modify or localize 
  SBuiltIn_Line = '__LINE__';
  SBuiltIn_File = '__FILE__';
  SBuiltIn_Date = '__DATE__';
  SBuiltIn_Time = '__TIME__';

{ TMacroItem }

constructor TMacroItem.Create(AMacroList: TMacroList);
begin
  inherited Create;
  FMacroList := AMacroList;
end;

procedure TMacroItem.Assign(Item: TMacroItem);
var i: Integer;
begin
  FHasBrackets := Item.FHasBrackets;
  SetLength(FArguments, Length(Item.FArguments));
  FReplacement := Item.FReplacement;
  for i := 0 to High(FArguments) do
    FArguments[i] := Item.FArguments[i];
end;

{ IndexOfArg() returns the index of the macro argument with the name ArgName. }
function TMacroItem.IndexOfArg(const ArgName: string): Integer;
var cmp: TMacroCompare;
begin
  cmp := FMacroList.FMacros.FCompare;
  for Result := 0 to High(Arguments) do
    if cmp(ArgName, Arguments[Result]) = 0 then Exit;
  Result := -1;
end;

function TMacroItem.IsEqual(Item: TMacroItem): Boolean;
var
  i: Integer;
  cmp: TMacroCompare;
begin
  Result := False;
  if FHasBrackets <> Item.FHasBrackets then Exit;
  if Length(FArguments) <> Length(Item.FArguments) then Exit;
  if FReplacement <> Item.FReplacement then Exit;

  cmp := FMacroList.FMacros.FCompare;
  for i := 0 to High(FArguments) do
    if cmp(FArguments[i], Item.FArguments[i]) <> 0 then Exit;

  Result := True;
end;

procedure SetTrimString(out S: string; P: PChar; Count: Integer);
var
  F: PChar;
begin
  while {(P[0] <> #0) and }(P[0] <= #32) and (Count > 0) do
  begin
    Inc(P);
    Dec(Count);
  end;
  if Count > 0 then
  begin
    F := P;
    Inc(P, Count);
    while (P > F) and (P[0] <= #32) do Dec(P);
    SetString(S, F, P - F);
  end
  else
    S := '';
end;

{ Parse() parses the macro declaration and split it to its name, arguments and
  replacement. }
function TMacroItem.Parse(const MacroNameArgReplacement: string; out ErrorMsg: string;
  AInterfaceMacro: Boolean): Boolean;
var
  F, P: PChar;
  ArgCount: Integer;
  i: Integer;
begin
  FInterfaceMacro := AInterfaceMacro;
  FHasBrackets := False;

  Result := False;

 // parse Macro name string
  F := Pointer(MacroNameArgReplacement);
  if F = nil then Exit;
  P := F;
  while not (P[0] in [#0, '(', #9, #10, #13, ' ']) do Inc(P);
  SetString(FName, F, P - F);
 // test for valid identifier
  if not IsValidIdent(FName) then
  begin
    ErrorMsg := Format(SMacroArgumentsSyntaxError, [FName]);
    Exit;
  end;

 // parse Macro arguments
  if P[0] = '(' then
  begin
    FHasBrackets := True;

    F := P + 1;
    ArgCount := CountCharsStop(',', ')', F) + 1;
    SetLength(FArguments, ArgCount);

    for i := 0 to ArgCount - 1 do
    begin
      while not (P[0] in [#0, ',', ')']) do Inc(P);
      SetTrimString(FArguments[i], F, P - F);
{      SetString(FArguments[i], F, P - F);
      FArguments[i] := Trim(FArguments[i]); // trim it}
      Inc(P); // next char
      F := P;

     // test for valid identifier
      if not IsValidIdent(FArguments[i]) then
      begin
        ErrorMsg := Format(SMacroArgumentsSyntaxError, [FArguments[i]]);
        Exit;
      end;
    end;

   // only one argument which is empty -> free memory
    if (ArgCount = 1) and (Length(FArguments[0]) = 0) then
      SetLength(FArguments, 0);
    if P[0] = ')' then Inc(P);
  end;

  Result := True;

  if P[0] = #0 then Exit;
  while (P[0] <> #0) and (P[0] <= #32) do Inc(P);
  FReplacement := TrimRight(P);
end;

{ TMacroList }

constructor TMacroList.Create(Macros: TMacros);
begin
  inherited Create;
  FMacros := Macros;
end;

procedure TMacroList.Assign(MacroList: TMacroList);
var
  i: Integer;
  Item: TMacroItem;
begin
  Clear;
  for i := 0 to MacroList.Count - 1 do
  begin
    Item := TMacroItem.Create(Self);
    Item.Assign(MacroList.Items[i]);
    Add(Item);
    MakeStringHash(Item.Name, Integer(Item), FHashTable);
  end;
end;

function TMacroList.GetItems(Index: Integer): TMacroItem;
begin
  Result := TMacroItem(inherited Items[Index]);
end;

function TMacroList.IndexOfMacro(const Name: string): Integer;
begin
  for Result := 0 to Count - 1 do
    if FMacros.FCompare(TMacroItem(inherited Items[Result]).Name, Name) = 0 then
      Exit;
  Result := -1;
end;

function TMacroList.FindMacro(const Name: string): TMacroItem;
{$ifndef HASHTABLE}
var
  i: Integer;
  cmp: TMacroCompare;
{$endif}
begin
{$ifdef HASHTABLE}
  Result := TMacroItem(FindStringHash(Name, FHashTable, FMacros.FCaseSensitive));
{$else}
  cmp := FMacros.FCompare;
  for i := 0 to Count - 1 do
  begin
    Result := TMacroItem(inherited Items[i]);
    if cmp(Result.Name, Name) = 0 then Exit;
  end;
  Result := nil;
{$endif}
end;

function TMacroList.IsMacroRegistered(const Name: string): Boolean;
begin
{$ifdef HASHTABLE}
  Result := FindStringHash(Name, FHashTable, FMacros.FCaseSensitive) <> 0;
{$else}
  Result := IndexOfMacro(Name) >= 0;
{$endif}
end;

{ RegisterMacro() registers a new macro. If the macro is already registered and
  the new version is different then the preprocessor warns the user. This
  warning is generated by the caller who test the property ErrorMsg.
  The Macro-string must be left trimmed. }
function TMacroList.RegisterMacro(const Macro: string; AInterfaceMacro: Boolean): TMacroItem;
var Item: TMacroItem;
begin
  FMacros.FErrorMsg := '';

  Result := TMacroItem.Create(Self);
  try
    if not Result.Parse(Macro, FMacros.FErrorMsg, AInterfaceMacro) then
    begin
      Result.Free;
      Result := nil;
      Exit;
    end;

    Item := FindMacro(Result.Name);
    if Item <> nil then
    begin
     // test if it is the same declaration
      if not Result.IsEqual(Item) then
      begin
        FMacros.FErrorMsg := Format(SMacroRedefinitionNotIdentical, [Result.Name]);
{$ifdef HASHTABLE}
        DelStringHash(Integer(Item), FHashTable);
{$endif}
        Remove(Item); // replace macro with the new one
      end;
    end;
{$ifdef HASHTABLE}
    MakeStringHash(Result.Name, Integer(Result), FHashTable);
{$endif}

    Add(Result);
  except
    Result.Free;
    raise;
  end;
end;

procedure TMacroList.UnregisterMacro(const Name: string);
{$ifdef HASHTABLE}
var Item: TMacroItem;
begin
  Item := TMacroItem(FindStringHash(Name, FHashTable, FMacros.FCaseSensitive));
  if Item <> nil then
  begin
    DelStringHash(Integer(Item), FHashTable);
    Delete(IndexOf(Item));
  end;
end;
{$else}
var Index: Integer;
begin
  Index := IndexOfMacro(Name);
  if Index >= 0 then
  begin
    DelStringHash(Integer(Items[Index]), FHashTable);
    Delete(Index);
  end;
end;
{$endif}

procedure TMacroList.Clear;
begin
{$ifdef HASHTABLE}
  SetLength(FHashTable, 0);
{$endif}
  inherited Clear;
end;

{ TMacros }

constructor TMacros.Create(AFileSys: IMacroFileSys);
begin
  inherited Create(Self);
  FUnits := TStringList.Create;
  FIncludeFiles := TStringList.Create;
  FMacroMacroRecursion := TList.Create;
  SetCaseSensitive(True);

  FConditionals := TStringList.Create;
  FCompilerOptions := TStringList.Create;
  FConditionalParseCode := TBooleanList.Create;
  FConditionalParse := False;

  FFileSys := AFileSys;
end;

destructor TMacros.Destroy;
begin
  FConditionalParseCode.Free;
  FCompilerOptions.Free;
  FConditionals.Free;
  FMacroMacroRecursion.Free;
  FIncludeFiles.Free;
  FUnits.Free;

  FFileSys := nil;
  inherited Destroy;
end;

procedure TMacros.SetCaseSensitive(const Value: Boolean);
begin
  FCaseSensitive := Value;
  if FCaseSensitive then FCompare := CompareStr else FCompare := CompareText;
end;

procedure TMacros.Error(const Msg, FileName: string; LineNum: Integer);
begin
  if Assigned(FOnError) then FOnError(Self, FileName, Msg, LineNum);
  Abort; // raise EAbort Exception -> exit all
end;

procedure TMacros.Error(const Msg: string; Token: PTokenInfo);
var
  Filename: string;
begin
  if Token = nil then
    Error(Msg, '', 0)
  else
  begin
    if Token.pFilename <> nil then
      Filename := Token.pFilename^;
    Error(Msg, FileName, Token.StartLine);
  end;
end;

procedure TMacros.Warning(const Msg, FileName: string; LineNum: Integer);
begin
  if Assigned(FOnError) then FOnWarning(Self, FileName, Msg, LineNum);
end;

procedure TMacros.Warning(const Msg: string; Token: PTokenInfo);
begin
  Warning(Msg, Token.pFileName^, Token.StartLine);
end;

procedure TMacros.PredefineMacros;
begin
  if Assigned(FOnPredefineMacros) then FOnPredefineMacros(Self);
end;

{ RegisterMacroByToken() registers a new macro and triggers error and warning
  messages if necessary. }
function TMacros.RegisterMacroByToken(const Macro: string; Token: PTokenInfo): TMacroItem;
begin
  Result := RegisterMacro(TrimLeft(Macro),
                          TPascalParserEx(Token^.Parser).NoReplaceMacros);  // sets ErrorMsg to '' or error message
  if (ErrorMsg <> '') then
  begin
    if Result = nil then Error(FErrorMsg, Token); // Abort
    Warning(FErrorMsg, Token);
    FErrorMsg := '';
  end;
end;

{ Parse() calls for every unit that it finds the ParseFile() method. }
function TMacros.Parse(const FileName: string; OnlyThisFile: Boolean): Boolean;
var i: Integer;
begin
  Result := True;
  FUnits.Clear;
  FIncludeFiles.Clear;
  FAppType := '';

  FUnits.AddObject(FileName, Pointer(0)); // Even if this file is no unit something has to be parsed
  try
   // parse all used units
    i := 0;
    while i < FUnits.Count do
    begin
      Clear; // clear macro list

      FMacroMacroRecursion.Clear; // reset macro macro recursion
      FFileRecursion := 0;        // reset file recursions

     // reset conditionals
      FConditionals.Clear;
      FConditionalParseCode.Clear; // Count=0 -> .LastItem=True
      DefaultConditionals;

     // get user predefined macros
      PredefineMacros;

     // parse macro include file for this unit
      ParseUnitMacroFile(i);
     // parse this unit
      ParseFile(FUnits[i], ptUnit, {TestFileExistence:=}False);

      Inc(i); // next unit

      if OnlyThisFile then Break; // do not parse other units
    end;
  except
    on EAbort do
      Result := False; // return False
  end;
end;


{ ParseUnitMacroFile() first checks if the macro file exists and then parses
  the file. }
function TMacros.ParseUnitMacroFile(UnitIndex: Integer): Integer;
var MacroFilename: string;
begin
 // Assert(UnitIndex >= 0);
  // Objects[]:
  //   -1: no macros file existence tested
  //    0: no macros file
  //    1: macros file exists
  Result := Integer(FUnits.Objects[UnitIndex]);

  case Result of
    -1:
      begin
        Result := 0;
        MacroFilename := ChangeFileExt(FUnits[UnitIndex], SMacroIncludeFileExt);
        if FFileSys.FileExists(MacroFilename) then
          if ParseFile(MacroFileName, ptInterfaceMacros,
                       {TestFileExistence:=}False) <> '' then
            Result := 1;
        FUnits.Objects[UnitIndex] := Pointer(Result);
      end;
    1:
      begin
        MacroFilename := ChangeFileExt(FUnits[UnitIndex], SMacroIncludeFileExt);
        ParseFile(MacroFileName, ptInterfaceMacros, {TestFileExistence:=}False);
      end;
  end;
end;


{ ParseFile() first checks for the existence of the file and then it calls
  ParseString() with the file content. If the file is an include file (called
  by ParseComments() ) then the file name is added to the FIncFiles list.
  The modified files are saved a .i.pas or .iX.* files (for more information
  see Utils.GetPreProcessedFilename() ). }
function TMacros.ParseFile(Filename: string; ParseType: TParseType;
  TestFileExistence: Boolean): string;
var
  S: string;
  Modified: Boolean;
  Index, IncludeIndex: Integer;
begin
  Result := '';

  Inc(FFileRecursion);
  try
    if FFileRecursion > MaxFileRecursion then // too many open files
      Error(SToManyRecursions, Filename, 1);

   // find the file
    if TestFileExistence then
    begin
      if not FFileSys.FileExists(Filename) then
      begin
        Filename := FFileSys.FindFile(Filename, ParseType = ptInclude); // try to get the file name
        if Length(Filename) = 0 then Exit;
      end;
    end;

    Modified := False;
    Result := Filename;
    if ParseType <> ptInterfaceMacros then
      FFileSys.BeforeFile(Filename, ParseType = ptInclude);
    try
      FFileSys.LoadFile(Filename, S, ParseType = ptInclude);  // load file into a string

      if S <> '' then
      begin
       // parse file and replace macros
        Modified := ParseString(S, Filename, {StartLinenum:=}1, ParseType);

        if ParseType = ptInterfaceMacros then
          Modified := False; // only read these files, do not write these files

        if (Modified) then
        begin
          if ParseType = ptInclude then
          begin
           // change file extension to .i1.*, .i2.* and so on for include files
            Index := IndexOfFilename(FIncludeFiles, Filename);
            if Index = -1 then
            begin
              FIncludeFiles.AddObject(Filename, Pointer(1));
              IncludeIndex := 1;
            end
            else
            begin
              IncludeIndex := Integer(FIncludeFiles.Objects[Index]) + 1;
              FIncludeFiles.Objects[Index] := Pointer(IncludeIndex);
            end;
          end
          else
            IncludeIndex := 0; // -> .i.pas

          Result := GetPreProcessedFilename(Filename, IncludeIndex);
          FFileSys.SaveFile(Filename, Result, S, ParseType = ptInclude); // save string to file
        end;
      end;

    finally
      if ParseType <> ptInterfaceMacros then
        FFileSys.AfterFile(Filename, Result, ParseType = ptInclude, Modified);
    end;
  finally
    Dec(FFileRecursion);
  end;
end;


{ ParseString() is the main parsing method. It is called by ParseComment() and
  ParseFile(). There is no interesting stuff in this function. }
function TMacros.ParseString(var Text: string; const Filename: string;
  StartLineNum: Integer; ParseType: TParseType): Boolean;
var
  Parser: TPascalParserEx;
  Token: PTokenInfo;
begin
  Parser := TPascalParserEx.Create(Filename, Text, StartLineNum);
  try
    Parser.NoReplaceMacros := ParseType = ptInterfaceMacros;
    while NextToken(Parser, Token) do // NextToken() replaces macros
    begin
      if Token.Kind = tkComment then
      begin
       // test for macro related comments
        ParseComment(Token); // FConditionalParseCode is tested in ParseComment()
      end
      else if Token.Kind = tkIdent then
      begin
        if FConditionalParse and not FConditionalParseCode.Last then // still in a "false condition"
          Continue;

        if (ParseType = ptInterfaceMacros) then
        begin
          if (SameText(Token.Value, 'implementation')) then
            Break; // do not parse any implementation macros
        end
        else
        begin
          if (SameText(Token.Value, 'uses')) or (SameText(Token.Value, 'contains')) then
            ParseUsesIdent(Parser); // parse USES statement
        end;
      end;
    end;
  finally
    Result := Parser.Modified;
    if Result then Text := Parser.Text; // replace Text
    Parser.Free;
  end;
end;


{ NextToken() returns the next token. If there is no further token the result
  value is FALSE else TRUE and the Token argument is NIL.
  For every token which is a macro name the ReplaceMacros() method is called. }
function TMacros.NextToken(Parser: TPascalParserEx; out Token: PTokenInfo): Boolean;
var
  Item: TMacroItem;
  Replacement: string;
begin
  if (Parser.NoReplaceMacros) or (FConditionalParse and not FConditionalParseCode.Last) then
  begin
   // just collect macros
    Result := Parser.GetToken(Token);
    Exit;
  end;
 // replace macros
  repeat
    Result := Parser.GetToken(Token);
    if (Result) and (Token.Kind = tkIdent) then
    begin
      if (Token.Value[1] = SBuiltInStartChars[1]) and
         (Token.Value[2] = SBuiltInStartChars[2]) and // may be point to #0 but this is no problem
         (BuiltInMacro(Token, Replacement)) then
      begin
       // built in simple macros
        Parser.ReplaceParseNext(Token, Token, Replacement);
      end
      else
      begin
        Item := FindMacro(Token.Value);
        if (Item <> nil) then
          ReplaceMacro(Parser, Item)
        else
          Break; // no macro -> return token
      end;
    end
    else
      Break; // no macro -> return token
  until False;
end;


{ NextToken(): no comment }
function TMacros.NextToken(Parser: TPascalParserEx): PTokenInfo;
begin
  NextToken(Parser, Result);
end;


{ ParseConditionals() parses all {$... compiler directives. It also interprets
  the $IFDEF, $IFNDEF, $ELSE, $ENDIF and $APPTYPE directives.

  $DEFINE und $UNDEF are directly handled by ParseComment().
}
function TMacros.ParseConditionals(var Line: string; const Filename: string;
  StartLineNum: Integer): Boolean;
type
  TConditionalWordType = (cwNone,
    cwIfdef, cwIfndef, {cwIfopt,} cwElse,
    cwEndif, {cwIf, cwElseif, cwIfend,} cwAppType
  );
const
  ConditionalWords: array[TConditionalWordType] of string = (
    '',
    'ifdef', 'ifndef', {'ifopt',}
    'else', 'endif',
    {'if', 'elseif', 'ifend'}
    'apptype'
  );
  procedure SyntaxError;
  begin
    Error(SConditionalSyntaxError, Filename, StartLineNum);
  end;

var
  Parser: TPascalParserEx;
  Token: PTokenInfo;
  ConditionalWord, Found: TConditionalWordType;
  Item: TMacroItem;
begin
  System.Delete(Line, 1, 1); // remove '$'
  if (Length(Line) = 0) or (Line[1] <= ' ') then SyntaxError;

  Parser := TPascalParserEx.Create(Filename, Line, StartLineNum);
  try
    if (not Parser.GetToken(Token)) or (Token.Kind <> tkIdent) then
      Token.Value := '';

    ConditionalWord := cwNone;
    for Found := Low(TConditionalWordType) to High(TConditionalWordType) do
      if SameText(Token.Value, ConditionalWords[Found]) then
      begin
        ConditionalWord := Found;
        Break;
      end;

    case ConditionalWord of
      cwIfdef, cwIfndef:
        begin
          if not FConditionalParse or FConditionalParseCode.Last then // can parse these line
          begin
            Token := Parser.GetToken;
            if Token = nil then SyntaxError;

            Item := FindMacro(Token.Value);
            case ConditionalWord of
              cwIfdef:
                begin
                  if (Item <> nil) and (Item.InterfaceMacro) then
                  begin
                   // replace the token by 'PREPROCESSOR'
                    FConditionalParseCode.Add(True);
                    Parser.ReplaceParseNext(Token, Token, 'PREPROCESSOR');
                  end
                  else
                    FConditionalParseCode.Add(IsDefined(Token.Value));
                end;

              cwIfndef:
                begin
                  if (Item <> nil) and (Item.InterfaceMacro) then
                  begin
                   // replace the token by 'NEVER_DEFINED'
                    FConditionalParseCode.Add(False);
                    Parser.ReplaceParseNext(Token, Token, 'NEVER_DEFINED');
                  end
                  else
                    FConditionalParseCode.Add(not IsDefined(Token.Value));
                end;
            end;
          end;
        end; // cwDefine, cwUndefine, cwIfdef, cwIfndef
      cwElse: FConditionalParseCode.ToggleLast;
      cwEndif: FConditionalParseCode.DeleteLast;
      cwAppType:
        begin
          if not FConditionalParse or FConditionalParseCode.Last then // can parse these line
          begin
            if (NextToken(Parser, Token)) and (Token.Kind = tkIdent) then
            begin
              FAppType := Token.Value;
              if SameText(FAppType, 'CONSOLE') then Define('CONSOLE')
                                               else Undefine('CONSOLE');
            end;
          end;
        end; // cwAppType

      else
        if not FConditionalParse or FConditionalParseCode.Last then // can parse these line
         // parse and replace macros
          while NextToken(Parser, Token) do ; // replace macros
    end; // case
  finally
    Result := Parser.Modified;
    if Result then Line := '$' + Parser.Text;
    Parser.Free;
  end;
end;


{ ParseComment() parses all comment tokens. Single line comments (//) are
  ignored. It registers all found MACRO statments. For include files $I and
  $INCLUDE the ParseFile() method is called. After parsing the include file
  the $I/$INCLUDE statment is replaced by the new filename returned by
  ParseFile().

  For compiler directives and conditional compilation macros are replaced. }
function TMacros.ParseComment(Token: PTokenInfo): Boolean;
var
  ps, BracketCount, ri, Len: Integer;
  Item: TMacroItem;
  s, Filename: string;
  IsCompilerDirective: Boolean;
begin
  Result := True;
  s := Token.Value;
  if s[1] = '/' then Exit; // single line comment are not parsed

 // remove comment brackets
  if s[1] = '(' then BracketCount := 2 else BracketCount := 1;
  System.Delete(s, 1, BracketCount);
  System.Delete(s, Length(s) - BracketCount + 1, BracketCount);
  if Pointer(s) = nil then Exit; // <==> if Length(s) = 0 then Exit;

  IsCompilerDirective := (s[1] = '$');

  if (IsCompilerDirective) and (not FConditionalParse or FConditionalParseCode.Last) and
     (StartsText(SMacroStartString, s)) then
  begin
   // register new macro
    System.Delete(s, 1, Length(SMacroStartString));
    Item := RegisterMacroByToken(s, Token);
    if Item <> nil then Define(Item.Name); // define also as conditional
  end

  else if (IsCompilerDirective) and
          (not FConditionalParse or FConditionalParseCode.Last) and
          (StartsText(SUnmacroStartString, s)) then
  begin
   // unregister macro
    System.Delete(s, 1, Length(SUnmacroStartString));
    s := Trim(s);
    UnregisterMacro(s);
    Undefine(s); // undefine conditional
  end

  else if (not IsCompilerDirective) and
          (not FConditionalParse or FConditionalParseCode.Last) and
          (s[1] in ['M', 'm']) and
          (StartsText(SMacroIncludeString, s)) then
  begin
   // parse macro include file

    System.Delete(s, 1, PosChar(' ', s));
    s := Trim(s);
    if s <> '' then
    begin
      if s[1] = '''' then
      begin
        System.Delete(s, 1, 1);
        System.Delete(s, Length(s), 1);
      end;

      FileName := ParseFile(s, ptInterfaceMacros, {TestFileExistence:=}True);
      if Filename = '' then
        Error(Format(SFindFile, [s]), Token);
    end;
  end

  else if (IsCompilerDirective) and
          ({not FConditionalParse or} FConditionalParseCode.Last) and
          ((StartsText('$I ', s)) or (StartsText('$INCLUDE ', s))) then
  begin
   // parse include file

    System.Delete(s, 1, PosChar(' ', s));
    s := Trim(s);
    if s <> '' then
    begin
      if s[1] = '''' then
      begin
        System.Delete(s, 1, 1);
        System.Delete(s, Length(s), 1);
      end;
     // parse include file
      if TPascalParserEx(Token.Parser).NoReplaceMacros then
        Filename := ParseFile(s, ptInterfaceMacros, {TestFileExistence:=}True)
      else
        Filename := ParseFile(s, ptInclude, {TestFileExistence:=}True);
      if Filename = '' then
        //Error(Format(SFindFile, [s]), Token);
        Exit;
      if ExtractFileName(Filename) = ExtractFileName(s) then Exit; // file was not modified so no file name change

      if TPascalParserEx(Token.Parser).NoReplaceMacros then Exit;

     // replace old filename by new one
      ps := Pos(s, Token.Value); // find file name start position
      s := Token.Value;
      ri := ps - 1;
      while (ri > 1) and (not (s[ri] in ['''', ' '])) do Dec(ri);
      Len := Length(s);
      while (ps < Len) and (not (s[ps] in [s[ri], '}', '*'])) do Inc(ps);
      if s[ps] <> '''' then Dec(ps);
      if s[ri] = ' ' then Inc(ri);
      System.Delete(s, ri, ps - ri + 1);
      System.Insert('''' + Filename + '''', s, ri);

      Token.Parser.ReplaceParseNext(Token, Token, s); // replace token
      Token.Parser.ClearCache; // clear cache
    end;

  end else if (IsCompilerDirective) then
  begin
    // compiler directive / conditional compilation

    if ParseConditionals(s, Token.pFilename^, Token.StartLine) then
    begin
      if BracketCount = 1 then s := '{' + s + '}'
                          else s := '(*' + s + '*)';
      Token.Parser.ReplaceParseNext(Token, Token, s); // replace token
      Token.Parser.ClearCache; // clear cache
    end;
  end;
end;


{ ParseUsesIdent() parses the USES statement and add all found units who's
  file exists to the FUnits-List. No duplicate files are added. }
procedure TMacros.ParseUsesIdent(Parser: TPascalParserEx);
var
  Token: PTokenInfo;
  s: string;
  UnitIndex: Integer;
begin
  while NextToken(Parser, Token) do // NextToken() replaces macros
  begin
    if Token.Kind = tkSymbol then
    begin
      if Token.Value = ';' then Break
      else if Token.Value = ',' then Continue;
    end

    else if Token.Kind = tkIdent then
    begin
      if SameText(Token.Value, 'in') then
      begin
        Token := NextToken(Parser); // NextToken() replaces macros
        s := RemoveQuotes(Token.Value);
      end else s := Token.Value + '.pas';

      s := FFileSys.FindFile(s, {IsInclude:=}False);
      if s <> '' then
      begin
        UnitIndex := IndexOfFilename(FUnits, s);
       // add this unit to the unit-parse-list.
        if UnitIndex = -1 then
          UnitIndex := FUnits.AddObject(s, Pointer(-1));

       // Include the macros from the interface section of the the new unit.
        ParseUnitMacroFile(UnitIndex);

       // Include the macro include file for the new unit.
        ParseUnitMacroFile(UnitIndex);
      end;
    end;
  end;
end;


{ GetReplacement() is called for "Function Macros" only. It replaces the
  arguments of the macro item with Args[] which is created in ReplaceMacros().
  All macros in Args[] are replaced when entering this method. }
function TMacros.GetReplacement(Item: TMacroItem; const Args: TStringDynArray;
  const Filename: string; StartLineNum: Integer): string;

  function GetArg(Token: PTokenInfo; var Arg: string): Boolean;
  var ArgIndex: Integer;
  begin
   // do not set Args to '' here
    Result := False;
    if (Token = nil) or (Token.Kind <> tkIdent) then Exit;

    ArgIndex := Item.IndexOfArg(Token.Value);
    if ArgIndex >= 0 then
    begin
      Arg := Args[ArgIndex];
      Result := True;
    end;
  end;

var
  Parser: TPascalParserEx;
  StartToken, Token: PTokenInfo;
begin
 // parsing "Macro Replacement" text
  Parser := TPascalParserEx.Create(Filename, Item.Replacement, StartLineNum);
  try
    while Parser.GetToken(Token) do
    begin

      if (Token.Kind = tkIdent) then
      begin
        if GetArg(Token, Token.Value) then
        begin
          StartToken := Token;
         // parse the new content but do not clear cache so it is possilbe
         // for '##' to get the string as identifier
          Parser.ReplaceParseNext(StartToken, Token, Token.Value);
        end;
      end


      else if (Token.Kind = tkSymbol) then
      begin
       // make string
        if Token.Value = '#' then
        begin
          StartToken := Token;
          if (not Parser.GetToken(Token)) then
            Error(Format(SMacroSytaxError, [SNoFurtherToken]), StartToken); // Abort
          if (not GetArg(Token, Token.Value)) then
            Error(Format(SMacroSytaxError, [SCanOnlyMakeStringFromArguments]), StartToken); // Abort

          Token.Value := '''' + Token.Value + '''';
          Parser.ReplaceParseNext(StartToken, Token, Token.Value);
         // parse the new content but do not clear cache so it is possilbe for
         // '##' to get the string as identifier
          Token.StartIndex := StartToken.StartIndex; // adjust tkIdent-token StartIndex becoming PreToken
        end


        else if (Token.Value = '##') then
        begin
          StartToken := Parser.PreToken;
          if (StartToken = nil) or
             (StartToken.Kind <> tkIdent) or
             (not Parser.GetToken(Token)) or
             (Token.Kind <> tkIdent) then  // changes <Token>
            Error(Format(SMacroSytaxError, [SCombineError]), Token); // Abort

          GetArg(Token, Token.Value); // get argument replacement if available

         // parse the new content but do not clear cache so it is possilbe for
         // '##' to get the string as identifier
          Parser.ReplaceParseNext(StartToken, Token, TrimRight(StartToken.Value) + TrimLeft(Token.Value));
          Token.StartIndex := StartToken.StartIndex; // adjust tkIdent-token StartIndex becoming PreToken
        end;
      end; // if Token.Kind = tkSymbol

    end; // while

    Result := Parser.Text;
  finally
    Parser.Free;
  end;
end;


{ ReplaceMacro() creates the argument array and replaces all array items by
  its macro(-function). For macro functions GetReplacement() is called. }
procedure TMacros.ReplaceMacro(Parser: TPascalParserEx; Item: TMacroItem);
var
  Token: PTokenInfo;
  ReplStartIndex, ReplEndIndex, LastCommaIndex: Integer;
  Replacement: string;
  BracketNum: Integer;
  Args: TStringDynArray;
  ArgIndex: Integer;
  AddedLines: Integer;
  EndLineNum: Integer;
begin
  if FMacroMacroRecursion.IndexOf(Item) >= 0 then Exit; // do not replace the macro with itself
  FMacroMacroRecursion.Add(Item);
  try
    Token := Parser.CurToken;
    ReplStartIndex := Token.StartIndex;
    ReplEndIndex := Token.EndIndex;
    EndLineNum := Token.EndLine;
    if Item.HasBrackets then
    begin
     // macro with arguments

     // is '(' the next token
      if (not Parser.GetToken(Token)) or (Token.Value <> '(') then
        Error(Format(SMacroSytaxError, [SNoArgumentSpecified]), Parser.PreToken); // Abort


     // Here we use NextToken(), because the arguments can also be macros and
     // NextToken() replaces them.

     // get macro arguments
      SetLength(Args, Length(Item.Arguments));
      ArgIndex := 0;
      LastCommaIndex := Token.StartIndex;
      BracketNum := 1;
      while NextToken(Parser, Token) do // NextToken() replaces macros
      begin
        EndLineNum := Token.EndLine;
        if Token.Value = '(' then Inc(BracketNum)
        else if Token.Value = ')' then
        begin
          Dec(BracketNum);
          if BracketNum = 0 then
          begin
            if Length(Args) > 0 then
            begin
              Args[ArgIndex] := Trim(Parser.GetPlainText(LastCommaIndex + 1, Token.StartIndex - 1)); // save last argument
              if Length(Args[ArgIndex]) = 0 then
                Error(Format(SMacroSytaxError, [SEmptyMacroArgument]), Token);
            end;
            Break; // last bracket
          end;
        end
        else if (BracketNum = 1) and (Token.Value = ',') then
        begin
          Args[ArgIndex] := Parser.GetPlainText(LastCommaIndex + 1, Token.StartIndex - 1);
          if IsStrEmpty(Args[ArgIndex]) then
            Error(Format(SMacroSytaxError, [SEmptyMacroArgument]), Token);

          LastCommaIndex := Token.StartIndex;
         // new argument
          Inc(ArgIndex);
          if ArgIndex >= Length(Args) then
            Error(SMacroSytaxError, Token);
        end;
      end;
      SetLength(Args, ArgIndex + 1); // set to correct length
      ReplEndIndex := Token.EndIndex; // new end index

     // check arguments
      if Length(Args) <> Length(Item.Arguments) then
        Error(Format(SMacroNotEnoughArguments, [Item.Name]), Token);

      Replacement := GetReplacement(Item, Args, Parser.Filename, Token.StartLine);
    end
    else
      Replacement := Item.Replacement; // just a simple replacement

   // parse replacement
    ParseString(Replacement, Parser.Filename, Parser.LineNum, ptInclude);
   // replace text
    Parser.ReplaceParseNext(ReplStartIndex, ReplEndIndex - ReplStartIndex + 1, Replacement);
    Parser.ClearCache; // new parse start and clear token cache

   // lines moved
    AddedLines := CountChars(#10, Replacement);
    if AddedLines > 0 then
      FFileSys.LinesMoved(Token^.Parser.Filename, EndLineNum, AddedLines);
  finally
    FMacroMacroRecursion.Remove(Item);
  end;
end;


function TMacros.BuiltInMacro(Token: PTokenInfo; var Replacement: string): Boolean;
begin
 // __LINE__
  if FCompare(Token.Value, SBuiltIn_Line) = 0 then
  begin
    Result := True;
    Replacement := IntToStr(Token.StartLine);
  end

 // __FILE__
  else if FCompare(Token.Value, SBuiltIn_File) = 0 then
  begin
    Result := True;
    Replacement := '''' + Token.pFilename^ + ''''
  end

 // __DATE__
  else if FCompare(Token.Value, SBuiltIn_Date) = 0 then
  begin
    Result := True;
    Replacement := '''' + DateToStr(Date) + ''''
  end

 // __TIME__
  else if FCompare(Token.Value, SBuiltIn_Time) = 0 then
  begin
    Result := True;
    Replacement := '''' + TimeToStr(Time) + ''''
  end

  else
  begin
    Result := False;
    if Assigned(FOnBuiltInMacro) then
    begin
      Replacement := '';
      FOnBuiltInMacro(Self, Token, Replacement, Result);
    end;
  end;
end;

procedure TMacros.DefaultConditionals;
begin
  Define('PREPROCESSOR'); // always defined
 //  Define('CONDITIONALEXPRESSIONS');  not supported

{$ifdef VER130}         Define('VER130'); {Delphi 5} {$endif}
{$ifdef VER140}         Define('VER140'); {Delphi 6} {$endif}
{$ifdef VER150}         Define('VER150'); {Delphi 7} {$endif}
{$ifdef VER160}         Define('VER160'); {Delphi 8} {$endif}

{$ifdef MSWINDOWS}      Define('MSWINDOWS'); {$endif}
{$ifdef WIN32}          Define('WIN32'); {$endif}
{$ifdef LINUX}          Define('LINUX'); {$endif}
{$ifdef CPU386}         Define('CPU386'); {$endif}

  if SameText(FAppType, 'CONSOLE') then
    Define('CONSOLE');

 // user defined
  if Assigned(FOnDefaultConditionals) then
    FOnDefaultConditionals(Self);

  Undefine('CONDITIONALEXPRESSIONS'); // not supported
end;

procedure TMacros.Define(const Condition: string);
begin
  if (Condition <> '') and (FConditionals.IndexOf(Condition) = -1) then
    FConditionals.Add(Condition);
end;

procedure TMacros.Undefine(const Condition: string);
var Index: Integer;
begin
  Index := FConditionals.IndexOf(Condition);
  if Index >= 0 then FConditionals.Delete(Index);
end;

function TMacros.IsDefined(const Condition: string): Boolean;
begin
  Result := (Condition <> '') and (FConditionals.IndexOf(Condition) >= 0);
end;

procedure TMacros.SetOption(const Option: string; Value: Boolean);
begin
{TODO set options for $ifopt, be carefull with $R+/- and $RANGECHECKS ON/OFF and so on}
end;

end.
