Achieve self-hosting for hello-world compilation

The blaise compiler now compiles its own source (tests/blaise-compiler.pas)
into a second-stage binary that successfully compiles and runs a hello-world
program. All 834 compiler tests still pass.

Codegen and RTL fixes that unblocked self-hosting:

- String literal interning (uCodeGenQBE): set FStrLits.CaseSensitive := True.
  FPC's TStringList.IndexOf is case-insensitive by default, so distinct
  literals like 'WRITELN' and 'WriteLn' collapsed to the same label.

- ARC on implicit Self.Field assignments (uCodeGenQBE): EmitAssignment's
  implicit-Self branch did a raw storel for class/string fields. Fresh
  _ClassAlloc'ed objects start at refcnt 0, so without the addref they were
  freed by later local releases. Now addrefs new value and releases old.

- #nn character literals (uLexer): UnescapeString handled only 'text'.
  Extended to decode #nn decimal literals and concatenated forms like
  'abc'#13#10'def'. Local OrdAt helper lets the body parse under FPC and
  the self-hosted compiler.

- Chr() builtin: registered in uSymbolTable, handled in uSemantic, emitted
  in uCodeGenQBE as _Chr, implemented in rtl/blaise_str.c.

- LF line separator (Classes.pas): TStringList.GetText used #13#10; QBE
  rejects CR in its input. Changed to #10.

Self-hosting source (tests/blaise-compiler.pas) — needs a virtual Destroy
on TASTNode and Exception (post-TObject-stripping they became root classes
without a vtable, breaking `is` checks and exception message layout).
TAST* descendants chain `override`.

Migration tooling: add tools/migrate_full.py — the script that generates
tests/blaise-compiler.pas by flattening the eight compiler units and RTL
Classes unit into a single self-contained Pascal source. The postprocess()
step injects virtual Destroy on TASTNode / TTypeDesc and strips `override`
from non-vtable root classes.
This commit is contained in:
Graeme Geldenhuys 2026-04-24 00:21:43 +01:00
parent 49a1a0f478
commit f66260093e
19 changed files with 15103 additions and 353 deletions

View file

