delphi
Skill1 starsChanged 13 days agoMITView on GitHub
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".
4 other projects use a version of this, 2 of them edited.
Adopted by
Most recently taken up by
- danieleteti/delphi-ai-skillsedited, first seen Nov 7, 2026
- danieleteti/delphimvcframeworkedited, first seen Nov 2, 2026
- G1Joshi/Agent-Skillstheir own version, first seen Oct 13, 2026
- gensyn-ai/gensyn-delphi-skillstheir own version, first seen Apr 21, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 2
- danieleteti/delphimvcframework★ 1.4k96% same
- danieleteti/delphi-ai-skills★ 2384% same
Wrote their own version · 2
- gensyn-ai/gensyn-delphi-skills★ 16same name
- G1Joshi/Agent-Skills★ 15same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository30 other files it gives its agents.
- angular-crud-gotchasSkill
- aws-lightsail-container-deploySkill
- bannerlord-decompileSkill
- codegen-t4-templatesSkill
- csharpSkill
- delphi-code-smellsSkill
- delphi-firedacSkill
- delphi-types-and-unitsSkill
- difftasticSkill
- dockerSkill
- efcore-aspnet-reviewSkill
- git-bash-editingSkill
- github-issue-creatorSkill
- microsoft-docsSkill
- minimal-comments-and-docsSkill
- msbuildSkill
- mssqlSkill
- mstest-assertionsSkill
- mysqlSkill
- nginx-config-testingSkill
- odata-generated-gridsSkill
- postgresSkill
- powershellSkill
- react-crud-gotchasSkill
- sanitize-for-open-sourceSkill
- self-contained-deploymentSkill
- skill-writerSkill
- ui-conventionsSkill
- verify-in-running-appSkill
- winui3Skill
The file
We split this skill into the rules it gives an agent
6 rules. 4 are common, 0 are distinctive, and 2 are very specific to this project.
4
Common
Most of the 3 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 39
Ask the user for the path if no Studio\<n>.0\source folder is there — do not guess the version.
- Common: line 59
Never present a shape you have not seen used somewhere.
- Common: line 64
When it is not obvious which tree to read, ask rather than assume:
- Common: line 100
Do not assume a version, and do not scatter guards for versions the user does not have.
- Common: line 113
Ask the user: when more than one is installed, when none is found, or when the project does not say:
- Very specific: line 116
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 …
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Delphi / Object Pascal — language and RTL guide
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\sourcefolder 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
FlorenceforAthens/Alexandriato check whether something existed on an older target). - If you cannot verify it, say so. "I am not sure
TFoo.Barexists — checkSystem.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
grepfor 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\sourceandStudio\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)
<!-- /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
CompilerVersionfrom 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 (folder23.0, compiler36.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.
With no stated target, write code that compiles on 11 Alexandria (step 5 above).
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 implDelphi 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). NativeIntbecame a weak alias — it stopped being a distinct type for overload resolution.System.Generics.Collections,System.Generics.DefaultsandSystem.Classes.TListswitched every index andCountfromIntegertoNativeInt.var lCount: Integer := MyList.Count;still compiles. ANativeIntvariable passed to avar Integerparameter compiles on Win32 and fails on Win64, where the two types differ in size — declare itNativeInt.
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>, TObjectHashSet<T> and TOrderedDictionary<K,V> are not in Delphi 11 (no docwiki page
under Libraries/Alexandria). They exist in 12 Athens and 13 Florence (System.Generics.Collections).
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 itFreeAndNil 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. |
lObj := Load; lObj.Save; Result := lObj; | try lObj.Save; except lObj.Free; raise; end; Result := lObj; | Until the handoff the object is yours: if Save raises it leaks. finally would be a double free — the caller owns it after. reference/memory.md §1 |
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 (the lowercase style used by 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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

