{-----------------------------------------------------------------------------
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/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: JvSAL.PAS, released on 2002-06-15.

The Initial Developer of the Original Code is Jan Verhoeven [jan1 dott verhoeven att wxs dott nl]
Portions created by Jan Verhoeven are Copyright (C) 2002 Jan Verhoeven.
All Rights Reserved.

Contributor(s): Robert Love [rlove att slcdug dott org].

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.delphi-jedi.org

Known Issues:
-----------------------------------------------------------------------------}
// $Id$

unit JvSAL;

{$I jvcl.inc}

interface

uses
  {$IFDEF UNITVERSIONING}
  JclUnitVersioning,
  {$ENDIF UNITVERSIONING}
  SysUtils, Classes, Windows, Messages, Graphics, Controls, Forms, Dialogs,
  JvSALHashList, JvStrings, JvComponentBase;

const
  StackLimit = 256;
  // message are processed every 250 milliseconds
  // use the stop procedure to stop a locked script
  TimeOut = 250;

type
  TOnGetUnitEvent = procedure(Sender: TObject; AUnit: string;
    var AValue: string; var Handled: Boolean) of object;

  TJvAtom = class(TObject)
  private
    FValue: Variant;
    FActor: TJvSALProc;
    procedure SetActor(const Value: TJvSALProc);
    procedure SetValue(const AValue: Variant);
  public
    property Value: Variant read FValue write SetValue;
    property Actor: TJvSALProc read FActor write SetActor;
  end;

  TJvSALProcAtom = class(TObject)
  private
    FParser: TJvSALProc;
    FActor: TJvSALProc;
    procedure SetActor(const Value: TJvSALProc);
    procedure SetParser(const Value: TJvSALProc);
  public
    property Actor: TJvSALProc read FActor write SetActor;
    property Parser: TJvSALProc read FParser write SetParser;
  end;

  TJvAtoms = class(TStringList)
  public
    procedure ClearAll;
    destructor Destroy; override;
  end;

  TStack = array [0..StackLimit] of Variant;
  TBStack = array [0..StackLimit] of Boolean;
  TRStack = array [0..StackLimit] of Integer;

  {$IFDEF RTL230_UP}
  [ComponentPlatformsAttribute(pidWin32 or pidWin64)]
  {$ENDIF RTL230_UP}
  TJvSAL = class(TJvComponent)
  private
    FStop: Boolean;
    FCaption: string;
    FSP: Integer;
    FRSP: Integer;
    FBSP: Integer;
    FStack: TStack;
    FBStack: TBStack;
    FRStack: TRStack;
    FProcs: TJvSALHashList;
    FScript: string;
    FUnits: TStringList;
    FTicks: cardinal;
    FOnGetUnit: TOnGetUnitEvent;
    FVariableName: string;
    FVariable: TJvAtom;
    FSelection: Variant;
    FUseDirective: string;
    FBeginOfComment: string;
    FEndOfComment: string;
    FStringDelimiter: string;
    FPC: Integer;
    FAtoms: TJvAtoms;
    FPCProc: Integer;
    FToken: string;
    procedure SetScript(const Value: string);
    procedure SetGetUnit(const Value: TOnGetUnitEvent);
    procedure SetVariable(const Value: TJvAtom);
    procedure SetVariableName(const Value: string);
    procedure SetSelection(const Value: Variant);
    procedure SetUseDirective(const Value: string);
    procedure SetBeginOfComment(const Value: string);
    procedure SetEndOfComment(const Value: string);
    procedure SetStringDelimiter(const Value: string);
    procedure SetPC(const Value: Integer);
    procedure SetToken(const Value: string);
    procedure SetCaption(const Value: string);
  protected
    procedure ParseScript;
    // return FStack methods
    // SAL language
    procedure xBoSub;
    procedure xEoSub;
    procedure xValue;
    procedure xDefVariable;
    procedure xVariable;
    procedure xProc;
    procedure xNoParser;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure ClearProcedures;
    procedure AddProcedure(AName: string; AProcedure, AParser: TJvSALProc);
    function APO(Op: string; AProc: TJvSALProc): Integer;
    procedure Push(AValue: Variant);
    function Pop: Variant;
    procedure RPush(AValue: Integer);
    function RPop: Integer;
    procedure BoolPush(AValue: Boolean);
    function BoolPop: Boolean;
    procedure LoadFromFile(FileName: string);
    procedure Execute;
    procedure Stop;
    property PC: Integer read FPC write SetPC;
    property Atoms: TJvAtoms read FAtoms;
    property PCProc: Integer read FPCProc;
    property Token: string read FToken write SetToken;
    property Script: string read FScript write SetScript;
    property Caption: string read FCaption write SetCaption;
    property Variable: TJvAtom read FVariable write SetVariable;
    property VariableName: string read FVariableName write SetVariableName;
    property TheSelect: Variant read FSelection write SetSelection;
    property UseDirective: string read FUseDirective write SetUseDirective;
    property BeginOfComment: string read FBeginOfComment write SetBeginOfComment;
    property EndOfComment: string read FEndOfComment write SetEndOfComment;
    property StringDelim: string read FStringDelimiter write SetStringDelimiter;
  published
    property OnGetUnit: TOnGetUnitEvent read FOnGetUnit write SetGetUnit;
  end;