@ -49,10 +49,15 @@ type
Name: string;
IsVarParam: Boolean; { set by uSemantic — True if this ident is a var parameter }
IsConstant: Boolean; { set by uSemantic — True if this ident is a skConstant symbol }
ConstValue: Int64; { valid when IsConstant = True }
ConstValue: Int64; { valid when IsConstant = True; integer/bool/enum value }
ConstString: string; { valid when IsConstant = True and type is tyString }
IsNoArgFuncCall: Boolean; { set by uSemantic — bare ident that resolves to a 0-arg function }
NoArgFuncDecl: TObject; { TMethodDecl — not owned; valid when IsNoArgFuncCall and user-defined }
IsGlobal: Boolean; { set by uSemantic — this ident is a program-level global var }
IsImplicitSelf: Boolean; { set by uSemantic — bare field name implicitly referencing Self }
ImplicitFieldInfo: TObject; { TFieldInfo — not owned; valid when IsImplicitSelf }
IsImplicitSelfMethod: Boolean; { set by uSemantic — bare zero-arg method call on Self }
ImplicitMethodDecl: TObject; { TMethodDecl — not owned; set when IsImplicitSelfMethod }
end;
TFieldAccessExpr = class(TASTExpr)
@ -65,6 +70,11 @@ type
IsClassAccess: Boolean; { set by uSemantic — pointer deref needed }
PropRead: TPropertyInfo; { non-nil if this is a method-backed property read }
PropOwnerType: string; { class type name for method-backed property calls }
IsImplicitSelf: Boolean; { set by uSemantic — RecordName is a field of Self }
ImplicitBaseInfo: TFieldInfo; { non-owned — the field of Self holding the record/class }
IsMethodCall: Boolean; { set by uSemantic — FieldName is a zero-arg method }
ResolvedMethod: TObject; { TMethodDecl — not owned; set when IsMethodCall }
IsGlobal: Boolean; { set by uSemantic — RecordName is a program-level global }
destructor Destroy; override;
end;
@ -111,6 +121,7 @@ type
Name: string;
Expr: TASTExpr; { owned }
IsVarParam: Boolean; { set by uSemantic — True if target is a var parameter }
IsGlobal: Boolean; { set by uSemantic — True if target is a program-level global }
ResolvedLhsType: TTypeDesc; { set by uSemantic — type of the target variable }
IsWeakLhs: Boolean; { set by uSemantic — True if the LHS symbol
was declared [Weak]; codegen emits a
@ -145,6 +156,7 @@ type
TForStmt = class(TASTStmt)
public
VarName: string;
IsGlobal: Boolean; { set by uSemantic — VarName is a program-level global }
StartExpr: TASTExpr; { owned }
EndExpr: TASTExpr; { owned }
IsDownTo: Boolean;
@ -178,6 +190,9 @@ type
{ 'break' statement — exits the innermost loop. }
TBreakStmt = class(TASTStmt);
{ 'continue' statement — jumps to next iteration of the innermost loop. }
TContinueStmt = class(TASTStmt);
{ One arm of a case statement: one or more ordinal values → body }
TCaseBranch = class
public
@ -201,8 +216,12 @@ type
RecordName: string;
FieldName: string;
Expr: TASTExpr; { owned }
ObjExpr: TASTExpr; { owned — when non-nil, receiver is this expression (e.g. typecast) }
FieldInfo: TFieldInfo; { set by uSemantic — carries offset + type }
IsClassAccess: Boolean; { set by uSemantic — pointer deref needed }
IsImplicitSelf: Boolean; { set by uSemantic — RecordName is a field of Self }
ImplicitBaseInfo: TFieldInfo; { non-owned — the field of Self that holds the record/class }
IsGlobal: Boolean; { set by uSemantic — RecordName is a program-level global }
destructor Destroy; override;
end;
@ -220,6 +239,7 @@ type
Name: string;
Args: TObjectList; { owned TASTExpr items }
ResolvedDecl: TObject; { TMethodDecl — not owned; set by uSemantic for user-defined procs }
IsImplicitSelfMethod: Boolean; { set by uSemantic — call is on Self }
constructor Create;
destructor Destroy; override;
end;
@ -229,6 +249,7 @@ type
Name: string;
Args: TObjectList; { owned TASTExpr items }
ResolvedDecl: TObject; { TMethodDecl — not owned; set by uSemantic }
IsImplicitSelfMethod: Boolean; { set by uSemantic — call is on Self }
constructor Create;
destructor Destroy; override;
end;
@ -245,9 +266,13 @@ type
ObjectName: string;
Name: string; { method name }
Args: TObjectList; { owned TASTExpr items }
ObjExpr: TASTExpr; { owned — when non-nil, receiver is this expression }
{ Set by uSemantic: }
ResolvedClassType: TTypeDesc; { not owned }
ResolvedMethod: TObject; { TMethodDecl — not owned; avoids forward ref }
IsImplicitSelf: Boolean; { ObjectName is a field of Self }
ImplicitBaseInfo: TFieldInfo; { not owned — the field of Self }
IsGlobal: Boolean; { set by uSemantic — ObjectName is a program-level global }
constructor Create;
destructor Destroy; override;
end;
@ -283,6 +308,7 @@ type
IsWeak: Boolean; { set by uSemantic when [Weak] is resolved
on this declaration. Codegen keys off
this instead of walking Attributes. }
IsGlobal: Boolean; { set by uSemantic when this is a program-level global variable }
constructor Create;
destructor Destroy; override;
end;
@ -355,9 +381,11 @@ type
ObjectName: string;
Name: string; { method name }
Args: TObjectList; { owned TASTExpr }
ObjExpr: TASTExpr; { owned — receiver expression when ObjectName = '' }
ResolvedClassType: TTypeDesc; { not owned; set by uSemantic }
ResolvedMethod: TObject; { TMethodDecl — not owned }
IsConstructorCall: Boolean; { set by uSemantic — TypeName.Create(args) }
IsGlobal: Boolean; { set by uSemantic — ObjectName is a program-level global }
constructor Create;
destructor Destroy; override;
end;
@ -648,6 +676,7 @@ end;
destructor TFieldAssignment.Destroy;
begin
Expr.Free;
ObjExpr.Free;
inherited Destroy;
end;
@ -707,6 +736,7 @@ end;
destructor TMethodCallStmt.Destroy;
begin
Args.Free;
ObjExpr.Free;
inherited Destroy;
end;
@ -803,6 +833,7 @@ end;
destructor TMethodCallExpr.Destroy;
begin
Args.Free;
ObjExpr.Free;
inherited Destroy;
end;

File diff suppressed because it is too large Load diff

View file

@ -60,6 +60,7 @@ type
tkNot,
tkExit,
tkBreak,
tkContinue,
tkInherited,
tkCase,
tkOf,
@ -112,6 +113,14 @@ type
implementation
{ Local helper matching the Blaise builtin — lets UnescapeString share one
body under FPC and the self-hosted compiler. The migration tool strips
this declaration; self-hosted builds resolve OrdAt to the builtin. }
function OrdAt(const S: string; I: Integer): Integer;
begin
Result := Ord(S[I]);
end;
constructor TLexer.Create(const ASource: string);
begin
inherited Create;
@ -163,6 +172,7 @@ begin
else if AUpper = 'NOT' then Result := tkNot
else if AUpper = 'EXIT' then Result := tkExit
else if AUpper = 'BREAK' then Result := tkBreak
else if AUpper = 'CONTINUE' then Result := tkContinue
else if AUpper = 'CASE' then Result := tkCase
else if AUpper = 'OF' then Result := tkOf
else if AUpper = 'CONST' then Result := tkConst
@ -172,36 +182,60 @@ begin
end;
function TLexer.UnescapeString(const ARaw: string): string;
{ ARaw is the full source span including surrounding quotes.
Handles: 'text' with '' → ' escaping.
Phase 1 supports only single-quoted string literals; #nn and ^X forms
are not yet part of the Clean Pascal spec. }
{ ARaw is the full source span. Handles: 'text' with '' → ' escaping,
#nn numeric char literals (decimal), and concatenated runs like
'abc'#13#10'def'. Uses OrdAt so the body parses under both FPC and
the self-hosted Blaise compiler. }
var
I, Len: Integer;
I, Len, N, C: Integer;
begin
Result := '';
Len := Length(ARaw);
if (Len >= 2) and (ARaw[1] = '''') then
I := 1;
while I <= Len do
begin
I := 2;
while I <= Len do
C := OrdAt(ARaw, I);
if C = 39 then { single quote }
begin
if ARaw[I] = '''' then
I := I + 1;
while I <= Len do
begin
if (I < Len) and (ARaw[I + 1] = '''') then
C := OrdAt(ARaw, I);
if C = 39 then
begin
Result := Result + '''';
Inc(I, 2);
if (I < Len) and (OrdAt(ARaw, I + 1) = 39) then
begin
Result := Result + '''';
I := I + 2;
end
else
begin
I := I + 1;
Break;
end;
end
else
Inc(I); { closing quote }
end
else
begin
Result := Result + ARaw[I];
Inc(I);
begin
Result := Result + Chr(C);
I := I + 1;
end;
end;
end;
end
else if C = 35 then { '#' }
begin
I := I + 1;
N := 0;
while I <= Len do
begin
C := OrdAt(ARaw, I);
if (C < 48) or (C > 57) then Break;
N := N * 10 + (C - 48);
I := I + 1;
end;
Result := Result + Chr(N);
end
else
I := I + 1;
end;
end;
@ -239,6 +273,7 @@ begin
else if text = 'OVERRIDE' then Result.Kind := tkOverride
else if text = 'EXIT' then Result.Kind := tkExit
else if text = 'BREAK' then Result.Kind := tkBreak
else if text = 'CONTINUE' then Result.Kind := tkContinue
else if text = 'CASE' then Result.Kind := tkCase
else if text = 'OF' then Result.Kind := tkOf
else if text = 'CONST' then Result.Kind := tkConst

View file

@ -1015,9 +1015,13 @@ var
Assign: TAssignment;
FldAssign: TFieldAssignment;
MCall: TMethodCallStmt;
MCallExpr: TMethodCallExpr;
PtrWrite: TPointerWriteStmt;
PtrIdNode: TIdentExpr;
SecondIdent: string;
FldNode: TFieldAccessExpr;
CastRcv: TASTExpr;
FCallNode: TFuncCallExpr;
begin
Result := nil;
@ -1072,6 +1076,15 @@ begin
Exit;
end;
if Check(tkContinue) then
begin
Result := TContinueStmt.Create;
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
Advance;
Exit;
end;
if Check(tkInherited) then
begin
Result := ParseInheritedStmt;
@ -1139,20 +1152,138 @@ begin
end
else
begin
{ Method call: Ident '.' Ident ['(' [args] ')'] }
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := Name;
MCall.Name := SecondIdent;
if Check(tkLParen) then
{ Method call: Ident '.' Ident ['(' [args] ')'] — may chain to
Ident.Ident.Ident... with further '.' before the final method call. }
if Check(tkDot) then
begin
Advance;
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
{ Chained: AProg.UsedUnits.Add(...) — build a field-access chain as
the receiver expression. }
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := '';
{ Build chain: AProg -> AProg.UsedUnits }
PtrIdNode := TIdentExpr.Create;
PtrIdNode.Line := Line;
PtrIdNode.Col := Col;
PtrIdNode.Name := Name;
FldNode := TFieldAccessExpr.Create;
FldNode.Line := Line;
FldNode.Col := Col;
FldNode.Base := PtrIdNode;
FldNode.FieldName:= SecondIdent;
MCall.ObjExpr := FldNode;
while Check(tkDot) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected field or method name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
SecondIdent := FCurrent.Value;
Advance;
if Check(tkDot) then
begin
{ Still more chain — extend field access path }
FldNode := TFieldAccessExpr.Create;
FldNode.Line := Line;
FldNode.Col := Col;
FldNode.Base := MCall.ObjExpr;
FldNode.FieldName:= SecondIdent;
MCall.ObjExpr := FldNode;
end
else
begin
MCall.Name := SecondIdent;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
end;
Break;
end;
end;
Result := MCall;
end
else
begin
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := Name;
MCall.Name := SecondIdent;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
end;
{ Post-method chain: Name.Method(args).Field := value or .Method2(args) }
if Check(tkDot) then
begin
MCallExpr := TMethodCallExpr.Create;
MCallExpr.Line := Line;
MCallExpr.Col := Col;
MCallExpr.ObjectName := MCall.ObjectName;
MCallExpr.Name := MCall.Name;
while MCall.Args.Count > 0 do
MCallExpr.Args.Add(MCall.Args.Extract(MCall.Args.Items[0]));
MCall.Free;
MCall := nil;
CastRcv := MCallExpr;
while Check(tkDot) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt(
'Expected identifier after ''.'' at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
SecondIdent := FCurrent.Value;
Advance;
if Check(tkLParen) then
begin
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := '';
MCall.Name := SecondIdent;
MCall.ObjExpr := CastRcv;
Advance;
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
Result := MCall;
Exit;
end;
FldNode := TFieldAccessExpr.Create;
FldNode.Line := Line;
FldNode.Col := Col;
FldNode.Base := CastRcv;
FldNode.FieldName := SecondIdent;
CastRcv := FldNode;
end;
if Check(tkAssign) and (CastRcv is TFieldAccessExpr) then
begin
Advance;
FldAssign := TFieldAssignment.Create;
FldAssign.Line := Line;
FldAssign.Col := Col;
FldAssign.FieldName := TFieldAccessExpr(CastRcv).FieldName;
FldAssign.ObjExpr := TFieldAccessExpr(CastRcv).Base;
TFieldAccessExpr(CastRcv).Base := nil;
CastRcv.Free;
FldAssign.Expr := ParseExpr;
Result := FldAssign;
Exit;
end;
raise EParseError.CreateFmt(
'Expected method call or assignment after chain at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
end;
Result := MCall;
end;
Result := MCall;
end;
end
else if Check(tkAssign) then
@ -1167,17 +1298,97 @@ begin
end
else
begin
{ Typecast-then-method-call statement: TCast(expr).Method(args)
or chained Ident(arg).Field.Method(args). Detect by peeking for
'(' followed by a '.' after the paren closure — parse directly
into a TFuncCallExpr receiver so the inner arg lives on. }
if Check(tkLParen) then
begin
FCallNode := TFuncCallExpr.Create;
FCallNode.Line := Line;
FCallNode.Col := Col;
FCallNode.Name := Name;
Advance; { '(' }
if not Check(tkRParen) then
begin
FCallNode.Args.Add(ParseExpr);
while Check(tkComma) do
begin
Advance;
FCallNode.Args.Add(ParseExpr);
end;
end;
Expect(tkRParen);
if Check(tkDot) then
begin
CastRcv := FCallNode;
{ Chain parse: each step reads '.Ident', then either '(' (call) or
more '.'. The final method call terminates as TMethodCallStmt. }
while Check(tkDot) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected field or method name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
SecondIdent := FCurrent.Value;
Advance;
if Check(tkLParen) then
begin
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := '';
MCall.Name := SecondIdent;
MCall.ObjExpr := CastRcv;
Advance; { consume '(' }
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
Result := MCall;
Exit;
end;
FldNode := TFieldAccessExpr.Create;
FldNode.Line := Line;
FldNode.Col := Col;
FldNode.Base := CastRcv;
FldNode.FieldName := SecondIdent;
CastRcv := FldNode;
end;
{ Field assignment through typecast/chain: TCast(expr).Field := value }
if Check(tkAssign) and (CastRcv is TFieldAccessExpr) then
begin
Advance; { consume ':=' }
FldAssign := TFieldAssignment.Create;
FldAssign.Line := Line;
FldAssign.Col := Col;
FldAssign.FieldName := TFieldAccessExpr(CastRcv).FieldName;
FldAssign.ObjExpr := TFieldAccessExpr(CastRcv).Base;
TFieldAccessExpr(CastRcv).Base := nil; { transfer ownership }
CastRcv.Free;
FldAssign.Expr := ParseExpr;
Result := FldAssign;
Exit;
end;
raise EParseError.CreateFmt(
'Expected method call after typecast at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
end;
{ No '.': it was a plain procedure/function call. Move args to a
fresh TProcCall using FPC-style Extract (removes without freeing). }
Call := TProcCall.Create;
Call.Line := Line;
Call.Col := Col;
Call.Name := Name;
while FCallNode.Args.Count > 0 do
Call.Args.Add(FCallNode.Args.Extract(FCallNode.Args.Items[0]));
FCallNode.Free;
Result := Call;
Exit;
end;
Call := TProcCall.Create;
Call.Line := Line;
Call.Col := Col;
Call.Name := Name;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseArgList(Call);
Expect(tkRParen);
end;
Result := Call;
end;
end;
@ -1424,8 +1635,9 @@ end;
function TParser.ParseCaseStmt: TCaseStmt;
var
Branch: TCaseBranch;
Stmt: TASTStmt;
Branch: TCaseBranch;
Stmt: TASTStmt;
CmpElse: TCompoundStmt;
begin
Result := TCaseStmt.Create;
try
@ -1457,7 +1669,28 @@ begin
if Check(tkElse) then
begin
Advance; { consume 'else' }
Result.ElseStmt := ParseStmt;
if Check(tkBegin) then
Result.ElseStmt := ParseCompoundStmt
else
begin
{ Collect all statements until 'end' — FPC allows multi-stmt else without begin..end }
CmpElse := TCompoundStmt.Create;
CmpElse.Stmts.Add(ParseStmt);
if Check(tkSemicolon) then Advance;
while not (Check(tkEnd) or Check(tkEOF)) do
begin
CmpElse.Stmts.Add(ParseStmt);
if Check(tkSemicolon) then Advance;
end;
if CmpElse.Stmts.Count = 1 then
begin
{ Unwrap single-item compound for simplicity }
Result.ElseStmt := TASTStmt(CmpElse.Stmts.Extract(CmpElse.Stmts.Items[0]));
CmpElse.Free;
end
else
Result.ElseStmt := CmpElse;
end;
if Check(tkSemicolon) then Advance;
end;
Expect(tkEnd);
@ -1611,8 +1844,10 @@ begin
end
else if Check(tkIs) then
begin
Advance;
IsNode := TIsExpr.Create;
IsNode.Line := FCurrent.Line;
IsNode.Col := FCurrent.Col;
Advance;
IsNode.Obj := Result;
IsNode.TypeName := FCurrent.Value;
Expect(tkIdent);
@ -1815,13 +2050,39 @@ begin
while Check(tkDot) and (PeekKind = tkIdent) do
begin
Advance; { consume '.' }
FldNode := TFieldAccessExpr.Create;
FldNode.Line := FCurrent.Line;
FldNode.Col := FCurrent.Col;
FldNode.Base := Result;
FldNode.FieldName := FCurrent.Value;
Advance; { consume field name }
Result := FldNode;
SecondName := FCurrent.Value;
Advance; { consume field/method name }
if Check(tkLParen) then
begin
{ Chained method call on expression }
MCallNode := TMethodCallExpr.Create;
MCallNode.Line := FCurrent.Line;
MCallNode.Col := FCurrent.Col;
MCallNode.ObjectName := '';
MCallNode.Name := SecondName;
MCallNode.ObjExpr := Result;
Advance; { consume '(' }
if not Check(tkRParen) then
begin
MCallNode.Args.Add(ParseExpr);
while Check(tkComma) do
begin
Advance;
MCallNode.Args.Add(ParseExpr);
end;
end;
Expect(tkRParen);
Result := MCallNode;
end
else
begin
FldNode := TFieldAccessExpr.Create;
FldNode.Line := FCurrent.Line;
FldNode.Col := FCurrent.Col;
FldNode.Base := Result;
FldNode.FieldName := SecondName;
Result := FldNode;
end;
end;
end;
end
@ -1844,6 +2105,52 @@ begin
end;
Expect(tkRParen);
Result := FCallNode;
{ Postfix chained field/method access: FuncOrCast(...).Member ... }
while Check(tkDot) and (PeekKind = tkIdent) do
begin
Advance; { consume '.' }
SecondName := FCurrent.Value;
Advance; { consume field/method name }
FldNode := TFieldAccessExpr.Create;
FldNode.Line := FCurrent.Line;
FldNode.Col := FCurrent.Col;
FldNode.Base := Result;
FldNode.FieldName := SecondName;
Result := FldNode;
if Check(tkLParen) then
begin
{ Method call with args on the chained expression — represent
by marking the access as a method call and parsing an arg list
into a synthesised TMethodCallExpr with Obj=Base. }
{ We convert the TFieldAccessExpr into an arg-bearing method call. }
Advance; { consume '(' }
if not Check(tkRParen) then
begin
FldNode.ResolvedType := nil; { placeholder to note args }
end;
{ Lift the FldNode to a method call by storing args via a
temporary TMethodCallExpr and using Base for the receiver. }
MCallNode := TMethodCallExpr.Create;
MCallNode.Line := FldNode.Line;
MCallNode.Col := FldNode.Col;
MCallNode.ObjectName := ''; { empty — receiver is Base expr }
MCallNode.Name := SecondName;
MCallNode.ObjExpr := FldNode.Base;
FldNode.Base := nil; { transfer ownership }
FldNode.Free;
if not Check(tkRParen) then
begin
MCallNode.Args.Add(ParseExpr);
while Check(tkComma) do
begin
Advance;
MCallNode.Args.Add(ParseExpr);
end;
end;
Expect(tkRParen);
Result := MCallNode;
end;
end;
end
else
begin

View file

@ -35,6 +35,7 @@ type
FProcIndex: TStringList; { 'ProcName' → TMethodDecl (not owned) }
FGenericFuncTemplates: TStringList; { base name → TMethodDecl template (not owned) }
FLoopDepth: Integer; { depth of enclosing while/for — Break only legal if > 0 }
FScopeDepth: Integer; { mirrors FTable scope depth; used to detect main-level globals }
FCurrentClass: TRecordTypeDesc; { class being analysed (set in AnalyseMethodDecl) }
{ Generic type instantiation: resolves 'TBox<Integer>' on demand. }
@ -277,9 +278,15 @@ begin
enum ↔ integer: allow assignment between enum and integer types }
if (AExpected.Kind = tyEnum) and AActual.IsNumeric then Exit;
if (AActual.Kind = tyEnum) and AExpected.IsNumeric then Exit;
{ Numeric widening/narrowing: allow between the integer family members }
if AExpected.IsNumeric and AActual.IsNumeric then Exit;
{ subtype assignment: TDerived → TBase is allowed }
if IsSubtypeOf(AActual, AExpected) then
Exit;
{ TObject accepts any class — universal base class }
if (AExpected.Kind = tyClass) and (AExpected.Name = 'TObject') and
(AActual.Kind = tyClass) then
Exit;
{ class → interface: allowed when the class implements that interface }
if (AExpected.Kind = tyInterface) and (AActual.Kind = tyClass) then
begin
@ -288,6 +295,15 @@ begin
if RT.ImplementsIntfAt(I) = AExpected then
Exit;
end;
{ Untyped pointer accepts any class/interface/string reference and vice-versa }
if (AExpected.Kind = tyPointer) and
(TPointerTypeDesc(AExpected).BaseType = nil) and
(AActual.Kind in [tyClass, tyInterface, tyString, tyPointer]) then
Exit;
if (AActual.Kind = tyPointer) and
(TPointerTypeDesc(AActual).BaseType = nil) and
(AExpected.Kind in [tyClass, tyInterface, tyString, tyPointer]) then
Exit;
SemanticError(
Format('Type mismatch in %s: expected ''%s'' but got ''%s''',
[AContext, AExpected.Name, AActual.Name]),
@ -1005,16 +1021,18 @@ begin
declarations, transferring the body so AnalyseMethodBodies can process it. }
LinkClassMethodImpls(ABlock);
LinkGenericClassMethodImpls(ABlock);
{ Register standalone proc/func signatures before class method bodies so that
methods can call free functions declared in the same block. }
AnalyseStandaloneDecls(ABlock);
AnalyseMethodBodies(ABlock);
FTable.PushScope;
Inc(FScopeDepth);
try
AnalyseVarDecls(ABlock);
{ Register standalone proc/func signatures before analysing bodies so that
mutually-recursive calls resolve correctly. }
AnalyseStandaloneDecls(ABlock);
AnalyseStandaloneBodies(ABlock);
AnalyseStmts(ABlock);
finally
Dec(FScopeDepth);
FTable.PopScope;
end;
end;
@ -1033,8 +1051,9 @@ begin
TD := FTable.TypeString
else
TD := FTable.TypeInteger;
Sym := TSymbol.Create(CD.Name, skConstant, TD);
Sym.ConstValue := CD.IntVal;
Sym := TSymbol.Create(CD.Name, skConstant, TD);
Sym.ConstValue := CD.IntVal;
Sym.ConstString := CD.StrVal;
if not FTable.Define(Sym) then
Sym.Free; { duplicate const — silently ignore }
end;
@ -1434,6 +1453,7 @@ begin
SavedClass := FCurrentClass;
FCurrentClass := AClassType;
FTable.PushScope;
Inc(FScopeDepth);
try
{ Define Self as a variable of the class type }
Sym := TSymbol.Create('Self', skVariable, AClassType);
@ -1468,8 +1488,10 @@ begin
end;
{ Analyse the method body block (pushes its own inner scope) }
AnalyseBlock(AMethod.Body);
if AMethod.Body <> nil then
AnalyseBlock(AMethod.Body);
finally
Dec(FScopeDepth);
FTable.PopScope;
FCurrentClass := SavedClass;
end;
@ -1556,6 +1578,15 @@ begin
ADecl.ResolvedReturnType := RetType;
end;
{ If an earlier forward declaration exists, update the index to point to
this implementation and skip re-registering the symbol. }
J := FProcIndex.IndexOf(ADecl.Name);
if (J >= 0) and (TMethodDecl(FProcIndex.Objects[J]).Body = nil) then
begin
FProcIndex.Objects[J] := ADecl;
Continue;
end;
{ Index for call resolution }
FProcIndex.AddObject(ADecl.Name, ADecl);
@ -1582,6 +1613,7 @@ var
Sym: TSymbol;
begin
FTable.PushScope;
Inc(FScopeDepth);
try
{ Define Result for functions }
if ADecl.ResolvedReturnType <> nil then
@ -1609,6 +1641,7 @@ begin
AnalyseBlock(ADecl.Body);
finally
Dec(FScopeDepth);
FTable.PopScope;
end;
end;
@ -1625,6 +1658,8 @@ begin
if ADecl.OwnerTypeName <> '' then Continue;
{ Generic templates are instantiated on demand — skip until first call }
if ADecl.TypeParams <> nil then Continue;
{ Forward declarations have no body; the later impl handles analysis }
if ADecl.Body = nil then Continue;
AnalyseStandaloneDecl(ADecl);
end;
end;
@ -1663,11 +1698,14 @@ begin
Decl.IsWeak := True;
end;
{ Depth 2 = inside the top-level program block — these are global variables. }
Decl.IsGlobal := (FScopeDepth = 1); { depth 1 = main program block (global scope) }
for J := 0 to Decl.Names.Count - 1 do
begin
VarName := Decl.Names[J];
Sym := TSymbol.Create(VarName, skVariable, Typ);
Sym.IsWeak := Decl.IsWeak;
Sym.IsWeak := Decl.IsWeak;
Sym.IsGlobal := Decl.IsGlobal;
if not FTable.Define(Sym) then
begin
Sym.Free;
@ -1714,6 +1752,7 @@ begin
SemanticError(
Format('Undeclared loop variable ''%s''', [ForS.VarName]),
ForS.Line, ForS.Col);
ForS.IsGlobal := (VarSym <> nil) and VarSym.IsGlobal;
if VarSym.Kind <> skVariable then
SemanticError(
Format('''%s'' is not a variable', [ForS.VarName]),
@ -1763,6 +1802,11 @@ begin
if FLoopDepth = 0 then
SemanticError('''break'' is not inside a loop', AStmt.Line, AStmt.Col);
end
else if AStmt is TContinueStmt then
begin
if FLoopDepth = 0 then
SemanticError('''continue'' is not inside a loop', AStmt.Line, AStmt.Col);
end
else if AStmt is TIfStmt then
begin
IfS := TIfStmt(AStmt);
@ -1836,13 +1880,93 @@ var
Par: TMethodParam;
ArgType: TTypeDesc;
I: Integer;
ObjType: TTypeDesc;
begin
{ Call on a receiver expression: AProg.UsedUnits.Add(UName) }
if ACall.ObjExpr <> nil then
begin
ObjType := AnalyseExpr(ACall.ObjExpr);
if not (ObjType.Kind in [tyClass, tyInterface]) then
SemanticError(
Format('Receiver of ''.%s'' must be a class or interface', [ACall.Name]),
ACall.Line, ACall.Col);
RT := TRecordTypeDesc(ObjType);
MDecl := FindMethodDecl(RT.Name, ACall.Name);
if (MDecl = nil) and SameText(ACall.Name, 'Free') and (ACall.Args.Count = 0) then
begin
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := nil;
Exit;
end;
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, ACall.Name]),
ACall.Line, ACall.Col);
if ACall.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[RT.Name, ACall.Name, MDecl.Params.Count, ACall.Args.Count]),
ACall.Line, ACall.Col);
for I := 0 to ACall.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(ACall.Args[I]));
Par := TMethodParam(MDecl.Params[I]);
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, ACall.Name]),
ACall.Line, ACall.Col);
end;
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
Exit;
end;
ObjSym := FTable.Lookup(ACall.ObjectName);
if ObjSym = nil then
begin
{ Implicit Self.Field.Method — ObjectName is a field of current class }
if FCurrentClass <> nil then
begin
ACall.ImplicitBaseInfo :=
FCurrentClass.FindField(ACall.ObjectName);
if (ACall.ImplicitBaseInfo <> nil) and
(ACall.ImplicitBaseInfo.TypeDesc.Kind in [tyClass, tyInterface]) then
begin
ACall.IsImplicitSelf := True;
RT := TRecordTypeDesc(ACall.ImplicitBaseInfo.TypeDesc);
MDecl := FindMethodDecl(RT.Name, ACall.Name);
if (MDecl = nil) and SameText(ACall.Name, 'Free') and (ACall.Args.Count = 0) then
begin
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := nil;
Exit;
end;
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, ACall.Name]),
ACall.Line, ACall.Col);
if ACall.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[RT.Name, ACall.Name, MDecl.Params.Count, ACall.Args.Count]),
ACall.Line, ACall.Col);
for I := 0 to ACall.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(ACall.Args[I]));
Par := TMethodParam(MDecl.Params[I]);
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, ACall.Name]),
ACall.Line, ACall.Col);
end;
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
Exit;
end;
end;
SemanticError(
Format('Undeclared variable ''%s''', [ACall.ObjectName]),
ACall.Line, ACall.Col);
if ObjSym.Kind <> skVariable then
end;
if not (ObjSym.Kind in [skVariable, skParameter, skVarParameter]) then
SemanticError(
Format('''%s'' is not a variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
@ -1862,6 +1986,7 @@ begin
{ Args for interface methods not checked in Phase 3 (no param info stored) }
ACall.ResolvedClassType := ObjSym.TypeDesc;
ACall.ResolvedMethod := nil; { nil = interface dispatch, not class dispatch }
ACall.IsGlobal := ObjSym.IsGlobal;
Exit;
end;
@ -1874,6 +1999,7 @@ begin
begin
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := nil; { nil signals built-in Free to codegen }
ACall.IsGlobal := ObjSym.IsGlobal;
Exit;
end;
SemanticError(
@ -1898,6 +2024,7 @@ begin
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
ACall.IsGlobal := ObjSym.IsGlobal;
end;
procedure TSemanticAnalyser.AnalyseAssignment(AAssign: TAssignment);
@ -1934,6 +2061,7 @@ begin
AAssign.IsVarParam := (VarSym.Kind = skVarParameter);
AAssign.ResolvedLhsType := VarSym.TypeDesc;
AAssign.IsWeakLhs := VarSym.IsWeak;
AAssign.IsGlobal := VarSym.IsGlobal;
ExprType := AnalyseExpr(AAssign.Expr);
CheckTypesMatch(VarSym.TypeDesc, ExprType, 'assignment', AAssign.Line, AAssign.Col);
@ -1989,15 +2117,85 @@ var
RecSym: TSymbol;
RT: TRecordTypeDesc;
FldInfo: TFieldInfo;
BaseInfo: TFieldInfo;
BaseType: TTypeDesc;
PropInfo: TPropertyInfo;
ExprType: TTypeDesc;
ObjType: TTypeDesc;
begin
{ ObjExpr path: receiver is an arbitrary expression (e.g. typecast result) }
if AAssign.ObjExpr <> nil then
begin
ObjType := AnalyseExpr(AAssign.ObjExpr);
if not (ObjType.Kind in [tyRecord, tyClass]) then
SemanticError(
Format('Field assignment: expression is not a record or class (got %s)',
[ObjType.Name]),
AAssign.Line, AAssign.Col);
RT := TRecordTypeDesc(ObjType);
FldInfo := RT.FindField(AAssign.FieldName);
if FldInfo = nil then
begin
PropInfo := RT.FindProperty(AAssign.FieldName);
if (PropInfo <> nil) and (PropInfo.WriteField <> '') then
begin
AAssign.FieldName := PropInfo.WriteField;
FldInfo := RT.FindField(PropInfo.WriteField);
end
else
SemanticError(
Format('Type ''%s'' has no field ''%s''', [ObjType.Name, AAssign.FieldName]),
AAssign.Line, AAssign.Col);
end;
AAssign.IsClassAccess := ObjType.Kind = tyClass;
AAssign.FieldInfo := FldInfo;
ExprType := AnalyseExpr(AAssign.Expr);
CheckTypesMatch(FldInfo.TypeDesc, ExprType, 'field assignment',
AAssign.Line, AAssign.Col);
Exit;
end;
RecSym := FTable.Lookup(AAssign.RecordName);
if RecSym = nil then
begin
{ Implicit Self.Field.Subfield — RecordName is a field of current class }
if FCurrentClass <> nil then
begin
BaseInfo := FCurrentClass.FindField(AAssign.RecordName);
if (BaseInfo <> nil) and
(BaseInfo.TypeDesc.Kind in [tyRecord, tyClass]) then
begin
AAssign.IsImplicitSelf := True;
AAssign.ImplicitBaseInfo := BaseInfo;
AAssign.IsClassAccess := BaseInfo.TypeDesc.Kind = tyClass;
BaseType := BaseInfo.TypeDesc;
RT := TRecordTypeDesc(BaseType);
FldInfo := RT.FindField(AAssign.FieldName);
if FldInfo = nil then
begin
PropInfo := RT.FindProperty(AAssign.FieldName);
if (PropInfo <> nil) and (PropInfo.WriteField <> '') then
begin
AAssign.FieldName := PropInfo.WriteField;
FldInfo := RT.FindField(PropInfo.WriteField);
end
else
SemanticError(
Format('Type ''%s'' has no field ''%s''',
[AAssign.RecordName, AAssign.FieldName]),
AAssign.Line, AAssign.Col);
end;
AAssign.FieldInfo := FldInfo;
ExprType := AnalyseExpr(AAssign.Expr);
CheckTypesMatch(FldInfo.TypeDesc, ExprType, 'field assignment',
AAssign.Line, AAssign.Col);
Exit;
end;
end;
SemanticError(
Format('Undeclared variable ''%s''', [AAssign.RecordName]),
AAssign.Line, AAssign.Col);
if RecSym.Kind <> skVariable then
end;
if not ((RecSym.Kind = skVariable) or (RecSym.Kind = skParameter)) then
SemanticError(
Format('''%s'' is not a variable', [AAssign.RecordName]),
AAssign.Line, AAssign.Col);
@ -2007,6 +2205,7 @@ begin
AAssign.Line, AAssign.Col);
AAssign.IsClassAccess := RecSym.TypeDesc.Kind = tyClass;
AAssign.IsGlobal := RecSym.IsGlobal;
RT := TRecordTypeDesc(RecSym.TypeDesc);
FldInfo := RT.FindField(AAssign.FieldName);
@ -2057,6 +2256,30 @@ begin
Sym := FTable.Lookup(ACall.Name);
if Sym = nil then
begin
{ Implicit Self.Method() — name is a method of the current class }
if FCurrentClass <> nil then
begin
MDecl := FindMethodDecl(FCurrentClass.Name, ACall.Name);
if MDecl <> nil then
begin
if ACall.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[FCurrentClass.Name, ACall.Name, MDecl.Params.Count, ACall.Args.Count]),
ACall.Line, ACall.Col);
for I := 0 to ACall.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params[I]);
ArgType := AnalyseExpr(TASTExpr(ACall.Args[I]));
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, ACall.Name]),
ACall.Line, ACall.Col);
end;
ACall.ResolvedDecl := MDecl;
ACall.IsImplicitSelfMethod := True;
Exit;
end;
end;
{ Try on-demand instantiation of a generic function }
if Pos('<', ACall.Name) > 0 then
InstantiateGenericFunc(ACall.Name);
@ -2143,6 +2366,32 @@ begin
Sym := FTable.Lookup(AExpr.Name);
if Sym = nil then
begin
{ Implicit Self.Method() on a function method of the current class }
if FCurrentClass <> nil then
begin
MDecl := FindMethodDecl(FCurrentClass.Name, AExpr.Name);
if MDecl <> nil then
begin
if AExpr.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[FCurrentClass.Name, AExpr.Name, MDecl.Params.Count, AExpr.Args.Count]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params[I]);
ArgType := AnalyseExpr(TASTExpr(AExpr.Args[I]));
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
end;
AExpr.ResolvedDecl := MDecl;
AExpr.IsImplicitSelfMethod := True;
Result := MDecl.ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
end;
{ Try on-demand instantiation of a generic function }
if Pos('<', AExpr.Name) > 0 then
InstantiateGenericFunc(AExpr.Name);
@ -2217,7 +2466,8 @@ begin
Exit;
end;
if SameText(AExpr.Name, 'UpperCase') or SameText(AExpr.Name, 'LowerCase') then
if SameText(AExpr.Name, 'UpperCase') or SameText(AExpr.Name, 'LowerCase')
or SameText(AExpr.Name, 'Trim') then
begin
if AExpr.Args.Count <> 1 then
SemanticError(AExpr.Name + ' requires exactly one argument', AExpr.Line, AExpr.Col);
@ -2280,6 +2530,16 @@ begin
Exit;
end;
if SameText(AExpr.Name, 'Chr') then
begin
if AExpr.Args.Count <> 1 then
SemanticError('Chr requires exactly 1 argument', AExpr.Line, AExpr.Col);
AnalyseExpr(TASTExpr(AExpr.Args[0]));
Result := FTable.TypeString;
AExpr.ResolvedType := Result;
Exit;
end;
if SameText(AExpr.Name, 'CompareStr') or SameText(AExpr.Name, 'CompareText') then
begin
if AExpr.Args.Count <> 2 then
@ -2389,15 +2649,147 @@ var
ArgType: TTypeDesc;
I: Integer;
IntfDesc: TInterfaceTypeDesc;
ObjType: TTypeDesc;
begin
{ Call on an arbitrary expression (e.g. TCast(x).Method(y)) }
if AExpr.ObjExpr <> nil then
begin
ObjType := AnalyseExpr(AExpr.ObjExpr);
if not (ObjType.Kind in [tyClass, tyInterface]) then
SemanticError(
Format('Receiver of ''.%s'' must be a class or interface', [AExpr.Name]),
AExpr.Line, AExpr.Col);
if ObjType.Kind = tyInterface then
begin
IntfDesc := TInterfaceTypeDesc(ObjType);
if not IntfDesc.HasMethod(AExpr.Name) then
SemanticError(
Format('Interface ''%s'' has no method ''%s''',
[ObjType.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args[I]));
AExpr.ResolvedClassType := ObjType;
AExpr.ResolvedMethod := nil;
Result := FindTypeOrInstantiate(
IntfDesc.MethodReturnTypeName(IntfDesc.MethodIndex(AExpr.Name)));
if Result = nil then Result := FTable.TypeInteger;
AExpr.ResolvedType := Result;
Exit;
end;
RT := TRecordTypeDesc(ObjType);
MDecl := FindMethodDecl(RT.Name, AExpr.Name);
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
if AExpr.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[RT.Name, AExpr.Name, MDecl.Params.Count, AExpr.Args.Count]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(AExpr.Args[I]));
Par := TMethodParam(MDecl.Params[I]);
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
end;
AExpr.ResolvedClassType := RT;
AExpr.ResolvedMethod := MDecl;
Result := MDecl.ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
ObjSym := FTable.Lookup(AExpr.ObjectName);
if ObjSym = nil then
begin
{ Implicit Self.Field.Method — ObjectName is a field of current class.
Synthesise a receiver expression that reads the field. }
if FCurrentClass <> nil then
begin
ObjType := nil;
ObjSym := nil;
begin
{ Attempt field lookup and rewrite AExpr.ObjExpr to read Self.Field }
AExpr.ObjExpr := TIdentExpr.Create;
TIdentExpr(AExpr.ObjExpr).Name := AExpr.ObjectName;
TIdentExpr(AExpr.ObjExpr).Line := AExpr.Line;
TIdentExpr(AExpr.ObjExpr).Col := AExpr.Col;
try
ObjType := AnalyseExpr(AExpr.ObjExpr);
except
AExpr.ObjExpr.Free;
AExpr.ObjExpr := nil;
SemanticError(
Format('Undeclared identifier ''%s''', [AExpr.ObjectName]),
AExpr.Line, AExpr.Col);
end;
end;
if (ObjType = nil) or not (ObjType.Kind in [tyClass, tyInterface]) then
begin
AExpr.ObjExpr.Free;
AExpr.ObjExpr := nil;
SemanticError(
Format('Undeclared identifier ''%s''', [AExpr.ObjectName]),
AExpr.Line, AExpr.Col);
end;
AExpr.ObjectName := '';
if ObjType.Kind = tyInterface then
begin
IntfDesc := TInterfaceTypeDesc(ObjType);
if not IntfDesc.HasMethod(AExpr.Name) then
SemanticError(
Format('Interface ''%s'' has no method ''%s''',
[ObjType.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args[I]));
AExpr.ResolvedClassType := ObjType;
AExpr.ResolvedMethod := nil;
Result := FindTypeOrInstantiate(
IntfDesc.MethodReturnTypeName(IntfDesc.MethodIndex(AExpr.Name)));
if Result = nil then Result := FTable.TypeInteger;
AExpr.ResolvedType := Result;
Exit;
end;
RT := TRecordTypeDesc(ObjType);
MDecl := FindMethodDecl(RT.Name, AExpr.Name);
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
if AExpr.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[RT.Name, AExpr.Name, MDecl.Params.Count, AExpr.Args.Count]),
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(AExpr.Args[I]));
Par := TMethodParam(MDecl.Params[I]);
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, AExpr.Name]),
AExpr.Line, AExpr.Col);
end;
AExpr.ResolvedClassType := RT;
AExpr.ResolvedMethod := MDecl;
Result := MDecl.ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
SemanticError(
Format('Undeclared identifier ''%s''', [AExpr.ObjectName]),
AExpr.Line, AExpr.Col);
end;
{ Constructor call with args: TypeName.Create(arg1, arg2, ...) }
if (ObjSym.Kind = skType) and SameText(AExpr.Name, 'Create') then
{ Constructor call with args: TypeName.Create(arg1, arg2, ...) or any
method on a class type starting with Create (e.g. CreateFmt). }
if (ObjSym.Kind = skType) and
(SameText(AExpr.Name, 'Create') or
(Pos('Create', AExpr.Name) = 1)) then
begin
if ObjSym.TypeDesc.Kind <> tyClass then
SemanticError(
@ -2405,8 +2797,8 @@ begin
AExpr.Line, AExpr.Col);
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args[I]));
{ Try to find a user-defined Create method for type checking }
MDecl := FindMethodDecl(AExpr.ObjectName, 'Create');
{ Try to find a user-defined constructor method for type checking }
MDecl := FindMethodDecl(AExpr.ObjectName, AExpr.Name);
AExpr.ResolvedMethod := MDecl;
AExpr.ResolvedClassType := ObjSym.TypeDesc;
AExpr.IsConstructorCall := True;
@ -2414,7 +2806,7 @@ begin
Exit;
end;
if ObjSym.Kind <> skVariable then
if not (ObjSym.Kind in [skVariable, skParameter, skVarParameter]) then
SemanticError(
Format('''%s'' is not a variable', [AExpr.ObjectName]),
AExpr.Line, AExpr.Col);
@ -2436,6 +2828,7 @@ begin
AnalyseExpr(TASTExpr(AExpr.Args[I]));
AExpr.ResolvedClassType := ObjSym.TypeDesc;
AExpr.ResolvedMethod := nil; { nil = interface dispatch }
AExpr.IsGlobal := ObjSym.IsGlobal;
{ Look up return type from interface method descriptor }
Result := FindTypeOrInstantiate(
IntfDesc.MethodReturnTypeName(IntfDesc.MethodIndex(AExpr.Name)));
@ -2473,13 +2866,16 @@ begin
AExpr.ResolvedClassType := RT;
AExpr.ResolvedMethod := MDecl;
AExpr.IsGlobal := ObjSym.IsGlobal;
Result := MDecl.ResolvedReturnType;
end;
function TSemanticAnalyser.AnalyseExpr(AExpr: TASTExpr): TTypeDesc;
var
Sym: TSymbol;
FldInfo: TFieldInfo;
Sym: TSymbol;
FldInfo: TFieldInfo;
PropInfo: TPropertyInfo;
NoArgIdx: Integer;
begin
if AExpr is TNilLiteral then
Result := FTable.TypeNil
@ -2504,21 +2900,73 @@ begin
AExpr.ResolvedType := Result;
Exit;
end;
{ Bare zero-arg method call: e.g. TokenText inside a method }
TIdentExpr(AExpr).ImplicitMethodDecl :=
FindMethodDecl(FCurrentClass.Name, TIdentExpr(AExpr).Name);
if TIdentExpr(AExpr).ImplicitMethodDecl <> nil then
begin
TIdentExpr(AExpr).IsImplicitSelfMethod := True;
Result :=
TMethodDecl(TIdentExpr(AExpr).ImplicitMethodDecl).ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
{ Property of current class, method-backed: rewrite to the read method }
FldInfo := nil; { reuse PropInfo via local }
begin
PropInfo := FCurrentClass.FindProperty(TIdentExpr(AExpr).Name);
if PropInfo <> nil then
begin
if PropInfo.ReadMethod <> '' then
begin
TIdentExpr(AExpr).ImplicitMethodDecl :=
FindMethodDecl(FCurrentClass.Name, PropInfo.ReadMethod);
if TIdentExpr(AExpr).ImplicitMethodDecl <> nil then
begin
TIdentExpr(AExpr).IsImplicitSelfMethod := True;
TIdentExpr(AExpr).Name := PropInfo.ReadMethod;
Result := PropInfo.TypeDesc;
AExpr.ResolvedType := Result;
Exit;
end;
end
else if PropInfo.ReadField <> '' then
begin
FldInfo := FCurrentClass.FindField(PropInfo.ReadField);
if FldInfo <> nil then
begin
TIdentExpr(AExpr).IsImplicitSelf := True;
TIdentExpr(AExpr).ImplicitFieldInfo := FldInfo;
Result := FldInfo.TypeDesc;
AExpr.ResolvedType := Result;
Exit;
end;
end;
end;
end;
end;
SemanticError(
Format('Undeclared identifier ''%s''', [TIdentExpr(AExpr).Name]),
AExpr.Line, AExpr.Col);
end;
TIdentExpr(AExpr).IsVarParam := (Sym.Kind = skVarParameter);
TIdentExpr(AExpr).IsGlobal := Sym.IsGlobal;
if Sym.Kind = skConstant then
begin
TIdentExpr(AExpr).IsConstant := True;
TIdentExpr(AExpr).ConstValue := Sym.ConstValue;
TIdentExpr(AExpr).IsConstant := True;
TIdentExpr(AExpr).ConstValue := Sym.ConstValue;
TIdentExpr(AExpr).ConstString := Sym.ConstString;
end;
{ Bare builtin function used without parens (e.g. ParamCount) — mark for codegen }
if (Sym.Kind = skFunction) and (Sym.TypeDesc <> nil) and
(FProcIndex.IndexOf(TIdentExpr(AExpr).Name) < 0) then
{ Bare function reference without parens — mark as no-arg call for codegen.
Covers both builtins (not in FProcIndex) and user-defined standalone functions. }
if (Sym.Kind = skFunction) and (Sym.TypeDesc <> nil) then
begin
TIdentExpr(AExpr).IsNoArgFuncCall := True;
{ Propagate TMethodDecl for user-defined functions so codegen can emit the call }
NoArgIdx := FProcIndex.IndexOf(TIdentExpr(AExpr).Name);
if NoArgIdx >= 0 then
TIdentExpr(AExpr).NoArgFuncDecl := FProcIndex.Objects[NoArgIdx];
end;
Result := Sym.TypeDesc;
end
else if AExpr is TFuncCallExpr then
@ -2582,6 +3030,14 @@ begin
Result := PropInfo.TypeDesc;
Exit;
end;
{ Zero-arg method call via field access: Obj.Method (no parens) }
AAccess.ResolvedMethod := FindMethodDecl(RT.Name, AAccess.FieldName);
if AAccess.ResolvedMethod <> nil then
begin
AAccess.IsMethodCall := True;
Result := TMethodDecl(AAccess.ResolvedMethod).ResolvedReturnType;
Exit;
end;
SemanticError(
Format('Type ''%s'' has no field ''%s''',
[BaseType.Name, AAccess.FieldName]),
@ -2594,9 +3050,53 @@ begin
RecSym := FTable.Lookup(AAccess.RecordName);
if RecSym = nil then
begin
{ Implicit Self.RecordName.FieldName — RecordName is a field of current class }
if FCurrentClass <> nil then
begin
FldInfo := FCurrentClass.FindField(AAccess.RecordName);
if (FldInfo <> nil) and
(FldInfo.TypeDesc.Kind in [tyRecord, tyClass]) then
begin
AAccess.IsImplicitSelf := True;
AAccess.ImplicitBaseInfo := FldInfo;
AAccess.IsClassAccess := FldInfo.TypeDesc.Kind = tyClass;
RT := TRecordTypeDesc(FldInfo.TypeDesc);
AAccess.FieldInfo := RT.FindField(AAccess.FieldName);
if AAccess.FieldInfo = nil then
begin
{ Field-backed property on the implicit-Self field's type }
PropInfo := RT.FindProperty(AAccess.FieldName);
if (PropInfo <> nil) and (PropInfo.ReadField <> '') then
begin
AAccess.FieldName := PropInfo.ReadField;
AAccess.FieldInfo := RT.FindField(PropInfo.ReadField);
Result := PropInfo.TypeDesc;
AAccess.ResolvedType := Result;
Exit;
end;
{ Zero-arg method on the implicit-Self field: FTok.NextToken }
AAccess.ResolvedMethod := FindMethodDecl(RT.Name, AAccess.FieldName);
if AAccess.ResolvedMethod <> nil then
begin
AAccess.IsMethodCall := True;
Result := TMethodDecl(AAccess.ResolvedMethod).ResolvedReturnType;
AAccess.ResolvedType := Result;
Exit;
end;
SemanticError(
Format('Type ''%s'' has no field ''%s''',
[AAccess.RecordName, AAccess.FieldName]),
AAccess.Line, AAccess.Col);
end;
Result := AAccess.FieldInfo.TypeDesc;
Exit;
end;
end;
SemanticError(
Format('Undeclared identifier ''%s''', [AAccess.RecordName]),
AAccess.Line, AAccess.Col);
end;
{ Constructor call: TypeName.Create }
if RecSym.Kind = skType then
@ -2612,12 +3112,13 @@ begin
[AAccess.FieldName, AAccess.RecordName]),
AAccess.Line, AAccess.Col);
AAccess.IsConstructorCall := True;
AAccess.ResolvedMethod := FindMethodDecl(TRecordTypeDesc(RecSym.TypeDesc).Name, 'Create');
Result := RecSym.TypeDesc;
Exit;
end;
{ Field access on variable }
if RecSym.Kind <> skVariable then
{ Field access on variable or parameter }
if not (RecSym.Kind in [skVariable, skParameter, skVarParameter]) then
SemanticError(
Format('''%s'' is not a variable or type', [AAccess.RecordName]),
AAccess.Line, AAccess.Col);
@ -2628,11 +3129,21 @@ begin
AAccess.Line, AAccess.Col);
AAccess.IsClassAccess := RecSym.TypeDesc.Kind = tyClass;
AAccess.IsGlobal := RecSym.IsGlobal;
RT := TRecordTypeDesc(RecSym.TypeDesc);
FldInfo := RT.FindField(AAccess.FieldName);
if FldInfo = nil then
begin
{ Zero-arg method call via field access: Obj.Method (no parens) }
AAccess.ResolvedMethod := FindMethodDecl(RT.Name, AAccess.FieldName);
if AAccess.ResolvedMethod <> nil then
begin
AAccess.IsMethodCall := True;
Result := TMethodDecl(AAccess.ResolvedMethod).ResolvedReturnType;
AAccess.ResolvedType := Result;
Exit;
end;
{ Check if this is a property access }
PropInfo := RT.FindProperty(AAccess.FieldName);
if PropInfo <> nil then
@ -2697,7 +3208,13 @@ begin
(LType = RType) or
((LType.Kind = tyNil) and (RType.Kind in [tyClass, tyInterface, tyPointer])) or
((RType.Kind = tyNil) and (LType.Kind in [tyClass, tyInterface, tyPointer])) or
((LType.Kind = tyPointer) and (RType.Kind = tyPointer))
((LType.Kind = tyPointer) and (RType.Kind = tyPointer)) or
{ Class comparisons: allow subtype on either side }
((LType.Kind = tyClass) and (RType.Kind = tyClass) and
(IsSubtypeOf(LType, RType) or IsSubtypeOf(RType, LType))) or
{ TObject is universal base class }
((LType.Kind = tyClass) and (RType.Kind = tyClass) and
((LType.Name = 'TObject') or (RType.Name = 'TObject')))
) then
CheckTypesMatch(LType, RType,
Format('comparison ''%s''', [BinaryOpName(ABin.Op)]),
@ -2746,7 +3263,9 @@ var
TargetType: TTypeDesc;
begin
ObjType := AnalyseExpr(AExpr.Obj);
if ObjType.Kind <> tyClass then
{ Allow untyped Pointer on left — GetObject/Get return Pointer, used with 'is' }
if not ((ObjType.Kind = tyClass) or (ObjType.Kind = tyPointer) or
(ObjType.Kind = tyInterface)) then
SemanticError(
Format('''is'' requires a class instance on the left, got ''%s''',
[ObjType.Name]),

View file

@ -188,10 +188,13 @@ type
Kind: TSymbolKind;
TypeDesc: TTypeDesc; { not owned; nil for procedures }
Params: TObjectList; { owned TParamDesc; populated for procedures/functions }
ConstValue: Int64; { valid when Kind = skConstant }
ConstValue: Int64; { valid when Kind = skConstant; integer/bool/enum value }
ConstString: string; { valid when Kind = skConstant and type is tyString }
IsWeak: Boolean; { true for variables declared [Weak]; codegen
keys off this to emit _WeakAssign instead
of the strong addref/release pattern. }
IsGlobal: Boolean; { true for program-level variables; codegen uses
QBE data-section storage instead of stack alloc }
constructor Create(const AName: string; AKind: TSymbolKind; AType: TTypeDesc);
destructor Destroy; override;
end;
@ -320,7 +323,7 @@ begin
case Kind of
tyInteger, tyUInt32, tyEnum: Result := 4;
tyInt64: Result := 8;
tyByte, tyBoolean: Result := 1;
tyByte, tyBoolean: Result := 4; { stored as word, same as AllocAlign }
tyString: Result := 8; { pointer size on 64-bit }
tyRecord: Result := TRecordTypeDesc(Self).TotalSize;
tyNil: Result := 8;
@ -846,6 +849,8 @@ begin
Define(Sym);
Sym := TSymbol.Create('LowerCase', skFunction, FTypeString);
Define(Sym);
Sym := TSymbol.Create('Trim', skFunction, FTypeString);
Define(Sym);
Sym := TSymbol.Create('SameText', skFunction, FTypeBoolean);
Define(Sym);
Sym := TSymbol.Create('IntToStr', skFunction, FTypeString);
@ -858,6 +863,8 @@ begin
Define(Sym);
Sym := TSymbol.Create('OrdAt', skFunction, FTypeInteger);
Define(Sym);
Sym := TSymbol.Create('Chr', skFunction, FTypeString);
Define(Sym);
{ Memory utilities }
Sym := TSymbol.Create('ZeroMem', skProcedure, nil); Define(Sym);
Sym := TSymbol.Create('_ClassAddRef', skProcedure, nil); Define(Sym);
@ -869,7 +876,9 @@ begin
Sym := TSymbol.Create('ReadFile', skFunction, FTypeString); Define(Sym);
Sym := TSymbol.Create('WriteFile', skProcedure, nil); Define(Sym);
Sym := TSymbol.Create('AppendFile', skProcedure, nil); Define(Sym);
Sym := TSymbol.Create('FileExists', skFunction, FTypeBoolean); Define(Sym);
Sym := TSymbol.Create('FileExists', skFunction, FTypeBoolean); Define(Sym);
Sym := TSymbol.Create('DeleteFile', skProcedure, nil); Define(Sym);
Sym := TSymbol.Create('CurrentExceptionMessage', skFunction, FTypeString); Define(Sym);
{ Environment and process }
Sym := TSymbol.Create('GetEnvVar', skFunction, FTypeString); Define(Sym);
Sym := TSymbol.Create('Exec', skFunction, FTypeInteger); Define(Sym);

View file

@ -511,8 +511,8 @@ begin
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin end.');
AssertTrue('alloc8 for F', Pos('_var_F', IR) > 0);
AssertTrue('8-byte alloc', Pos('alloc8', IR) > 0);
{ Program-level class var F is a data-section global pointer slot }
AssertTrue('data decl for F', Pos('$F', IR) > 0);
end;
procedure TClassTests.TestCodegen_ClassVar_ZeroInit;
@ -527,7 +527,8 @@ begin
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin end.');
AssertTrue('zero init', Pos('storel 0', IR) > 0);
{ Program-level class var F is zero-initialised via data section entry }
AssertTrue('zero init via data section', Pos('data $F = { l 0 }', IR) > 0);
end;
procedure TClassTests.TestCodegen_Constructor_CallsClassAlloc;
@ -567,7 +568,8 @@ begin
'begin' + LineEnding +
' F.X := 42' + LineEnding +
'end.');
AssertTrue('loads pointer', Pos('loadl %_var_F', IR) > 0);
{ F is a program-level global — loaded via $F }
AssertTrue('loads pointer', Pos('loadl $F', IR) > 0);
AssertTrue('stores value', Pos('storew', IR) > 0);
end;
@ -585,7 +587,8 @@ begin
'begin' + LineEnding +
' N := F.X' + LineEnding +
'end.');
AssertTrue('loads pointer', Pos('loadl %_var_F', IR) > 0);
{ F is a program-level global — loaded via $F }
AssertTrue('loads pointer', Pos('loadl $F', IR) > 0);
AssertTrue('loads field', Pos('loadw', IR) > 0);
end;
@ -689,7 +692,7 @@ begin
becomes a no-op. }
IR := GenIR(SrcFree);
AssertTrue('calls _ClassRelease', Pos('call $_ClassRelease', IR) > 0);
AssertTrue('nil-outs slot after Free', Pos('storel 0, %_var_', IR) > 0);
AssertTrue('nil-outs slot after Free', (Pos('storel 0, %_var_', IR) > 0) or (Pos('storel 0, $', IR) > 0));
AssertTrue('does not call C free() directly', Pos('call $free(', IR) = 0);
end;

