Code Style

Authoritative FreePascal naming, structure, and performance conventions for the GocciaScript codebase.

Executive Summary#

  • Naming — PascalCase functions, TGoccia* classes, F prefix fields, A prefix parameters, no abbreviations
  • Constants — Use centralized constant units (Goccia.Constants.*, Goccia.Keywords.*, Goccia.FileExtensions) instead of hardcoded literals
  • Performance — Use TStringBuffer (not TStringBuilder), purpose-built hash maps (not TDictionary) on hot paths, and TObjectList<T> type aliases for cross-unit generics
  • Auto-formatting./format.pas enforces uses clause ordering, PascalCase naming, and parameter prefixes; see Tooling for setup

At a glance (details follow in the subsections):

  • Naming — PascalCase functions and methods; TGoccia* classes; `F` = private fields; `A` = multi-letter parameters; full words, no abbreviations (exceptions: AST, JSON, GocciaREPL, ISO, Utils); locals: PascalCase, no underscores, no numeric suffixes
  • ConstantsGoccia.Constants.*, Goccia.Keywords.*, Goccia.FileExtensions (not string literals)
  • PerformanceTStringBuffer not TStringBuilder; purpose-built maps on hot paths; TObjectList<T> named aliases across units
  • Formatting./format.pas + Lefthook; rules under Tooling

Pascal Conventions#

Compiler Directives#

All units include the shared compiler directives file:

{$I Goccia.inc}

Which sets:

{$mode delphi}           // Delphi-compatible mode
{$H+}                     // Long strings by default
{$overflowchecks on}     // Runtime overflow checking
{$rangechecks on}        // Runtime range checking
{$modeswitch advancedrecords}   // Records with methods

Overflow and range checks are enabled — correctness is prioritized over raw performance.

Naming Conventions#

ElementConventionExample
UnitsGoccia.<Category>.<Name>.pasGoccia.Values.Primitives.pas
ClassesTGoccia<Name> prefixTGocciaObjectValue
InterfacesI<Name> prefixIGocciaSerializable
Private fieldsF prefixFValue, FPrototype
Functions/ProceduresPascalCaseEvaluate, EvaluateBinary
MethodsPascalCaseGetProperty, ToStringLiteral
ConstantsPascalCase or UPPER_CASEDefaultPreprocessors, NaN
ParametersA prefix (multi-letter only)AScope, AValue, AFileName
Local variablesPascalCase; no underscores; no trailing digit suffixes (Value1, temp2)CandidateScope, ResolvedName
EnumsTGoccia<Name> for type, lowercase prefix for valuesTGocciaScopeKind, skGlobal

Source Directory Layout and Namespacing#

Source files live in four directories under source/, each with different naming rules:

DirectoryPurposeUnit prefixInclude fileExample
source/units/Engine internals — the GocciaScript runtimeGoccia.*{$I Goccia.inc}Goccia.Values.Primitives.pas
source/shared/Reusable infrastructure — not GocciaScript-specificNo prefix{$I Shared.inc}HashMap.pas, StringBuffer.pas, CLI.Options.pas
source/generated/Generated source and resources consumed by the runtime; do not edit by handNo required prefixGenerator-definedGenerated.TimeZoneData.pas, Generated.TimeZoneData.res
source/app/CLI application entrypoints and app-level wiringGoccia.* for units, Goccia prefix (no dot) for programs{$I Goccia.inc}GocciaTestRunner.dpr, Goccia.CLI.Application.pas

Generated units should contain generated data, generated resource links, and the minimum schema needed to describe that data. Hand-authored lookup, decoding, validation, and other behavior belongs in source/units/ or source/shared/, depending on ownership.

*When to use `Goccia.`:** Any unit that depends on or extends the GocciaScript engine (runtime values, evaluator, compiler, modules, etc.).

When to omit the prefix: Generic data structures, utilities, and framework code that could be extracted to a standalone library. These live in source/shared/ and use {$I Shared.inc} instead of {$I Goccia.inc}.