{$IFDEF UNITVERSIONING}
const
  UnitVersioning: TUnitVersionInfo = (
    RCSfile: '$URL$';
    Revision: '$Revision$';
    Date: '$Date$';
    LogPath: 'JVCL\run'
  );
{$ENDIF UNITVERSIONING}

implementation

uses
  JvConsts, JvResources, JvTypes;

const
  // do not localize these strings
  cSAL = 'SAL';
  cUse = 'use::';
  cLiteral = 'literal';
  cProc = 'proc-';
  cEndProc = 'end-proc';
  cVar = 'var-';

//=== { TJvSAL } =============================================================

constructor TJvSAL.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAtoms := TJvAtoms.Create;
  FProcs := TJvSALHashList.Create(ITinyHash, HashSecondaryOne, SameText);
  FUnits := TStringList.Create;
  FCaption := cSAL;
  FUseDirective := cUse;
  FBeginOfComment := '{';
  FEndOfComment := '}';
  FStringDelimiter := '"';
end;

destructor TJvSAL.Destroy;
begin
  FAtoms.Free;
  FProcs.Free;
  FUnits.Free;
  inherited Destroy;
end;

function TJvSAL.BoolPop: Boolean;
begin
  Dec(FBSP);
  if FBSP < 0 then
    raise EJVCLException.CreateRes(@RsEBooleanStackUnderflow);
  Result := FBStack[FBSP];
end;

procedure TJvSAL.BoolPush(AValue: Boolean);
begin
  FBStack[FBSP] := AValue;
  Inc(FBSP);
  if FBSP > StackLimit then
    raise EJVCLException.CreateRes(@RsEBooleanStackOverflow);
end;

procedure TJvSAL.Execute;
var
  A: TJvAtom;
  C: Integer;
begin
  PC := 0;
  FSP := 0;
  FRSP := 0;
  FBSP := 0;
  C := Atoms.Count;
  FStop := False;
  FTicks := GetTickCount;
  if C = 0 then
    Exit;
  repeat
    A := TJvAtom(Atoms.Objects[PC]);
    FPCProc := PC;
    Inc(FPC);
    A.Actor;
    if (GetTickCount - FTicks) > TimeOut then
    begin
      FTicks := GetTickCount;
      Application.ProcessMessages;
    end;
    if FStop then
      raise EJVCLException.CreateRes(@RsEProgramStopped);
  until PC >= C;
end;

procedure TJvSAL.ParseScript;
var
  S: string;
  //  iprocs: Integer;
  haveproc: Boolean;
  AActor: TJvSALProc;
  AParser: TJvSALProc;
  I, P, P2: Integer;
  fv: Double;
  A: TJvAtom;
  fn, TheUnit: string;
  Handled: Boolean;

  function CharFrom(From: Integer; AChar: Char; AText: string): Integer;
  var
    C: Integer;
  begin
    Result := 0;
    C := Length(AText);
    repeat
      if AText[From] = AChar then
      begin
        Result := From;
        Exit;
      end;
      Inc(From);
    until From > C;
  end;