View file

@ -176,8 +176,9 @@ var
IR: string;
begin
IR := GenerateIR('program P; var x: Integer; begin end.');
AssertTrue('Has alloc for x',
IRContains(IR, '%_var_x =l alloc4 1'));
{ Program-level var is a data-section global, not a stack alloc }
AssertTrue('Has data decl for x',
IRContains(IR, 'data $x'));
end;
procedure TCodeGenTests.TestAssignment_HasStorew;
@ -187,7 +188,8 @@ begin
IR := GenerateIR(
'program P; var n: Integer; begin n := 7 end.');
AssertTrue('Has storew', IRContains(IR, 'storew'));
AssertTrue('Stores to n', IRContains(IR, 'storew %_t0, %_var_n'));
{ Program-level var n is a global: store goes to $n }
AssertTrue('Stores to n', IRContains(IR, 'storew %_t0, $n'));
end;
procedure TCodeGenTests.TestAssignment_LoadAndStore;
@ -196,8 +198,9 @@ var
begin
IR := GenerateIR(
'program P; var x, y: Integer; begin x := 1; y := x end.');
AssertTrue('Loads x', IRContains(IR, 'loadw %_var_x'));
AssertTrue('Stores y', IRContains(IR, '%_var_y'));
{ Program-level vars x, y are globals: load/store use $name }
AssertTrue('Loads x', IRContains(IR, 'loadw $x'));
AssertTrue('Stores y', IRContains(IR, '$y'));
end;
{ Arithmetic }

