| name | delphi |
| description | Use when writing, reviewing or fixing Delphi / Object Pascal code — any .pas or .dpr unit, the RTL or the VCL — independently of any framework. Covers language level and version gating, inline var, generics, anonymous methods, memory and lifetime (Free, try/finally, interfaces, managed records), strings and encodings, exceptions, System.Generics.Collections, RTTI and attributes, threading, and the naming/formatting conventions. Triggers on "Delphi", "Object Pascal", "pas unit", "TStringList", "TObjectList", "memory leak", "double free", "interface reference", "anonymous method", "TThread", "TStringBuilder", "access violation", "compiler version". |
Delphi / Object Pascal — language and RTL guide
The foundation the DMVCFramework skills sit on. Nothing here assumes a framework or a project layout —
it applies to a console app, a VCL form, a service, a library.
Everything below was copied from the Delphi 13 Florence RTL/VCL source on disk, or confirmed on the
official docwiki. Version boundaries were checked release by release, not recalled.
When in doubt about an API — verify it, never guess
A plausible-but-wrong RTL identifier costs the user a compile error and their trust. Do not answer from
memory. Verify, in this order:
-
Read the shipped source — the tree of the release you are targeting. It is on every developer
machine, one tree per installed release:
C:\Program Files (x86)\Embarcadero\Studio\<n>.0\source\rtl\sys\ System.pas, System.SysUtils.pas, System.Types.pas
C:\Program Files (x86)\Embarcadero\Studio\<n>.0\source\rtl\common\ System.Classes.pas, System.Generics.*, System.IOUtils.pas,
System.Rtti.pas, System.Threading.pas, System.JSON.pas, …
C:\Program Files (x86)\Embarcadero\Studio\<n>.0\source\vcl\ Vcl.Forms.pas, Vcl.Controls.pas, …
<n> is the product version: 37 = 13 Florence, 23 = 12 Athens, 22 = 11 Alexandria. The
layout is identical in every tree, so targeting 12 Athens means reading
…\Studio\23.0\source\rtl\common\System.Classes.pas, not the 37.0 copy of the same file.
Which tree matters. The RTL grows release by release: a type, an overload or a parameter that is in
the 13 Florence source may simply not exist in 12 Athens or 11 Alexandria. Once the target is known
(see Establish the target, below), verify against that tree. When the target's tree is not
installed — you are on 13 Florence but writing for a customer's 12 Athens — check the symbol against
the matching docwiki (step 2), which is versioned, and say which release you confirmed it on.
Ask the user for the path if no Studio\<n>.0\source folder is there — do not guess the version.
-
The docwiki, always current: https://docwiki.embarcadero.com/RADStudio/Florence/en/Delphi_Language_Reference ·
library reference at https://docwiki.embarcadero.com/Libraries/Florence/en/ (swap Florence for
Athens / Alexandria to check whether something existed on an older target).
-
If you cannot verify it, say so. "I am not sure TFoo.Bar exists — check System.Classes.pas" is a
useful answer. A confidently wrong one is not.
A declaration is half the answer — read a real call site too
The declaration gives you the arity and the types. A call site gives you the ownership, the lifetime,
the required order of calls and the idiom, which is where plausible-but-wrong code actually dies. Before
writing or judging a call, read both: the unit that declares the API, and one real use of it. In order of
value:
- The user's own code in this project. It carries the house conventions you must match, and it is the
codebase the answer has to live in.
- The shipped RTL/VCL source itself — the RTL uses its own APIs constantly, so a
grep for the
identifier across …\source\ returns working call sites, not documentation prose.
- The docwiki code examples for that page.
Never present a shape you have not seen used somewhere. If you have only the declaration, say that the
usage is unverified.
Ask for the path, then record it so nobody asks twice
When it is not obvious which tree to read, ask rather than assume:
Which Delphi source tree should I verify against? I can see Studio\23.0\source and
Studio\37.0\source, and I do not want to confirm a signature on a release you do not build with.
Once the user answers and you have confirmed the folder exists, offer to write it down, so the next
session starts knowing it. Ask first, then add a block of its own to the instruction file this agent
already reads (CLAUDE.md in the project root, or AGENTS.md / GEMINI.md):
<!-- delphi-local-sources -->
Delphi RTL/VCL source: C:\Program Files (x86)\Embarcadero\Studio\23.0\source (12 Athens, CompilerVersion 36.0)
DelphiMVCFramework checkout: C:\DEV\dmvcframework (sources/ + samples/)
<!-- /delphi-local-sources -->
Read that block before asking anything: it is the first place to look when a session starts. Keep it
current: if a path in it no longer exists, or the user moves to another Delphi release, say so and ask
again instead of quietly falling back to memory.
Language level — get this right before writing a line
Feature availability is the single most common source of "it looks fine and does not compile".
CompilerVersion and the product version, from the docwiki Compiler Versions table:
| Release | CompilerVersion | Conditional | Studio folder |
|---|
| 10.3 Rio | 33.0 | VER330 | 20.0 |
| 10.4 Sydney | 34.0 | VER340 | 21.0 |
| 11 Alexandria | 35.0 | VER350 | 22.0 |
| 12 Athens | 36.0 | VER360 | 23.0 |
| 13 Florence | 37.0 | VER370 | 37.0 |
Establish the target before writing anything version-dependent
Do not assume a version, and do not scatter guards for versions the user does not have. Work down this
list and stop at the first step that answers:
-
The user stated it in this session. That wins over anything on disk. Remember it for the rest of
the conversation.
-
Ask the compiler. What it prints is the CompilerVersion from the table above:
> "C:\Program Files (x86)\Embarcadero\Studio\37.0\bin\dcc32.exe" --version
dcc (Embarcadero Delphi for Windows) 37.0
-
List the installs and read the folder numbers: dir "C:\Program Files (x86)\Embarcadero\Studio".
22.0 = 11 Alexandria, 23.0 = 12 Athens, 37.0 = 13 Florence. Careful: the folder number is not
the compiler version for 12 Athens (folder 23.0, compiler 36.0).
-
Ask the user when more than one is installed, when none is found, or when the project does not say:
Which Delphi do you build this with: 11 Alexandria, 12 Athens or 13 Florence?
Do not infer it from the .dproj: <ProjectVersion> is the project-file format version, not the
product, and a project last saved by an older IDE opens unchanged in a newer one.
-
No answer available: write for 11 Alexandria, guard anything newer with
{$IF CompilerVersion >= …}, and say in your reply that you did so and why.
Then use what you found. On a confirmed 13 Florence target the if-expression and NameOf are simply
available, and wrapping them in a {$IF} is noise; on 11 they must not appear at all.
The DMVCFramework skills in this repository target Delphi 11 Alexandria as the minimum. Unless the user
states a higher target, write code that compiles on 11.
Safe on Delphi 11 (introduced in 10.3 Rio or 10.4 Sydney)
var lTotal := 0; // inline var + type inference (10.3)
var lName: string := 'ok'; // inline var, explicit type (10.3)
const cHalf = (L + H) div 2; // inline const (10.3)
for var i := 0 to List.Count - 1 do ... // for-var (10.3)
for var lItem in Collection do ... // for-in-var (10.3)
class operator Initialize(out Dest: TRec); // custom managed records (10.4)
Added by 11 Alexandria, so also safe there:
const cFlags = %1001001; // binary literal
const cMillion = 1_000_000; // digit separator
TMySingleton = class(TNoRefCountObject) // non-refcounted IInterface impl
Delphi 12 Athens only — do NOT use on an 11 target
var lSql := ''' // multiline string literal
select *
from customers
''';
- String literals may exceed 255 characters (they were
ShortString-limited before).
NativeInt became a weak alias — it stopped being a distinct type for overload resolution.
System.Generics.Collections, System.Generics.Defaults and System.Classes.TList switched every index
and Count from Integer to NativeInt. Code that declares var lCount: Integer := MyList.Count;
still compiles; code that passes Count to a var Integer parameter does not.
Delphi 13 Florence only — do NOT use on an 11 or 12 target
X := if Left < 100 then 22 else 45; // if-then-else *expression* (ternary)
ShowMessage(NameOf(Sender)); // NameOf intrinsic
if Obj is not TFoo then ... // "is not" / "not in" operators
procedure Fail(const AMsg: string); noreturn;
class operator Initialize; // implicit Self, no out parameter
TFoo<T: interface> / TFoo<T: unmanaged> // new generic constraints
{$PUSHOPT} {$POPOPT}
THashSet<T> and TOrderedDictionary<K,V> are not in Delphi 11 (no docwiki page under
Libraries/Alexandria). They exist in 13. Verify before using them on a 12 target.
Gate anything version-dependent explicitly, never by hoping:
{$IF CompilerVersion >= 36} // Delphi 12 Athens and later
...
{$ENDIF}
Reference files — read the one you need
This SKILL.md is the working core. Depth lives in reference/; open a file only when the task needs it.
| File | Read it when the task involves |
|---|
reference/memory.md | Object lifetime: try/finally, FreeAndNil, interfaces and reference counting, the mixed object/interface bug, TObjectList<T> and TComponent ownership, custom managed records, smart pointers |
reference/strings.md | string/UTF8String/RawByteString/TBytes, 1-based vs the 0-based TStringHelper, TEncoding, Format, TStringBuilder, SameText vs AnsiSameText, TStringList |
reference/exceptions.md | The Exception hierarchy, raise ... at, re-raise, InnerException / RaiseOuterException, try/except/finally nesting, what not to catch |
reference/generics-and-rtti.md | System.Generics.Collections / .Defaults, comparers, ownership flags, iteration invalidation, generic constraints, System.Rtti, writing and reading attributes |
reference/concurrency.md | TThread, Synchronize/Queue/ForceQueue, TTask/ITask, TParallel.&For, TMonitor, TInterlocked, and the VCL single-thread rule |
reference/style.md | Naming and formatting: the docwiki style guide, one widely-used community guide, and this repository's own house style — plus where they disagree |
Memory and lifetime — the non-negotiables
Delphi objects are not garbage collected and not reference counted. Interfaces are. Records are
managed by the compiler. Mixing the three is where the bugs live. Full treatment in reference/memory.md;
these four rules cover most code:
// 1. Create OUTSIDE the try. Inside is the classic bug: if Create raises,
// the finally frees an uninitialised reference.
lList := TStringList.Create;
try
...
finally
lList.Free; // .Free is nil-safe; "if x <> nil then x.Free" is noise
end;
// 2. An object reached only through an interface is freed by the reference count.
// Never Free it, and never hold a second *object* reference to it.
var lLogger: ILogger := TLogger.Create; // released at end of scope
// 3. Ownership is a property of the container, not a habit.
var lPeople := TObjectList<TPerson>.Create; // OwnsObjects defaults to True
// Remove/Delete/Clear free the item; Extract/ExtractAt hand it back to you unfreed.
// 4. A TComponent with an owner is freed by its owner. Do not free it yourself.
lTimer := TTimer.Create(Self); // Self frees it
FreeAndNil is not a synonym for Free. Use it for a field that outlives the statement and will be
tested for nil later. On a local that is about to go out of scope it buys nothing and hides the fact that
the variable is dead — see reference/memory.md.
Common mistakes — what an LLM plausibly writes in Delphi, and why it is wrong
| Wrong | Right | Why |
|---|
s[0] for the first character | s[1], or s.Chars[0] | string is 1-based (desktop). TStringHelper — Chars, IndexOf, Substring — is compiled with {$ZEROBASEDSTRINGS ON} and is 0-based. Two indexing bases in one type. |
for i := 0 to List.Count do | ... to List.Count - 1 do | Off-by-one; Count is not the last index. |
Using the loop variable after a classic for | declare for var i := ... | The value of a classic for counter after the loop is undefined by the language. With for var the identifier does not even exist outside. |
if s == 'x' / != / && / || | =, <>, and, or | C operators do not exist. = is comparison; := is assignment. |
return X; | Result := X; (Exit(X) to leave immediately) | Result is an implicit variable, not a statement. Reading Result before assigning it reads garbage. |
Exit; in a function and no Result set | assign Result on every path | W1035; the caller gets an indeterminate value. |
string.Format(...), s.ToUpper() | Format('%s', [x]) (System.SysUtils), s.ToUpper | Format is a plain function, not a class method. Delphi omits () on no-arg calls. |
s.IsNullOrEmpty on an instance | string.IsNullOrEmpty(s) | It is a class function on TStringHelper. |
List.Add(x) on TArray<T> | TList<T>, or SetLength + index | Dynamic arrays have no methods; TArray is a class of static helpers in System.Generics.Collections. |
try ... except ... finally ... end | nest them: try try ... except ... end; finally ... end; | A single block cannot have both. This is a syntax error, not a style choice. |
catch (E: Exception) | on E: Exception do | And except without on catches everything. |
Swallowing with except end | re-raise, or handle one specific class | A bare except end deletes the diagnosis. |
with lObj do ... | a local variable | with silently shadows identifiers; a later field added to lObj can hijack a name in the enclosing scope. Never generate with. |
| A method that hides an ancestor's | override (virtual/dynamic), reintroduce (deliberate hiding), overload (same name, other signature) | Without override you get a new method: the ancestor still calls its own. Silent, and it compiles. |
procedure Foo(AText: string) | procedure Foo(const AText: string) | const on managed types (string, interface, dynamic array, record) skips the refcount/copy. Recommended by the docwiki style guide. |
var where you meant out | out when the value is produced, var when it is read and written | out does not require the caller to initialise; both pass by reference and both forbid passing a property. |
TCriticalSection from System.Classes | System.SyncObjs | Wrong unit; also TMonitor (in System.pas) locks any TObject with no extra field. |
TStringList.Create and assuming it frees Objects[] | TStringList.Create(True) | The two containers default the opposite way: TObjectList<T>.Create owns, TStringList.Create does not. TStringList.Create(OwnsObjects: Boolean) is the overload that owns. |
Sleep on the main thread | TTask.Run, TThread.CreateAnonymousThread | Freezes the UI. |
| Touching a control from a worker thread | TThread.Synchronize / TThread.Queue | The VCL is single-threaded. TControl.RaiseOnNonMainThreadUsage := True makes the violation loud. |
System.SysUtils spelled SysUtils | the full dotted name | Both resolve, but the docwiki requires the complete unit scope name; mixed spellings break uses-cleanup tooling. |
Key units — what you go there for
| Unit | Go there for |
|---|
System (implicit) | TObject, IInterface, TInterfacedObject, TNoRefCountObject, TCustomAttribute, TMonitor, ExceptObject/ExceptAddr, string/UTF8String/RawByteString |
System.SysUtils | Exception and its hierarchy, FreeAndNil, Supports, Format, TStringHelper, TStringBuilder, TEncoding, TFormatSettings, TProc/TFunc/TPredicate, SameText/CompareText, TryStrToInt |
System.Classes | TComponent, TPersistent, TStream family, TStringList/TStrings, TStreamReader/TStreamWriter, TThread, TCollection |
System.Generics.Collections | TList<T>, TObjectList<T>, TDictionary<K,V>, TObjectDictionary, TQueue, TStack, TEnumerable<T>, TArray helpers |
System.Generics.Defaults | IComparer<T>, IEqualityComparer<T>, TComparer<T>.Default / .Construct, TDelegatedComparer<T> |
System.SyncObjs | TCriticalSection, TEvent, TMutex, TSemaphore, TInterlocked, TSpinLock, TCountdownEvent |
System.Threading | TTask, ITask, IFuture<T>, TParallel, TThreadPool |
System.Rtti | TRttiContext, TRttiType, TRttiProperty, TRttiMethod, TValue, TVirtualMethodInterceptor |
System.TypInfo | TTypeKind, GetEnumName/GetEnumValue, lightweight property access without System.Rtti |
System.IOUtils | TPath, TFile, TDirectory |
System.StrUtils | SplitString, ContainsText, StartsText/EndsText, MatchStr/MatchText, IndexStr, IfThen, ReverseString |
System.Math | Min/Max, IfThen, InRange, EnsureRange, SameValue, IsZero |
System.DateUtils | IncDay/IncYear, DaysBetween, StartOfTheDay, ISO8601ToDate, DateToISO8601 (note: IncMonth lives in System.SysUtils) |
System.JSON | TJSONValue.ParseJSONValue, TJSONObject, TJSONArray (DOM); System.JSON.Serializers for object mapping |
System.NetEncoding | TNetEncoding.Base64, .URL, .HTML |
System.Hash | THashSHA2, THashMD5, THashBobJenkins |
Vcl.Forms | Application, Screen, TForm, TCustomForm |
Vcl.Controls | TControl, TWinControl, TControl.RaiseOnNonMainThreadUsage |
Unit skeleton
unit Acme.Orders.Service;
interface
uses
System.SysUtils,
System.Generics.Collections,
Acme.Orders.Entities;
type
EOrderNotFound = class(Exception);
IOrderService = interface
['{B0F0B7E4-9E4D-4C2E-9E97-3B5D1A2C4F10}'] // Ctrl+Shift+G in the IDE
function GetByID(const AID: Integer): TOrder;
end;
TOrderService = class(TInterfacedObject, IOrderService)
private
fOrders: TObjectList<TOrder>;
public
constructor Create;
destructor Destroy; override;
function GetByID(const AID: Integer): TOrder;
end;
implementation
constructor TOrderService.Create;
begin
inherited Create; // always chain
fOrders := TObjectList<TOrder>.Create;
end;
destructor TOrderService.Destroy;
begin
fOrders.Free; // free your own before inherited
inherited Destroy;
end;
function TOrderService.GetByID(const AID: Integer): TOrder;
begin
for var lOrder in fOrders do
if lOrder.ID = AID then
Exit(lOrder);
raise EOrderNotFound.CreateFmt('Order %d not found', [AID]);
end;
end.
Notes that are not optional: the interface GUID (without it Supports and as do not work), inherited
in both constructor and destructor, and a destructor that survives a half-built object — Destroy runs
even when the constructor raised, so every field it touches must be nil-safe (they are: Delphi zeroes
instance memory before the constructor runs).
House style, in one paragraph
Two-space indent, no tabs. begin/end on their own lines. Types T…, interfaces I…, exceptions
E…. Parameters A-prefixed, fields F- (docwiki) or f-prefixed (DMVCFramework's own source and the
skills in this repository), locals l-prefixed. Full dotted unit names in uses. const on managed-type
parameters. Details, sources and the places where the published guides disagree: reference/style.md.
When editing an existing file, its conventions win over any guide.