CLI program naming: Application .dpr files use Goccia as a prefix without a dot separator — GocciaTestRunner.dpr, GocciaScriptLoader.dpr, GocciaREPL.dpr. This produces binary names like build/GocciaTestRunner.

Do not use snake_case or mixed_Case for locals — use full words in PascalCase. Do not disambiguate with numeric suffixes; choose a descriptive name (PrimaryScope, FallbackScope) instead. Short single-letter names in very small scopes (e.g. loop I, J) remain acceptable.

Centralized Constants#

Use centralized constant units instead of hardcoded string literals:

  • Keywords — Use Goccia.Keywords.Reserved (KEYWORD_THIS, KEYWORD_SUPER, etc.) and Goccia.Keywords.Contextual (KEYWORD_GET, KEYWORD_SET, etc.) instead of raw 'this', 'get' strings.
  • File extensions — Use Goccia.FileExtensions constants (EXT_JS, EXT_JSX, EXT_TS, EXT_TSX, EXT_MJS, EXT_JSON, EXT_JSON5, EXT_JSONL, EXT_TOML, EXT_YAML, EXT_YML, EXT_TXT, EXT_MD, EXT_GBC) instead of raw string literals. Use the appropriate shared arrays and helpers (ScriptExtensions, ModuleImportExtensions, IsScriptExtension, IsTextAssetExtension, IsJSXNativeExtension, etc.) instead of duplicating extension lists.

Adding a new keyword or file extension requires a single change in the constants unit — all consumers pick it up automatically.

No Magic Numbers#

Avoid bare numeric literals in the implementation section. Extract them into named constants so the value is defined once and the name conveys intent:

// Wrong — magic number repeated and unexplained
if ACapacity > 0 then
  Result.FCap := ACapacity
else
  Result.FCap := 64;

// Correct — named constant, defined once
const
  DEFAULT_CAPACITY = 64;

if ACapacity > 0 then
  Result.FCap := ACapacity
else
  Result.FCap := DEFAULT_CAPACITY;

When the same constant is used in both the interface section (e.g., as a default parameter value) and the implementation section (e.g., as a fallback), declare it in interface so both sites can reference it. Implementation-only constants stay in implementation.

Trivial literals that are self-explanatory in context (0, 1, -1, '', True, False) do not need extraction.

Locale-Independent Number Formatting#

Never use bare FloatToStr or FormatFloat without an explicit TFormatSettings argument — these use the system decimal separator which varies by locale and breaks deterministic output.

Use FormatDouble (from Goccia.Values.Primitives) for any float-to-string conversion that must produce ECMAScript-compliant output (property keys, error messages, serialization). For formatted output that needs a specific pattern (e.g., toFixed, toExponential, timing), pass InvariantFormatSettings (same unit) to FormatFloat:

// Wrong — locale-dependent, breaks on systems with comma decimal separator
Result := FloatToStr(AValue);
Result := FormatFloat('0.###', AValue);

// Correct — ES2026 §6.1.6.1.20 Number::toString, always uses '.'
Result := FormatDouble(AValue);

// Correct — formatted output with invariant decimal separator
Result := FormatFloat('0.###', AValue, InvariantFormatSettings);

For source/shared/ units that cannot import Goccia.Values.Primitives, use explicit inline format settings:

FS := DefaultFormatSettings;
FS.DecimalSeparator := '.';
Result := FloatToStr(AValue, FS);

ECMAScript Spec Annotations#

When implementing ECMAScript-specified behavior, annotate each function or method with a comment referencing the relevant specification section. Place the annotation immediately above the function body in the implementation section. For multi-step spec algorithms, also annotate individual steps inline within the function body:

