diff --git a/client/lib/battle.service.ts b/client/lib/battle.service.ts index 8718bd4..349d2b1 100644 --- a/client/lib/battle.service.ts +++ b/client/lib/battle.service.ts @@ -4,6 +4,10 @@ * Provides battle simulation and on-chain interaction for the Chomp battle system. * Supports both local TypeScript simulation and on-chain viem interactions. * + * This service composes the BattleHarness internally for local simulation, + * providing Angular-specific reactivity via signals while delegating + * battle logic to the transpiled Engine. + * * Requirements: Angular 20+, viem * * Usage: @@ -24,14 +28,12 @@ import { WritableSignal, signal, computed, - effect, inject, PLATFORM_ID, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { createPublicClient, - createWalletClient, http, type PublicClient, type WalletClient, @@ -39,7 +41,6 @@ import { type Address, type Hash, keccak256, - encodePacked, encodeAbiParameters, toHex, } from 'viem'; @@ -48,38 +49,31 @@ import { mainnet } from 'viem/chains'; import { MoveMetadata, MoveType, - MoveClass, - ExtraDataType, BattleState, TeamState, MonBattleState, BattleEvent, BattleServiceConfig, - MonDefinition, - DEFAULT_CONSTANTS, } from './types'; -import { loadMoveMetadata, convertMoveMetadata } from './metadata-converter'; +import { loadMoveMetadata } from './metadata-converter'; + +// Import harness types and factory +import { + BattleHarness, + createBattleHarness, + type MonConfig, + type TeamConfig, + type BattleConfig as HarnessBattleConfig, + type TurnInput, + type BattleState as HarnessBattleState, + type MonState as HarnessMonState, +} from '../../transpiler/runtime/battle-harness'; // ============================================================================= -// LOCAL SIMULATION TYPES (matches transpiled code) +// RE-EXPORT HARNESS TYPES FOR CONVENIENCE // ============================================================================= -/** - * Mon structure as used by the Engine - */ -interface EngineMon { - stats: bigint; - moves: string[]; - ability: string; -} - -/** - * Move selection structure - */ -interface MoveSelection { - packedMoveIndex: bigint; - extraData: bigint; -} +export type { MonConfig, TeamConfig }; // ============================================================================= // BATTLE SERVICE @@ -170,12 +164,10 @@ export class BattleService { ); // ------------------------------------------------------------------------- - // Local Simulation State (for TypeScript engine) + // Battle Harness (for local simulation) // ------------------------------------------------------------------------- - private localEngine: any = null; - private localTypeCalculator: any = null; - private localMoves: Map = new Map(); + private harness: BattleHarness | null = null; // ------------------------------------------------------------------------- // Configuration Methods @@ -253,12 +245,13 @@ export class BattleService { } // ------------------------------------------------------------------------- - // Local Simulation Methods + // Local Simulation Methods (using BattleHarness) // ------------------------------------------------------------------------- /** - * Initialize the local TypeScript simulation engine - * This dynamically imports the transpiled Engine + * Initialize the local TypeScript simulation engine. + * Creates a BattleHarness that handles module loading with caching + * and dependency injection via ContractContainer. */ async initializeLocalEngine(): Promise { if (!this._config().localSimulation) { @@ -266,32 +259,10 @@ export class BattleService { } try { - // Dynamic imports for the transpiled code - // These paths should be adjusted based on your build setup - const [ - { Engine }, - { TypeCalculator }, - { StandardAttack }, - Structs, - Enums, - Constants, - ] = await Promise.all([ - import('../../transpiler/ts-output/Engine'), - import('../../transpiler/ts-output/TypeCalculator'), - import('../../transpiler/ts-output/StandardAttack'), - import('../../transpiler/ts-output/Structs'), - import('../../transpiler/ts-output/Enums'), - import('../../transpiler/ts-output/Constants'), - ]); - - // Create engine instance - this.localEngine = new Engine(); - this.localTypeCalculator = new TypeCalculator(); - - // Initialize battle config storage - (this.localEngine as any).battleConfig = {}; - (this.localEngine as any).battleData = {}; - (this.localEngine as any).storageKeyMap = {}; + // Create harness with module loader that uses cached dynamic imports + this.harness = await createBattleHarness( + (name: string) => import(`../../transpiler/ts-output/${name}`) + ); } catch (err) { this._error.set(`Failed to initialize local engine: ${(err as Error).message}`); throw err; @@ -299,121 +270,42 @@ export class BattleService { } /** - * Initialize a local battle with two teams + * Initialize a local battle with two teams. + * Uses the harness's MonConfig format with individual stat fields. */ async initializeLocalBattle( p0Address: string, p1Address: string, - p0Team: EngineMon[], - p1Team: EngineMon[] + p0Team: MonConfig[], + p1Team: MonConfig[] ): Promise { - if (!this.localEngine) { + if (!this.harness) { await this.initializeLocalEngine(); } - const engine = this.localEngine; - - // Compute battle key - const [battleKey] = engine.computeBattleKey(p0Address, p1Address); - - // Initialize storage - this.initializeLocalBattleConfig(battleKey); - this.initializeLocalBattleData(battleKey, p0Address, p1Address); + const battleConfig: HarnessBattleConfig = { + player0: p0Address, + player1: p1Address, + teams: [ + { mons: p0Team }, + { mons: p1Team }, + ], + }; - // Set up teams - this.setupLocalTeams(battleKey, p0Team, p1Team); + const battleKey = await this.harness!.startBattle(battleConfig); this._battleKey.set(battleKey); this._battleEvents.set([]); - // Create initial battle state - this.updateBattleStateFromEngine(battleKey); + // Create initial battle state from harness + this.updateBattleStateFromHarness(battleKey); return battleKey; } - private initializeLocalBattleConfig(battleKey: string): void { - const engine = this.localEngine as any; - - const emptyConfig = { - validator: new MockValidator(), - packedP0EffectsCount: 0n, - rngOracle: new MockRNGOracle(), - packedP1EffectsCount: 0n, - moveManager: '0x0000000000000000000000000000000000000000', - globalEffectsLength: 0n, - teamSizes: 0n, - engineHooksLength: 0n, - koBitmaps: 0n, - startTimestamp: BigInt(Math.floor(Date.now() / 1000)), - p0Salt: '0x0000000000000000000000000000000000000000000000000000000000000000', - p1Salt: '0x0000000000000000000000000000000000000000000000000000000000000000', - p0Move: { packedMoveIndex: 0n, extraData: 0n }, - p1Move: { packedMoveIndex: 0n, extraData: 0n }, - p0Team: {} as any, - p1Team: {} as any, - p0States: {} as any, - p1States: {} as any, - globalEffects: {} as any, - p0Effects: {} as any, - p1Effects: {} as any, - engineHooks: {} as any, - }; - - engine.battleConfig[battleKey] = emptyConfig; - engine.storageKeyForWrite = battleKey; - engine.storageKeyMap[battleKey] = battleKey; - } - - private initializeLocalBattleData( - battleKey: string, - p0: string, - p1: string - ): void { - const engine = this.localEngine as any; - engine.battleData[battleKey] = { - p0, - p1, - winnerIndex: 2n, // No winner yet - prevPlayerSwitchForTurnFlag: 2n, - playerSwitchForTurnFlag: 2n, // Both players move - activeMonIndex: 0n, // Both start with mon 0 - turnId: 0n, - }; - } - - private setupLocalTeams( - battleKey: string, - p0Team: EngineMon[], - p1Team: EngineMon[] - ): void { - const engine = this.localEngine as any; - const config = engine.battleConfig[battleKey]; - - // Set team sizes (p0 in lower 4 bits, p1 in upper 4 bits) - config.teamSizes = BigInt(p0Team.length) | (BigInt(p1Team.length) << 4n); - - // Add mons to teams - for (let i = 0; i < p0Team.length; i++) { - config.p0Team[i] = p0Team[i]; - config.p0States[i] = this.createEmptyMonState(); - } - for (let i = 0; i < p1Team.length; i++) { - config.p1Team[i] = p1Team[i]; - config.p1States[i] = this.createEmptyMonState(); - } - } - - private createEmptyMonState(): any { - return { - packedStatDeltas: 0n, - isKnockedOut: false, - shouldSkipTurn: false, - }; - } - /** - * Execute a turn in the local simulation + * Execute a turn in the local simulation. + * Delegates to the BattleHarness which uses Engine's public API. */ async executeLocalTurn( p0MoveIndex: number, @@ -424,7 +316,7 @@ export class BattleService { p1Salt: string ): Promise { const battleKey = this._battleKey(); - if (!battleKey || !this.localEngine) { + if (!battleKey || !this.harness) { throw new Error('No active battle'); } @@ -432,21 +324,24 @@ export class BattleService { this._error.set(null); try { - const engine = this.localEngine; - - // Advance block timestamp - engine._block = engine._block || { timestamp: BigInt(Math.floor(Date.now() / 1000)) }; - engine._block.timestamp += 1n; - - // Set moves for both players - engine.setMove(battleKey, 0n, BigInt(p0MoveIndex), p0Salt, p0ExtraData); - engine.setMove(battleKey, 1n, BigInt(p1MoveIndex), p1Salt, p1ExtraData); + const turnInput: TurnInput = { + player0: { + moveIndex: p0MoveIndex, + salt: p0Salt, + extraData: p0ExtraData, + }, + player1: { + moveIndex: p1MoveIndex, + salt: p1Salt, + extraData: p1ExtraData, + }, + }; - // Execute the turn - engine.execute(battleKey); + // Execute turn via harness (delegates to Engine) + this.harness.executeTurn(battleKey, turnInput); - // Update battle state - this.updateBattleStateFromEngine(battleKey); + // Update battle state from harness + this.updateBattleStateFromHarness(battleKey); // Record event this._battleEvents.update((events) => [ @@ -467,80 +362,64 @@ export class BattleService { } /** - * Update battle state from engine data + * Update battle state from harness. + * Converts harness state (with deltas) to client state (with absolute values). */ - private updateBattleStateFromEngine(battleKey: string): void { - const engine = this.localEngine as any; - const config = engine.battleConfig[battleKey]; - const data = engine.battleData[battleKey]; - - if (!config || !data) return; - - // Extract team sizes - const p0Size = Number(config.teamSizes & 0xfn); - const p1Size = Number(config.teamSizes >> 4n); + private updateBattleStateFromHarness(battleKey: string): void { + if (!this.harness) return; - // Build team states - const p0Mons: MonBattleState[] = []; - const p1Mons: MonBattleState[] = []; - - for (let i = 0; i < p0Size; i++) { - const mon = config.p0Team[i]; - const state = config.p0States[i]; - p0Mons.push(this.extractMonState(mon, state)); - } - - for (let i = 0; i < p1Size; i++) { - const mon = config.p1Team[i]; - const state = config.p1States[i]; - p1Mons.push(this.extractMonState(mon, state)); - } + const harnessState = this.harness.getBattleState(battleKey); - // Extract active mon indices - const p0Active = Number(data.activeMonIndex & 0xfn); - const p1Active = Number(data.activeMonIndex >> 4n); + // Convert harness state to client state + const p0Mons = harnessState.p0States.map((state) => + this.convertHarnessMonState(state) + ); + const p1Mons = harnessState.p1States.map((state) => + this.convertHarnessMonState(state) + ); - // Determine game over state - const isGameOver = data.winnerIndex !== 2n; - const winner = isGameOver ? (Number(data.winnerIndex) as 0 | 1) : undefined; + const isGameOver = harnessState.winnerIndex !== 2; + const winner = isGameOver ? (harnessState.winnerIndex as 0 | 1) : undefined; this._battleState.set({ battleKey, players: [ - { mons: p0Mons, activeMonIndex: p0Active }, - { mons: p1Mons, activeMonIndex: p1Active }, + { mons: p0Mons, activeMonIndex: harnessState.activeMonIndex[0] }, + { mons: p1Mons, activeMonIndex: harnessState.activeMonIndex[1] }, ], - turn: Number(data.turnId), + turn: Number(harnessState.turnId), isGameOver, winner, }); } /** - * Extract mon state from engine data + * Convert harness MonState (with deltas) to client MonBattleState. + * Note: The harness returns stat deltas. For now we return the deltas directly + * since the base stats would need to be tracked separately. */ - private extractMonState(mon: any, state: any): MonBattleState { - // Parse packed stats from mon - const stats = mon?.stats ?? 0n; - + private convertHarnessMonState(state: HarnessMonState): MonBattleState { return { - hp: this.extractStat(stats, 0), - stamina: this.extractStat(stats, 1), - speed: this.extractStat(stats, 2), - attack: this.extractStat(stats, 3), - defense: this.extractStat(stats, 4), - specialAttack: this.extractStat(stats, 5), - specialDefense: this.extractStat(stats, 6), - isKnockedOut: state?.isKnockedOut ?? false, - shouldSkipTurn: state?.shouldSkipTurn ?? false, - type1: MoveType.None, - type2: MoveType.None, + // Deltas represent change from base stats + hp: state.hpDelta, + stamina: state.staminaDelta, + speed: state.speedDelta, + attack: state.attackDelta, + defense: state.defenseDelta, + specialAttack: state.specialAttackDelta, + specialDefense: state.specialDefenseDelta, + isKnockedOut: state.isKnockedOut, + shouldSkipTurn: state.shouldSkipTurn, + type1: MoveType.None, // TODO: Get from mon config + type2: MoveType.None, // TODO: Get from mon config }; } - private extractStat(packedStats: bigint, index: number): bigint { - // Stats are packed as uint32 values - return (packedStats >> BigInt(index * 32)) & 0xffffffffn; + /** + * Get access to the underlying harness for advanced usage. + */ + getHarness(): BattleHarness | null { + return this.harness; } // ------------------------------------------------------------------------- @@ -676,43 +555,6 @@ export class BattleService { this._battleEvents.set([]); this._isExecuting.set(false); this._error.set(null); - } -} - -// ============================================================================= -// MOCK IMPLEMENTATIONS FOR LOCAL SIMULATION -// ============================================================================= - -/** - * Mock RNG Oracle - computes deterministic RNG from both salts - */ -class MockRNGOracle { - getRNG(p0Salt: string, p1Salt: string): bigint { - const encoded = encodeAbiParameters( - [{ type: 'bytes32' }, { type: 'bytes32' }], - [p0Salt as `0x${string}`, p1Salt as `0x${string}`] - ); - return BigInt(keccak256(encoded)); - } -} - -/** - * Mock Validator - allows all moves - */ -class MockValidator { - validateGameStart(): boolean { - return true; - } - - validateSwitch(): boolean { - return true; - } - - validateSpecificMoveSelection(): boolean { - return true; - } - - validateTimeout(): string { - return '0x0000000000000000000000000000000000000000'; + this.harness = null; } } diff --git a/client/lib/metadata-converter.ts b/client/lib/metadata-converter.ts index 84a077e..ed322c4 100644 --- a/client/lib/metadata-converter.ts +++ b/client/lib/metadata-converter.ts @@ -74,20 +74,20 @@ export function resolveConstant( return value; } - // Constant references + // Constant references (constants are bigint from transpiled output) switch (value) { case 'DEFAULT_PRIORITY': - return DEFAULT_CONSTANTS.DEFAULT_PRIORITY; + return Number(DEFAULT_CONSTANTS.DEFAULT_PRIORITY); case 'DEFAULT_STAMINA': - return DEFAULT_CONSTANTS.DEFAULT_STAMINA; + return Number(DEFAULT_CONSTANTS.DEFAULT_STAMINA); case 'DEFAULT_CRIT_RATE': - return DEFAULT_CONSTANTS.DEFAULT_CRIT_RATE; + return Number(DEFAULT_CONSTANTS.DEFAULT_CRIT_RATE); case 'DEFAULT_VOL': - return DEFAULT_CONSTANTS.DEFAULT_VOL; + return Number(DEFAULT_CONSTANTS.DEFAULT_VOL); case 'DEFAULT_ACCURACY': - return DEFAULT_CONSTANTS.DEFAULT_ACCURACY; + return Number(DEFAULT_CONSTANTS.DEFAULT_ACCURACY); case 'SWITCH_PRIORITY': - return DEFAULT_CONSTANTS.SWITCH_PRIORITY; + return Number(DEFAULT_CONSTANTS.SWITCH_PRIORITY); case 'dynamic': // Dynamic values are calculated at runtime, return 0 as placeholder return 0; @@ -141,13 +141,13 @@ export function convertMoveMetadata(raw: RawMoveMetadata): MoveMetadata { inheritsFrom: raw.inheritsFrom, name: raw.name, basePower: resolveConstant(raw.basePower), - staminaCost: resolveConstant(raw.staminaCost, DEFAULT_CONSTANTS.DEFAULT_STAMINA), - accuracy: resolveConstant(raw.accuracy, DEFAULT_CONSTANTS.DEFAULT_ACCURACY), - priority: resolveConstant(raw.priority, DEFAULT_CONSTANTS.DEFAULT_PRIORITY), + staminaCost: resolveConstant(raw.staminaCost, Number(DEFAULT_CONSTANTS.DEFAULT_STAMINA)), + accuracy: resolveConstant(raw.accuracy, Number(DEFAULT_CONSTANTS.DEFAULT_ACCURACY)), + priority: resolveConstant(raw.priority, Number(DEFAULT_CONSTANTS.DEFAULT_PRIORITY)), moveType: resolveMoveType(raw.moveType), moveClass: resolveMoveClass(raw.moveClass), - critRate: resolveConstant(raw.critRate, DEFAULT_CONSTANTS.DEFAULT_CRIT_RATE), - volatility: resolveConstant(raw.volatility, DEFAULT_CONSTANTS.DEFAULT_VOL), + critRate: resolveConstant(raw.critRate, Number(DEFAULT_CONSTANTS.DEFAULT_CRIT_RATE)), + volatility: resolveConstant(raw.volatility, Number(DEFAULT_CONSTANTS.DEFAULT_VOL)), effectAccuracy: resolveConstant(raw.effectAccuracy), effect: raw.effect, extraDataType: resolveExtraDataType(raw.extraDataType), diff --git a/client/lib/types.ts b/client/lib/types.ts index a5b8b10..4373489 100644 --- a/client/lib/types.ts +++ b/client/lib/types.ts @@ -1,53 +1,85 @@ /** - * Type definitions for Chomp battle system metadata - * These types mirror the Solidity enums and structs + * Type definitions for Chomp battle system + * + * Enums and constants are imported directly from the transpiled Solidity output. + * Client-specific types (BattleState, MoveMetadata, etc.) are defined here. */ -// Mirrors src/Enums.sol -export enum MoveType { - Yin = 0, - Yang = 1, - Earth = 2, - Liquid = 3, - Fire = 4, - Metal = 5, - Ice = 6, - Nature = 7, - Lightning = 8, - Mythic = 9, - Air = 10, - Math = 11, - Cyber = 12, - Wild = 13, - Cosmic = 14, - None = 15, -} - -export enum MoveClass { - Physical = 0, - Special = 1, - Self = 2, - Other = 3, -} +// ============================================================================= +// IMPORTS FROM TRANSPILED SOLIDITY +// ============================================================================= + +// Import enums from transpiled Enums.ts (source: src/Enums.sol) +import { + Type, + MoveClass, + ExtraDataType, + MonStateIndexName, + GameStatus, + EffectStep, + EffectRunCondition, + StatBoostType, + StatBoostFlag, +} from '../../transpiler/ts-output/Enums'; + +// Import constants from transpiled Constants.ts (source: src/Constants.sol) +import { + NO_OP_MOVE_INDEX, + SWITCH_MOVE_INDEX, + SWITCH_PRIORITY, + DEFAULT_PRIORITY, + DEFAULT_STAMINA, + DEFAULT_CRIT_RATE, + DEFAULT_VOL, + DEFAULT_ACCURACY, + CRIT_NUM, + CRIT_DENOM, +} from '../../transpiler/ts-output/Constants'; + +// ============================================================================= +// RE-EXPORTS WITH ALIASES +// ============================================================================= + +// Re-export Type as MoveType for backwards compatibility +// (Solidity uses "Type" but "MoveType" is clearer in client context) +export { Type as MoveType }; + +// Re-export other enums directly +export { + MoveClass, + ExtraDataType, + MonStateIndexName, + GameStatus, + EffectStep, + EffectRunCondition, + StatBoostType, + StatBoostFlag, +}; + +// ============================================================================= +// CONSTANTS (re-exported with client-friendly format) +// ============================================================================= -export enum ExtraDataType { - None = 0, - SelfTeamIndex = 1, -} +/** + * Game constants from src/Constants.sol + * Values are bigint to match transpiled output. + */ +export const DEFAULT_CONSTANTS = { + DEFAULT_PRIORITY, + DEFAULT_STAMINA, + DEFAULT_CRIT_RATE, + DEFAULT_VOL, + DEFAULT_ACCURACY, + SWITCH_PRIORITY, + CRIT_NUM, + CRIT_DENOM, + NO_OP_MOVE_INDEX, + SWITCH_MOVE_INDEX, +} as const; -export enum MonStateIndexName { - Hp = 0, - Stamina = 1, - Speed = 2, - Attack = 3, - Defense = 4, - SpecialAttack = 5, - SpecialDefense = 6, - IsKnockedOut = 7, - ShouldSkipTurn = 8, - Type1 = 9, - Type2 = 10, -} +// ============================================================================= +// CLIENT-SPECIFIC TYPES +// ============================================================================= /** * Raw move metadata as extracted from Solidity files @@ -69,7 +101,6 @@ export interface RawMoveMetadata { effectAccuracy: string | number; effect: string | null; extraDataType: string; - // Additional custom fields for non-StandardAttack moves customConstants?: Record; customBehavior?: string; } @@ -87,11 +118,11 @@ export interface MoveMetadata { staminaCost: number; accuracy: number; priority: number; - moveType: MoveType; + moveType: Type; moveClass: MoveClass; critRate: number; - volatility: number; effectAccuracy: number; + volatility: number; effect: string | null; extraDataType: ExtraDataType; customConstants?: Record; @@ -103,7 +134,7 @@ export interface MoveMetadata { */ export interface MonDefinition { name: string; - types: [MoveType, MoveType]; + types: [Type, Type]; baseStats: { hp: number; attack: number; @@ -112,7 +143,7 @@ export interface MonDefinition { specialDefense: number; speed: number; }; - moves: string[]; // Contract names + moves: string[]; ability?: string; } @@ -129,8 +160,8 @@ export interface MonBattleState { specialDefense: bigint; isKnockedOut: boolean; shouldSkipTurn: boolean; - type1: MoveType; - type2: MoveType; + type1: Type; + type2: Type; } /** @@ -185,30 +216,9 @@ export interface BattleEvent { * Configuration for the battle service */ export interface BattleServiceConfig { - /** RPC URL for on-chain interactions (optional for local simulation) */ rpcUrl?: string; - /** Chain ID (default: 1 for mainnet) */ chainId?: number; - /** Engine contract address (required for on-chain mode) */ engineAddress?: `0x${string}`; - /** Type calculator contract address */ typeCalculatorAddress?: `0x${string}`; - /** Enable local simulation mode (default: true) */ localSimulation?: boolean; } - -/** - * Default constant values from src/Constants.sol - */ -export const DEFAULT_CONSTANTS = { - DEFAULT_PRIORITY: 3, - DEFAULT_STAMINA: 5, - DEFAULT_CRIT_RATE: 5, - DEFAULT_VOL: 10, - DEFAULT_ACCURACY: 100, - SWITCH_PRIORITY: 6, - CRIT_NUM: 3, - CRIT_DENOM: 2, - NO_OP_MOVE_INDEX: 126, - SWITCH_MOVE_INDEX: 125, -} as const; diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index afe7acc..ee4085f 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -227,6 +227,74 @@ class Token: 'string': TokenType.STRING, } +# Two-character operators (moved from inside tokenize() for performance) +TWO_CHAR_OPS = { + '++': TokenType.PLUS_PLUS, + '--': TokenType.MINUS_MINUS, + '**': TokenType.STAR_STAR, + '&&': TokenType.AMPERSAND_AMPERSAND, + '||': TokenType.PIPE_PIPE, + '==': TokenType.EQ_EQ, + '!=': TokenType.BANG_EQ, + '<=': TokenType.LT_EQ, + '>=': TokenType.GT_EQ, + '<<': TokenType.LT_LT, + '>>': TokenType.GT_GT, + '+=': TokenType.PLUS_EQ, + '-=': TokenType.MINUS_EQ, + '*=': TokenType.STAR_EQ, + '/=': TokenType.SLASH_EQ, + '%=': TokenType.PERCENT_EQ, + '&=': TokenType.AMPERSAND_EQ, + '|=': TokenType.PIPE_EQ, + '^=': TokenType.CARET_EQ, + '=>': TokenType.ARROW, +} + +# Single-character operators and delimiters (moved from inside tokenize() for performance) +SINGLE_CHAR_OPS = { + '+': TokenType.PLUS, + '-': TokenType.MINUS, + '*': TokenType.STAR, + '/': TokenType.SLASH, + '%': TokenType.PERCENT, + '&': TokenType.AMPERSAND, + '|': TokenType.PIPE, + '^': TokenType.CARET, + '~': TokenType.TILDE, + '<': TokenType.LT, + '>': TokenType.GT, + '!': TokenType.BANG, + '=': TokenType.EQ, + '?': TokenType.QUESTION, + ':': TokenType.COLON, + '(': TokenType.LPAREN, + ')': TokenType.RPAREN, + '{': TokenType.LBRACE, + '}': TokenType.RBRACE, + '[': TokenType.LBRACKET, + ']': TokenType.RBRACKET, + ';': TokenType.SEMICOLON, + ',': TokenType.COMMA, + '.': TokenType.DOT, +} + +# Precompiled regex patterns for Yul transpilation (moved from _transpile_yul_block for performance) +YUL_NORMALIZE_PATTERNS = [ + (re.compile(r':\s*='), ':='), # ": =" -> ":=" + (re.compile(r'\s*\.\s*'), '.'), # " . " -> "." + (re.compile(r'(\w)\s+\('), r'\1('), # "func (" -> "func(" + (re.compile(r'\(\s+'), '('), # "( " -> "(" + (re.compile(r'\s+\)'), ')'), # " )" -> ")" + (re.compile(r'\s+,'), ','), # " ," -> "," + (re.compile(r',\s+'), ', '), # normalize comma spacing +] +YUL_LET_PATTERN = re.compile(r'let\s+(\w+)\s*:=\s*([^{}\n]+?)(?=\s+(?:let|if|for|switch|$)|\s*$)') +YUL_SLOT_PATTERN = re.compile(r'(\w+)\.slot') +YUL_IF_PATTERN = re.compile(r'if\s+([^{]+)\s*\{([^}]*)\}') +YUL_IF_STRIP_PATTERN = re.compile(r'if\s+[^{]+\{[^}]*\}') +YUL_CALL_PATTERN = re.compile(r'\b(sstore|mstore|revert)\s*\(([^)]+)\)') + class Lexer: def __init__(self, source: str): @@ -383,65 +451,17 @@ def tokenize(self) -> List[Token]: self.tokens.append(Token(token_type, three_char, start_line, start_col)) continue - # Two-character operators - two_char_ops = { - '++': TokenType.PLUS_PLUS, - '--': TokenType.MINUS_MINUS, - '**': TokenType.STAR_STAR, - '&&': TokenType.AMPERSAND_AMPERSAND, - '||': TokenType.PIPE_PIPE, - '==': TokenType.EQ_EQ, - '!=': TokenType.BANG_EQ, - '<=': TokenType.LT_EQ, - '>=': TokenType.GT_EQ, - '<<': TokenType.LT_LT, - '>>': TokenType.GT_GT, - '+=': TokenType.PLUS_EQ, - '-=': TokenType.MINUS_EQ, - '*=': TokenType.STAR_EQ, - '/=': TokenType.SLASH_EQ, - '%=': TokenType.PERCENT_EQ, - '&=': TokenType.AMPERSAND_EQ, - '|=': TokenType.PIPE_EQ, - '^=': TokenType.CARET_EQ, - '=>': TokenType.ARROW, - } - if two_char in two_char_ops: + # Two-character operators (using module-level constant) + if two_char in TWO_CHAR_OPS: self.advance() self.advance() - self.tokens.append(Token(two_char_ops[two_char], two_char, start_line, start_col)) + self.tokens.append(Token(TWO_CHAR_OPS[two_char], two_char, start_line, start_col)) continue - # Single-character operators and delimiters - single_char_ops = { - '+': TokenType.PLUS, - '-': TokenType.MINUS, - '*': TokenType.STAR, - '/': TokenType.SLASH, - '%': TokenType.PERCENT, - '&': TokenType.AMPERSAND, - '|': TokenType.PIPE, - '^': TokenType.CARET, - '~': TokenType.TILDE, - '<': TokenType.LT, - '>': TokenType.GT, - '!': TokenType.BANG, - '=': TokenType.EQ, - '?': TokenType.QUESTION, - ':': TokenType.COLON, - '(': TokenType.LPAREN, - ')': TokenType.RPAREN, - '{': TokenType.LBRACE, - '}': TokenType.RBRACE, - '[': TokenType.LBRACKET, - ']': TokenType.RBRACKET, - ';': TokenType.SEMICOLON, - ',': TokenType.COMMA, - '.': TokenType.DOT, - } - if ch in single_char_ops: + # Single-character operators and delimiters (using module-level constant) + if ch in SINGLE_CHAR_OPS: self.advance() - self.tokens.append(Token(single_char_ops[ch], ch, start_line, start_col)) + self.tokens.append(Token(SINGLE_CHAR_OPS[ch], ch, start_line, start_col)) continue # Unknown character - skip @@ -3176,27 +3196,22 @@ def transpile_yul(self, yul_code: str) -> str: def _normalize_yul(self, code: str) -> str: """Normalize Yul code by fixing tokenizer spacing.""" code = ' '.join(code.split()) - code = re.sub(r':\s*=', ':=', code) # ": =" -> ":=" - code = re.sub(r'\s*\.\s*', '.', code) # " . " -> "." - code = re.sub(r'(\w)\s+\(', r'\1(', code) # "func (" -> "func(" - code = re.sub(r'\(\s+', '(', code) # "( " -> "(" - code = re.sub(r'\s+\)', ')', code) # " )" -> ")" - code = re.sub(r'\s+,', ',', code) # " ," -> "," - code = re.sub(r',\s+', ', ', code) # normalize comma spacing + # Use precompiled patterns for better performance + for pattern, replacement in YUL_NORMALIZE_PATTERNS: + code = pattern.sub(replacement, code) return code def _transpile_yul_block(self, code: str, slot_vars: Dict[str, str]) -> str: """Transpile a block of Yul code to TypeScript.""" lines = [] - # Parse let bindings: let var := expr - let_pattern = re.compile(r'let\s+(\w+)\s*:=\s*([^{}\n]+?)(?=\s+(?:let|if|for|switch|$)|\s*$)') - for match in let_pattern.finditer(code): + # Parse let bindings: let var := expr (using precompiled pattern) + for match in YUL_LET_PATTERN.finditer(code): var_name = match.group(1) expr = match.group(2).strip() # Check if this is a .slot access (storage key) - slot_match = re.match(r'(\w+)\.slot', expr) + slot_match = YUL_SLOT_PATTERN.match(expr) if slot_match: storage_var = slot_match.group(1) slot_vars[var_name] = storage_var @@ -3206,9 +3221,8 @@ def _transpile_yul_block(self, code: str, slot_vars: Dict[str, str]) -> str: ts_expr = self._transpile_yul_expr(expr, slot_vars) lines.append(f'let {var_name} = {ts_expr};') - # Parse if statements: if cond { body } - if_pattern = re.compile(r'if\s+([^{]+)\s*\{([^}]*)\}') - for match in if_pattern.finditer(code): + # Parse if statements: if cond { body } (using precompiled pattern) + for match in YUL_IF_PATTERN.finditer(code): cond = match.group(1).strip() body = match.group(2).strip() @@ -3223,9 +3237,8 @@ def _transpile_yul_block(self, code: str, slot_vars: Dict[str, str]) -> str: # Parse standalone function calls (sstore, mstore, etc.) that aren't inside if blocks # Remove if block contents to avoid matching calls inside them - code_without_ifs = re.sub(r'if\s+[^{]+\{[^}]*\}', '', code) - call_pattern = re.compile(r'\b(sstore|mstore|revert)\s*\(([^)]+)\)') - for match in call_pattern.finditer(code_without_ifs): + code_without_ifs = YUL_IF_STRIP_PATTERN.sub('', code) + for match in YUL_CALL_PATTERN.finditer(code_without_ifs): func = match.group(1) args = match.group(2) ts_stmt = self._transpile_yul_call(func, args, slot_vars) @@ -3375,12 +3388,14 @@ def generate_array_literal(self, arr: ArrayLiteral) -> str: def generate_literal(self, lit: Literal) -> str: """Generate literal.""" if lit.kind == 'number': - # For large numbers (> 2^53), use string to avoid precision loss - # JavaScript numbers lose precision beyond 2^53 (about 16 digits) - if len(lit.value.replace('_', '')) > 15: + # Use bigint literal syntax (Xn) which is more efficient than BigInt(X) + # For large numbers (> 2^53), use BigInt("X") to avoid precision loss + clean_value = lit.value.replace('_', '') + if len(clean_value) > 15: return f'BigInt("{lit.value}")' - return f'BigInt({lit.value})' + return f'{lit.value}n' elif lit.kind == 'hex': + # Hex literals: 0x... -> BigInt("0x...") return f'BigInt("{lit.value}")' elif lit.kind == 'string': return lit.value # Already has quotes @@ -3561,6 +3576,9 @@ def generate_function_call(self, call: FunctionCall) -> str: # For simple identifiers that are likely already bigint, pass through if call.arguments and isinstance(call.arguments[0], Identifier): return args + # Use efficient bigint literal syntax for simple numbers + if args.isdigit(): + return f'{args}n' return f'BigInt({args})' elif name == 'address': # Handle address literals like address(0xdead)