Tooling

Auto-formatting, editor configuration, and platform-specific pitfalls for FreePascal development.

Executive Summary#

  • Auto-formatter./format.pas auto-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 Int64Double conversion 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 --check

What 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 in path 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 A prefix 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.

Open the project and accept the "Install Recommended Extensions" prompt, or install them manually:

ExtensionIDPurpose
Pascalalefragnani.pascalSyntax highlighting, code navigation, and symbol search for Pascal/Delphi
Run on Saveemeraldwalk.RunOnSaveTriggers ./format.pas automatically when a .pas or .dpr file is saved
EditorConfigeditorconfig.editorconfigApplies .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 requires instantfpc (ships with FreePascal) to be on your PATH. If you installed FreePascal via the methods in Getting Started, this is already the case.

How the Layers Work Together#

LayerWhen it runsWhat it does
.editorconfigWhile typingSets indent size, line endings, trailing whitespace, charset
runOnSaveOn file saveRuns ./format.pas to auto-fix code conventions
Lefthook pre-commitOn git commitRuns ./format.pas on staged files as a safety net
CI --checkOn push / PRFails 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.