// ES2026 §23.1.3.18 Array.prototype.map(callbackfn [, thisArg])
function TGocciaArrayValue.Map(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
var
  Arr: TGocciaArrayValue;
  ResultArray: TGocciaArrayValue;
begin
  Arr := TGocciaArrayValue(AThisValue);
  // ES2026 §23.1.3.18 step 4: ArraySpeciesCreate(O, len)
  ResultArray := ArraySpeciesCreate(Arr, Arr.Elements.Count);
  // ...
end;

Format: // ESYYYY §X.Y.Z SpecMethodName(specParams)

  • YYYY is the current edition year of the ECMA-262 specification (e.g., ES2026 for 2026, ES2027 for 2027). Use the year matching the current year.
  • The section numbers reference ECMA-262, the living standard.
  • The method name and parameter list must match the spec's pseudo-code exactly — use Array.prototype.map(callbackfn [, thisArg]), not the Pascal implementation name TGocciaArrayValue.Map(AArgs, AThisValue). The annotation is a spec cross-reference, not a Pascal signature.
  • Use the full qualified name as it appears in the spec (e.g., Array.prototype.map, Object.keys, Number.parseInt).
  • For abstract operations, use the spec's operation name and parameters (e.g., Await(value), ToPrimitive(input [, preferredType]), IteratorNext(iteratorRecord [, value])).
  • For individual algorithm steps within a function body, use // ESYYYY §X.Y.Z step N: description.

What to annotate:

CategoryExample
Built-in prototype methods// ES2026 §22.1.3.22 String.prototype.slice(start, end)
Built-in static methods// ES2026 §20.1.2.1 Object.assign(target, ...sources)
Built-in constructors// ES2026 §23.1.1.1 Array(len)
Abstract operations// ES2026 §7.1.1 ToPrimitive(input [, preferredType])
Internal algorithms// ES2026 §7.3.35 ArraySpeciesCreate(originalArray, length)
Algorithm steps (inline)// ES2026 §23.1.3.18 step 4: ArraySpeciesCreate(O, len)

TC39 proposals not yet merged into ECMA-262 use the proposal name instead of a section number:

// TC39 Temporal §5.5.3 Temporal.Duration.prototype.add(other)
// TC39 Iterator Helpers §2.1.3.1 Iterator.prototype.map(mapper)
// TC39 Set Methods §2.1 Set.prototype.union(other)

What not to annotate: Internal GocciaScript helpers that don't correspond to a spec algorithm (e.g., EvaluateStatements, SpreadIterableInto, Pascal-specific utilities).

Documentation — Edition Year vs Proposal Stage#

In the built-in documentation (docs/built-ins*.md), do not annotate features with their ECMAScript edition year. If a feature is part of the ratified standard, it is simply "the standard" — no (ES2025) or (ES2026) suffix. Only annotate features that are still TC39 proposals with their current stage (e.g., "Stage 2", "Stage 3"). This avoids documentation churn every time a new edition is ratified.

  • Standard feature: Math.sumPrecise(iterable) — Precise summation of iterables.
  • Proposal: Math.clamp(x, min, max) — Clamp to range (Stage 2 proposal-math-clamp).

The language tables (docs/language-tables.md) are the one place where edition years are listed, as a feature-provenance reference. Source code annotations (// ES2026 §X.Y.Z) always use the current edition year per the rules above.

No Abbreviations#

Class names, function names, method names, and type names must use full words — do not abbreviate. This keeps the codebase consistent and self-documenting.

// Correct
TGarbageCollector
MarkReferences
IsExternalDeclaration
DateTimeAdd

// Wrong — abbreviated
TGocciaGC
GCMarkReferences
IsExternalDecl
DTAdd

Exceptions: Industry-standard abbreviations are kept as-is: AST, JSON, GocciaREPL, ISO, Utils.

Generic Lists for Class Types#

Prefer TObjectList<T> over TList<T> when T is a class. TObjectList makes ownership semantics explicit via OwnsObjects — use Create (or Create(True)) for owning collections, Create(False) for non-owning references.

Named type aliases: When a generic specialization like TObjectList<TSomeClass> is used across multiple compilation units, define a single named type alias in the unit that declares TSomeClass. This ensures FPC produces one VMT for the specialization, avoiding "Invalid type cast" failures when {$OBJECTCHECKS ON} performs cross-unit type checks.

// In Goccia.Values.Primitives.pas (where TGocciaValue is declared)
TGocciaValueList = TObjectList<TGocciaValue>;

// In Goccia.Scope.pas (where TGocciaScope is declared)
TGocciaScopeList = TObjectList<TGocciaScope>;

All consumers import the alias from the declaring unit — never re-specialize TObjectList<TGocciaValue> or TObjectList<TGocciaScope> locally:

// Correct — uses the shared alias
FElements: TGocciaValueList;
FManagedScopes: TGocciaScopeList;

// Wrong — local re-specialization creates a separate VMT
FElements: TObjectList<TGocciaValue>;
FManagedScopes: TObjectList<TGocciaScope>;

Why `TObjectList(False)` instead of `TList`? Even when the collection does not own its elements (e.g., the GC's managed scopes list, which uses manual mark-and-sweep), using TObjectList<T>.Create(False) with a named alias keeps the VMT consistent. TList<T> and TObjectList<T> produce incompatible VMTs, so mixing them across units reintroduces the same cross-unit type check failures. See spikes/fpc-generics-performance.md for the benchmark analysis confirming generics have zero runtime cost.

Hash Map Selection#

The codebase provides purpose-built hash maps that replace TDictionary on hot paths. Choose based on key type and ordering requirements:

Use caseMap typeNotes
String keys, insertion orderTOrderedStringMap<V>4-6x faster inserts than TDictionary at N=20-100; static inline DJB2 hash/equality; tracks deleted buckets and compacts after delete-heavy phases to bound probe chains
Generic keys, insertion orderTOrderedMap<K,V>Virtual HashKey/KeysEqual; default: DJB2 over raw key bytes
Any key, unorderedTHashMap<K,V>Backshift deletion (no tombstones); static inline hash/equality; 2x faster inserts for pointer keys
Scope bindingsTOrderedStringMap<V>Hash-based O(1) lookup per scope level; chain walking in TGocciaScope
Cold-path / diagnosticTDictionary<K,V>Acceptable where performance is not critical

Never use TFPDataHashTable — it has catastrophic insert performance (400,000 ns/insert vs 50 ns for TOrderedStringMap at N=20). See spikes/fpc-hashmap-performance.md for the full benchmark analysis.

API compatibility: All custom maps share the same core API as TDictionary: Add, AddOrSetValue, TryGetValue, ContainsKey, Remove, Clear. The Keys, Values, and ToArray accessors return dynamic arrays, so index those with a for loop rather than for...in.

Sequential iteration: TOrderedStringMap and TOrderedMap also expose a key/value-pair enumerator — prefer for Pair in Map do, which yields entries in declaration order at O(1) amortized per step (this is how the class, scope, and module code iterate these maps). Do not iterate by driving the random-access EntryAt(Index) from a for Index := 0 to Count - 1 loop: EntryAt scans active entries from the start (O(Index)), so the loop is O(N²).

Function and Method Names#

All function, procedure, constructor, and destructor names must be PascalCase — the first letter of each word is uppercase, no underscores. This applies to both free functions and class methods:

// Correct
function EvaluateBinary(const AExpr: TGocciaBinaryExpression): TGocciaValue;
procedure RegisterBuiltin(const AName: string; const AValue: TGocciaValue);
class function CreateFromPairs(const APairs: TGocciaArrayValue): TGocciaMapValue;

// Wrong — camelCase or snake_case
function evaluateBinary(const AExpr: TGocciaBinaryExpression): TGocciaValue;
procedure register_builtin(const AName: string; const AValue: TGocciaValue);

Exception: External C function bindings (declared with external) retain their original C naming (e.g., clock_gettime).

This is auto-fixed by ./format.pas.

Uses Clauses#

Each unit in the uses clause must appear on its own line, following Embarcadero's recommended style. Units are grouped by category with a blank line between groups, and sorted alphabetically within each group:

1. System units — FPC standard library (Classes, SysUtils, Math, Generics.Collections, etc.) 2. Third-party / non-prefixed project units — units without Goccia.* prefix and without an in path (TimingUtils, etc.) 3. Project unitsGoccia.* namespaced units 4. Relative units — units with an explicit in path (FileUtils in 'source/shared/FileUtils.pas', etc.)

uses
  Classes,
  Generics.Collections,
  SysUtils,

  TimingUtils,

  Goccia.Scope,
  Goccia.Values.Primitives,

  FileUtils in 'source/shared/FileUtils.pas';

This ordering is enforced automatically by ./format.pas via Lefthook pre-commit hook.

Parameters#

All function and procedure parameters must follow these rules:

1. `A` prefix — Every parameter name with two or more characters starts with A (e.g., AScope, AValue, AFileName). This distinguishes parameters from fields (F prefix) and local variables. Single-letter names (e.g., A, B, E, T) are left as-is — the A prefix is not applied to them.

2. `const` where possible — Use const for parameters that are not modified within the function body. This applies to all types: objects, strings, integers, records, etc. For records, const prevents field modification, so only omit it when the function needs to mutate the record locally.

// Correct — multi-letter parameters get A prefix + const
procedure ProcessValue(const AValue: TGocciaValue; const AName: string);
function CreateChild(const AKind: TGocciaScopeKind): TGocciaScope;

// Correct — single-letter parameters keep their name
function DefaultCompare(constref A, B: TGocciaValue): Integer;
function DoSubtract(const A, B: Double): Double;

// Wrong — missing A prefix on multi-letter name, missing const
procedure ProcessValue(Value: TGocciaValue; Name: string);

Exceptions to const:

  • Parameters that are genuinely modified inside the function (e.g., loop counters, accumulator records)
  • out parameters (which are written, not read)
  • var parameters (which are both read and written)

File Organization#

Each unit follows this structure:

unit Goccia.Category.Name;

{$I Goccia.inc}

interface

uses
  Classes,
  Generics.Collections,
  SysUtils,

  Goccia.Scope,
  Goccia.Values.Primitives;

type
  // Type declarations (classes, interfaces, records, enums)

// Free function declarations (for evaluator modules)

implementation

uses
  Goccia.Evaluator;

// Implementation

end.

Key principle: Use the implementation uses clause to break circular dependencies. The interface section declares only what's needed for the public API; heavy dependencies go in the implementation section.

Unit Naming#

Units are organized by category using dot-separated names:

CategoryPurposeExamples
Goccia.Values.*Value type hierarchyPrimitives, ObjectValue, ArrayValue, ClassValue
Goccia.Builtins.*Built-in object implementationsConsole, Math, JSON, GlobalObject
Goccia.Evaluator.*Evaluator sub-modulesArithmetic, Bitwise, Comparison, Assignment
Goccia.AST.*AST node definitionsNode, Expressions, Statements
Goccia.Arguments.*Function argument handlingCollection, Converter, Validator

Collection Return Types#

Avoid returning TArray<T> from public API methods. Prefer indexed access (GetItem(Index) + Count) or returning the dictionary's own TKeyCollection / TValueCollection which support for..in without allocating an intermediate array. This is consistent with how the codebase iterates dictionaries throughout (e.g., for Key in Dictionary.Keys do).

When a method only needs to expose elements for iteration, an indexed getter with a count property is the most lightweight approach — no allocation, no ownership ambiguity.

Code Organization Principles#

1. Explicitness — Modules, classes, methods, and properties use explicit, descriptive names even at the cost of verbosity. Shortcuts and abbreviations are avoided.

2. OOP over everything — Rely on type safety of specialized classes. Each concept gets its own class rather than using generic data structures.

3. Separation of concerns — Each unit has a single, clear responsibility. The evaluator doesn't know about built-ins; the engine doesn't know about AST structure.

4. Minimal public API — Units expose only what's needed. Implementation details stay in the implementation section.

5. No global mutable state — State flows through parameters (evaluation context, scope) rather than global variables. The only globals are immutable singletons.