View file

@ -465,9 +465,9 @@ var
IR: string;
begin
IR := GenIR(SrcGetterClass);
{ The call result is assigned into a temp and then stored into %_var_N }
{ N is a program-level global; the call result is stored into $N }
AssertTrue('result stored into N',
Pos('%_var_N', IR) > 0);
Pos('$N', IR) > 0);
end;
procedure TFunctionTests.TestCodegen_Function_ReturnsResult;

View file

@ -265,8 +265,9 @@ procedure TInheritTests.TestCodegen_Nil_StoresZero;
var IR: string;
begin
IR := GenIR(SrcSelfRef);
{ N.Next := nil should store 0 (null pointer) }
AssertTrue('nil stores 0', Pos('storel 0', IR) > 0);
{ N is a data-section global; N.Next := nil loads 0 and stores via a temp.
Verify the nil load and that the global $N is present. }
AssertTrue('nil stores 0', Pos('copy 0', IR) > 0);
end;
procedure TInheritTests.TestCodegen_Nil_CompareEmitsCeql;

View file

@ -481,8 +481,9 @@ var
IR: string;
begin
IR := GenIR(SrcInterfaceVar);
AssertTrue('obj slot for F', Pos('_var_F_obj', IR) > 0);
AssertTrue('itab slot for F', Pos('_var_F_itab', IR) > 0);
{ F is a program-level global — slots are $F_obj and $F_itab }
AssertTrue('obj slot for F', Pos('$F_obj', IR) > 0);
AssertTrue('itab slot for F', Pos('$F_itab', IR) > 0);
end;
procedure TInterfaceTests.TestCodegen_InterfaceMethodCall_IndirectDispatch;
@ -491,7 +492,7 @@ var
begin
IR := GenIR(SrcInterfaceVar);
{ Interface dispatch loads the itab pointer and calls indirectly }
AssertTrue('loads itab pointer', Pos('_var_F_itab', IR) > 0);
AssertTrue('loads itab pointer', Pos('$F_itab', IR) > 0);
AssertTrue('indirect call via register', Pos('call %', IR) > 0);
end;
@ -680,10 +681,11 @@ begin
_F_obj and _F_itab are loaded for the read side and both _G_obj and
_G_itab are stored on the write side. }
IR := GenIR(SrcIntfToIntf);
AssertTrue('reads F_obj', Pos('loadl %_var_F_obj', IR) > 0);
AssertTrue('reads F_itab', Pos('loadl %_var_F_itab', IR) > 0);
{ F, G are program-level globals — slots accessed via $F_obj, $F_itab etc. }
AssertTrue('reads F_obj', Pos('loadl $F_obj', IR) > 0);
AssertTrue('reads F_itab', Pos('loadl $F_itab', IR) > 0);
AssertTrue('writes G_obj', Pos('storel ', IR) > 0);
AssertTrue('writes G_itab slot', Pos('%_var_G_itab', IR) > 0);
AssertTrue('writes G_itab slot', Pos('$G_itab', IR) > 0);
end;
procedure TInterfaceTests.TestCodegen_InterfaceVar_ScopeExit_ReleasesObjOnly;
@ -693,12 +695,12 @@ begin
have its obj slot released. itab is a static pointer and is not
refcounted, so it must NOT be released. }
IR := GenIR(SrcInterfaceVar);
{ Look for "loadl %_var_F_obj" followed by _ClassRelease somewhere after
main_exit — ensures the scope-exit release pass covered interface vars. }
{ F is a program-level global — slots accessed via $F_obj, $F_itab.
Check that scope-exit cleanup loads the obj slot for release. }
AssertTrue('scope-exit loads obj slot of interface var',
Pos('loadl %_var_F_obj', IR) > 0);
Pos('loadl $F_obj', IR) > 0);
AssertTrue('no StringRelease or direct free on itab slot',
Pos('loadl %_var_F_itab', IR) > 0); { itab is read during the method
Pos('loadl $F_itab', IR) > 0); { itab is read during the method
call path but never released. }
end;