begin
  PC := 1;
  S := FScript;
  FUnits.Clear;
  // process any includes
  repeat
    P := Pos(FUseDirective, S); // default use::
    if P > 0 then
    begin
      P2 := CharFrom(P, ' ', S);
      if P2 = 0 then
        raise EJVCLException.CreateResFmt(@RsEUnterminatedIncludeDirectiveNears, [Copy(S, P, 50)]);
      fn := Trim(Copy(S, P + Length(FUseDirective), P2 - P - Length(FUseDirective)));
      if not Assigned(FOnGetUnit) then
        raise EJVCLException.CreateRes(@RsEOngetUnitEventHandlerIsNotAssigned);
      Handled := False;
      fn := LowerCase(fn);
      if FUnits.IndexOf(fn) = -1 then
      begin
        OnGetUnit(Self, fn, TheUnit, Handled);
        if not Handled then
          raise EJVCLException.CreateResFmt(@RsECouldNotIncludeUnits, [fn]);
        TheUnit := StringReplace(TheUnit, Cr, ' ', [rfReplaceAll]);
        Delete(S, P, P2 - P);
        Insert(TheUnit, S, P);
        FUnits.Append(fn);
      end;
    end;
  until P = 0;

  while S <> '' do
  begin
    if Pos(FBeginOfComment, S) = 1 then
    begin // default= {
      P := Pos(FEndOfComment, S); // default= }
      if P = 0 then
        raise EJVCLException.CreateResFmt(@RsEUnterminatedCommentNears, [S]);
      Delete(S, 1, P + Length(FEndOfComment) - 1);
      S := Trim(S);
    end
    else
    if Pos(FStringDelimiter, S) = 1 then
    begin // default = "
      Delete(S, 1, Length(FStringDelimiter));
      P := Pos(FStringDelimiter, S);
      if P = 0 then
        raise EJVCLException.CreateResFmt(@RsEUnterminatedStringNears, [S]);
      Token := Copy(S, 1, P - 1);
      Delete(S, 1, P + Length(FStringDelimiter) - 1);
      S := Trim(S);
      A := TJvAtom.Create;
      A.Value := Token;
      A.Actor := xValue;
      Atoms.AddObject(cLiteral, A);
    end
    else
    begin
      P := Pos(' ', S);
      if P = 0 then
      begin
        Token := S;
        S := '';
      end
      else
      begin
        Token := Copy(S, 1, P - 1);
        Delete(S, 1, P);
        S := Trim(S);
      end;
      // take care of aliases
      if Token = '.' then
        Token := '+=';
      // check for user procs
      haveproc := FProcs.Hash(Token, AActor, AParser);
      try // float
        fv := StrToFloat(Token);
        A := TJvAtom.Create;
        A.Value := fv;
        A.Actor := xValue;
        Atoms.AddObject(cLiteral, A);
      except
        if Pos(cProc, Token) = 1 then
        begin // begin of procedure
          if Pos(cEndProc, S) = 0 then
            raise EJVCLException.CreateResFmt(@RsEUnterminatedProcedureNears, [S]);
          APO(Token, xBoSub);
        end
        else
        if Token = cEndProc then
          APO(Token, xEoSub)
        else
        if Copy(Token, Length(Token) - 1, 2) = '()' then
          APO(Token, xProc) // proc call
        else
        if Pos(cVar, Token) = 1 then
        begin // define variable
          if Atoms.IndexOf(Token) <> -1 then
            raise EJVCLException.CreateResFmt(@RsEVariablesAllreadyDefineds, [Token, S]);
          A := TJvAtom.Create;
          A.Actor := xDefVariable;
          Atoms.AddObject(Token, A);
        end
        else
        if Token[1] = '$' then
        begin // variable value
          // find address
          I := Atoms.IndexOf(cVar + Copy(Token, 2, MaxInt));
          if I = -1 then
            raise EJVCLException.CreateResFmt(@RsEVariablesIsNotYetDefineds, [Token, S]);
          A := TJvAtom.Create;
          A.Value := I;
          A.Actor := xVariable;
          Atoms.AddObject(Token, A);
        end
        else
        if haveproc then
        begin
          if Assigned(AParser) then
            AParser
          else
            APO(Token, AActor);
        end
        else
          raise EJVCLException.CreateResFmt(@RsEProceduresNears, [Token, S]);
      end
    end
  end;

  // now resolve procs()
  if Atoms.Count = 0 then
    Exit;
  for I := 0 to Atoms.Count - 1 do
  begin
    S := Atoms[I];
    if Copy(S, Length(S) - 1, 2) = '()' then
    begin
      S := cProc + Copy(S, 1, Length(S) - 2);
      P := Atoms.IndexOf(S);
      if P = -1 then
        raise EJVCLException.CreateResFmt(@RsEUndefinedProcedures, [S]);
      TJvAtom(Atoms.Objects[I]).Value := P;
    end;
  end;
end;

function TJvSAL.Pop: Variant;
begin
  Dec(FSP);
  if FSP < 0 then
    raise EJVCLException.CreateRes(@RsEStackUnderflow);
  Result := FStack[FSP];
end;

procedure TJvSAL.Push(AValue: Variant);
begin
  FStack[FSP] := AValue;
  Inc(FSP);
  if FSP > StackLimit then
    raise EJVCLException.CreateRes(@RsEStackOverflow);
end;

procedure TJvSAL.SetScript(const Value: string);
begin
  FScript := Trim(StringReplace(Value, Cr, ' ', [rfReplaceAll]));
  Atoms.ClearAll;
  ParseScript;
end;

procedure TJvSAL.xDefVariable;
var
  A: TJvAtom;
begin
  A := TJvAtom(Atoms.Objects[PCProc]);
  FVariableName := Atoms[PCProc];
  FVariableName := '$' + Copy(FVariableName, 5, MaxInt);
  FVariable := A;
end;

procedure TJvSAL.xValue;
begin
  Push(TJvAtom(Atoms.Objects[PCProc]).Value);
end;

