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 — stale FPC artifacts after branch changes, 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#
Stale FPC Build Artifacts#
After a branch switch, merge, PR sync, generated resource update, or unexplained compiler/resource failure, start with an explicit --clean build before diagnosing source code. FPC 3.2.2 can report stale compiled state as misleading internal compiler exceptions or resource-list errors.
./build.pas --clean loaderbare
./build.pas --clean testrunner
./build.pas --cleanTreat messages such as Compilation raised exception internally and Error while compiling resources as "clean first, diagnose second". Only investigate the reported Pascal source line after the same target still fails from a clean build.
Shared -FU Directories Across Programs — Internal Error 200611011#
FPC 3.2.2 aborts with Fatal: Internal error 200611011 when a second program is compiled against the .ppu files another program left in a shared -FU unit-output directory (the inliner trips while recompiling a unit it loaded from the first program's build). This is why build.pas compiles every target into its own build/compiled/targets/<target> directory, and why scripts/run_json5_test_suite.py gives each program its own subdirectory of its build dir. Any script that compiles more than one program with fpc @config.cfg must use a separate -FU directory per program.
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.