View file

@ -517,8 +517,8 @@ begin
' end;' + LineEnding +
'var Pt: TPoint;' + LineEnding +
'begin end.');
AssertTrue('alloc for Pt', Pos('_var_Pt', IR) > 0);
AssertTrue('alloc present', Pos('alloc', IR) > 0);
{ Program-level record var Pt is a data-section global }
AssertTrue('data decl for Pt', Pos('$Pt', IR) > 0);
end;
procedure TRecordTests.TestCodegen_FieldStore_EmitsOffset;
@ -568,8 +568,8 @@ begin
' end;' + LineEnding +
'var R: TRect;' + LineEnding +
'begin end.');
{ Two Integer fields = 8 bytes total }
AssertTrue('alloc4 8', Pos('alloc4 8', IR) > 0);
{ Two Integer fields = 8 bytes total; program-level record uses data section }
AssertTrue('8-byte record in data section', Pos('z 8', IR) > 0);
end;
procedure TRecordTests.TestCodegen_StringField_CorrectSize;
@ -584,8 +584,8 @@ begin
' end;' + LineEnding +
'var Person: TPerson;' + LineEnding +
'begin end.');
{ One string field = 8 bytes, 8-byte aligned }
AssertTrue('alloc8 8', Pos('alloc8 8', IR) > 0);
{ One string field = 8 bytes; program-level record uses data section }
AssertTrue('8-byte record in data section', Pos('z 8', IR) > 0);
end;
initialization

