Code Style
Authoritative FreePascal naming, structure, and performance conventions for the GocciaScript codebase.
Executive Summary
- Naming — PascalCase functions,
TGoccia*classes,Fprefix fields,Aprefix parameters, no abbreviations - Constants — Use centralized constant units (
Goccia.Constants.*,Goccia.Keywords.*,Goccia.FileExtensions) instead of hardcoded literals - Performance — Use
TStringBuffer(notTStringBuilder), purpose-built hash maps (notTDictionary) on hot paths, andTObjectList<T>type aliases for cross-unit generics - Auto-formatting —
./format.pasenforces 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 - Constants —
Goccia.Constants.*,Goccia.Keywords.*,Goccia.FileExtensions(not string literals) - Performance —
TStringBuffernotTStringBuilder; 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.
Keep the entire compiler-conditional routine directive sequence on the declaration's final line:
function NormalizeValue(const AValue: Double): Double; {$IFDEF FPC}inline;{$ENDIF}
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Units | Goccia.<Category>.<Name>.pas | Goccia.Values.Primitives.pas |
| Classes | TGoccia<Name> prefix | TGocciaObjectValue |
| Interfaces | I<Name> prefix | IGocciaSerializable |
| Private fields | F prefix | FValue, FPrototype |
| Functions/Procedures | PascalCase | Evaluate, EvaluateBinary |
| Methods | PascalCase | GetProperty, ToStringLiteral |
| Constants | PascalCase or UPPER_CASE | DefaultPreprocessors, NaN |
| Parameters | A prefix (multi-letter only) | AScope, AValue, AFileName |
| Local variables | PascalCase; no underscores; no trailing digit suffixes (Value1, temp2) | CandidateScope, ResolvedName |
| Enums | TGoccia<Name> for type, lowercase prefix for values | TGocciaScopeKind, skGlobal |
Source Directory Layout and Namespacing
Source files live in four directories under source/, each with different naming rules:
| Directory | Purpose | Unit prefix | Include file | Example |
|---|---|---|---|---|
source/units/ | Engine internals — the GocciaScript runtime | Goccia.* | {$I Goccia.inc} | Goccia.Values.Primitives.pas |
source/shared/ | Reusable infrastructure — not GocciaScript-specific | No 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 hand | No required prefix | Generator-defined | Generated.TimeZoneData.pas, Generated.TimeZoneData.res |
source/app/ | CLI application entrypoints and app-level wiring | Goccia.* 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.) andGoccia.Keywords.Contextual(KEYWORD_GET,KEYWORD_SET, etc.) instead of raw'this','get'strings. - File extensions — Use
Goccia.FileExtensionsconstants (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 unexplainedif ACapacity > 0 thenResult.FCap := ACapacityelseResult.FCap := 64;// Correct — named constant, defined onceconstDEFAULT_CAPACITY = 64;if ACapacity > 0 thenResult.FCap := ACapacityelseResult.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 separatorResult := 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 separatorResult := 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;varArr: TGocciaArrayValue;ResultArray: TGocciaArrayValue;beginArr := 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)
YYYYis the current edition year of the ECMA-262 specification (e.g.,ES2026for 2026,ES2027for 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 nameTGocciaArrayValue.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:
| Category | Example |
|---|---|
| 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.
Comments That Quote JavaScript
Use (* ... *) rather than brace delimiters for any comment containing a
JavaScript brace. A brace comment ends at the first } inside it, and the
compiler then reports an "illegal character" at whatever token follows — a
column with no relation to the cause.
Neither comment form nests with itself, so a comment that discusses these delimiters spells them out in words instead of quoting the characters.
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.
// CorrectTGarbageCollectorMarkReferencesIsExternalDeclarationDateTimeAdd// Wrong — abbreviatedTGocciaGCGCMarkReferencesIsExternalDeclDTAdd
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 aliasFElements: TGocciaValueList;FManagedScopes: TGocciaScopeList;// Wrong — local re-specialization creates a separate VMTFElements: 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 case | Map type | Notes |
|---|---|---|
| String keys, insertion order | TOrderedStringMap<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 order | TOrderedMap<K,V> | Virtual HashKey/KeysEqual; default: DJB2 over raw key bytes |
| Any key, unordered | THashMap<K,V> | Backshift deletion (no tombstones); static inline hash/equality; 2x faster inserts for pointer keys |
| Scope bindings | TOrderedStringMap<V> | Hash-based O(1) lookup per scope level; chain walking in TGocciaScope |
| Cold-path / diagnostic | TDictionary<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:
// Correctfunction EvaluateBinary(const AExpr: TGocciaBinaryExpression): TGocciaValue;procedure RegisterBuiltin(const AName: string; const AValue: TGocciaValue);class function CreateFromPairs(const APairs: TGocciaArrayValue): TGocciaMapValue;// Wrong — camelCase or snake_casefunction 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:
- System units — FPC standard library (
Classes,SysUtils,Math,Generics.Collections, etc.) - Third-party / non-prefixed project units — units without
Goccia.*prefix and without aninpath (TimingUtils, etc.) - Project units —
Goccia.*namespaced units - Relative units — units with an explicit
inpath (FileUtils in 'source/shared/FileUtils.pas', etc.)
usesClasses,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:
-
Aprefix — Every parameter name with two or more characters starts withA(e.g.,AScope,AValue,AFileName). This distinguishes parameters from fields (Fprefix) and local variables. Single-letter names (e.g.,A,B,E,T) are left as-is — theAprefix is not applied to them. -
constwhere possible — Useconstfor parameters that are not modified within the function body. This applies to all types: objects, strings, integers, records, etc. For records,constprevents field modification, so only omit it when the function needs to mutate the record locally.
// Correct — multi-letter parameters get A prefix + constprocedure ProcessValue(const AValue: TGocciaValue; const AName: string);function CreateChild(const AKind: TGocciaScopeKind): TGocciaScope;// Correct — single-letter parameters keep their namefunction DefaultCompare(constref A, B: TGocciaValue): Integer;function DoSubtract(const A, B: Double): Double;// Wrong — missing A prefix on multi-letter name, missing constprocedure ProcessValue(Value: TGocciaValue; Name: string);
Exceptions to const:
- Parameters that are genuinely modified inside the function (e.g., loop counters, accumulator records)
outparameters (which are written, not read)varparameters (which are both read and written)
File Organization
Each unit follows this structure:
unit Goccia.Category.Name;{$I Goccia.inc}interfaceusesClasses,Generics.Collections,SysUtils,Goccia.Scope,Goccia.Values.Primitives;type// Type declarations (classes, interfaces, records, enums)// Free function declarations (for evaluator modules)implementationusesGoccia.Evaluator;// Implementationend.
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:
| Category | Purpose | Examples |
|---|---|---|
Goccia.Values.* | Value type hierarchy | Primitives, ObjectValue, ArrayValue, ClassValue |
Goccia.Builtins.* | Built-in object implementations | Console, Math, JSON, GlobalObject |
Goccia.Evaluator.* | Evaluator sub-modules | Arithmetic, Bitwise, Comparison, Assignment |
Goccia.AST.* | AST node definitions | Node, Expressions, Statements |
Goccia.Arguments.* | Function argument handling | Collection, 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
-
Explicitness — Modules, classes, methods, and properties use explicit, descriptive names even at the cost of verbosity. Shortcuts and abbreviations are avoided.
-
OOP over everything — Rely on type safety of specialized classes. Each concept gets its own class rather than using generic data structures.
-
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.
-
Minimal public API — Units expose only what's needed. Implementation details stay in the
implementationsection. -
No global mutable state — State flows through parameters (evaluation context, scope) rather than global variables. The only globals are immutable singletons.