feat(lang): static class/record members (within-unit)

Introduce `static` (class-level) members to the Blaise language using the
`static` keyword — never an overloaded `class` keyword. A `static` member is
type-associated, not instance-associated: static methods take no implicit
Self, and static vars/consts are a single shared storage slot.

Surface, on classes and records:

* `static var` / `static const` — section form (`private static var`) or as a
  bare `static` continuing the current visibility. Static vars lower to one
  shared global slot (mangled `<Unit><Type>_<Name>`), zero-initialised, NOT an
  instance field. Class- and interface-typed static vars are supported (the
  canonical singleton storage) with store-time ARC and a program-exit release;
  string and dynamic-array static vars remain deferred.
* `static function` / `static procedure` — per-member prefix or section form;
  no implicit Self. Out-of-line bodies are `static function T.M`.
* `static property` — sugar over a static getter (no Self at the call site).
* record `static function` — the factory / namespaced-function form
  (`TPoint.Make(x, y): TPoint`), required to be marked `static` explicitly.

There is no `static constructor` / `static destructor` (rejected at parse):
the zero-initialisation guarantee covers nil singletons, and eager setup
belongs in a unit's `initialization`/`finalization` (the Swift/Rust/Go model,
not Java/C#/Delphi). `class` is never a member qualifier.

Implementation spans the full pipeline:

* parser — `static` is a soft keyword; section qualifier (followed by
  var/const) and per-member prefix forms; `static constructor/destructor`
  rejected.
* semantic — static vars register a shared global (bare + qualified) under a
  mangled emit label; static methods skip the Self binding; qualified
  `Type.StaticVar` / `Type.StaticProp` / `Type.StaticMethod()` resolution.
* QBE + native x86-64 codegen — no-Self method signatures and call sites
  (including the record-return sret and >6-arg paths), shared global data
  slots, qualified static var/property reads, and class/interface static-var
  release at program exit.
* `.bif` interface format — IsClassVar/ClassVarEmitName, property IsStatic, and
  record/class const decls are encoded (BLAISE-IFACE version 2 -> 3).
* OPDF debug info — static vars are emitted as `recGlobalVar`s under their
  mangled label so a debugger can print `TFoo.FInstance`.

Static members currently work within a single program/unit; carrying them
across separately-compiled units (export clone, import, TRoutineSig.IsStatic)
is a tracked follow-up — the .bif wire format is already in place for it.

Tests: cp.test.staticmembers (parser + semantic + IR) and
cp.test.e2e.staticmembers (compile+run on both backends). Full suite green on
QBE- and native-built runners (3908 tests); all fixpoints pass; bif-coverage
clean. docs/grammar.ebnf and docs/language-rationale.adoc updated.
This commit is contained in:
Graeme Geldenhuys 2026-06-28 16:11:40 +01:00
parent 50b8d75e6d
commit 0977dc16cb
16 changed files with 2556 additions and 122 deletions

View file