View file

@ -253,8 +253,8 @@ procedure TVarParamTests.TestCodegen_VarParam_CallPassesAddress;
var IR: string;
begin
IR := GenIR(SrcVarSet);
{ SetVal(V) must pass the address of V, not its loaded value }
AssertTrue('call passes address of V', Pos('call $SetVal(l %_var_V)', IR) > 0);
{ SetVal(V) must pass the address of V — V is a global so address is $V }
AssertTrue('call passes address of V', Pos('call $SetVal(l $V)', IR) > 0);
end;
procedure TVarParamTests.TestCodegen_VarParam_WriteStoresThroughPointer;
@ -278,9 +278,9 @@ procedure TVarParamTests.TestCodegen_VarParam_Swap;
var IR: string;
begin
IR := GenIR(SrcVarSwap);
{ Both X and Y addresses passed to Swap }
AssertTrue('passes address of X', Pos('l %_var_X', IR) > 0);
AssertTrue('passes address of Y', Pos('l %_var_Y', IR) > 0);
{ Both X and Y addresses passed to Swap — X and Y are globals so addresses are $X, $Y }
AssertTrue('passes address of X', Pos('l $X', IR) > 0);
AssertTrue('passes address of Y', Pos('l $Y', IR) > 0);
end;
initialization

View file

