Tooling
Auto-formatting, editor configuration, and platform-specific pitfalls for FreePascal development.
Executive Summary#
- Auto-formatter —
./format.pasauto-fixes uses clauses, PascalCase naming, parameter prefixes, and stray spaces; runs via Lefthook pre-commit hook - Editor config —
.editorconfig+ VSCode/Cursor extensions for zero-config formatting on save - Platform pitfalls — FPC 3.2.2
Int64→Doubleconversion bugs (all platforms + AArch64-specific); endian-dependent byte indexing
Auto-Formatting#
The project includes ./format.pas, an instantfpc script that formats Pascal source files. It runs automatically as a pre-commit hook via Lefthook and can also be invoked manually — no build step needed.
Setup#
Install Lefthook as described in Workflow — Local setup, then register hooks with lefthook install.
Manual Usage#
# Format all project Pascal files
./format.pas
# Format specific files
./format.pas source/units/Goccia.Engine.pas
# Check only (exit 1 if changes needed)
./format.pas --checkWhat It Enforces#
All of the following are auto-fixed (not just warned about):
- Uses clauses: one unit per line, grouped (System > Third-party > Project > Relative), alphabetically sorted within each group, blank line between groups. Units with an
inpath are always in the Relative group;Goccia.*units are Project; known FPC standard library units are System; everything else is Third-party. - Function naming: capitalizes the first letter of function, procedure, constructor, and destructor names to enforce PascalCase. Renames all references within the same file. External C bindings are excluded.
- Parameter naming: adds the
Aprefix to multi-letter parameters (e.g.,Value->AValue) and renames all references within the function scope (declaration, local variables, and body). Single-letter parameters and Pascal keyword conflicts are skipped. - Stray spaces: removes spurious spaces before
;,), and,(e.g.,string ;->string;). String literals and comments are left untouched.
Editor Configuration#
.editorconfig#
The project uses .editorconfig for consistent formatting:
- Indent: 2 spaces (no tabs)
- Line endings: LF
- Trailing whitespace: trimmed
- Final newline: inserted
- Charset: UTF-8
VSCode / Cursor Setup#
The repository includes .vscode/settings.json and .vscode/extensions.json for a zero-config experience in VSCode and Cursor.
Recommended Extensions#
Open the project and accept the "Install Recommended Extensions" prompt, or install them manually:
| Extension | ID | Purpose |
|---|---|---|
| Pascal | alefragnani.pascal | Syntax highlighting, code navigation, and symbol search for Pascal/Delphi |
| Run on Save | emeraldwalk.RunOnSave | Triggers ./format.pas automatically when a .pas or .dpr file is saved |
| EditorConfig | editorconfig.editorconfig | Applies .editorconfig rules (indent size, line endings, etc.) |
These are declared in .vscode/extensions.json so VSCode/Cursor will prompt to install them on first open.
Format on Save#
.vscode/settings.json configures the Run on Save extension to run ./format.pas on every .pas and .dpr file when saved. This keeps code style consistent without manual intervention — the formatter fixes uses clause ordering, PascalCase naming, parameter prefixes, and stray spaces in the background.
The runOnSave command runs silently ("runIn": "backend"), so it will not open a terminal or interrupt your workflow. The file is re-read by the editor after formatting, so changes appear immediately.
Note: This requiresinstantfpc(ships with FreePascal) to be on yourPATH. If you installed FreePascal via the methods in Getting Started, this is already the case.
How the Layers Work Together#
| Layer | When it runs | What it does |
|---|---|---|
.editorconfig | While typing | Sets indent size, line endings, trailing whitespace, charset |
runOnSave | On file save | Runs ./format.pas to auto-fix code conventions |
| Lefthook pre-commit | On git commit | Runs ./format.pas on staged files as a safety net |
CI --check | On push / PR | Fails the build if any file needs formatting |
All four layers enforce the same rules, providing defence in depth. The typical developer experience is: EditorConfig handles whitespace while you type, format-on-save fixes everything else when you save, and the pre-commit hook and CI catch anything that slips through.
Platform-Specific Pitfalls#
Int64 to Double Conversion on FPC 3.2.2#
FPC 3.2.2 has two bugs affecting Int64 -> Double conversion. Bug A is a Delphi-mode front-end issue that affects all platforms. Bug B is an AArch64-specific codegen issue.
Bug A: Double(Int64Var) bit reinterpretation — FPC #35886#
In {$mode delphi}, an explicit Double(Int64Var) cast performs a Turbo Pascal-style bit reinterpretation instead of a value conversion. This produces garbage floating-point values (e.g., Double(Int64(1000)) yields ~4.94e-315 instead of 1000.0). This is a compiler front-end bug in defcmp.pas that affects all platforms in Delphi mode. Fixed in FPC trunk (3.3.1, commit `1da43f67`) but not backported to 3.2.x.
Bug B: Int64 * 1.0 wrong results near +/-2^31 (AArch64 only)#
Mixed Int64 * Double arithmetic produces wrong results for Int64 values near the LongInt boundary (+/-2,147,483,648). This affects all arithmetic operators (*, +, -, /) where one operand is Int64 and the other is Double. FPC appears to use a 32-bit SCVTF instruction instead of 64-bit when promoting Int64 through arithmetic expressions. This is AArch64-specific and has not yet been reported upstream.
// Observed on FPC 3.2.2 AArch64, all optimization levels:
Int64(-2147483647) * 1.0 -> -2147483648 // WRONG (should be -2147483647)
Int64(-2147483649) * 1.0 -> -2147483648 // WRONG (should be -2147483649)
Int64( 2147483649) * 1.0 -> 2147483648 // WRONG (should be 2147483649)
Int64(-3000000000) * 1.0 -> -3000000000 // correct (far from boundary)Safe conversion#
Use implicit assignment or function parameter passing — both use the correct 64-bit conversion path and are unaffected by either bug:
// WRONG — bit reinterpretation in Delphi mode (Bug A)
Result := Double(FEpochMilliseconds) * 1000000.0;
// WRONG — wrong results near +/-2^31 on AArch64 (Bug B)
Result := FEpochMilliseconds * 1.0;
// CORRECT — implicit assignment
var D: Double;
D := FEpochMilliseconds;
Result := D * 1000000.0;
// CORRECT — implicit promotion at function call boundary
// When passing Int64 to a function/constructor that takes Double,
// FPC generates the correct 64-bit SCVTF instruction.
Result := TGocciaNumberLiteralValue.Create(SomeInt64Value);This affects any code that converts Int64 fields to Double for floating-point arithmetic. Note that Int64 / Int64 is safe — FPC's / operator already returns Extended for integer operands, so no explicit promotion is needed for division.
Endian-Dependent Byte Indexing#
Do not inspect raw byte arrays of Double values to check the sign bit (e.g., Bytes[7] and $80). This assumes little-endian byte layout and breaks on big-endian platforms.
Instead, overlay the Double with Int64 absolute and test via integer sign:
// WRONG — assumes little-endian byte order
var V: Double; Bytes: array[0..7] of Byte absolute V;
begin
Result := (V = 0.0) and ((Bytes[7] and $80) <> 0);
end;
// CORRECT — endian-neutral sign bit check
var V: Double; Bits: Int64 absolute V;
begin
Result := (V = 0.0) and (Bits < 0);
end;This works because Int64 and Double share the same sign bit position (bit 63) at the integer level, regardless of byte ordering.