@ -196,6 +196,7 @@ type
registration's type wins). The width and signedness drive both the
.data directive and every load/store of the slot. }
procedure AddGlobal(const AName: string; AType: TTypeDesc);
procedure RegisterClassVars(AFields: TObjectList);
procedure MarkThreadVar(const AName: string);
procedure MarkWeakGlobal(const AName: string);
function IsThreadVarGlobal(const AName: string): Boolean;
@ -1005,6 +1006,26 @@ begin
FDataGlobals.Add(AName, AType);
end;
{ Register a shared data global for every STATIC (class-level) variable in a
class/record field list, under the mangled ClassVarEmitName stamped by the
semantic pass. EmitDataSection then emits a correctly-sized zero slot. This
covers read-only static vars that no assignment site auto-registers. }
procedure TX86_64Backend.RegisterClassVars(AFields: TObjectList);
var
I: Integer;
FDecl: TFieldDecl;
begin
if AFields = nil then Exit;
for I := 0 to AFields.Count - 1 do
begin
FDecl := TFieldDecl(AFields.Items[I]);
if not FDecl.IsClassVar then Continue;
if FDecl.ClassVarEmitName = '' then Continue;
if FDecl.ResolvedType = nil then Continue;
Self.AddGlobal(FDecl.ClassVarEmitName, FDecl.ResolvedType);
end;
end;
procedure TX86_64Backend.MarkThreadVar(const AName: string);
begin
if not FThreadVarGlobals.ContainsKey(AName) then
@ -1575,6 +1596,19 @@ begin
end;
end;
end;
{ Static (class-level) variables: register one shared global per IsClassVar
field so EmitDataSection emits a zero-initialised slot under the mangled
label (ClassVarEmitName). Done for BOTH class and record types, and
independently of any assignment (a read-only static var has no assignment to
auto-register it via AddGlobal at the write site). }
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
if TD.Def is TClassTypeDef then
RegisterClassVars(TClassTypeDef(TD.Def).Fields)
else if TD.Def is TRecordTypeDef then
RegisterClassVars(TRecordTypeDef(TD.Def).Fields);
end;
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
@ -3979,8 +4013,11 @@ begin
Self.AddSlot('_cap_' + ADecl.CapturedVars.Strings[I], nil, Offset);
{ For class methods, Self is the implicit first integer param (%rdi).
Allocate a pointer-size slot for it; normal params start at IntIdx=1. }
if ADecl.OwnerTypeName <> '' then
Allocate a pointer-size slot for it; normal params start at IntIdx=1.
A `static` method takes NO implicit Self (like a class function/Java
static), so no Self slot is reserved and the first user param occupies
%rdi directly. }
if (ADecl.OwnerTypeName <> '') and not ADecl.IsStatic then
Self.AddSlot('Self', nil, Offset); { nil = pointer size (8 bytes) }
{ Params: first 6 integer register slots are register-passed (spilled to
@ -4000,8 +4037,8 @@ begin
Inc(IntIdx2, ADecl.CapturedVars.Count);
if FSretFunc then
Inc(IntIdx2); { sret buffer pointer in %rdi }
if (ADecl.OwnerTypeName <> '') then
Inc(IntIdx2); { Self in the next integer register }
if (ADecl.OwnerTypeName <> '') and not ADecl.IsStatic then
Inc(IntIdx2); { Self in the next integer register (static methods have none) }
for I := 0 to ADecl.Params.Count - 1 do
begin
P := TMethodParam(ADecl.Params.Items[I]);
@ -7168,6 +7205,32 @@ begin
Exit;
end;
{ Static var read: TypeName.StaticVar — a plain global load of the mangled
label resolved by the semantic pass (no Self, no instance). }
if (AExpr is TFieldAccessExpr) and TFieldAccessExpr(AExpr).IsClassVarRead then
begin
FAE := TFieldAccessExpr(AExpr);
Self.Emit(Format(#9'movq %s(%%rip), %%rax',
[NativeMangle(FAE.ClassVarEmitName)]));
if (FAE.ResolvedType <> nil) and IsIntFamily(FAE.ResolvedType) then
Self.EmitNarrowToType(FAE.ResolvedType);
Exit;
end;
{ Static property read: TypeName.StaticProp — call the static getter (no Self
argument); result in %rax. }
if (AExpr is TFieldAccessExpr) and TFieldAccessExpr(AExpr).IsStaticPropGet then
begin
FAE := TFieldAccessExpr(AExpr);
MD := TMethodDecl(FAE.ResolvedMethod);
Self.Emit(#9'callq ' +
MethodEmitNameNative(MD,
TRecordTypeDesc(FAE.ResolvedClassType).Name, MD.Name));
if (FAE.ResolvedType <> nil) and IsIntFamily(FAE.ResolvedType) then
Self.EmitNarrowToType(FAE.ResolvedType);
Exit;
end;
if (AExpr is TFieldAccessExpr) and
TFieldAccessExpr(AExpr).IsInterfaceCall then
begin
@ -8281,6 +8344,20 @@ begin
'native backend: TMethodCallExpr has no ResolvedMethod (' + ACall.Name + ')');
Sym := MethodEmitNameNative(MD, MD.OwnerTypeName, ACall.Name);
{ Static method call (TypeName.StaticMethod(args)): NO implicit Self — the
callee shifts every parameter down one register (first user arg in %rdi).
A value-returning static method is exactly a plain function call to the
method's mangled symbol, so route it through EmitCall, which marshals args
into %rdi.. with no receiver. Record-returning static factories use the
sret ABI and are handled in EmitMethodSretCall. }
if ACall.IsStaticCall then
begin
Sym := MethodEmitNameNative(MD,
TRecordTypeDesc(ACall.ResolvedClassType).Name, ACall.Name);
Self.EmitCall(Sym, MD, ACall.Args);
Exit;
end;
UserSlots := Self.CountArgSlots(MD.Params);
TotalSlots := UserSlots + 1;
@ -9314,6 +9391,37 @@ begin
'native backend: TMethodCallStmt has no ResolvedMethod (' + ACall.Name + ')');
Sym := MethodEmitNameNative(MD, MD.OwnerTypeName, ACall.Name);
{ Static method call in statement position (TypeName.StaticMethod(args), result
discarded): NO implicit Self. A void/scalar-returning static method is a
plain function call (EmitCall marshals args into %rdi.., result ignored). A
record-returning static factory whose result is discarded needs a throwaway
sret buffer, dispatched via EmitSretCall, with its managed fields released. }
if ACall.IsStaticCall then
begin
Sym := MethodEmitNameNative(MD,
TRecordTypeDesc(ACall.ResolvedClassType).Name, ACall.Name);
if (ACall.ResolvedReturnTypeDesc <> nil) and
(ACall.ResolvedReturnTypeDesc.Kind = tyRecord) then
begin
SretRT := TRecordTypeDesc(ACall.ResolvedReturnTypeDesc);
SretBufSize := (SretRT.TotalSize() + 15) and (-16);
Self.Emit(#9'pushq %rbx');
Self.Emit(Format(#9'subq $%d, %%rsp', [SretBufSize]));
Self.Emit(#9'movq %rsp, %rbx');
Self.EmitSretCall(Sym, MD, ACall.Args, '(%rbx)', False);
{ EmitSretCall may have clobbered %rbx during arg evaluation; %rsp still
points at the buffer, so re-derive its address before releasing. }
Self.Emit(#9'movq %rsp, %rbx');
if not Self.IsRecordManagedClean(SretRT) then
Self.EmitRecordFieldReleases(SretRT, '%rbx');
Self.Emit(Format(#9'addq $%d, %%rsp', [SretBufSize]));
Self.Emit(#9'popq %rbx');
end
else
Self.EmitCall(Sym, MD, ACall.Args);
Exit;
end;
IsSretDiscard := (ACall.ResolvedReturnTypeDesc <> nil) and
(ACall.ResolvedReturnTypeDesc.Kind = tyRecord) and
(ClassifyRecordReturn(TRecordTypeDesc(ACall.ResolvedReturnTypeDesc)) = rcSret);
@ -15235,6 +15343,19 @@ begin
'native backend: method-sret call has no ResolvedMethod (' + ACall.Name + ')');
Sym := MethodEmitNameNative(MD, MD.OwnerTypeName, ACall.Name);
{ Record-returning static factory (TypeName.Make(args): TRec): NO implicit
Self, so it follows the plain record-return ABI exactly — sret buffer in
%rdi, user args in %rsi.. — which is what EmitSretCall (the free-function
record-return emitter) already produces. Delegate to it with the method's
mangled symbol; the Self-threading code below is for instance methods only. }
if ACall.IsStaticCall then
begin
Sym := MethodEmitNameNative(MD,
TRecordTypeDesc(ACall.ResolvedClassType).Name, ACall.Name);
Self.EmitSretCall(Sym, MD, ACall.Args, ASretAddr, ASretIsIndirect);
Exit;
end;
{ Record-returning-call receiver (e.g. A.Plus(B).Scale(2), where the OUTER
method Scale itself returns a record via sret): materialise the inner call
result into a stack buffer up front and carry its address in callee-saved
@ -16531,9 +16652,11 @@ begin
Self.Emit(Format(#9'movq %%rdi, %s', [Self.VarOperand('Result')]));
IntIdx := 1;
end;
if ADecl.OwnerTypeName <> '' then
if (ADecl.OwnerTypeName <> '') and not ADecl.IsStatic then
begin
{ Class method: spill Self from the first int arg register into its slot. }
{ Class method: spill Self from the first int arg register into its slot.
A `static` method has NO implicit Self — its first user parameter is the
first integer arg (%rdi), so no Self spill and IntIdx stays put. }
Self.Emit(Format(#9'movq %s, %s',
[SysVArg64(IntIdx), Self.VarOperand('Self')]));
Inc(IntIdx);

View file

@ -259,6 +259,7 @@ type
function ConstElemQbeDataType(const AElemType: string): string;
procedure EmitArrayConstData(CD: TConstDecl; const APrefix: string);
procedure EmitClassConstData(AClassDef: TClassTypeDef; const AClassName: string);
procedure EmitClassVarData(AFields: TObjectList; const AClassName: string);
procedure EmitGlobalConstData(ABlock: TBlock);
procedure EmitLocalArrayConstsInBlock(ABlock: TBlock);
procedure EmitLocalArrayConstsInTypeDecls(ABlock: TBlock);
@ -266,6 +267,7 @@ type
procedure EmitLocalArrayConstsInUnit(AUnit: TUnit);
procedure EmitParamAllocs(AMethod: TMethodDecl; AClassType: TRecordTypeDesc);
procedure EmitArcCleanup(ABlock: TBlock);
procedure EmitStaticVarReleases(ABlock: TBlock);
procedure EmitExcPathArcCleanup(ABlock: TBlock);
procedure EmitExcUnwind(ATargetDepth: Integer);
procedure EmitStmt(AStmt: TASTStmt);
@ -2254,6 +2256,74 @@ begin
EmitArrayConstData(TConstDecl(AClassDef.ConstDecls.Items[I]), AClassName);
end;
{ Emit one shared global data slot per STATIC (class-level) variable. The label
must match the semantic pass's GlobalEmitName: ClassUnitPrefix(Class)+Class+'_'+
Field. Static vars are never instance fields, so they appear here, not in the
record's field layout. v1 supports only unmanaged element types (the semantic
pass rejects managed ones), so no ARC init/cleanup is needed. }
procedure TCodeGenQBE.EmitClassVarData(AFields: TObjectList; const AClassName: string);
var
I, J: Integer;
FDecl: TFieldDecl;
T: TTypeDesc;
Nm: string;
Pfx: string;
begin
if AFields = nil then Exit;
Pfx := 'export data';
for I := 0 to AFields.Count - 1 do
begin
FDecl := TFieldDecl(AFields.Items[I]);
if not FDecl.IsClassVar then Continue;
T := FDecl.ResolvedType;
if T = nil then Continue;
for J := 0 to FDecl.Names.Count - 1 do
begin
if (FDecl.ClassVarEmitName <> '') and (FDecl.Names.Count = 1) then
Nm := FDecl.ClassVarEmitName
else
Nm := ClassUnitPrefix(AClassName) + AClassName + '_' + FDecl.Names.Strings[J];
case T.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum:
EmitLine(Format('%s $%s = { w 0 }', [Pfx, Nm]));
tySmallInt, tyWord:
EmitLine(Format('%s $%s = { h 0 }', [Pfx, Nm]));
tyInt64, tyUInt64, tyPointer, tyPChar:
EmitLine(Format('%s $%s = { l 0 }', [Pfx, Nm]));
tyDouble:
EmitLine(Format('%s $%s = { d 0 }', [Pfx, Nm]));
tySingle:
EmitLine(Format('%s $%s = { s 0 }', [Pfx, Nm]));
tySet:
if TSetTypeDesc(T).BitCount <= 32 then
EmitLine(Format('%s $%s = { w 0 }', [Pfx, Nm]))
else if TSetTypeDesc(T).BitCount <= 64 then
EmitLine(Format('%s $%s = { l 0 }', [Pfx, Nm]))
else
EmitLine(Format('%s $%s = { z %d }', [Pfx, Nm, TSetTypeDesc(T).RawSize()]));
tyRecord:
if TRecordTypeDesc(T).TotalSize() > 0 then
EmitLine(Format('%s $%s = { z %d }', [Pfx, Nm, TRecordTypeDesc(T).TotalSize()]))
else
EmitLine(Format('%s $%s = { l 0 }', [Pfx, Nm]));
tyStaticArray:
if TStaticArrayTypeDesc(T).ByteSize() > 0 then
EmitLine(Format('%s $%s = { z %d }', [Pfx, Nm, TStaticArrayTypeDesc(T).ByteSize()]))
else
EmitLine(Format('%s $%s = { l 0 }', [Pfx, Nm]));
tyInterface:
{ Contiguous 16-byte fat pointer (obj at +0, itab at +8) under the
'_obj' suffix, matching program-global interface vars so itab
accesses ($Name_obj + 8) stay adjacent. }
EmitLine(Format('%s $%s_obj = { l 0, l 0 }', [Pfx, Nm]));
else
{ tyClass and every other pointer-sized kind: a single nil slot. }
EmitLine(Format('%s $%s = { l 0 }', [Pfx, Nm]));
end;
end;
end;
end;
procedure TCodeGenQBE.EmitGlobalConstData(ABlock: TBlock);
{ Emit QBE data-section entries for array-typed constants.
Each array const is emitted as a labelled data object whose elements are
@ -2270,7 +2340,12 @@ begin
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
if TD.Def is TClassTypeDef then
begin
EmitClassConstData(TClassTypeDef(TD.Def), TD.Name);
EmitClassVarData(TClassTypeDef(TD.Def).Fields, TD.Name);
end
else if TD.Def is TRecordTypeDef then
EmitClassVarData(TRecordTypeDef(TD.Def).Fields, TD.Name);
end;
end;
@ -2441,6 +2516,57 @@ begin
end;
end;
procedure TCodeGenQBE.EmitStaticVarReleases(ABlock: TBlock);
{ Release class- and interface-typed static (class-level) variables at program
exit. A static var is a single shared global pointer slot (the mangled
ClassVarEmitName), zero-initialised to nil; the slot holds at most one
retained reference, so one balancing release at teardown is correct. Value
static vars (Integer, record, …) and the still-deferred string/dynarray kinds
emit nothing here. Mirrors EmitArcCleanup, but sourced from the type decls'
IsClassVar fields rather than the block's var decls. }
var
I, K, N: Integer;
TD: TTypeDecl;
Flds: TObjectList;
FDecl: TFieldDecl;
T: TTypeDesc;
Nm: string;
ValTemp: string;
begin
if ABlock = nil then Exit;
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
Flds := nil;
if TD.Def is TClassTypeDef then
Flds := TClassTypeDef(TD.Def).Fields
else if TD.Def is TRecordTypeDef then
Flds := TRecordTypeDef(TD.Def).Fields;
if Flds = nil then Continue;
for K := 0 to Flds.Count - 1 do
begin
FDecl := TFieldDecl(Flds.Items[K]);
if not FDecl.IsClassVar then Continue;
T := FDecl.ResolvedType;
if T = nil then Continue;
if not (T.Kind in [tyClass, tyInterface]) then Continue;
for N := 0 to FDecl.Names.Count - 1 do
begin
if (FDecl.ClassVarEmitName <> '') and (FDecl.Names.Count = 1) then
Nm := FDecl.ClassVarEmitName
else
Nm := ClassUnitPrefix(TD.Name) + TD.Name + '_' + FDecl.Names.Strings[N];
ValTemp := AllocTemp();
if T.Kind = tyInterface then
EmitLine(Format(' %s =l loadl $%s_obj', [ValTemp, Nm]))
else
EmitLine(Format(' %s =l loadl $%s', [ValTemp, Nm]));
EmitLine(Format(' call $_ClassRelease(l %s)', [ValTemp]));
end;
end;
end;
end;
procedure TCodeGenQBE.EmitBlock(ABlock: TBlock);
var
I: Integer;
@ -2457,6 +2583,12 @@ begin
EmitLine('@' + FExitLabel);
end;
EmitArcCleanup(ABlock);
{ Program exit: release class/interface-typed static (class-level) variables.
They are shared globals not present in ABlock.Decls, so EmitArcCleanup does
not cover them. Only the program's top block carries the 'main_exit' label,
so this fires exactly once, at process teardown. }
if FExitLabel = 'main_exit' then
EmitStaticVarReleases(ABlock);
end;
procedure TCodeGenQBE.EmitExcPathArcCleanup(ABlock: TBlock);
@ -5753,6 +5885,47 @@ begin
end;
MDecl := TMethodDecl(MCallExpr.ResolvedMethod);
RetType := TRecordTypeDesc(MDecl.ResolvedReturnType);
{ Static (class-level) record-returning function: TypeName.Make(args).
No Self — the visible args begin with the first user parameter. (A record
factory is the canonical case; the user opted records into requiring an
explicit `static` marker for factory methods.) }
if MCallExpr.IsStaticCall then
begin
FuncName := '$' + MethodEmitName(MDecl,
TRecordTypeDesc(MCallExpr.ResolvedClassType).Name, MCallExpr.Name);
VisArgs := '';
for I := 0 to MCallExpr.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params.Items[I]);
if Par.IsVarParam then
begin
if VisArgs <> '' then VisArgs := VisArgs + ', ';
VisArgs := VisArgs + Format('l %s',
[EmitLValueAddr(TASTExpr(MCallExpr.Args.Items[I]))]);
end
else if (Par.ResolvedType <> nil) and (Par.ResolvedType.Kind = tyInterface) then
begin
if VisArgs = '' then
VisArgs := Copy(InterfaceArgFragment(TASTExpr(MCallExpr.Args.Items[I]),
Par.ResolvedType), 2, MaxInt)
else
VisArgs := VisArgs + InterfaceArgFragment(TASTExpr(MCallExpr.Args.Items[I]),
Par.ResolvedType);
end
else
begin
ArgTemp := EmitExpr(TASTExpr(MCallExpr.Args.Items[I]));
ArgTemp := CoerceArg(ArgTemp, TASTExpr(MCallExpr.Args.Items[I]),
QbeTypeOf(Par.ResolvedType));
if VisArgs <> '' then VisArgs := VisArgs + ', ';
VisArgs := VisArgs + Format('%s %s',
[QbeParamTypeOf(Par.ResolvedType), ArgTemp]);
end;
end;
EmitRecordReturnCallSite(FuncName, VisArgs, RetType, ASretAddr);
FlushPendingReleases(PMark);
Exit;
end;
if MCallExpr.ObjExpr <> nil then
SelfTemp := EmitExpr(MCallExpr.ObjExpr)
else if MDecl.IsRecordMethod and MCallExpr.IsVarParam then
@ -6633,6 +6806,55 @@ begin
Exit;
end;
{ Static (class-level) method call in statement position: TypeName.M(args).
No Self; a plain call to the mangled method symbol. }
if ACall.IsStaticCall and (ACall.ResolvedMethod <> nil) then
begin
RT := TRecordTypeDesc(ACall.ResolvedClassType);
MDecl := TMethodDecl(ACall.ResolvedMethod);
ArgLine := '';
ArgTemps := TStringList.Create();
try
for I := 0 to ACall.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params.Items[I]);
if ArgLine <> '' then ArgLine := ArgLine + ', ';
if Par.IsOpenArray then
begin
ArgTemps.Add('');
ArgLine := ArgLine + OpenArrayArgFragment(TASTExpr(ACall.Args.Items[I]));
end
else if Par.IsVarParam then
begin
ArgTemps.Add('');
ArgLine := ArgLine + Format('l %s',
[EmitLValueAddr(TASTExpr(ACall.Args.Items[I]))]);
end
else if (Par.ResolvedType <> nil) and (Par.ResolvedType.Kind = tyInterface) then
begin
ArgTemps.Add('');
ArgLine := ArgLine +
InterfaceArgFragment(TASTExpr(ACall.Args.Items[I]), Par.ResolvedType);
end
else
begin
ArgTemp := EmitExpr(TASTExpr(ACall.Args.Items[I]));
ArgTemps.Add(ArgTemp);
ArgLine := ArgLine + Format('%s %s',
[QbeParamTypeOf(Par.ResolvedType), ArgTemp]);
end;
end;
FuncName := '$' + MethodEmitName(MDecl, RT.Name, ACall.Name);
EmitLine(Format(' call %s(%s)', [FuncName, ArgLine]));
EmitOwnedArgReleases(ACall.Args, ArgTemps, MDecl.Params);
ReleaseConstStringArgs(ACall.Args, ArgTemps, MDecl.Params);
finally
ArgTemps.Free();
end;
FlushPendingReleases(PMark);
Exit;
end;
{ Interface method dispatch: load obj + itab, index by method slot }
if (ACall.ResolvedClassType <> nil) and
(ACall.ResolvedClassType.Kind = tyInterface) then
@ -7337,9 +7559,13 @@ var
I: Integer;
Par: TMethodParam;
begin
{ Self: store incoming pointer into a local slot }
EmitLine(' %_var_Self =l alloc8 1');
EmitLine(' storel %_par_Self, %_var_Self');
{ Self: store incoming pointer into a local slot. A STATIC method has no
Self — skip the slot entirely. }
if not AMethod.IsStatic then
begin
EmitLine(' %_var_Self =l alloc8 1');
EmitLine(' storel %_par_Self, %_var_Self');
end;
{ Explicit params }
for I := 0 to AMethod.Params.Count - 1 do
@ -7442,6 +7668,7 @@ procedure TCodeGenQBE.EmitMethodDef(const ATypeName: string;
AMethod: TMethodDecl);
var
Sig: string;
ParSep: string;
I: Integer;
Par: TMethodParam;
FuncName: string;
@ -7463,31 +7690,52 @@ begin
if IsFunc and (AMethod.ResolvedReturnType.Kind = tyRecord) then
RC := Self.ClassifyRecordReturn(TRecordTypeDesc(AMethod.ResolvedReturnType));
Sig := 'l %_par_Self';
if IsFunc and ((AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo())) then
Sig := 'l %_par__sret, l %_par_Self'
else if IsFunc and (AMethod.ResolvedReturnType.Kind = tyRecord) then
EmitRecordReturnSignature(Sig, RC);
{ A STATIC (class-level) method takes no implicit Self. Its signature starts
empty (or with just the sret pointer for aggregate returns). }
if AMethod.IsStatic then
begin
Sig := '';
if IsFunc and ((AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo())) then
Sig := 'l %_par__sret'
else if IsFunc and (AMethod.ResolvedReturnType.Kind = tyRecord) then
{ Sig is '' here, so EmitRecordReturnSignature yields just the sret ptr. }
EmitRecordReturnSignature(Sig, RC);
end
else
begin
Sig := 'l %_par_Self';
if IsFunc and ((AMethod.ResolvedReturnType.Kind in [tyInterface, tyStaticArray]) or
((AMethod.ResolvedReturnType.Kind = tySet) and
TSetTypeDesc(AMethod.ResolvedReturnType).IsJumbo())) then
Sig := 'l %_par__sret, l %_par_Self'
else if IsFunc and (AMethod.ResolvedReturnType.Kind = tyRecord) then
EmitRecordReturnSignature(Sig, RC);
end;
for I := 0 to AMethod.Params.Count - 1 do
begin
Par := TMethodParam(AMethod.Params.Items[I]);
{ A static method with a scalar return has an empty Sig at this point, so
the first user parameter must NOT be prefixed with ', ' (that would emit
'function w $F(, w %x)', which QBE rejects as an invalid class specifier).
The separator is empty only when nothing precedes this parameter. }
if Sig <> '' then ParSep := ', ' else ParSep := '';
if Par.IsOpenArray then
Sig := Sig + Format(', l %%_par_%s, l %%_par_%s_high',
Sig := Sig + ParSep + Format('l %%_par_%s, l %%_par_%s_high',
[Par.ParamName, Par.ParamName])
else if Par.IsVarParam then
Sig := Sig + Format(', l %%_par_%s', [Par.ParamName])
Sig := Sig + ParSep + Format('l %%_par_%s', [Par.ParamName])
else if (Par.ResolvedType <> nil) and
(Par.ResolvedType.Kind = tyInterface) then
{ Interfaces are two-slot fat pointers (obj + itab); caller passes both,
callee allocs two local slots in EmitParamAllocs. Mirrors the
standalone-routine path so method dispatch and value-param ARC both
find %_var_<p>_obj / %_var_<p>_itab. }
Sig := Sig + Format(', l %%_par_%s_obj, l %%_par_%s_itab',
Sig := Sig + ParSep + Format('l %%_par_%s_obj, l %%_par_%s_itab',
[Par.ParamName, Par.ParamName])
else
Sig := Sig + Format(', %s %%_par_%s',
Sig := Sig + ParSep + Format('%s %%_par_%s',
[QbeParamTypeOf(Par.ResolvedType), Par.ParamName]);
end;
@ -11035,6 +11283,61 @@ begin
begin
MCallExpr := TMethodCallExpr(AExpr);
{ Static (class-level) function call in expression position:
TypeName.F(args). No Self; plain call to the mangled symbol, returning
the result. v1: scalar/pointer return only (managed/record-return static
functions can be added later via the record-return ABI path). }
if MCallExpr.IsStaticCall and (MCallExpr.ResolvedMethod <> nil) then
begin
RT := TRecordTypeDesc(MCallExpr.ResolvedClassType);
MDecl := TMethodDecl(MCallExpr.ResolvedMethod);
ArgLine := '';
ArgTemps := TStringList.Create();
try
for I := 0 to MCallExpr.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params.Items[I]);
if ArgLine <> '' then ArgLine := ArgLine + ', ';
if Par.IsOpenArray then
begin
ArgTemps.Add('');
ArgLine := ArgLine + OpenArrayArgFragment(TASTExpr(MCallExpr.Args.Items[I]));
end
else if Par.IsVarParam then
begin
ArgTemps.Add('');
ArgLine := ArgLine + Format('l %s',
[EmitLValueAddr(TASTExpr(MCallExpr.Args.Items[I]))]);
end
else if (Par.ResolvedType <> nil) and (Par.ResolvedType.Kind = tyInterface) then
begin
ArgTemps.Add('');
ArgLine := ArgLine +
InterfaceArgFragment(TASTExpr(MCallExpr.Args.Items[I]), Par.ResolvedType);
end
else
begin
ArgTemp := EmitExpr(TASTExpr(MCallExpr.Args.Items[I]));
ArgTemps.Add(ArgTemp);
ArgLine := ArgLine + Format('%s %s',
[QbeParamTypeOf(Par.ResolvedType), ArgTemp]);
end;
end;
FuncName := '$' + MethodEmitName(MDecl, RT.Name, MCallExpr.Name);
Result := AllocTemp();
if MDecl.ResolvedReturnType <> nil then
EmitLine(Format(' %s =%s call %s(%s)',
[Result, QbeTypeOf(MDecl.ResolvedReturnType), FuncName, ArgLine]))
else
EmitLine(Format(' call %s(%s)', [FuncName, ArgLine]));
EmitOwnedArgReleases(MCallExpr.Args, ArgTemps, MDecl.Params);
ReleaseConstStringArgs(MCallExpr.Args, ArgTemps, MDecl.Params);
finally
ArgTemps.Free();
end;
Exit(Result);
end;
{ Procedure-type field call expression: load (Code, Data) pair and
dispatch indirectly. Only applies to function-of-object fields that
return a value (otherwise they go through TMethodCallStmt). }
@ -11803,6 +12106,30 @@ begin
EmitLine(Format(' call $_ClassRelease(l %s)', [BaseReleaseTemp]));
Exit(T);
end;
{ Static var read: TypeName.StaticVar — a plain global load of the mangled
label resolved by the semantic pass. }
if FldAccess.IsClassVarRead then
begin
QType := QbeTypeOf(FldAccess.ResolvedType);
LoadInstr := LoadInstrFor(FldAccess.ResolvedType);
T := AllocTemp();
EmitLine(Format(' %s =%s %s $%s',
[T, QType, LoadInstr, FldAccess.ClassVarEmitName]));
Exit(T);
end;
{ Static property read: TypeName.StaticProp — call the static getter
(no Self argument). }
if FldAccess.IsStaticPropGet then
begin
MDecl := TMethodDecl(FldAccess.ResolvedMethod);
QType := QbeTypeOf(MDecl.ResolvedReturnType);
T := AllocTemp();
EmitLine(Format(' %s =%s call $%s()',
[T, QType,
MethodEmitName(MDecl,
TRecordTypeDesc(FldAccess.ResolvedClassType).Name, MDecl.Name)]));
Exit(T);
end;
if FldAccess.IsImplicitSelf then
begin
{ Implicit Self.Base.Field — Base is a field of Self }

View file

@ -145,6 +145,9 @@ type
PropIndexExpr: TASTExpr; { owned — non-nil = indexed property read (e.g. List.Items[i]) }
IsCharAccess: Boolean; { set by uSemantic — PropIndexExpr indexes a string field: S.Field[N] }
IsArrayAccess: Boolean; { set by uSemantic — PropIndexExpr subscripts an array field: R.Arr[I] }
IsClassVarRead: Boolean; { set by uSemantic — TypeName.StaticVar read; lower as a global load of ClassVarEmitName }
ClassVarEmitName: string; { mangled global label for an IsClassVarRead access }
IsStaticPropGet: Boolean; { set by uSemantic — TypeName.StaticProp read; ResolvedMethod is the static getter }
destructor Destroy; override;
end;
@ -554,6 +557,9 @@ type
IsBuiltinToString: Boolean; { set by uSemantic — built-in TObject.ToString virtual dispatch }
IsConstructorCall: Boolean; { set by uSemantic — metaclass-var Create in stmt position }
IsMetaclassDispatch: Boolean; { set by uSemantic — receiver is a metaclass var }
IsStaticCall: Boolean; { set by uSemantic — TypeName.StaticMethod(): a static
(class-level) call with NO Self. Codegen lowers it as a
plain call to the mangled method symbol, no receiver. }
IsProcFieldCall: Boolean; { set by uSemantic — Name is a procedural-typed field of
the receiver, invoked directly (F.Handler;). Codegen
loads the (Code, Data) pair from the field slot and
@ -732,14 +738,26 @@ type
Attributes: TStringList; { owned — see TVarDecl.Attributes. }
IsWeak: Boolean; { set by uSemantic when [Weak] is resolved. }
IsUnretained: Boolean; { set by uSemantic when [Unretained] is resolved. }
IsClassVar: Boolean; { set by uParser — declared `static var` (a.k.a.
class variable): one shared storage slot for the
whole type, not a per-instance field. Lowered to
a global slot by uSemantic; never gets an instance
offset. }
ClassVarEmitName: string; { set by uSemantic for an IsClassVar field — the
mangled global data-label (CurrentUnitPrefix +
Class + '_' + Name). Single source of truth so
BOTH backends emit the slot under the exact label
the read/write sites use, without re-deriving the
unit prefix. }
constructor Create;
destructor Destroy; override;
end;
TRecordTypeDef = class(TASTTypeDef)
public
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
ConstDecls: TObjectList; { owned TConstDecl — record-level constants (static const) }
IsPacked: Boolean; { True iff declared `packed record` — disables
field alignment padding and tail padding;
ARC-managed fields (string/class/intf) still
@ -826,6 +844,11 @@ type
block that owns the whole frame; codegen
emits no compiler prologue/epilogue }
IsRecordMethod: Boolean; { set by uSemantic — owner type is a record (not a class) }
IsStatic: Boolean; { set by uParser — declared `static` (class method):
no implicit Self; type-associated dispatch.
Distinct from VTableSlot's "static dispatch"
(= non-virtual); a static method also never
receives a Self pointer. }
VTableSlot: Integer; { -1 = static; >=0 = vtable index (set by uSemantic) }
TypeParams: TStringList; { non-nil = generic function template; owns param names }
TypeParamConstraints: TStringList; { parallel to TypeParams; '' = unconstrained,
@ -865,6 +888,8 @@ type
[Unretained] ResolvedMethod: TObject; { TMethodDecl — not owned }
IsConstructorCall: Boolean; { set by uSemantic — TypeName.Create(args) }
IsMetaclassDispatch: Boolean; { set by uSemantic — receiver is a metaclass var }
IsStaticCall: Boolean; { set by uSemantic — TypeName.StaticMethod(): static
(class-level) call with NO Self; plain call to mangled name }
IsGlobal: Boolean; { set by uSemantic — ObjectName is a program-level global }
IsVarParam: Boolean; { set by uSemantic — ObjectName is a var/out parameter }
IsBuiltinToString: Boolean; { set by uSemantic — built-in TObject.ToString virtual dispatch }
@ -886,6 +911,8 @@ type
IndexParamName: string; { '' = non-indexed property }
IndexTypeName: string; { type name of the index parameter; '' when non-indexed }
IsDefault: Boolean; { declared with the `default` directive (Obj[I] sugar) }
IsStatic: Boolean; { set by uParser — declared `static`: sugar over a
static getter/setter; no implicit Self. }
end;
TClassTypeDef = class(TASTTypeDef)
@ -1460,8 +1487,9 @@ end;
constructor TRecordTypeDef.Create;
begin
inherited Create();
Fields := TObjectList.Create(True);
Methods := TObjectList.Create(True);
Fields := TObjectList.Create(True);
Methods := TObjectList.Create(True);
ConstDecls := TObjectList.Create(True);
end;
destructor TRecordTypeDef.Destroy;

View file

@ -74,6 +74,7 @@ type
procedure EmitConstantsFromList(AList: TObjectList);
procedure EmitTypesFromBlock(ABlock: TBlock);
procedure EmitGlobalVarsFromList(AList: TObjectList);
procedure EmitStaticVarsFromBlock(ABlock: TBlock);
procedure EmitAllTypes;
procedure EmitGlobalVars;
procedure EmitFunctionScopes;
@ -1264,15 +1265,58 @@ begin
end;
end;
{ Static (class-level) variables are shared global pointer/value slots, not
instance fields, so they are not in any block's var-decl list and EmitFields
(instance fields only) skips them. Emit one recGlobalVar per IsClassVar field
under its mangled ClassVarEmitName so a debugger can print TFoo.FInstance. }
procedure TOPDFEmitter.EmitStaticVarsFromBlock(ABlock: TBlock);
var
I, K, N: Integer;
TD: TTypeDecl;
Flds: TObjectList;
FDecl: TFieldDecl;
Nm: string;
begin
if ABlock = nil then Exit;
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
Flds := nil;
if TD.Def is TClassTypeDef then
Flds := TClassTypeDef(TD.Def).Fields
else if TD.Def is TRecordTypeDef then
Flds := TRecordTypeDef(TD.Def).Fields;
if Flds = nil then Continue;
for K := 0 to Flds.Count - 1 do
begin
FDecl := TFieldDecl(Flds.Items[K]);
if not FDecl.IsClassVar then Continue;
for N := 0 to FDecl.Names.Count - 1 do
begin
if (FDecl.ClassVarEmitName <> '') and (FDecl.Names.Count = 1) then
Nm := FDecl.ClassVarEmitName
else
Continue; { multi-name static vars carry no per-name emit label yet }
EmitGlobalVar(Nm, FDecl.ResolvedType);
end;
end;
end;
end;
procedure TOPDFEmitter.EmitGlobalVars;
begin
if FUnit <> nil then
begin
EmitGlobalVarsFromList(FUnit.IntfBlock.Decls);
EmitGlobalVarsFromList(FUnit.ImplBlock.Decls);
EmitStaticVarsFromBlock(FUnit.IntfBlock);
EmitStaticVarsFromBlock(FUnit.ImplBlock);
end
else if FProgram <> nil then
begin
EmitGlobalVarsFromList(FProgram.Block.Decls);
EmitStaticVarsFromBlock(FProgram.Block);
end;
end;
procedure TOPDFEmitter.SetFacts(AFacts: TDbgFacts);

View file

@ -845,7 +845,9 @@ begin
as required when concatenating multiple Pascal units into one file. }
while Check(tkType) or Check(tkVar) or Check(tkThreadVar) or
Check(tkProcedure) or Check(tkFunction) or Check(tkConst) or
Check(tkConstructor) or Check(tkDestructor) do
Check(tkConstructor) or Check(tkDestructor) or
(Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkFunction, tkProcedure])) do
begin
if Check(tkType) then
ParseTypeSection(Result)
@ -873,7 +875,11 @@ end;
procedure TParser.ParseTypeSection(ABlock: TBlock);
begin
Expect(tkType);
while Check(tkIdent) or Check(tkLBracket) do
while (Check(tkIdent) or Check(tkLBracket)) and
{ Stop the type section at a leading `static function/procedure` — that
is an out-of-line static-method implementation, not a type decl. }
not (Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkFunction, tkProcedure])) do
ParseTypeDecl(ABlock);
end;
@ -1298,6 +1304,21 @@ begin
Expect(tkConst);
while Check(tkIdent) do
begin
{ Stop at a class/record section keyword: a const section inside a class or
record body is terminated by the next visibility/`static` section or by a
`property` member. A genuine const decl is always `IDENT (':' | '=')`, so
if the current identifier is a section keyword and is NOT immediately
followed by `:` or `=`, it begins the next section, not another const.
(Top-level const blocks never hit this: they are followed by `var`/`type`/
`begin`, which are not tkIdent.) }
if (SameText(FCurrent.Value, 'private') or
SameText(FCurrent.Value, 'protected') or
SameText(FCurrent.Value, 'public') or
SameText(FCurrent.Value, 'published') or
SameText(FCurrent.Value, 'static') or
SameText(FCurrent.Value, 'property')) and
not (PeekKind() in [tkColon, tkEquals]) then
Break;
CD := TConstDecl.Create();
CD.Line := FCurrent.Line;
CD.Col := FCurrent.Col;
@ -1904,25 +1925,73 @@ end;
function TParser.ParseRecordDef: TRecordTypeDef;
var
MethDecl: TMethodDecl;
MethDecl: TMethodDecl;
CurrStatic: Boolean; { current section's static (class-level) association }
LocalStatic: Boolean;
FieldCountBefore: Integer;
FieldIdx: Integer;
begin
Result := TRecordTypeDef.Create();
CurrStatic := False;
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
Expect(tkRecord);
repeat
if Check(tkIdent) or Check(tkLBracket) then
ParseFieldDecl(Result.Fields)
else if Check(tkFunction) then
{ Bare `static var` / `static const` SECTION keyword. `static` is a soft
keyword: a section qualifier only when followed by `var`/`const`; a
field literally named `Static` (`Static: Integer;`) falls through. }
if Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkVar, tkConst]) then
begin
MethDecl := ParseMethodDecl(True);
CurrStatic := True;
Advance(); { the following var/const section belongs to this static section }
end
{ Visibility section keywords are accepted (and currently cosmetic), with
an optional trailing `static`. A new visibility section resets static. }
else if Check(tkIdent) and (SameText(FCurrent.Value, 'private') or
SameText(FCurrent.Value, 'public') or
SameText(FCurrent.Value, 'protected')) then
begin
CurrStatic := False;
Advance();
if Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkVar, tkConst, tkFunction, tkProcedure]) then
begin
CurrStatic := True;
Advance();
end;
end
else if Check(tkConst) then
ParseConstBlock(Result.ConstDecls)
else if Check(tkVar) then
Advance() { optional `var` keyword before field declarations }
{ Method declarations — before the field branch so a leading `static`
prefix is not mistaken for a field name. }
else if Check(tkFunction) or Check(tkProcedure) or
(Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkFunction, tkProcedure])) then
begin
LocalStatic := CurrStatic;
if Check(tkIdent) and SameText(FCurrent.Value, 'static') then
begin
LocalStatic := True;
Advance();
end;
if Check(tkFunction) then
MethDecl := ParseMethodDecl(True)
else
MethDecl := ParseMethodDecl(False);
MethDecl.IsStatic := LocalStatic;
Result.Methods.Add(MethDecl);
end
else if Check(tkProcedure) then
else if Check(tkIdent) or Check(tkLBracket) then
begin
MethDecl := ParseMethodDecl(False);
Result.Methods.Add(MethDecl);
FieldCountBefore := Result.Fields.Count;
ParseFieldDecl(Result.Fields);
if CurrStatic then
for FieldIdx := FieldCountBefore to Result.Fields.Count - 1 do
TFieldDecl(Result.Fields.Items[FieldIdx]).IsClassVar := True;
end
else
Break;
@ -1967,8 +2036,13 @@ end;
function TParser.ParseClassDef: TClassTypeDef;
var
CurrPublished: Boolean;
MethDecl: TMethodDecl;
CurrPublished: Boolean;
CurrStatic: Boolean; { current section's static (class-level) association }
LocalStatic: Boolean; { effective static for the member being parsed }
MethDecl: TMethodDecl;
PropDecl: TPropertyDecl;
FieldCountBefore: Integer;
FieldIdx: Integer;
begin
Result := TClassTypeDef.Create();
try
@ -2004,34 +2078,104 @@ begin
is then attached to each method decl so codegen can emit a
published-method table entry. }
CurrPublished := False;
CurrStatic := False;
repeat
{ Visibility section keyword (private/public/protected/published),
optionally followed by a `static` qualifier. A visibility keyword
resets the static section (you must re-state `static` after it). }
if Check(tkIdent) and (SameText(FCurrent.Value, 'private') or
SameText(FCurrent.Value, 'public') or
SameText(FCurrent.Value, 'protected') or
SameText(FCurrent.Value, 'published')) then
begin
CurrPublished := SameText(FCurrent.Value, 'published');
CurrStatic := False; { a new visibility section is non-static unless
`static` follows }
Advance(); { consume the visibility modifier }
{ Optional trailing `static` section qualifier. `static` is a soft
keyword: treat it as a qualifier only when it introduces a static
member (followed by var/const/method/property), NOT when it is a
field literally named `Static` (`private Static: Boolean;`). }
if Check(tkIdent) and SameText(FCurrent.Value, 'static') and
((PeekKind() in [tkVar, tkConst, tkFunction, tkProcedure,
tkConstructor, tkDestructor]) or
((PeekKind() = tkIdent) and SameText(PeekValueAt(1), 'property'))) then
begin
CurrStatic := True;
Advance();
end;
end
{ Bare `static` SECTION keyword — `static var` / `static const` only.
`static` is a soft keyword: it acts as a section qualifier only when
immediately followed by `var` or `const`. Followed by anything else
(e.g. `Static: Boolean;` — a field literally named Static, or `Static,`)
it is an ordinary field identifier and falls through to the field
branch. The `static function/procedure/property` PREFIX forms are
handled in the method/property branches below. }
else if Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkVar, tkConst]) then
begin
CurrStatic := True;
Advance(); { consume `static`; the following `var`/`const` section is
part of this static section }
end
else if Check(tkConst) then
ParseConstBlock(Result.ConstDecls)
else if Check(tkVar) then
Advance() { optional 'var' keyword before field declarations — consume and continue }
else if Check(tkIdent) and SameText(FCurrent.Value, 'property') then
Result.Properties.Add(ParsePropertyDecl())
else if Check(tkIdent) or Check(tkLBracket) then
ParseFieldDecl(Result.Fields)
else if Check(tkFunction) then
else if (Check(tkIdent) and SameText(FCurrent.Value, 'property')) or
(Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() = tkIdent) and SameText(PeekValueAt(1), 'property')) then
begin
MethDecl := ParseMethodDecl(True);
LocalStatic := CurrStatic;
if Check(tkIdent) and SameText(FCurrent.Value, 'static') then
begin
LocalStatic := True;
Advance(); { consume per-member `static` prefix }
end;
PropDecl := ParsePropertyDecl();
PropDecl.IsStatic := LocalStatic;
Result.Properties.Add(PropDecl);
end
{ Method declarations — checked BEFORE the generic field branch so that a
leading `static` prefix on a method is not mistaken for a field name. }
else if Check(tkFunction) or Check(tkProcedure) or
Check(tkConstructor) or Check(tkDestructor) or
(Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkFunction, tkProcedure, tkConstructor, tkDestructor])) then
begin
LocalStatic := CurrStatic;
if Check(tkIdent) and SameText(FCurrent.Value, 'static') then
begin
LocalStatic := True;
Advance(); { consume per-member `static` prefix }
end;
if Check(tkConstructor) or Check(tkDestructor) then
begin
if LocalStatic then
raise EParseError.Create(Format(
'A constructor or destructor cannot be declared static at line %d col %d in %s'
+ ' — Blaise has no type-initialiser (class constructor); use the unit'
+ ' initialization/finalization sections',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
MethDecl := ParseMethodDecl(False);
end
else if Check(tkFunction) then
MethDecl := ParseMethodDecl(True)
else
MethDecl := ParseMethodDecl(False);
MethDecl.IsPublished := CurrPublished;
MethDecl.IsStatic := LocalStatic;
Result.Methods.Add(MethDecl);
end
else if Check(tkProcedure) or Check(tkConstructor) or Check(tkDestructor) then
else if Check(tkIdent) or Check(tkLBracket) then
begin
MethDecl := ParseMethodDecl(False);
MethDecl.IsPublished := CurrPublished;
Result.Methods.Add(MethDecl);
FieldCountBefore := Result.Fields.Count;
ParseFieldDecl(Result.Fields);
{ Stamp class-var flag on every field this decl produced. }
if CurrStatic then
for FieldIdx := FieldCountBefore to Result.Fields.Count - 1 do
TFieldDecl(Result.Fields.Items[FieldIdx]).IsClassVar := True;
end
else
Break;
@ -2301,7 +2445,6 @@ begin
SameText(FCurrent.Value, 'pascal') or
SameText(FCurrent.Value, 'safecall') or
SameText(FCurrent.Value, 'reintroduce') or
SameText(FCurrent.Value, 'static') or
SameText(FCurrent.Value, 'final') or
SameText(FCurrent.Value, 'assembler') or
SameText(FCurrent.Value, 'nostackframe') or
@ -2482,11 +2625,24 @@ end;
procedure TParser.ParseStandaloneDecl(ABlock: TBlock);
var
IsFunc: Boolean;
MD: TMethodDecl;
IsFunc: Boolean;
IsStat: Boolean;
MD: TMethodDecl;
begin
{ Leading `static` on an out-of-line implementation: `static function T.M ...`.
Consume it and record the static-ness on the parsed decl, where it is later
reconciled against the in-class declaration. }
IsStat := False;
if Check(tkIdent) and SameText(FCurrent.Value, 'static') and
(PeekKind() in [tkFunction, tkProcedure]) then
begin
IsStat := True;
Advance();
end;
IsFunc := Check(tkFunction);
MD := ParseMethodDecl(IsFunc, True); { True = may have nested proc/func decls }
if IsStat then
MD.IsStatic := True;
ABlock.ProcDecls.Add(MD);
end;

View file

@ -3271,6 +3271,7 @@ begin
if NewPDecl.IndexTypeName <> '' then
PropInfo.IndexTypeDesc := FindTypeOrInstantiate(NewPDecl.IndexTypeName);
PropInfo.IsDefault := NewPDecl.IsDefault;
PropInfo.IsStatic := NewPDecl.IsStatic;
RT.AddProperty(PropInfo);
end;
@ -4839,6 +4840,7 @@ var
MethodList: TObjectList;
Grp: TObjectList;
FDecl: TFieldDecl;
ClassVarEmit: string;
MDecl: TMethodDecl;
Par: TMethodParam;
ParType: TTypeDesc;
@ -5523,14 +5525,52 @@ begin
for K := 0 to FDecl.Names.Count - 1 do
begin
FldName := FDecl.Names.Strings[K];
RT.AddField(FldName, FldType);
{ Propagate the weak/unretained flags to the just-added field info so
codegen and the field cleanup emitter can consult them without
walking back to the AST. }
if FDecl.IsWeak then
RT.FindField(FldName).IsWeak := True;
if FDecl.IsUnretained then
RT.FindField(FldName).IsUnretained := True;
if FDecl.IsClassVar then
begin
{ STATIC (class-level) variable: not an instance field — do NOT call
AddField (that would advance the instance layout). Register a single
shared global instead, under a mangled emit label, reachable by both
the bare name (inside methods) and the qualified 'TFoo.Name' form.
Class- and interface-typed static vars ARE supported: they are a
single shared pointer slot, zero-initialised to nil, and assignment
goes through the normal managed-global store ARC (retain new /
release old). String and dynamic-array static vars remain deferred
(their exit-time release story is not yet wired). }
if (FldType.Kind = tyString) or (FldType.Kind = tyDynArray) then
SemanticError(Format(
'static var ''%s'': string and dynamic-array static fields are ' +
'not yet supported (class and interface are)', [FldName]),
FDecl.Line, FDecl.Col);
ClassVarEmit := CurrentUnitPrefix() + TD.Name + '_' + FldName;
{ Single source of truth for codegen: stash the mangled label on the
field decl (used by both backends to emit the data slot, even for a
read-only static var that no assignment auto-registers). }
if FDecl.Names.Count = 1 then
FDecl.ClassVarEmitName := ClassVarEmit;
{ Bare name — usable unqualified inside the class's own methods. }
Sym := TSymbol.Create(FldName, skVariable, FldType);
Sym.IsGlobal := True;
Sym.IsClassVar := True;
Sym.GlobalEmitName := ClassVarEmit;
if not FTable.Define(Sym) then Sym.Free();
{ Qualified name — usable as TFoo.Name from anywhere. }
Sym := TSymbol.Create(TD.Name + '.' + FldName, skVariable, FldType);
Sym.IsGlobal := True;
Sym.IsClassVar := True;
Sym.GlobalEmitName := ClassVarEmit;
if not FTable.Define(Sym) then Sym.Free();
end
else
begin
RT.AddField(FldName, FldType);
{ Propagate the weak/unretained flags to the just-added field info so
codegen and the field cleanup emitter can consult them without
walking back to the AST. }
if FDecl.IsWeak then
RT.FindField(FldName).IsWeak := True;
if FDecl.IsUnretained then
RT.FindField(FldName).IsUnretained := True;
end;
end;
end;
@ -5625,6 +5665,7 @@ begin
if PropDecl.IndexTypeName <> '' then
PropInfo.IndexTypeDesc := FTable.FindType(PropDecl.IndexTypeName);
PropInfo.IsDefault := PropDecl.IsDefault;
PropInfo.IsStatic := PropDecl.IsStatic;
RT.AddProperty(PropInfo);
end;
@ -5847,18 +5888,29 @@ begin
Exit;
end;
SavedClass := FCurrentClass;
FCurrentClass := AClassType;
{ A STATIC (class-level) method has no instance receiver. Leave FCurrentClass
at its saved value (do NOT bind it to AClassType) so that an implicit
instance-member reference inside the body resolves as an undeclared
identifier rather than silently binding to a non-existent Self. Static
members of the same type remain reachable through their registered bare
global symbols. }
if not AMethod.IsStatic then
FCurrentClass := AClassType;
FTable.PushScope();
Inc(FScopeDepth);
try
{ Record methods receive the record by pointer (like a var param); class
methods receive the object pointer as a value. Declaring Self as
skVarParameter for records makes the codegen dereference it correctly. }
if AMethod.IsRecordMethod then
Sym := TSymbol.Create('Self', skVarParameter, AClassType)
else
Sym := TSymbol.Create('Self', skVariable, AClassType);
FTable.Define(Sym);
skVarParameter for records makes the codegen dereference it correctly.
A static method receives NO Self. }
if not AMethod.IsStatic then
begin
if AMethod.IsRecordMethod then
Sym := TSymbol.Create('Self', skVarParameter, AClassType)
else
Sym := TSymbol.Create('Self', skVariable, AClassType);
FTable.Define(Sym);
end;
{ For function methods, define Result as a writable variable }
if AMethod.ResolvedReturnType <> nil then
@ -7622,6 +7674,35 @@ begin
end;
ObjSym := FTable.Lookup(ACall.ObjectName);
{ TypeName.StaticMethod() — a static (class-level) method called through the
class name in statement position. Resolve the method on the class; it must
be declared static. Lowered with NO Self. }
if (ObjSym <> nil) and (ObjSym.Kind = skType) and (ObjSym.TypeDesc <> nil) and
(ObjSym.TypeDesc.Kind = tyClass) then
begin
RT := TRecordTypeDesc(ObjSym.TypeDesc);
for I := 0 to ACall.Args.Count - 1 do
AnalyseExpr(TASTExpr(ACall.Args.Items[I]));
MDecl := ResolveMethodOverload(RT.Name, ACall.Name, ACall.Args,
ACall.Line, ACall.Col);
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, ACall.Name]),
ACall.Line, ACall.Col);
if not MDecl.IsStatic then
SemanticError(
Format('''%s.%s'' is not a static method — call it on an instance',
[RT.Name, ACall.Name]),
ACall.Line, ACall.Col);
AppendDefaultArgs(ACall.Args, MDecl, ACall.Name, ACall.Line, ACall.Col);
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
ACall.IsStaticCall := True;
if (MDecl.ResolvedReturnType <> nil) and
(MDecl.ResolvedReturnType.Kind = tyRecord) then
ACall.ResolvedReturnTypeDesc := MDecl.ResolvedReturnType;
Exit;
end;
{ Inside a method, class fields shadow same-named globals. Try the
implicit Self.Field path when there is no match (ObjSym=nil) OR when
the only match is a global variable that a class field should shadow. }
@ -7858,6 +7939,11 @@ begin
AAssign.Line, AAssign.Col);
AAssign.Name := VarSym.Name; { normalise to declared casing }
{ Static (class-level) var: the assignment target is the single shared global
emitted under the mangled GlobalEmitName, so the LHS name must be that label
(matches the read path in AnalyseIdentExpr). }
if VarSym.IsClassVar and (VarSym.GlobalEmitName <> '') then
AAssign.Name := VarSym.GlobalEmitName;
AAssign.IsVarParam := (VarSym.Kind = skVarParameter);
AAssign.ResolvedLhsType := VarSym.TypeDesc;
AAssign.IsWeakLhs := VarSym.IsWeak;
@ -10472,6 +10558,38 @@ begin
if SameText(ObjSym.Name, AExpr.ObjectName) then
AExpr.ObjectName := ObjSym.Name;
{ TypeName.StaticFunction(args) in expression position — a static (class-level)
method reached through the class name. Must be declared static; lowered with
NO Self. Checked before the Create-constructor branch so a non-Create static
method resolves here rather than falling through to "is not a variable". }
if (ObjSym.Kind = skType) and (ObjSym.TypeDesc <> nil) and
(ObjSym.TypeDesc.Kind in [tyClass, tyRecord]) and
not (SameText(AExpr.Name, 'Create') or (StrPos('Create', AExpr.Name) = 0)) then
begin
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args.Items[I]));
MDecl := ResolveMethodOverload(ObjSym.Name, AExpr.Name, AExpr.Args,
AExpr.Line, AExpr.Col);
if MDecl = nil then
MDecl := FindMethodDecl(ObjSym.Name, AExpr.Name);
if MDecl = nil then
SemanticError(
Format('Type ''%s'' has no method ''%s''', [ObjSym.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
if not MDecl.IsStatic then
SemanticError(
Format('''%s.%s'' is not a static method — call it on an instance',
[ObjSym.Name, AExpr.Name]),
AExpr.Line, AExpr.Col);
AppendDefaultArgs(AExpr.Args, MDecl, AExpr.Name, AExpr.Line, AExpr.Col);
AExpr.ResolvedMethod := MDecl;
AExpr.ResolvedClassType := ObjSym.TypeDesc;
AExpr.IsStaticCall := True;
Result := MDecl.ResolvedReturnType;
AExpr.ResolvedType := Result;
Exit;
end;
{ 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
@ -10791,6 +10909,12 @@ begin
the QBE ABI level: the local slot holds a pointer, not the aggregate
bytes. Codegen must dereference the slot before reading fields. }
TIdentExpr(AExpr).Name := Sym.Name; { normalise to declared casing }
{ A static (class-level) variable resolves to a single shared global whose
emit label is the mangled GlobalEmitName, distinct from the lookup key
('FInstance' or 'TFoo.FInstance' both reach the one slot). Rewrite the
node's emitted name to that label so codegen lowers $TFoo_FInstance. }
if Sym.IsClassVar and (Sym.GlobalEmitName <> '') then
TIdentExpr(AExpr).Name := Sym.GlobalEmitName;
if Sym.Kind = skVarParameter then
TIdentExpr(AExpr).ParamMode := pmVar
else if (Sym.Kind = skParameter) and (Sym.TypeDesc <> nil) and
@ -10944,6 +11068,7 @@ var
PropInfo: TPropertyInfo;
BaseType: TTypeDesc;
IntfDesc: TInterfaceTypeDesc;
MDecl: TMethodDecl;
begin
{ Chained access: A.B.C — base is another expression whose type must be
a record or class. Leaf lookup uses Base.ResolvedType; RecordName path
@ -11262,6 +11387,41 @@ begin
AAccess.ResolvedType := Sym.TypeDesc;
Exit(Sym.TypeDesc);
end;
{ Static (class-level) variable read: TypeName.StaticVar. The qualified
symbol carries the mangled GlobalEmitName; rewrite the access to a plain
global read of that label (codegen lowers it like a global ident). }
if (Sym <> nil) and (Sym.Kind = skVariable) and Sym.IsClassVar then
begin
AAccess.IsClassVarRead := True;
AAccess.IsGlobal := True;
AAccess.ClassVarEmitName := Sym.GlobalEmitName;
AAccess.ResolvedType := Sym.TypeDesc;
Exit(Sym.TypeDesc);
end;
{ Static property read: TypeName.StaticProp. Resolve to its (static)
getter and lower as a static getter call. Only method-backed static
getters are supported (a field-backed static property would resolve to
the static var directly). }
PropInfo := TRecordTypeDesc(RecSym.TypeDesc).FindProperty(AAccess.FieldName);
if (PropInfo <> nil) and PropInfo.IsStatic then
begin
if PropInfo.ReadMethod = '' then
SemanticError(
Format('Static property ''%s.%s'' has no readable static getter',
[AAccess.RecordName, AAccess.FieldName]),
AAccess.Line, AAccess.Col);
MDecl := FindMethodDecl(TRecordTypeDesc(RecSym.TypeDesc).Name, PropInfo.ReadMethod);
if (MDecl = nil) or not MDecl.IsStatic then
SemanticError(
Format('Static property ''%s.%s'' getter ''%s'' is not a static method',
[AAccess.RecordName, AAccess.FieldName, PropInfo.ReadMethod]),
AAccess.Line, AAccess.Col);
AAccess.IsStaticPropGet := True;
AAccess.ResolvedMethod := MDecl;
AAccess.ResolvedClassType := RecSym.TypeDesc;
AAccess.ResolvedType := MDecl.ResolvedReturnType;
Exit(MDecl.ResolvedReturnType);
end;
SemanticError(
Format('Unknown class method ''%s'' on type ''%s''',
[AAccess.FieldName, AAccess.RecordName]),

View file

@ -219,6 +219,7 @@ type
IndexParamName: string; { '' = non-indexed property }
[Unretained] IndexTypeDesc: TTypeDesc; { not owned; non-nil when IndexParamName <> '' }
IsDefault: Boolean; { declared with the `default` directive (Obj[I] sugar) }
IsStatic: Boolean; { declared `static property` — accessors are static (no Self) }
end;
{ Type descriptor for zero-GUID interface types (Phase 3). }
@ -368,6 +369,16 @@ type
of the strong addref/release pattern. }
IsGlobal: Boolean; { true for program-level variables; codegen uses
QBE data-section storage instead of stack alloc }
IsClassVar: Boolean; { true for a STATIC (class-level) variable. Stored
as a single shared global (IsGlobal is also True),
under the mangled GlobalEmitName, not as a
per-instance field. }
GlobalEmitName: string; { when non-empty, the mangled data-label this symbol
emits/loads/stores under (e.g. 'TFoo_FInstance').
Lets a bare lookup key ('FInstance') and a
qualified one ('TFoo.FInstance') resolve to ONE
storage slot. Mirrors ConstArrayQbe for consts.
Empty = emit under Name. }
IsThreadVar: Boolean; { true for threadvar declarations; codegen emits
TLS storage instead of plain global data }
IsOverload: Boolean; { true when declared with the 'overload' directive;

View file

@ -53,7 +53,14 @@ uses
const
IFACE_MAGIC = 'BLAISE-IFACE';
IFACE_VERSION = 2; { v1: shipped through release v0.11.x (last public commit
IFACE_VERSION = 3; { v3 (this cycle): `static` (class-level) member facts now
serialised — TFieldDecl.IsClassVar + ClassVarEmitName in
EncodeFieldList/ReadFieldList, TPropertyDecl.IsStatic in
EncodePropertyList/ReadPropertyList, and record/class
static-const declarations (ConstDecls) in the record/class
payloads. The layout grew, so v2 readers must reject these
.bif and recompile.
v1: shipped through release v0.11.x (last public commit
d56bdbf).
v2 (release v0.12.0): a batch of META/record additions
made this cycle, all gated by this single bump since
@ -266,7 +273,39 @@ begin
Result := Result +
EncodeLpstr(F.Names.Strings[J]) +
EncodeLpstr(F.TypeName) +
EncodeBool(F.IsWeak);
EncodeBool(F.IsWeak) +
{ `static var` (class variable) facts: IsClassVar marks the field
as a shared global rather than an instance slot; ClassVarEmitName
is the defining unit's mangled global label (set by uSemantic).
An importer cannot re-derive that label — its own unit prefix
differs — so the label is carried verbatim. Both default to
False/'' for ordinary instance fields. }
EncodeBool(F.IsClassVar) +
EncodeLpstr(F.ClassVarEmitName);
end;
end;
{ Record/class-level `static const` declarations. These are scalar
constants folded by the semantic pass; we serialise the same five-
field scalar shape the unit-level CONST block uses (Name, TypeName,
IntVal, StrVal, IsString/IsFloat flags). Non-scalar const forms
(array consts, unfolded expression tokens) are not carried here —
static const members are scalar in the current language surface. }
function EncodeConstDeclList(AConsts: TObjectList): string;
var
I: Integer;
C: TConstDecl;
begin
Result := EncodeCount(AConsts.Count);
for I := 0 to AConsts.Count - 1 do
begin
C := TConstDecl(AConsts.Items[I]);
Result := Result +
EncodeLpstr(C.Name) +
EncodeLpstr(C.TypeName) +
EncodeInt64(C.IntVal) +
EncodeLpstr(C.StrVal) +
EncodeFlags(C.IsString, C.IsFloat);
end;
end;
@ -313,7 +352,9 @@ var
begin
Def := TRecordTypeDef(AEntry.Def);
Result := EncodeBool(Def.IsPacked) +
EncodeFieldList(Def.Fields);
EncodeFieldList(Def.Fields) +
{ record-level `static const` declarations. }
EncodeConstDeclList(Def.ConstDecls);
end;
function EncodePropertyList(AList: TObjectList): string;
@ -332,7 +373,10 @@ begin
EncodeLpstr(P.WriteName) +
EncodeLpstr(P.IndexParamName) +
EncodeLpstr(P.IndexTypeName) +
EncodeBool(P.IsDefault);
EncodeBool(P.IsDefault) +
{ `static property` — accessors take no implicit Self. An importer
needs this to dispatch reads/writes without a receiver. }
EncodeBool(P.IsStatic);
end;
end;
@ -348,7 +392,9 @@ begin
EncodeStringList(AEntry.Implements) +
EncodeFieldList(Def.Fields) +
EncodeMethodList(AEntry.Methods) +
EncodePropertyList(Def.Properties);
EncodePropertyList(Def.Properties) +
{ class-level `static const` declarations. }
EncodeConstDeclList(Def.ConstDecls);
end;
{ Encode a TMethodDecl (AST) using the same per-method payload shape
@ -1769,11 +1815,13 @@ end;
procedure ReadFieldList(const AText: string; var APos: Integer;
ATarget: TObjectList);
var
C, I: Integer;
FldName: string;
FldType: string;
IsWeak: Boolean;
F: TFieldDecl;
C, I: Integer;
FldName: string;
FldType: string;
IsWeak: Boolean;
IsClassVar: Boolean;
ClassVarEmit: string;
F: TFieldDecl;
begin
C := DecodeCount(AText, APos);
for I := 1 to C do
@ -1781,14 +1829,44 @@ begin
FldName := ReadLpstrAt(AText, APos);
FldType := ReadLpstrAt(AText, APos);
IsWeak := DecodeBool(AText, APos);
{ Order must mirror EncodeFieldList: IsClassVar then ClassVarEmitName. }
IsClassVar := DecodeBool(AText, APos);
ClassVarEmit := ReadLpstrAt(AText, APos);
F := TFieldDecl.Create();
F.Names.Add(FldName);
F.TypeName := FldType;
F.IsWeak := IsWeak;
F.IsClassVar := IsClassVar;
F.ClassVarEmitName := ClassVarEmit;
ATarget.Add(F);
end;
end;
{ Inverse of EncodeConstDeclList — rebuild record/class `static const`
decls. Mirrors the writer's five-field scalar shape exactly. }
procedure ReadConstDeclList(const AText: string; var APos: Integer;
ATarget: TObjectList);
var
C, I: Integer;
CD: TConstDecl;
IsString: Boolean;
IsFloat: Boolean;
begin
C := DecodeCount(AText, APos);
for I := 1 to C do
begin
CD := TConstDecl.Create();
CD.Name := ReadLpstrAt(AText, APos);
CD.TypeName := ReadLpstrAt(AText, APos);
CD.IntVal := ReadInt64At(AText, APos);
CD.StrVal := ReadLpstrAt(AText, APos);
ReadFlagsAt(AText, APos, IsString, IsFloat);
CD.IsString := IsString;
CD.IsFloat := IsFloat;
ATarget.Add(CD);
end;
end;
{ Inverse of EncodeMethodSig — builds a TRoutineSig from the
per-method payload. }
function ReadMethodSig(const AText: string; var APos: Integer): TRoutineSig;
@ -1840,6 +1918,7 @@ begin
Def := TRecordTypeDef.Create();
Def.IsPacked := DecodeBool(AText, APos);
ReadFieldList(AText, APos, Def.Fields);
ReadConstDeclList(AText, APos, Def.ConstDecls);
AEntry.Def := Def;
end;
@ -1862,6 +1941,7 @@ begin
ReadFieldList(AText, APos, Def.Fields);
ReadMethodList(AText, APos, AEntry.Methods);
ReadPropertyList(AText, APos, Def.Properties);
ReadConstDeclList(AText, APos, Def.ConstDecls);
AEntry.IsClass := True;
AEntry.Def := Def;
end;
@ -1919,6 +1999,8 @@ begin
P.IndexParamName := ReadLpstrAt(AText, APos);
P.IndexTypeName := ReadLpstrAt(AText, APos);
P.IsDefault := DecodeBool(AText, APos);
{ Order must mirror EncodePropertyList: IsStatic follows IsDefault. }
P.IsStatic := DecodeBool(AText, APos);
ATarget.Add(P);
end;
end;

View file

@ -122,6 +122,8 @@ uses
cp.test.proctypes,
cp.test.overload,
cp.test.classof,
cp.test.staticmembers,
cp.test.e2e.staticmembers,
cp.test.publishedrtti,
cp.test.attributes,
cp.test.proctypes_ofobject,

View file

@ -0,0 +1,225 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit cp.test.e2e.staticmembers;
{ E2E tests for `static` class/record members: static var, static method,
static property, and the lazy-singleton pattern. Each program is compiled
and run on every backend (QBE + native). Parser/semantic/IR tests live in
cp.test.staticmembers.pas; these guard the codegen -> QBE -> run boundary
the IR harness cannot see (data-slot emission, no-Self call ABI, qualified
static reads). }
interface
uses
classes, blaise.testing, cp.test.e2e.base;
type
[Threaded]
TE2EStaticMembersTests = class(TE2ETestCase)
protected
procedure SetUp; override;
published
procedure TestRun_StaticMethod_NoSelf;
procedure TestRun_StaticVar_SharedAcrossCalls;
procedure TestRun_StaticVar_QualifiedRead;
procedure TestRun_StaticProperty_QualifiedRead;
procedure TestRun_Singleton_LazyGetInstance;
procedure TestRun_StaticConst_OnClass;
procedure TestRun_RecordStaticFactory;
end;
implementation
procedure TE2EStaticMembersTests.SetUp;
begin
inherited SetUp();
SetUpScratch('compiler/target/test-e2e-staticmembers')
end;
procedure TE2EStaticMembersTests.TestRun_StaticMethod_NoSelf;
const Src =
'''
program P;
type
TMath = class
public
static function Square(X: Integer): Integer;
end;
static function TMath.Square(X: Integer): Integer;
begin
Result := X * X;
end;
begin
WriteLn(TMath.Square(7))
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertRunsOnAll(Src, '49' + LineEnding, 0);
end;
procedure TE2EStaticMembersTests.TestRun_StaticVar_SharedAcrossCalls;
const Src =
'''
program P;
type
TCounter = class
private static var
FN: Integer;
public
static function Next: Integer;
end;
static function TCounter.Next: Integer;
begin
FN := FN + 1;
Result := FN;
end;
begin
WriteLn(TCounter.Next());
WriteLn(TCounter.Next());
WriteLn(TCounter.Next())
end.
''';
var LE: string;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
LE := LineEnding;
AssertRunsOnAll(Src, '1' + LE + '2' + LE + '3' + LE, 0);
end;
procedure TE2EStaticMembersTests.TestRun_StaticVar_QualifiedRead;
const Src =
'''
program P;
type
TCounter = class
public static var
Total: Integer;
static procedure Bump;
end;
static procedure TCounter.Bump;
begin
Total := Total + 10;
end;
begin
TCounter.Bump();
TCounter.Bump();
WriteLn(TCounter.Total)
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertRunsOnAll(Src, '20' + LineEnding, 0);
end;
procedure TE2EStaticMembersTests.TestRun_StaticProperty_QualifiedRead;
const Src =
'''
program P;
type
TRegistry = class
private static var
FCounter: Integer;
public
static function NextId: Integer;
static property Counter: Integer read NextId;
end;
static function TRegistry.NextId: Integer;
begin
FCounter := FCounter + 1;
Result := FCounter;
end;
begin
WriteLn(TRegistry.Counter);
WriteLn(TRegistry.Counter)
end.
''';
var LE: string;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
LE := LineEnding;
AssertRunsOnAll(Src, '1' + LE + '2' + LE, 0);
end;
procedure TE2EStaticMembersTests.TestRun_Singleton_LazyGetInstance;
const Src =
'''
program P;
type
TConfig = class
private static var
FInstance: TConfig;
public
FValue: Integer;
static function Instance: TConfig;
end;
static function TConfig.Instance: TConfig;
begin
if FInstance = nil then
FInstance := TConfig.Create();
Result := FInstance;
end;
begin
TConfig.Instance().FValue := 42;
WriteLn(TConfig.Instance().FValue)
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertRunsOnAll(Src, '42' + LineEnding, 0);
end;
procedure TE2EStaticMembersTests.TestRun_StaticConst_OnClass;
const Src =
'''
program P;
type
TLimits = class
public static const
MaxItems = 128;
end;
begin
WriteLn(TLimits.MaxItems)
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertRunsOnAll(Src, '128' + LineEnding, 0);
end;
procedure TE2EStaticMembersTests.TestRun_RecordStaticFactory;
const Src =
'''
program P;
type
TPoint = record
X, Y: Integer;
static function Make(AX, AY: Integer): TPoint;
end;
static function TPoint.Make(AX, AY: Integer): TPoint;
begin
Result.X := AX;
Result.Y := AY;
end;
var Pt: TPoint;
begin
Pt := TPoint.Make(3, 4);
WriteLn(Pt.X + Pt.Y)
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertRunsOnAll(Src, '7' + LineEnding, 0);
end;
initialization
RegisterTest(TE2EStaticMembersTests);
end.

View file

@ -0,0 +1,967 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit cp.test.staticmembers;
{ Tests for `static` (class-level) members: static var, static const,
static function/procedure, static property, on classes and records.
Feature 1 of the static-members work. Covers the PARSER layer
(TStaticMembersParseTests — the forms parse, the IsStatic / IsClassVar AST
flags are set, and `static constructor` / `static destructor` are rejected)
and the SEMANTIC + IR layer (TStaticMembersSemTests — resolution of static
vars to shared globals, no-Self static methods, qualified static var/property
reads, and the program-exit release of class-typed static vars). End-to-end
compile+run behaviour lives in cp.test.e2e.staticmembers.pas. }
interface
uses
Classes, SysUtils, blaise.testing,
uLexer, uParser, uAST, uSemantic, uSymbolTable, blaise.codegen.qbe;
type
TStaticMembersParseTests = class(TTestCase)
private
function ParseSrc(const ASrc: string): TProgram;
function ClassOf(AProg: TProgram; AIndex: Integer): TClassTypeDef;
function RecordOf(AProg: TProgram; AIndex: Integer): TRecordTypeDef;
procedure ParseExpectError(const ASrc: string);
procedure ParseExpectErrorMsg(const ASrc, AExpectedSubstr: string);
published
{ static var / static const sections on a class }
procedure TestParse_StaticVarSection_SetsIsClassVar;
procedure TestParse_PlainFieldAfterStaticVar_NotClassVar;
procedure TestParse_PrivateStaticVar_SetsIsClassVar;
procedure TestParse_StaticConstSection_OnClass;
procedure TestParse_StaticConstSection_FollowedByMoreMembers;
{ static method prefix and section }
procedure TestParse_StaticFunctionPrefix_SetsIsStatic;
procedure TestParse_StaticProcedurePrefix_SetsIsStatic;
procedure TestParse_StaticSection_AppliesToMethods;
procedure TestParse_PlainMethodAfterStaticSection_NotStatic;
procedure TestParse_BareStaticKeepsCurrentVisibility;
{ static property }
procedure TestParse_StaticProperty_SetsIsStatic;
{ out-of-line static impl }
procedure TestParse_StaticImpl_OutOfLine;
{ records }
procedure TestParse_StaticFunctionInRecord_SetsIsStatic;
procedure TestParse_StaticVarInRecord_SetsIsClassVar;
procedure TestParse_StaticConstInRecord;
{ rejections }
procedure TestParse_StaticConstructor_Rejected;
procedure TestParse_StaticDestructor_Rejected;
end;
implementation
function TStaticMembersParseTests.ParseSrc(const ASrc: string): TProgram;
var L: TLexer; P: TParser;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
try
Result := P.Parse();
finally
P.Free(); L.Free();
end;
end;
function TStaticMembersParseTests.ClassOf(AProg: TProgram; AIndex: Integer): TClassTypeDef;
var TD: TTypeDecl;
begin
TD := TTypeDecl(AProg.Block.TypeDecls.Items[AIndex]);
Result := TD.Def as TClassTypeDef;
end;
function TStaticMembersParseTests.RecordOf(AProg: TProgram; AIndex: Integer): TRecordTypeDef;
var TD: TTypeDecl;
begin
TD := TTypeDecl(AProg.Block.TypeDecls.Items[AIndex]);
Result := TD.Def as TRecordTypeDef;
end;
procedure TStaticMembersParseTests.ParseExpectError(const ASrc: string);
var Prog: TProgram;
begin
try
Prog := ParseSrc(ASrc);
Prog.Free();
Fail('Expected EParseError');
except
on E: EParseError do ;
end;
end;
procedure TStaticMembersParseTests.ParseExpectErrorMsg(const ASrc, AExpectedSubstr: string);
var Prog: TProgram;
begin
try
Prog := ParseSrc(ASrc);
Prog.Free();
Fail('Expected EParseError');
except
on E: EParseError do
AssertTrue('error message contains "' + AExpectedSubstr + '" (got: ' +
E.Message + ')', Pos(AExpectedSubstr, E.Message) >= 0);
end;
end;
{ ------------------------------------------------------------------ }
{ static var / static const }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticVarSection_SetsIsClassVar;
const
Src =
'''
program P;
type
TFoo = class
static var
FInstance: TFoo;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef; F: TFieldDecl;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one field', 1, C.Fields.Count);
F := TFieldDecl(C.Fields.Items[0]);
AssertEquals('field name', 'FInstance', F.Names[0]);
AssertTrue('FInstance is a class var', F.IsClassVar);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_PlainFieldAfterStaticVar_NotClassVar;
const
Src =
'''
program P;
type
TFoo = class
static var
FShared: Integer;
public
FInst: Integer;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('two fields', 2, C.Fields.Count);
AssertTrue('FShared is class var', TFieldDecl(C.Fields.Items[0]).IsClassVar);
AssertFalse('FInst is NOT class var (section reset by public)',
TFieldDecl(C.Fields.Items[1]).IsClassVar);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_PrivateStaticVar_SetsIsClassVar;
const
Src =
'''
program P;
type
TFoo = class
private static var
FInstance: TFoo;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one field', 1, C.Fields.Count);
AssertTrue('private static var is class var',
TFieldDecl(C.Fields.Items[0]).IsClassVar);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticConstSection_OnClass;
const
Src =
'''
program P;
type
TFoo = class
static const
MaxItems = 256;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one const', 1, C.ConstDecls.Count);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticConstSection_FollowedByMoreMembers;
const
{ A static const section is not the last section: the const block must stop
at the following `private` visibility keyword rather than swallowing it. }
Src =
'''
program P;
type
TFoo = class
private static const
MaxEntries = 256;
private
FCount: Integer;
public
function GetCount: Integer;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one const', 1, C.ConstDecls.Count);
AssertEquals('one field', 1, C.Fields.Count);
AssertEquals('field name', 'FCount', TFieldDecl(C.Fields.Items[0]).Names[0]);
AssertEquals('one method', 1, C.Methods.Count);
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ static methods }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticFunctionPrefix_SetsIsStatic;
const
Src =
'''
program P;
type
TFoo = class
static function GetInstance: TFoo;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef; M: TMethodDecl;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one method', 1, C.Methods.Count);
M := TMethodDecl(C.Methods.Items[0]);
AssertEquals('method name', 'GetInstance', M.Name);
AssertTrue('GetInstance is static', M.IsStatic);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticProcedurePrefix_SetsIsStatic;
const
Src =
'''
program P;
type
TFoo = class
static procedure Reset;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertTrue('Reset is static', TMethodDecl(C.Methods.Items[0]).IsStatic);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticSection_AppliesToMethods;
const
Src =
'''
program P;
type
TFoo = class
public static
function A: Integer;
function B: Integer;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('two methods', 2, C.Methods.Count);
AssertTrue('A is static', TMethodDecl(C.Methods.Items[0]).IsStatic);
AssertTrue('B is static', TMethodDecl(C.Methods.Items[1]).IsStatic);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_PlainMethodAfterStaticSection_NotStatic;
const
Src =
'''
program P;
type
TFoo = class
public static
function A: Integer;
public
function B: Integer;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertTrue('A is static', TMethodDecl(C.Methods.Items[0]).IsStatic);
AssertFalse('B is NOT static (section reset by public)',
TMethodDecl(C.Methods.Items[1]).IsStatic);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_BareStaticKeepsCurrentVisibility;
const
Src =
'''
program P;
type
TFoo = class
private
FInst: Integer;
static
function A: Integer;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertFalse('FInst not class var', TFieldDecl(C.Fields.Items[0]).IsClassVar);
AssertTrue('A is static', TMethodDecl(C.Methods.Items[0]).IsStatic);
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ static property }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticProperty_SetsIsStatic;
const
Src =
'''
program P;
type
TFoo = class
static function GetInstance: TFoo;
static property Instance: TFoo read GetInstance;
end;
begin end.
''';
var Prog: TProgram; C: TClassTypeDef; Pr: TPropertyDecl;
begin
Prog := ParseSrc(Src);
try
C := ClassOf(Prog, 0);
AssertEquals('one property', 1, C.Properties.Count);
Pr := TPropertyDecl(C.Properties.Items[0]);
AssertEquals('property name', 'Instance', Pr.Name);
AssertTrue('Instance property is static', Pr.IsStatic);
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ out-of-line static impl }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticImpl_OutOfLine;
const
Src =
'''
program P;
type
TFoo = class
static function GetInstance: TFoo;
end;
static function TFoo.GetInstance: TFoo;
begin
Result := nil;
end;
begin end.
''';
var Prog: TProgram; M: TMethodDecl;
begin
Prog := ParseSrc(Src);
try
{ The out-of-line impl is a standalone proc decl in the block. }
AssertEquals('one standalone proc', 1, Prog.Block.ProcDecls.Count);
M := TMethodDecl(Prog.Block.ProcDecls.Items[0]);
AssertTrue('out-of-line impl is static', M.IsStatic);
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ records }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticFunctionInRecord_SetsIsStatic;
const
Src =
'''
program P;
type
TInfo = record
Category: string;
static function New(const C: string): TInfo;
end;
begin end.
''';
var Prog: TProgram; R: TRecordTypeDef;
begin
Prog := ParseSrc(Src);
try
R := RecordOf(Prog, 0);
AssertEquals('one method', 1, R.Methods.Count);
AssertTrue('New is static', TMethodDecl(R.Methods.Items[0]).IsStatic);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticVarInRecord_SetsIsClassVar;
const
Src =
'''
program P;
type
TInfo = record
Category: string;
static var
Count: Integer;
end;
begin end.
''';
var Prog: TProgram; R: TRecordTypeDef;
begin
Prog := ParseSrc(Src);
try
R := RecordOf(Prog, 0);
AssertEquals('two fields', 2, R.Fields.Count);
AssertFalse('Category not class var', TFieldDecl(R.Fields.Items[0]).IsClassVar);
AssertTrue('Count is class var', TFieldDecl(R.Fields.Items[1]).IsClassVar);
finally
Prog.Free();
end;
end;
procedure TStaticMembersParseTests.TestParse_StaticConstInRecord;
const
Src =
'''
program P;
type
TInfo = record
X: Integer;
static const
Tag = 7;
end;
begin end.
''';
var Prog: TProgram; R: TRecordTypeDef;
begin
Prog := ParseSrc(Src);
try
R := RecordOf(Prog, 0);
AssertEquals('one const', 1, R.ConstDecls.Count);
finally
Prog.Free();
end;
end;
{ ------------------------------------------------------------------ }
{ rejections }
{ ------------------------------------------------------------------ }
procedure TStaticMembersParseTests.TestParse_StaticConstructor_Rejected;
const
Src =
'''
program P;
type
TFoo = class
static constructor Create;
end;
begin end.
''';
begin
ParseExpectErrorMsg(Src, 'static');
end;
procedure TStaticMembersParseTests.TestParse_StaticDestructor_Rejected;
const
Src =
'''
program P;
type
TFoo = class
static destructor Destroy;
end;
begin end.
''';
begin
ParseExpectErrorMsg(Src, 'static');
end;
{ ================================================================== }
{ Semantic + IR (codegen) tests }
{ ================================================================== }
type
TStaticMembersSemTests = class(TTestCase)
private
function AnalyseSrc(const ASrc: string): TProgram;
function GenIR(const ASrc: string): string;
procedure AnalyseExpectErrorMsg(const ASrc, AExpectedSubstr: string);
published
{ static var resolves to a shared global, not an instance field }
procedure TestSem_StaticVar_BareReadResolves;
procedure TestSem_StaticVar_NotAnInstanceField;
procedure TestSem_StaticVar_RejectsStringType;
procedure TestSem_StaticMethod_NoSelf_CannotTouchInstanceField;
procedure TestSem_StaticMethod_CanReadStaticVar;
{ IR: static var lowers to a single global data slot; static method has
no Self parameter. }
procedure TestIR_StaticVar_EmitsGlobalDataSlot;
procedure TestIR_StaticVar_NoInstanceOffset;
procedure TestIR_StaticMethod_NoSelfParam;
procedure TestIR_Singleton_LazyGetInstance;
procedure TestIR_StaticVar_QualifiedRead_LoadsGlobal;
procedure TestIR_StaticProperty_QualifiedRead_CallsGetter;
procedure TestIR_ClassStaticVar_ReleasedAtExit;
end;
function TStaticMembersSemTests.AnalyseSrc(const ASrc: string): TProgram;
var L: TLexer; P: TParser; A: TSemanticAnalyser;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
try
Result := P.Parse();
finally
P.Free(); L.Free();
end;
A := TSemanticAnalyser.Create();
try
A.Analyse(Result);
finally
A.Free();
end;
end;
function TStaticMembersSemTests.GenIR(const ASrc: string): string;
var Prog: TProgram; CG: TCodeGenQBE;
begin
Prog := AnalyseSrc(ASrc);
try
CG := TCodeGenQBE.Create();
try
CG.Generate(Prog);
Result := CG.GetOutput();
finally
CG.Free();
end;
finally
Prog.Free();
end;
end;
procedure TStaticMembersSemTests.AnalyseExpectErrorMsg(const ASrc, AExpectedSubstr: string);
var Prog: TProgram;
begin
try
Prog := AnalyseSrc(ASrc);
Prog.Free();
Fail('Expected ESemanticError');
except
on E: ESemanticError do
AssertTrue('error message contains "' + AExpectedSubstr + '" (got: ' +
E.Message + ')', Pos(AExpectedSubstr, E.Message) >= 0);
end;
end;
procedure TStaticMembersSemTests.TestSem_StaticVar_BareReadResolves;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
static procedure Bump;
end;
static procedure TFoo.Bump;
begin
FCount := FCount + 1;
end;
begin end.
''';
var Prog: TProgram;
begin
{ Just analysing without an "undeclared identifier" error proves the bare
static-var name resolves inside a static method. }
Prog := AnalyseSrc(Src);
Prog.Free();
end;
procedure TStaticMembersSemTests.TestSem_StaticVar_NotAnInstanceField;
const
Src =
'''
program P;
type
TFoo = class
private static var
FShared: Integer;
public
FInst: Integer;
end;
var
F: TFoo;
begin
F := TFoo.Create();
F.FInst := 7;
end.
''';
var Prog: TProgram; RT: TRecordTypeDesc; Sym: TSymbol;
begin
Prog := AnalyseSrc(Src);
try
Sym := Prog.SymbolTable.Lookup('TFoo');
AssertNotNull('TFoo type present', Sym);
RT := TRecordTypeDesc(Sym.TypeDesc);
{ The class has exactly ONE instance field (FInst); FShared is a static
var and must NOT occupy an instance slot. }
AssertNotNull('FInst is an instance field', RT.FindField('FInst'));
AssertNull('FShared is NOT an instance field', RT.FindField('FShared'));
finally
Prog.Free();
end;
end;
procedure TStaticMembersSemTests.TestSem_StaticVar_RejectsStringType;
const
Src =
'''
program P;
type
TFoo = class
private static var
FName: string;
end;
begin end.
''';
begin
{ String (and dynamic-array) static vars remain deferred; class and
interface are supported (see TestSem_StaticVar_AcceptsClassType). }
AnalyseExpectErrorMsg(Src, 'static var');
end;
procedure TStaticMembersSemTests.TestSem_StaticMethod_NoSelf_CannotTouchInstanceField;
const
Src =
'''
program P;
type
TFoo = class
public
FInst: Integer;
static procedure Bad;
end;
static procedure TFoo.Bad;
begin
FInst := 1;
end;
begin end.
''';
begin
{ A static method has no Self, so an instance field is not in scope. }
AnalyseExpectErrorMsg(Src, 'FInst');
end;
procedure TStaticMembersSemTests.TestSem_StaticMethod_CanReadStaticVar;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
public
static function GetCount: Integer;
end;
static function TFoo.GetCount: Integer;
begin
Result := FCount;
end;
begin end.
''';
var Prog: TProgram;
begin
Prog := AnalyseSrc(Src);
Prog.Free();
end;
procedure TStaticMembersSemTests.TestIR_StaticVar_EmitsGlobalDataSlot;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
static procedure Bump;
end;
static procedure TFoo.Bump;
begin
FCount := FCount + 1;
end;
begin end.
''';
var IR: string;
begin
IR := GenIR(Src);
{ A single shared global data slot named TFoo_FCount must be emitted. }
AssertTrue('emits $TFoo_FCount data slot (IR: ' + Copy(IR, 0, 400) + ')',
Pos('data $TFoo_FCount', IR) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_StaticVar_NoInstanceOffset;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
static procedure Bump;
end;
static procedure TFoo.Bump;
begin
FCount := FCount + 1;
end;
begin end.
''';
var IR: string;
begin
IR := GenIR(Src);
{ The static var read/write must reference the global $TFoo_FCount, not an
instance-offset load from a Self pointer. }
AssertTrue('references $TFoo_FCount global',
Pos('$TFoo_FCount', IR) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_StaticMethod_NoSelfParam;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
static procedure Bump;
end;
static procedure TFoo.Bump;
begin
FCount := FCount + 1;
end;
begin end.
''';
var IR: string; FnPos: Integer;
begin
IR := GenIR(Src);
FnPos := Pos('function $TFoo_Bump(', IR);
if FnPos < 0 then
FnPos := Pos('$TFoo_Bump(', IR);
AssertTrue('TFoo_Bump function emitted', FnPos >= 0);
{ The signature must NOT contain %_par_Self. }
AssertFalse('static method has no Self parameter',
Pos('%_par_Self', Copy(IR, FnPos, 60)) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_Singleton_LazyGetInstance;
const
Src =
'''
program P;
type
TFoo = class
private static var
FInstanceId: Integer;
public
static function GetId: Integer;
end;
static function TFoo.GetId: Integer;
begin
if FInstanceId = 0 then
FInstanceId := 42;
Result := FInstanceId;
end;
begin end.
''';
var IR: string;
begin
IR := GenIR(Src);
AssertTrue('static var global slot present',
Pos('data $TFoo_FInstanceId', IR) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_StaticVar_QualifiedRead_LoadsGlobal;
const
Src =
'''
program P;
type
TFoo = class
public static var
Total: Integer;
static procedure Bump;
end;
static procedure TFoo.Bump;
begin
Total := Total + 1;
end;
var n: Integer;
begin
n := TFoo.Total;
end.
''';
var IR: string;
begin
IR := GenIR(Src);
{ A qualified read TFoo.Total must load the SAME mangled global slot, not
dereference an instance. }
AssertTrue('qualified read loads $TFoo_Total (IR: ' + Copy(IR, 0, 600) + ')',
Pos('loadw $TFoo_Total', IR) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_StaticProperty_QualifiedRead_CallsGetter;
const
Src =
'''
program P;
type
TFoo = class
private static var
FCount: Integer;
public
static function NextId: Integer;
static property Counter: Integer read NextId;
end;
static function TFoo.NextId: Integer;
begin
FCount := FCount + 1;
Result := FCount;
end;
var n: Integer;
begin
n := TFoo.Counter;
end.
''';
var IR: string;
begin
IR := GenIR(Src);
AssertTrue('static prop read calls $TFoo_NextId() with no Self (IR: ' +
Copy(IR, 0, 800) + ')',
Pos('call $TFoo_NextId()', IR) >= 0);
end;
procedure TStaticMembersSemTests.TestIR_ClassStaticVar_ReleasedAtExit;
const
Src =
'''
program P;
type
TFoo = class
private static var
FInst: TFoo;
public
static procedure Init;
end;
static procedure TFoo.Init;
begin
if FInst = nil then
FInst := TFoo.Create();
end;
begin
TFoo.Init();
end.
''';
var IR: string;
begin
IR := GenIR(Src);
{ A class-typed static var holds one retained reference; the program-exit
cleanup at @main_exit must release the shared global slot. }
AssertTrue('static var class slot present',
Pos('data $TFoo_FInst', IR) >= 0);
AssertTrue('static var released at @main_exit',
(Pos('@main_exit', IR) >= 0) and (Pos('loadl $TFoo_FInst', IR) >= 0));
end;
initialization
RegisterTest(TStaticMembersParseTests);
RegisterTest(TStaticMembersSemTests);
end.

View file

@ -2681,9 +2681,11 @@ begin
Iface := TUnitInterface.Create('U');
try
Buf := WriteUnitInterface(Iface);
{ Blaise Pos is 0-based; match-at-start returns 0. }
{ Blaise Pos is 0-based; match-at-start returns 0. Version is 3 since the
static-members fields (IsClassVar/ClassVarEmitName, property IsStatic,
record/class ConstDecls) were added to the encoded layout. }
AssertTrue('starts with magic',
Pos('BLAISE-IFACE 2', Buf) = 0);
Pos('BLAISE-IFACE 3', Buf) = 0);
finally
Iface.Free();
end;

View file

@ -269,13 +269,17 @@ IntSubrange
* enum-bounded subranges (TLow..THigh, red..blue) are not a named-type form. *)
RecordDef
= [ PACKED ] RECORD { FieldDecl | MethodDecl } END
= [ PACKED ] RECORD { VisibilitySection | FieldDecl | MethodDecl | ConstSection } END
;
(* Records may declare methods alongside fields. Method bodies are written
* out-of-line as TRecordName.MethodName, identical to classes. The
* receiver is passed by pointer (`Self` is a `var` parameter under the hood);
* the language gives normal value semantics — assignments copy the record.
* out-of-line as TRecordName.MethodName, identical to classes. An ordinary
* record method receives the receiver by pointer (`Self` is a `var` parameter
* under the hood); the language gives normal value semantics — assignments
* copy the record. A method declared STATIC (see VisibilitySection /
* MethodDecl) receives no `Self` and is the record's factory / namespaced
* function form, called as TRecordName.Method(...). Records may also carry
* STATIC VAR / STATIC CONST members.
* No constructors, destructors, or virtual methods are allowed in records.
*
* `packed` is permitted only in front of `record`. It disables natural
@ -289,11 +293,34 @@ RecordDef
ClassDef
= CLASS [ LPAREN GenericName { COMMA GenericName } RPAREN ]
{ ConstSection | FieldList }
MethodDeclList
{ VisibilitySection | ConstSection | FieldList | MethodDeclList }
END
;
(* A class body is a sequence of members in any order, optionally grouped under
* visibility / storage sections. A VisibilitySection keyword sets the current
* visibility (and, with STATIC, the current member-association) for every
* member that follows until the next such keyword. *)
VisibilitySection
= ( PRIVATE | PROTECTED | PUBLIC | PUBLISHED ) [ STATIC ]
| STATIC (* STATIC alone keeps current visibility *)
;
(* STATIC as a section qualifier (e.g. `private static`, or a bare `static`
* continuing the current visibility) makes every field, const, method, and
* property declared until the next section keyword a type-associated (class-
* level) member: no implicit Self, a single shared storage slot for vars/
* consts. STATIC may equally be written as a per-member prefix on a single
* method or property (see MethodDecl / PropertyDecl). `private static var`
* and `private static const` are the section forms for static fields and
* static constants. There is no `class var` / `class function` /
* `class property` — the word `class` is never a member qualifier in Blaise.
*
* Visibility keywords (PRIVATE/PROTECTED/PUBLIC/PUBLISHED) and STATIC are
* contextual: outside a class/record body they remain usable as identifiers,
* except those already reserved elsewhere. *)
GenericName
= IDENT [ LESS TypeArgList GREATER ] (* plain name or generic specialisation *)
;
@ -363,7 +390,8 @@ MethodDeclList
;
MethodDecl
= ( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
= [ STATIC ] (* per-member static prefix (no implicit Self) *)
( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
[ LESS TypeParamList GREATER ] (* optional generic type params *)
[ LPAREN ParamList RPAREN ]
[ COLON TypeName ] (* return type for functions *)
@ -372,6 +400,12 @@ MethodDecl
[ ( Block | AsmBlock ) SEMICOLON ] (* body absent for forward, external, or abstract decls *)
;
(* A STATIC method (whether via the per-member prefix here or an enclosing
* STATIC VisibilitySection) takes no implicit Self. CONSTRUCTOR / DESTRUCTOR
* may NOT be declared STATIC: Blaise has no type-initialiser construct (no
* `class constructor`); eager one-time setup belongs in the unit
* initialization / finalization sections. See language-rationale.adoc. *)
MethodDirective
= VIRTUAL
| OVERRIDE
@ -401,9 +435,13 @@ ExternalDirective
= EXTERNAL [ NAME StrLit ]
;
(* Standalone procedure/function/constructor/destructor — top-level or within a block. *)
(* Standalone procedure/function/constructor/destructor — top-level or within a block.
* A leading STATIC marks an out-of-line static-method implementation
* (`static function TName.Method ...`); it must agree with the in-class
* declaration's static-ness and takes no implicit Self. *)
StandaloneDecl
= ( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
= [ STATIC ]
( PROCEDURE | FUNCTION | CONSTRUCTOR | DESTRUCTOR ) IDENT
[ DOT IDENT ] (* TTypeName.MethodName — non-generic class impl *)
[ LESS TypeParamList GREATER DOT IDENT ] (* TName<T>.MethodName — generic class impl *)
[ LESS TypeParamList GREATER ] (* function Identity<T> — generic standalone *)
@ -437,7 +475,8 @@ MethodSignatureList
;
PropertyDecl
= "property" IDENT
= [ STATIC ] (* per-member static prefix *)
"property" IDENT
[ LBRACKET IDENT COLON TypeName RBRACKET ] (* optional index parameter *)
COLON TypeName
[ "read" IDENT ]
@ -446,6 +485,10 @@ PropertyDecl
[ "default" SEMICOLON ] (* default array property *)
;
(* A STATIC property (via the prefix here or an enclosing STATIC section) is
* sugar over a STATIC getter/setter: it occupies no slot and dispatches with
* no Self. A static property's accessors must themselves be static. *)
(* If the index parameter is present, the property is indexed:
* property Items[Index: Integer]: Pointer read Get write Put;
* Indexed reads emit a getter call with one extra argument (the index).

View file

@ -5341,3 +5341,262 @@ the reference and definition sites would disagree.
context.* Heavier plumbing for the same effect; the analyser already owns one
shared table that carries every unit's symbols, so scoping the *view* to the
emitted unit is the smaller, sufficient change.
== `static` Class Members — One Keyword, No `class`-Keyword Overload
=== Decision
Type-associated (class-level) members — fields, constants, methods, and
properties that belong to the type itself rather than to an instance — are
declared with the `static` keyword:
[source,pascal]
----
type
TRegistry = class
private static var
FInstance: TRegistry; { one shared slot, not per-instance }
private static const
MaxEntries = 256;
private
FItems: TList<TEntry>; { ordinary instance field }
static function GetInstance: TRegistry; { no Self; type-associated }
function GetItem(I: Integer): TEntry; { ordinary instance method }
public
static property Instance: TRegistry read GetInstance;
property Items[I: Integer]: TEntry read GetItem;
end;
static function TRegistry.GetInstance: TRegistry; { out-of-line impl }
begin
if FInstance = nil then
FInstance := TRegistry.Create(); { lazy singleton }
Result := FInstance;
end;
----
`static` may appear in two positions:
* as a *section qualifier* — `private static var`, `public static const`,
`protected static` — applying to every member declared until the next
visibility or section keyword; and
* as a *member prefix* — `static function`, `static procedure`,
`static property` — applying to that one member.
A `static` member receives no implicit `Self`. A `static var` / `static const`
occupies a single shared storage slot for the whole type, addressed as a
global; it does not advance the instance layout and is not copied per object.
A `static function` / `static procedure` is an ordinary routine that happens to
be name-qualified by its owning type and may read the type's other `static`
members. A `static property` is pure sugar over a `static` getter/setter.
Inside a record, a method declared `static` is the record's factory /
namespaced-function form — it takes no hidden `Self` pointer and is called as
`RecType.Method(...)`. (An ordinary record method still receives `Self` by
pointer; see <<_record_methods>>.) Records may also carry `static var` and
`static const` members.
Blaise has *no* `class` keyword in member position. The word `class` appears
only where it introduces a reference type (`T = class`), never as a member
qualifier. There is also no `static constructor` / `static destructor` — see
_No type-initialiser construct_ below.
=== Rationale
Delphi and FPC overload the single word `class` into three unrelated meanings:
the reference-type introducer (`T = class`), the member-association qualifier
(`class var`, `class function`, `class property`), and the type-initialiser
form (`class constructor`, `class destructor`). The three concepts have nothing
in common; sharing one keyword forces the reader to disambiguate by position
every time. Blaise has no existing source to keep compatible, so it splits the
concerns: `class` keeps only its structural meaning, and `static` carries the
member-association meaning it already carries in Java, C#, Swift, Rust, Go, and
Kotlin (`companion`/`static`). One keyword, one meaning.
The member-association concept is genuinely needed: the canonical singleton /
registry pattern (a private shared instance reached through a type-level
accessor) cannot be expressed with instance members alone. `static`
delivers it with a single, universally-understood keyword.
==== No type-initialiser construct (`static`/`class constructor`)
Blaise deliberately omits Delphi's `class constructor` / `class destructor`
(the run-once-at-startup type initialiser and its teardown counterpart). The
construct is unnecessary in Blaise because the two jobs it does are already
covered:
* *Zeroing a `static var`.* Blaise guarantees every variable — including
globals — holds its zero value before any code runs (see
<<_zero_initialisation_of_variables>>). A `static var FInstance: TRegistry`
is therefore `nil` at startup with no initialiser. Delphi needs the type
constructor here only because it makes no such guarantee.
* *Non-trivial one-time setup or teardown.* A lazy singleton initialises itself
on first access inside its `static` getter (the `if FInstance = nil` pattern
above); eager setup or teardown that genuinely must run once belongs in the
unit's existing `initialization` / `finalization` sections, which live in the
implementation section where code is permitted and run once in dependency
order. This is exactly Go's package-level `init()` model.
Re-using the spelling `static constructor Create` was rejected for the same
reason `class` was: it overloads `constructor` for something that is not a
callable constructor (it takes no arguments, is never invoked by name, and
cannot be overloaded). A nameless `static begin … end` initialiser block (the
Java/Kotlin shape) was also rejected: Pascal forbids code in the interface
section, where the class declaration lives, so an inline initialiser block
there would be alien to the language. The unit `initialization` section is the
idiomatic Pascal home and needs no new syntax.
=== Cross-language comparison
[cols="1,1,1,1,2", options="header"]
|===
| Language | Static field | Static method | Static property | Type initialiser
| *Blaise*
| `static var`
| `static function`
| `static property`
| none — zero-init + unit `initialization`
| Java
| `static`
| `static`
| (getter)
| `static { }` block (lazy)
| C#
| `static`
| `static`
| `static P { }`
| `static TFoo()` static constructor (lazy)
| Swift
| `static let/var`
| `static func`
| `static var { }`
| none — `static let` lazily initialised per-field
| Rust
| `static` / assoc `const`
| assoc `fn` (no `self`)
| (none)
| none — `static` is const-expr; lazy is library-level
| Go
| package `var`
| package func
| (none)
| `func init()` (package, once at startup)
| Kotlin
| `companion object`
| companion `fun`
| companion `val/var`
| companion `init { }`
| Delphi / FPC / Oxygene
| `class var`
| `class function`
| `class property`
| `class constructor` (lazy)
|===
The languages divide on the type-initialiser question. Java, C#, Kotlin, and
the Delphi family provide an explicit type-constructor block because a static
field can be left null/indeterminate and the programmer must run code to
populate it. Swift, Rust, and Go barely need one — either every static is
zero/const-initialised by the language, or lazy init is a per-field runtime
feature. Blaise sits with the second group precisely because of its zero-init
guarantee, so it follows Swift/Rust/Go and omits the type-constructor entirely
rather than following the Java/C#/Delphi machinery.
=== Visibility
`static` is orthogonal to visibility. It composes with `private`,
`protected`, and `public` (and, once visibility enforcement lands, with
`strict private`). `static` governs *storage and dispatch* — where the slot
lives and whether `Self` is passed — not *who may see the member*. The two
are independent axes and are implemented as independent features (see
<<_member_visibility_enforcement_private_protected_strict>>).
=== Alternatives rejected
* *Keep Delphi's `class var` / `class function` / `class property`.* Rejected.
It preserves the `class`-keyword overload that is the whole confusion this
decision removes, for no compatibility benefit Blaise can collect.
* *Infer static-ness of a record method from whether its body touches `Self`.*
Rejected. The hidden-`Self` ABI would then flip silently when an edit adds or
removes a `Self` reference. `static` makes the calling convention explicit at
the declaration. (See <<_record_methods>>.)
* *Adopt a `static constructor` or `static begin … end` type initialiser.*
Rejected — see _No type-initialiser construct_ above.
== Member Visibility Enforcement — `private` / `protected` / `strict`
=== Status
_Decided; implemented as the second half of the static-members work. This entry
records the decision; the spelling and enforcement rules below are the
committed design._
=== Decision
Visibility modifiers on class and record members are *enforced*, not merely
parsed. A member declared `private` is visible only within the unit that
declares the owning type; a `protected` member is additionally visible within
descendant types; a `public` (or `published`) member is visible everywhere the
type is. Blaise additionally supports `strict private` and `strict protected`,
which narrow visibility to the declaring *type* itself (and, for
`strict protected`, its descendants) rather than the whole declaring unit.
`strict` composes with `static`: `strict private static var FInstance: TFoo;`
is permitted, exactly as in Delphi. The `strict`-ness (who may see the member)
is independent of the `static`-ness (where the slot lives and whether `Self`
is passed).
A non-`strict` `private` member remains visible to the unit's
`initialization` / `finalization` sections and to other code in the same unit,
because those sections are part of the declaring unit. A `strict private`
member is *not* visible to them — it is reachable only from the methods of the
declaring type. This is the one interaction point between visibility and the
static-members feature: a `static var` that the unit `initialization` section
must populate eagerly must therefore not be `strict private`.
=== Rationale
Before this change, Blaise parsed `private` / `protected` / `published` but
enforced none of them, and had no `strict` keyword at all. `private` was
effectively a comment. Enforcing visibility closes a real gap — a unit's
internal class members were silently part of its usable surface — and makes the
`static` singleton pattern honest: a `private static var FInstance` can now be
genuinely unreachable from outside while the `public static property Instance`
is the sole entry point.
`strict` is included because the unit-scoped `private` of classic Pascal is
looser than most programmers coming from Java, C#, Swift, or Rust expect; those
languages scope `private` to the *type*. Offering both the Pascal unit-scoped
`private` and the type-scoped `strict private` lets the author choose, matching
Delphi and FPC.
=== Implementation order
Visibility enforcement lands *after* the `static`-members feature, as a
separate commit. Static members are on the critical path (they unblock real
code and exercise the codegen/OPDF stack); visibility enforcement is a pure
rejection pass with no codegen impact, and is cleaner to write once against the
complete, final set of member kinds (instance and static fields, methods, and
properties) than to write first and revisit when `static` arrives.
=== Alternatives rejected
* *Leave visibility unenforced (status quo).* Rejected — `private` that
protects nothing is a latent footgun and undermines the `static` singleton
pattern's encapsulation.
* *Enforce only the unit-scoped `private` (no `strict`).* Rejected — the
type-scoped `strict private` is the form most non-Pascal programmers expect,
and omitting it would force every genuinely type-private member to leak to
the whole unit.
* *Bundle visibility enforcement into the static-members commit.* Rejected —
the two are independent axes; sequencing them keeps each commit focused and
lets the codegen-touching feature accumulate field exposure first.

View file

@ -68,34 +68,37 @@ TFieldAccessExpr.IsVarParam safe
TFieldAccessExpr.PropIndexExpr serialise
TFieldAccessExpr.IsCharAccess safe
TFieldAccessExpr.IsArrayAccess safe
TFieldAccessExpr.IsClassVarRead safe
TFieldAccessExpr.ClassVarEmitName safe
TFieldAccessExpr.IsStaticPropGet safe
# TIsExpr (uAST.pas:151)
# TIsExpr (uAST.pas:154)
TIsExpr.Obj serialise
TIsExpr.TypeName serialise
# TAsExpr (uAST.pas:159)
# TAsExpr (uAST.pas:162)
TAsExpr.Obj serialise
TAsExpr.TypeName serialise
# TSupportsExpr (uAST.pas:168)
# TSupportsExpr (uAST.pas:171)
TSupportsExpr.Obj serialise
TSupportsExpr.IntfTypeName serialise
TSupportsExpr.OutVarName serialise
TSupportsExpr.OutVarIsGlobal safe
# TInheritedCallExpr (uAST.pas:182)
# TInheritedCallExpr (uAST.pas:185)
TInheritedCallExpr.Name serialise
TInheritedCallExpr.Args serialise
# TBinaryExpr (uAST.pas:199)
# TBinaryExpr (uAST.pas:202)
TBinaryExpr.Op serialise
TBinaryExpr.Left serialise
TBinaryExpr.Right serialise
# TNotExpr (uAST.pas:207)
# TNotExpr (uAST.pas:210)
TNotExpr.Expr serialise
# TAssignment (uAST.pas:219)
# TAssignment (uAST.pas:222)
TAssignment.Name serialise
TAssignment.Expr serialise
TAssignment.IsVarParam safe
@ -104,26 +107,26 @@ TAssignment.IsThreadVar safe
TAssignment.IsWeakLhs safe
TAssignment.ImplicitSelfField safe
# TIfStmt (uAST.pas:235)
# TIfStmt (uAST.pas:238)
TIfStmt.Condition serialise
TIfStmt.ThenStmt serialise
TIfStmt.ElseStmt serialise
# TCompoundStmt (uAST.pas:243)
# TCompoundStmt (uAST.pas:246)
TCompoundStmt.Stmts serialise
# TAsmStmt (uAST.pas:255)
# TAsmStmt (uAST.pas:256)
TAsmStmt.Code serialise
# TWhileStmt (uAST.pas:250)
# TWhileStmt (uAST.pas:261)
TWhileStmt.Condition serialise
TWhileStmt.Body serialise
# TRepeatStmt (uAST.pas:257)
# TRepeatStmt (uAST.pas:268)
TRepeatStmt.Body serialise
TRepeatStmt.Condition serialise
# TForStmt (uAST.pas:264)
# TForStmt (uAST.pas:275)
TForStmt.VarName serialise
TForStmt.IsGlobal safe
TForStmt.StartExpr serialise
@ -131,7 +134,7 @@ TForStmt.EndExpr serialise
TForStmt.IsDownTo serialise
TForStmt.Body serialise
# TForInStmt (uAST.pas:275)
# TForInStmt (uAST.pas:286)
TForInStmt.VarName serialise
TForInStmt.VarIsGlobal safe
TForInStmt.CollExpr serialise
@ -151,34 +154,34 @@ TForInStmt.SetBitCount safe
TForInStmt.SetMaskVarName safe
TForInStmt.SetIsJumbo safe
# TTryFinallyStmt (uAST.pas:312)
# TTryFinallyStmt (uAST.pas:323)
TTryFinallyStmt.TryBody serialise
TTryFinallyStmt.FinallyBody serialise
# TTryExceptStmt (uAST.pas:329)
# TTryExceptStmt (uAST.pas:340)
TTryExceptStmt.TryBody serialise
TTryExceptStmt.Handlers serialise
TTryExceptStmt.ElseBody serialise
TTryExceptStmt.ExceptBody serialise
# TRaiseStmt (uAST.pas:342)
# TRaiseStmt (uAST.pas:353)
TRaiseStmt.Expr serialise
# TExitStmt (uAST.pas:353)
# TExitStmt (uAST.pas:364)
TExitStmt.Value serialise
TExitStmt.ResultAssign safe
# TBreakStmt (uAST.pas:361)
# TBreakStmt (uAST.pas:372)
# TContinueStmt (uAST.pas:364)
# TContinueStmt (uAST.pas:375)
# TCaseStmt (uAST.pas:375)
# TCaseStmt (uAST.pas:386)
TCaseStmt.Selector serialise
TCaseStmt.Branches serialise
TCaseStmt.ElseStmt serialise
TCaseStmt.IsStringCase safe
# TFieldAssignment (uAST.pas:386)
# TFieldAssignment (uAST.pas:397)
TFieldAssignment.RecordName serialise
TFieldAssignment.FieldName serialise
TFieldAssignment.Expr serialise
@ -193,7 +196,7 @@ TFieldAssignment.PropOwnerType safe
TFieldAssignment.PropAccessorVSlot safe
TFieldAssignment.IsElemWrite safe
# TStaticSubscriptAssign (uAST.pas:414)
# TStaticSubscriptAssign (uAST.pas:425)
TStaticSubscriptAssign.ArrayName serialise
TStaticSubscriptAssign.IndexExpr serialise
TStaticSubscriptAssign.ValueExpr serialise
@ -204,11 +207,11 @@ TStaticSubscriptAssign.IsImplicitSelf safe
TStaticSubscriptAssign.PropOwnerType safe
TStaticSubscriptAssign.PropAccessorVSlot safe
# TPointerWriteStmt (uAST.pas:444)
# TPointerWriteStmt (uAST.pas:455)
TPointerWriteStmt.PtrExpr serialise
TPointerWriteStmt.ValExpr serialise
# TProcCall (uAST.pas:452)
# TProcCall (uAST.pas:463)
TProcCall.Name serialise
TProcCall.Args serialise
TProcCall.IsImplicitSelfMethod safe
@ -216,7 +219,7 @@ TProcCall.IsIndirectCall safe
TProcCall.IndirectCallIsGlobal safe
TProcCall.IsProcFieldCall safe
# TFuncCallExpr (uAST.pas:465)
# TFuncCallExpr (uAST.pas:480)
TFuncCallExpr.Name serialise
TFuncCallExpr.Args serialise
TFuncCallExpr.IsImplicitSelfMethod safe
@ -227,18 +230,18 @@ TFuncCallExpr.HasClassAttrClass safe
TFuncCallExpr.HasClassAttrAttr safe
TFuncCallExpr.IsProcFieldCall safe
# TIndirectFuncCallExpr (uAST.pas:491)
# TIndirectFuncCallExpr (uAST.pas:510)
TIndirectFuncCallExpr.CalleeExpr serialise
TIndirectFuncCallExpr.Args serialise
# TDerefExpr (uAST.pas:501)
# TDerefExpr (uAST.pas:520)
TDerefExpr.Expr serialise
# TAddrOfExpr (uAST.pas:508)
# TAddrOfExpr (uAST.pas:527)
TAddrOfExpr.Expr serialise
TAddrOfExpr.ResolvedFreeRoutine safe
# TMethodCallStmt (uAST.pas:520)
# TMethodCallStmt (uAST.pas:539)
TMethodCallStmt.ObjectName serialise
TMethodCallStmt.Name serialise
TMethodCallStmt.Args serialise
@ -249,19 +252,21 @@ TMethodCallStmt.IsVarParam safe
TMethodCallStmt.IsBuiltinToString safe
TMethodCallStmt.IsConstructorCall safe
TMethodCallStmt.IsMetaclassDispatch safe
TMethodCallStmt.IsStaticCall safe
TMethodCallStmt.IsProcFieldCall safe
# TInheritedCallStmt (uAST.pas:554)
# TInheritedCallStmt (uAST.pas:576)
TInheritedCallStmt.Name serialise
TInheritedCallStmt.Args serialise
# TMethodCallExpr (uAST.pas:839)
# TMethodCallExpr (uAST.pas:881)
TMethodCallExpr.ObjectName serialise
TMethodCallExpr.Name serialise
TMethodCallExpr.Args serialise
TMethodCallExpr.ObjExpr serialise
TMethodCallExpr.IsConstructorCall safe
TMethodCallExpr.IsMetaclassDispatch safe
TMethodCallExpr.IsStaticCall safe
TMethodCallExpr.IsGlobal safe
TMethodCallExpr.IsVarParam safe
TMethodCallExpr.IsBuiltinToString safe
@ -305,7 +310,7 @@ TUnitInterface.UsedUnits serialise
TUnitInterface.ImplUsedUnits serialise
TUnitInterface.HasInitialization serialise
# TMethodParam (uAST.pas:764)
# TMethodParam (uAST.pas:798)
TMethodParam.ParamName serialise
TMethodParam.TypeName serialise
TMethodParam.IsVarParam serialise

View file

@ -357,7 +357,7 @@ begin
Tok := ReadIdent(Stripped, P);
if Tok = '' then
begin
Result.Clear;
Result.Clear();
Exit;
end;
Result.Add(Tok);
@ -372,7 +372,7 @@ begin
(* Must be followed by `:` then a type, then a `;` somewhere. *)
if (P >= Len) or (StrAt(Stripped, P) <> Ord(':')) then
begin
Result.Clear;
Result.Clear();
Exit;
end;
end;
@ -515,7 +515,7 @@ begin
Allow.Add(GIfaceTypes[I]);
ScanClassFile(GIfaceFile, GIfaceNames, GIfaceObjs, Allow);
Allow.Clear;
Allow.Clear();
Allow.Add('TMethodParam');
ScanClassFile(GAstFile, GIfaceNames, GIfaceObjs, Allow);
end;