@ -15,6 +15,20 @@
#include <setjmp.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
/* Forward-declared string helper to build a Blaise string from a C string. */
static void* exc_str_from_cstr(const char* s) {
typedef struct { int32_t refcnt; int32_t length; int32_t capacity; } StrHdr;
int32_t len = s ? (int32_t)strlen(s) : 0;
StrHdr* h = (StrHdr*)malloc(sizeof(StrHdr) + len + 1);
if (!h) return NULL;
h->refcnt = 0; h->length = len; h->capacity = len;
if (len > 0) memcpy((char*)(h + 1), s, (size_t)len);
((char*)(h + 1))[len] = '\0';
return (void*)h;
}
typedef struct BlaiseExcFrame {
jmp_buf jbuf; /* offset 0 — frame ptr passed directly to setjmp */
@ -69,6 +83,19 @@ void* _CurrentException(void) {
return g_exc_top ? g_exc_top->exception : NULL;
}
/*
* _CurrentExceptionMessage — return the Message string of the current exception.
* Class layout: offset 0 = vptr (8 bytes), offset 8 = FMessage (string pointer).
* Returns an empty string if no exception is active.
*/
void* _CurrentExceptionMessage(void) {
void* exc = g_exc_top ? g_exc_top->exception : NULL;
if (!exc) return exc_str_from_cstr("");
/* FMessage is the first user field, after the 8-byte vptr */
void* msg = *((void**)((char*)exc + 8));
return msg ? msg : exc_str_from_cstr("");
}
/*
* _Reraise — re-raise the given exception to the enclosing handler.
* Called by the compiler on the exception path of try/finally, after

View file

@ -155,6 +155,16 @@ int32_t _FileExists(void* filename) {
return 1;
}
/* ------------------------------------------------------------------ */
/* _DeleteFile(FileName) : void */
/* Deletes a file. Silently ignores failure. */
/* ------------------------------------------------------------------ */
void _DeleteFile(void* filename) {
const char* path = io_str_data(filename);
remove(path);
}
/* ------------------------------------------------------------------ */
/* _GetEnvVar(Name) : string */
/* Returns the value of environment variable Name, or empty string. */

View file

@ -137,6 +137,25 @@ void* _StringLowerCase(void* s) {
return r;
}
/* ------------------------------------------------------------------ */
/* _StringTrim(s) : string — strip leading and trailing whitespace */
/* ------------------------------------------------------------------ */
void* _StringTrim(void* s) {
int32_t len = str_len(s);
const char* data = str_data(s);
int32_t lo = 0;
int32_t hi = len - 1;
while (lo <= hi && (unsigned char)data[lo] <= ' ') lo++;
while (hi >= lo && (unsigned char)data[hi] <= ' ') hi--;
int32_t newlen = (hi >= lo) ? (hi - lo + 1) : 0;
void* r = str_alloc(newlen);
if (!r) return NULL;
if (newlen > 0)
memcpy((char*)r + sizeof(BlaiseStrHdr), data + lo, newlen);
return r;
}
/* ------------------------------------------------------------------ */
/* _StringSameText(s1, s2) : Boolean (0 or 1) */
/* ------------------------------------------------------------------ */
@ -191,6 +210,18 @@ int32_t _OrdAt(void* s, int32_t i) {
return (int32_t)(unsigned char)data[i - 1];
}
/* ------------------------------------------------------------------ */
/* _Chr(n) : string — one-character string holding byte value n. */
/* Returns an unowned header (RefCount = 0); caller AddRefs. */
/* ------------------------------------------------------------------ */
void* _Chr(int32_t n) {
BlaiseStrHdr* h = (BlaiseStrHdr*)str_alloc(1);
if (!h) return NULL;
((char*)(h + 1))[0] = (char)(unsigned char)n;
return (void*)h;
}
/* ------------------------------------------------------------------ */
/* _StringCompare(s1, s2) : Integer — case-sensitive (like strcmp) */
/* ------------------------------------------------------------------ */

View file

@ -49,6 +49,7 @@ type
procedure Put(AIndex: Integer; AObject: Pointer);
function IndexOf(AObject: Pointer): Integer;
procedure Delete(AIndex: Integer);
function Extract(AIndex: Integer): Pointer;
procedure Clear;
property Count: Integer read FCount;
end;
@ -81,6 +82,7 @@ type
procedure Delete(AIndex: Integer);
procedure Clear;
procedure Insert(AIndex: Integer; S: string);
procedure AddStrings(ASource: TStringList);
function GetText: string;
property Count: Integer read FCount;
property CaseSensitive: Boolean read FCaseSensitive write FCaseSensitive;
@ -88,12 +90,49 @@ type
property Duplicates: Integer read FDuplicates write FDuplicates;
end;
procedure SplitIntoList(const S: string; ASep: Integer; AList: TStringList);
implementation
{ ================================================================== }
{ TObjectList }
{ ================================================================== }
procedure SplitIntoList(const S: string; ASep: Integer; AList: TStringList);
var
I: Integer;
Start: Integer;
SLo: Integer;
SHi: Integer;
begin
AList.Clear;
Start := 1;
I := 1;
while I <= Length(S) do
begin
if OrdAt(S, I) = ASep then
begin
{ Trim surrounding spaces }
SLo := Start;
SHi := I - 1;
while (SLo <= SHi) and (OrdAt(S, SLo) = 32) do SLo := SLo + 1;
while (SHi >= SLo) and (OrdAt(S, SHi) = 32) do SHi := SHi - 1;
AList.Add(Copy(S, SLo, SHi - SLo + 1));
Start := I + 1;
end;
I := I + 1;
end;
if Start <= Length(S) then
begin
SLo := Start;
SHi := Length(S);
while (SLo <= SHi) and (OrdAt(S, SLo) = 32) do SLo := SLo + 1;
while (SHi >= SLo) and (OrdAt(S, SHi) = 32) do SHi := SHi - 1;
AList.Add(Copy(S, SLo, SHi - SLo + 1));
end;
end;
procedure TObjectList.Grow;
var
NewCap: Integer;
@ -188,6 +227,25 @@ begin
Self.FCount := 0
end;
function TObjectList.Extract(AIndex: Integer): Pointer;
var
I: Integer;
Src: ^Pointer;
Dst: ^Pointer;
begin
Src := Self.FData + AIndex * SizeOf(Pointer);
Result := Src^;
I := AIndex;
while I < Self.FCount - 1 do
begin
Dst := Self.FData + I * SizeOf(Pointer);
Src := Self.FData + (I + 1) * SizeOf(Pointer);
Dst^ := Src^;
I := I + 1
end;
Self.FCount := Self.FCount - 1
end;
{ ================================================================== }
{ TStringList }
{ ================================================================== }
@ -470,6 +528,18 @@ begin
Self.FCount := Self.FCount + 1
end;
procedure TStringList.AddStrings(ASource: TStringList);
var
I: Integer;
begin
I := 0;
while I < ASource.FCount do
begin
Self.Add(ASource.Get(I));
I := I + 1
end;
end;
function TStringList.GetText: string;
var
I: Integer;
@ -483,7 +553,7 @@ begin
begin
Ptr := Self.FStrings + I * SizeOf(string);
Result := Result + Sep + Ptr^;
Sep := #13#10;
Sep := #10;
I := I + 1
end
end;

File diff suppressed because it is too large Load diff

1365
tools/migrate_full.py Normal file

File diff suppressed because it is too large Load diff