API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Hash

The Hash macro generates a hashCode() method for computing numeric hash codes. This is analogous to Rust’s Hash trait and Java’s hashCode() method, enabling objects to be used as keys in hash-based collections.

Generated Output

TypeGenerated CodeDescription
ClassclassNameHashCode(value) + static hashCode(value)Standalone function + static wrapper method
EnumenumNameHashCode(value: EnumName): numberStandalone function hashing by enum value
InterfaceinterfaceNameHashCode(value: InterfaceName): numberStandalone function computing hash
Type AliastypeNameHashCode(value: TypeName): numberStandalone function computing hash

Hash Algorithm

Uses the standard polynomial rolling hash algorithm:

hash = 17  // Initial seed
for each field:
    hash = (hash * 31 + fieldHash) | 0  // Bitwise OR keeps it 32-bit integer

This algorithm is consistent with Java’s Objects.hash() implementation.

Type-Specific Hashing

TypeHash Strategy
numberInteger: direct value; Float: string hash of decimal
bigintString hash of decimal representation
stringCharacter-by-character polynomial hash
boolean1231 for true, 1237 for false (Java convention)
DategetTime() timestamp
ArraysElement-by-element hash combination
MapEntry-by-entry key+value hash
SetElement-by-element hash
ObjectsCalls hashCode() if available, else JSON string hash

Field-Level Options

The @hash decorator supports:

  • skip - Exclude the field from hash calculation

Example

Before (Your Code)
/** @derive(Hash, PartialEq) */
class User {
    id: number;
    name: string;

    /** @hash({ skip: true }) */
    cachedScore: number;
}
After (Generated)
class User {
    id: number;
    name: string;

    cachedScore: number;

    static hashCode(value: User): number {
        return userHashCode(value);
    }

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name && a.cachedScore === b.cachedScore;
}

Generated output:

class User {
    id: number;
    name: string;

    cachedScore: number;

    static hashCode(value: User): number {
        return userHashCode(value);
    }

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name && a.cachedScore === b.cachedScore;
}

Hash Contract

Objects that are equal (PartialEq) should produce the same hash code. When using @hash(skip), ensure the same fields are skipped in both Hash and PartialEq to maintain this contract.