procedure TJvSAL.xVariable;
var
  Index: Integer;
  A: TJvAtom;
begin
  A := TJvAtom(Atoms.Objects[PCProc]);
  VariableName := Atoms[PCProc];
  Index := A.Value;
  Variable := TJvAtom(Atoms.Objects[Index]);
end;

procedure TJvSAL.Stop;
begin
  FStop := True;
end;

procedure TJvSAL.LoadFromFile(FileName: string);
begin
  Script := Loadstring(FileName);
end;

procedure TJvSAL.ClearProcedures;
begin
  //  FProcs.ClearAll;
  FProcs.Clear;
end;

procedure TJvSAL.AddProcedure(AName: string; AProcedure, AParser: TJvSALProc);
//var
//  A: TJvSALProcAtom;
begin
  //  A:=TJvSALProcAtom.Create;
  //  A.Actor:=AProcedure;
  //  A.Parser:=AParser;
  //  FProcs.AddObject(AName,A);
  FProcs.AddString(AName, AProcedure, AParser);
end;

function TJvSAL.RPop: Integer;
begin
  Dec(FRSP);
  if FRSP < 0 then
    raise EJVCLException.CreateRes(@RsEReturnStackUnderflow);
  Result := FRStack[FRSP];
end;

procedure TJvSAL.RPush(AValue: Integer);
begin
  FRStack[FRSP] := AValue;
  Inc(FRSP);
  if FRSP > StackLimit then
    raise EJVCLException.CreateRes(@RsEReturnStackOverflow);
end;

// end of subroutine, marked with end-proc

procedure TJvSAL.xEoSub;
begin
  PC := RPop;
end;

// begin of subroutine, marked with [
// loop to ]

procedure TJvSAL.xBoSub;
var
  Op: string;
  C: Integer;
begin
  C := Atoms.Count;
  repeat
    Op := Atoms[PC];
    Inc(FPC);
    if Op = cEndProc then
      Exit;
  until PC >= C;
  raise EJVCLException.CreateRes(@RsECouldNotFindEndOfProcedure);
end;

procedure TJvSAL.SetGetUnit(const Value: TOnGetUnitEvent);
begin
  FOnGetUnit := Value;
end;

// function call

procedure TJvSAL.xProc;
var
  Index: Integer;
begin
  Index := TJvAtom(Atoms.Objects[PCProc]).Value;
  RPush(PC);
  PC := Index + 1;
end;

procedure TJvSAL.SetVariable(const Value: TJvAtom);
begin
  FVariable := Value;
end;

procedure TJvSAL.SetVariableName(const Value: string);
begin
  FVariableName := Value;
end;

procedure TJvSAL.SetSelection(const Value: Variant);
begin
  FSelection := Value;
end;

procedure TJvSAL.SetUseDirective(const Value: string);
begin
  FUseDirective := Value;
end;

procedure TJvSAL.SetBeginOfComment(const Value: string);
begin
  FBeginOfComment := Value;
end;

procedure TJvSAL.SetEndOfComment(const Value: string);
begin
  FEndOfComment := Value;
end;

procedure TJvSAL.SetStringDelimiter(const Value: string);
begin
  FStringDelimiter := Value;
end;

procedure TJvSAL.SetPC(const Value: Integer);
begin
  FPC := Value;
end;

function TJvSAL.APO(Op: string; AProc: TJvSALProc): Integer;
var
  A: TJvAtom;
begin
  A := TJvAtom.Create;
  A.Actor := AProc;
  Result := Atoms.AddObject(Op, A);
end;

procedure TJvSAL.SetToken(const Value: string);
begin
  FToken := Value;
end;

procedure TJvSAL.SetCaption(const Value: string);
begin
  FCaption := Value;
end;

procedure TJvSAL.xNoParser;
begin
  // do nothing
end;

//=== { TJvAtom } ============================================================

procedure TJvAtom.SetActor(const Value: TJvSALProc);
begin
  FActor := Value;
end;

procedure TJvAtom.SetValue(const AValue: Variant);
begin
  FValue := AValue;
end;

//=== { TJvAtoms } ===========================================================

destructor TJvAtoms.Destroy;
begin
  ClearAll;
  inherited Destroy;
end;

procedure TJvAtoms.ClearAll;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    TJvAtom(Objects[I]).Free;
  Clear;
end;

//=== { TJvSALProcAtom } =====================================================

procedure TJvSALProcAtom.SetActor(const Value: TJvSALProc);
begin
  FActor := Value;
end;

procedure TJvSALProcAtom.SetParser(const Value: TJvSALProc);
begin
  FParser := Value;
end;

{$IFDEF UNITVERSIONING}
initialization
  RegisterUnitVersion(HInstance, UnitVersioning);

finalization
  UnregisterUnitVersion(HInstance);
{$ENDIF UNITVERSIONING}

end.
