-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathexec.pas
More file actions
3001 lines (2790 loc) · 89.9 KB
/
exec.pas
File metadata and controls
3001 lines (2790 loc) · 89.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{******************************************************************************
Plan9Basic Interpreter Engine
MIT License
Copyright (c) 2026 André Murta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************}
unit exec;
interface
uses
System.Classes, System.SysUtils, System.Character, System.Generics.Collections,
System.TypInfo, System.Math, System.Diagnostics, System.UITypes, System.SyncObjs,
FMX.Types, FMX.Platform, FMX.Controls, FMX.Forms, FMX.Graphics,
FMX.Dialogs, FMX.DialogService, FMX.DialogService.Async,
UnitUtils, lexer, UnitGC;
const
// Stack and memory limits
MAXSTACK = 16384; // Maximum stack items (both main and auxiliary)
MAXLOCALS = 259; // (256 args && locals) + 3 local registers (@3 @4 @5)
MAXVARS = 515; // 512 global vars + 3 generic registers (@0 @1 @2)
INITASMSIZE = 1000; // Asm program initial allocation size
// Default execution timeout in seconds (0 = no timeout)
DEFAULT_TIMEOUT = 30;
// Numeric limits for integer type
MAX_INTEGER_VALUE = 2147483647.0; // Values above this become float
// UI refresh throttling for PRINT statements (in milliseconds)
// Lower = more responsive but slower execution
// Higher = faster execution but less responsive UI
// 0 = refresh on every PRINT (original behavior)
DEFAULT_UI_REFRESH_INTERVAL = 50;
type
//Assembly tokens
TAsmToken = (
{A}
atkAdd, atkAddCRLFS, atkAddS, atkAnd, atkAssert,
{B}
atkBreak, atkBreakpoint, atkCRLF, atkCallFar, atkCallFarP, atkCallFarS, atkCallNear,
atkCls, atkComma, atkComment, atkContinue,
{C}
atkCaseEnd, atkCaseElse, atkCaseStart,
{D}
atkData, atkDataS, atkDiv, atkDoStart, atkDoUntil, atkDoWhile, atkDump,
{E}
atkElse, atkElseIfBody, atkElseIfTest, atkEnd, atkEndFunction, atkEndIf,
atkEndWhile, atkEq, atkEqs, atkErr, atkExit,
{F}
atkFloat, {atkFnAddress,} atkForCycle, atkFunction,
{G}
atkGe, atkGeS, atkGt, atkGtS,
{H}
{I}
atkIdentifier, atkIf, atkIndirectCall, atkInitFunc, atkInput, atkInputS,
atkInteger, atkInv,
{J}
atkJsonObj, atkJump,
{K}
{L}
atkLabel, atkLe, atkLeS, atkLoopEnd, atkLoopUntil, atkLoopWhile, atkLt, atkLtS,
{M}
atkMax, atkMin, atkMod, atkMul,
{N}
atkNe, atkNeS, atkNext, atkNone, atkNop, atkNot, atkNull,
{O}
atkOnCallFar, atkOnCallFarP, atkOnCallFarS, atkOnGoto, atkOnGosub, atkOr,
{P}
atkPause, atkPointerFunction, atkPointerIdentifier, atkPop, atkPopAux,
atkPopStore, atkPopStorePtr, atkPopStoreS, atkPopnCall, atkPopnJump, atkPopnJump_CRLF,
atkPopnJump_EndIf, atkPow, atkPrint, atkPush, atkPushAux, atkPushAuxS, atkPushAuxTOS,
atkPushC, atkPushCS, atkPushPtr, atkPushPtrTag, atkPushS,
{Q}
{R}
atkRead, atkReadS, atkRefreshRate, atkRepeat, atkRetFunction, atkReturn, atkRestore,
{S}
atkStrIdentifier, atkString, atkSub, atkSubS, atkSymbol,
{T}
atkTo, atkTrace, atkTraceOff, atkTraceOn,
{U}
atkUnknown, atkUntil, atkUnwatch,
{V}
{W}
atkWatch, atkWhile
{X}
{Y}
{Z}
);
//TAsmDataType = (dtNum, dtPtr, dtStr);
TAsmData = record //Data cell format
n: Extended;
p: Pointer;
s: String;
end;
//Type for functions that will be integrated into the basic engine
TBindFunction = function(var Args: Array of TAsmData): TAsmData;
TLinkFunction = record //Functions call type and entry point
FarCall: Boolean; //True: imported from Delphi / False: user defined
Entry: TBindFunction; //Function header if imported
end;
//The function signature is the dictionary index
TFunctionsDictionary = TDictionary<String, TLinkFunction>;
//Function type
TExeFunc = procedure of object;
//PRINT function type
TPrintProc = procedure(p: PChar) of object;
//BASIC tokenized instructions
TBasInstr = record
id: TBasToken; //Token type
pos, len: Integer; //position, length
n: Extended; //Numeric constant value
end;
TBasArray = array of TBasInstr;
//Assembly tokenized instructions
TInstr = record
proc: TExeFunc; //Delphi function associated to the ASM instruction
token: TAsmToken; //The token identification
i: Integer; //string offset, variable index
n: Extended; //Numeric constant value
end;
TInstrArray = array of TInstr;
//Type used by the DATA/READ statements
TDataItem = record
DataType: AnsiChar; //String '$' or numeric 'n'
DataPos: Integer; //Position at the code
end;
TDataItems = TList<TDataItem>;
//Token identification
TStringToken = record
Str: String; //Textual representation of the token
Token: TAsmToken; //Token 'id'
end;
TStringTokens = TList<TStringToken>;
//Types of expresion
TExprKind = (ekNumber, ekPointer, ekString);
//Stack machine execution status
TExecStatus = (esRun, esIdle);
//Runtime errors
TRTErrors = (
rteStackOverflow, rteStackUnderflow, rteStackTypeMismatch, rteInvalidParams,
rteDimIndexBound, rtePrintStackOverflow, rtePrintSyntaxMismatch,
rteAuxStackTypeMismatch, rteAuxStackOverflow, rteAuxStackUnderflow,
rteDivisionByZero, rteStringSize, rteUnknownInstr, rteUserMessage
);
TStrList = TList<String>; //Type for a list of strings
//*********
//TAsmLexer
//*********
//
//Intermediate code lexer.
//Used by the compiler and stack machine to tokenize postfix code.
//
TAsmLexer = class(TObject)
private
pSource: PChar;
idx: Integer;
//Identify assembly instructions
function AsmIdentKind(orig: TAsmToken; tokstr: String): TAsmToken;
//Postfix tokenizer
procedure AsmGetToken(var tokenstr: String; var tokenPos: Integer; var tok: TAsmToken);
public
constructor Create;
destructor Destroy; override;
procedure LoadLine(p: PChar);
//Get the second argument of an instruction
function SecondArg(s: String): String;
//get the String representation of the next token
function NextString(): String;
//Advance to the next token. After execution "data" holds the token string
//representation, "tokenPos" holds the token start position, "tok" holds the
//token label.
procedure Advance(var data: String; var tokenPos: Integer; var tok: TAsmToken);
end;
//TExec
//*****
//
// Stack machine for the final assembly code
//
TExec = class
private
PRG_IP, //current instruction index
STKP, //top of stack
BASEP: Integer; //functions and jumps control
AuxStackIdx: Integer; //SELECT CASE top of stack
//Keeps the last error message
FErrorMessage: String;
//Keeps the relationship between the Asm instructions and the BASIC source
//code lines responsible for its generation.
srcLine: Integer;
FPrintProc: TPrintProc;
strConst: String;
sourceAlloc, ended: Boolean;
asmLexer: TAsmLexer;
asmProg: TInstrArray; //List of Asm code to exec
FTotInsts: Integer; //Total of Asm instructions
FCallbackProc: TNotifyEvent;
FCallbackObj: TObject;
FTimeOut: Int64; //Exec timeout
ExecStatus: TExecStatus;
FTraceLevel: Integer; //Debug trace level (0=off, 1=basic, 2=standard, 3=verbose)
FWatchList: TStringList; //List of variables to watch during trace
FCurrentFunction: String; //Name of current function being executed
FGlobalVarNames: TStrList; //Variable names list (for WATCH value lookup)
//UI refresh throttling for PRINT statements
FUIRefreshInterval: Integer; //Minimum ms between UI refreshes (0=every print)
FLastUIRefresh: Cardinal; //Tick count of last UI refresh
//FTraceEnabled: Boolean; //Debug trace mode flag
//--------------------------------------------------------------------------
//Data comes from TCompiler
//--------------------------------------------------------------------------
HeapMem: array [0 .. MAXVARS] of TAsmData; //Global data area
StackMem: array [0 .. MAXSTACK] of TAsmData; //Local vars stack
TypeStack: array [0 .. MAXSTACK] of TExprKind; //Local vars types
AuxStack: array[0 .. MAXSTACK] of TAsmData; //Auxiliary stack
AuxStackTypes: array [0 .. MAXSTACK] of TExprKind; //Auxiliary stack types
//auxiliary functions
procedure PushAsmData(const dt: TAsmData; st: TExprKind);
function PopAsmData(checkType: TExprKind): TAsmData;
procedure Pop();
function TokenToFunc(tk: TAsmToken): TExeFunc;
function ICallReturnType(signature: String): String;
function ICallGetParams(signature: String): String;
//Stack machine assembly instructions
procedure fPop(); //decrease top of stack.
procedure fPush(); //push numeric variable content
procedure fPushPtr(); //push pointer variable content
procedure fPushPtrTag(); //Push pointer TAG constant
procedure fPushC(); //push(n)
procedure fPushS(); //push string variable content
procedure fPushSC(); //push(s)
procedure fAdd(); //pop(n1), pop(n2), push(n1+n2)
procedure fSub(); //pop(n1), pop(n2), push(n1-n2)
procedure fMul(); //pop(n1), pop(n2), push(n1*n2)
procedure fDiv(); //pop(n1), pop(n2), push(n1/n2)
procedure fMod(); //pop(n1), pop(n2), push(n1%n2)
procedure fInv(); //pop(n1), push(inv(n1))
procedure fMin(); //pop(n1), pop(n2), (n1<n2)?push(n1):push(n2)
procedure fMax(); //pop(n1), pop(n2), (n1>n2)?push(n1):push(n2)
procedure fPow(); //pop(n1), pop(n2), push(n1^n2)
procedure fNot(); //pop(n1), (n1==0)?push(1):push(0)
procedure fGE(); //pop(n1), pop(n2), (n1>=n2)?push(1):push(0)
procedure fGT(); //pop(n1), pop(n2), (n1>n2)?push(1):push(0)
procedure fLE(); //pop(n1), pop(n2), (n1<=n2)?push(1):push(0)
procedure fLT(); //pop(n1), pop(n2), (n1<n2)?push(1):push(0)
procedure fNE(); //pop(n1), pop(n2), (n1!=n2)?push(1):push(0)
procedure fEQ(); //pop(n1), pop(n2), (n1==n2)?push(1):push(0)
procedure fGES(); //pop(s1), pop(s2), (s1>=s2)?push(1):push(0)
procedure fGTS(); //pop(s1), pop(s2), (s1>s2)?push(1):push(0)
procedure fLES(); //pop(s1), pop(s2), (s1<=s2)?push(1):push(0)
procedure fLTS(); //pop(s1), pop(s2), (s1<s2)?push(1):push(0)
procedure fNES(); //pop(s1), pop(s2), (s1!=s2)?push(1):push(0)
procedure fEQS(); //pop(s1), pop(s2), (s1==s2)?push(1):push(0)
procedure fAddS(); //pop(s1), pop(s2), push(s1+s2)
procedure fSubS(); //pop(n1), pop(s1), push(s1[0,length(s1)-n1])
procedure fAddCRLFS(); //pop(s1), pop(s2), push(s1+'/n'+s2)
procedure fRead();
procedure fReadS();
procedure fRefreshRate(); //Set UI refresh interval
procedure fRestore();
procedure fInput();
procedure fInputS();
procedure fForCycle();
procedure fPopStore(); //pop(n)
procedure fPopStoreS(); //pop(s)
procedure fPopStorePtr(); //pop(p)
procedure fCallNear(); //push(p)
procedure fCallFar(); //pop(n), push(n)
procedure fCallFarS(); //pop(n), push(s)
procedure fCallFarP(); //pop(n), push(p)
procedure fOnCallFar(); //pop(n), pop(n), pop(s), push(n)
procedure fOnCallFarS(); //pop(n), pop(s), pop(s), push(s)
procedure fOnCallFarP(); //pop(n), pop(p), pop(s), push(p)
procedure fJump();
procedure fPopNCall();
procedure fPopNJump();
procedure fReturn(); //pop(p)
procedure fInitFunc(); //push(p)
procedure fRetFunction(); //pop(p), pop(p), pop(n)
procedure fNOp();
procedure fEnd();
procedure fCls();
procedure fPrint(); //pop(?)[,pop(?)]*
procedure fErr();
procedure fComma();
//procedure fFnAddr; //pop(s), push(n)
procedure fIndirectCall();
procedure fPushAuxStack();
procedure fPushAuxStackS();
procedure fPopAuxStack();
procedure fPushAuxTOS();
//Debug instructions
procedure fAssert(); //assert condition with message
procedure fBreakpoint(); //breakpoint (only when trace enabled)
procedure fDump(); //dump all global variables
procedure fTrace(); //set trace level
procedure fTraceOn(); //enable trace mode (legacy)
procedure fTraceOff(); //disable trace mode (legacy)
procedure fWatch(); //add variables to watch list
procedure fUnwatch(); //remove variables from watch list
//Debug helper functions
function GetTraceEnabled(): Boolean;
function GetVariableValue(const varName: String): String;
function GetWatchedVariablesInfo(): String;
public
//Entry point for every function available to the running program.
ProgramFunctions: TFunctionsDictionary;
//--------------------------------------------------------------------------
DataStmts: TDataItems; //DATA statements type and position
ReadIdx: Integer; //READ statement index
//--------------------------------------------------------------------------
TagObject: Pointer;
constructor Create();
destructor Destroy(); override;
procedure Clear();
procedure LoadSource(ls: TStringTokens);
procedure ExecuteFunction(
Entry: Integer; //function entry point
ParamCount: Integer; //total arguments
ParamType: Array of TExprKind; //arguments type
Params: Array of TAsmData; //parameters value
RetType: TExprKind; //function return type
out RetValue: TAsmData //function return value
);
//execute from the beginning
procedure ExecuteProgram();
//Get global var contents
function GetGlobalNum(const Index: Integer): Extended;
function GetGlobalPtr(const Index: Integer): Pointer;
function GetGlobalStr(const Index: Integer): String;
//Runtime error
procedure RTError(msg: TRTErrors; unkInstr: TAsmToken; auxMsg: String='');
procedure Stop(); //Immediately stops the VM
property ErrorMessage: String read FErrorMessage;
property TotalASMInst: Integer Read FTotInsts;
property IP: Integer read PRG_IP;
property SourceLine: Integer read srcLine;
property PrintProc: TPrintProc read FPrintProc write FPrintProc;
property CallbackProc: TNotifyEvent read FCallbackProc write FCallbackProc;
property CallbackObj: TObject read FCallbackObj write FCallbackObj;
property TimeOut: Int64 read FTimeOut write FTimeOut;
property TraceLevel: Integer read FTraceLevel write FTraceLevel;
property TraceEnabled: Boolean read GetTraceEnabled; //Legacy - returns TraceLevel > 0
property WatchList: TStringList read FWatchList;
property GlobalVarNames: TList<String> read FGlobalVarNames write FGlobalVarNames;
property UIRefreshInterval: Integer read FUIRefreshInterval write FUIRefreshInterval;
end;
implementation
uses
TimerLib; //For PauseAllTimers/ResumeAllTimers in breakpoint handling
{ TAsmLexer }
procedure TAsmLexer.Advance(var data: String; var tokenPos: Integer; var tok: TAsmToken);
begin
AsmGetToken(data, tokenPos, tok);
end;
procedure TAsmLexer.AsmGetToken(var tokenstr: String; var tokenPos: Integer; var tok: TAsmToken);
var
d: Double;
ok: Boolean;
ch: Char;
isEscaped: Boolean;
begin
ch := pSource[idx];
while ch.IsInArray([#8, #9, #32]) do //skip blanks
begin
Inc(idx);
ch := pSource[idx];
end;
tokenPos := idx;
case pSource[idx] of
';': //comment
begin
tokenPos := idx;
{$IFDEF ANDROID}
while (pSource[idx] <> System.sLineBreak) do
{$ENDIF}
{$IFDEF MSWINDOWS}
while (pSource[idx] <> #0) do
{$ENDIF}
Inc(idx);
SetString(tokenStr, pSource + tokenPos, idx - tokenPos);
tok := atkComment;
end;
'A' .. 'Z', 'a' .. 'z', '_', '@': //identifier
begin
tokenPos := idx;
Inc(idx);
Ch := pSource[idx];
while Ch.IsInArray(['A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'0','1','2','3','4','5','6','7','8','9',
'_','$','#','@','.']) do
begin
Inc(idx);
Ch := pSource[idx];
end;
setstring(tokenStr, pSource + tokenPos, idx - tokenPos);
tok := atkIdentifier;
case pSource[idx - 1] of
'$': tok := atkStrIdentifier;
'#': tok := atkPointerIdentifier;
end;
tok := AsmIdentKind(tok, tokenStr)
end;
'-', '0' .. '9', '.': //number
begin
tokenPos := idx;
Inc(idx);
tok := atkInteger;
Ch := pSource[idx];
while Ch.IsInArray(['0','1','2','3','4','5','6','7','8','9','.','e','E']) do
begin
case pSource[idx] of
'.': tok := atkFloat;
'e', 'E':
begin
Inc(idx);
Ch := pSource[idx];
tok := atkFloat;
end;
end;
Inc(idx);
Ch := pSource[idx];
end;
SetString(tokenStr, pSource + tokenPos, idx - tokenPos);
if pSource[tokenPos] = '.' then
begin
tok := atkFloat;
tokenStr := '0' + tokenStr;
end;
d := TUtils.StrToFloat2(tokenStr, ok);
if not ok then tok := atkUnknown;
if d > MAX_INTEGER_VALUE then tok := atkFloat;
if pSource[idx] = '#' then Inc(idx);
end;
#10: //CRLF
begin
tok := atkCRLF;
tokenStr := System.sLineBreak;
tokenPos := idx;
Inc(idx);
end;
#13: //CRLF
begin
tok := atkCRLF;
tokenStr := System.sLineBreak;
tokenPos := idx;
Inc(idx);
if pSource[idx] = Char(#10) then
Inc(idx);
end;
'%', '/', '&', '('..',', ':', '<'..'?', '['..'^', '{'..'~': //operator
begin
tokenPos := idx;
tok := atkSymbol;
if pSource[idx] = ',' then tok := atkComma;
Inc(idx);
SetString(tokenStr, pSource + tokenPos, idx - tokenPos);
end;
'"': //It's a string constant
begin
tok := atkString;
tokenStr := '';
isEscaped := False;
repeat
case pSource[idx] of
#0, #10, #13:
begin
Dec(idx);
tok := atkUnknown;
Break;
end;
'\':
begin
if isEscaped then
begin
tokenStr := tokenStr + '\';
isEscaped := False;
end
else
isEscaped := True;
end;
else
begin
if isEscaped then
begin
case pSource[idx] of
'"': tokenStr := tokenStr + '"'; // \" -> "
'\': tokenStr := tokenStr + '\'; // \\ -> \
'n': tokenStr := tokenStr + #10; // \n -> newline (LF)
'r': tokenStr := tokenStr + #13; // \r -> carriage return (CR)
't': tokenStr := tokenStr + #9; // \t -> horizontal tab
'0': tokenStr := tokenStr + #0; // \0 -> null character
'b': tokenStr := tokenStr + #8; // \b -> backspace
'f': tokenStr := tokenStr + #12; // \f -> form feed
'v': tokenStr := tokenStr + #11; // \v -> vertical tab
'a': tokenStr := tokenStr + #7; // \a -> alert/bell
else tokenStr := tokenStr + '\' + pSource[idx]; // Invalid sequence
end;
isEscaped := False;
end
else if pSource[idx] <> '"' then
tokenStr := tokenStr + pSource[idx];
end;
end;
Inc(idx);
until (not isEscaped) and (pSource[idx] = '"');
Inc(idx);
tokenPos := tokenPos + 1;
end;
#0:
begin
tok := atkNull;
tokenStr := '';
tokenPos := idx;
end;
else
begin
tokenPos := idx;
Inc(idx);
tok := atkUnknown;
SetString(tokenStr, pSource + tokenPos, idx - tokenPos);
end;
end;
end;
function TAsmLexer.AsmIdentKind(orig: TAsmToken; tokstr: String): TAsmToken;
var
code: Integer;
begin
result := orig;
tokstr := UpperCase(tokstr);
code := TUtils.StringCode(tokstr);
if (code < 140) or (code > 1086) then
Exit();
case code of
140: if tokStr = 'GE' then Result := atkGe;
145: if tokStr = 'LE' then Result := atkLe;
147: if tokStr = 'NE' then Result := atkNe;
150: if tokStr = 'EQ' then Result := atkEq;
155: if tokStr = 'GT' then Result := atkGt;
160: if tokStr = 'LT' then Result := atkLt;
161: if tokStr = 'OR' then Result := atkOr;
176: if tokStr = 'GE$' then Result := atkGeS;
181: if tokStr = 'LE$' then Result := atkLeS;
183: if tokStr = 'NE$' then Result := atkNeS;
186: if tokStr = 'EQ$' then Result := atkEqS;
191: if tokStr = 'GT$' then Result := atkGtS;
196: if tokStr = 'LT$' then Result := atkLtS;
201: if tokStr = 'ADD' then Result := atkAdd;
211: if tokStr = 'AND' then Result := atkAnd;
215: if tokStr = 'END' then Result := atkEnd;
224: if tokStr = 'MOD' then Result := atkMod;
226: if tokStr = 'CLS' then Result := atkCls;
227: if tokStr = 'DIV' then Result := atkDiv;
228: if tokStr = 'MIN' then Result := atkMin;
230: if tokStr = 'MAX' then Result := atkMax;
233: if tokStr = 'ERR' then Result := atkErr;
234: if tokStr = 'SUB' then Result := atkSub;
237:
begin
if tokStr = 'INV' then Result := atkInv
else if tokStr = 'NOP' then Result := atkNop
else if tokStr = 'ADD$' then Result := atkAddS;
end;
238: if tokStr = 'MUL' then Result := atkMul;
239: if tokStr = 'POP' then Result := atkPop;
241: if tokStr = 'NOT' then Result := atkNot;
246: if tokStr = 'POW' then Result := atkPow;
270: if tokStr = 'SUB$' then Result := atkSubS;
282: if tokStr = 'DATA' then Result := atkData;
//283: if tokStr = 'ADDR' then Result := atkFnAddress;
284:
begin
if tokStr = 'CALL' then Result := atkCallNear
else if tokStr = 'READ' then Result := atkRead;
end;
297: if tokStr = 'ELSE' then Result := atkElse;
310: if tokStr = 'DUMP' then Result := atkDump;
314: if tokStr = 'EXIT' then Result := atkExit;
316: if tokStr = 'JUMP' then Result := atkJump;
318: if tokStr = 'DATA$' then Result := atkDataS;
319: if tokStr = 'NEXT' then Result := atkNext;
320:
begin
if tokStr = 'PUSH' then Result := atkPush
else if tokStr = 'READ$' then Result := atkReadS;
end;
355: if tokStr = 'PUSH#' then Result := atkPushPtr;
356: if tokStr = 'PUSH$' then Result := atkPushS;
357: if tokStr = 'BREAK' then Result := atkBreak;
358: if tokStr = 'ENDIF' then Result := atkEndIf;
367: if tokStr = 'TRACE' then Result := atkTrace;
375: if tokStr = 'WATCH' then Result := atkWatch;
377: if tokStr = 'WHILE' then Result := atkWhile;
382: if tokStr = 'PAUSE' then Result := atkPause;
387: if tokStr = 'PUSHC' then Result := atkPushC;
396: if tokStr = 'UNTIL' then Result := atkUntil;
397: if tokStr = 'PRINT' then Result := atkPrint;
400: if tokStr = 'INPUT' then Result := atkInput;
423: if tokStr = 'PUSHC$' then Result := atkPushCS;
436: if tokStr = 'INPUT$' then Result := atkInputS;
441: if tokStr = 'CALLEX' then Result := atkCallFar;
449: if tokStr = 'REPEAT' then Result := atkRepeat;
452: if tokStr = 'I_CALL' then Result := atkIndirectCall;
466: if tokStr = 'ASSERT' then Result := atkAssert;
470: if tokStr = 'ONGOTO' then Result := atkOnGoto;
476: if tokStr = 'CALLEX#' then Result := atkCallFarP;
477:
begin
if tokStr = 'CALLEX$' then Result := atkCallFarS
else if tokStr = 'POPAUX' then Result := atkPopAux;
end;
480: if tokStr = 'RETURN' then Result := atkReturn;
499: if tokStr = 'CASEEND' then Result := atkCaseEnd;
524: if tokStr = 'TRACEON' then Result := atkTraceOn;
532: if tokStr = 'ADDCRLF$' then Result := atkAddCRLFS;
538: if tokStr = 'UNWATCH' then Result := atkUnwatch;
541: if tokStr = 'ONGOSUB' then Result := atkOnGosub;
548: if tokStr = 'RESTORE' then Result := atkRestore;
558: if tokStr = 'PUSHAUX' then Result := atkPushAux;
581: if tokStr = 'CASEELSE' then Result := atkCaseElse;
586: if tokStr = 'TRACEOFF' then Result := atkTraceOff;
592: if tokStr = 'ENDWHILE' then Result := atkEndWhile;
594: if tokStr = 'PUSHAUX$' then Result := atkPushAuxS;
598: if tokStr = 'ONCALLEX' then Result := atkOnCallFar;
599: if tokStr = 'FORCYCLE' then Result := atkForCycle;
601: if tokStr = 'POPNCALL' then Result := atkPopnCall;
608: if tokStr = 'INITFUNC' then Result := atkInitFunc;
613: if tokStr = 'CONTINUE' then Result := atkContinue;
614: if tokStr = 'FUNCTION' then Result := atkFunction;
619: if tokStr = 'DO_WHILE' then Result := atkDoWhile;
624: if tokStr = 'LOOP_END' then Result := atkLoopEnd;
633: if tokStr = 'POPNJUMP' then Result := atkPopNJump;
636: if tokStr = 'POPSTORE' then Result := atkPopStore;
638: if tokStr = 'DO_UNTIL' then Result := atkDoUntil;
640: if tokStr = 'DO_START' then Result := atkDoStart;
670: if tokStr = 'PUSH#_TAG' then Result := atkPushPtrTag;
671: if tokStr = 'POPSTORE#' then Result := atkPopStorePtr;
672: if tokStr = 'POPSTORE$' then Result := atkPopStoreS;
682: if tokStr = 'CASESTART' then Result := atkCaseStart;
742: if tokStr = 'ELSEIFBODY' then Result := atkElseIfBody;
751: if tokStr = 'BREAKPOINT' then Result := atkBreakpoint;
760: if tokStr = 'ELSEIFTEST' then Result := atkElseIfTest;
786: if tokStr = 'LOOP_WHILE' then Result := atkLoopWhile;
804: if tokStr = 'PUSHAUXTOS' then Result := atkPushAuxTOS;
805: if tokStr = 'LOOP_UNTIL' then Result := atkLoopUntil;
827: if tokStr = 'REFRESHRATE' then Result := atkRefreshRate;
829: if tokStr = 'ENDFUNCTION' then Result := atkEndfunction;
849: if tokStr = 'RETFUNCTION' then Result := atkRetFunction;
1023: if tokStr = 'POPNJUMP_CRLF' then Result := atkPopNJump_CRLF;
1086: if tokStr = 'POPNJUMP_ENDIF' then Result := atkPopNJump_EndIf;
end;
end;
constructor TAsmLexer.Create();
begin
inherited Create();
end;
destructor TAsmLexer.Destroy();
begin
inherited Destroy();
end;
procedure TAsmLexer.LoadLine(p: PChar);
begin
pSource := p;
idx := 0;
end;
function TAsmLexer.NextString(): String;
var
p: Integer;
tk: TAsmToken;
begin
Result := '';
Advance(Result, p, tk);
end;
function TAsmLexer.SecondArg(s: String): String;
var
p: Integer;
tk: TAsmToken;
begin
LoadLine(PChar(s));
Advance(result, p, tk);
Advance(result, p, tk);
end;
{ TExec }
procedure TExec.Clear();
var
i: Integer;
begin
// FIX #6: Changed MAXVARS-1 to MAXVARS to clear ALL elements.
// HeapMem is array[0..MAXVARS] (indices 0..515). The loop previously
// skipped index 515. A program with 513+ globals would see stale data
// in the last variable slot after re-running.
for i := 0 to MAXVARS do
begin
HeapMem[i].n := 0; // Classic BASIC: uninitialized numbers default to 0
HeapMem[i].p := nil;
HeapMem[i].s := '';
end;
FErrorMessage := '';
STKP := 0;
PRG_IP := 0;
BASEP := 0; // HIGH PRIORITY FIX: Initialize base pointer
AuxStackIdx := 0; // HIGH PRIORITY FIX: Initialize auxiliary stack index
ended := false;
end;
constructor TExec.Create();
begin
TagObject := nil;
sourceAlloc := false;
FTraceLevel := 0; //Debug trace mode disabled by default
FCurrentFunction := ''; //No function being executed
FWatchList := TStringList.Create; //Create watch list for debug
FWatchList.Duplicates := dupIgnore; //Ignore duplicate variable names
//This dictionary contains the BASIC program available functions. Those
//imported from Delphi and those declared by the user in the BASIC program
//source code.
//This information is sent to the stack machine by the parser and by the
//preprocessor.
ProgramFunctions := TFunctionsDictionary.Create();
//Holds the position and type for each compiled DATA statement.
DataStmts := TDataItems.Create;
//Holds the index for the READ statements
ReadIdx := 0;
//Stack machine execution timeout
FTimeOut := DEFAULT_TIMEOUT;
//UI refresh throttling
FUIRefreshInterval := DEFAULT_UI_REFRESH_INTERVAL;
FLastUIRefresh := 0;
asmLexer := TAsmLexer.Create();
try
SetLength(asmProg, INITASMSIZE + 1);
except
on E:Exception do
begin
FErrorMessage := 'ERROR. Unable to allocate memory for program execution: '+E.Message;
ended := true;
end;
end;
Self.Clear();
end;
destructor TExec.Destroy();
begin
if Assigned(FWatchList) then FreeAndNil(FWatchList);
if Assigned(asmLexer) then FreeAndNil(asmLexer);
if Assigned(DataStmts) then FreeAndNil(DataStmts);
if Assigned(ProgramFunctions) then FreeAndNil(ProgramFunctions);
inherited Destroy();
end;
//Executes a user defined function (called directly only for callbacks)
//Normal function calls within the main program use fCallNear instead.
//CRITICAL: This method MUST run callbacks in a completely isolated stack
//environment to prevent corrupting the main program's stack state.
procedure TExec.ExecuteFunction(Entry, ParamCount: Integer; ParamType: array of TExprKind; Params: array of TAsmData; RetType: TExprKind; out RetValue: TAsmData);
var
deltaTicks: Int64;
Timer: TStopWatch;
innerProc, i, TmpIP, TmpSTKP, TmpBASEP, TmpAuxStackIdx: Integer;
dt: TAsmData;
WasEnded, HadError: Boolean;
begin
//Starts with 1, because the execution point in the assembly code will always
//start in a function
innerProc := 1;
HadError := false;
//CRITICAL FIX: ALWAYS save the COMPLETE VM state before callback execution.
//This method is called directly only for callbacks (via ExecuteUserFunction).
//Normal function calls go through fCallNear within ExecuteProgram.
//Without saving state, callbacks fired during main program execution
//(e.g., from property setters like switch_ischecked#) would corrupt the stack.
WasEnded := ended;
TmpIP := PRG_IP;
TmpSTKP := STKP;
TmpBASEP := BASEP;
// FIX: Save AuxStackIdx to prevent SELECT/CASE corruption during callbacks.
// If a callback fires while the main program is inside a SELECT/CASE block,
// and the callback also uses SELECT/CASE, the auxiliary stack would be corrupted.
TmpAuxStackIdx := AuxStackIdx;
//Use try/finally to GUARANTEE state restoration even if exceptions occur
try
//CRITICAL FIX: Do NOT reset STKP to 0!
//Callbacks can fire during main program execution (e.g., OnSelChanged triggers
//while PopulateData sets cell values). If we reset STKP to 0, the callback
//writes its parameters and locals over positions 1..N, DESTROYING the main
//program's live stack data. After restoring STKP, the main program finds
//corrupted TypeStack entries => "Stack type mismatch".
//
//Instead, build the callback's stack frame ON TOP of the existing stack.
//After the callback completes, we restore STKP/BASEP to discard the
//callback's frame, leaving the main program's stack memory untouched.
if ended then
ended := false;
PRG_IP := 0;
//STKP stays at current value - callback builds on top of existing stack
//BASEP stays at current value - will be saved/restored by fInitFunc/fRetFunction
for i := 0 to ParamCount-1 do //Push parameters
PushAsmData(Params[i], ParamType[i]);
dt.n := ParamCount;
PushAsmData(dt, ekNumber); //Push total of passed parameters.
// HIGH PRIORITY FIX: Use NativeInt for 64-bit compatibility
dt.p := Pointer(NativeInt(PRG_IP)); //Record the entry point.
PushAsmData(dt, ekPointer); //Push function entry point.
PRG_IP := Entry; //Move index to the function's entry point.
//It must be an "atkInitFunc" instruction
if (asmProg[PRG_IP].token <> atkInitFunc) then
begin
RTError(rteUserMessage, atkNull, 'Entry point is not a function address in function call by address.');
Exit(); //finally will still run and restore state
end;
deltaTicks := FTimeOut * 1000; //Timeout in milliseconds
Timer := TStopWatch.StartNew; //Create watch
try
repeat //run function's body
if (asmProg[PRG_IP].token = atkCallNear) then Inc(innerProc);
if (asmProg[PRG_IP].token = atkRetFunction) then Dec(innerProc);
asmProg[PRG_IP].proc(); //call proc linked to the instruction
Inc(PRG_IP); //move to next instruction
//If there is a callback, run it.
if (callBackObj <> nil) then CallBackProc(callBackObj);
//Check for the script timeout
if FTimeOut > 0 then //0 = no timeout (be careful)
begin
Timer.Stop(); //Stop watch
if Timer.ElapsedMilliseconds > deltaTicks then //Check for timeout
begin
HadError := true;
Break; //Exit loop instead of raising exception
end;
Timer.Start(); //Restart the watch
end;
until (ended or (innerProc = 0));
finally
Timer.Stop(); //Stop watch
end;
//Track if an error occurred during callback execution
if ended then HadError := true;
// HIGH PRIORITY FIX: Use NativeInt for 64-bit compatibility
PRG_IP := NativeInt(dt.p);
//After running, pop function result from stack (only if no error occurred)
if not HadError then
begin
case RetType of
ekNumber: RetValue.n := PopAsmData(ekNumber).n;
ekPointer: RetValue.p := PopAsmData(ekPointer).p;
ekString: RetValue.s := PopAsmData(ekString).s;
end;
end;
finally
//ALWAYS restore VM state after callback completes - this is CRITICAL
//Restoring STKP effectively discards the callback's entire stack frame,
//leaving the main program's stack data intact below.
ended := WasEnded;
PRG_IP := TmpIP;
STKP := TmpSTKP;
BASEP := TmpBASEP;
AuxStackIdx := TmpAuxStackIdx; // Restore auxiliary stack (SELECT/CASE)
end;
end;
//Execute the entire program
procedure TExec.ExecuteProgram();
const
TIMEOUT_CHECK_INTERVAL = 10000; // Check timeout every N instructions
var
deltaTicks: Int64;
Timer: TStopWatch;
instructionCount: Integer;
begin
ExecStatus := TExecStatus.esRun; //Change status to 'running'
Self.Clear(); //Reset the stack machine
deltaTicks := FTimeOut * 1000; //Timeout in milliseconds
Timer := TStopWatch.StartNew(); //Create watch
instructionCount := 0; // HIGH PRIORITY FIX: Counter for optimized timeout checking
repeat //for each instruction...
if ExecStatus <> TExecStatus.esRun then
begin
Application.ProcessMessages();
// FIX #10: Prevent 100% CPU usage during breakpoint/pause.
// Without sleep, this tight loop burns all CPU resources.
// On mobile devices this drains battery and can trigger
// Android ANR or iOS watchdog. 16ms ≈ 60fps refresh rate.
TThread.Sleep(16);
continue;
end;
// FIX #12: Wrap instruction execution in try/except to catch unexpected
// exceptions from library functions. Without this, an unhandled exception
// propagates with PRG_IP/STKP/BASEP in potentially inconsistent state,
// causing incorrect error line reporting. We catch it here and set the
// error state cleanly before re-raising.
try
asmProg[PRG_IP].proc(); //Exec instr.
except
on E: Exception do
begin
// If ended is already set (e.g. by fErr or RTError), the error
// was expected and handled — just re-raise to exit the loop.
if not ended then
begin
FErrorMessage := 'Unexpected error at ASM[' + IntToStr(PRG_IP) +
'] Source[' + IntToStr(srcLine) + ']: ' + E.Message;
ended := true;
end;
raise;
end;
end;
Inc(PRG_IP); //Increment instruction pointer
//Check for callback object, If assigned process it.
if (callBackObj <> nil) then CallBackProc(callBackObj);
// HIGH PRIORITY FIX: Optimized timeout checking - only check every N instructions
if FTimeOut > 0 then //0 = no timeout (be careful)
begin
Inc(instructionCount);
if instructionCount >= TIMEOUT_CHECK_INTERVAL then
begin
instructionCount := 0;
if Timer.ElapsedMilliseconds > deltaTicks then //Check for timeout
begin
ended := true;
Timer.Stop();
raise Exception.Create('Script timeout');
end;
end;
end;
until ended;
Timer.Stop(); //Stop watch
end;
procedure TExec.fAdd();
begin
Pop();
StackMem[STKP].n := StackMem[STKP].n + StackMem[STKP + 1].n;
end;
procedure TExec.fAddCRLFS();
begin
Pop();
StackMem[STKP].s := StackMem[STKP].s + System.sLineBreak + StackMem[STKP + 1].s;
end;
procedure TExec.fAddS();
begin