diff --git a/.gitignore b/.gitignore index 2179e68a..480ab16d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ +dist/ *.tsbuildinfo coverage/ diff --git a/bin/start b/bin/start index 86769399..e9f2c0a4 100755 --- a/bin/start +++ b/bin/start @@ -10,9 +10,10 @@ else fi # Run the commands with concurrently -concurrently --names=format,pointers,web,tests \ +concurrently --names=format,pointers,bugc,web,tests \ "cd ./packages/format && yarn watch" \ "cd ./packages/pointers && yarn watch" \ + "cd ./packages/bugc && yarn watch" \ "cd ./packages/web && yarn start $DOCUSAURUS_NO_OPEN" \ "sleep 5 && yarn test --ui --watch --coverage $VITEST_NO_OPEN" diff --git a/package.json b/package.json index 50be9566..c58ffd69 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packages/*" ], "scripts": { - "build": "tsc --build packages/format packages/pointers", + "build": "tsc --build packages/format packages/pointers packages/bugc", "bundle": "tsx ./bin/bundle-schema.ts", "test": "vitest", "test:coverage": "vitest run --coverage", diff --git a/packages/bugc/.ignore b/packages/bugc/.ignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/bugc/.ignore @@ -0,0 +1 @@ +dist diff --git a/packages/bugc/bin/bugc.ts b/packages/bugc/bin/bugc.ts new file mode 100755 index 00000000..4049fde1 --- /dev/null +++ b/packages/bugc/bin/bugc.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env tsx +/* eslint-disable no-console */ + +/** + * BUG compiler CLI + */ + +import { handleCompileCommand } from "#cli"; + +// Parse command line arguments +const args = process.argv.slice(2); + +// Show help if no arguments or help flag +if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + showHelp(); + process.exit(0); +} + +// Run the compiler +handleCompileCommand(process.argv); + +function showHelp(): void { + console.log(`bugc - The BUG language compiler + +Usage: bugc [options] + +Options: + -s, --stop-after Stop compilation after phase (ast, ir, bytecode, debug) + Default: bytecode + -O, --optimize Set optimization level (0-3) + Default: 0 + -f, --format Output format (text, json) + Default: text + -o, --output Write output to file instead of stdout + -p, --pretty Pretty-print JSON output + -d, --disassembly Show bytecode disassembly + --validate Validate output (IR or debug info) + --stats Show IR statistics + --show-both Show both unoptimized and optimized IR + -h, --help Show this help message + +Examples: + bugc program.bug # Compile to bytecode + bugc -s ast program.bug # Show AST only + bugc -s ir -O 2 program.bug # Show optimized IR + bugc -s bytecode -d program.bug # Show bytecode with disassembly + bugc -s debug -o debug.json program.bug # Generate debug info + +Phase descriptions: + ast Parse source and output abstract syntax tree + ir Compile to intermediate representation + bytecode Compile to EVM bytecode (default) + debug Generate ethdebug/format debug information`); +} diff --git a/packages/bugc/examples/README.md b/packages/bugc/examples/README.md new file mode 100644 index 00000000..197a50db --- /dev/null +++ b/packages/bugc/examples/README.md @@ -0,0 +1,145 @@ +# BUG Language Examples + +This directory contains example programs demonstrating various BUG language +features, organized by complexity. + +## Directory Structure + +``` +examples/ + basic/ # Simple single-concept examples + intermediate/ # Multiple concepts combined + advanced/ # Complex real-world patterns + optimizations/ # Compiler optimization demos + wip/ # Work-in-progress (not yet compiling) +``` + +## Running Tests + +All examples are automatically tested by the test suite: + +```bash +yarn test examples +``` + +## Test Annotations + +Examples can include special comments to control test behavior: + +```bug +// @wip - Skip test (work in progress) +// @skip Reason - Skip test with reason +// @expect-parse-error - Expected to fail parsing +// @expect-typecheck-error - Expected to fail typechecking +// @expect-ir-error - Expected to fail IR generation +// @expect-bytecode-error - Expected to fail bytecode generation +``` + +## Examples by Category + +### Basic + +Simple examples demonstrating single language features: + +- `minimal.bug` - Simplest possible BUG contract +- `conditionals.bug` - If/else statements +- `functions.bug` - Function definitions and calls +- `variables.bug` - Variable types and declarations +- `array-length.bug` - Array length property +- `constructor-init.bug` - Constructor initialization + +### Intermediate + +Examples combining multiple language features: + +- `arrays.bug` - Array operations with loops +- `mappings.bug` - Mapping access patterns +- `scopes.bug` - Variable scoping and shadowing +- `slices.bug` - Byte slice operations +- `calldata.bug` - Calldata access via msg.data +- `owner-counter.bug` - Owner checks with counters +- `storage-arrays.bug` - Dynamic arrays in storage +- `memory-arrays.bug` - Memory array allocation +- `internal-functions.bug` - Internal function calls + +### Advanced + +Complex examples demonstrating real-world patterns: + +- `nested-mappings.bug` - Mapping of mappings +- `nested-arrays.bug` - Multi-dimensional arrays +- `nested-structs.bug` - Nested struct storage +- `voting-system.bug` - Realistic voting contract +- `token-registry.bug` - Token with function selectors + +### Optimizations + +Examples showcasing compiler optimizations: + +- `cse.bug` - Common subexpression elimination +- `cse-simple.bug` - Simple CSE example +- `constant-folding.bug` - Compile-time constant evaluation + +### Work in Progress + +Features not yet fully implemented: + +- `transient-storage.bug` - TSTORE/TLOAD opcodes (no syntax) +- `returndata.bug` - Return data access + +--- + +## Storage Access Patterns + +The BUG language has specific rules about how storage variables can be +accessed and modified. + +### Key Concept: Storage References vs Local Copies + +In BUG, when you read a complex type (struct, array, or mapping) from storage +into a local variable, you get a **copy** of the data, not a reference. This +means: + +- ✅ **Reading** from local copies works fine +- ❌ **Writing** to local copies does NOT update storage + +### Correct Patterns + +```bug +// Direct storage access - changes are persisted +accounts[user].balance = 1000; +votes[proposalId][0].amount = 100; +allowances[owner][spender] = 500; + +// Reading into locals is fine +let currentBalance = accounts[user].balance; +let voteCount = votes[proposalId][0].amount; +``` + +### Incorrect Patterns + +```bug +// ❌ WRONG: Changes to local copies don't persist +let userAccount = accounts[user]; +userAccount.balance = 1000; // This doesn't update storage! + +// ❌ WRONG: Same issue with array elements +let firstVote = votes[proposalId][0]; +firstVote.amount = 200; // This doesn't update storage! +``` + +### Workaround + +If you need to perform multiple operations on a storage struct, access each +field directly: + +```bug +// Instead of: +let account = accounts[user]; +account.balance = account.balance + 100; +account.isActive = true; + +// Do this: +accounts[user].balance = accounts[user].balance + 100; +accounts[user].isActive = true; +``` diff --git a/packages/bugc/examples/advanced/memory-to-storage.bug b/packages/bugc/examples/advanced/memory-to-storage.bug new file mode 100644 index 00000000..42a7ac46 --- /dev/null +++ b/packages/bugc/examples/advanced/memory-to-storage.bug @@ -0,0 +1,37 @@ +name MemoryToStorageCopy; + +// Example 13: Memory to Storage Copy +// +// Expected IR: +// // Read from memory +// %id = read location="memory", offset=%mem_user, length=32 +// %name = read location="memory", offset=add(%mem_user, 32), length=32 +// +// // Write to storage (consecutive slots) +// write location="storage", slot=30, offset=0, length=32, value=%id +// write location="storage", slot=31, offset=0, length=32, value=%name +// +// EVM Strategy: MLOAD fields from memory, SSTORE to consecutive slots +// +// Key Insight: Mixed locations (e.g., memory→storage copies) need explicit read/write pairs + +define { + struct User { + id: uint256; + name: bytes32; + }; +} + +storage { + [30] stored_user: User; +} + +code { + // Memory struct values that will be copied to storage + let id: uint256 = 999; + let name: bytes32 = 0x4a6f686e00000000000000000000000000000000000000000000000000000000; + + // Copy to storage struct (would be field by field in IR) + stored_user.id = id; + stored_user.name = name; +} \ No newline at end of file diff --git a/packages/bugc/examples/advanced/nested-arrays.bug b/packages/bugc/examples/advanced/nested-arrays.bug new file mode 100644 index 00000000..8900faf7 --- /dev/null +++ b/packages/bugc/examples/advanced/nested-arrays.bug @@ -0,0 +1,31 @@ +name NestedArrays; + +// Example 7: Nested Arrays +// +// Expected IR: +// // First dimension: matrix[i] +// %outer_base = compute_slot kind="array", base=15 +// %inner_array_slot = add %outer_base, %i +// +// // Second dimension: matrix[i][j] +// %inner_base = compute_slot kind="array", base=%inner_array_slot +// %element_slot = add %inner_base, %j +// +// write location="storage", slot=%element_slot, offset=0, length=32, value=%value +// %elem = read location="storage", slot=%element_slot, offset=0, length=32 +// +// EVM Strategy: Double keccak256 for nested dynamic arrays +// +// Key Insight: Dynamic arrays in storage use keccak hashing at each nesting level + +storage { + [15] matrix: array>; +} + +code { + let i = 2; + let j = 5; + let value = 999; + matrix[i][j] = value; + let elem = matrix[i][j]; +} \ No newline at end of file diff --git a/packages/bugc/examples/advanced/nested-mappings.bug b/packages/bugc/examples/advanced/nested-mappings.bug new file mode 100644 index 00000000..d3a9cbbc --- /dev/null +++ b/packages/bugc/examples/advanced/nested-mappings.bug @@ -0,0 +1,23 @@ +name NestedMappings; + +// Example 4: Nested Mappings +// +// Expected IR: +// %slot1 = slot[10].mapping[%owner] +// %slot2 = slot[%slot1].mapping[%spender] +// storage[%slot2*] = %amount +// +// EVM Strategy: Double keccak256 hashing for nested mappings +// +// Key Insight: Each mapping level requires a separate compute_slot operation + +storage { + [10] approvals: mapping>; +} + +code { + let owner = msg.sender; + let spender = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; + let amount = 1000; + approvals[owner][spender] = amount; +} diff --git a/packages/bugc/examples/advanced/nested-structs.bug b/packages/bugc/examples/advanced/nested-structs.bug new file mode 100644 index 00000000..01b0add8 --- /dev/null +++ b/packages/bugc/examples/advanced/nested-structs.bug @@ -0,0 +1,39 @@ +name NestedStructsStorage; + +// Example 9: Nested Structs in Storage +// +// Expected IR: +// // CEO salary is at slot 4 (base slot 2 + struct layout) +// write location="storage", slot=4, offset=0, length=32, value=1000000 +// +// // CEO address is at slot 3, first 20 bytes +// %ceo_addr = read location="storage", slot=3, offset=0, length=20 +// +// EVM Strategy: Calculate nested struct slot offsets based on layout +// +// Key Insight: Nested structures need recursive offset computation, +// especially when crossing slot boundaries + +define { + struct CEO { + addr: address; // offset 0-19 + salary: uint256; // next slot + }; + + struct Company { + name: string; // slot 0 (relative) + founded: uint64; // slot 1 (relative) + ceo: CEO; // slots 2-3 (relative) + }; +} + +storage { + [2] company: Company; +} + +code { + company.ceo.salary = 1000000; + company.founded = 1988; + let ceo_addr = company.ceo.addr; + let x = ceo_addr; +} diff --git a/packages/bugc/examples/advanced/token-registry.bug b/packages/bugc/examples/advanced/token-registry.bug new file mode 100644 index 00000000..205d6682 --- /dev/null +++ b/packages/bugc/examples/advanced/token-registry.bug @@ -0,0 +1,328 @@ +// @wip +name TokenRegistry; + +// This example demonstrates: +// 1. A create {} block for constructor/initialization code +// 2. User-defined functions for modular code organization +// 3. Realistic entrypoints using calldata parsing with 4-byte function selectors +// 4. Full selector comparison (all 4 bytes) +// 5. Selector computation that should be optimized out by the compiler +// 6. Calldata parameter validation and amount decoding +// +// Function selectors (first 4 bytes of keccak256 hash): +// - transfer(address,uint256): 0xa9059cbb +// - mint(uint256): 0xa0712d68 +// - pause(): 0x8456cb59 +// - unpause(): 0x3f4ba83a + +define { + struct Token { + totalSupply: uint256; + decimals: uint8; + owner: address; + isPaused: bool; + mintingFinished: bool; + }; + + // Helper function to check if caller is admin + function isAdmin(sender: address) -> bool { + return admins[sender]; + }; + + // Helper function to check if caller is owner + function isOwner(sender: address) -> bool { + return sender == token.owner; + }; + + // Transfer tokens from sender to recipient + function doTransfer(from: address, to: address, amount: uint256) -> bool { + // Check frozen accounts + if (frozen[from] || frozen[to]) { + return false; + } + + // Check transfer limit + if (amount > maxTransferAmount) { + return false; + } + + let senderBalance = balances[from]; + let totalAmount = amount + transferFee; + + // Check sufficient balance + if (senderBalance < totalAmount) { + return false; + } + + // Execute transfer + balances[from] = senderBalance - totalAmount; + balances[to] = balances[to] + amount; + balances[token.owner] = balances[token.owner] + transferFee; + + return true; + }; + + // Mint new tokens + function doMint(recipient: address, amount: uint256) -> bool { + // Check minting not finished + if (token.mintingFinished) { + return false; + } + + // Update total supply and balance + token.totalSupply = token.totalSupply + amount; + balances[recipient] = balances[recipient] + amount; + + return true; + }; + + // Decode address from calldata (bytes 4-35) + function decodeAddress(offset: uint256) -> address { + return msg.data[offset:offset+32] as address; + }; + + // Decode uint256 from calldata (32 bytes) + function decodeUint256(offset: uint256) -> uint256 { + return msg.data[offset:offset+32] as uint256; + }; + +} + +storage { + // Token data + [0] token: Token; + [1] balances: mapping; + + // Access control + [2] admins: mapping; + [3] frozen: mapping; + + // Parameters + [4] transferFee: uint256; + [5] maxTransferAmount: uint256; +} + +create { + // Initialize token with 1M tokens (18 decimals) + token.totalSupply = 1000000000000000000000000; + token.decimals = 18; + token.owner = msg.sender; + token.isPaused = false; + token.mintingFinished = false; + + // Give initial supply to owner + balances[msg.sender] = token.totalSupply; + + // Set up initial admin + admins[msg.sender] = true; + + // Set initial parameters + transferFee = 1000000000000000; // 0.001 tokens + maxTransferAmount = 10000000000000000000000; // 10k tokens +} + +code { + // Minimum calldata size check + if (msg.data.length < 4) { + return; + } + + // Extract 4-byte function selector from calldata + let selector = msg.data[0:4]; + + // Compute function selectors from signatures (optimizer should evaluate these as constants) + // These keccak256 computations should be optimized out to constants + let TRANSFER_SELECTOR = keccak256("transfer(address,uint256)")[0:4]; + let MINT_SELECTOR = keccak256("mint(uint256)")[0:4]; + let PAUSE_SELECTOR = keccak256("pause()")[0:4]; + let UNPAUSE_SELECTOR = keccak256("unpause()")[0:4]; + + // Check if token is paused for non-admin operations + if (token.isPaused && !isAdmin(msg.sender)) { + // Only admin functions allowed when paused + if (selector != PAUSE_SELECTOR && selector != UNPAUSE_SELECTOR) { + return; + } + } + + // transfer(address,uint256) + if (selector == TRANSFER_SELECTOR) { + // Ensure we have enough calldata (4 + 32 + 32 = 68 bytes) + if (msg.data.length < 68) { + return; + } + + // Decode parameters + let to = decodeAddress(4); + let amount = decodeUint256(36); + + // Execute transfer + doTransfer(msg.sender, to, amount); + return; + } + + // mint(uint256) + if (selector == MINT_SELECTOR) { + // Only admins can mint + if (!isAdmin(msg.sender)) { + return; + } + + // Ensure we have enough calldata (4 + 32 = 36 bytes) + if (msg.data.length < 36) { + return; + } + + // Decode amount parameter + let mintAmount = decodeUint256(4); + + // Execute mint + doMint(msg.sender, mintAmount); + return; + } + + // pause() + if (selector == PAUSE_SELECTOR) { + // Only owner can pause + if (!isOwner(msg.sender)) { + return; + } + + token.isPaused = true; + return; + } + + // unpause() + if (selector == UNPAUSE_SELECTOR) { + // Only owner can unpause + if (!isOwner(msg.sender)) { + return; + } + + token.isPaused = false; + return; + } + + // Additional selectors for completeness + // finishMinting() + if (selector == keccak256("finishMinting()")[0:4]) { + if (!isOwner(msg.sender)) { + return; + } + + token.mintingFinished = true; + return; + } + + // addAdmin(address) + if (selector == keccak256("addAdmin(address)")[0:4]) { + if (!isOwner(msg.sender)) { + return; + } + + // Ensure we have enough calldata + if (msg.data.length < 36) { + return; + } + + // Decode address parameter + let newAdmin = decodeAddress(4); + + admins[newAdmin] = true; + return; + } + + // freezeAccount(address) + if (selector == keccak256("freezeAccount(address)")[0:4]) { + if (!isAdmin(msg.sender)) { + return; + } + + // Ensure we have enough calldata + if (msg.data.length < 36) { + return; + } + + // Decode address parameter + let account = decodeAddress(4); + + frozen[account] = true; + return; + } + + // unfreezeAccount(address) + if (selector == keccak256("unfreezeAccount(address)")[0:4]) { + if (!isAdmin(msg.sender)) { + return; + } + + // Ensure we have enough calldata + if (msg.data.length < 36) { + return; + } + + // Decode address parameter + let account = decodeAddress(4); + + frozen[account] = false; + return; + } + + // setTransferFee(uint256) + if (selector == keccak256("setTransferFee(uint256)")[0:4]) { + if (!isOwner(msg.sender)) { + return; + } + + // Ensure we have enough calldata + if (msg.data.length < 36) { + return; + } + + // Decode uint256 parameter + let newFee = decodeUint256(4); + + transferFee = newFee; + return; + } + + // setMaxTransfer(uint256) + if (selector == keccak256("setMaxTransfer(uint256)")[0:4]) { + if (!isOwner(msg.sender)) { + return; + } + + // Ensure we have enough calldata + if (msg.data.length < 36) { + return; + } + + // Decode uint256 parameter + let newMax = decodeUint256(4); + + maxTransferAmount = newMax; + return; + } + + // emergencyWithdraw() + if (selector == keccak256("emergencyWithdraw()")[0:4]) { + // Emergency function - owner only, sends all owner balance + if (!isOwner(msg.sender)) { + return; + } + + let ownerBalance = balances[token.owner]; + if (ownerBalance > 0) { + balances[token.owner] = 0; + // In real contract, would transfer ETH/tokens out + } + + // Also pause the contract + token.isPaused = true; + return; + } + + // Default: revert for unknown selectors + // In real EVM, this would revert the transaction + return; +} \ No newline at end of file diff --git a/packages/bugc/examples/advanced/voting-system.bug b/packages/bugc/examples/advanced/voting-system.bug new file mode 100644 index 00000000..a91e8708 --- /dev/null +++ b/packages/bugc/examples/advanced/voting-system.bug @@ -0,0 +1,165 @@ +// @wip +name VotingSystem; + +// This example demonstrates: +// 1. Complex data structures with nested mappings and arrays +// 2. Realistic calldata parsing with slice syntax for function dispatch +// 3. State management for a decentralized voting system +// +// Function selectors (first 4 bytes of keccak256 hash): +// - createProposal(): 0x9cb5dfac +// - vote(uint256): 0x0121b93f +// - executeProposal(uint256): 0x28bf0f4e +// - setQuorum(uint256): 0x2c79b42a + +define { + struct Vote { + supporter: address; + weight: uint256; + timestamp: uint256; + }; + + struct Proposal { + id: uint256; + proposer: address; + voteCount: uint256; + executed: bool; + deadline: uint256; + }; +} + +storage { + [0] proposals: mapping; + [1] votes: mapping>; // proposalId -> votes array + [2] userVotes: mapping>; // user -> proposalId -> voted + [3] votingPower: mapping; + [4] proposalCount: uint256; + [5] quorum: uint256; + [6] owner: address; + [7] paused: bool; +} + +create { + // Initialize voting system + owner = msg.sender; + proposalCount = 0; + quorum = 50; // Default quorum of 50 votes + paused = false; + + // Give owner initial voting power + votingPower[msg.sender] = 10; +} + +code { + // Check if system is paused + if (paused) { + return; + } + + // Initialize voting power for new users + if (votingPower[msg.sender] == 0) { + votingPower[msg.sender] = 1; + } + + // Extract 4-byte function selector from calldata + let functionSelector = msg.data[0:4]; + + // createProposal() - selector starts with 0x9c + if (functionSelector[0] == 156 && msg.value >= 1000000000000000) { + // 0.001 ether proposal fee required + proposalCount = proposalCount + 1; + + proposals[proposalCount].id = proposalCount; + proposals[proposalCount].proposer = msg.sender; + proposals[proposalCount].voteCount = 0; + proposals[proposalCount].executed = false; + proposals[proposalCount].deadline = block.timestamp + 604800; // 7 days + return; + } + + // vote(uint256 proposalId) - selector starts with 0x01 + if (functionSelector[0] == 1) { + // Parse proposal ID parameter + let proposalIdParam = msg.data[4:36]; // bytes 4-35: proposal ID + + // For demo, vote on proposal 1 (real implementation would decode proposalIdParam) + let targetProposal = 1; + + if (targetProposal <= proposalCount && targetProposal > 0) { + // Check if proposal exists and is active + if (!proposals[targetProposal].executed) { + if (block.timestamp <= proposals[targetProposal].deadline) { + // Check if user hasn't voted yet + if (!userVotes[msg.sender][targetProposal]) { + // Find next empty vote slot + let voteIndex = 0; + for (let i = 0; i < 100; i = i + 1) { + if (votes[targetProposal][i].supporter == 0x0000000000000000000000000000000000000000) { + voteIndex = i; + break; + } + } + + // Record the vote + votes[targetProposal][voteIndex].supporter = msg.sender; + votes[targetProposal][voteIndex].weight = votingPower[msg.sender]; + votes[targetProposal][voteIndex].timestamp = block.timestamp; + + // Update vote count + proposals[targetProposal].voteCount = proposals[targetProposal].voteCount + votingPower[msg.sender]; + + // Mark user as voted + userVotes[msg.sender][targetProposal] = true; + } + } + } + } + return; + } + + // executeProposal(uint256 proposalId) - selector starts with 0x28 + if (functionSelector[0] == 40) { + // Parse proposal ID parameter + let proposalIdParam = msg.data[4:36]; // bytes 4-35: proposal ID + + // For demo, execute proposal 1 + let executeProposal = 1; + + if (executeProposal <= proposalCount && executeProposal > 0) { + if (!proposals[executeProposal].executed) { + if (proposals[executeProposal].voteCount >= quorum) { + proposals[executeProposal].executed = true; + + // Reward proposer + let proposerAddr = proposals[executeProposal].proposer; + votingPower[proposerAddr] = votingPower[proposerAddr] + 10; + } + } + } + return; + } + + // setQuorum(uint256 newQuorum) - selector starts with 0x2c (owner only) + if (functionSelector[0] == 44 && msg.sender == owner) { + // Parse new quorum parameter + let quorumParam = msg.data[4:36]; // bytes 4-35: new quorum value + + // For demo, set quorum to 100 + quorum = 100; + return; + } + + // emergencyPause() - selector starts with 0xff (owner only) + if (functionSelector[0] == 255 && msg.sender == owner) { + paused = true; + return; + } + + // emergencyUnpause() - selector starts with 0xfe (owner only) + if (functionSelector[0] == 254 && msg.sender == owner) { + paused = false; + return; + } + + return; +} \ No newline at end of file diff --git a/packages/bugc/examples/basic/array-length.bug b/packages/bugc/examples/basic/array-length.bug new file mode 100644 index 00000000..858ae8db --- /dev/null +++ b/packages/bugc/examples/basic/array-length.bug @@ -0,0 +1,34 @@ +name ArrayLength; + +storage { + [0] fixedArray: array; + [10] arraySize: uint256; + [11] dataSize: uint256; + [12] isLarge: bool; +} + +create { + // Get length of fixed-size array + arraySize = fixedArray.length; // Should be constant 10 + + // Get length of msg.data (dynamic bytes) + dataSize = msg.data.length; + + // Use length in conditional + if (msg.data.length > 4) { + isLarge = true; + } else { + isLarge = false; + } + + /*@test array-length-after-deploy + variables: + arraySize: + pointer: { location: storage, slot: 10 } + value: 10 + */ + return; +} + +code { +} diff --git a/packages/bugc/examples/basic/array-values.bug b/packages/bugc/examples/basic/array-values.bug new file mode 100644 index 00000000..e5d91a20 --- /dev/null +++ b/packages/bugc/examples/basic/array-values.bug @@ -0,0 +1,20 @@ +name ArrayValues; + +storage { + [0] nums: array; +} + +create { + nums = [100, 200, 300]; + + /**@test array-values-check + * variables: + * nums: + * values: + * array-length: 3 + * element: [100, 200, 300] + */ +} + +code { +} diff --git a/packages/bugc/examples/basic/conditionals.bug b/packages/bugc/examples/basic/conditionals.bug new file mode 100644 index 00000000..101c6d04 --- /dev/null +++ b/packages/bugc/examples/basic/conditionals.bug @@ -0,0 +1,73 @@ +name Conditionals; + +storage { + [0] count: uint256; + [1] threshold: uint256; + [2] isActive: bool; + [3] owner: address; + [4] lastSender: address; + [5] permission: uint8; +} + +create { + // Simple if statement + if (count == 0) { + count = 1; + } + + // If-else statement + if (msg.sender == owner) { + permission = 255; // Full permissions + } else { + permission = 1; // Basic permissions + } + + // Nested conditions + if (isActive) { + if (count < threshold) { + count = count + 1; + } else { + isActive = false; + count = 0; + } + } + + // Complex boolean expressions + if (msg.sender != owner && count > 10) { + return; // Early exit for non-owners when count is high + } + + // Multiple conditions with logical operators + if (permission > 100 || (isActive && msg.sender == owner)) { + threshold = threshold * 2; + } + + // Comparison operators + if (msg.value >= 1000000000000000000) { // 1 ether in wei + count = count + 10; + } else { + if (msg.value >= 100000000000000000) { // 0.1 ether + count = count + 1; + } + } + + // Store last sender + lastSender = msg.sender; + + /*@test count-after-deploy + variables: + count: + pointer: { location: storage, slot: 0 } + value: 1 + */ + + /*@test permission-pointer + variables: + permission: + pointer: { location: storage, slot: 5, length: 1 } + */ + return; +} + +code { +} diff --git a/packages/bugc/examples/basic/constructor-init.bug b/packages/bugc/examples/basic/constructor-init.bug new file mode 100644 index 00000000..fa942928 --- /dev/null +++ b/packages/bugc/examples/basic/constructor-init.bug @@ -0,0 +1,16 @@ +name ConstructorArray; + +storage { + [0] items: array; +} + +create { + items[0] = 0x101 as uint256; + items[1] = 0x102 as uint256; + items[2] = 0x103 as uint256; +} + +code { + // Access items to make it available for testing + let first = items[0]; +} diff --git a/packages/bugc/examples/basic/functions.bug b/packages/bugc/examples/basic/functions.bug new file mode 100644 index 00000000..62b98237 --- /dev/null +++ b/packages/bugc/examples/basic/functions.bug @@ -0,0 +1,42 @@ +name SimpleFunctions; + +define { + // Simple arithmetic function + function add(a: uint256, b: uint256) -> uint256 { + return a + b; + }; + + // Function that calls another function + function addThree(x: uint256, y: uint256, z: uint256) -> uint256 { + let sum1 = add(x, y); + let sum2 = add(sum1, z); + return sum2; + }; +} + +storage { + [0] result: uint256; +} + +code { + // Test function calls + let a = 10; + let b = 20; + let c = 30; + + // Call add function + let sum = add(a, b); + + // Call addThree function + let total = addThree(a, b, c); + + // Store result + result = total; + /*@test function-call-result + fails: stack underflow in internal function calls + variables: + result: + pointer: { location: storage, slot: 0 } + value: 60 + */ +} diff --git a/packages/bugc/examples/basic/minimal.bug b/packages/bugc/examples/basic/minimal.bug new file mode 100644 index 00000000..2f605193 --- /dev/null +++ b/packages/bugc/examples/basic/minimal.bug @@ -0,0 +1,18 @@ +name Minimal; + +storage { + [0] value: uint256; +} + +create { + value = 1; + /**@test value-initialized + * variables: + * value: + * pointer: { location: storage, slot: 0 } + * value: 1 + */ +} + +code { +} diff --git a/packages/bugc/examples/basic/variables.bug b/packages/bugc/examples/basic/variables.bug new file mode 100644 index 00000000..fd973925 --- /dev/null +++ b/packages/bugc/examples/basic/variables.bug @@ -0,0 +1,20 @@ +// @wip +name TypedLocals; + +create { + // Test explicit type annotations + let x: uint256 = 42; + let y: uint128 = 100; + let addr: address = 0x1234567890123456789012345678901234567890; + let flag: bool = true; + + // Test bytes type annotations (using supported types) + let small: bytes4 = 0xABCDEF12; + let medium: bytes8 = 0xDEADBEEFCAFEBABE; + let large: bytes32 = 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20; + let dynamic: bytes = large; + let sliced = dynamic[3:7]; + + // Test that dynamic bytes work with explicit annotation + let data: bytes = 0x11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111; +} diff --git a/packages/bugc/examples/intermediate/arrays.bug b/packages/bugc/examples/intermediate/arrays.bug new file mode 100644 index 00000000..a70820d0 --- /dev/null +++ b/packages/bugc/examples/intermediate/arrays.bug @@ -0,0 +1,100 @@ +name ArraysAndLoops; + +storage { + [0] numbers: array; + [1] sum: uint256; + [2] max: uint256; + [3] count: uint256; + [4] found: bool; + [5] searchValue: uint256; +} + +code { + // Initialize array with values + for (let i = 0; i < 10; i = i + 1) { + numbers[i] = i * i; // Store squares: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 + } + + // Calculate sum of all elements + sum = 0; + /*@test sum-initialized + after: deploy + variables: + sum: + pointer: { location: storage, slot: 1 } + value: 0 + */ + for (let i = 0; i < 10; i = i + 1) { + sum = sum + numbers[i]; + } + + // Find maximum value + max = numbers[0]; + for (let i = 1; i < 10; i = i + 1) { + if (numbers[i] > max) { + max = numbers[i]; + } + } + + // Search for a specific value with early exit + searchValue = 25; + found = false; + for (let i = 0; i < 10; i = i + 1) { + if (numbers[i] == searchValue) { + found = true; + break; // Exit loop early + } + } + + // Count elements greater than a threshold + count = 0; + for (let i = 0; i < 10; i = i + 1) { + if (numbers[i] > 20) { + count = count + 1; + } + } + + // Nested loop example (bubble sort first 5 elements) + for (let i = 0; i < 5; i = i + 1) { + for (let j = 0; j < 4 - i; j = j + 1) { + if (numbers[j] > numbers[j + 1]) { + // Swap elements + let temp = numbers[j]; + numbers[j] = numbers[j + 1]; + numbers[j + 1] = temp; + } + } + } + + // Count elements below 50 + let idx = 0; + for (let k = 0; k < 10; k = k + 1) { + if (numbers[k] < 50) { + idx = idx + 1; + } + } + count = idx; // Store count of elements < 50 + /*@test final-sum + fails: stack overflow during call - needs investigation + variables: + sum: + pointer: { location: storage, slot: 1 } + value: 285 + */ + /*@test final-max + fails: stack overflow during call - needs investigation + variables: + max: + pointer: { location: storage, slot: 2 } + value: 81 + */ + /*@test final-count + fails: stack overflow during call - needs investigation + variables: + count: + pointer: { location: storage, slot: 3 } + value: 8 + */ + + return; +} diff --git a/packages/bugc/examples/intermediate/bytes.bug b/packages/bugc/examples/intermediate/bytes.bug new file mode 100644 index 00000000..728440e5 --- /dev/null +++ b/packages/bugc/examples/intermediate/bytes.bug @@ -0,0 +1,10 @@ +// @wip +name Bytes; + +create { + let small = 0xAB; // 1 byte + let medium = 0xDEADBEEF; // 4 bytes + let large = 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20; // 32 bytes + let toolarge = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; // 52 bytes + +} diff --git a/packages/bugc/examples/intermediate/calldata.bug b/packages/bugc/examples/intermediate/calldata.bug new file mode 100644 index 00000000..6870cbca --- /dev/null +++ b/packages/bugc/examples/intermediate/calldata.bug @@ -0,0 +1,16 @@ +name CalldataAccess; + +// Example 14: Calldata Access +// +// Expected IR: +// %param1 = calldata[4*] +// %param2 = calldata[36*] +// +// EVM Strategy: CALLDATALOAD at respective offsets +// +// Note: Calldata is read-only, standard ABI encoding starts at offset 4 (after selector) + +code { + let param1 = msg.data[4:36]; // First parameter + let param2 = msg.data[36:68]; // Second parameter +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/internal-functions.bug b/packages/bugc/examples/intermediate/internal-functions.bug new file mode 100644 index 00000000..1ca03043 --- /dev/null +++ b/packages/bugc/examples/intermediate/internal-functions.bug @@ -0,0 +1,51 @@ +name InternalFunctions; + +// Example 10: Internal Function Calls +// +// Note: As of the recent IR redesign, function calls are now block terminators, +// not regular instructions. This ensures explicit control flow and proper SSA form. +// +// Expected IR (simplified): +// function main() { +// entry: +// %t0 = const 10 +// %t1 = const 20 +// %t2 = const 30 +// %result = call addThree(%t0, %t1, %t2) -> call_cont_1 +// call_cont_1: +// return void +// } +// +// function addThree(^x, ^y, ^z) -> uint256 { +// entry: +// %sum1 = call add(^x, ^y) -> call_cont_1 +// call_cont_1: +// %sum2 = call add(%sum1, ^z) -> call_cont_2 +// call_cont_2: +// return %sum2 +// } +// +// EVM Strategy: +// - Internal functions become labeled sections in bytecode +// - Parameters passed via stack manipulation +// - JUMP to function label, JUMP back after return +// - Could be inlined by optimizer for small functions +// +// Key Insight: Call terminators split blocks at call sites with explicit +// continuation blocks + +define { + function add(a: uint256, b: uint256) -> uint256 { + return a + b; + }; + + function addThree(x: uint256, y: uint256, z: uint256) -> uint256 { + let sum1 = add(x, y); + let sum2 = add(sum1, z); + return sum2; + }; +} + +code { + let result = addThree(10, 20, 30); +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/literals.bug b/packages/bugc/examples/intermediate/literals.bug new file mode 100644 index 00000000..b1298b99 --- /dev/null +++ b/packages/bugc/examples/intermediate/literals.bug @@ -0,0 +1,32 @@ +// @wip +name Literals; + +define { + struct Point { + x: uint256; + y: uint256; + }; + + struct User { + id: uint256; + name: bytes32; + balance: uint256; + }; +} + +code { + // Test array literals + let numbers = [1, 2, 3, 4, 5]; + let hexValues = [0x41, 0x42, 0x43]; + let empty = []; + + // Test struct literals + let origin = Point { x: 0, y: 0 }; + let point = Point { x: 10, y: 20 }; + + let user = User { + id: 123, + name: 0x4a6f686e000000000000000000000000000000000000000000000000000000, + balance: 1000 + }; +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/mappings.bug b/packages/bugc/examples/intermediate/mappings.bug new file mode 100644 index 00000000..689bee59 --- /dev/null +++ b/packages/bugc/examples/intermediate/mappings.bug @@ -0,0 +1,40 @@ +name Mappings; + +define { + struct UserInfo { + name: bytes32; + age: uint256; + active: bool; + }; +} + +storage { + [0] balances: mapping; + [1] allowances: mapping>; + [2] userInfo: mapping; +} + +code { + // Simple mapping access + let sender: address = msg.sender; + balances[sender] = 1000; + /*@test balances-pointer + after: deploy + variables: + balances: + pointer: { location: storage, slot: 0 } + */ + + // Read from mapping + let balance: uint256 = balances[sender]; + + // Nested mapping access + let spender: address = 0x1234567890123456789012345678901234567890; + allowances[sender][spender] = 500; + + // Read nested mapping + let allowance: uint256 = allowances[sender][spender]; + + // Struct in mapping (would need more complex handling) + // userInfo[sender].age = 25; +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/memory-arrays.bug b/packages/bugc/examples/intermediate/memory-arrays.bug new file mode 100644 index 00000000..a9ca5f96 --- /dev/null +++ b/packages/bugc/examples/intermediate/memory-arrays.bug @@ -0,0 +1,50 @@ +name MemoryArray; + +// Example 6: Memory Array Access +// +// Expected IR: +// // Array expression would expand to memory allocation and writes: +// // First, allocate memory for the array (length + elements) +// %t0: uint256 = const 160 // 32 bytes for length + 4*32 bytes for elements +// %t1: uint256 = allocate.memory, size=%t0 +// +// // Store array length at base +// %t2: uint256 = const 4 +// memory[%t1*] = %t2 +// +// // Store elements at base + 32 + (index * 32) +// %t3: uint256 = const 42 +// %t4: uint256 = const 0 +// %t5: uint256 = const 32 // add 32 to skip length field +// %t6 = add %t1, %t5 +// %t7 = offset[%t6].array[%t4] +// memory[%t7*] = %t3 +// +// %t8: uint256 = const 12 +// %t9: uint256 = const 1 +// %t10 = offset[%t6].array[%t9] +// memory[%t10*] = %t8 +// +// %t11: uint256 = const 99 +// %t12: uint256 = const 2 +// %t13 = offset[%t6].array[%t12] +// memory[%t13*] = %t11 +// +// %t14: uint256 = const 101 +// %t15: uint256 = const 3 +// %t16 = offset[%t6].array[%t15] +// memory[%t16*] = %t14 +// +// // Array index access: items[3] +// %t17: uint256 = const 3 +// %t18 = offset[%t6].array[%t17] +// %t19: uint256 = memory[%t18*] +// +// EVM Strategy: Calculate arr + 3*32, then MSTORE/MLOAD +// +// Note: Memory arrays use simple offset arithmetic, no hashing needed + +code { + let items: array = [42, 12, 99, 101]; + let item = items[3]; +} diff --git a/packages/bugc/examples/intermediate/memory-slices.bug b/packages/bugc/examples/intermediate/memory-slices.bug new file mode 100644 index 00000000..75be3f18 --- /dev/null +++ b/packages/bugc/examples/intermediate/memory-slices.bug @@ -0,0 +1,17 @@ +name MemorySlice; + +// Example 11: Memory Slice +// +// Expected IR: +// %slice_offset = compute_offset location="memory", base=%data, byte_offset=10 +// %slice = slice location="memory", offset=%slice_offset, length=40 +// +// EVM Strategy: Calculate memory offsets, copy to new location with proper length prefix +// +// Note: Slice operations might deserve optimization passes to batch adjacent reads + +code { + let data: bytes = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627282930313233343536373839; + let slice = data[10:50]; // 40 bytes from offset 10 +} + diff --git a/packages/bugc/examples/intermediate/memory-structs.bug b/packages/bugc/examples/intermediate/memory-structs.bug new file mode 100644 index 00000000..239f9502 --- /dev/null +++ b/packages/bugc/examples/intermediate/memory-structs.bug @@ -0,0 +1,22 @@ +// @wip +name MemoryTest; + +define { + struct Point { + x: uint256; + y: uint256; + }; +} + +code { + // Test struct field operations + let p: Point; + p.x = 50; + p.y = 75; + let xCoord: uint256 = p.x; + + // Test bytes operations + let data: bytes32 = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + let firstByte: uint256 = data[0]; + data[1] = 0xFF as uint8; +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/owner-check.bug b/packages/bugc/examples/intermediate/owner-check.bug new file mode 100644 index 00000000..72346356 --- /dev/null +++ b/packages/bugc/examples/intermediate/owner-check.bug @@ -0,0 +1,20 @@ +name OwnerCheck; + +storage { + [0] owner: address; +} + +create { + owner = msg.sender; + /*@test owner-pointer + variables: + owner: + pointer: { location: storage, slot: 0, length: 20 } + */ +} + +code { + if (msg.sender == owner) { + owner = 0x0000000000000000000000000000000000000000; + } +} diff --git a/packages/bugc/examples/intermediate/owner-counter.bug b/packages/bugc/examples/intermediate/owner-counter.bug new file mode 100644 index 00000000..174f19a6 --- /dev/null +++ b/packages/bugc/examples/intermediate/owner-counter.bug @@ -0,0 +1,48 @@ +name OwnedCounter; + +storage { + [0] count: uint256; + [1] owner: address; + [2] maxCount: uint256; +} + +create { + // Set the deployer as owner + owner = msg.sender; + + // Initialize counter to 0 + count = 0; + + // Set max count limit + maxCount = 1000000; + /*@test count-after-deploy + variables: + count: + pointer: { location: storage, slot: 0 } + value: 0 + maxCount: + pointer: { location: storage, slot: 2 } + value: 1000000 + */ +} + +code { + // Only owner can increment + if (msg.sender != owner) { + return; + } + + // Check if we've hit the limit + if (count >= maxCount) { + return; + } + + // Increment counter + count = count + 1; + /*@test count-after-call + variables: + count: + pointer: { location: storage, slot: 0 } + value: 1 + */ +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/packed-struct.bug b/packages/bugc/examples/intermediate/packed-struct.bug new file mode 100644 index 00000000..7860d43b --- /dev/null +++ b/packages/bugc/examples/intermediate/packed-struct.bug @@ -0,0 +1,31 @@ +// @wip +// Test packed struct fields +module PackedStruct { + struct Packed { + uint8 a; // 1 byte at offset 0 + uint16 b; // 2 bytes at offset 1 + uint8 c; // 1 byte at offset 3 + address d; // 20 bytes at offset 4 + uint32 e; // 4 bytes at offset 24 + uint32 f; // 4 bytes at offset 28 + } + + storage { + data: Packed + } + + @main + function main() { + // Write to individual packed fields + data.a = 42; + data.b = 1234; + data.c = 99; + data.d = address(0xdead); + data.e = 0x12345678; + data.f = 0xabcdef00; + + // Read from packed fields + let x = data.b; + let y = data.e; + } +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/phi-nodes.bug b/packages/bugc/examples/intermediate/phi-nodes.bug new file mode 100644 index 00000000..449a142c --- /dev/null +++ b/packages/bugc/examples/intermediate/phi-nodes.bug @@ -0,0 +1,56 @@ +name PhiNodes; + +storage { + [0] x: uint256; + [1] y: uint256; +} + +code { + // Test 1: Simple phi node for local variable + let a = 10; + + if (x > 5) { + a = 20; + } else { + a = 30; + } + + y = a; // 'a' should need a phi node here + /*@test y-at-assignment + after: deploy + variables: + y: + pointer: { location: storage, slot: 1 } + value: 0 + */ + + // Test 2: Phi node with multiple updates + let b = 0; + + if (x < 10) { + b = 1; + } else { + if (x < 20) { + b = 2; + } else { + b = 3; + } + } + + x = b; // 'b' should need a phi node here + + // Test 3: Multiple reassignments through different paths + let c = 100; + + if (x == 0) { + c = 200; + } else { + if (y == 0) { + c = 300; + } else { + c = 400; + } + } + + y = c; // 'c' should need a phi node here +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/scopes.bug b/packages/bugc/examples/intermediate/scopes.bug new file mode 100644 index 00000000..89424102 --- /dev/null +++ b/packages/bugc/examples/intermediate/scopes.bug @@ -0,0 +1,15 @@ +name Scopes; + +code { + let x = 0; + let y = 1; + if (x == 0) { + let x = "hello"; + let z = 2; + if (y == 1) { + let x = true; + let y = false; + } + } + let x = 0x42; +} diff --git a/packages/bugc/examples/intermediate/slices.bug b/packages/bugc/examples/intermediate/slices.bug new file mode 100644 index 00000000..37c33f00 --- /dev/null +++ b/packages/bugc/examples/intermediate/slices.bug @@ -0,0 +1,7 @@ +name SlicesTest; + +create { + let short = "hello world"; + let long = "all the world's a stage, and all the men and women merely players. they have their exits and their entrances, and one man in his time plays many parts, his acts being seven ages."; + let shortSlice = (short as bytes)[3:8]; +} diff --git a/packages/bugc/examples/intermediate/storage-arrays.bug b/packages/bugc/examples/intermediate/storage-arrays.bug new file mode 100644 index 00000000..fe48ebb1 --- /dev/null +++ b/packages/bugc/examples/intermediate/storage-arrays.bug @@ -0,0 +1,45 @@ +name StorageArray; + +// Example 5: Storage Array Access +// +// Expected IR: +// // Array literal assignment would expand to: +// // First, store the array length at slot 7 +// %t0: uint256 = const 4 +// storage[7*] = %t0 +// +// // Then store each element at keccak256(7) + index +// %t1: uint256 = const 42 +// %t2: uint256 = const 0 +// %t3: uint256 = slot[7].array[%t2] +// storage[%t3*] = %t1 +// %t4: uint256 = const 12 +// %t5: uint256 = const 1 +// %t6: uint256 = slot[7].array[%t5] +// storage[%t6*] = %t4 +// %t7: uint256 = const 99 +// %t8: uint256 = const 2 +// %t9: uint256 = slot[7].array[%t8] +// storage[%t9*] = %t7 +// %t10: uint256 = const 101 +// %t11: uint256 = const 3 +// %t12: uint256 = slot[7].array[%t11] +// storage[%t12*] = %t10 +// +// // Array index access: +// %t13: uint256 = const 3 +// %t14: uint256 = slot[7].array[%t13] +// %t15: uint256 = storage[%t14*] +// +// EVM Strategy: Keccak256(7) + i, then SSTORE/SLOAD +// +// Key Insight: Dynamic arrays in storage use keccak hashing for base slot + +storage { + [7] items: array; +} + +code { + items = [42, 12, 99, 101]; + let item = items[3]; +} diff --git a/packages/bugc/examples/intermediate/storage-bytes-slice.bug b/packages/bugc/examples/intermediate/storage-bytes-slice.bug new file mode 100644 index 00000000..f321cdd4 --- /dev/null +++ b/packages/bugc/examples/intermediate/storage-bytes-slice.bug @@ -0,0 +1,35 @@ +name StorageBytesSlice; + +// Example 12: Storage Bytes Slice (Complex) +// +// Expected IR (simplified): +// // Storage bytes: length at slot 20, data starts at keccak256(20) +// %length = read location="storage", slot=20, offset=0, length=32 +// +// // Calculate starting position +// %data_base = compute_array_slot location="storage", base=20 +// %start_slot = div 100, 32 +// %start_slot_abs = add %data_base, %start_slot +// %start_offset = mod 100, 32 +// +// // Allocate memory for result +// %slice = alloc_memory length=100 +// +// // Read loop (simplified - would need proper loop construct) +// %slot0 = read location="storage", slot=%start_slot_abs, offset=%start_offset, length=32 +// write location="memory", offset=%slice, length=32, value=%slot0 +// // ... continue reading slots and writing to memory +// +// EVM Strategy: Complex multi-slot reading with offset calculation, reassemble in memory +// +// Key Insight: Storage slicing is complex - requires multi-slot reads and +// reassembly in memory due to how dynamic bytes are stored + +storage { + [20] data: bytes; // Dynamic bytes in storage +} + +code { + // Assume data is initialized with at least 200 bytes + let slice = data[100:200]; // Extract 100 bytes +} \ No newline at end of file diff --git a/packages/bugc/examples/intermediate/strings.bug b/packages/bugc/examples/intermediate/strings.bug new file mode 100644 index 00000000..980e06cd --- /dev/null +++ b/packages/bugc/examples/intermediate/strings.bug @@ -0,0 +1,40 @@ +// @wip +name StringLength; + +define { + function getSelector() -> bytes4 { + let sig = "transfer(address,uint256)"; + let hash = keccak256(sig); + + // Note: sig.length would return 25 (the length of the string) + // But for this example, we'll just return the function selector + return hash as bytes4; + }; +} + +storage { + [0] lastDataSize: uint256; + [1] lastSelector: bytes4; +} + +code { + // Store the calldata size + lastDataSize = msg.data.length; + + // Check if we have at least 4 bytes for a function selector + if (msg.data.length >= 4) { + // Extract and store the selector + let selector = msg.data[0:4] as bytes4; + + lastSelector = selector; + + // Compare with our known selector + let transferSelector = getSelector(); + if (selector == transferSelector) { + // This is a transfer call + return; + } + } + + return; +} diff --git a/packages/bugc/examples/optimizations/constant-folding.bug b/packages/bugc/examples/optimizations/constant-folding.bug new file mode 100644 index 00000000..8b6abc7f --- /dev/null +++ b/packages/bugc/examples/optimizations/constant-folding.bug @@ -0,0 +1,27 @@ +name Optimizations; + +storage { + [0] result: uint256; +} + +code { + // Constant folding example + let a = 10 + 20; // Should fold to 30 + let b = a * 2; // After constant prop, should fold to 60 + let c = 100 - 40; // Should fold to 60 + + // This comparison should be folded + if (a > 25) { // 30 > 25 = true, always taken + result = b + c; // 60 + 60 = 120 + /*@test constant-folding-result + fails: constant comparison folding not implemented + variables: + result: + pointer: { location: storage, slot: 0 } + value: 120 + */ + } else { + // Dead code - should be eliminated + result = 999; + } +} diff --git a/packages/bugc/examples/optimizations/cse-simple.bug b/packages/bugc/examples/optimizations/cse-simple.bug new file mode 100644 index 00000000..be8631e3 --- /dev/null +++ b/packages/bugc/examples/optimizations/cse-simple.bug @@ -0,0 +1,23 @@ +name CSESimple; + +storage { + [0] value: uint256; + [1] result: uint256; +} + +code { + // Simple CSE within a single basic block + // These should get deduplicated + let a = value * 2; + let b = value * 2; // Same computation as line 11 + let c = value * 2; // Same computation as line 11 + + result = a + b + c; +} + +/*@test cse-with-zero-value +after: call +storage: + 0: 0 + 1: 0 +*/ diff --git a/packages/bugc/examples/optimizations/cse.bug b/packages/bugc/examples/optimizations/cse.bug new file mode 100644 index 00000000..5893a780 --- /dev/null +++ b/packages/bugc/examples/optimizations/cse.bug @@ -0,0 +1,16 @@ +name CSEDemo; + +storage { + [0] values: mapping; + [1] result: uint256; +} + +create { + let userValue = values[msg.sender]; + + if (userValue > 100) { + result = values[msg.sender] * 2; + } else { + result = values[msg.sender] + msg.value; + } +} diff --git a/packages/bugc/examples/wip/returndata.bug b/packages/bugc/examples/wip/returndata.bug new file mode 100644 index 00000000..2a587bfd --- /dev/null +++ b/packages/bugc/examples/wip/returndata.bug @@ -0,0 +1,22 @@ +// @wip +name ReturnDataAccess; + +// Example 16: Return Data Access +// +// Expected IR (simplified): +// %result = call function="external", target=%other_contract, ... +// %success = extract_field %result, 0 // Extract success flag +// %return_val = read location="returndata", offset=0, length=32 +// +// EVM Strategy: After CALL, use RETURNDATASIZE and RETURNDATACOPY +// +// Note: Return data is available after external calls, read-only + +code { + let other_contract: address = 0x1234567890123456789012345678901234567890; + // Simplified - in reality would use call opcode and check success + // let result = other_contract.call(0x); + // if (success) { + let return_val = returndata[0:32]; + // } +} \ No newline at end of file diff --git a/packages/bugc/examples/wip/transient-storage.bug b/packages/bugc/examples/wip/transient-storage.bug new file mode 100644 index 00000000..ee637f49 --- /dev/null +++ b/packages/bugc/examples/wip/transient-storage.bug @@ -0,0 +1,19 @@ +// @wip +name TransientStorage; + +// Example 15: Transient Storage +// +// Expected IR: +// transient[5*] = %temp_value +// %val = transient[5*] +// +// EVM Strategy: TSTORE/TLOAD opcodes (available since Cancun) +// +// Note: Transient storage follows same pattern as regular storage with +// segment-based addressing + +code { + let temp_value = 42; + transient[5] = temp_value; // Store temporarily + let val = transient[5]; // Read back +} \ No newline at end of file diff --git a/packages/bugc/package.json b/packages/bugc/package.json new file mode 100644 index 00000000..39be792d --- /dev/null +++ b/packages/bugc/package.json @@ -0,0 +1,109 @@ +{ + "name": "@ethdebug/bugc", + "version": "0.1.0-0", + "description": "The BUG language compiler with ethdebug/format debug information support", + "type": "module", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", + "files": [ + "dist", + "bin" + ], + "bin": { + "bugc": "./bin/bugc.ts" + }, + "imports": { + "#ast": "./dist/src/ast/index.js", + "#ast/spec": "./dist/src/ast/spec.js", + "#ast/visitor": "./dist/src/ast/visitor.js", + "#ast/analysis": "./dist/src/ast/analysis/index.js", + "#cli": "./dist/src/cli/index.js", + "#compiler": "./dist/src/compiler/index.js", + "#errors": "./dist/src/errors/index.js", + "#evm": "./dist/src/evm/index.js", + "#evm/spec": "./dist/src/evm/spec/index.js", + "#evm/analysis": "./dist/src/evm/analysis/index.js", + "#evmgen": "./dist/src/evmgen/index.js", + "#evmgen/analysis": "./dist/src/evmgen/analysis/index.js", + "#evmgen/errors": "./dist/src/evmgen/errors.js", + "#evmgen/state": "./dist/src/evmgen/state.js", + "#evmgen/operations": "./dist/src/evmgen/operations.js", + "#evmgen/generation": "./dist/src/evmgen/generation/index.js", + "#evmgen/serialize": "./dist/src/evmgen/serialize.js", + "#evmgen/pass": "./dist/src/evmgen/pass.js", + "#evmgen/program-builder": "./dist/src/evmgen/program-builder.js", + "#ir": "./dist/src/ir/index.js", + "#ir/spec": "./dist/src/ir/spec/index.js", + "#ir/analysis": "./dist/src/ir/analysis/index.js", + "#irgen": "./dist/src/irgen/index.js", + "#irgen/pass": "./dist/src/irgen/pass.js", + "#irgen/errors": "./dist/src/irgen/errors.js", + "#irgen/type": "./dist/src/irgen/type.js", + "#irgen/generate": "./dist/src/irgen/generate/index.js", + "#irgen/debug/variables": "./dist/src/irgen/debug/variables.js", + "#optimizer": "./dist/src/optimizer/index.js", + "#optimizer/*": "./dist/src/optimizer/*.js", + "#parser": "./dist/src/parser/index.js", + "#parser/pass": "./dist/src/parser/pass.js", + "#result": "./dist/src/result.js", + "#typechecker": "./dist/src/typechecker/index.js", + "#typechecker/pass": "./dist/src/typechecker/pass.js", + "#types": "./dist/src/types/index.js", + "#types/analysis": "./dist/src/types/analysis/index.js", + "#types/spec": "./dist/src/types/spec.js", + "#test/*": "./dist/test/*.js" + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch --preserveWatchOutput", + "watch": "tsc --watch --preserveWatchOutput", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint src bin --ext .ts", + "typecheck": "tsc --noEmit", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"", + "prepare": "tsc" + }, + "keywords": [ + "ethereum", + "compiler", + "debugging", + "ethdebug" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@ethdebug/pointers": "^0.1.0-0", + "@ethereumjs/common": "^10.0.0", + "@ethereumjs/evm": "^10.0.0", + "@ethereumjs/statemanager": "^10.0.0", + "@ethereumjs/util": "^10.0.0", + "@ethereumjs/vm": "^10.0.0", + "@hyperjump/browser": "^1.2.0", + "@hyperjump/json-schema": "^1.11.0", + "@types/node": "^20.0.0", + "@types/parsimmon": "^1.10.9", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitest/coverage-v8": "^2.1.8", + "@vitest/ui": "^2.1.8", + "eslint": "^8.0.0", + "ethereum-cryptography": "^3.2.0", + "fast-check": "^4.2.0", + "prettier": "^3.5.3", + "tsx": "^4.19.4", + "typescript": "^5.0.0", + "vitest": "^2.1.8", + "yaml": "^2.8.2" + }, + "dependencies": { + "@ethdebug/format": "^0.1.0-0", + "fp-ts": "^2.16.11", + "parsimmon": "^1.18.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/bugc/src/ast/analysis/index.ts b/packages/bugc/src/ast/analysis/index.ts new file mode 100644 index 00000000..c8442d44 --- /dev/null +++ b/packages/bugc/src/ast/analysis/index.ts @@ -0,0 +1 @@ +export * from "./source-location.js"; diff --git a/packages/bugc/src/ast/analysis/source-location.ts b/packages/bugc/src/ast/analysis/source-location.ts new file mode 100644 index 00000000..fe62ffce --- /dev/null +++ b/packages/bugc/src/ast/analysis/source-location.ts @@ -0,0 +1,67 @@ +import type { SourceLocation } from "#ast/spec"; + +/** + * Converts a byte offset to line and column position in source text + */ +export function offsetToLineCol( + source: string, + offset: number, +): { line: number; col: number } { + let line = 1; + let col = 1; + + for (let i = 0; i < Math.min(offset, source.length); i++) { + if (source[i] === "\n") { + line++; + col = 1; + } else { + col++; + } + } + + return { line, col }; +} + +/** + * Formats a source location as a human-readable string + */ +export function formatSourceLocation( + loc: SourceLocation | undefined, + source?: string, +): string { + if (!loc) { + return ""; + } + + if (source) { + const offset = + typeof loc.offset === "string" ? parseInt(loc.offset, 10) : loc.offset; + const length = + typeof loc.length === "string" ? parseInt(loc.length, 10) : loc.length; + const start = offsetToLineCol(source, offset); + const end = offsetToLineCol(source, offset + length); + + if (start.line === end.line) { + return `${start.line}:${start.col}-${end.col}`; + } else { + return `${start.line}:${start.col}-${end.line}:${end.col}`; + } + } else { + return `offset ${loc.offset}, length ${loc.length}`; + } +} + +/** + * Formats a source location as a comment for IR output + */ +export function formatSourceComment( + loc: SourceLocation | undefined, + source?: string, +): string { + if (!loc) { + return ""; + } + + const formatted = formatSourceLocation(loc, source); + return formatted ? `; source: ${formatted}` : ""; +} diff --git a/packages/bugc/src/ast/index.ts b/packages/bugc/src/ast/index.ts new file mode 100644 index 00000000..055a2735 --- /dev/null +++ b/packages/bugc/src/ast/index.ts @@ -0,0 +1,3 @@ +export * from "#ast/spec"; +export * from "#ast/visitor"; +export * as Analysis from "#ast/analysis"; diff --git a/packages/bugc/src/ast/spec.test.ts b/packages/bugc/src/ast/spec.test.ts new file mode 100644 index 00000000..cdefcfd9 --- /dev/null +++ b/packages/bugc/src/ast/spec.test.ts @@ -0,0 +1,550 @@ +import { describe, it, expect } from "vitest"; + +import * as Ast from "#ast/spec"; + +// Helper to create test IDs +let testIdCounter = 0; +const createId = (): Ast.Id => `test-${testIdCounter++}` as Ast.Id; + +describe("Ast", () => { + describe("Factory Functions", () => { + it("should create program nodes", () => { + const program = Ast.program( + createId(), + "Test", + [], + Ast.Block.definitions(createId(), []), + Ast.Block.statements(createId(), []), + Ast.Block.statements(createId(), []), + ); + expect(program.kind).toBe("program"); + expect(program.name).toBe("Test"); + expect(program.definitions?.items).toEqual([]); + expect(program.body?.kind).toBe("block:statements"); + expect(program.loc).toBeNull(); + }); + + it("should create variable declaration nodes", () => { + const decl = Ast.Declaration.variable( + createId(), + "x", + Ast.Type.Elementary.uint(createId(), 256), + ); + expect(decl.kind).toBe("declaration:variable"); + expect(decl.name).toBe("x"); + expect(decl.type?.kind).toBe("type:elementary:uint"); + }); + + it("should create struct declarations with fields", () => { + const fields = [ + Ast.Declaration.field( + createId(), + "x", + Ast.Type.Elementary.uint(createId(), 256), + ), + Ast.Declaration.field( + createId(), + "y", + Ast.Type.Elementary.uint(createId(), 256), + ), + ]; + const struct = Ast.Declaration.struct(createId(), "Point", fields); + + expect(struct.kind).toBe("declaration:struct"); + expect(struct.fields).toHaveLength(2); + expect(struct.fields[0].name).toBe("x"); + }); + + it("should create storage declarations with slot", () => { + const storage = Ast.Declaration.storage( + createId(), + "balance", + Ast.Type.Elementary.uint(createId(), 256), + 0, + ); + + expect(storage.kind).toBe("declaration:storage"); + expect(storage.slot).toBe(0); + }); + + it("should create block nodes", () => { + const block = Ast.Block.statements(createId(), [ + Ast.Statement.express( + createId(), + Ast.Expression.identifier(createId(), "x"), + ), + ]); + expect(block.kind).toBe("block:statements"); + expect(block.items).toHaveLength(1); + }); + + it("should create type nodes", () => { + const elementary = Ast.Type.Elementary.uint(createId(), 256); + expect(elementary.kind).toBe("type:elementary:uint"); + expect(elementary.bits).toBe(256); + + const array = Ast.Type.Complex.array(createId(), elementary, 10); + expect(array.kind).toBe("type:complex:array"); + expect(array.size).toBe(10); + expect(array.element).toBeDefined(); + + const mapping = Ast.Type.Complex.mapping( + createId(), + Ast.Type.Elementary.address(createId()), + Ast.Type.Elementary.uint(createId(), 256), + ); + expect(mapping.kind).toBe("type:complex:mapping"); + expect(mapping.key).toBeDefined(); + expect(mapping.value).toBeDefined(); + + const ref = Ast.Type.reference(createId(), "Point"); + expect(ref.kind).toBe("type:reference"); + expect(ref.name).toBe("Point"); + }); + + it("should create expression nodes", () => { + const id = Ast.Expression.identifier(createId(), "x"); + expect(id.kind).toBe("expression:identifier"); + expect(id.name).toBe("x"); + + const literal = Ast.Expression.Literal.number(createId(), "42"); + expect(literal.kind).toBe("expression:literal:number"); + expect(literal.value).toBe("42"); + + const weiLiteral = Ast.Expression.Literal.number( + createId(), + "1", + "ether", + ); + expect(weiLiteral.unit).toBe("ether"); + + const binary = Ast.Expression.operator(createId(), "+", [id, literal]); + expect(binary.kind).toBe("expression:operator"); + expect(binary.operator).toBe("+"); + expect(binary.operands).toHaveLength(2); + + const unary = Ast.Expression.operator(createId(), "!", [id]); + expect(unary.operands).toHaveLength(1); + + const member = Ast.Expression.Access.member(createId(), id, "field"); + expect(member.kind).toBe("expression:access:member"); + expect(member.property).toBe("field"); + + const index = Ast.Expression.Access.index(createId(), id, literal); + expect(index.kind).toBe("expression:access:index"); + expect(index.index.kind).toBe("expression:literal:number"); + + const call = Ast.Expression.call(createId(), id, [literal]); + expect(call.kind).toBe("expression:call"); + expect(call.arguments).toHaveLength(1); + + const special = Ast.Expression.Special.msgSender(createId()); + expect(special.kind).toBe("expression:special:msg.sender"); + }); + + it("should create statement nodes", () => { + const declStmt = Ast.Statement.declare( + createId(), + Ast.Declaration.variable( + createId(), + "x", + undefined, + Ast.Expression.Literal.number(createId(), "42"), + ), + ); + expect(declStmt.kind).toBe("statement:declare"); + + const assign = Ast.Statement.assign( + createId(), + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "10"), + ); + expect(assign.kind).toBe("statement:assign"); + expect(assign.operator).toBeUndefined(); + + const compoundAssign = Ast.Statement.assign( + createId(), + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "10"), + "+=", + ); + expect(compoundAssign.operator).toBe("+="); + + const ifStmt = Ast.Statement.ControlFlow.if_( + createId(), + Ast.Expression.Literal.boolean(createId(), "true"), + Ast.Block.statements(createId(), []), + ); + expect(ifStmt.kind).toBe("statement:control-flow:if"); + + const forStmt = Ast.Statement.ControlFlow.for_( + createId(), + Ast.Block.statements(createId(), []), + declStmt, + Ast.Expression.Literal.boolean(createId(), "true"), + assign, + ); + expect(forStmt.kind).toBe("statement:control-flow:for"); + + const returnStmt = Ast.Statement.ControlFlow.return_( + createId(), + Ast.Expression.identifier(createId(), "x"), + ); + expect(returnStmt.kind).toBe("statement:control-flow:return"); + + const breakStmt = Ast.Statement.ControlFlow.break_(createId()); + expect(breakStmt.kind).toBe("statement:control-flow:break"); + + const exprStmt = Ast.Statement.express( + createId(), + Ast.Expression.identifier(createId(), "x"), + ); + expect(exprStmt.kind).toBe("statement:express"); + }); + + it("should handle source locations", () => { + const loc = { + offset: 0, + length: 5, + }; + + const node = Ast.Expression.identifier(createId(), "test", loc); + expect(node.loc).toEqual(loc); + }); + }); + + describe("Type Guards", () => { + it("should identify expressions", () => { + expect(Ast.isExpression(Ast.Expression.identifier(createId(), "x"))).toBe( + true, + ); + expect( + Ast.isExpression(Ast.Expression.Literal.number(createId(), "42")), + ).toBe(true); + expect( + Ast.isExpression( + Ast.Expression.operator(createId(), "+", [ + Ast.Expression.Literal.number(createId(), "1"), + Ast.Expression.Literal.number(createId(), "2"), + ]), + ), + ).toBe(true); + expect( + Ast.isExpression( + Ast.Expression.Access.member( + createId(), + Ast.Expression.identifier(createId(), "x"), + "y", + ), + ), + ).toBe(true); + expect( + Ast.isExpression( + Ast.Expression.call( + createId(), + Ast.Expression.identifier(createId(), "f"), + [], + ), + ), + ).toBe(true); + expect( + Ast.isExpression(Ast.Expression.Special.msgSender(createId())), + ).toBe(true); + + expect(Ast.isExpression(Ast.Block.statements(createId(), []))).toBe( + false, + ); + expect(Ast.isExpression(Ast.Type.Elementary.uint(createId(), 256))).toBe( + false, + ); + }); + + it("should identify statements", () => { + expect( + Ast.isStatement( + Ast.Statement.declare( + createId(), + Ast.Declaration.variable(createId(), "x"), + ), + ), + ).toBe(true); + expect( + Ast.isStatement( + Ast.Statement.assign( + createId(), + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "1"), + ), + ), + ).toBe(true); + expect( + Ast.isStatement( + Ast.Statement.ControlFlow.if_( + createId(), + Ast.Expression.Literal.boolean(createId(), "true"), + Ast.Block.statements(createId(), []), + ), + ), + ).toBe(true); + expect( + Ast.isStatement( + Ast.Statement.express( + createId(), + Ast.Expression.identifier(createId(), "x"), + ), + ), + ).toBe(true); + + expect(Ast.isStatement(Ast.Expression.identifier(createId(), "x"))).toBe( + false, + ); + expect(Ast.isStatement(Ast.Block.statements(createId(), []))).toBe(false); + }); + + it("should identify type nodes", () => { + expect(Ast.isType(Ast.Type.Elementary.uint(createId(), 256))).toBe(true); + expect( + Ast.isType( + Ast.Type.Complex.array( + createId(), + Ast.Type.Elementary.uint(createId(), 256), + ), + ), + ).toBe(true); + expect(Ast.isType(Ast.Type.reference(createId(), "Point"))).toBe(true); + + expect(Ast.isType(Ast.Expression.identifier(createId(), "x"))).toBe( + false, + ); + expect(Ast.isType(Ast.Block.statements(createId(), []))).toBe(false); + }); + + it("should identify assignable expressions", () => { + expect( + Ast.Expression.isAssignable(Ast.Expression.identifier(createId(), "x")), + ).toBe(true); + expect( + Ast.Expression.isAssignable( + Ast.Expression.Access.member( + createId(), + Ast.Expression.identifier(createId(), "x"), + "y", + ), + ), + ).toBe(true); + expect( + Ast.Expression.isAssignable( + Ast.Expression.Access.index( + createId(), + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "0"), + ), + ), + ).toBe(true); + + expect( + Ast.Expression.isAssignable( + Ast.Expression.Literal.number(createId(), "42"), + ), + ).toBe(false); + expect( + Ast.Expression.isAssignable( + Ast.Expression.operator(createId(), "+", [ + Ast.Expression.Literal.number(createId(), "1"), + Ast.Expression.Literal.number(createId(), "2"), + ]), + ), + ).toBe(false); + expect( + Ast.Expression.isAssignable( + Ast.Expression.call( + createId(), + Ast.Expression.identifier(createId(), "f"), + [], + ), + ), + ).toBe(false); + expect( + Ast.Expression.isAssignable( + Ast.Expression.Special.msgSender(createId()), + ), + ).toBe(false); + }); + }); + + describe("Utility Functions", () => { + describe("cloneNode", () => { + it("should deep clone nodes", () => { + const original = Ast.Expression.operator(createId(), "+", [ + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "42"), + ]); + const clone = Ast.Node.clone(original); + + expect(clone).not.toBe(original); + expect(clone.operands[0]).not.toBe(original.operands[0]); + expect(clone.operands[1]).not.toBe(original.operands[1]); + expect(clone).toEqual(original); + }); + + it("should handle complex nested structures", () => { + const program = Ast.program( + createId(), + "Test", + undefined, + Ast.Block.definitions(createId(), [ + Ast.Declaration.struct(createId(), "Point", [ + Ast.Declaration.field( + createId(), + "x", + Ast.Type.Elementary.uint(createId(), 256), + ), + Ast.Declaration.field( + createId(), + "y", + Ast.Type.Elementary.uint(createId(), 256), + ), + ]), + ]), + Ast.Block.statements(createId(), [ + Ast.Statement.ControlFlow.if_( + createId(), + Ast.Expression.operator(createId(), "==", [ + Ast.Expression.Special.msgSender(createId()), + Ast.Expression.identifier(createId(), "owner"), + ]), + Ast.Block.statements(createId(), [ + Ast.Statement.assign( + createId(), + Ast.Expression.identifier(createId(), "x"), + Ast.Expression.Literal.number(createId(), "42"), + ), + ]), + ), + ]), + ); + + const clone = Ast.Node.clone(program); + expect(clone).not.toBe(program); + expect(clone.definitions).not.toBe(program.definitions); + expect(clone.body).not.toBe(program.body); + expect(clone).toEqual(program); + }); + }); + + describe("updateNode", () => { + it("should create updated copy", () => { + const original = Ast.Expression.identifier(createId(), "x"); + const updated = Ast.Node.update(original, { name: "y" }); + + expect(updated).not.toBe(original); + expect(updated.name).toBe("y"); + expect(original.name).toBe("x"); + }); + }); + }); + + describe("Complex Scenarios", () => { + it("should handle realistic program structure", () => { + const program = Ast.program( + createId(), + "SimpleStorage", + [ + // storage { 0: owner: address; 1: users: mapping; } + Ast.Declaration.storage( + createId(), + "owner", + Ast.Type.Elementary.address(createId()), + 0, + ), + Ast.Declaration.storage( + createId(), + "users", + Ast.Type.Complex.mapping( + createId(), + Ast.Type.Elementary.address(createId()), + Ast.Type.reference(createId(), "User"), + ), + 1, + ), + ], + Ast.Block.definitions(createId(), [ + // struct User { name: string; balance: uint256; } + Ast.Declaration.struct(createId(), "User", [ + Ast.Declaration.field( + createId(), + "name", + Ast.Type.Elementary.string(createId()), + ), + Ast.Declaration.field( + createId(), + "balance", + Ast.Type.Elementary.uint(createId(), 256), + ), + ]), + ]), + Ast.Block.statements(createId(), [ + // let sender = msg.sender; + Ast.Statement.declare( + createId(), + Ast.Declaration.variable( + createId(), + "sender", + undefined, + Ast.Expression.Special.msgSender(createId()), + ), + ), + + // if (sender == owner) { users[sender].balance = users[sender].balance + msg.value; } + Ast.Statement.ControlFlow.if_( + createId(), + Ast.Expression.operator(createId(), "==", [ + Ast.Expression.identifier(createId(), "sender"), + Ast.Expression.identifier(createId(), "owner"), + ]), + Ast.Block.statements(createId(), [ + Ast.Statement.assign( + createId(), + Ast.Expression.Access.member( + createId(), + Ast.Expression.Access.index( + createId(), + Ast.Expression.identifier(createId(), "users"), + Ast.Expression.identifier(createId(), "sender"), + ), + "balance", + ), + Ast.Expression.operator(createId(), "+", [ + Ast.Expression.Access.member( + createId(), + Ast.Expression.Access.index( + createId(), + Ast.Expression.identifier(createId(), "users"), + Ast.Expression.identifier(createId(), "sender"), + ), + "balance", + ), + Ast.Expression.Special.msgValue(createId()), + ]), + ), + ]), + ), + ]), + Ast.Block.statements(createId(), []), + ); + + expect(program.kind).toBe("program"); + expect(program.storage).toHaveLength(2); + expect(program.definitions?.items).toHaveLength(1); + expect(program.body?.items).toHaveLength(2); + + // Verify structure + const structDecl = program.definitions!.items[0]; + expect(structDecl.kind).toBe("declaration:struct"); + expect((structDecl as Ast.Declaration.Struct).fields).toHaveLength(2); + + const ifStmt = program.body?.items[1] as { kind: string }; + expect(ifStmt.kind).toBe("statement:control-flow:if"); + }); + }); +}); diff --git a/packages/bugc/src/ast/spec.ts b/packages/bugc/src/ast/spec.ts new file mode 100644 index 00000000..13310fd2 --- /dev/null +++ b/packages/bugc/src/ast/spec.ts @@ -0,0 +1,1539 @@ +/** + * Normalized AST node types for the BUG language + * + * Aligned with ethdebug format domain language for compatibility + * with debugging tooling and standardization. + * + * Key principles: + * 1. Unified patterns for similar constructs (declarations, blocks, etc.) + * 2. Use discriminated unions with 'kind' fields for variants + * 3. Minimize special cases + * 4. Clear separation between syntactic and semantic information + * 5. Alignment with ethdebug format terminology and structure + */ + +import * as Format from "@ethdebug/format"; + +export type SourceLocation = NonNullable; + +export const isSourceLocation = (loc: unknown): loc is SourceLocation => + Format.Materials.isSourceRange({ source: { id: "pending" }, range: loc }); + +// ID type for AST nodes - using string type with numeric identifiers +export type Id = string; + +export type Node = + | Program + | Declaration + | Block + | Type + | Statement + | Expression; + +export namespace Node { + export interface Base { + id: Id; + kind: string; + loc: SourceLocation | null; + } + + export const isBase = (node: unknown): node is Node.Base => + typeof node === "object" && + !!node && + "id" in node && + typeof node.id === "string" && + "kind" in node && + typeof node.kind === "string" && + !!node.kind && + "loc" in node && + (node.loc === null || isSourceLocation(node.loc)); + + export function clone(node: T): T { + const clone = { ...node }; + + // Deep clone child nodes + for (const [key, value] of Object.entries(clone)) { + if (value && typeof value === "object") { + if (Array.isArray(value)) { + (clone as unknown as Record)[key] = value.map( + (item) => + item && typeof item === "object" && "kind" in item + ? Node.clone(item) + : item, + ); + } else if ("kind" in value) { + (clone as unknown as Record)[key] = + Node.clone(value); + } + } + } + + return clone; + } + + export function update(node: T, updates: Partial): T { + return { ...node, ...updates }; + } +} + +// Program structure + +export interface Program extends Node.Base { + kind: "program"; + name: string; + definitions?: Block.Definitions; // All top-level declarations + storage?: Declaration.Storage[]; + create?: Block.Statements; // Constructor code block (may be empty) + body?: Block.Statements; // Runtime code block (may be empty) +} + +export function program( + id: Id, + name: string, + storage?: Declaration.Storage[], + definitions?: Block.Definitions, + body?: Block.Statements, + create?: Block.Statements, + loc?: SourceLocation, +): Program { + return { + id, + kind: "program", + name, + storage, + definitions, + body, + create, + loc: loc ?? null, + }; +} + +export const isProgram = (program: unknown): program is Program => + Node.isBase(program) && + program.kind === "Program" && + "name" in program && + typeof program.name === "string" && + "declarations" in program && + Array.isArray(program.declarations) && + program.declarations.every(isDeclaration); //&& +// "create" in program && isBlock(program.create) && +// "body" in program && isBlock(program.body); + +export type Declaration = + | Declaration.Struct + | Declaration.Field + | Declaration.Storage + | Declaration.Variable + | Declaration.Function + | Declaration.Parameter; + +export const isDeclaration = (node: unknown): node is Declaration => + Declaration.isBase(node) && + [ + Declaration.isStruct, + Declaration.isField, + Declaration.isStorage, + Declaration.isVariable, + Declaration.isFunction, + Declaration.isParameter, + ].some((guard) => guard(node)); + +export namespace Declaration { + export interface Base extends Node.Base { + kind: `declaration:${string}`; + name: string; + } + + export const isBase = ( + declaration: unknown, + ): declaration is Declaration.Base => + Node.isBase(declaration) && + declaration.kind.startsWith("declaration") && + "name" in declaration && + typeof declaration.name === "string"; + + export interface Struct extends Declaration.Base { + kind: "declaration:struct"; + fields: Declaration[]; + } + + export function struct( + id: Id, + name: string, + fields: Declaration.Field[], + loc?: SourceLocation, + ): Declaration.Struct { + return { + id, + kind: "declaration:struct", + name, + fields, + loc: loc ?? null, + }; + } + + export const isStruct = ( + declaration: Declaration.Base, + ): declaration is Declaration.Struct => + "kind" in declaration && declaration.kind === "declaration:struct"; + + export interface Field extends Declaration.Base { + kind: "declaration:field"; + type: Type; + initializer?: Expression; + } + + export function field( + id: Id, + name: string, + type: Type, + initializer?: Expression, + loc?: SourceLocation, + ): Declaration.Field { + return { + id, + kind: "declaration:field", + name, + type, + initializer, + loc: loc ?? null, + }; + } + + export const isField = ( + declaration: Declaration.Base, + ): declaration is Declaration.Field => + "kind" in declaration && declaration.kind === "declaration:field"; + + export interface Storage extends Declaration.Base { + kind: "declaration:storage"; + type: Type; + slot: number; + } + + export function storage( + id: Id, + name: string, + type: Type, + slot: number, + loc?: SourceLocation, + ): Declaration.Storage { + return { + id, + kind: "declaration:storage", + name, + type, + slot, + loc: loc ?? null, + }; + } + + export const isStorage = ( + declaration: Declaration.Base, + ): declaration is Declaration.Storage => + "kind" in declaration && declaration.kind === "declaration:storage"; + + export interface Variable extends Declaration.Base { + kind: "declaration:variable"; + type?: Type; + initializer?: Expression; + } + + export function variable( + id: Id, + name: string, + type?: Type, + initializer?: Expression, + loc?: SourceLocation, + ): Declaration.Variable { + return { + id, + kind: "declaration:variable", + name, + type, + initializer, + loc: loc ?? null, + }; + } + + export const isVariable = ( + declaration: Declaration.Base, + ): declaration is Declaration.Variable => + "kind" in declaration && declaration.kind === "declaration:variable"; + + export interface Function extends Declaration.Base { + kind: "declaration:function"; + parameters: Declaration.Parameter[]; + returnType?: Type; + body: Block; + } + + export function function_( + id: Id, + name: string, + parameters: Declaration.Parameter[], + returnType: Type | undefined, + body: Block, + loc?: SourceLocation, + ): Declaration.Function { + return { + id, + kind: "declaration:function", + name, + parameters, + returnType, + body, + loc: loc ?? null, + }; + } + + export const isFunction = ( + declaration: Declaration.Base, + ): declaration is Declaration.Function => + "kind" in declaration && declaration.kind === "declaration:function"; + + export interface Parameter extends Declaration.Base { + kind: "declaration:parameter"; + name: string; + type: Type; + } + + export function parameter( + id: Id, + name: string, + type: Type, + loc?: SourceLocation, + ): Declaration.Parameter { + return { + id, + kind: "declaration:parameter", + name, + type, + loc: loc ?? null, + }; + } + + export const isParameter = ( + declaration: Declaration.Base, + ): declaration is Declaration.Parameter => + declaration.kind === "declaration:parameter"; +} + +// Data locations aligned with ethdebug format +export type DataLocation = + | "storage" + | "memory" + | "stack" + | "calldata" + | "returndata" + | "transient" + | "code"; + +// Unified Block pattern +// Covers: code blocks, storage blocks, statement blocks + +export type Block = Block.Statements | Block.Definitions; + +export const isBlock = (block: unknown): block is Block => + Block.isBase(block) && + [Block.isStatements, Block.isDefinitions].some((guard) => guard(block)); + +export namespace Block { + export interface Base extends Node.Base { + kind: `block:${string}`; + } + + export const isBase = (block: unknown): block is Block.Base => + Node.isBase(block) && block.kind.startsWith("block:"); + + export interface Statements extends Block.Base { + kind: "block:statements"; + items: Statement[]; + } + + export function statements( + id: Id, + items: Statement[], + loc?: SourceLocation, + ): Block.Statements { + return { id, kind: "block:statements", items, loc: loc ?? null }; + } + + export const isStatements = (block: Block.Base): block is Block.Statements => + block.kind === "block:statements"; + + export interface Definitions extends Block.Base { + kind: "block:definitions"; + items: Declaration[]; + } + + export function definitions( + id: Id, + items: Declaration[], + loc?: SourceLocation, + ): Block.Definitions { + return { id, kind: "block:definitions", items, loc: loc ?? null }; + } + + export const isDefinitions = ( + block: Block.Base, + ): block is Block.Definitions => block.kind === "block:definitions"; +} + +// Type nodes - aligned with ethdebug format + +export type Type = Type.Elementary | Type.Complex | Type.Reference; + +export const isType = (node: unknown): node is Type => Type.isBase(node); + +export namespace Type { + export interface Base extends Node.Base { + kind: `type:${string}`; + } + + export const isBase = (type: unknown): type is Type.Base => + Node.isBase(type) && type.kind.startsWith("type:"); + + export type Elementary = + | Elementary.Uint + | Elementary.Int + | Elementary.Address + | Elementary.Bool + | Elementary.Bytes + | Elementary.String + | Elementary.Fixed + | Elementary.Ufixed; + + export const isElementary = (type: Type.Base): type is Elementary => + type.kind.startsWith("type:elementary:"); + + export namespace Elementary { + export interface Base extends Type.Base { + kind: `type:elementary:${string}`; + } + + export const isBase = (type: Type.Base): type is Elementary.Base => + type.kind.startsWith("type:elementary:"); + export interface Uint extends Elementary.Base { + kind: "type:elementary:uint"; + bits: number; + } + + export const isUint = (type: Type.Base): type is Uint => + type.kind === "type:elementary:uint"; + + export function uint( + id: Id, + bits: number = 256, + loc?: SourceLocation, + ): Uint { + return { id, kind: "type:elementary:uint", bits, loc: loc ?? null }; + } + + export interface Int extends Elementary.Base { + kind: "type:elementary:int"; + bits: number; + } + + export const isInt = (type: Type.Base): type is Int => + type.kind === "type:elementary:int"; + + export function int(id: Id, bits: number = 256, loc?: SourceLocation): Int { + return { id, kind: "type:elementary:int", bits, loc: loc ?? null }; + } + + export interface Address extends Elementary.Base { + kind: "type:elementary:address"; + } + + export const isAddress = (type: Type.Base): type is Address => + type.kind === "type:elementary:address"; + + export function address(id: Id, loc?: SourceLocation): Address { + return { id, kind: "type:elementary:address", loc: loc ?? null }; + } + + export interface Bool extends Elementary.Base { + kind: "type:elementary:bool"; + } + + export const isBool = (type: Type.Base): type is Bool => + type.kind === "type:elementary:bool"; + + export function bool(id: Id, loc?: SourceLocation): Bool { + return { id, kind: "type:elementary:bool", loc: loc ?? null }; + } + + export interface Bytes extends Elementary.Base { + kind: "type:elementary:bytes"; + size?: number; + } + + export const isBytes = (type: Type.Base): type is Bytes => + type.kind === "type:elementary:bytes"; + + export function bytes(id: Id, size?: number, loc?: SourceLocation): Bytes { + return { id, kind: "type:elementary:bytes", size, loc: loc ?? null }; + } + + export interface String extends Elementary.Base { + kind: "type:elementary:string"; + } + + export const isString = (type: Type.Base): type is Type.Elementary.String => + type.kind === "type:elementary:string"; + + export function string( + id: Id, + loc?: SourceLocation, + ): Type.Elementary.String { + return { id, kind: "type:elementary:string", loc: loc ?? null }; + } + + export interface Fixed extends Elementary.Base { + kind: "type:elementary:fixed"; + bits: number; + } + + export const isFixed = (type: Type.Base): type is Fixed => + type.kind === "type:elementary:fixed"; + + export function fixed( + id: Id, + bits: number = 128, + loc?: SourceLocation, + ): Fixed { + return { id, kind: "type:elementary:fixed", bits, loc: loc ?? null }; + } + + export interface Ufixed extends Elementary.Base { + kind: "type:elementary:ufixed"; + bits: number; + } + + export const isUfixed = (type: Type.Base): type is Ufixed => + type.kind === "type:elementary:ufixed"; + + export function ufixed( + id: Id, + bits: number = 128, + loc?: SourceLocation, + ): Ufixed { + return { id, kind: "type:elementary:ufixed", bits, loc: loc ?? null }; + } + } + + export type Complex = + | Complex.Array + | Complex.Mapping + | Complex.Struct + | Complex.Tuple + | Complex.Function + | Complex.Alias + | Complex.Contract + | Complex.Enum; + + export const isComplex = (type: Type.Base): type is Complex => + type.kind.startsWith("type:complex:"); + + export namespace Complex { + export interface Base extends Type.Base { + kind: `type:complex:${string}`; + } + + export const isBase = (type: Type.Base): type is Complex.Base => + type.kind.startsWith("type:complex:"); + export interface Array extends Complex.Base { + kind: "type:complex:array"; + element: Type; + size?: number; + } + + export const isArray = (type: Type.Base): type is Array => + type.kind === "type:complex:array"; + + export function array( + id: Id, + element: Type, + size?: number, + loc?: SourceLocation, + ): Array { + return { + id, + kind: "type:complex:array", + element, + size, + loc: loc ?? null, + }; + } + + export interface Mapping extends Complex.Base { + kind: "type:complex:mapping"; + key: Type; + value: Type; + } + + export const isMapping = (type: Type.Base): type is Mapping => + type.kind === "type:complex:mapping"; + + export function mapping( + id: Id, + key: Type, + value: Type, + loc?: SourceLocation, + ): Mapping { + return { id, kind: "type:complex:mapping", key, value, loc: loc ?? null }; + } + + export interface Struct extends Complex.Base { + kind: "type:complex:struct"; + fields: Map; + } + + export const isStruct = (type: Type.Base): type is Struct => + type.kind === "type:complex:struct"; + + export function struct( + id: Id, + fields: Map, + loc?: SourceLocation, + ): Struct { + return { id, kind: "type:complex:struct", fields, loc: loc ?? null }; + } + + export interface Tuple extends Complex.Base { + kind: "type:complex:tuple"; + members: Type[]; + } + + export const isTuple = (type: Type.Base): type is Tuple => + type.kind === "type:complex:tuple"; + + export function tuple( + id: Id, + members: Type[], + loc?: SourceLocation, + ): Tuple { + return { id, kind: "type:complex:tuple", members, loc: loc ?? null }; + } + + export interface Function extends Complex.Base { + kind: "type:complex:function"; + parameters: Type[]; + returns: Type[]; + } + + export const isFunction = ( + type: Type.Base, + ): type is Type.Complex.Function => type.kind === "type:complex:function"; + + export function function_( + id: Id, + parameters: Type[], + returns: Type[], + loc?: SourceLocation, + ): Type.Complex.Function { + return { + id, + kind: "type:complex:function", + parameters, + returns, + loc: loc ?? null, + }; + } + + export interface Alias extends Complex.Base { + kind: "type:complex:alias"; + base: Type; + } + + export const isAlias = (type: Type.Base): type is Alias => + type.kind === "type:complex:alias"; + + export function alias(id: Id, base: Type, loc?: SourceLocation): Alias { + return { id, kind: "type:complex:alias", base, loc: loc ?? null }; + } + + export interface Contract extends Complex.Base { + kind: "type:complex:contract"; + name: string; + } + + export const isContract = (type: Type.Base): type is Contract => + type.kind === "type:complex:contract"; + + export function contract( + id: Id, + name: string, + loc?: SourceLocation, + ): Contract { + return { id, kind: "type:complex:contract", name, loc: loc ?? null }; + } + + export interface Enum extends Complex.Base { + kind: "type:complex:enum"; + members: string[]; + } + + export const isEnum = (type: Type.Base): type is Enum => + type.kind === "type:complex:enum"; + + export function enum_( + id: Id, + members: string[], + loc?: SourceLocation, + ): Enum { + return { id, kind: "type:complex:enum", members, loc: loc ?? null }; + } + } + + export interface Reference extends Type.Base { + kind: "type:reference"; + name: string; + } + + export const isReference = (type: Type.Base): type is Reference => + type.kind === "type:reference"; + + export function reference( + id: Id, + name: string, + loc?: SourceLocation, + ): Reference { + return { id, kind: "type:reference", name, loc: loc ?? null }; + } +} + +// Statements - unified pattern + +export type Statement = + | Statement.Declare + | Statement.Assign + | Statement.ControlFlow + | Statement.Express; + +export const isStatement = (node: unknown): node is Statement => + Statement.isBase(node) && + [ + Statement.isDeclare, + Statement.isAssign, + Statement.isControlFlow, + Statement.isExpress, + ].some((guard) => guard(node)); + +export namespace Statement { + export interface Base extends Node.Base { + kind: `statement:${string}`; + } + + export const isBase = (statement: unknown): statement is Statement.Base => + Node.isBase(statement) && statement.kind.startsWith("statement:"); + + export interface Declare extends Statement.Base { + kind: "statement:declare"; + declaration: Declaration; + } + + export const isDeclare = ( + statement: Statement.Base, + ): statement is Statement.Declare => statement.kind === "statement:declare"; + + export function declare( + id: Id, + declaration: Declaration, + loc?: SourceLocation, + ): Statement.Declare { + return { id, kind: "statement:declare", declaration, loc: loc ?? null }; + } + + export interface Assign extends Statement.Base { + kind: "statement:assign"; + target: Expression; // Must be assignable (validated during semantic analysis) + value: Expression; + operator?: string; // For compound assignments like += (future) + } + + export const isAssign = ( + statement: Statement.Base, + ): statement is Statement.Assign => statement.kind === "statement:assign"; + + export function assign( + id: Id, + target: Expression, + value: Expression, + operator?: string, + loc?: SourceLocation, + ): Statement.Assign { + return { + id, + kind: "statement:assign", + target, + value, + operator, + loc: loc ?? null, + }; + } + + export type ControlFlow = + | Statement.ControlFlow.If + | Statement.ControlFlow.For + | Statement.ControlFlow.While + | Statement.ControlFlow.Return + | Statement.ControlFlow.Break + | Statement.ControlFlow.Continue; + + export const isControlFlow = ( + statement: Statement.Base, + ): statement is Statement.ControlFlow => + Statement.ControlFlow.isBase(statement) && + [ + Statement.ControlFlow.isIf, + Statement.ControlFlow.isFor, + Statement.ControlFlow.isWhile, + Statement.ControlFlow.isReturn, + Statement.ControlFlow.isBreak, + Statement.ControlFlow.isContinue, + ].some((guard) => guard(statement)); + + export namespace ControlFlow { + export interface Base extends Statement.Base { + kind: `statement:control-flow:${string}`; + } + + export const isBase = ( + statement: Statement.Base, + ): statement is Statement.ControlFlow.Base => + statement.kind.startsWith("statement:control-flow:"); + export interface If extends Statement.ControlFlow.Base { + kind: "statement:control-flow:if"; + condition: Expression; + body: Block; + alternate?: Block; + } + + export const isIf = (statement: Statement.Base): statement is If => + statement.kind === "statement:control-flow:if"; + + export function if_( + id: Id, + condition: Expression, + body: Block, + alternate?: Block, + loc?: SourceLocation, + ): If { + return { + id, + kind: "statement:control-flow:if", + condition, + body, + alternate, + loc: loc ?? null, + }; + } + + export interface For extends Statement.ControlFlow.Base { + kind: "statement:control-flow:for"; + init?: Statement; + condition?: Expression; + update?: Statement; + body: Block; + } + + export const isFor = (statement: Statement.Base): statement is For => + statement.kind === "statement:control-flow:for"; + + export function for_( + id: Id, + body: Block, + init?: Statement, + condition?: Expression, + update?: Statement, + loc?: SourceLocation, + ): For { + return { + id, + kind: "statement:control-flow:for", + init, + condition, + update, + body, + loc: loc ?? null, + }; + } + + export interface While extends Statement.ControlFlow.Base { + kind: "statement:control-flow:while"; + condition: Expression; + body: Block; + } + + export const isWhile = (statement: Statement.Base): statement is While => + statement.kind === "statement:control-flow:while"; + + export function while_( + id: Id, + condition: Expression, + body: Block, + loc?: SourceLocation, + ): While { + return { + id, + kind: "statement:control-flow:while", + condition, + body, + loc: loc ?? null, + }; + } + + export interface Return extends Statement.ControlFlow.Base { + kind: "statement:control-flow:return"; + value?: Expression; + } + + export const isReturn = (statement: Statement.Base): statement is Return => + statement.kind === "statement:control-flow:return"; + + export function return_( + id: Id, + value?: Expression, + loc?: SourceLocation, + ): Return { + return { + id, + kind: "statement:control-flow:return", + value, + loc: loc ?? null, + }; + } + + export interface Break extends Statement.ControlFlow.Base { + kind: "statement:control-flow:break"; + label?: string; + } + + export const isBreak = (statement: Statement.Base): statement is Break => + statement.kind === "statement:control-flow:break"; + + export function break_( + id: Id, + label?: string, + loc?: SourceLocation, + ): Break { + return { + id, + kind: "statement:control-flow:break", + label, + loc: loc ?? null, + }; + } + + export interface Continue extends Statement.ControlFlow.Base { + kind: "statement:control-flow:continue"; + label?: string; + } + + export const isContinue = ( + statement: Statement.Base, + ): statement is Continue => + statement.kind === "statement:control-flow:continue"; + + export function continue_( + id: Id, + label?: string, + loc?: SourceLocation, + ): Continue { + return { + id, + kind: "statement:control-flow:continue", + label, + loc: loc ?? null, + }; + } + } + + export interface Express extends Statement.Base { + kind: "statement:express"; + expression: Expression; + } + + export const isExpress = ( + statement: Statement.Base, + ): statement is Statement.Express => statement.kind === "statement:express"; + + export function express( + id: Id, + expression: Expression, + loc?: SourceLocation, + ): Statement.Express { + return { id, kind: "statement:express", expression, loc: loc ?? null }; + } +} + +// Expressions - normalized hierarchy + +export type Expression = + | Expression.Identifier + | Expression.Literal.Number + | Expression.Literal.String + | Expression.Literal.Boolean + | Expression.Literal.Address + | Expression.Literal.Hex + | Expression.Array + | Expression.Struct + | Expression.Operator + | Expression.Access + | Expression.Call + | Expression.Cast + | Expression.Special; + +export const isExpression = (node: unknown): node is Expression => + Expression.isBase(node) && + [ + Expression.isIdentifier, + Expression.isLiteral, + Expression.isArray, + Expression.isStruct, + Expression.isOperator, + Expression.isAccess, + Expression.isCall, + Expression.isCast, + Expression.isSpecial, + ].some((guard) => guard(node)); + +export namespace Expression { + export interface Base extends Node.Base { + kind: `expression:${string}`; + } + + export const isBase = (expression: unknown): expression is Expression.Base => + Node.isBase(expression) && expression.kind.startsWith("expression:"); + export function isAssignable(expr: Expression): boolean { + // Only certain expressions can be assigned to + return ( + expr.kind === "expression:identifier" || + expr.kind.startsWith("expression:access") + ); + } + + export interface Identifier extends Expression.Base { + kind: "expression:identifier"; + name: string; + } + + export const isIdentifier = ( + expression: Node.Base, + ): expression is Expression.Identifier => + expression.kind === "expression:identifier" && + "name" in expression && + typeof expression.name === "string"; + + export function identifier( + id: Id, + name: string, + loc?: SourceLocation, + ): Expression.Identifier { + return { id, kind: "expression:identifier", name, loc: loc ?? null }; + } + + export type Literal = + | Expression.Literal.Number + | Expression.Literal.String + | Expression.Literal.Boolean + | Expression.Literal.Address + | Expression.Literal.Hex; + + export const isLiteral = ( + expression: Expression.Base, + ): expression is Expression.Literal => + Expression.Literal.isBase(expression) && + [ + Expression.Literal.isNumber, + Expression.Literal.isString, + Expression.Literal.isBoolean, + Expression.Literal.isAddress, + Expression.Literal.isHex, + ].some((guard) => guard(expression)); + + export namespace Literal { + export interface Base extends Expression.Base { + kind: `expression:literal:${string}`; + value: string; + } + + export const isBase = ( + expression: Expression.Base, + ): expression is Expression.Literal.Base => + expression.kind.startsWith("expression:literal:") && + "value" in expression && + typeof expression.value === "string"; + + export interface Number extends Expression.Literal.Base { + kind: "expression:literal:number"; + unit?: string; + } + + export const isNumber = ( + expression: Expression.Base, + ): expression is Expression.Literal.Number => + expression.kind === "expression:literal:number"; + + export function number( + id: Id, + value: string, + unit?: string, + loc?: SourceLocation, + ): Expression.Literal.Number { + return { + id, + kind: "expression:literal:number", + value, + unit, + loc: loc ?? null, + }; + } + + export interface String extends Expression.Literal.Base { + kind: "expression:literal:string"; + } + + export const isString = ( + expression: Expression.Base, + ): expression is Expression.Literal.String => + expression.kind === "expression:literal:string"; + + export function string( + id: Id, + value: string, + loc?: SourceLocation, + ): Expression.Literal.String { + return { id, kind: "expression:literal:string", value, loc: loc ?? null }; + } + + export interface Boolean extends Expression.Literal.Base { + kind: "expression:literal:boolean"; + } + + export const isBoolean = ( + expression: Expression.Base, + ): expression is Expression.Literal.Boolean => + expression.kind === "expression:literal:boolean"; + + export function boolean( + id: Id, + value: string, + loc?: SourceLocation, + ): Expression.Literal.Boolean { + return { + id, + kind: "expression:literal:boolean", + value, + loc: loc ?? null, + }; + } + + export interface Address extends Expression.Literal.Base { + kind: "expression:literal:address"; + } + + export const isAddress = ( + expression: Expression.Base, + ): expression is Expression.Literal.Address => + expression.kind === "expression:literal:address"; + + export function address( + id: Id, + value: string, + loc?: SourceLocation, + ): Expression.Literal.Address { + return { + id, + kind: "expression:literal:address", + value, + loc: loc ?? null, + }; + } + + export interface Hex extends Expression.Literal.Base { + kind: "expression:literal:hex"; + } + + export const isHex = ( + expression: Expression.Base, + ): expression is Expression.Literal.Hex => + expression.kind === "expression:literal:hex"; + + export function hex( + id: Id, + value: string, + loc?: SourceLocation, + ): Expression.Literal.Hex { + return { id, kind: "expression:literal:hex", value, loc: loc ?? null }; + } + } + + // Use underscore pattern to avoid conflict with built-in Array + export interface Array extends Expression.Base { + kind: "expression:array"; + elements: readonly Expression[]; + } + + export const isArray = ( + expression: Node.Base, + ): expression is Expression.Array => + expression.kind === "expression:array" && + "elements" in expression && + Array_.isArray(expression.elements) && + expression.elements.every(isExpression); + + export function array( + id: Id, + elements: readonly Expression[], + loc?: SourceLocation, + ): Expression.Array { + return { + id, + kind: "expression:array", + elements, + loc: loc ?? null, + }; + } + + export interface Struct extends Expression.Base { + kind: "expression:struct"; + structName?: string; // Optional struct type name + fields: readonly { + name: string; + value: Expression; + }[]; + } + + export const isStruct = ( + expression: Node.Base, + ): expression is Expression.Struct => + expression.kind === "expression:struct" && + "fields" in expression && + Array_.isArray(expression.fields) && + expression.fields.every( + (field: unknown) => + typeof field === "object" && + field !== null && + "name" in field && + typeof field.name === "string" && + "value" in field && + isExpression(field.value), + ); + + export function struct( + id: Id, + fields: readonly { name: string; value: Expression }[], + structName?: string, + loc?: SourceLocation, + ): Expression.Struct { + return { + id, + kind: "expression:struct", + structName, + fields, + loc: loc ?? null, + }; + } + + export interface Operator extends Expression.Base { + kind: "expression:operator"; + operator: string; + operands: readonly [Expression] | readonly [Expression, Expression]; + } + + export const isOperator = ( + expression: Node.Base, + ): expression is Expression.Operator => + expression.kind === "expression:operator" && + "operator" in expression && + typeof expression.operator === "string" && + "operands" in expression && + Array.isArray(expression.operands); + + export function operator( + id: Id, + operator: string, + operands: readonly [Expression] | readonly [Expression, Expression], + loc?: SourceLocation, + ): Expression.Operator { + return { + id, + kind: "expression:operator", + operator, + operands, + loc: loc ?? null, + }; + } + + export type Access = + | Expression.Access.Member + | Expression.Access.Slice + | Expression.Access.Index; + + export const isAccess = ( + expression: Node.Base, + ): expression is Expression.Access => + Expression.Access.isBase(expression) && + [ + Expression.Access.isMember, + Expression.Access.isSlice, + Expression.Access.isIndex, + ].some((guard) => guard(expression)); + + export namespace Access { + export interface Base extends Expression.Base { + kind: `expression:access:${string}`; + object: Expression; + } + + export const isBase = (access: unknown): access is Expression.Access.Base => + Node.isBase(access) && + access.kind.startsWith("expression:access:") && + "object" in access && + isExpression(access.object); + + export interface Member extends Expression.Access.Base { + kind: "expression:access:member"; + property: string; + } + + export function member( + id: Id, + object: Expression, + property: string, + loc?: SourceLocation, + ): Expression.Access.Member { + return { + id, + kind: "expression:access:member", + object, + property, + loc: loc ?? null, + }; + } + + export const isMember = ( + access: Expression.Access.Base, + ): access is Expression.Access.Member => + access.kind === "expression:access:member"; + + export interface Slice extends Expression.Access.Base { + kind: "expression:access:slice"; + start: Expression; + end: Expression; + } + + export function slice( + id: Id, + object: Expression, + start: Expression, + end: Expression, + loc?: SourceLocation, + ): Expression.Access.Slice { + return { + id, + kind: "expression:access:slice", + object, + start, + end, + loc: loc ?? null, + }; + } + + export const isSlice = ( + access: Expression.Access.Base, + ): access is Expression.Access.Slice => + access.kind === "expression:access:slice"; + + export interface Index extends Expression.Access.Base { + kind: "expression:access:index"; + index: Expression; + } + + export function index( + id: Id, + object: Expression, + index: Expression, + loc?: SourceLocation, + ): Expression.Access.Index { + return { + id, + kind: "expression:access:index", + object, + index, + loc: loc ?? null, + }; + } + + export const isIndex = ( + access: Expression.Access.Base, + ): access is Expression.Access.Index => + access.kind === "expression:access:index"; + } + + export interface Call extends Expression.Base { + kind: "expression:call"; + callee: Expression; + arguments: Expression[]; + } + + export const isCall = ( + expression: Node.Base, + ): expression is Expression.Call => + expression.kind === "expression:call" && + "callee" in expression && + typeof expression.callee === "object" && + "arguments" in expression && + Array.isArray(expression.arguments); + + export function call( + id: Id, + callee: Expression, + args: Expression[], + loc?: SourceLocation, + ): Expression.Call { + return { + id, + kind: "expression:call", + callee, + arguments: args, + loc: loc ?? null, + }; + } + + export interface Cast extends Expression.Base { + kind: "expression:cast"; + expression: Expression; + targetType: Type; + } + + export const isCast = ( + expression: Node.Base, + ): expression is Expression.Cast => + expression.kind === "expression:cast" && + "expression" in expression && + typeof expression.expression === "object" && + "targetType" in expression && + typeof expression.targetType === "object"; + + export function cast( + id: Id, + expression: Expression, + targetType: Type, + loc?: SourceLocation, + ): Expression.Cast { + return { + id, + kind: "expression:cast", + expression, + targetType, + loc: loc ?? null, + }; + } + + export type Special = + | Expression.Special.MsgSender + | Expression.Special.MsgValue + | Expression.Special.MsgData + | Expression.Special.BlockTimestamp + | Expression.Special.BlockNumber; + + export const isSpecial = ( + expression: Expression.Base, + ): expression is Expression.Special => + Expression.Special.isBase(expression) && + [ + Expression.Special.isMsgData, + Expression.Special.isMsgValue, + Expression.Special.isMsgSender, + Expression.Special.isBlockNumber, + Expression.Special.isBlockTimestamp, + ].some((guard) => guard(expression)); + + export namespace Special { + export interface Base extends Expression.Base { + kind: `expression:special:${string}`; + } + + export const isBase = (expression: Expression.Base): expression is Base => + expression.kind.startsWith("expression:special:"); + + export interface MsgSender extends Expression.Special.Base { + kind: "expression:special:msg.sender"; + } + + export const isMsgSender = ( + expression: Expression.Base, + ): expression is MsgSender => + expression.kind === "expression:special:msg.sender"; + + export function msgSender(id: Id, loc?: SourceLocation): MsgSender { + return { id, kind: "expression:special:msg.sender", loc: loc ?? null }; + } + + export interface MsgValue extends Expression.Special.Base { + kind: "expression:special:msg.value"; + } + + export const isMsgValue = ( + expression: Expression.Base, + ): expression is MsgValue => + expression.kind === "expression:special:msg.value"; + + export function msgValue(id: Id, loc?: SourceLocation): MsgValue { + return { id, kind: "expression:special:msg.value", loc: loc ?? null }; + } + + export interface MsgData extends Expression.Special.Base { + kind: "expression:special:msg.data"; + } + + export const isMsgData = ( + expression: Expression.Base, + ): expression is MsgData => + expression.kind === "expression:special:msg.data"; + + export function msgData(id: Id, loc?: SourceLocation): MsgData { + return { id, kind: "expression:special:msg.data", loc: loc ?? null }; + } + + export interface BlockTimestamp extends Base { + kind: "expression:special:block.timestamp"; + } + + export const isBlockTimestamp = ( + expression: Expression.Base, + ): expression is BlockTimestamp => + expression.kind === "expression:special:block.timestamp"; + + export function blockTimestamp( + id: Id, + loc?: SourceLocation, + ): BlockTimestamp { + return { + id, + kind: "expression:special:block.timestamp", + loc: loc ?? null, + }; + } + + export interface BlockNumber extends Base { + kind: "expression:special:block.number"; + } + + export const isBlockNumber = ( + expression: Expression.Base, + ): expression is BlockNumber => + expression.kind === "expression:special:block.number"; + + export function blockNumber(id: Id, loc?: SourceLocation): BlockNumber { + return { id, kind: "expression:special:block.number", loc: loc ?? null }; + } + } +} + +type Array_ = Array; +const Array_ = Array; diff --git a/packages/bugc/src/ast/visitor.test.ts b/packages/bugc/src/ast/visitor.test.ts new file mode 100644 index 00000000..cdbfd0d1 --- /dev/null +++ b/packages/bugc/src/ast/visitor.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect } from "vitest"; + +import * as Ast from "#ast"; + +// Helper to create test IDs +let testIdCounter = 0; +const createId = (): Ast.Id => `test-${testIdCounter++}` as Ast.Id; + +describe("Visitor Pattern", () => { + class TestVisitor implements Ast.Visitor { + program(node: Ast.Program): string { + return `Program(${node.name})`; + } + declaration(node: Ast.Declaration): string { + return `Declaration(${node.kind}:${node.name})`; + } + block(node: Ast.Block): string { + return `Block(${node.kind})`; + } + type(node: Ast.Type): string { + if (node.kind.startsWith("type:elementary:")) { + return `ElementaryType(${node.kind}${"bits" in node ? node.bits : ""})`; + } else if (node.kind.startsWith("type:complex:")) { + return `ComplexType(${node.kind})`; + } else if (node.kind === "type:reference") { + return `ReferenceType(${(node as Ast.Type.Reference).name})`; + } + return `Type(${node.kind})`; + } + statement(node: Ast.Statement): string { + if (node.kind === "statement:declare") { + return "DeclarationStatement"; + } else if (node.kind === "statement:assign") { + return "AssignmentStatement"; + } else if (node.kind.startsWith("statement:control-flow:")) { + return `ControlFlowStatement(${node.kind})`; + } else if (node.kind === "statement:express") { + return "ExpressionStatement"; + } + return `Statement(${node.kind})`; + } + expression(node: Ast.Expression): string { + if (node.kind === "expression:identifier") { + return `Identifier(${(node as Ast.Expression.Identifier).name})`; + } else if (node.kind.startsWith("expression:literal:")) { + return `Literal(${node.kind}:${(node as Ast.Expression.Literal).value})`; + } else if (node.kind === "expression:array") { + return `Array(${(node as Ast.Expression.Array).elements.length} elements)`; + } else if (node.kind === "expression:struct") { + return `Struct(${(node as Ast.Expression.Struct).fields.length} fields)`; + } else if (node.kind === "expression:operator") { + return `Operator(${(node as Ast.Expression.Operator).operator})`; + } else if (node.kind.startsWith("expression:access:")) { + return `Access(${node.kind})`; + } else if (node.kind === "expression:call") { + return "Call"; + } else if (node.kind.startsWith("expression:special:")) { + return `Special(${node.kind})`; + } else if (node.kind === "expression:cast") { + return "Cast"; + } + return `Expression(${node.kind})`; + } + } + + it("should visit all node types", () => { + const visitor = new TestVisitor(); + + expect( + Ast.visit( + visitor, + Ast.program( + createId(), + "Test", + undefined, + Ast.Block.definitions(createId(), []), + Ast.Block.statements(createId(), []), + Ast.Block.statements(createId(), []), + ), + undefined as never, + ), + ).toBe("Program(Test)"); + expect( + Ast.visit( + visitor, + Ast.Declaration.variable(createId(), "x"), + undefined as never, + ), + ).toBe("Declaration(declaration:variable:x)"); + expect( + Ast.visit( + visitor, + Ast.Block.statements(createId(), []), + undefined as never, + ), + ).toBe("Block(block:statements)"); + expect( + Ast.visit( + visitor, + Ast.Type.Elementary.uint(createId(), 256), + undefined as never, + ), + ).toBe("ElementaryType(type:elementary:uint256)"); + expect( + Ast.visit( + visitor, + Ast.Type.Complex.array( + createId(), + Ast.Type.Elementary.uint(createId(), 256), + ), + undefined as never, + ), + ).toBe("ComplexType(type:complex:array)"); + expect( + Ast.visit( + visitor, + Ast.Type.reference(createId(), "Point"), + undefined as never, + ), + ).toBe("ReferenceType(Point)"); + expect( + Ast.visit( + visitor, + Ast.Expression.identifier(createId(), "x"), + undefined as never, + ), + ).toBe("Identifier(x)"); + expect( + Ast.visit( + visitor, + Ast.Expression.Literal.number(createId(), "42"), + undefined as never, + ), + ).toBe("Literal(expression:literal:number:42)"); + expect( + Ast.visit( + visitor, + Ast.Expression.operator(createId(), "+", [ + Ast.Expression.Literal.number(createId(), "1"), + Ast.Expression.Literal.number(createId(), "2"), + ]), + undefined as never, + ), + ).toBe("Operator(+)"); + expect( + Ast.visit( + visitor, + Ast.Expression.Access.member( + createId(), + Ast.Expression.identifier(createId(), "x"), + "y", + ), + undefined as never, + ), + ).toBe("Access(expression:access:member)"); + expect( + Ast.visit( + visitor, + Ast.Expression.call( + createId(), + Ast.Expression.identifier(createId(), "f"), + [], + ), + undefined as never, + ), + ).toBe("Call"); + expect( + Ast.visit( + visitor, + Ast.Expression.Special.msgSender(createId()), + undefined as never, + ), + ).toBe("Special(expression:special:msg.sender)"); + expect( + Ast.visit( + visitor, + Ast.Statement.ControlFlow.if_( + createId(), + Ast.Expression.Literal.boolean(createId(), "true"), + Ast.Block.statements(createId(), []), + ), + undefined as never, + ), + ).toBe("ControlFlowStatement(statement:control-flow:if)"); + }); + + it("should throw on unknown node type", () => { + const visitor = new TestVisitor(); + const badNode = { kind: "Unknown", loc: null } as unknown as Ast.Node; + + expect(() => Ast.visit(visitor, badNode, undefined as never)).toThrow( + "Unknown node kind: Unknown", + ); + }); +}); diff --git a/packages/bugc/src/ast/visitor.ts b/packages/bugc/src/ast/visitor.ts new file mode 100644 index 00000000..066cba79 --- /dev/null +++ b/packages/bugc/src/ast/visitor.ts @@ -0,0 +1,209 @@ +import * as Ast from "#ast/spec"; + +export interface Visitor { + program(node: Ast.Program, context: C): T; + declaration(node: Ast.Declaration, context: C): T; + block(node: Ast.Block, context: C): T; + type(node: Ast.Type, context: C): T; + statement(node: Ast.Statement, context: C): T; + expression(node: Ast.Expression, context: C): T; +} + +// Base visitor implementation with kind-based dispatch +export function visit( + visitor: Visitor, + node: N, + context: C, +): T { + // Handle top-level kinds + if (node.kind === "program") { + return visitor.program(node as Ast.Program, context); + } + + // Handle hierarchical kinds by checking prefixes + if (node.kind.startsWith("declaration:")) { + return visitor.declaration(node as Ast.Declaration, context); + } + + if (node.kind.startsWith("block:")) { + return visitor.block(node as Ast.Block, context); + } + + if (node.kind.startsWith("type:")) { + return visitor.type(node as Ast.Type, context); + } + + if (node.kind.startsWith("statement:")) { + return visitor.statement(node as Ast.Statement, context); + } + + if (node.kind.startsWith("expression:")) { + return visitor.expression(node as Ast.Expression, context); + } + + throw new Error(`Unknown node kind: ${node.kind}`); +} + +// More specific visitor interface for detailed traversal +export interface DetailedVisitor { + // Program + program(node: Ast.Program, context: C): T; + + // Declarations + declarationStruct(node: Ast.Declaration.Struct, context: C): T; + declarationField(node: Ast.Declaration.Field, context: C): T; + declarationStorage(node: Ast.Declaration.Storage, context: C): T; + declarationVariable(node: Ast.Declaration.Variable, context: C): T; + declarationFunction(node: Ast.Declaration.Function, context: C): T; + declarationParameter(node: Ast.Declaration.Parameter, context: C): T; + + // Blocks + blockStatements(node: Ast.Block.Statements, context: C): T; + blockDefinitions(node: Ast.Block.Definitions, context: C): T; + + // Types + typeElementary(node: Ast.Type, context: C): T; + typeComplex(node: Ast.Type, context: C): T; + typeReference(node: Ast.Type.Reference, context: C): T; + + // Statements + statementDeclare(node: Ast.Statement.Declare, context: C): T; + statementAssign(node: Ast.Statement.Assign, context: C): T; + statementControlFlow(node: Ast.Statement.ControlFlow, context: C): T; + statementExpress(node: Ast.Statement.Express, context: C): T; + + // Expressions + expressionIdentifier(node: Ast.Expression.Identifier, context: C): T; + expressionLiteral(node: Ast.Expression.Literal, context: C): T; + expressionArray(node: Ast.Expression.Array, context: C): T; + expressionStruct(node: Ast.Expression.Struct, context: C): T; + expressionOperator(node: Ast.Expression.Operator, context: C): T; + expressionAccess(node: Ast.Expression.Access, context: C): T; + expressionCall(node: Ast.Expression.Call, context: C): T; + expressionCast(node: Ast.Expression.Cast, context: C): T; + expressionSpecial(node: Ast.Expression.Special, context: C): T; +} + +// Detailed visitor implementation with full kind-based dispatch +export function visitDetailed( + visitor: DetailedVisitor, + node: N, + context: C, +): T { + switch (node.kind) { + // Program + case "program": + return visitor.program(node as Ast.Program, context); + + // Declarations + case "declaration:struct": + return visitor.declarationStruct(node as Ast.Declaration.Struct, context); + case "declaration:field": + return visitor.declarationField(node as Ast.Declaration.Field, context); + case "declaration:storage": + return visitor.declarationStorage( + node as Ast.Declaration.Storage, + context, + ); + case "declaration:variable": + return visitor.declarationVariable( + node as Ast.Declaration.Variable, + context, + ); + case "declaration:function": + return visitor.declarationFunction( + node as Ast.Declaration.Function, + context, + ); + case "declaration:parameter": + return visitor.declarationParameter( + node as Ast.Declaration.Parameter, + context, + ); + + // Blocks + case "block:statements": + return visitor.blockStatements(node as Ast.Block.Statements, context); + case "block:definitions": + return visitor.blockDefinitions(node as Ast.Block.Definitions, context); + + // Types + case "type:elementary:uint": + case "type:elementary:int": + case "type:elementary:address": + case "type:elementary:bool": + case "type:elementary:bytes": + case "type:elementary:string": + case "type:elementary:fixed": + case "type:elementary:ufixed": + return visitor.typeElementary(node as Ast.Type, context); + case "type:complex:array": + case "type:complex:mapping": + case "type:complex:struct": + case "type:complex:tuple": + case "type:complex:function": + case "type:complex:alias": + case "type:complex:contract": + case "type:complex:enum": + return visitor.typeComplex(node as Ast.Type, context); + case "type:reference": + return visitor.typeReference(node as Ast.Type.Reference, context); + + // Statements + case "statement:declare": + return visitor.statementDeclare(node as Ast.Statement.Declare, context); + case "statement:assign": + return visitor.statementAssign(node as Ast.Statement.Assign, context); + case "statement:control-flow:if": + case "statement:control-flow:for": + case "statement:control-flow:while": + case "statement:control-flow:return": + case "statement:control-flow:break": + case "statement:control-flow:continue": + return visitor.statementControlFlow( + node as Ast.Statement.ControlFlow, + context, + ); + case "statement:express": + return visitor.statementExpress(node as Ast.Statement.Express, context); + + // Expressions + case "expression:identifier": + return visitor.expressionIdentifier( + node as Ast.Expression.Identifier, + context, + ); + case "expression:literal:number": + case "expression:literal:string": + case "expression:literal:boolean": + case "expression:literal:address": + case "expression:literal:hex": + return visitor.expressionLiteral(node as Ast.Expression.Literal, context); + case "expression:array": + return visitor.expressionArray(node as Ast.Expression.Array, context); + case "expression:struct": + return visitor.expressionStruct(node as Ast.Expression.Struct, context); + case "expression:operator": + return visitor.expressionOperator( + node as Ast.Expression.Operator, + context, + ); + case "expression:access:member": + case "expression:access:slice": + case "expression:access:index": + return visitor.expressionAccess(node as Ast.Expression.Access, context); + case "expression:call": + return visitor.expressionCall(node as Ast.Expression.Call, context); + case "expression:cast": + return visitor.expressionCast(node as Ast.Expression.Cast, context); + case "expression:special:msg.sender": + case "expression:special:msg.value": + case "expression:special:msg.data": + case "expression:special:block.timestamp": + case "expression:special:block.number": + return visitor.expressionSpecial(node as Ast.Expression.Special, context); + + default: + throw new Error(`Unknown node kind: ${(node as Ast.Node).kind}`); + } +} diff --git a/packages/bugc/src/cli/compile.ts b/packages/bugc/src/cli/compile.ts new file mode 100644 index 00000000..c34d0759 --- /dev/null +++ b/packages/bugc/src/cli/compile.ts @@ -0,0 +1,474 @@ +/** + * BUG compiler CLI implementation + */ +/* eslint-disable no-console */ + +import { parseArgs } from "util"; +import { readFileSync, writeFileSync } from "fs"; +import { resolve } from "path"; +import { + commonOptions, + optimizationOption, + parseOptimizationLevel, +} from "./options.js"; +import { displayErrors, displayWarnings } from "./output.js"; +import { formatJson, formatIrText } from "./formatters.js"; +import * as Ir from "#ir"; +import { EvmFormatter } from "#evm/analysis"; +import type { Program } from "#ast"; +import type { EvmGenerationOutput } from "#evmgen/pass"; +import type { Types, Bindings } from "#types"; +import * as TypesAnalysis from "#types/analysis"; +import { compile } from "#compiler"; +import { Result } from "#result"; +import type { BugError } from "#errors"; + +type Phase = "ast" | "types" | "ir" | "bytecode"; + +// Helper type to represent the compiler output +type CompilerOutput = T extends "ast" + ? { ast: Program } + : T extends "types" + ? { ast: Program; types: Types; bindings: Bindings } + : T extends "ir" + ? { ast: Program; types: Types; bindings: Bindings; ir: Ir.Module } + : T extends "bytecode" + ? { + ast: Program; + types: Types; + bindings: Bindings; + ir: Ir.Module; + bytecode: EvmGenerationOutput; + } + : never; + +const compileOptions = { + ...optimizationOption, + "stop-after": { + type: "string" as const, + short: "s", + default: "bytecode", + }, + format: { + type: "string" as const, + short: "f", + default: "text", + }, + "show-both": { + type: "boolean" as const, + default: false, + }, + stats: { + type: "boolean" as const, + default: false, + }, + validate: { + type: "boolean" as const, + default: false, + }, + pretty: { + type: "boolean" as const, + short: "p", + default: false, + }, +}; + +export async function handleCompileCommand(args: string[]): Promise { + const allOptions = { ...commonOptions, ...compileOptions }; + + const { values, positionals } = parseArgs({ + args: args.slice(2), // Skip 'node' and 'bugc' + options: allOptions, + allowPositionals: true, + }); + + // Cast values to include all possible properties + const parsedValues = values as { + help?: boolean; + output?: string; + optimize?: string; + "stop-after"?: string; + format?: string; + "show-both"?: boolean; + stats?: boolean; + validate?: boolean; + pretty?: boolean; + }; + + if (parsedValues.help || positionals.length === 0) { + showHelp(); + process.exit(0); + } + + try { + // Validate arguments + const phase = String(parsedValues["stop-after"] || "bytecode") as Phase; + if (!["ast", "types", "ir", "bytecode"].includes(phase)) { + throw new Error("--stop-after must be one of: ast, types, ir, bytecode"); + } + + const format = String(parsedValues.format || "text"); + if (!["text", "json", "asm"].includes(format)) { + throw new Error("--format must be 'text', 'json', or 'asm'"); + } + + // Read source file + const filePath = resolve(positionals[0]); + const source = readFileSync(filePath, "utf-8"); + + // Compile using new interface + const optimizationLevel = parseOptimizationLevel( + String(parsedValues.optimize || "0"), + ); + + // Call compile with properly typed options based on phase + const result = await compileForPhase( + phase, + source, + filePath, + optimizationLevel, + ); + + if (!result.success) { + displayErrors(result.messages, source); + process.exit(1); + } + displayWarnings(result.messages, source); + + // Format output + const output = formatOutput( + result.value, + phase, + format, + parsedValues, + source, + ); + + // Write output + if (parsedValues.output) { + writeFileSync(parsedValues.output, output); + } else { + console.log(output); + } + + // Post-process if needed + await postProcess(result.value, phase, { ...parsedValues, filePath }); + } catch (error) { + console.error( + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); + } +} + +function showHelp(): void { + console.log(`Usage: bugc [options] + +Compile BUG source code with configurable output phase + +Options: + -s, --stop-after Stop compilation after phase (ast, types, ir, bytecode) + Default: bytecode + -O, --optimize Set optimization level (0-3) + Default: 0 + -f, --format Output format (text, json, asm) + Default: text + -o, --output Write output to file instead of stdout + -p, --pretty Pretty-print JSON output + --validate Validate IR output + --stats Show IR statistics + --show-both Show both unoptimized and optimized IR + -h, --help Show this help message`); +} + +// Helper function to compile with proper types +async function compileForPhase( + phase: T, + source: string, + filePath: string, + optimizationLevel: number, +): Promise, BugError>> { + if (phase === "ast") { + const result = await compile({ + to: "ast", + source, + sourcePath: filePath, + }); + return result as Result, BugError>; + } else if (phase === "types") { + const result = await compile({ + to: "types", + source, + sourcePath: filePath, + }); + return result as Result, BugError>; + } else if (phase === "ir") { + const result = await compile({ + to: "ir", + source, + optimizer: { + level: optimizationLevel as 0 | 1 | 2 | 3, + }, + sourcePath: filePath, + }); + return result as Result, BugError>; + } else { + const result = await compile({ + to: "bytecode", + source, + optimizer: { + level: optimizationLevel as 0 | 1 | 2 | 3, + }, + sourcePath: filePath, + }); + return result as Result, BugError>; + } +} + +function formatOutput( + result: CompilerOutput, + phase: T, + format: string, + values: Record, + source?: string, +): string { + switch (phase) { + case "ast": + return formatAst( + (result as CompilerOutput<"ast">).ast, + format, + values.pretty as boolean, + ); + case "types": + return formatTypes( + (result as CompilerOutput<"types">).types, + (result as CompilerOutput<"types">).bindings, + format, + values.pretty as boolean, + source, + ); + case "ir": + return formatIr((result as CompilerOutput<"ir">).ir, format, source); + case "bytecode": + return formatBytecode( + (result as CompilerOutput<"bytecode">).bytecode, + format, + values.pretty as boolean, + source, + ); + default: + return ""; + } +} + +async function postProcess( + result: CompilerOutput, + phase: T, + values: Record & { filePath?: string }, +): Promise { + switch (phase) { + case "ir": + if ("ir" in result) { + await postProcessIr(result.ir, values); + } + break; + } +} + +// Formatting functions +function formatAst(ast: Program, format: string, pretty: boolean): string { + if (format === "json") { + return formatJson(ast, pretty); + } else { + // For text format, show the AST structure + return formatJson(ast, true); + } +} + +function formatTypes( + types: Types, + bindings: Bindings, + format: string, + pretty: boolean, + source?: string, +): string { + if (format === "json") { + // Convert Maps to objects for JSON serialization + const typesObj = Object.fromEntries( + Array.from(types.entries()).map(([id, type]) => [id, type]), + ); + const bindingsObj = Object.fromEntries( + Array.from(bindings.entries()).map(([id, decl]) => [id, decl]), + ); + return formatJson({ types: typesObj, bindings: bindingsObj }, pretty); + } else { + // Use the formatter for text output + const formatter = new TypesAnalysis.Formatter(); + return formatter.format(types, bindings, source); + } +} + +function formatIr(ir: Ir.Module, format: string, source?: string): string { + if (format === "json") { + return formatJson(ir, false); + } else { + return formatIrText(ir, source); + } +} + +function formatBytecode( + bytecode: EvmGenerationOutput, + format: string, + pretty: boolean, + source?: string, +): string { + if (format === "json") { + // For JSON, return the bytecode with instructions and debug info + const output = { + runtime: { + bytecode: "0x" + Buffer.from(bytecode.runtime).toString("hex"), + instructions: bytecode.runtimeInstructions, + }, + create: bytecode.create + ? { + bytecode: "0x" + Buffer.from(bytecode.create).toString("hex"), + instructions: bytecode.createInstructions, + } + : undefined, + }; + return formatJson(output, pretty); + } else if (format === "asm") { + // For asm format, use the instruction objects directly + let output = `; Runtime bytecode (${bytecode.runtime.length} bytes)\n`; + output += EvmFormatter.formatInstructions( + bytecode.runtimeInstructions, + source, + ); + + if (bytecode.create && bytecode.createInstructions) { + output += `\n\n; Creation bytecode (${bytecode.create.length} bytes)\n`; + output += EvmFormatter.formatInstructions( + bytecode.createInstructions, + source, + ); + } + + return output; + } else { + // For text format, show hex strings with labels + const runtimeHex = "0x" + Buffer.from(bytecode.runtime).toString("hex"); + const createHex = bytecode.create + ? "0x" + Buffer.from(bytecode.create).toString("hex") + : undefined; + + let output = `Runtime bytecode (${bytecode.runtime.length} bytes):\n${runtimeHex}\n`; + + if (createHex) { + output += `\nCreation bytecode (${bytecode.create!.length} bytes):\n${createHex}\n`; + } + + return output; + } +} + +// Post-processing functions +async function postProcessIr( + ir: Ir.Module, + args: Record & { filePath?: string }, +): Promise { + // Handle --validate flag + if (args.validate) { + validateIr(ir); + } + + // Handle --stats flag + if (args.stats) { + showStats(ir); + } + + // Handle --show-both flag + const optimizationLevel = parseOptimizationLevel( + String(args.optimize || "0"), + ); + if (args["show-both"] && optimizationLevel > 0) { + const filePath = + args.filePath || resolve(process.argv[process.argv.length - 1]); + const source = readFileSync(filePath, "utf-8"); + await showBothVersions( + source, + optimizationLevel, + String(args.format), + filePath, + ); + } +} + +function validateIr(ir: Ir.Module): void { + const validator = new Ir.Analysis.Validator(); + const validationResult = validator.validate(ir); + + if (!validationResult.isValid) { + console.error("IR Validation Failed:"); + for (const error of validationResult.errors) { + console.error(` - ${error}`); + } + process.exit(1); + } + + if (validationResult.warnings.length > 0) { + console.error("IR Validation Warnings:"); + for (const warning of validationResult.warnings) { + console.error(` - ${warning}`); + } + } + + console.error("✓ IR validation passed"); + console.error(""); +} + +function showStats(ir: Ir.Module): void { + const stats = new Ir.Analysis.Statistics.Analyzer(); + const statistics = stats.analyze(ir); + + console.log("=== IR Statistics ==="); + console.log(`Blocks: ${statistics.blockCount}`); + console.log(`Instructions: ${statistics.instructionCount}`); + console.log(`Temporaries: ${statistics.tempCount}`); + console.log(`Max block size: ${statistics.maxBlockSize}`); + console.log(`Avg block size: ${statistics.avgBlockSize.toFixed(2)}`); + console.log(`CFG edges: ${statistics.cfgEdges}`); + console.log("\nInstruction types:"); + for (const [type, count] of Object.entries(statistics.instructionTypes)) { + console.log(` ${type}: ${count}`); + } + console.log(""); +} + +async function showBothVersions( + source: string, + optimizationLevel: number, + format: string, + sourcePath: string, +): Promise { + // First compile without optimization + const unoptResult = await compile({ + to: "ir", + optimizer: { + level: 0, + }, + source, + sourcePath, + }); + + if (unoptResult.success) { + const unoptIr = unoptResult.value.ir; + console.log("=== Unoptimized IR ==="); + if (format === "json") { + console.log(JSON.stringify(unoptIr, null, 2)); + } else { + const formatter = new Ir.Analysis.Formatter(); + console.log(formatter.format(unoptIr, source)); + } + console.log("\n=== Optimized IR (Level " + optimizationLevel + ") ==="); + } +} diff --git a/packages/bugc/src/cli/error-formatter.ts b/packages/bugc/src/cli/error-formatter.ts new file mode 100644 index 00000000..973bf2d1 --- /dev/null +++ b/packages/bugc/src/cli/error-formatter.ts @@ -0,0 +1,118 @@ +/** + * Error formatting utilities for CLI display + */ + +import type { SourceLocation } from "#ast"; +import type { BugError } from "#errors"; +import { Severity, type MessagesBySeverity } from "#result"; + +/** + * Format a source location for display + */ +export function formatLocation(location: SourceLocation): string { + return `offset ${location.offset}, length ${location.length}`; +} + +/** + * Format messages organized by severity + */ +export function formatMessages( + messages: MessagesBySeverity, + source?: string, +): string { + const output: string[] = []; + + // Format errors first + const errors = messages[Severity.Error] || []; + for (const error of errors) { + output.push(formatError(error, source)); + } + + // Then warnings + const warnings = messages[Severity.Warning] || []; + for (const warning of warnings) { + output.push(formatWarning(warning, source)); + } + + return output.join("\n\n"); +} + +/** + * Format an error for CLI display + */ +export function formatError(error: BugError, source?: string): string { + const icon = error.severity === Severity.Error ? "❌" : "⚠️"; + const severityText = error.severity === Severity.Error ? "Error" : "Warning"; + + let output = `${icon} ${severityText} [${error.code}]: ${error.message}`; + + if (error.location) { + output += `\n at ${formatLocation(error.location)}`; + + // If we have source code, show the relevant snippet + if (source) { + const snippet = getSourceSnippet(source, error.location); + if (snippet) { + output += `\n${snippet}`; + } + } + } + + if (error.stack) { + output += `\n${error.stack.split("\n").slice(1, 4).join("\n")}`; + } + + return output; +} + +/** + * Format a warning (convenience function) + */ +export function formatWarning(warning: BugError, source?: string): string { + return formatError(warning, source); +} + +/** + * Get a source code snippet around an error location + */ +export function getSourceSnippet( + source: string, + location: SourceLocation, +): string | null { + const lines = source.split("\n"); + const { offset, length } = location; + + // Find which line the error starts on + let currentOffset = 0; + let lineNumber = 0; + let columnNumber = 0; + + for (let i = 0; i < lines.length; i++) { + const lineLength = lines[i].length + 1; // +1 for newline + + if (currentOffset + lineLength > Number(offset)) { + lineNumber = i; + columnNumber = Number(offset) - currentOffset; + break; + } + + currentOffset += lineLength; + } + + if (lineNumber >= lines.length) { + return null; + } + + // Show the line with the error + const line = lines[lineNumber]; + const lineNumberStr = String(lineNumber + 1).padStart(4, " "); + + let output = `\n${lineNumberStr} | ${line}\n`; + output += " | "; + + // Add the error indicator + output += " ".repeat(columnNumber); + output += "^".repeat(Math.min(Number(length), line.length - columnNumber)); + + return output; +} diff --git a/packages/bugc/src/cli/formatters.ts b/packages/bugc/src/cli/formatters.ts new file mode 100644 index 00000000..50b3fbba --- /dev/null +++ b/packages/bugc/src/cli/formatters.ts @@ -0,0 +1,82 @@ +import * as Ir from "#ir"; + +export function formatJson( + data: unknown, + pretty: boolean, + excludeKeys?: string[], +): string { + if (excludeKeys && excludeKeys.length > 0) { + data = removeKeys(data, excludeKeys); + } + + // Use a custom replacer that properly handles repeated (non-circular) references + const seen = new WeakMap(); + let uniqueId = 0; + + // First pass: identify truly circular references + const findCircular = ( + obj: unknown, + ancestors: Set = new Set(), + ): void => { + if (typeof obj !== "object" || obj === null) return; + + if (ancestors.has(obj)) { + // This is a true circular reference + seen.set(obj, `__circular_${uniqueId++}__`); + return; + } + + ancestors.add(obj); + + if (Array.isArray(obj)) { + obj.forEach((item) => findCircular(item, ancestors)); + } else { + // Skip parent field when traversing to avoid false circular detection + Object.entries(obj).forEach(([key, value]) => { + if (key !== "parent") { + findCircular(value, ancestors); + } + }); + } + + ancestors.delete(obj); + }; + + findCircular(data); + + // Second pass: stringify with proper handling + return JSON.stringify( + data, + (key, value) => { + // Skip parent references to avoid circular references + if (key === "parent") return undefined; + + if (typeof value === "object" && value !== null && seen.has(value)) { + return "[Circular Reference]"; + } + + return value; + }, + pretty ? 2 : 0, + ); +} + +function removeKeys(obj: unknown, keysToRemove: string[]): unknown { + if (Array.isArray(obj)) { + return obj.map((item) => removeKeys(item, keysToRemove)); + } else if (obj !== null && typeof obj === "object") { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (!keysToRemove.includes(key)) { + result[key] = removeKeys(value, keysToRemove); + } + } + return result; + } + return obj; +} + +export function formatIrText(ir: Ir.Module, source?: string): string { + const formatter = new Ir.Analysis.Formatter(); + return formatter.format(ir, source); +} diff --git a/packages/bugc/src/cli/index.ts b/packages/bugc/src/cli/index.ts new file mode 100644 index 00000000..0bc6b349 --- /dev/null +++ b/packages/bugc/src/cli/index.ts @@ -0,0 +1,6 @@ +/** + * CLI module exports + */ + +export { handleCompileCommand } from "./compile.js"; +export { formatError } from "./error-formatter.js"; diff --git a/packages/bugc/src/cli/options.ts b/packages/bugc/src/cli/options.ts new file mode 100644 index 00000000..19904412 --- /dev/null +++ b/packages/bugc/src/cli/options.ts @@ -0,0 +1,39 @@ +import type { ParseArgsConfig } from "util"; + +export const commonOptions: ParseArgsConfig["options"] = { + help: { + type: "boolean", + short: "h", + default: false, + }, + output: { + type: "string", + short: "o", + }, +}; + +export const optimizationOption: ParseArgsConfig["options"] = { + optimize: { + type: "string", + short: "O", + default: "0", + }, +}; + +export const prettyOption: ParseArgsConfig["options"] = { + pretty: { + type: "boolean", + short: "p", + default: false, + }, +}; + +export function parseOptimizationLevel(value: string): number { + const level = parseInt(value, 10); + if (isNaN(level) || level < 0 || level > 3) { + throw new Error( + `Invalid optimization level: ${value}. Must be 0, 1, 2, or 3.`, + ); + } + return level; +} diff --git a/packages/bugc/src/cli/output.ts b/packages/bugc/src/cli/output.ts new file mode 100644 index 00000000..c0938919 --- /dev/null +++ b/packages/bugc/src/cli/output.ts @@ -0,0 +1,65 @@ +/** + * CLI output and display utilities + */ +/* eslint-disable no-console */ + +import { writeFileSync } from "fs"; +import type { BugError } from "#errors"; +import { Severity, type MessagesBySeverity } from "#result"; +import { formatError } from "./error-formatter.js"; + +/** + * Display compilation errors to stderr + */ +export function displayErrors( + messages: MessagesBySeverity, + source: string, +): void { + const errors = messages[Severity.Error] || []; + if (errors.length > 0) { + console.error("Compilation failed:\n"); + for (const error of errors) { + console.error(formatError(error, source)); + } + } +} + +/** + * Display compilation warnings to stderr + */ +export function displayWarnings( + messages: MessagesBySeverity, + source: string, +): void { + const warnings = messages[Severity.Warning] || []; + if (warnings.length > 0) { + console.error("Warnings:\n"); + for (const warning of warnings) { + console.error(formatError(warning, source)); + } + console.error(""); + } +} + +/** + * Write output to file or stdout + */ +export function writeOutput( + content: string, + options: { output?: string; message?: string }, +): void { + if (options.output) { + writeFileSync(options.output, content); + console.log(options.message || `Output written to ${options.output}`); + } else { + console.log(content); + } +} + +/** + * Exit with error message + */ +export function exitWithError(message: string): never { + console.error(message); + process.exit(1); +} diff --git a/packages/bugc/src/compiler/compile.ts b/packages/bugc/src/compiler/compile.ts new file mode 100644 index 00000000..dd8e8e60 --- /dev/null +++ b/packages/bugc/src/compiler/compile.ts @@ -0,0 +1,32 @@ +/** + * Unified compile interface for the BUG compiler + */ + +import { + buildSequence, + type SequenceNeeds, + type SequenceAdds, +} from "./sequence.js"; +import { + targetSequences, + type Target, + type TargetSequence, +} from "./sequences/index.js"; +import type { Result } from "#result"; +import type { BugError } from "#errors"; + +export type CompileOptions = { to: T } & SequenceNeeds< + TargetSequence +>; + +/** + * Compile BUG source code to the specified target + */ +export async function compile( + options: CompileOptions, +): Promise>, BugError>> { + const { to, ...input } = options; + + const sequence = buildSequence(targetSequences[to]); + return await sequence.run(input); +} diff --git a/packages/bugc/src/compiler/index.ts b/packages/bugc/src/compiler/index.ts new file mode 100644 index 00000000..6793b8f3 --- /dev/null +++ b/packages/bugc/src/compiler/index.ts @@ -0,0 +1,11 @@ +/** + * Compiler pass system for composing compilation passes + */ + +// Re-export everything from submodules +export type { Pass } from "./pass.js"; +export * from "./sequence.js"; +export * from "./sequences/index.js"; + +// Export new compile interface +export { compile, type CompileOptions } from "./compile.js"; diff --git a/packages/bugc/src/compiler/pass.ts b/packages/bugc/src/compiler/pass.ts new file mode 100644 index 00000000..a62a2bfe --- /dev/null +++ b/packages/bugc/src/compiler/pass.ts @@ -0,0 +1,26 @@ +import { Result } from "#result"; +import type { BugError } from "#errors"; + +export interface Pass { + run: Pass.Run; +} + +export namespace Pass { + export type Config = { + needs: unknown; + adds: unknown; + error: BugError; + }; + + export type Needs = C["needs"]; + export type Adds = C["adds"]; + export type Error = C["error"]; + + /** + * A compiler pass is a pure function that transforms input to output + * and may produce messages (errors/warnings) + */ + export type Run = ( + input: Pass.Needs, + ) => Promise, Pass.Error>>; +} diff --git a/packages/bugc/src/compiler/sequence.ts b/packages/bugc/src/compiler/sequence.ts new file mode 100644 index 00000000..0a1bb4df --- /dev/null +++ b/packages/bugc/src/compiler/sequence.ts @@ -0,0 +1,90 @@ +/** + * Pass sequence builder for composing compiler passes + */ + +import { Result, type MessagesBySeverity } from "#result"; +import type { Pass } from "./pass.js"; + +// Type helper to merge all needs from a sequence of passes +export type SequenceNeeds = Omit< + L extends readonly [Pass, ...infer R] + ? SequenceNeeds & Pass.Needs + : unknown, + keyof SequenceAdds +>; + +// Type helper to merge all adds from a sequence of passes +export type SequenceAdds = Pick< + _SequenceAdds, + keyof _SequenceAdds +>; + +type _SequenceAdds = L extends readonly [ + Pass, + ...infer R, +] + ? SequenceAdds & Pass.Adds + : unknown; + +// Type helper to union all error types from a sequence of passes +export type SequenceErrors = L extends readonly [ + Pass, + ...infer R, +] + ? SequenceErrors | Pass.Error + : never; + +// A sequence of passes that acts as a single pass +export type SequencePass = Pass<{ + needs: SequenceNeeds; + adds: SequenceAdds; + error: SequenceErrors; +}>; + +/** + * Build a sequence of passes that executes them in order + */ +export function buildSequence( + passes: L, +): SequencePass { + return { + async run( + input: SequenceNeeds, + ): Promise, SequenceErrors>> { + let currentState = input; + let messages: MessagesBySeverity> = {}; + + // Process each pass in sequence + for (const pass of passes) { + const result = await (pass as Pass).run(currentState); + + // Accumulate messages from this pass + messages = Result.mergeMessages( + messages, + result.messages as MessagesBySeverity>, + ); + + // If pass failed, return error with all messages so far + if (!result.success) { + return { + success: false, + messages, + }; + } + + // Update state for next pass by merging in the additions + currentState = { + ...currentState, + ...(result.value as SequenceAdds), + }; + } + + // All passes succeeded - return final state with all messages + return { + success: true, + value: currentState as SequenceAdds, + messages, + }; + }, + }; +} diff --git a/packages/bugc/src/compiler/sequences/index.ts b/packages/bugc/src/compiler/sequences/index.ts new file mode 100644 index 00000000..0a1c8f71 --- /dev/null +++ b/packages/bugc/src/compiler/sequences/index.ts @@ -0,0 +1,42 @@ +/** + * Concrete compilation sequences for different targets + */ + +import parsingPass from "#parser/pass"; +import typeCheckingPass from "#typechecker/pass"; +import irGenerationPass from "#irgen/pass"; +import { pass as optimizationPass } from "#optimizer/pass"; +import evmGenerationPass from "#evmgen/pass"; + +// AST-only sequence (just parsing) +export const astSequence = [parsingPass] as const; + +// Types sequence (parsing through type checking) +export const typesSequence = [parsingPass, typeCheckingPass] as const; + +// IR sequence (parsing through IR generation and optimization) +// Note: phi insertion is now integrated into irGenerationPass +export const irSequence = [ + parsingPass, + typeCheckingPass, + irGenerationPass, + optimizationPass, +] as const; + +// Bytecode sequence (parsing through bytecode generation) +export const bytecodeSequence = [...irSequence, evmGenerationPass] as const; + +// Future sequences will go here: +// export const debugSequence = [...bytecodeSequence, debugGenerationPass] as const; + +// Consolidated target sequences +export const targetSequences = { + ast: astSequence, + types: typesSequence, + ir: irSequence, + bytecode: bytecodeSequence, + // debug: debugSequence, +} as const; + +export type Target = keyof typeof targetSequences; +export type TargetSequence = (typeof targetSequences)[T]; diff --git a/packages/bugc/src/errors/base.ts b/packages/bugc/src/errors/base.ts new file mode 100644 index 00000000..4c6d14d6 --- /dev/null +++ b/packages/bugc/src/errors/base.ts @@ -0,0 +1,44 @@ +import type { SourceLocation } from "#ast"; +import { Severity } from "#result"; + +/** + * Base class for all BUG compiler errors + */ +export abstract class BugError extends Error { + public readonly code: string; + public readonly location?: SourceLocation; + public readonly severity: Severity; + + constructor( + message: string, + code: string, + location?: SourceLocation, + severity: Severity = Severity.Error, + ) { + super(message); + this.name = this.constructor.name; + this.code = code; + this.location = location; + this.severity = severity; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } + + /** + * Format the error with location information + */ + toString(): string { + const severityPrefix = + this.severity === Severity.Error ? "Error" : "Warning"; + const codePrefix = `[${this.code}]`; + + if (this.location) { + return `${severityPrefix} ${codePrefix}: ${this.message} at offset ${this.location.offset}`; + } + + return `${severityPrefix} ${codePrefix}: ${this.message}`; + } +} diff --git a/packages/bugc/src/errors/errors.test.ts b/packages/bugc/src/errors/errors.test.ts new file mode 100644 index 00000000..c1b5460c --- /dev/null +++ b/packages/bugc/src/errors/errors.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect } from "vitest"; +import { compile } from "#compiler"; +import { formatError } from "#cli"; +import { Severity, Result } from "#result"; + +import "#test/matchers"; + +describe("Standardized Error Handling", () => { + describe("Parse Errors", () => { + it("should return parse errors with location", async () => { + const source = ` +name Test + +storage { + [0] balance: uint256; +} +`; // Missing semicolon after name + + const result = await compile({ to: "ast", source }); + + expect(result.success).toBe(false); + expect(result).toHaveMessage({ + severity: Severity.Error, + code: "PARSE_ERROR", + message: "Parse error", + }); + + const error = Result.firstError(result); + expect(error).toBeDefined(); + expect(error?.location).toBeDefined(); + }); + + it("should handle multiple parse errors", async () => { + const source = ` +name Test + +storage { + [0 balance uint256; // Missing ] and : +} + +code { + x = +} +`; + + const result = await compile({ to: "ast", source }); + + expect(result.success).toBe(false); + expect(Result.hasErrors(result)).toBe(true); + }); + }); + + describe("Type Errors", () => { + it("should return type errors with location", async () => { + const source = ` +name Test; + +storage { + [0] balance: uint256; +} + +code { + balance = "hello"; // Type mismatch +} +`; + + const result = await compile({ to: "ir", source }); + + expect(result.success).toBe(false); + + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Type mismatch", + }); + }); + + it("should collect multiple type errors", async () => { + const source = ` +name Test; + +storage { + [0] balance: uint256; + [1] flag: bool; +} + +code { + balance = true; // Type error 1 + flag = 42; // Type error 2 + unknown = 123; // Type error 3: undefined variable +} +`; + + const result = await compile({ to: "ir", source }); + + expect(result.success).toBe(false); + + const typeErrors = Result.findMessages(result, { + severity: Severity.Error, + }).filter((e) => e.code.startsWith("TYPE")); + expect(typeErrors.length).toBeGreaterThanOrEqual(3); + }); + }); + + describe("IR Generation Errors", () => { + it("should handle IR errors gracefully", async () => { + // Since parser doesn't support call expressions, we'll test a different IR error + const source = ` +name Test; + +storage { + [0] x: uint256; +} + +code { + // This will cause an IR error - using undefined variable + let temp = undefinedVar + 1; +} +`; + + const result = await compile({ to: "ir", source }); + + // Should get a type error for undefined variable + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Undefined variable", + }); + }); + }); + + describe("Error Formatting", () => { + it("should format errors nicely", async () => { + const source = `name Test`; // Missing semicolon + const result = await compile({ to: "ast", source }); + + expect(result.success).toBe(false); + + const error = Result.firstError(result)!; + const formatted = formatError(error, source); + + expect(formatted).toContain("❌ Error [PARSE_ERROR]"); + expect(formatted).toContain("at offset"); + }); + + it("should show source snippet in error", async () => { + const source = `name Test +storage { + [0] x: unknowntype; +}`; + + const result = await compile({ to: "ast", source }); + + const firstError = Result.firstError(result); + if (!result.success && firstError) { + const formatted = formatError(firstError, source); + // Should show line numbers and context + expect(formatted).toMatch(/\d+ \|/); // Line number format + } + }); + }); + + describe("Diagnostic Collection", () => { + it("should accumulate messages across phases", async () => { + const source = ` +name Test; + +storage { + [0] x: uint256; +} + +code { + y = x + z; // y and z are undefined +} +`; + + const result = await compile({ to: "ir", source }); + + // Should have type errors for undefined variables + const typeErrors = Result.findMessages(result, { + severity: Severity.Error, + }).filter((e) => e.code.startsWith("TYPE")); + expect(typeErrors.length).toBeGreaterThan(0); + }); + + it("should distinguish between errors and warnings", async () => { + // Once we add warnings, this test will verify they're handled properly + const result = await compile({ to: "ir", source: "name Test;" }); + + if (result.success) { + // Check for any warnings + const warnings = Result.warnings(result); + expect(warnings).toBeDefined(); // Just check the structure exists + } + }); + }); +}); diff --git a/packages/bugc/src/errors/index.ts b/packages/bugc/src/errors/index.ts new file mode 100644 index 00000000..863a312d --- /dev/null +++ b/packages/bugc/src/errors/index.ts @@ -0,0 +1 @@ +export { BugError } from "./base.js"; diff --git a/packages/bugc/src/evm/analysis/formatter.ts b/packages/bugc/src/evm/analysis/formatter.ts new file mode 100644 index 00000000..8998fbb7 --- /dev/null +++ b/packages/bugc/src/evm/analysis/formatter.ts @@ -0,0 +1,153 @@ +/** + * EVM instruction formatting utilities + */ + +import * as Format from "@ethdebug/format"; + +import type { Instruction, InstructionDebug } from "#evm/spec"; +import { Analysis as AstAnalysis } from "#ast"; + +/** + * Formats EVM instructions for display + */ +export class EvmFormatter { + /** + * Format instruction objects as assembly text + */ + static formatInstructions( + instructions: Instruction[], + source?: string, + ): string { + const lines: string[] = []; + let offset = 0; + + for (const inst of instructions) { + // Add source comment if debug info exists + const sourceComment = this.formatSourceComment(inst.debug, source); + if (sourceComment) { + lines.push(sourceComment); + } + + // Format the instruction + let line = `${offset.toString().padStart(4, "0")}: ${inst.mnemonic}`; + if (inst.immediates && inst.immediates.length > 0) { + const dataHex = inst.immediates + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + line += ` 0x${dataHex}`; + } + + lines.push(line); + + // Update offset for next instruction + offset += 1 + (inst.immediates?.length || 0); + } + + return lines.join("\n"); + } + + /** + * Format debug information as source comment + */ + private static formatSourceComment( + debug?: InstructionDebug, + source?: string, + ): string { + if (!debug?.context || !source) { + return ""; + } + + const context = debug.context; + + // Handle pick context - show all source locations on one line + if ("pick" in context && Array.isArray(context.pick)) { + const locations: string[] = []; + for (const pickContext of context.pick) { + const location = this.extractSourceLocation(pickContext, source); + if (location) { + locations.push(location); + } + } + if (locations.length > 0) { + return `; source: ${locations.join(" or ")}`; + } + return ""; + } + + // Handle direct code context + return this.formatContextSourceComment(context, source); + } + + /** + * Format a single context as source comment + */ + private static formatContextSourceComment( + context: Format.Program.Context | undefined, + source: string, + ): string { + if (!context) { + return ""; + } + + const parts: string[] = []; + + // Check for remark (ethdebug format) + if (Format.Program.Context.isRemark(context)) { + parts.push(`; ${context.remark}`); + } + + // Check for code.range (ethdebug format) + if (Format.Program.Context.isCode(context) && context.code.range) { + const range = context.code.range; + if ( + typeof range.offset === "number" && + typeof range.length === "number" + ) { + // Convert to AST SourceLocation format for the existing formatter + const loc = { + offset: range.offset, + length: range.length, + }; + const sourceComment = AstAnalysis.formatSourceComment(loc, source); + if (sourceComment) { + parts.push(sourceComment); + } + } + } + + return parts.join("\n"); + } + + /** + * Extract just the location part from a context (for pick contexts) + */ + private static extractSourceLocation( + context: Format.Program.Context | undefined, + source: string, + ): string | null { + if (!context) { + return null; + } + + // Check for code.range (ethdebug format) + if (Format.Program.Context.isCode(context) && context.code.range) { + const range = context.code.range; + if ( + typeof range.offset === "number" && + typeof range.length === "number" + ) { + // Convert to AST SourceLocation format and extract just the location + const loc = { + offset: range.offset, + length: range.length, + }; + const fullComment = AstAnalysis.formatSourceComment(loc, source); + // Extract just the location part (e.g., "16:3-11" from "; source: 16:3-11") + const match = fullComment.match(/; source: (.+)/); + return match ? match[1] : null; + } + } + + return null; + } +} diff --git a/packages/bugc/src/evm/analysis/index.ts b/packages/bugc/src/evm/analysis/index.ts new file mode 100644 index 00000000..b52ed32e --- /dev/null +++ b/packages/bugc/src/evm/analysis/index.ts @@ -0,0 +1,5 @@ +/** + * EVM analysis utilities exports + */ + +export { EvmFormatter } from "./formatter.js"; diff --git a/packages/bugc/src/evm/index.ts b/packages/bugc/src/evm/index.ts new file mode 100644 index 00000000..727c5ed5 --- /dev/null +++ b/packages/bugc/src/evm/index.ts @@ -0,0 +1,2 @@ +export * as Analysis from "./analysis/index.js"; +export * from "./spec/index.js"; diff --git a/packages/bugc/src/evm/spec/builder.ts b/packages/bugc/src/evm/spec/builder.ts new file mode 100644 index 00000000..46ae60d6 --- /dev/null +++ b/packages/bugc/src/evm/spec/builder.ts @@ -0,0 +1,79 @@ +import type { $ } from "./hkts.js"; + +import type { Stack } from "./stack.js"; +import type { State } from "./state.js"; +import { makeRebrands } from "./rebrand.js"; + +export type Transition = ( + state: $, +) => $; + +export const makePipe = + (controls: State.Controls) => + () => + new PipeBuilder(controls, (state) => state); + +export class PipeBuilder { + constructor( + private readonly controls: State.Controls, + private readonly transition: Transition, + ) {} + + err(error: Error): PipeBuilder { + return this.then(() => { + throw error; + }); + } + + peek( + func: ( + state: $, + builder: PipeBuilder, + ) => PipeBuilder, + ): PipeBuilder { + const newTransition = (initialState: $) => { + const currentState = this.transition(initialState); + const continuationBuilder = new PipeBuilder( + this.controls, + (s) => s, + ); + const resultBuilder = func(currentState, continuationBuilder); + return resultBuilder.transition(currentState); + }; + + return new PipeBuilder(this.controls, newTransition); + } + + then(func: Transition): PipeBuilder; + then( + func: Transition, + options: ThenOptions, + ): PipeBuilder; + then( + func: (state: $) => $, + options?: ThenOptions, + ) { + const { rebrandTop } = makeRebrands(this.controls); + + const newTransition = (state: $) => func(this.transition(state)); + + if (!options) { + return new PipeBuilder(this.controls, newTransition); + } + + return new PipeBuilder( + this.controls, + (state: $) => rebrandTop(options.as)(newTransition(state)), + ); + } + + done() { + return (state: $): $ => { + return this.transition(state); + }; + } +} + +export interface ThenOptions { + as: B; +} diff --git a/packages/bugc/src/evm/spec/definitions.ts b/packages/bugc/src/evm/spec/definitions.ts new file mode 100644 index 00000000..89f4933e --- /dev/null +++ b/packages/bugc/src/evm/spec/definitions.ts @@ -0,0 +1,2203 @@ +/** + * Complete EVM instruction set definitions with type-safe stack operations. + * + * This file provides factory functions for all EVM instructions, mapping each + * opcode to a type-safe operation that correctly handles stack manipulation. + * Instructions are organized by functionality and include: + * + * - Arithmetic operations (ADD, MUL, SUB, etc.) + * - Comparison operations (LT, GT, EQ, etc.) + * - Bitwise operations (AND, OR, XOR, etc.) + * - Stack operations (POP, PUSH*, DUP*, SWAP*) + * - Memory/Storage operations (MLOAD, SLOAD, etc.) + * - Environment/blockchain data access + * - Control flow operations (JUMP, JUMPI, etc.) + * - System operations (CALL, CREATE, etc.) + * + * Each instruction is defined with precise stack consumption/production patterns + * using semantic stack brands for type safety. + */ + +import type { $ } from "./hkts.js"; + +import { type Stack } from "./stack.js"; + +import { type InstructionOptions, type State, Specifiers } from "./state.js"; + +export type Operations = ReturnType>; + +/** + * Creates a complete set of type-safe EVM operations from unsafe state controls. + * Returns an object mapping instruction mnemonics to their operation functions. + */ +export const makeOperations = (controls: State.Controls) => { + const { + mapInstruction, + makeOperationForInstruction, + makeOperationWithImmediatesForInstruction, + } = Specifiers.makeUsing(controls); + + return { + /* + * ============================================ + * 0x00: Stop and Control Flow + * ============================================ + */ + ...mapInstruction( + { opcode: 0x00, mnemonic: "STOP" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x01-0x07: Arithmetic Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x01, mnemonic: "ADD" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a + b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x02, mnemonic: "MUL" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a * b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x03, mnemonic: "SUB" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a - b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x04, mnemonic: "DIV" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a // b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x05, mnemonic: "SDIV" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a // b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x06, mnemonic: "MOD" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a % b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x07, mnemonic: "SMOD" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a % b"] as const, + }), + ), + + /* + * ============================================ + * 0x08-0x09: Modular Arithmetic + * ============================================ + */ + ...mapInstruction( + { opcode: 0x08, mnemonic: "ADDMOD" } as const, + makeOperationForInstruction({ + consumes: ["a", "b", "N"] as const, + produces: ["(a + b) % N"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x09, mnemonic: "MULMOD" } as const, + makeOperationForInstruction({ + consumes: ["a", "b", "N"] as const, + produces: ["(a * b) % N"] as const, + }), + ), + + /* + * ============================================ + * 0x0a: Exponentiation + * ============================================ + */ + ...mapInstruction( + { opcode: 0x0a, mnemonic: "EXP" } as const, + makeOperationForInstruction({ + consumes: ["a", "exponent"] as const, + produces: ["a ** exponent"] as const, + }), + ), + + /* + * ============================================ + * 0x0b: Sign Extension + * ============================================ + */ + ...mapInstruction( + { opcode: 0x0b, mnemonic: "SIGNEXTEND" } as const, + makeOperationForInstruction({ + consumes: ["b", "x"] as const, + produces: ["y"] as const, + }), + ), + + /* + * ============================================ + * 0x0c-0x0f: Undefined opcodes + * ============================================ + * { opcode: 0x0c, mnemonic: "???" } + * { opcode: 0x0d, mnemonic: "???" } + * { opcode: 0x0e, mnemonic: "???" } + * { opcode: 0x0f, mnemonic: "???" } + */ + + /* + * ============================================ + * 0x10-0x14: Comparison Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x10, mnemonic: "LT" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a < b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x11, mnemonic: "GT" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a > b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x12, mnemonic: "SLT" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a < b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x13, mnemonic: "SGT" }, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a > b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x14, mnemonic: "EQ" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a == b"] as const, + }), + ), + + /* + * ============================================ + * 0x15: Zero Check + * ============================================ + */ + ...mapInstruction( + { opcode: 0x15, mnemonic: "ISZERO" } as const, + makeOperationForInstruction({ + consumes: ["a"] as const, + produces: ["a == 0"] as const, + }), + ), + + /* + * ============================================ + * 0x16-0x18: Bitwise Logic Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x16, mnemonic: "AND" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a & b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x17, mnemonic: "OR" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a | b"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x18, mnemonic: "XOR" } as const, + makeOperationForInstruction({ + consumes: ["a", "b"] as const, + produces: ["a ^ b"] as const, + }), + ), + + /* + * ============================================ + * 0x19: Bitwise NOT + * ============================================ + */ + ...mapInstruction( + { opcode: 0x19, mnemonic: "NOT" } as const, + makeOperationForInstruction({ + consumes: ["a"] as const, + produces: ["~a"] as const, + }), + ), + + /* + * ============================================ + * 0x1a: Byte Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x1a, mnemonic: "BYTE" } as const, + makeOperationForInstruction({ + consumes: ["i", "x"] as const, + produces: ["y"] as const, + }), + ), + + /* + * ============================================ + * 0x1b-0x1d: Shift Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x1b, mnemonic: "SHL" } as const, + makeOperationForInstruction({ + consumes: ["shift", "value"] as const, + produces: ["value << shift"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x1c, mnemonic: "SHR" } as const, + makeOperationForInstruction({ + consumes: ["shift", "value"] as const, + produces: ["value >> shift"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x1d, mnemonic: "SAR" } as const, + makeOperationForInstruction({ + consumes: ["shift", "value"] as const, + produces: ["value >> shift"] as const, + }), + ), + + /* + * ============================================ + * 0x1e-0x1f: Undefined opcodes + * ============================================ + * { opcode: 0x1e, mnemonic: "???" } + * { opcode: 0x1f, mnemonic: "???" } + */ + + /* + * ============================================ + * 0x20: Keccak256 Hash + * ============================================ + */ + ...mapInstruction( + { opcode: 0x20, mnemonic: "KECCAK256" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size"] as const, + produces: ["hash"] as const, + }), + ), + + /* + * ============================================ + * 0x21-0x2f: Undefined opcodes + * ============================================ + * { opcode: 0x21, mnemonic: "???" } + * { opcode: 0x22, mnemonic: "???" } + * { opcode: 0x23, mnemonic: "???" } + * { opcode: 0x24, mnemonic: "???" } + * { opcode: 0x25, mnemonic: "???" } + * { opcode: 0x26, mnemonic: "???" } + * { opcode: 0x27, mnemonic: "???" } + * { opcode: 0x28, mnemonic: "???" } + * { opcode: 0x29, mnemonic: "???" } + * { opcode: 0x2a, mnemonic: "???" } + * { opcode: 0x2b, mnemonic: "???" } + * { opcode: 0x2c, mnemonic: "???" } + * { opcode: 0x2d, mnemonic: "???" } + * { opcode: 0x2e, mnemonic: "???" } + * { opcode: 0x2f, mnemonic: "???" } + */ + + /* + * ============================================ + * 0x30-0x34: Environment Information + * ============================================ + */ + ...mapInstruction( + { opcode: 0x30, mnemonic: "ADDRESS" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["address"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x31, mnemonic: "BALANCE" } as const, + makeOperationForInstruction({ + consumes: ["address"] as const, + produces: ["balance"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x32, mnemonic: "ORIGIN" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["address"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x33, mnemonic: "CALLER" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["address"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x34, mnemonic: "CALLVALUE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + + /* + * ============================================ + * 0x35-0x39: Input Data Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x35, mnemonic: "CALLDATALOAD" } as const, + makeOperationForInstruction({ + consumes: ["i"] as const, + produces: ["data[i]"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x36, mnemonic: "CALLDATASIZE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["size"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x37, mnemonic: "CALLDATACOPY" } as const, + makeOperationForInstruction({ + consumes: ["destOffset", "offset", "size"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x38, mnemonic: "CODESIZE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["size"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x39, mnemonic: "CODECOPY" } as const, + makeOperationForInstruction({ + consumes: ["destOffset", "offset", "size"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x3a-0x3f: External Information + * ============================================ + */ + ...mapInstruction( + { opcode: 0x3a, mnemonic: "GASPRICE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["price"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x3b, mnemonic: "EXTCODESIZE" } as const, + makeOperationForInstruction({ + consumes: ["address"] as const, + produces: ["size"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x3c, mnemonic: "EXTCODECOPY" } as const, + makeOperationForInstruction({ + consumes: ["address", "destOffset", "offset", "size"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x3d, mnemonic: "RETURNDATASIZE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["size"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x3e, mnemonic: "RETURNDATACOPY" } as const, + makeOperationForInstruction({ + consumes: ["destOffset", "offset", "size"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x3f, mnemonic: "EXTCODEHASH" } as const, + makeOperationForInstruction({ + consumes: ["address"] as const, + produces: ["hash"] as const, + }), + ), + + /* + * ============================================ + * 0x40-0x49: Block Information + * ============================================ + */ + ...mapInstruction( + { opcode: 0x40, mnemonic: "BLOCKHASH" } as const, + makeOperationForInstruction({ + consumes: ["blockNumber"] as const, + produces: ["hash"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x41, mnemonic: "COINBASE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["address"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x42, mnemonic: "TIMESTAMP" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["timestamp"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x43, mnemonic: "NUMBER" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["blockNumber"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x44, mnemonic: "PREVRANDAO" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["difficulty"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x45, mnemonic: "GASLIMIT" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["gasLimit"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x46, mnemonic: "CHAINID" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["chainId"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x47, mnemonic: "SELFBALANCE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["balance"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x48, mnemonic: "BASEFEE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["baseFee"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x49, mnemonic: "BLOBHASH" } as const, + makeOperationForInstruction({ + consumes: ["index"] as const, + produces: ["blobVersionedHashesAtIndex"] as const, + }), + ), + + /* + * ============================================ + * 0x4a: Blob Base Fee + * ============================================ + */ + ...mapInstruction( + { opcode: 0x4a, mnemonic: "BLOBBASEFEE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["blobBaseFee"] as const, + }), + ), + + /* + * ============================================ + * 0x4b-0x4f: Undefined opcodes + * ============================================ + * { opcode: 0x4b, mnemonic: "???" } + * { opcode: 0x4c, mnemonic: "???" } + * { opcode: 0x4d, mnemonic: "???" } + * { opcode: 0x4e, mnemonic: "???" } + * { opcode: 0x4f, mnemonic: "???" } + */ + + /* + * ============================================ + * 0x50: Stack Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x50, mnemonic: "POP" } as const, + makeOperationForInstruction({ + consumes: ["y"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x51-0x53: Memory Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x51, mnemonic: "MLOAD" } as const, + makeOperationForInstruction({ + consumes: ["offset"] as const, + produces: ["value"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x52, mnemonic: "MSTORE" } as const, + makeOperationForInstruction({ + consumes: ["offset", "value"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x53, mnemonic: "MSTORE8" } as const, + makeOperationForInstruction({ + consumes: ["offset", "value"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x54-0x55: Storage Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x54, mnemonic: "SLOAD" } as const, + makeOperationForInstruction({ + consumes: ["key"] as const, + produces: ["value"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x55, mnemonic: "SSTORE" } as const, + makeOperationForInstruction({ + consumes: ["key", "value"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x56-0x5b: Flow Operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x56, mnemonic: "JUMP" } as const, + makeOperationForInstruction({ + consumes: ["counter"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x57, mnemonic: "JUMPI" } as const, + makeOperationForInstruction({ + consumes: ["counter", "b"] as const, + produces: [] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x58, mnemonic: "PC" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["counter"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x59, mnemonic: "MSIZE" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["size"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x5a, mnemonic: "GAS" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["gas"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x5b, mnemonic: "JUMPDEST" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x5c-0x5d: Transient Storage + * ============================================ + */ + ...mapInstruction( + { opcode: 0x5c, mnemonic: "TLOAD" } as const, + makeOperationForInstruction({ + consumes: ["key"] as const, + produces: ["value"] as const, + }), + ), + + ...mapInstruction( + { opcode: 0x5d, mnemonic: "TSTORE" } as const, + makeOperationForInstruction({ + consumes: ["key", "value"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x5e: Memory Copy + * ============================================ + */ + ...mapInstruction( + { opcode: 0x5e, mnemonic: "MCOPY" } as const, + makeOperationForInstruction({ + consumes: ["destOffset", "offset", "size"] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0x5f: PUSH0 + * ============================================ + */ + ...mapInstruction( + { opcode: 0x5f, mnemonic: "PUSH0" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + + /* + * ============================================ + * 0x60-0x7f: PUSH1 through PUSH32 + * ============================================ + */ + ...mapInstruction( + { opcode: 0x60, mnemonic: "PUSH1" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x61, mnemonic: "PUSH2" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x62, mnemonic: "PUSH3" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x63, mnemonic: "PUSH4" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x64, mnemonic: "PUSH5" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x65, mnemonic: "PUSH6" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x66, mnemonic: "PUSH7" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x67, mnemonic: "PUSH8" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x68, mnemonic: "PUSH9" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x69, mnemonic: "PUSH10" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6a, mnemonic: "PUSH11" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6b, mnemonic: "PUSH12" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6c, mnemonic: "PUSH13" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6d, mnemonic: "PUSH14" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6e, mnemonic: "PUSH15" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x6f, mnemonic: "PUSH16" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x70, mnemonic: "PUSH17" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x71, mnemonic: "PUSH18" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x72, mnemonic: "PUSH19" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x73, mnemonic: "PUSH20" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x74, mnemonic: "PUSH21" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x75, mnemonic: "PUSH22" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x76, mnemonic: "PUSH23" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x77, mnemonic: "PUSH24" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x78, mnemonic: "PUSH25" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x79, mnemonic: "PUSH26" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7a, mnemonic: "PUSH27" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7b, mnemonic: "PUSH28" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7c, mnemonic: "PUSH29" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7d, mnemonic: "PUSH30" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7e, mnemonic: "PUSH31" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + ...mapInstruction( + { opcode: 0x7f, mnemonic: "PUSH32" } as const, + makeOperationWithImmediatesForInstruction({ + consumes: [] as const, + produces: ["value"] as const, + }), + ), + + /* + * ============================================ + * 0x80-0x8f: DUP operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x80, mnemonic: "DUP1" } as const, + (instruction) => + (options?: InstructionOptions) => + ( + initialState: $, + ): $ => { + // DUP1 duplicates the top stack item + // Stack: [value, ...] -> [value, value, ...] + const [a] = controls.topN(initialState, 1); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(a, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x81, mnemonic: "DUP2" } as const, + (instruction) => + (options?: InstructionOptions) => + ( + initialState: $, + ): $ => { + const [_a, b] = controls.topN(initialState, 2); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(b, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x82, mnemonic: "DUP3" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, c] = controls.topN(initialState, 3); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(c, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x83, mnemonic: "DUP4" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, d] = controls.topN(initialState, 4); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(d, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x84, mnemonic: "DUP5" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, e] = controls.topN(initialState, 5); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(e, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x85, mnemonic: "DUP6" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, _e, f] = controls.topN(initialState, 6); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(f, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x86, mnemonic: "DUP7" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, _e, _f, g] = controls.topN(initialState, 7); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(g, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x87, mnemonic: "DUP8" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, _e, _f, _g, h] = controls.topN( + initialState, + 8, + ); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(h, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x88, mnemonic: "DUP9" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, _e, _f, _g, _h, i] = controls.topN( + initialState, + 9, + ); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(i, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x89, mnemonic: "DUP10" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, j] = controls.topN( + initialState, + 10, + ); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(j, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8a, mnemonic: "DUP11" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, ...S]] + >, + ): $ => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, k] = controls.topN( + initialState, + 11, + ); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(k, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8b, mnemonic: "DUP12" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, ...S]] + >, + ): $ => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, l] = controls.topN( + initialState, + 12, + ); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(l, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8c, mnemonic: "DUP13" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, ...S]] + >, + ): $< + U, + [readonly [M, A, B, C, D, E, F, G, H, I, J, K, L, M, ...S]] + > => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, m] = + controls.topN(initialState, 13); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(m, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8d, mnemonic: "DUP14" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, ...S]] + >, + ): $< + U, + [readonly [N, A, B, C, D, E, F, G, H, I, J, K, L, M, N, ...S]] + > => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, n] = + controls.topN(initialState, 14); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(n, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8e, mnemonic: "DUP15" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + O extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, ...S]] + >, + ): $< + U, + [readonly [O, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, ...S]] + > => { + const [_a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, o] = + controls.topN(initialState, 15); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(o, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + ...mapInstruction( + { opcode: 0x8f, mnemonic: "DUP16" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + O extends Stack.Brand, + P extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, ...S]] + >, + ): $< + U, + [readonly [P, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, ...S]] + > => { + const [ + _a, + _b, + _c, + _d, + _e, + _f, + _g, + _h, + _i, + _j, + _k, + _l, + _m, + _n, + _o, + p, + ] = controls.topN(initialState, 16); + const { id, state } = controls.generateId(initialState, "dup"); + const stateWithPush = controls.push(state, controls.duplicate(p, id)); + return controls.emit(stateWithPush, { ...instruction, ...options }); + }, + ), + + /* + * ============================================ + * 0x90-0x9f: SWAP operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0x90, mnemonic: "SWAP1" } as const, + (instruction) => + (options?: InstructionOptions) => + ( + initialState: $, + ): $ => { + // SWAP1 exchanges 1st and 2nd stack items + // Get the top 2 items with their IDs and brands + const [a, b] = controls.topN(initialState, 2); + + // Pop the top 2 items + let state = controls.popN(initialState, 2); + + // Push them back in swapped order + // Original order was [A, B], we want [B, A] + // Push A first (items[0]), then B (items[1]) to get B on top + state = controls.push(state, a); + state = controls.push(state, b); + + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x91, mnemonic: "SWAP2" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + // SWAP2 exchanges 1st and 3rd stack items + const [a, b, c] = controls.topN(initialState, 3); + + let state = controls.popN(initialState, 3); + + // Push in order to get [C, B, A] on stack + state = controls.push(state, a); + state = controls.push(state, b); + state = controls.push(state, c); + + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x92, mnemonic: "SWAP3" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + // SWAP3 exchanges 1st and 4th stack items + const [a, b, c, d] = controls.topN(initialState, 4); + + let state = controls.popN(initialState, 4); + + // Push in order to get [D, B, C, A] on stack + state = controls.push(state, a); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, d); + + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x93, mnemonic: "SWAP4" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + // SWAP4 exchanges 1st and 5th stack items + const [a, b, c, d, e] = controls.topN(initialState, 5); + + let state = controls.popN(initialState, 5); + + // Push in order to get [E, B, C, D, A] on stack + state = controls.push(state, a); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, e); + + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x94, mnemonic: "SWAP5" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [a, b, c, d, e, f] = controls.topN(initialState, 6); + let state = controls.popN(initialState, 6); + state = controls.push(state, a); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, f); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x95, mnemonic: "SWAP6" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [a, b, c, d, e, f, g] = controls.topN(initialState, 7); + let state = controls.popN(initialState, 7); + state = controls.push(state, a); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, g); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x96, mnemonic: "SWAP7" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [a, b, c, d, e, f, g, h] = controls.topN(initialState, 8); + let state = controls.popN(initialState, 8); + state = controls.push(state, a); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, h); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x97, mnemonic: "SWAP8" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [a, b, c, d, e, f, g, h, i] = controls.topN(initialState, 9); + let state = controls.popN(initialState, 9); + state = controls.push(state, a); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, i); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x98, mnemonic: "SWAP9" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + S extends Stack, + >( + initialState: $, + ): $ => { + const [a, b, c, d, e, f, g, h, i, j] = controls.topN( + initialState, + 10, + ); + let state = controls.popN(initialState, 10); + state = controls.push(state, a); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, j); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x99, mnemonic: "SWAP10" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, ...S]] + >, + ): $ => { + const [a, b, c, d, e, f, g, h, i, j, k] = controls.topN( + initialState, + 11, + ); + let state = controls.popN(initialState, 11); + state = controls.push(state, a); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, k); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9a, mnemonic: "SWAP11" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, ...S]] + >, + ): $ => { + const [a, b, c, d, e, f, g, h, i, j, k, l] = controls.topN( + initialState, + 12, + ); + let state = controls.popN(initialState, 12); + state = controls.push(state, a); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, l); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9b, mnemonic: "SWAP12" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, ...S]] + >, + ): $ => { + const [a, b, c, d, e, f, g, h, i, j, k, l, m] = controls.topN( + initialState, + 13, + ); + let state = controls.popN(initialState, 13); + state = controls.push(state, a); + state = controls.push(state, l); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, m); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9c, mnemonic: "SWAP13" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, ...S]] + >, + ): $< + U, + [readonly [N, B, C, D, E, F, G, H, I, J, K, L, M, A, ...S]] + > => { + const [a, b, c, d, e, f, g, h, i, j, k, l, m, n] = controls.topN( + initialState, + 14, + ); + let state = controls.popN(initialState, 14); + state = controls.push(state, a); + state = controls.push(state, m); + state = controls.push(state, l); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, n); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9d, mnemonic: "SWAP14" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + O extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, ...S]] + >, + ): $< + U, + [readonly [O, B, C, D, E, F, G, H, I, J, K, L, M, N, A, ...S]] + > => { + const [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o] = controls.topN( + initialState, + 15, + ); + let state = controls.popN(initialState, 15); + state = controls.push(state, a); + state = controls.push(state, n); + state = controls.push(state, m); + state = controls.push(state, l); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, o); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9e, mnemonic: "SWAP15" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + O extends Stack.Brand, + P extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, ...S]] + >, + ): $< + U, + [readonly [P, B, C, D, E, F, G, H, I, J, K, L, M, N, O, A, ...S]] + > => { + const [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = + controls.topN(initialState, 16); + let state = controls.popN(initialState, 16); + state = controls.push(state, a); + state = controls.push(state, o); + state = controls.push(state, n); + state = controls.push(state, m); + state = controls.push(state, l); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, p); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + ...mapInstruction( + { opcode: 0x9f, mnemonic: "SWAP16" } as const, + (instruction) => + (options?: InstructionOptions) => + < + A extends Stack.Brand, + B extends Stack.Brand, + C extends Stack.Brand, + D extends Stack.Brand, + E extends Stack.Brand, + F extends Stack.Brand, + G extends Stack.Brand, + H extends Stack.Brand, + I extends Stack.Brand, + J extends Stack.Brand, + K extends Stack.Brand, + L extends Stack.Brand, + M extends Stack.Brand, + N extends Stack.Brand, + O extends Stack.Brand, + P extends Stack.Brand, + Q extends Stack.Brand, + S extends Stack, + >( + initialState: $< + U, + [readonly [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, ...S]] + >, + ): $< + U, + [readonly [Q, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, A, ...S]] + > => { + const [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q] = + controls.topN(initialState, 17); + let state = controls.popN(initialState, 17); + state = controls.push(state, a); + state = controls.push(state, p); + state = controls.push(state, o); + state = controls.push(state, n); + state = controls.push(state, m); + state = controls.push(state, l); + state = controls.push(state, k); + state = controls.push(state, j); + state = controls.push(state, i); + state = controls.push(state, h); + state = controls.push(state, g); + state = controls.push(state, f); + state = controls.push(state, e); + state = controls.push(state, d); + state = controls.push(state, c); + state = controls.push(state, b); + state = controls.push(state, q); + return controls.emit(state, { ...instruction, ...options }); + }, + ), + + /* + * ============================================ + * 0xa0-0xa4: LOG operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0xa0, mnemonic: "LOG0" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xa1, mnemonic: "LOG1" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size", "topic"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xa2, mnemonic: "LOG2" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size", "topic1", "topic2"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xa3, mnemonic: "LOG3" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size", "topic1", "topic2", "topic3"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xa4, mnemonic: "LOG4" } as const, + makeOperationForInstruction({ + consumes: [ + "offset", + "size", + "topic1", + "topic2", + "topic3", + "topic4", + ] as const, + produces: [] as const, + }), + ), + + /* + * ============================================ + * 0xf0-0xff: System operations + * ============================================ + */ + ...mapInstruction( + { opcode: 0xf0, mnemonic: "CREATE" } as const, + makeOperationForInstruction({ + consumes: ["value", "offset", "size"] as const, + produces: ["address"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xf1, mnemonic: "CALL" } as const, + makeOperationForInstruction({ + consumes: [ + "gas", + "address", + "value", + "argsOffset", + "argsSize", + "retOffset", + "retSize", + ] as const, + produces: ["success"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xf2, mnemonic: "CALLCODE" } as const, + makeOperationForInstruction({ + consumes: [ + "gas", + "address", + "value", + "argsOffset", + "argsSize", + "retOffset", + "retSize", + ] as const, + produces: ["success"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xf3, mnemonic: "RETURN" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xf4, mnemonic: "DELEGATECALL" } as const, + makeOperationForInstruction({ + consumes: [ + "gas", + "address", + "argsOffset", + "argsSize", + "retOffset", + "retSize", + ] as const, + produces: ["success"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xf5, mnemonic: "CREATE2" } as const, + makeOperationForInstruction({ + consumes: ["value", "offset", "size", "salt"] as const, + produces: ["address"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xfa, mnemonic: "STATICCALL" } as const, + makeOperationForInstruction({ + consumes: [ + "gas", + "address", + "argsOffset", + "argsSize", + "retOffset", + "retSize", + ] as const, + produces: ["success"] as const, + }), + ), + ...mapInstruction( + { opcode: 0xfd, mnemonic: "REVERT" } as const, + makeOperationForInstruction({ + consumes: ["offset", "size"] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xfe, mnemonic: "INVALID" } as const, + makeOperationForInstruction({ + consumes: [] as const, + produces: [] as const, + }), + ), + ...mapInstruction( + { opcode: 0xff, mnemonic: "SELFDESTRUCT" } as const, + makeOperationForInstruction({ + consumes: ["address"] as const, + produces: [] as const, + }), + ), + } as const; +}; diff --git a/packages/bugc/src/evm/spec/hkts.ts b/packages/bugc/src/evm/spec/hkts.ts new file mode 100644 index 00000000..1a9dab09 --- /dev/null +++ b/packages/bugc/src/evm/spec/hkts.ts @@ -0,0 +1,138 @@ +/* + * To support recent TypeScript compiler changes, this file has + * been copied with small modifications from https://github.com/pelotom/hkts. + * The following copyright and license apply in this file only: + * + * The MIT License (MIT) + * + * Copyright (c) 2018 Tom Crockett + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +declare const index: unique symbol; + +/** + * Placeholder representing an indexed type variable. + */ +export interface _ { + [index]: N; +} +export type _0 = _<0>; +export type _1 = _<1>; +export type _2 = _<2>; +export type _3 = _<3>; +export type _4 = _<4>; +export type _5 = _<5>; +export type _6 = _<6>; +export type _7 = _<7>; +export type _8 = _<8>; +export type _9 = _<9>; + +/** + * Type application (simultaneously substitutes all placeholders within the target type) + */ +// prettier-ignore +export type $ = ( + T extends Fixed ? { [indirect]: U } : + T extends _ ? { [indirect]: S[N] } : + T extends undefined | null | boolean | string | number ? { [indirect]: T } : + T extends (infer A)[] & { length: infer L } ? { + [indirect]: L extends keyof TupleTable + ? TupleTable[L] + : $[] + } : + T extends (...x: infer I) => infer O ? { [indirect]: (...x: $) => $ } : + T extends object ? { [indirect]: { [K in keyof T]: $ } } : + { [indirect]: T } +)[typeof indirect]; + +declare const fixed: unique symbol; + +/** + * Marks a type to be ignored by the application operator `$`. This is used to protect + * bound type parameters. + */ +export interface Fixed { + [fixed]: T; +} + +/** + * Used as a level of indirection to avoid circularity errors. + */ +declare const indirect: unique symbol; + +/** + * Allows looking up the type for a tuple based on its `length`, instead of trying + * each possibility one by one in a single long conditional. + */ +// prettier-ignore +type TupleTable = { + 0: []; + 1: T extends [ + infer A0 + ] ? [ + $ + ] : never + 2: T extends [ + infer A0, infer A1 + ] ? [ + $, $ + ] : never + 3: T extends [ + infer A0, infer A1, infer A2 + ] ? [ + $, $, $ + ] : never + 4: T extends [ + infer A0, infer A1, infer A2, infer A3 + ] ? [ + $, $, $, $ + ] : never + 5: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4 + ] ? [ + $, $, $, $, $ + ] : never + 6: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4, infer A5 + ] ? [ + $, $, $, $, $, $ + ] : never + 7: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6 + ] ? [ + $, $, $, $, $, $, $ + ] : never + 8: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7 + ] ? [ + $, $, $, $, $, $, $, $ + ] : never + 9: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8 + ] ? [ + $, $, $, $, $, $, $, $, $ + ] : never + 10: T extends [ + infer A0, infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, infer A9 + ] ? [ + $, $, $, $, $, $, $, $, $, $ + ] : never +} diff --git a/packages/bugc/src/evm/spec/index.ts b/packages/bugc/src/evm/spec/index.ts new file mode 100644 index 00000000..7a98abfb --- /dev/null +++ b/packages/bugc/src/evm/spec/index.ts @@ -0,0 +1,29 @@ +/** + * Type-safe EVM operations framework. + * + * This module provides a complete abstraction for EVM stack operations with + * compile-time type safety. It includes: + * - Stack type definitions and manipulation utilities + * - State management with type-safe operation builders + * - Instruction definitions and operation factories + * - Type rebranding utilities + * - Higher-kinded types for generic programming + */ + +export { type Stack } from "./stack.js"; + +export { + type Instruction, + type InstructionOptions, + type InstructionDebug, + type Unsafe, + State, +} from "./state.js"; + +export { type Operations, makeOperations } from "./definitions.js"; + +export { makeRebrands } from "./rebrand.js"; + +export { type Transition, makePipe } from "./builder.js"; + +export type { $, _ } from "./hkts.js"; diff --git a/packages/bugc/src/evm/spec/rebrand.ts b/packages/bugc/src/evm/spec/rebrand.ts new file mode 100644 index 00000000..cd688347 --- /dev/null +++ b/packages/bugc/src/evm/spec/rebrand.ts @@ -0,0 +1,756 @@ +/** + * Stack rebranding utilities for changing semantic types of stack items. + * + * This module provides type-level operations for changing the semantic brands + * of existing stack items without affecting the runtime stack structure. + * Useful for refining types when more specific information becomes available. + */ + +import type { $ } from "./hkts.js"; + +import type { Stack, PopN } from "./stack.js"; +import type { State } from "./state.js"; + +/** + * Rebrand stack items at specified positions. + * Uses 1-based indexing to match EVM convention: + * - Position 1 is the top of the stack + * - Position 2 is the second item from top + * - etc. + * + * This matches DUP and SWAP opcode numbering where DUP1 + * duplicates the 1st item (top), DUP2 duplicates the 2nd, etc. + */ +export const makeRebrands = (controls: State.Controls) => { + function rebrand(rebrands: { + 1: A1; + }): ( + state: $, + ) => $; + + function rebrand< + A0 extends Stack.Brand, + A1 extends Stack.Brand, + B0 extends Stack.Brand, + B1 extends Stack.Brand, + >(rebrands: { + 1: A1; + 2: B1; + }): ( + state: $, + ) => $; + + function rebrand< + A extends Stack.Brand, + B0 extends Stack.Brand, + B1 extends Stack.Brand, + >(rebrands: { + 2: B1; + }): ( + state: $, + ) => $; + + function rebrand< + A0 extends Stack.Brand, + A1 extends Stack.Brand, + B0 extends Stack.Brand, + B1 extends Stack.Brand, + C0 extends Stack.Brand, + C1 extends Stack.Brand, + >(rebrands: { + 1: A1; + 2: B1; + 3: C1; + }): ( + state: $, + ) => $; + + function rebrand< + A extends Stack.Brand, + B0 extends Stack.Brand, + B1 extends Stack.Brand, + C0 extends Stack.Brand, + C1 extends Stack.Brand, + >(rebrands: { + 2: B1; + 3: C1; + }): ( + state: $, + ) => $; + + function rebrand< + A0 extends Stack.Brand, + A1 extends Stack.Brand, + B extends Stack.Brand, + C0 extends Stack.Brand, + C1 extends Stack.Brand, + >(rebrands: { + 1: A1; + 3: C1; + }): ( + state: $, + ) => $; + + function rebrand< + A extends Stack.Brand, + B extends Stack.Brand, + C0 extends Stack.Brand, + C1 extends Stack.Brand, + >(rebrands: { + 3: C1; + }): ( + state: $, + ) => $; + + function rebrand>( + brands: Rebrands, + ) { + return ( + state: $, + ): $]> => { + // Find the maximum position we need to rebrand + const positions = Object.keys(brands) + .map(Number) + .sort((a, b) => b - a); + + if (positions.length === 0) { + return state as $]>; + } + + const maxPosition = positions[0]; + + // Pop the top N items from the stack + const items = controls.topN(state, maxPosition); + const poppedState = controls.popN(state, maxPosition); + + // Work backwards and push each item possibly rebranded + return items.reduceRight( + (accState, originalItem, i) => + controls.push( + accState, + // note addition because stack offsets in user code use 1-index + i + 1 in brands + ? controls.rebrand(originalItem, brands[i + 1]) + : originalItem, + ), + poppedState, + ); + }; + } + + const rebrandTop = + (brand: B) => + ( + state: $, + ): $ => + rebrand({ 1: brand })(state); + + return { rebrand, rebrandTop }; +}; + +export type Rebranded< + S extends Stack, + Rebrands extends Record, +> = ApplyRebrands>; + +// Helper to get the highest key in the Rebrands record +type MaxKey> = 17 extends keyof R + ? 17 + : 16 extends keyof R + ? 16 + : 15 extends keyof R + ? 15 + : 14 extends keyof R + ? 14 + : 13 extends keyof R + ? 13 + : 12 extends keyof R + ? 12 + : 11 extends keyof R + ? 11 + : 10 extends keyof R + ? 10 + : 9 extends keyof R + ? 9 + : 8 extends keyof R + ? 8 + : 7 extends keyof R + ? 7 + : 6 extends keyof R + ? 6 + : 5 extends keyof R + ? 5 + : 4 extends keyof R + ? 4 + : 3 extends keyof R + ? 3 + : 2 extends keyof R + ? 2 + : 1 extends keyof R + ? 1 + : 0; + +// Apply rebranding to the first N elements +type ApplyRebrands< + S extends Stack, + Rebrands extends Record, + N extends number, +> = N extends 0 + ? S + : N extends 1 + ? S extends readonly [ + infer E1 extends Stack.Brand, + ...infer _Rest extends Stack, + ] + ? readonly [ + ...[1 extends keyof Rebrands ? Rebrands[1] : E1], + ...PopN, + ] + : S + : N extends 2 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + ...Rest, + ] + : S + : N extends 3 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + ...Rest, + ] + : S + : N extends 4 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + ...Rest, + ] + : S + : N extends 5 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + ...Rest, + ] + : S + : N extends 6 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + ...Rest, + ] + : S + : N extends 7 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + ...Rest, + ] + : S + : N extends 8 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + ...Rest, + ] + : S + : N extends 9 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + ...Rest, + ] + : S + : N extends 10 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + 10 extends keyof Rebrands ? Rebrands[10] : E10, + ...Rest, + ] + : S + : N extends 11 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + 10 extends keyof Rebrands ? Rebrands[10] : E10, + 11 extends keyof Rebrands ? Rebrands[11] : E11, + ...Rest, + ] + : S + : N extends 12 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + 10 extends keyof Rebrands ? Rebrands[10] : E10, + 11 extends keyof Rebrands ? Rebrands[11] : E11, + 12 extends keyof Rebrands ? Rebrands[12] : E12, + ...Rest, + ] + : S + : N extends 13 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + 10 extends keyof Rebrands + ? Rebrands[10] + : E10, + 11 extends keyof Rebrands + ? Rebrands[11] + : E11, + 12 extends keyof Rebrands + ? Rebrands[12] + : E12, + 13 extends keyof Rebrands + ? Rebrands[13] + : E13, + ...Rest, + ] + : S + : N extends 14 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands ? Rebrands[1] : E1, + 2 extends keyof Rebrands ? Rebrands[2] : E2, + 3 extends keyof Rebrands ? Rebrands[3] : E3, + 4 extends keyof Rebrands ? Rebrands[4] : E4, + 5 extends keyof Rebrands ? Rebrands[5] : E5, + 6 extends keyof Rebrands ? Rebrands[6] : E6, + 7 extends keyof Rebrands ? Rebrands[7] : E7, + 8 extends keyof Rebrands ? Rebrands[8] : E8, + 9 extends keyof Rebrands ? Rebrands[9] : E9, + 10 extends keyof Rebrands + ? Rebrands[10] + : E10, + 11 extends keyof Rebrands + ? Rebrands[11] + : E11, + 12 extends keyof Rebrands + ? Rebrands[12] + : E12, + 13 extends keyof Rebrands + ? Rebrands[13] + : E13, + 14 extends keyof Rebrands + ? Rebrands[14] + : E14, + ...Rest, + ] + : S + : N extends 15 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands + ? Rebrands[1] + : E1, + 2 extends keyof Rebrands + ? Rebrands[2] + : E2, + 3 extends keyof Rebrands + ? Rebrands[3] + : E3, + 4 extends keyof Rebrands + ? Rebrands[4] + : E4, + 5 extends keyof Rebrands + ? Rebrands[5] + : E5, + 6 extends keyof Rebrands + ? Rebrands[6] + : E6, + 7 extends keyof Rebrands + ? Rebrands[7] + : E7, + 8 extends keyof Rebrands + ? Rebrands[8] + : E8, + 9 extends keyof Rebrands + ? Rebrands[9] + : E9, + 10 extends keyof Rebrands + ? Rebrands[10] + : E10, + 11 extends keyof Rebrands + ? Rebrands[11] + : E11, + 12 extends keyof Rebrands + ? Rebrands[12] + : E12, + 13 extends keyof Rebrands + ? Rebrands[13] + : E13, + 14 extends keyof Rebrands + ? Rebrands[14] + : E14, + 15 extends keyof Rebrands + ? Rebrands[15] + : E15, + ...Rest, + ] + : S + : N extends 16 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + infer E16 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands + ? Rebrands[1] + : E1, + 2 extends keyof Rebrands + ? Rebrands[2] + : E2, + 3 extends keyof Rebrands + ? Rebrands[3] + : E3, + 4 extends keyof Rebrands + ? Rebrands[4] + : E4, + 5 extends keyof Rebrands + ? Rebrands[5] + : E5, + 6 extends keyof Rebrands + ? Rebrands[6] + : E6, + 7 extends keyof Rebrands + ? Rebrands[7] + : E7, + 8 extends keyof Rebrands + ? Rebrands[8] + : E8, + 9 extends keyof Rebrands + ? Rebrands[9] + : E9, + 10 extends keyof Rebrands + ? Rebrands[10] + : E10, + 11 extends keyof Rebrands + ? Rebrands[11] + : E11, + 12 extends keyof Rebrands + ? Rebrands[12] + : E12, + 13 extends keyof Rebrands + ? Rebrands[13] + : E13, + 14 extends keyof Rebrands + ? Rebrands[14] + : E14, + 15 extends keyof Rebrands + ? Rebrands[15] + : E15, + 16 extends keyof Rebrands + ? Rebrands[16] + : E16, + ...Rest, + ] + : S + : N extends 17 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + infer E16 extends Stack.Brand, + infer E17 extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [ + 1 extends keyof Rebrands + ? Rebrands[1] + : E1, + 2 extends keyof Rebrands + ? Rebrands[2] + : E2, + 3 extends keyof Rebrands + ? Rebrands[3] + : E3, + 4 extends keyof Rebrands + ? Rebrands[4] + : E4, + 5 extends keyof Rebrands + ? Rebrands[5] + : E5, + 6 extends keyof Rebrands + ? Rebrands[6] + : E6, + 7 extends keyof Rebrands + ? Rebrands[7] + : E7, + 8 extends keyof Rebrands + ? Rebrands[8] + : E8, + 9 extends keyof Rebrands + ? Rebrands[9] + : E9, + 10 extends keyof Rebrands + ? Rebrands[10] + : E10, + 11 extends keyof Rebrands + ? Rebrands[11] + : E11, + 12 extends keyof Rebrands + ? Rebrands[12] + : E12, + 13 extends keyof Rebrands + ? Rebrands[13] + : E13, + 14 extends keyof Rebrands + ? Rebrands[14] + : E14, + 15 extends keyof Rebrands + ? Rebrands[15] + : E15, + 16 extends keyof Rebrands + ? Rebrands[16] + : E16, + 17 extends keyof Rebrands + ? Rebrands[17] + : E17, + ...Rest, + ] + : S + : S; diff --git a/packages/bugc/src/evm/spec/stack.ts b/packages/bugc/src/evm/spec/stack.ts new file mode 100644 index 00000000..aada2dfc --- /dev/null +++ b/packages/bugc/src/evm/spec/stack.ts @@ -0,0 +1,667 @@ +import type { $ } from "./hkts.js"; + +/** + * Represents an EVM stack as an ordered list of semantic stack brands. + * The stack grows from left to right, with the leftmost item being the top. + */ +export type Stack = readonly Stack.Brand[]; + +export namespace Stack { + export type Brand = string; + + /** + * Converts a stack type specification into concrete stack items. + * Maps each Stack.Brand in the stack to a concrete item of type I. + */ + export type Items = S extends unknown + ? S extends readonly [] + ? readonly [] + : S extends readonly [ + infer B extends Stack.Brand, + ...infer Rest extends Stack, + ] + ? readonly [$, ...Stack.Items] + : never + : never; +} + +/** + * Type-level function to extract the top N items from a stack without modifying it. + * Supports up to 17 items (maximum EVM instruction operand count). + */ +export type TopN = S extends unknown + ? N extends 0 + ? readonly [] + : N extends 1 + ? S extends readonly [infer E1 extends Stack.Brand, ...Stack] + ? readonly [E1] + : never + : N extends 2 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2] + : never + : N extends 3 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3] + : never + : N extends 4 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4] + : never + : N extends 5 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5] + : never + : N extends 6 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5, E6] + : never + : N extends 7 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5, E6, E7] + : never + : N extends 8 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5, E6, E7, E8] + : never + : N extends 9 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5, E6, E7, E8, E9] + : never + : N extends 10 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + ...Stack, + ] + ? readonly [E1, E2, E3, E4, E5, E6, E7, E8, E9, E10] + : never + : N extends 11 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + ] + : never + : N extends 12 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + ] + : never + : N extends 13 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + ] + : never + : N extends 14 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + E14, + ] + : never + : N extends 15 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + E14, + E15, + ] + : never + : N extends 16 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + infer E16 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + E14, + E15, + E16, + ] + : never + : N extends 17 + ? S extends readonly [ + infer E1 extends Stack.Brand, + infer E2 extends Stack.Brand, + infer E3 extends Stack.Brand, + infer E4 extends Stack.Brand, + infer E5 extends Stack.Brand, + infer E6 extends Stack.Brand, + infer E7 extends Stack.Brand, + infer E8 extends Stack.Brand, + infer E9 extends Stack.Brand, + infer E10 extends Stack.Brand, + infer E11 extends Stack.Brand, + infer E12 extends Stack.Brand, + infer E13 extends Stack.Brand, + infer E14 extends Stack.Brand, + infer E15 extends Stack.Brand, + infer E16 extends Stack.Brand, + infer E17 extends Stack.Brand, + ...Stack, + ] + ? readonly [ + E1, + E2, + E3, + E4, + E5, + E6, + E7, + E8, + E9, + E10, + E11, + E12, + E13, + E14, + E15, + E16, + E17, + ] + : never + : never + : never; + +/** + * Type-level function to push new items onto the front of a stack. + * Items in T are prepended to stack S, making T[0] the new top item. + */ +export type Push = S extends unknown + ? T extends unknown + ? readonly [...T, ...S] + : never + : never; + +/** + * Type-level function to remove N items from the top of a stack. + * Supports up to 17 items (maximum EVM instruction operand count). + */ +export type PopN = S extends unknown + ? N extends N + ? N extends 0 + ? S + : N extends 1 + ? S extends readonly [Stack.Brand, ...infer Rest extends Stack] + ? Rest + : never + : N extends 2 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 3 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 4 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 5 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 6 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 7 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 8 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 9 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 10 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 11 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 12 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 13 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 14 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 15 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 16 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : N extends 17 + ? S extends readonly [ + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + Stack.Brand, + ...infer Rest extends Stack, + ] + ? Rest + : never + : never + : never + : never; diff --git a/packages/bugc/src/evm/spec/state.ts b/packages/bugc/src/evm/spec/state.ts new file mode 100644 index 00000000..b944f85d --- /dev/null +++ b/packages/bugc/src/evm/spec/state.ts @@ -0,0 +1,285 @@ +import type { $ } from "./hkts.js"; +import type { Stack, TopN, PopN, Push } from "./stack.js"; +import type * as Format from "@ethdebug/format"; + +export namespace Unsafe { + /** + * Type-unsafe representation of EVM execution state containing a stack. + * The type parameter U represents the concrete state implementation. + */ + export type State = $; + + /** + * Type-unsafe representation of a single stack item. + * The type parameter I represents the concrete stack item implementation. + */ + export type StackItem = $; + + /** + * Low-level, type-unsafe operations on EVM execution state. + * These operations work directly with the concrete implementations without type safety. + */ + export interface StateControls { + /** Remove items from the top of the stack, returning the remaining state */ + slice( + state: Unsafe.State, + start?: number, + end?: number, + ): Unsafe.State; + + /** Add a new item to the top of the stack */ + prepend(state: Unsafe.State, item: Unsafe.StackItem): Unsafe.State; + + /** Read the top N items from the stack without modifying the state */ + readTop( + state: Unsafe.State, + num: number, + ): readonly Unsafe.StackItem[]; + + /** Create a new stack item with a unique identifier and type brand */ + create(id: string, brand: Stack.Brand): Unsafe.StackItem; + + /** Create a copy of an existing stack item with a new identifier */ + duplicate(item: Unsafe.StackItem, id: string): Unsafe.StackItem; + + /** Rebrand a stack item while keeping everything else the same */ + rebrand(item: Unsafe.StackItem, brand: Stack.Brand): Unsafe.StackItem; + + /** Generate a unique identifier and update state to track it */ + generateId( + state: Unsafe.State, + prefix?: string, + ): { + id: string; + state: Unsafe.State; + }; + + /** Emit an instruction and update the execution state accordingly */ + emit(state: Unsafe.State, instruction: Instruction): Unsafe.State; + } +} + +/** + * Debug context type for instructions + */ +export type InstructionDebug = { + context?: Format.Program.Context; +}; + +/** + * Represents an EVM instruction with its mnemonic, opcode, optional immediate + * values, and optional debug context for source mapping. + */ +export interface Instruction { + mnemonic: string; + opcode: number; + immediates?: number[]; + debug?: InstructionDebug; +} + +export type InstructionOptions = Pick; + +export namespace State { + export type Controls = ReturnType>; + + /** + * Creates type-safe wrappers around unsafe state control operations. + * This provides compile-time guarantees about stack operations while delegating + * the actual implementation to the unsafe controls. + */ + export const makeControls = ({ + slice, + prepend, + readTop, + generateId, + create, + duplicate, + rebrand, + emit, + }: Unsafe.StateControls) => + ({ + /** Pop N items from the stack, updating the stack type accordingly */ + popN( + state: $, + num: N, + ): $]> { + return slice(state, num) as unknown; + }, + /** Push an item onto the stack, updating the stack type accordingly */ + push( + state: $, + item: $, + ): $]> { + return prepend(state, item) as unknown; + }, + /** Read the top N items from the stack with proper typing */ + topN( + state: $, + num: N, + ): Stack.Items> { + return readTop(state, num) as unknown as Stack.Items>; + }, + /** Create a new typed stack item */ + create(id: string, brand: B): $ { + return create(id, brand); + }, + /** Duplicate a typed stack item with a new identifier */ + duplicate(item: $, id: string) { + return duplicate(item, id); + }, + /** Duplicate a typed stack item with a new identifier */ + rebrand(item: $, brand: B) { + return rebrand(item, brand); + }, + /** Generate a unique identifier while preserving stack type */ + generateId( + state: $, + prefix?: string, + ): { + id: string; + state: $; + } { + return generateId(state, prefix); + }, + /** Emit an instruction while preserving stack type */ + emit( + state: $, + instruction: Instruction, + ): $ { + return emit(state, instruction); + }, + }) as const; +} + +export namespace Specifiers { + /** + * Configuration options for creating EVM operation functions. + */ + export interface MakeOperationOptions { + /** Stack types that this operation will consume (pop from stack) */ + consumes: C; + /** Stack types that this operation will produce (push to stack) */ + produces: P; + /** Optional prefix for generated identifiers */ + idPrefix?: string; + } + + /** + * Maps a list of instructions to their corresponding operation functions. + * Creates an object where keys are instruction mnemonics and values are of type F. + */ + export type MappedInstructions = { + [M in L[number]["mnemonic"]]: F; + }; + + /** + * Creates factory functions for building type-safe EVM operations. + * This is the main entry point for creating operations that consume and produce + * stack items with compile-time type safety. + */ + export function makeUsing(controls: State.Controls) { + /** + * Creates operation functions for instructions that don't require immediate values. + * Returns a curried function: options -> instruction -> state transition function + */ + const makeOperationForInstruction = + ({ + consumes, + produces, + idPrefix, + }: Specifiers.MakeOperationOptions) => + (instruction: T) => + (options?: InstructionOptions) => + ( + initialState: $, + ): $ => + executeOperation( + controls, + initialState, + consumes, + produces, + { ...instruction, ...options }, + idPrefix, + undefined, + ); + + /** + * Creates operation functions for instructions that require immediate values. + * Returns a curried function: options -> instruction -> state transition function + * The resulting function requires an immediates parameter. + */ + const makeOperationWithImmediatesForInstruction = + ({ + consumes, + produces, + idPrefix, + }: Specifiers.MakeOperationOptions) => + (instruction: T) => + (immediates: number[], options?: InstructionOptions) => + ( + initialState: $, + ): $ => + executeOperation( + controls, + initialState, + consumes, + produces, + { ...instruction, immediates, ...options }, + idPrefix, + undefined, + ); + + /** + * Helper function to create a mnemonic-keyed mapping for a single instruction. + * Useful for building instruction operation lookup tables. + */ + const mapInstruction = ( + instruction: T, + forInstruction: (instruction: T) => F, + ): Specifiers.MappedInstructions => + ({ + [instruction.mnemonic]: forInstruction(instruction), + }) as Specifiers.MappedInstructions; + + return { + mapInstruction, + makeOperationForInstruction, + makeOperationWithImmediatesForInstruction, + }; + } +} + +/** + * Core implementation shared by both operation factories. + * Handles the common pattern of: pop items -> generate IDs -> push results -> emit instruction + */ +function executeOperation< + U, + I, + S extends Stack, + C extends Stack, + P extends Stack, + T extends Instruction, + P2 extends Stack = P, +>( + controls: State.Controls, + initialState: $, + consumes: C, + produces: P, + instruction: T, + idPrefix?: string, + options?: { produces: P2 }, +): $ { + let state = controls.popN(initialState, consumes.length); + + let id; + for (let i = produces.length - 1; i >= 0; i--) { + ({ id, state } = controls.generateId(state, idPrefix)); + state = controls.push( + state, + controls.create(id, (options?.produces || produces)[i]), + ); + } + + return controls.emit(state, instruction); +} diff --git a/packages/bugc/src/evmgen/analysis/index.ts b/packages/bugc/src/evmgen/analysis/index.ts new file mode 100644 index 00000000..1e91d2bc --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/index.ts @@ -0,0 +1,3 @@ +export * as Memory from "./memory.js"; +export * as Liveness from "./liveness.js"; +export * as Layout from "./layout.js"; diff --git a/packages/bugc/src/evmgen/analysis/layout.ts b/packages/bugc/src/evmgen/analysis/layout.ts new file mode 100644 index 00000000..24c9caa9 --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/layout.ts @@ -0,0 +1,144 @@ +/** + * Block Layout Planning for EVM Code Generation + * + * Determines the order of basic blocks and their bytecode offsets + * for jump target resolution. + */ + +import type * as Ir from "#ir"; +import { Result } from "#result"; + +import * as Memory from "./memory.js"; + +export namespace Module { + /** + * Module-level block layout information + */ + export interface Info { + create?: Function.Info; + main: Function.Info; + functions: { + [functionName: string]: Function.Info; + }; + } + + /** + * Analyze block layout for entire module + */ + export function perform( + module: Ir.Module, + ): Result { + const result: Module.Info = { + main: {} as Function.Info, + functions: {}, + }; + + // Process constructor if present + if (module.create) { + const createLayout = Function.perform(module.create); + if (!createLayout.success) { + return createLayout; + } + result.create = createLayout.value; + } + + // Process main function + const mainLayout = Function.perform(module.main); + if (!mainLayout.success) { + return mainLayout; + } + result.main = mainLayout.value; + + // Process user-defined functions + for (const [name, func] of module.functions) { + const funcLayout = Function.perform(func); + if (!funcLayout.success) { + return funcLayout; + } + result.functions[name] = funcLayout.value; + } + + return Result.ok(result); + } +} + +export namespace Function { + export interface Info { + /** Order in which to generate blocks */ + order: string[]; + /** Bytecode offset for each block (filled during generation) */ + offsets: Map; + } + + /** + * Layout blocks for a function + * + * Uses depth-first order to keep related blocks together, + * minimizing jump distances. + */ + export function perform( + func: Ir.Function, + ): Result { + try { + const visited = new Set(); + const order = dfsOrder(func, func.entry, visited); + + // Add any unreachable blocks at the end + const unreachable = Array.from(func.blocks.keys()).filter( + (id) => !visited.has(id), + ); + + return Result.ok({ + order: [...order, ...unreachable], + offsets: new Map(), + }); + } catch (error) { + return Result.err( + new Memory.Error( + Memory.ErrorCode.INVALID_LAYOUT, + error instanceof Error ? error.message : "Unknown error", + ), + ); + } + } +} + +/** + * Perform depth-first traversal to order blocks + */ +function dfsOrder( + func: Ir.Function, + blockId: string, + visited: Set = new Set(), +): string[] { + if (visited.has(blockId)) return []; + visited.add(blockId); + + const block = func.blocks.get(blockId); + if (!block) return []; + + const term = block.terminator; + + if (term.kind === "jump") { + return [blockId, ...dfsOrder(func, term.target, visited)]; + } else if (term.kind === "branch") { + // Visit true branch first (arbitrary but consistent) + const trueBranch = dfsOrder(func, term.trueTarget, visited); + const falseBranch = dfsOrder(func, term.falseTarget, visited); + return [blockId, ...trueBranch, ...falseBranch]; + } else { + return [blockId]; + } +} + +// Legacy exports for compatibility +export type BlockLayout = Function.Info; +export const layoutBlocks = (func: Ir.Function): Function.Info => { + const result = Function.perform(func); + if (!result.success) { + throw new Error( + Object.values(result.messages)[0]?.[0]?.message || "Layout failed", + ); + } + return result.value; +}; diff --git a/packages/bugc/src/evmgen/analysis/liveness.test.ts b/packages/bugc/src/evmgen/analysis/liveness.test.ts new file mode 100644 index 00000000..613cd682 --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/liveness.test.ts @@ -0,0 +1,330 @@ +import { describe, it, expect } from "vitest"; + +import * as Ir from "#ir"; +import * as Liveness from "./liveness.js"; + +describe("Liveness Analysis", () => { + it("should identify live-in and live-out sets for a simple function", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + { + kind: "const", + value: 10n, + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%2", + type: Ir.Type.Scalar.uint256, + }, + dest: "%3", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + + // Entry block should have no live-in (it's the entry) + expect(liveness.liveIn.get("entry")?.size).toBe(0); + + // No live-out since it returns + expect(liveness.liveOut.get("entry")?.size).toBe(0); + + // No cross-block values in a single-block function + expect(liveness.crossBlockValues.size).toBe(0); + }); + + it("should track values across blocks", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { + kind: "branch", + condition: { kind: "temp", id: "%1", type: Ir.Type.Scalar.bool }, + trueTarget: "then", + falseTarget: "else", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "then", + { + id: "then", + phis: [], + instructions: [ + { + kind: "const", + value: 10n, + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "merge", + operationDebug: {}, + }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + [ + "else", + { + id: "else", + phis: [], + instructions: [ + { + kind: "const", + value: 20n, + type: Ir.Type.Scalar.uint256, + dest: "%3", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "merge", + operationDebug: {}, + }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + [ + "merge", + { + id: "merge", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["then", "else"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + + // %1 is used in the branch, but it's also defined in entry, so it's not live-out + // (it's consumed within the block) + expect(liveness.liveOut.get("entry")?.has("%1")).toBe(false); + + // No values cross from then/else to merge + expect(liveness.liveIn.get("merge")?.size).toBe(0); + }); + + it("should handle phi nodes correctly", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "loop", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "loop", + { + id: "loop", + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "entry", + { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + ], + [ + "loop", + { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "%2", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + instructions: [ + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + dest: "%inc", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%2", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%inc", + type: Ir.Type.Scalar.uint256, + }, + dest: "%3", + operationDebug: {}, + }, + { + kind: "const", + value: 10n, + type: Ir.Type.Scalar.uint256, + dest: "%limit", + operationDebug: {}, + }, + { + kind: "binary", + op: "lt", + left: { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%limit", + type: Ir.Type.Scalar.uint256, + }, + dest: "%cond", + operationDebug: {}, + }, + ], + terminator: { + kind: "branch", + condition: { + kind: "temp", + id: "%cond", + type: Ir.Type.Scalar.bool, + }, + trueTarget: "loop", + falseTarget: "exit", + operationDebug: {}, + }, + predecessors: new Set(["entry", "loop"]), + debug: {}, + } as Ir.Block, + ], + [ + "exit", + { + id: "exit", + phis: [], + instructions: [], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + predecessors: new Set(["loop"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + + // %1 should be live-out of entry (used by phi in loop) + expect(liveness.liveOut.get("entry")).toContain("%1"); + + // %3 should be live-out of loop (used by phi and return) + expect(liveness.liveOut.get("loop")).toContain("%3"); + + // %3 should be live-in to exit (used in return) + expect(liveness.liveIn.get("exit")).toContain("%3"); + + // Cross-block values + expect(liveness.crossBlockValues).toContain("%1"); + expect(liveness.crossBlockValues).toContain("%3"); + }); +}); diff --git a/packages/bugc/src/evmgen/analysis/liveness.ts b/packages/bugc/src/evmgen/analysis/liveness.ts new file mode 100644 index 00000000..95e2871a --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/liveness.ts @@ -0,0 +1,317 @@ +/** + * Liveness Analysis for EVM Code Generation + * + * Determines which values are live at each point in the program, + * essential for memory allocation and stack management. + */ + +import * as Ir from "#ir"; + +export namespace Function { + export interface Info { + /** Values live at block entry */ + liveIn: Map>; + /** Values live at block exit */ + liveOut: Map>; + /** Last instruction where each value is used */ + lastUse: Map; + /** Values that cross block boundaries */ + crossBlockValues: Set; + } + + /** + * Perform liveness analysis on a function + */ + export function analyze(func: Ir.Function): Function.Info { + const liveIn = new Map>(); + const liveOut = new Map>(); + const lastUse = new Map(); + const crossBlockValues = new Set(); + + // Initialize empty sets + for (const blockId of func.blocks.keys()) { + liveIn.set(blockId, new Set()); + liveOut.set(blockId, new Set()); + } + + // Track uses and defs per block + const blockUses = new Map>(); + const blockDefs = new Map>(); + + for (const [blockId, block] of func.blocks) { + const uses = new Set(); + const defs = new Set(); + + // Process phi nodes + for (const phi of block.phis) { + defs.add(phi.dest); + // Phi sources will be handled in a separate pass + } + + // Process instructions + for (const inst of block.instructions) { + // Uses before defs + for (const used of getUsedValues(inst)) { + if (!defs.has(used)) { + uses.add(used); + } + lastUse.set(used, `${blockId}:${inst.kind}`); + } + + const defined = getDefinedValue(inst); + if (defined) { + defs.add(defined); + } + } + + // Process terminator + const term = block.terminator; + if (term.kind === "branch") { + const condId = valueId(term.condition); + if (!defs.has(condId)) { + uses.add(condId); + } + lastUse.set(condId, `${blockId}:branch`); + } else if (term.kind === "return" && term.value) { + const retId = valueId(term.value); + if (!defs.has(retId)) { + uses.add(retId); + } + lastUse.set(retId, `${blockId}:return`); + } + + blockUses.set(blockId, uses); + blockDefs.set(blockId, defs); + } + + // Fixed-point iteration for liveness + let changed = true; + while (changed) { + changed = false; + + for (const [blockId, block] of func.blocks) { + const oldInSize = liveIn.get(blockId)!.size; + const oldOutSize = liveOut.get(blockId)!.size; + + // LiveOut = union of LiveIn of all successors + phi sources + const newOut = new Set(); + const term = block.terminator; + + if (term.kind === "jump") { + const succIn = liveIn.get(term.target); + if (succIn) { + for (const val of succIn) newOut.add(val); + } + // Add phi sources for this predecessor + const succBlock = func.blocks.get(term.target); + if (succBlock) { + for (const phi of succBlock.phis) { + const source = phi.sources.get(blockId); + if (source && source.kind !== "const") { + newOut.add(valueId(source)); + crossBlockValues.add(valueId(source)); + } + } + } + } else if (term.kind === "branch") { + const trueIn = liveIn.get(term.trueTarget); + const falseIn = liveIn.get(term.falseTarget); + if (trueIn) { + for (const val of trueIn) newOut.add(val); + } + if (falseIn) { + for (const val of falseIn) newOut.add(val); + } + // Add phi sources for both targets + for (const target of [term.trueTarget, term.falseTarget]) { + const succBlock = func.blocks.get(target); + if (succBlock) { + for (const phi of succBlock.phis) { + const source = phi.sources.get(blockId); + if (source && source.kind !== "const") { + newOut.add(valueId(source)); + crossBlockValues.add(valueId(source)); + } + } + } + } + } + + liveOut.set(blockId, newOut); + + // LiveIn = (LiveOut - Defs) ∪ Uses + const newIn = new Set(newOut); + const defs = blockDefs.get(blockId)!; + const uses = blockUses.get(blockId)!; + + for (const def of defs) { + newIn.delete(def); + } + for (const use of uses) { + newIn.add(use); + } + + liveIn.set(blockId, newIn); + + if (newIn.size !== oldInSize || newOut.size !== oldOutSize) { + changed = true; + } + } + } + + // Identify cross-block values + for (const outSet of liveOut.values()) { + for (const val of outSet) { + crossBlockValues.add(val); + } + } + + return { + liveIn, + liveOut, + lastUse, + crossBlockValues, + }; + } +} + +export namespace Module { + export interface Info { + create?: Function.Info; + main?: Function.Info; + functions: { + [functionName: string]: Function.Info; + }; + } + + /** + * Analyze liveness for entire module + */ + export function analyze(module: Ir.Module): Module.Info { + const result: Module.Info = { + functions: {}, + }; + + if (module.create) { + result.create = Function.analyze(module.create); + } + + result.main = Function.analyze(module.main); + + for (const [name, func] of module.functions) { + result.functions[name] = Function.analyze(func); + } + + return result; + } +} + +/** + * Get the ID from a Value + */ +function valueId(val: Ir.Value): string { + if (val.kind === "const") { + return `$const_${val.value}`; + } else if (val.kind === "temp") { + return val.id; + } else { + // @ts-expect-error should be exhausted + throw new Error(`Unknown value kind: ${val.kind}`); + } +} + +/** + * Collect all values used by an instruction + */ +function getUsedValues(inst: Ir.Instruction): Set { + const used = new Set(); + + // Helper to add a value if it's not a constant + const addValue = (val: Ir.Value | undefined): void => { + if (val && val.kind !== "const") { + used.add(valueId(val)); + } + }; + + // Check instruction type and extract used values + switch (inst.kind) { + case "binary": + addValue(inst.left); + addValue(inst.right); + break; + case "unary": + addValue(inst.operand); + break; + case "compute_slot": + addValue(inst.base); + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + addValue(inst.key); + } + break; + case "hash": + addValue(inst.value); + break; + case "cast": + addValue(inst.value); + break; + // Call instruction removed - calls are now block terminators + case "length": + addValue(inst.object); + break; + case "allocate": + addValue(inst.size); + break; + // NEW: unified read instruction + case "read": + addValue(inst.slot); // For storage/transient + addValue(inst.offset); // For memory/calldata/etc + addValue(inst.length); + break; + // NEW: unified write instruction + case "write": + addValue(inst.slot); // For storage/transient + addValue(inst.offset); // For memory/calldata/etc + addValue(inst.length); + addValue(inst.value); + break; + // NEW: unified compute offset + case "compute_offset": + addValue(inst.base); + if (Ir.Instruction.ComputeOffset.isArray(inst)) { + addValue(inst.index); + } else if (Ir.Instruction.ComputeOffset.isByte(inst)) { + addValue(inst.offset); + } + // Field type doesn't have any Values to add (fieldOffset is a number) + break; + // These instructions don't use any values + case "const": + case "env": + break; + } + + return used; +} + +/** + * Get the value defined by an instruction + */ +function getDefinedValue(inst: Ir.Instruction): string | undefined { + switch (inst.kind) { + case "const": + case "binary": + case "unary": + case "read": // NEW: unified read + case "compute_slot": + case "compute_offset": // NEW: unified compute offset + case "env": + case "hash": + case "cast": + case "length": + case "allocate": + return inst.dest; + // These instructions don't define values + case "write": // NEW: unified write + return undefined; + } +} diff --git a/packages/bugc/src/evmgen/analysis/memory.test.ts b/packages/bugc/src/evmgen/analysis/memory.test.ts new file mode 100644 index 00000000..8760ed3b --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/memory.test.ts @@ -0,0 +1,302 @@ +import { describe, it, expect } from "vitest"; + +import * as Ir from "#ir"; + +import * as Memory from "./memory.js"; +import * as Liveness from "./liveness.js"; + +describe("Memory Planning", () => { + it("should allocate memory for phi destinations", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + { + kind: "const", + value: 2n, + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + ], + terminator: { + kind: "branch", + condition: { + kind: "const", + value: true, + type: Ir.Type.Scalar.bool, + }, + trueTarget: "merge", + falseTarget: "merge", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "merge", + { + id: "merge", + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "entry", + { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "%3", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + + expect(memoryResult.success).toBe(true); + if (!memoryResult.success) throw new Error("Memory planning failed"); + + const memory = memoryResult.value; + + // Phi destination %3 should be allocated memory + expect("%3" in memory.allocations).toBe(true); + // %1 is allocated first at 0x80, then %3 at 0xa0 (160) + expect(memory.allocations["%3"].offset).toBe(0xa0); + + // Cross-block value %1 should also be allocated + expect("%1" in memory.allocations).toBe(true); + expect(memory.allocations["%1"].offset).toBe(0x80); + }); + + it("should allocate memory for cross-block values", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "next", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "next", + { + id: "next", + phis: [], + instructions: [ + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + }, + dest: "%2", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + + expect(memoryResult.success).toBe(true); + if (!memoryResult.success) throw new Error("Memory planning failed"); + + const memory = memoryResult.value; + + // %1 crosses block boundary, should be allocated + expect("%1" in memory.allocations).toBe(true); + }); + + it("should allocate memory for deeply nested stack values", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + // Create many values to simulate deep stack + ...Array.from({ length: 20 }, (_, i) => ({ + kind: "const" as const, + value: BigInt(i), + type: Ir.Type.Scalar.uint256, + dest: `%${i}`, + operationDebug: {}, + })), + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + + expect(memoryResult.success).toBe(true); + if (!memoryResult.success) throw new Error("Memory planning failed"); + + const memory = memoryResult.value; + + // Some bottom values should be spilled to memory + // (exact values depend on threshold, but some should be allocated) + expect(Object.keys(memory.allocations).length).toBeGreaterThan(0); + }); + + it("should use sequential memory slots", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { + kind: "jump", + target: "block1", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "block1", + { + id: "block1", + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "entry", + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "%phi1", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + { + kind: "phi", + sources: new Map([ + [ + "entry", + { + kind: "const", + value: 2n, + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "%phi2", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + + expect(memoryResult.success).toBe(true); + if (!memoryResult.success) throw new Error("Memory planning failed"); + + const memory = memoryResult.value; + + // Both phi destinations should be allocated + expect("%phi1" in memory.allocations).toBe(true); + expect("%phi2" in memory.allocations).toBe(true); + + // Should use sequential 32-byte slots + const phi1Offset = memory.allocations["%phi1"].offset; + const phi2Offset = memory.allocations["%phi2"].offset; + expect(Math.abs(phi2Offset - phi1Offset)).toBe(32); + + // Free pointer should be after all allocations + expect(memory.nextStaticOffset).toBeGreaterThanOrEqual(0x80 + 64); + }); +}); diff --git a/packages/bugc/src/evmgen/analysis/memory.ts b/packages/bugc/src/evmgen/analysis/memory.ts new file mode 100644 index 00000000..5305d873 --- /dev/null +++ b/packages/bugc/src/evmgen/analysis/memory.ts @@ -0,0 +1,491 @@ +/** + * Memory Planning for EVM Code Generation + * + * Allocates memory slots for values that need to persist across + * stack operations or block boundaries. + */ + +import { BugError } from "#errors"; +import type { SourceLocation } from "#ast"; +import * as Ir from "#ir"; +import { Result, Severity } from "#result"; + +import type * as Liveness from "./liveness.js"; + +export enum ErrorCode { + STACK_TOO_DEEP = "MEMORY_STACK_TOO_DEEP", + ALLOCATION_FAILED = "MEMORY_ALLOCATION_FAILED", + INVALID_LAYOUT = "MEMORY_INVALID_LAYOUT", +} + +class MemoryError extends BugError { + constructor(code: ErrorCode, message: string, location?: SourceLocation) { + super(message, code, location, Severity.Error); + } +} + +export { MemoryError as Error }; + +/** + * EVM memory layout following Solidity conventions + */ +export const regions = { + SCRATCH_SPACE_1: 0x00, // 0x00-0x1f: First scratch space slot + SCRATCH_SPACE_2: 0x20, // 0x20-0x3f: Second scratch space slot + FREE_MEMORY_POINTER: 0x40, // 0x40-0x5f: Dynamic memory pointer + ZERO_SLOT: 0x60, // 0x60-0x7f: Zero slot (reserved) + STATIC_MEMORY_START: 0x80, // 0x80+: Static allocations start here +} as const; + +export interface Allocation { + /** Memory offset in bytes */ + offset: number; + /** Size in bytes */ + size: number; +} + +export namespace Module { + /** + * Module-level memory information + */ + export interface Info { + create?: Function.Info; + main: Function.Info; + functions: { + [functionName: string]: Function.Info; + }; + } + + /** + * Analyze memory requirements for entire module + */ + export function plan( + module: Ir.Module, + liveness: Liveness.Module.Info, + ): Result { + const result: Module.Info = { + main: {} as Function.Info, + functions: {}, + }; + + // Process constructor if present + if (module.create && liveness.create) { + const createMemory = Function.plan(module.create, liveness.create); + if (!createMemory.success) { + return createMemory; + } + result.create = createMemory.value; + } + + // Process main function + if (!liveness.main) { + return Result.err( + new MemoryError( + ErrorCode.INVALID_LAYOUT, + "Missing liveness info for main function", + ), + ); + } + const mainMemory = Function.plan(module.main, liveness.main); + if (!mainMemory.success) { + return mainMemory; + } + result.main = mainMemory.value; + + // Process user-defined functions + for (const [name, func] of module.functions) { + const funcLiveness = liveness.functions[name]; + if (!funcLiveness) { + return Result.err( + new MemoryError( + ErrorCode.INVALID_LAYOUT, + `Missing liveness info for function ${name}`, + ), + ); + } + const funcMemory = Function.plan(func, funcLiveness); + if (!funcMemory.success) { + return funcMemory; + } + result.functions[name] = funcMemory.value; + } + + return Result.ok(result); + } +} + +export namespace Function { + export interface Info { + /** Memory allocation info for each value that needs allocation */ + allocations: Record; + /** Next available memory offset after all static allocations */ + nextStaticOffset: number; + } + + /** + * Plan memory layout for a function with type-aware packing + */ + export function plan( + func: Ir.Function, + liveness: Liveness.Function.Info, + ): Result { + try { + const allocations: Record = {}; + let nextStaticOffset = regions.STATIC_MEMORY_START; + + const needsMemory = identifyMemoryValues(func, liveness); + + // Also allocate memory for all parameters (they always need memory) + for (const param of func.parameters || []) { + needsMemory.set(param.tempId, param.type); + } + + // Check if we have too many values for memory + if (needsMemory.size > 1000) { + return Result.err( + new MemoryError( + ErrorCode.ALLOCATION_FAILED, + `Too many values need memory allocation: ${needsMemory.size}`, + ), + ); + } + + // Sort values by size (largest first) for better packing + const sortedValues = Array.from(needsMemory.entries()).sort( + ([_a, typeA], [_b, typeB]) => getTypeSize(typeB) - getTypeSize(typeA), + ); + + // Track current slot usage for packing + let currentSlotOffset = nextStaticOffset; + let currentSlotUsed = 0; + const SLOT_SIZE = 32; + + for (const [valueId, type] of sortedValues) { + const size = getTypeSize(type); + + // If this value needs a full slot or won't fit in current slot, start new slot + if (size >= SLOT_SIZE || currentSlotUsed + size > SLOT_SIZE) { + if (currentSlotUsed > 0) { + // Move to next slot if current slot has something + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } + } + + // Allocate in current slot + allocations[valueId] = { + offset: currentSlotOffset + currentSlotUsed, + size: size, + }; + + currentSlotUsed += size; + + // If we filled the slot exactly, prepare for next slot + if (currentSlotUsed >= SLOT_SIZE) { + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } + } + + // Update next static offset to next available slot + if (currentSlotUsed > 0) { + nextStaticOffset = currentSlotOffset + SLOT_SIZE; + } else { + nextStaticOffset = currentSlotOffset; + } + + return Result.ok({ + allocations, + nextStaticOffset, + }); + } catch (error) { + return Result.err( + new MemoryError( + ErrorCode.ALLOCATION_FAILED, + error instanceof Error ? error.message : "Unknown error", + ), + ); + } + } +} + +/** + * Simulate stack effects of an instruction + */ +function simulateInstruction(stack: string[], inst: Ir.Instruction): string[] { + const newStack = [...stack]; + + // Pop consumed values based on instruction type + switch (inst.kind) { + case "binary": + case "hash": + newStack.pop(); // Two operands + newStack.pop(); + break; + case "compute_slot": + // Depends on kind + newStack.pop(); // base + if ( + inst.kind === "compute_slot" && + Ir.Instruction.ComputeSlot.isMapping(inst) + ) { + newStack.pop(); // key for mappings + } + break; + case "unary": + case "cast": + case "length": + newStack.pop(); // One operand + break; + // NEW: unified read - pops slot/offset/length as needed + case "read": + if (inst.slot) newStack.pop(); + if (inst.offset) newStack.pop(); + if (inst.length) newStack.pop(); + break; + // NEW: unified write - pops slot/offset/length/value as needed + case "write": + if (inst.slot) newStack.pop(); + if (inst.offset) newStack.pop(); + if (inst.length) newStack.pop(); + newStack.pop(); // value + break; + // NEW: compute offset + case "compute_offset": + newStack.pop(); // base + if (Ir.Instruction.ComputeOffset.isArray(inst)) { + newStack.pop(); // index + } else if (Ir.Instruction.ComputeOffset.isByte(inst)) { + newStack.pop(); // offset + } + // Field type doesn't pop any additional values (fieldOffset is a number) + break; + // Call instruction removed - calls are now block terminators + // These don't pop anything + case "const": + case "env": + break; + } + + // Push produced value + if ("dest" in inst && inst.dest) { + newStack.push(inst.dest); + } + + return newStack; +} + +/** + * Get the ID from a Value + */ +function valueId(val: Ir.Value): string { + if (val.kind === "const") { + return `$const_${val.value}`; + } else if (val.kind === "temp") { + return val.id; + } else { + // @ts-expect-error should be exhausted + throw new Error(`Unknown value kind: ${val.kind}`); + } +} + +/** + * Collect all values used by an instruction + */ +function getUsedValues(inst: Ir.Instruction): Set { + const used = new Set(); + + // Helper to add a value if it's not a constant + const addValue = (val: Ir.Value | undefined): void => { + if (val && val.kind !== "const") { + used.add(valueId(val)); + } + }; + + // Check instruction type and extract used values + switch (inst.kind) { + case "binary": + addValue(inst.left); + addValue(inst.right); + break; + case "unary": + addValue(inst.operand); + break; + case "compute_slot": + addValue(inst.base); + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + addValue(inst.key); + } + break; + case "cast": + addValue(inst.value); + break; + case "length": + addValue(inst.object); + break; + case "hash": + addValue(inst.value); + break; + // Call instruction removed - calls are now block terminators + } + + return used; +} + +/** + * Find position of value in stack (0 = top) + */ +function findStackPosition(stack: string[], value: string): number { + const index = stack.lastIndexOf(value); + return index === -1 ? -1 : stack.length - 1 - index; +} + +/** + * Get the size in bytes for a given type + */ +function getTypeSize(type: Ir.Type): number { + switch (type.kind) { + case "scalar": + // Scalar types have their size directly + return type.size; + case "ref": + // References are always 32-byte pointers on the stack + return 32; + default: + return 32; // Conservative default + } +} + +/** + * Get type information for a value ID + */ +function getValueType(valueId: string, func: Ir.Function): Ir.Type | undefined { + // Check if it's a parameter + for (const param of func.parameters || []) { + if (param.tempId === valueId) { + return param.type; + } + } + + // Search through instructions for the definition + for (const [_, block] of func.blocks) { + // Check phi nodes + for (const phi of block.phis) { + if (phi.dest === valueId) { + return phi.type; + } + } + + // Check instructions + for (const inst of block.instructions) { + if ("dest" in inst && inst.dest === valueId) { + // Get type based on instruction kind + if ("type" in inst && inst.type) { + return inst.type as Ir.Type; + } + // For instructions without explicit type, infer from operation + if (inst.kind === "binary" || inst.kind === "unary") { + // Binary/unary ops typically produce uint256 + return Ir.Type.Scalar.uint256; + } + if (inst.kind === "env") { + // Environment ops produce address or uint256 + return inst.op === "msg_sender" + ? Ir.Type.Scalar.address + : Ir.Type.Scalar.uint256; + } + } + } + } + + return undefined; +} + +/** + * Identify values that need memory allocation with their types + */ +function identifyMemoryValues( + func: Ir.Function, + liveness: Liveness.Function.Info, +): Map { + const needsMemory = new Map(); + + // All cross-block values need memory + for (const value of liveness.crossBlockValues) { + const type = getValueType(value, func); + if (type) { + needsMemory.set(value, type); + } + } + + // All phi destinations need memory + for (const [_, block] of func.blocks) { + for (const phi of block.phis) { + needsMemory.set(phi.dest, phi.type); + } + } + + // Simulate stack to find values that might overflow + for (const blockId of func.blocks.keys()) { + const block = func.blocks.get(blockId)!; + const liveAtEntry = liveness.liveIn.get(blockId) || new Set(); + + // Start with live-in values on stack + let stack: string[] = Array.from(liveAtEntry); + + for (const inst of block.instructions) { + // Check if any used values are too deep in stack + for (const usedId of getUsedValues(inst)) { + const position = findStackPosition(stack, usedId); + if (position > 16 || position === -1) { + const type = getValueType(usedId, func); + if (type) { + needsMemory.set(usedId, type); + } + } + } + + // Values used in compute_slot need memory for hashing + if (inst.kind === "compute_slot" && inst.slotKind === "array") { + // The base might need to be preserved + const baseId = valueId(inst.base); + if (liveAtEntry.has(baseId)) { + const type = getValueType(baseId, func); + if (type) { + needsMemory.set(baseId, type); + } + } + } + + // Simulate the instruction's effect on stack + stack = simulateInstruction(stack, inst); + + // If stack is getting too deep, spill bottom values + if (stack.length > 14) { + // Conservative threshold + // Mark bottom values as needing memory + for (let i = 0; i < stack.length - 14; i++) { + const type = getValueType(stack[i], func); + if (type) { + needsMemory.set(stack[i], type); + } + } + } + } + + // Check terminator usage + const term = block.terminator; + if (term.kind === "branch" && term.condition.kind !== "const") { + const condId = valueId(term.condition); + const position = findStackPosition(stack, condId); + if (position > 16 || position === -1) { + const type = getValueType(condId, func); + if (type) { + needsMemory.set(condId, type); + } + } + } + } + + return needsMemory; +} diff --git a/packages/bugc/src/evmgen/constructor-array.test.ts b/packages/bugc/src/evmgen/constructor-array.test.ts new file mode 100644 index 00000000..af236f8f --- /dev/null +++ b/packages/bugc/src/evmgen/constructor-array.test.ts @@ -0,0 +1,219 @@ +import { describe, it, expect } from "vitest"; +import { parse } from "#parser"; +import * as TypeChecker from "#typechecker"; +import * as Irgen from "#irgen"; +import type * as Ir from "#ir"; + +import { Layout, Liveness, Memory } from "#evmgen/analysis"; +import { Module, Function } from "#evmgen/generation"; + +describe("Constructor array storage", () => { + it("should correctly store values in fixed-size arrays during construction", () => { + const source = `name ConstructorArray; + +storage { + [0] items: array; +} + +create { + items[0] = 1005; + items[1] = 1006; + items[2] = 1007; +} + +code {} +`; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + // Parse error details available in parseResult + } + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeCheckResult = TypeChecker.checkProgram(parseResult.value); + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + // Generate IR + const irResult = Irgen.generateModule( + parseResult.value, + typeCheckResult.value.types, + ); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + expect(module.create).toBeDefined(); + + // Check IR - should have compute_slot for array access + const createFunc = module.create!; + const entry = createFunc.blocks.get("entry")!; + + // Check that we have compute_slot instructions for array access + const computeSlotInstructions = entry.instructions.filter( + (i) => i.kind === "compute_slot" && i.slotKind === "array", + ); + expect(computeSlotInstructions.length).toBe(3); + + // Instructions are verified by checking write instructions below + + // Check write instructions (new unified format for storage writes) + const storeInstructions = entry.instructions.filter( + (i) => i.kind === "write" && i.location === "storage", + ); + expect(storeInstructions.length).toBe(3); + + // Generate bytecode + const liveness = Liveness.Function.analyze(createFunc); + const memoryResult = Memory.Function.plan(createFunc, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(createFunc); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = Function.generate(createFunc, memory, layout); + + // Check instructions contain SSTORE operations + const sstoreInstructions = instructions.filter( + (inst) => inst.mnemonic === "SSTORE", + ); + expect(sstoreInstructions.length).toBe(3); + + // Should have exactly 3 SSTORE operations + expect(sstoreInstructions).toHaveLength(3); + }); + + it("should generate correct deployment bytecode for array constructor", () => { + const source = `name ConstructorArray; + +storage { + [0] items: array; +} + +create { + items[0] = 1005; + items[1] = 1006; + items[2] = 1007; +} + +code {} +`; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + // Parse error details available in parseResult + } + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeCheckResult = TypeChecker.checkProgram(parseResult.value); + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + // Generate IR + const irResult = Irgen.generateModule( + parseResult.value, + typeCheckResult.value.types, + ); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + // Generate full module bytecode + const module = irResult.value; + + const liveness = Liveness.Module.analyze(module); + const memoryResult = Memory.Module.plan(module, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + + const blockResult = Layout.Module.perform(module); + if (!blockResult.success) throw new Error("Block layout failed"); + + const result = Module.generate( + module, + memoryResult.value, + blockResult.value, + ); + + expect(result.create).toBeDefined(); + expect(result.runtime).toBeDefined(); + expect(result.createInstructions).toBeDefined(); + + // Should have 3 SSTORE instructions in the constructor + const sstoreInstructions = result.createInstructions!.filter( + (inst) => inst.mnemonic === "SSTORE", + ); + expect(sstoreInstructions).toHaveLength(3); + + // Should have deployment wrapper instructions + expect( + result.createInstructions!.some((inst) => inst.mnemonic === "CODECOPY"), + ).toBe(true); + expect( + result.createInstructions!.some((inst) => inst.mnemonic === "RETURN"), + ).toBe(true); + }); + + it("should not allocate memory for intermediate slot calculations", () => { + const source = `name ConstructorArray; + +storage { + [0] items: array; +} + +create { + items[0] = 1005; + items[1] = 1006; + items[2] = 1007; +} + +code {} +`; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + // Parse error details available in parseResult + } + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeCheckResult = TypeChecker.checkProgram(parseResult.value); + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + // Generate IR + const irResult = Irgen.generateModule( + parseResult.value, + typeCheckResult.value.types, + ); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + const createFunc = module.create!; + + // Analyze what gets allocated to memory + const liveness = Liveness.Function.analyze(createFunc); + const memoryResult = Memory.Function.plan(createFunc, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + + // The slot calculation results (t2, t5, t8) should NOT be in memory + // because they're only used once immediately after creation + const entry = createFunc.blocks.get("entry")!; + + // Find the add instruction destinations (these are the computed slots) + const slotTemps = entry.instructions + .filter((i) => i.kind === "binary" && i.op === "add") + .map((i) => (i as Ir.Instruction & { kind: "binary" }).dest); + + // These should NOT be allocated to memory if they're only used once + for (const temp of slotTemps) { + expect(temp in memory.allocations).toBe(false); + } + }); +}); diff --git a/packages/bugc/src/evmgen/errors.ts b/packages/bugc/src/evmgen/errors.ts new file mode 100644 index 00000000..cebdb1c7 --- /dev/null +++ b/packages/bugc/src/evmgen/errors.ts @@ -0,0 +1,52 @@ +import { BugError } from "#errors"; +import type { SourceLocation } from "#ast"; +import { Severity } from "#result"; + +export enum ErrorCode { + STACK_OVERFLOW = "EVM001", + STACK_UNDERFLOW = "EVM002", + INVALID_STACK_ACCESS = "EVM003", + MEMORY_ALLOCATION_FAILED = "EVM004", + JUMP_TARGET_NOT_FOUND = "EVM005", + PHI_NODE_UNRESOLVED = "EVM006", + UNSUPPORTED_INSTRUCTION = "EVM007", + INTERNAL_ERROR = "EVM999", +} + +export const ErrorMessages = { + [ErrorCode.STACK_OVERFLOW]: "Stack depth exceeds EVM limit of 1024", + [ErrorCode.STACK_UNDERFLOW]: + "Stack underflow: attempted to access non-existent stack item", + [ErrorCode.INVALID_STACK_ACCESS]: + "Invalid stack access: position out of range", + [ErrorCode.MEMORY_ALLOCATION_FAILED]: "Failed to allocate memory for value", + [ErrorCode.JUMP_TARGET_NOT_FOUND]: "Jump target block not found", + [ErrorCode.PHI_NODE_UNRESOLVED]: + "Phi node value not resolved for predecessor", + [ErrorCode.UNSUPPORTED_INSTRUCTION]: "Unsupported IR instruction", + [ErrorCode.INTERNAL_ERROR]: "Internal code generation error", +}; + +class EvmgenError extends BugError { + constructor( + code: ErrorCode, + message?: string, + location?: SourceLocation, + severity: Severity = Severity.Error, + ) { + const baseMessage = ErrorMessages[code]; + const fullMessage = message ? `${baseMessage}: ${message}` : baseMessage; + super(fullMessage, code, location, severity); + } +} + +export function assertExhausted(_: never) { + throw new EvmgenError( + ErrorCode.INTERNAL_ERROR, + `Unexpected code path; expected exhaustive conditionals`, + undefined, + Severity.Error, + ); +} + +export { EvmgenError as Error }; diff --git a/packages/bugc/src/evmgen/generation/block.ts b/packages/bugc/src/evmgen/generation/block.ts new file mode 100644 index 00000000..3d540a13 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/block.ts @@ -0,0 +1,189 @@ +/** + * Block-level code generation + */ + +import * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { Error, ErrorCode } from "#evmgen/errors"; +import { type Transition, pipe, operations } from "#evmgen/operations"; +import { Memory } from "#evmgen/analysis"; +import { calculateSize } from "#evmgen/serialize"; + +import * as Instruction from "./instruction.js"; +import { loadValue } from "./values/index.js"; +import { + generateTerminator, + generateCallTerminator, +} from "./control-flow/index.js"; +import { annotateTop } from "./values/identify.js"; + +/** + * Generate code for a basic block + */ +export function generate( + block: Ir.Block, + predecessor?: string, + isLastBlock: boolean = false, + isFirstBlock: boolean = false, + isUserFunction: boolean = false, + func?: Ir.Function, +): Transition { + const { JUMPDEST } = operations; + + return pipe() + .peek((state, builder) => { + // Record block offset for jump patching (byte offset, not instruction index) + const blockOffset = calculateSize(state.instructions); + + let result = builder.then((s) => ({ + ...s, + blockOffsets: { + ...s.blockOffsets, + [block.id]: blockOffset, + }, + })); + + // Initialize memory for first block + if (isFirstBlock) { + // Always initialize the free memory pointer for consistency + // This ensures dynamic allocations start after static ones + result = result.then(initializeMemory(state.memory.nextStaticOffset)); + } + + // Set JUMPDEST for non-first blocks + if (!isFirstBlock) { + // Check if this is a call continuation + let isContinuation = false; + let calledFunction = ""; + if (func && predecessor) { + const predBlock = func.blocks.get(predecessor); + if ( + predBlock?.terminator.kind === "call" && + predBlock.terminator.continuation === block.id + ) { + isContinuation = true; + calledFunction = predBlock.terminator.function; + } + } + + // Add JUMPDEST with continuation annotation if applicable + if (isContinuation) { + const continuationDebug = { + context: { + remark: `call-continuation: resume after call to ${calledFunction}`, + }, + }; + result = result.then(JUMPDEST({ debug: continuationDebug })); + } else { + result = result.then(JUMPDEST()); + } + + // Annotate TOS with dest variable if this is a continuation with return value + if (func && predecessor) { + const predBlock = func.blocks.get(predecessor); + if ( + predBlock?.terminator.kind === "call" && + predBlock.terminator.continuation === block.id && + predBlock.terminator.dest + ) { + // TOS has the return value, annotate it + result = result.then(annotateTop(predBlock.terminator.dest)); + } + } + } + + // Process phi nodes if we have a predecessor + if (predecessor && block.phis.length > 0) { + result = result.then(generatePhis(block.phis, predecessor)); + } + + // Process regular instructions + for (const inst of block.instructions) { + result = result.then(Instruction.generate(inst)); + } + + // Process terminator + // Handle call terminators specially (they cross function boundaries) + if (block.terminator.kind === "call") { + result = result.then(generateCallTerminator(block.terminator)); + } else { + result = result.then( + generateTerminator(block.terminator, isLastBlock, isUserFunction), + ); + } + + return result; + }) + .done(); +} + +/** + * Generate code for phi nodes + */ +function generatePhis( + phis: Ir.Block.Phi[], + predecessor: string, +): Transition { + return phis + .reduce( + (builder, phi) => builder.then(generatePhi(phi, predecessor)), + pipe(), + ) + .done(); +} + +function generatePhi( + phi: Ir.Block.Phi, + predecessor: string, +): Transition { + const { PUSHn, MSTORE } = operations; + + const source = phi.sources.get(predecessor); + if (!source) { + throw new Error( + ErrorCode.PHI_NODE_UNRESOLVED, + `Phi ${phi.dest} missing source from ${predecessor}`, + ); + } + + return ( + pipe() + // Load source value and store to phi destination + .then(loadValue(source)) + .peek((state, builder) => { + const allocation = state.memory.allocations[phi.dest]; + if (allocation === undefined) { + throw new Error( + ErrorCode.MEMORY_ALLOCATION_FAILED, + `Phi destination ${phi.dest} not allocated`, + ); + } + return builder + .then(PUSHn(BigInt(allocation.offset)), { as: "offset" }) + .then(MSTORE()); + }) + .done() + ); +} + +/** + * Initialize the free memory pointer at runtime + * Sets the value at 0x40 to the next available memory location after static allocations + */ +function initializeMemory( + nextStaticOffset: number, +): Transition { + const { PUSHn, MSTORE } = operations; + + return ( + pipe() + // Push the static offset value (the value to store) + .then(PUSHn(BigInt(nextStaticOffset)), { as: "value" }) + // Push the free memory pointer location (0x40) (the offset) + .then(PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER)), { as: "offset" }) + // Store the initial free pointer (expects [value, offset] on stack) + .then(MSTORE()) + .done() + ); +} diff --git a/packages/bugc/src/evmgen/generation/control-flow/index.ts b/packages/bugc/src/evmgen/generation/control-flow/index.ts new file mode 100644 index 00000000..6a1a06b1 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/control-flow/index.ts @@ -0,0 +1 @@ +export { generateTerminator, generateCallTerminator } from "./terminator.js"; diff --git a/packages/bugc/src/evmgen/generation/control-flow/terminator.ts b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts new file mode 100644 index 00000000..39fa8f46 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/control-flow/terminator.ts @@ -0,0 +1,232 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; +import type { State } from "#evmgen/state"; + +import { type Transition, operations, pipe } from "#evmgen/operations"; + +import { valueId, loadValue } from "../values/index.js"; + +/** + * Generate code for a block terminator + */ +export function generateTerminator( + term: Ir.Block.Terminator, + isLastBlock: boolean = false, + isUserFunction: boolean = false, +): Transition { + const { PUSHn, PUSH2, MSTORE, MLOAD, RETURN, STOP, JUMP, JUMPI } = operations; + + switch (term.kind) { + case "return": { + // Internal function return: load return PC, jump back + // Note: For returns with a value, we assume the return value is already + // on TOS from the previous instruction in the block. This avoids an + // unnecessary DUP that would leave an extra value on the stack. + if (isUserFunction) { + const returnDebug = { + context: { + remark: term.value + ? "function-return: return with value" + : "function-return: void return", + }, + }; + if (term.value) { + // Return with value (assume value already on TOS) + return pipe() + .then(PUSHn(0x60n, { debug: returnDebug }), { as: "offset" }) + .then(MLOAD({ debug: returnDebug }), { as: "counter" }) + .then(JUMP({ debug: returnDebug })) + .done() as unknown as Transition; + } else { + // Return without value (void return) + return pipe() + .then(PUSHn(0x60n, { debug: returnDebug }), { as: "offset" }) + .then(MLOAD({ debug: returnDebug }), { as: "counter" }) + .then(JUMP({ debug: returnDebug })) + .done() as unknown as Transition; + } + } + + // Contract return (main function or create) + if (term.value) { + const value = term.value; + const id = valueId(value); + + return pipe() + .peek((state, builder) => { + const allocation = state.memory.allocations[id]; + + if (allocation === undefined) { + const offset = state.memory.nextStaticOffset; + return builder + .then(loadValue(value)) + .then(PUSHn(BigInt(offset)), { as: "offset" }) + .then(MSTORE()) + .then(PUSHn(32n), { as: "size" }) + .then(PUSHn(BigInt(offset)), { as: "offset" }) + .then(RETURN()); + } else { + const offset = allocation.offset; + return builder + .then(PUSHn(32n), { as: "size" }) + .then(PUSHn(BigInt(offset)), { as: "offset" }) + .then(RETURN()); + } + }) + .done(); + } else { + return isLastBlock ? (state) => state : pipe().then(STOP()).done(); + } + } + + case "jump": { + return pipe() + .peek((state, builder) => { + const patchIndex = state.instructions.length; + + return builder + .then(PUSH2([0, 0]), { as: "counter" }) + .then(JUMP()) + .then((newState) => ({ + ...newState, + patches: [ + ...newState.patches, + { + index: patchIndex, + target: term.target, + }, + ], + })); + }) + .done(); + } + + case "branch": { + return pipe() + .then(loadValue(term.condition), { as: "b" }) + .peek((state, builder) => { + // Record offset for true target patch + const trueIndex = state.instructions.length; + + return builder + .then(PUSH2([0, 0]), { as: "counter" }) + .then(JUMPI()) + .peek((state2, builder2) => { + // Record offset for false target patch + const falseIndex = state2.instructions.length; + + return builder2 + .then(PUSH2([0, 0]), { as: "counter" }) + .then(JUMP()) + .then((finalState) => ({ + ...finalState, + patches: [ + ...finalState.patches, + { + index: trueIndex, + target: term.trueTarget, + }, + { + index: falseIndex, + target: term.falseTarget, + }, + ], + })); + }); + }) + .done(); + } + + case "call": + // Call terminators should be handled specially in block.ts + throw new Error( + "Call terminator should be handled by generateCallTerminator", + ); + } +} + +/** + * Generate code for a call terminator - handled specially since it crosses function boundaries + */ +export function generateCallTerminator( + term: Extract, +): Transition { + const funcName = term.function; + const args = term.arguments; + const cont = term.continuation; + + return ((state: State): State => { + let currentState: State = state as State; + const returnPcPatchIndex = currentState.instructions.length; + + // Store return PC to memory at 0x60 + const returnPcDebug = { + context: { + remark: `call-preparation: store return address for ${funcName}`, + }, + }; + currentState = { + ...currentState, + instructions: [ + ...currentState.instructions, + { + mnemonic: "PUSH2", + opcode: 0x61, + immediates: [0, 0], + debug: returnPcDebug, + }, + { mnemonic: "PUSH1", opcode: 0x60, immediates: [0x60] }, + { mnemonic: "MSTORE", opcode: 0x52 }, + ], + patches: [ + ...currentState.patches, + { + type: "continuation" as const, + index: returnPcPatchIndex, + target: cont, + }, + ], + }; + + // Push arguments using loadValue + const argsDebug = { + context: { + remark: `call-arguments: push ${args.length} argument(s) for ${funcName}`, + }, + }; + for (const arg of args) { + currentState = loadValue(arg, { debug: argsDebug })(currentState); + } + + // Push function address and jump + const funcAddrPatchIndex = currentState.instructions.length; + const invocationDebug = { + context: { + remark: `call-invocation: jump to function ${funcName}`, + }, + }; + currentState = { + ...currentState, + instructions: [ + ...currentState.instructions, + { + mnemonic: "PUSH2", + opcode: 0x61, + immediates: [0, 0], + debug: invocationDebug, + }, + { mnemonic: "JUMP", opcode: 0x56 }, + ], + patches: [ + ...currentState.patches, + { + type: "function" as const, + index: funcAddrPatchIndex, + target: funcName, + }, + ], + }; + + return currentState; + }) as Transition; +} diff --git a/packages/bugc/src/evmgen/generation/function.test.ts b/packages/bugc/src/evmgen/generation/function.test.ts new file mode 100644 index 00000000..e2533945 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/function.test.ts @@ -0,0 +1,1483 @@ +import { describe, it, expect } from "vitest"; + +import * as Ir from "#ir"; +import { Memory, Liveness, Layout } from "#evmgen/analysis"; + +import { generate } from "./function.js"; + +describe("Function.generate", () => { + it("should generate bytecode for simple constants", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: {}, + nextStaticOffset: 0x80, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have memory initialization (PUSH1 0x80, PUSH1 0x40, MSTORE) followed by PUSH1 42 + // No JUMPDEST for entry with no predecessors, no STOP since it's the last block + expect(instructions).toHaveLength(4); + expect(instructions[0]).toMatchObject({ + mnemonic: "PUSH1", + immediates: [0x80], + }); + expect(instructions[1]).toMatchObject({ + mnemonic: "PUSH1", + immediates: [0x40], + }); + expect(instructions[2]).toMatchObject({ + mnemonic: "MSTORE", + }); + expect(instructions[3]).toMatchObject({ + mnemonic: "PUSH1", + immediates: [42], + }); + }); + + it("should handle SSA temporary operations", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + // In SSA form, we don't have store_local/load_local + // %1 is directly used where needed + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + dest: "%3", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + dest: "%4", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + // In SSA form, temporaries are directly used without locals + // The memory planner allocates values that need to persist across stack operations + + const { instructions } = generate(func, memory, layout); + + // Should contain the constants and ADD operation + expect(instructions.some((inst) => inst.mnemonic === "ADD")).toBe(true); + }); + + it.skip("should generate slice operation with MCOPY - REMOVED SLICE INSTRUCTION", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 0x100n, // Array pointer in memory + type: Ir.Type.Ref.memory(), + dest: "%1", + operationDebug: {}, + }, + { + kind: "const", + value: 2n, // Start index + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + { + kind: "const", + value: 5n, // End index + type: Ir.Type.Scalar.uint256, + dest: "%3", + operationDebug: {}, + }, + { + kind: "slice", + object: { + kind: "temp", + id: "%1", + type: Ir.Type.Ref.memory(), + }, + start: { + kind: "temp", + id: "%2", + type: Ir.Type.Scalar.uint256, + }, + end: { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + dest: "%4", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%4", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = generate(func, memory, layout); + + // Should contain: + // 1. Memory initialization (PUSH 0x80, PUSH 0x40, MSTORE) + // 2. Loading start and end indices + // 3. SUB to calculate length + // 4. MUL by 32 to get byte size + // 5. Memory allocation (updating free memory pointer) + // 6. MCOPY for the actual copy + // 7. Return sequence + + // Check for key instructions + const mnemonics = instructions.map((inst) => inst.mnemonic); + + // Should have SUB for length calculation + expect(mnemonics).toContain("SUB"); + + // Should have MUL for byte size calculation + expect(mnemonics).toContain("MUL"); + + // Should have MLOAD for reading free memory pointer + expect(mnemonics).toContain("MLOAD"); + + // Should have MCOPY for the memory copy + expect(mnemonics).toContain("MCOPY"); + + // Should have RETURN since we're returning a value + expect(mnemonics).toContain("RETURN"); + }); + + it("should generate binary operations", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 10n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + { + kind: "const", + value: 20n, + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%1", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%2", + type: Ir.Type.Scalar.uint256, + }, + dest: "%3", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%1": makeAllocation(0x80), + "%2": makeAllocation(0xa0), + "%3": makeAllocation(0xc0), + }, + nextStaticOffset: 0xe0, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should contain ADD instruction + expect(instructions.some((inst) => inst.mnemonic === "ADD")).toBe(true); + + // Should have memory stores (MSTORE instructions) + expect(instructions.some((inst) => inst.mnemonic === "MSTORE")).toBe(true); + }); + + it("should handle jumps between blocks", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { + kind: "jump", + target: "next", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "next", + { + id: "next", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: {}, + nextStaticOffset: 0x80, + }; + + const layout: Layout.Function.Info = { + order: ["entry", "next"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have JUMP instruction + expect(instructions.some((inst) => inst.mnemonic === "JUMP")).toBe(true); + + // Should have one JUMPDEST (only for the 'next' block which is jumped to) + const jumpdests = instructions.filter( + (inst) => inst.mnemonic === "JUMPDEST", + ); + expect(jumpdests).toHaveLength(1); + + // Should have PUSH2 for jump target + const push2Instructions = instructions.filter( + (inst) => inst.mnemonic === "PUSH2", + ); + expect(push2Instructions).toHaveLength(1); + + // Target should be patched (not [0, 0]) + const push2 = push2Instructions[0]; + expect(push2.immediates).toBeDefined(); + expect(push2.immediates!.length).toBe(2); + const target = (push2.immediates![0] << 8) | push2.immediates![1]; + expect(target).toBeGreaterThan(0); + }); + + it("should handle conditional branches", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.bool, + dest: "%cond", + operationDebug: {}, + }, + ], + terminator: { + kind: "branch", + condition: { + kind: "temp", + id: "%cond", + type: Ir.Type.Scalar.bool, + }, + trueTarget: "then", + falseTarget: "else", + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + [ + "then", + { + id: "then", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + [ + "else", + { + id: "else", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(["entry"]), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { "%cond": { offset: 0x80, size: 32 } }, + nextStaticOffset: 0xa0, + }; + + const layout: Layout.Function.Info = { + order: ["entry", "then", "else"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have JUMPI for conditional jump + expect(instructions.some((inst) => inst.mnemonic === "JUMPI")).toBe(true); + + // Should have PUSH2 instructions for both targets + const push2Instructions = instructions.filter( + (inst) => inst.mnemonic === "PUSH2", + ); + expect(push2Instructions.length).toBe(2); + + // Should have JUMP for unconditional fallthrough + expect(instructions.some((inst) => inst.mnemonic === "JUMP")).toBe(true); + }); + + it("should handle storage operations", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + dest: "%slot", + operationDebug: {}, + }, + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%value", + operationDebug: {}, + }, + { + kind: "write", + location: "storage", + slot: { + kind: "temp", + id: "%slot", + type: Ir.Type.Scalar.uint256, + }, + value: { + kind: "temp", + id: "%value", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%slot": { offset: 0x80, size: 32 }, + "%value": { offset: 0xa0, size: 32 }, + }, + nextStaticOffset: 0xc0, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have SSTORE instruction + expect(instructions.some((inst) => inst.mnemonic === "SSTORE")).toBe(true); + }); + + it("should handle environment operations", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "env", + op: "msg_sender", + dest: "%sender", + operationDebug: {}, + }, + { + kind: "env", + op: "msg_value", + dest: "%value", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%sender": { offset: 0x80, size: 20 }, + "%value": { offset: 0xa0, size: 32 }, + }, + nextStaticOffset: 0xc0, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have CALLER and CALLVALUE instructions + expect(instructions.some((inst) => inst.mnemonic === "CALLER")).toBe(true); + expect(instructions.some((inst) => inst.mnemonic === "CALLVALUE")).toBe( + true, + ); + }); + + it("should handle array slot computation", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%value", + operationDebug: {}, + }, + { + kind: "const", + value: 3n, + type: Ir.Type.Scalar.uint256, + dest: "%index", + operationDebug: {}, + }, + { + kind: "compute_slot", + slotKind: "array", + base: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + dest: "%first_slot", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%first_slot", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%index", + type: Ir.Type.Scalar.uint256, + }, + dest: "%slot", + operationDebug: {}, + }, + { + kind: "write", + location: "storage", + slot: { + kind: "temp", + id: "%slot", + type: Ir.Type.Scalar.uint256, + }, + value: { + kind: "temp", + id: "%value", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%value": { offset: 0x80, size: 32 }, + "%index": { offset: 0xa0, size: 32 }, + "%first_slot": { offset: 0xc0, size: 32 }, + "%slot": { offset: 0xe0, size: 32 }, + }, + nextStaticOffset: 0x100, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should contain KECCAK256 for array slot computation + expect(instructions.some((inst) => inst.mnemonic === "KECCAK256")).toBe( + true, + ); + + // Should contain MSTORE instructions for hash setup + const mstores = instructions.filter((inst) => inst.mnemonic === "MSTORE"); + expect(mstores.length).toBeGreaterThanOrEqual(1); + + // Should contain ADD for index offset + expect(instructions.some((inst) => inst.mnemonic === "ADD")).toBe(true); + + // Should contain SSTORE for storage write + expect(instructions.some((inst) => inst.mnemonic === "SSTORE")).toBe(true); + }); + + it("should handle array element load", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 2n, + type: Ir.Type.Scalar.uint256, + dest: "%index", + operationDebug: {}, + }, + { + kind: "compute_slot", + slotKind: "array", + base: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + dest: "%first_slot", + operationDebug: {}, + }, + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%first_slot", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%index", + type: Ir.Type.Scalar.uint256, + }, + dest: "%slot", + operationDebug: {}, + }, + { + kind: "read", + location: "storage", + slot: { + kind: "temp", + id: "%slot", + type: Ir.Type.Scalar.uint256, + }, + type: Ir.Type.Scalar.uint256, + dest: "%value", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%value", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%index": { offset: 0x80, size: 32 }, + "%first_slot": { offset: 0xa0, size: 32 }, + "%slot": { offset: 0xc0, size: 32 }, + "%value": { offset: 0xe0, size: 32 }, + }, + nextStaticOffset: 0x100, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should compute array base with KECCAK256 + expect(instructions.some((inst) => inst.mnemonic === "KECCAK256")).toBe( + true, + ); + + // Should add index to base + expect(instructions.some((inst) => inst.mnemonic === "ADD")).toBe(true); + + // Should load from storage + expect(instructions.some((inst) => inst.mnemonic === "SLOAD")).toBe(true); + + // No STOP at the end since it's the last block + expect(instructions.some((inst) => inst.mnemonic === "STOP")).toBe(false); + }); + + it("should handle mapping slot computation", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "env", + op: "msg_sender", + dest: "%sender", + operationDebug: {}, + }, + { + kind: "const", + value: 100n, + type: Ir.Type.Scalar.uint256, + dest: "%value", + operationDebug: {}, + }, + { + kind: "compute_slot", + slotKind: "mapping", + base: { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + }, + key: { + kind: "temp", + id: "%sender", + type: Ir.Type.Scalar.address, + }, + keyType: { kind: "address" }, + dest: "%slot", + operationDebug: {}, + }, + { + kind: "write", + location: "storage", + slot: { + kind: "temp", + id: "%slot", + type: Ir.Type.Scalar.uint256, + }, + value: { + kind: "temp", + id: "%value", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%sender": { offset: 0x80, size: 20 }, + "%value": { offset: 0xa0, size: 32 }, + "%slot": { offset: 0xc0, size: 32 }, + }, + nextStaticOffset: 0xe0, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have CALLER for msg.sender + expect(instructions.some((inst) => inst.mnemonic === "CALLER")).toBe(true); + + // Should have KECCAK256 for mapping slot computation + expect(instructions.some((inst) => inst.mnemonic === "KECCAK256")).toBe( + true, + ); + + // Should have SSTORE for final storage + expect(instructions.some((inst) => inst.mnemonic === "SSTORE")).toBe(true); + }); + + it("should handle mapping value load", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "env", + op: "msg_sender", + dest: "%sender", + operationDebug: {}, + }, + { + kind: "compute_slot", + slotKind: "mapping", + base: { + kind: "const", + value: 1n, + type: Ir.Type.Scalar.uint256, + }, + key: { + kind: "temp", + id: "%sender", + type: Ir.Type.Scalar.address, + }, + keyType: { kind: "address" }, + dest: "%slot", + operationDebug: {}, + }, + { + kind: "read", + location: "storage", + slot: { + kind: "temp", + id: "%slot", + type: Ir.Type.Scalar.uint256, + }, + type: Ir.Type.Scalar.uint256, + dest: "%balance", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%balance", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%sender": { offset: 0x80, size: 20 }, + "%slot": { offset: 0xa0, size: 32 }, + "%balance": { offset: 0xc0, size: 32 }, + }, + nextStaticOffset: 0xe0, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should get msg.sender + expect(instructions.some((inst) => inst.mnemonic === "CALLER")).toBe(true); + + // Should compute slot with KECCAK256 + expect(instructions.some((inst) => inst.mnemonic === "KECCAK256")).toBe( + true, + ); + + // Should load from storage + expect(instructions.some((inst) => inst.mnemonic === "SLOAD")).toBe(true); + + // Should have proper memory operations for hash + const mstores = instructions.filter((inst) => inst.mnemonic === "MSTORE"); + expect(mstores.length).toBeGreaterThanOrEqual(2); // For key and baseSlot + }); + + it("should handle nested array/mapping access", () => { + // Test something like: mapping> + // users[msg.sender][index] + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "env", + op: "msg_sender", + dest: "%sender", + operationDebug: {}, + }, + { + kind: "const", + value: 5n, + type: Ir.Type.Scalar.uint256, + dest: "%index", + operationDebug: {}, + }, + // First compute mapping slot for users[msg.sender] + { + kind: "compute_slot", + slotKind: "mapping", + base: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + key: { + kind: "temp", + id: "%sender", + type: Ir.Type.Scalar.address, + }, + keyType: { kind: "address" }, + dest: "%userSlot", + operationDebug: {}, + }, + // Then compute array first slot + { + kind: "compute_slot", + slotKind: "array", + base: { + kind: "temp", + id: "%userSlot", + type: Ir.Type.Scalar.uint256, + }, + dest: "%arrayFirstSlot", + operationDebug: {}, + }, + // Add the index + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "%arrayFirstSlot", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "%index", + type: Ir.Type.Scalar.uint256, + }, + dest: "%finalSlot", + operationDebug: {}, + }, + { + kind: "read", + location: "storage", + slot: { + kind: "temp", + id: "%finalSlot", + type: Ir.Type.Scalar.uint256, + }, + type: Ir.Type.Scalar.uint256, + dest: "%value", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%value", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: { + "%sender": { offset: 0x80, size: 20 }, + "%index": { offset: 0xa0, size: 32 }, + "%userSlot": { offset: 0xc0, size: 32 }, + "%arrayFirstSlot": { offset: 0xe0, size: 32 }, + "%finalSlot": { offset: 0x100, size: 32 }, + "%value": { offset: 0x120, size: 32 }, + }, + nextStaticOffset: 0x140, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Should have KECCAK256 operations for both mapping and array + const keccakInstructions = instructions.filter( + (inst) => inst.mnemonic === "KECCAK256", + ); + // We expect at least 2 (one for mapping, one for array) + expect(keccakInstructions.length).toBeGreaterThanOrEqual(2); + + // Should have ADD for index offset + expect(instructions.some((inst) => inst.mnemonic === "ADD")).toBe(true); + + // Should load from storage + expect(instructions.some((inst) => inst.mnemonic === "SLOAD")).toBe(true); + }); + + it.skip("should handle slice with zero length - REMOVED SLICE INSTRUCTION", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 0x100n, // Array pointer + type: Ir.Type.Ref.memory(), + dest: "%1", + operationDebug: {}, + }, + { + kind: "const", + value: 3n, // Start index + type: Ir.Type.Scalar.uint256, + dest: "%2", + operationDebug: {}, + }, + { + kind: "const", + value: 3n, // End index (same as start = empty slice) + type: Ir.Type.Scalar.uint256, + dest: "%3", + operationDebug: {}, + }, + { + kind: "slice", + object: { + kind: "temp", + id: "%1", + type: Ir.Type.Ref.memory(), + }, + start: { + kind: "temp", + id: "%2", + type: Ir.Type.Scalar.uint256, + }, + end: { + kind: "temp", + id: "%3", + type: Ir.Type.Scalar.uint256, + }, + dest: "%4", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = generate(func, memory, layout); + + // Should still have MCOPY even for zero-length copy + // (though it will copy 0 bytes) + const mnemonics = instructions.map((inst) => inst.mnemonic); + expect(mnemonics).toContain("MCOPY"); + }); + + it.skip("should handle slice operation on calldata (msg.data) - REMOVED SLICE INSTRUCTION", () => { + // Test removed - slice instruction no longer exists in IR + // Slicing is now done through decomposed operations (sub, add, allocate, read, write) + }); + + it("should handle msg.data.length using CALLDATASIZE", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + predecessors: new Set(), + instructions: [ + { + kind: "env", + op: "msg_data", + dest: "%msg_data", + operationDebug: {}, + }, + { + kind: "length", + object: { + kind: "temp", + id: "%msg_data", + type: Ir.Type.Ref.calldata(), + }, + dest: "%data_length", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%data_length", + type: Ir.Type.Scalar.uint256, + }, + operationDebug: {}, + }, + debug: {}, + }, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = generate(func, memory, layout); + const mnemonics = instructions.map((inst) => inst.mnemonic); + + // Should have PUSH0 for msg.data + expect(mnemonics).toContain("PUSH0"); + + // Should have CALLDATASIZE for getting the length + expect(mnemonics).toContain("CALLDATASIZE"); + + // Should NOT have SLOAD (not storage array length) + expect(mnemonics).not.toContain("SLOAD"); + }); + + it.skip("should handle slice operation on bytes - REMOVED SLICE INSTRUCTION", () => { + // Test removed - slice instruction no longer exists in IR + // Slicing is now done through decomposed operations (sub, add, allocate, read, write) + }); + + it("should handle string constants with UTF-8 encoding", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + predecessors: new Set(), + instructions: [ + { + kind: "const", + value: "Hello, world!", + type: Ir.Type.Ref.memory(), + dest: "%greeting", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%greeting", + type: Ir.Type.Ref.memory(), + }, + operationDebug: {}, + }, + debug: {}, + }, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = generate(func, memory, layout); + const mnemonics = instructions.map((inst) => inst.mnemonic); + + // Should allocate memory for the string + expect(mnemonics).toContain("MLOAD"); // Loading free memory pointer + expect(mnemonics).toContain("MSTORE"); // Storing length and data + + // Should have pushed the string length (13 bytes for "Hello, world!") + const pushInstructions = instructions.filter((inst) => + inst.mnemonic.startsWith("PUSH"), + ); + const hasLengthValue = pushInstructions.some( + (inst) => inst.immediates && inst.immediates[0] === 13, + ); + expect(hasLengthValue).toBe(true); + }); + + it("should handle UTF-8 multi-byte characters correctly", () => { + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + predecessors: new Set(), + instructions: [ + { + kind: "const", + value: "Hello 世界! 😊", // Mix of ASCII, Chinese, and emoji + type: Ir.Type.Ref.memory(), + dest: "%greeting", + operationDebug: {}, + }, + ], + terminator: { + kind: "return", + value: { + kind: "temp", + id: "%greeting", + type: Ir.Type.Ref.memory(), + }, + operationDebug: {}, + }, + debug: {}, + }, + ], + ]), + }; + + const liveness = Liveness.Function.analyze(func); + const memoryResult = Memory.Function.plan(func, liveness); + if (!memoryResult.success) throw new Error("Memory planning failed"); + const memory = memoryResult.value; + const layoutResult = Layout.Function.perform(func); + if (!layoutResult.success) throw new Error("Block layout failed"); + const layout = layoutResult.value; + + const { instructions } = generate(func, memory, layout); + const mnemonics = instructions.map((inst) => inst.mnemonic); + + // Should allocate memory for the string + expect(mnemonics).toContain("MLOAD"); // Loading free memory pointer + expect(mnemonics).toContain("MSTORE"); // Storing length and data + + // The UTF-8 byte length should be: + // "Hello " = 6 bytes + // "世界" = 6 bytes (3 bytes each for the Chinese characters) + // "! " = 2 bytes + // "😊" = 4 bytes (emoji) + // Total = 18 bytes + const pushInstructions = instructions.filter((inst) => + inst.mnemonic.startsWith("PUSH"), + ); + const hasLengthValue = pushInstructions.some( + (inst) => inst.immediates && inst.immediates[0] === 18, + ); + expect(hasLengthValue).toBe(true); + }); + + it("should attach debug context to generated instructions", () => { + const debugContext = { + context: { + remark: "test debug context", + }, + }; + + const func: Ir.Function = { + name: "test", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: debugContext, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }; + + const memory: Memory.Function.Info = { + allocations: {}, + nextStaticOffset: 0x80, + }; + + const layout: Layout.Function.Info = { + order: ["entry"], + offsets: new Map(), + }; + + const { instructions } = generate(func, memory, layout); + + // Find the PUSH1 42 instruction + const push42 = instructions.find( + (inst) => inst.mnemonic === "PUSH1" && inst.immediates?.[0] === 42, + ); + + expect(push42).toBeDefined(); + expect(push42?.debug).toEqual(debugContext); + }); +}); + +// Helper to create memory allocations for tests +function makeAllocation(offset: number, size: number = 32): Memory.Allocation { + return { offset, size }; +} diff --git a/packages/bugc/src/evmgen/generation/function.ts b/packages/bugc/src/evmgen/generation/function.ts new file mode 100644 index 00000000..94bb3ff4 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/function.ts @@ -0,0 +1,251 @@ +/** + * Function-level code generation + */ + +import * as Ir from "#ir"; +import type * as Evm from "#evm"; +import type { Stack } from "#evm"; + +import type { State } from "#evmgen/state"; +import type { Layout, Memory } from "#evmgen/analysis"; + +import * as Block from "./block.js"; +import { serialize } from "../serialize.js"; +import { type Transition } from "../operations.js"; + +/** + * Generate prologue for user-defined functions + * Stack on entry: [arg0] [arg1] ... [argN] [return_pc] + * After prologue: empty stack, args stored in memory, return_pc at 0x60 + */ +function generatePrologue( + func: Ir.Function, +): Transition { + const params = func.parameters || []; + + return ((state: State): State => { + let currentState = state; + + // Add JUMPDEST with function entry annotation + const entryDebug = { + context: { + remark: `function-entry: ${func.name || "anonymous"}`, + }, + }; + currentState = { + ...currentState, + instructions: [ + ...currentState.instructions, + { mnemonic: "JUMPDEST", opcode: 0x5b, debug: entryDebug }, + ], + }; + + // Store each parameter to memory and pop from stack + // Stack layout on entry: [arg0, arg1, ..., argN] + // Return PC is already in memory at 0x60 (stored by caller) + // Pop and store each arg from argN down to arg0 + + const prologueDebug = { + context: { + remark: `prologue: store ${params.length} parameter(s) to memory`, + }, + }; + + for (let i = params.length - 1; i >= 0; i--) { + const param = params[i]; + const allocation = currentState.memory.allocations[param.tempId]; + + if (!allocation) continue; + + // Push memory offset + const highByte = (allocation.offset >> 8) & 0xff; + const lowByte = allocation.offset & 0xff; + currentState = { + ...currentState, + instructions: [ + ...currentState.instructions, + { + mnemonic: "PUSH2", + opcode: 0x61, + immediates: [highByte, lowByte], + debug: prologueDebug, + }, + ], + }; + + // MSTORE pops arg and offset + currentState = { + ...currentState, + instructions: [ + ...currentState.instructions, + { mnemonic: "MSTORE", opcode: 0x52 }, + ], + }; + } + + // Return with empty stack + return { + ...currentState, + stack: [], + brands: [], + } as State; + }) as Transition; +} + +/** + * Generate bytecode for a function + */ +export function generate( + func: Ir.Function, + memory: Memory.Function.Info, + layout: Layout.Function.Info, + options: { isUserFunction?: boolean } = {}, +) { + const initialState: State = { + brands: [], + stack: [], + instructions: [], + memory, + nextId: 0, + patches: [], + blockOffsets: {}, + warnings: [], + functionRegistry: {}, + callStackPointer: 0x60, + }; + + // Add prologue for user functions (not main/create) + let stateAfterPrologue = initialState; + if (options.isUserFunction) { + const prologueTransition = generatePrologue(func); + stateAfterPrologue = prologueTransition(initialState); + } + + const finalState = layout.order.reduce( + (state: State, blockId: string, index: number) => { + const block = func.blocks.get(blockId); + if (!block) return state; + + // Determine predecessor for phi resolution + // This is simplified - real implementation would track actual control flow + const predecessor = index > 0 ? layout.order[index - 1] : undefined; + + // Check if this is the first or last block + const isFirstBlock = index === 0; + const isLastBlock = index === layout.order.length - 1; + + return Block.generate( + block, + predecessor, + isLastBlock, + isFirstBlock, + options.isUserFunction || false, + func, + )(state); + }, + stateAfterPrologue, + ); + + // Patch block jump targets (not function calls yet) + const patchedState = patchJumps(finalState); + + // Serialize to bytecode + const bytecode = serialize(patchedState.instructions); + + return { + instructions: patchedState.instructions, + bytecode, + warnings: patchedState.warnings, + patches: finalState.patches, // Return patches for module-level patching + }; +} + +/** + * Patch jump targets after all blocks have been generated + */ +function patchJumps(state: State): State { + const patchedInstructions = [...state.instructions]; + + for (const patch of state.patches) { + // Skip function patches - they'll be handled at module level + if (patch.type === "function") { + continue; + } + + // Both block jumps and continuation patches use blockOffsets + const targetOffset = state.blockOffsets[patch.target]; + if (targetOffset === undefined) { + throw new Error(`Jump target ${patch.target} not found`); + } + + // Convert offset to bytes for PUSH2 (2 bytes, big-endian) + const highByte = (targetOffset >> 8) & 0xff; + const lowByte = targetOffset & 0xff; + + // Update the PUSH2 instruction at the patch index + const instruction = patchedInstructions[patch.index]; + if (instruction && instruction.immediates) { + instruction.immediates = [highByte, lowByte]; + } + } + + return { + ...state, + instructions: patchedInstructions, + }; +} + +/** + * Patch function call addresses in bytecode + */ +export function patchFunctionCalls( + bytecode: number[], + instructions: Evm.Instruction[], + patches: State["patches"], + functionRegistry: Record, +): { bytecode: number[]; instructions: Evm.Instruction[] } { + const patchedInstructions = [...instructions]; + const patchedBytecode = [...bytecode]; + + for (const patch of patches) { + // Only handle function patches + if (patch.type !== "function") { + continue; + } + + const targetOffset = functionRegistry[patch.target]; + if (targetOffset === undefined) { + throw new Error(`Function ${patch.target} not found in registry`); + } + + // Convert offset to bytes for PUSH2 (2 bytes, big-endian) + const highByte = (targetOffset >> 8) & 0xff; + const lowByte = targetOffset & 0xff; + + // Update the PUSH2 instruction + const instruction = patchedInstructions[patch.index]; + if (instruction && instruction.immediates) { + instruction.immediates = [highByte, lowByte]; + } + + // Also patch in the bytecode + // Find the instruction's position in bytecode + let bytePos = 0; + for (let i = 0; i < patch.index; i++) { + const inst = instructions[i]; + bytePos += 1; // opcode + if (inst.immediates) { + bytePos += inst.immediates.length; + } + } + // Skip opcode byte + bytePos += 1; + patchedBytecode[bytePos] = highByte; + patchedBytecode[bytePos + 1] = lowByte; + } + + return { + bytecode: patchedBytecode, + instructions: patchedInstructions, + }; +} diff --git a/packages/bugc/src/evmgen/generation/index.ts b/packages/bugc/src/evmgen/generation/index.ts new file mode 100644 index 00000000..b4baf860 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/index.ts @@ -0,0 +1,4 @@ +export * as Instruction from "./instruction.js"; +export * as Block from "./block.js"; +export * as Function from "./function.js"; +export * as Module from "./module.js"; diff --git a/packages/bugc/src/evmgen/generation/instruction.ts b/packages/bugc/src/evmgen/generation/instruction.ts new file mode 100644 index 00000000..e299aff6 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instruction.ts @@ -0,0 +1,63 @@ +/** + * IR instruction code generation - dispatcher + */ + +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import type { Transition } from "#evmgen/operations"; + +import { + generateBinary, + generateUnary, + generateCast, + generateConst, + generateEnvOp, + generateHashOp, + generateLength, + generateComputeSlot, + generateRead, + generateWrite, + generateAllocate, + generateComputeOffset, +} from "./instructions/index.js"; + +/** + * Generate code for an IR instruction + */ +export function generate( + inst: Ir.Instruction, +): Transition { + switch (inst.kind) { + case "const": + return generateConst(inst); + case "binary": + return generateBinary(inst); + case "unary": + return generateUnary(inst); + case "read": + return generateRead(inst); + case "write": + return generateWrite(inst); + case "env": + return generateEnvOp(inst); + case "hash": + return generateHashOp(inst); + case "length": + return generateLength(inst); + case "compute_slot": + return generateComputeSlot(inst); + case "cast": + return generateCast(inst); + case "allocate": + return generateAllocate(inst); + case "compute_offset": + return generateComputeOffset(inst); + // Call instruction removed - calls are now block terminators + default: { + // This should be unreachable if all instruction types are handled + const _: never = inst; + return _ as never; + } + } +} diff --git a/packages/bugc/src/evmgen/generation/instructions/allocate.ts b/packages/bugc/src/evmgen/generation/instructions/allocate.ts new file mode 100644 index 00000000..4a1f719f --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/allocate.ts @@ -0,0 +1,31 @@ +/** + * Allocate instruction code generation + */ + +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { type Transition, pipe } from "#evmgen/operations"; +import { allocateMemoryDynamic } from "../memory/index.js"; +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +/** + * Generate code for allocate instructions + * Loads the size value onto the stack, then allocates memory + */ +export function generateAllocate( + inst: Ir.Instruction.Allocate, +): Transition { + const debug = inst.operationDebug; + + return ( + pipe() + // Load the size value onto the stack + .then(loadValue(inst.size, { debug }), { as: "size" }) + // Allocate memory using the dynamic allocator + .then(allocateMemoryDynamic({ debug }), { as: "value" }) + // Store the result if needed + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/binary.ts b/packages/bugc/src/evmgen/generation/instructions/binary.ts new file mode 100644 index 00000000..3298d568 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/binary.ts @@ -0,0 +1,65 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import type { State } from "#evmgen/state"; +import { type Transition, operations, pipe, rebrand } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { ADD, SUB, MUL, DIV, MOD, EQ, LT, GT, AND, OR, ISZERO, SHL, SHR } = + operations; + +/** + * Generate code for binary operations + */ +export function generateBinary( + inst: Ir.Instruction.BinaryOp, +): Transition { + const debug = inst.operationDebug; + + const map: { + [O in Ir.Instruction.BinaryOp["op"]]: ( + state: State, + ) => State; + } = { + add: ADD({ debug }), + sub: SUB({ debug }), + mul: MUL({ debug }), + div: DIV({ debug }), + mod: MOD({ debug }), + shl: pipe() + .then(rebrand<"a", "shift", "b", "value">({ 1: "shift", 2: "value" })) + .then(SHL({ debug })) + .done(), + shr: pipe() + .then(rebrand<"a", "shift", "b", "value">({ 1: "shift", 2: "value" })) + .then(SHR({ debug })) + .done(), + eq: EQ({ debug }), + ne: pipe() + .then(EQ({ debug }), { as: "a" }) + .then(ISZERO({ debug })) + .done(), + // Note: operands are loaded as [left=b, right=a] so EVM comparisons are reversed + // EVM LT returns a < b (right < left), so use GT for IR lt (left < right) + lt: GT({ debug }), + le: pipe() + .then(LT({ debug }), { as: "a" }) + .then(ISZERO({ debug })) + .done(), + gt: LT({ debug }), + ge: pipe() + .then(GT({ debug }), { as: "a" }) + .then(ISZERO({ debug })) + .done(), + and: AND({ debug }), + or: OR({ debug }), + }; + + return pipe() + .then(loadValue(inst.left, { debug }), { as: "b" }) + .then(loadValue(inst.right, { debug }), { as: "a" }) + .then(map[inst.op], { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/cast.ts b/packages/bugc/src/evmgen/generation/instructions/cast.ts new file mode 100644 index 00000000..3fb3a97b --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/cast.ts @@ -0,0 +1,22 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { type Transition, pipe } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +/** + * Generate code for cast instructions + * Cast is a no-op at the EVM level since types are checked at compile time + */ +export function generateCast( + inst: Ir.Instruction.Cast, +): Transition { + const debug = inst.operationDebug; + + // Just load the value and store it with the new type annotation + return pipe() + .then(loadValue(inst.value, { debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/compute-offset.ts b/packages/bugc/src/evmgen/generation/instructions/compute-offset.ts new file mode 100644 index 00000000..e1cf9824 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/compute-offset.ts @@ -0,0 +1,98 @@ +/** + * Compute offset instruction code generation + */ + +import * as Ir from "#ir"; +import type { Stack } from "#evm"; +import { assertExhausted } from "#evmgen/errors"; + +import { type Transition, pipe, rebrand, operations } from "#evmgen/operations"; +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { MUL, ADD } = operations; + +/** + * Generate code for compute_offset instructions + * Computes: base + (index * stride) + byteOffset/fieldOffset + */ +export function generateComputeOffset( + inst: Ir.Instruction.ComputeOffset, +): Transition { + const debug = inst.operationDebug; + + // For now, handle memory/calldata/returndata the same way + // The location property may matter for future optimizations or bounds checking + + // Handle index-based offset (for arrays) + if (Ir.Instruction.ComputeOffset.isArray(inst)) { + return ( + pipe() + // Load base address + .then(loadValue(inst.base, { debug }), { as: "base" }) + .then(loadValue(inst.index, { debug }), { as: "index" }) + // Load stride + .then( + loadValue( + Ir.Value.constant(BigInt(inst.stride), Ir.Type.Scalar.uint256), + { debug }, + ), + { as: "stride" }, + ) + .then(rebrand<"stride", "a", "index", "b">({ 1: "a", 2: "b" })) + // Compute index * stride + .then(MUL({ debug }), { as: "scaled_index" }) + // Add to base + .then(rebrand<"scaled_index", "a", "base", "b">({ 1: "a", 2: "b" })) + .then(ADD({ debug }), { as: "value" }) + // Store the result + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + + // Handle field offset (for structs) + if (Ir.Instruction.ComputeOffset.isField(inst)) { + if (inst.fieldOffset === 0) { + // No offset needed, just load the base + return pipe() + .then(loadValue(inst.base, { debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + return ( + pipe() + // Load base address + .then(loadValue(inst.base, { debug }), { as: "base" }) + .then( + loadValue( + Ir.Value.constant(BigInt(inst.fieldOffset), Ir.Type.Scalar.uint256), + { debug }, + ), + { as: "field_offset" }, + ) + .then(rebrand<"field_offset", "a", "base", "b">({ 1: "a", 2: "b" })) + .then(ADD({ debug }), { as: "value" }) + // Store the result + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + + // Handle byte offset + if (Ir.Instruction.ComputeOffset.isByte(inst)) { + return ( + pipe() + // Load base address + .then(loadValue(inst.base, { debug }), { as: "base" }) + .then(loadValue(inst.offset, { debug }), { as: "byte_offset" }) + .then(rebrand<"byte_offset", "a", "base", "b">({ 1: "a", 2: "b" })) + .then(ADD({ debug }), { as: "value" }) + // Store the result + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + + assertExhausted(inst); + throw new Error(`Unknown compute_offset type`); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/compute-slot.ts b/packages/bugc/src/evmgen/generation/instructions/compute-slot.ts new file mode 100644 index 00000000..a0cea115 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/compute-slot.ts @@ -0,0 +1,80 @@ +import * as Ir from "#ir"; + +import type { Stack } from "#evm"; + +import { type Transition, pipe, operations } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { PUSHn, MSTORE, KECCAK256, ADD } = operations; + +/** + * Generate code for computing a storage slot based on kind + */ +export function generateComputeSlot( + inst: Ir.Instruction.ComputeSlot, +): Transition { + const debug = inst.operationDebug; + + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + // For mappings: keccak256(key || baseSlot) + if (!inst.key) { + throw new Error("Mapping compute_slot requires key"); + } + return ( + pipe() + // store key then base in memory as 32 bytes each + .then(loadValue(inst.key, { debug })) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + + .then(loadValue(inst.base, { debug })) + .then(PUSHn(32n, { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .then(PUSHn(64n, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(KECCAK256({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + + if (Ir.Instruction.ComputeSlot.isArray(inst)) { + // For arrays: just compute keccak256(base) - the first slot + // The index will be added separately by the IR generator + return ( + pipe() + // Store base at memory offset 0 + .then(loadValue(inst.base, { debug })) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + + // Hash 32 bytes starting at offset 0 + .then(PUSHn(32n, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(KECCAK256({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + + if (Ir.Instruction.ComputeSlot.isField(inst)) { + // For struct fields: base + (fieldOffset / 32) to get the slot + if (inst.fieldOffset === undefined) { + throw new Error("Field compute_slot requires fieldOffset"); + } + // Convert byte offset to slot offset + const slotOffset = Math.floor(inst.fieldOffset / 32); + return pipe() + .then(loadValue(inst.base, { debug }), { as: "b" }) + .then(PUSHn(BigInt(slotOffset), { debug }), { as: "a" }) + .then(ADD({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + + // This should never be reached due to exhaustive type checking + const _exhaustive: never = inst; + void _exhaustive; + throw new Error(`Unknown compute_slot kind`); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/const.ts b/packages/bugc/src/evmgen/generation/instructions/const.ts new file mode 100644 index 00000000..95821deb --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/const.ts @@ -0,0 +1,127 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { + type Transition, + operations, + pipe, + rebrandTop, +} from "#evmgen/operations"; + +import { storeValueIfNeeded } from "../values/index.js"; +import { allocateMemory } from "../memory/index.js"; + +const { PUSHn, DUP2, MSTORE, ADD } = operations; + +/** + * Generate code for const instructions + */ +export function generateConst( + inst: Ir.Instruction.Const, +): Transition { + const debug = inst.operationDebug; + + // Check the type to determine how to handle the constant + // Scalar values are stored directly on the stack + if (inst.type.kind === "scalar") { + // Scalar - just push the value + let value: bigint; + if (typeof inst.value === "string" && inst.value.startsWith("0x")) { + // It's a hex string, convert to bigint + value = BigInt(inst.value); + } else if (typeof inst.value === "bigint") { + value = inst.value; + } else { + value = BigInt(inst.value); + } + return pipe() + .then(PUSHn(value, { debug })) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + + // References need memory allocation for the data they point to + if (inst.type.kind === "ref" && inst.type.location === "memory") { + let bytes: Uint8Array; + let byteLength: bigint; + + // For memory references, we need to check the origin type to understand what data to store + // For now, we'll handle hex strings and regular strings + if (typeof inst.value === "string" && inst.value.startsWith("0x")) { + // Dynamic bytes from hex string - decode the hex + const hexStr = inst.value.slice(2); // Remove 0x prefix + const hexBytes = []; + for (let i = 0; i < hexStr.length; i += 2) { + hexBytes.push(parseInt(hexStr.substr(i, 2), 16)); + } + bytes = new Uint8Array(hexBytes); + byteLength = BigInt(bytes.length); + } else { + // String or non-hex bytes - use UTF-8 encoding + const strValue = String(inst.value); + const encoder = new TextEncoder(); + bytes = encoder.encode(strValue); + byteLength = BigInt(bytes.length); + } + + // Calculate memory needed: 32 bytes for length + actual data (padded to 32-byte words) + const dataWords = (byteLength + 31n) / 32n; + const totalBytes = 32n + dataWords * 32n; + + // String/bytes constants need to be stored in memory + return ( + pipe() + // Allocate memory dynamically + .then(allocateMemory(totalBytes, { debug }), { as: "offset" }) + + // Store the length at the allocated offset + .then(PUSHn(BigInt(byteLength), { debug }), { as: "value" }) + .then(DUP2({ debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .peek((_, builder) => { + let result = builder; + + // Store the actual bytes + // For simplicity, we'll pack bytes into 32-byte words + for (let wordIdx = 0n; wordIdx < dataWords; wordIdx++) { + const wordStart = wordIdx * 32n; + const wordEnd = + byteLength < wordStart + 32n ? byteLength : wordStart + 32n; + + // Pack up to 32 bytes into a single word + let wordValue = 0n; + for (let i = wordStart; i < wordEnd; i++) { + // Shift left and add the byte (big-endian) + wordValue = (wordValue << 8n) | BigInt(bytes[Number(i)]); + } + + // Pad remaining bytes with zeros (already done by shifting) + const remainingBytes = 32n - (wordEnd - wordStart); + wordValue = wordValue << (remainingBytes * 8n); + + // Store the word at offset + 32 + (wordIdx * 32) + const storeOffset = 32n + wordIdx * 32n; + result = result + .then(PUSHn(wordValue, { debug }), { as: "value" }) + .then(DUP2({ debug }), { as: "b" }) + .then(PUSHn(storeOffset, { debug }), { as: "a" }) + .then(ADD({ debug }), { as: "offset" }) + .then(MSTORE({ debug })); + } + + // The original offset is still on the stack (from DUP2 operations) + // Rebrand it as value for return + return result + .then(rebrandTop("value")) + .then(storeValueIfNeeded(inst.dest, { debug })); + }) + .done() + ); + } + + // For numeric and boolean constants, use existing behavior + return pipe() + .then(PUSHn(BigInt(inst.value), { debug })) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/env.ts b/packages/bugc/src/evmgen/generation/instructions/env.ts new file mode 100644 index 00000000..56ea8597 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/env.ts @@ -0,0 +1,35 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; +import type { State } from "#evmgen/state"; + +import { type Transition, pipe, operations } from "#evmgen/operations"; + +import { storeValueIfNeeded } from "../values/index.js"; + +const { CALLER, CALLVALUE, PUSH0, TIMESTAMP, NUMBER } = operations; + +/** + * Generate code for environment operations + */ +export function generateEnvOp( + inst: Ir.Instruction.Env, +): Transition { + const debug = inst.operationDebug; + + const map: { + [O in Ir.Instruction.Env["op"]]: ( + state: State, + ) => State; + } = { + msg_sender: CALLER({ debug }), + msg_value: CALLVALUE({ debug }), + msg_data: PUSH0({ debug }), // Returns calldata offset (0) + block_timestamp: TIMESTAMP({ debug }), + block_number: NUMBER({ debug }), + }; + + return pipe() + .then(map[inst.op], { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/hash.ts b/packages/bugc/src/evmgen/generation/instructions/hash.ts new file mode 100644 index 00000000..ce5fe783 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/hash.ts @@ -0,0 +1,27 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { type Transition, pipe, operations } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { PUSHn, MSTORE, KECCAK256 } = operations; + +/** + * Generate code for hash operations + */ +export function generateHashOp( + inst: Ir.Instruction.Hash, +): Transition { + const debug = inst.operationDebug; + + return pipe() + .then(loadValue(inst.value, { debug })) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .then(PUSHn(32n, { debug }), { as: "size" }) + .then(PUSHn(0n, { debug }), { as: "offset" }) + .then(KECCAK256({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/index.ts b/packages/bugc/src/evmgen/generation/instructions/index.ts new file mode 100644 index 00000000..a952a63d --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/index.ts @@ -0,0 +1,11 @@ +export { generateBinary } from "./binary.js"; +export { generateUnary } from "./unary.js"; +export { generateCast } from "./cast.js"; +export { generateConst } from "./const.js"; +export { generateEnvOp } from "./env.js"; +export { generateHashOp } from "./hash.js"; +export { generateLength } from "./length.js"; +export { generateRead, generateWrite } from "./storage.js"; +export { generateComputeSlot } from "./compute-slot.js"; +export { generateAllocate } from "./allocate.js"; +export { generateComputeOffset } from "./compute-offset.js"; diff --git a/packages/bugc/src/evmgen/generation/instructions/length.ts b/packages/bugc/src/evmgen/generation/instructions/length.ts new file mode 100644 index 00000000..70373fd5 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/length.ts @@ -0,0 +1,90 @@ +import * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { Error, ErrorCode } from "#evmgen/errors"; +import { type Transition, pipe, operations } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded, valueId } from "../values/index.js"; + +const { PUSHn, CALLDATASIZE, SLOAD, MLOAD, SUB, SHR } = operations; + +/** + * Generate code for length operations + */ +export function generateLength( + inst: Ir.Instruction.Length, +): Transition { + const debug = inst.operationDebug; + + // Check if this is msg.data (calldata) - use CALLDATASIZE + const objectId = valueId(inst.object); + const isCalldata = + objectId.includes("calldata") || + objectId.includes("msg_data") || + objectId.includes("msg.data"); + + if (isCalldata) { + return pipe() + .then(CALLDATASIZE({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + + // Length instruction - with the new type system, we need to handle references + const objectType = inst.object.type; + + // For references, we need to check the origin type to understand the data structure + if (objectType.kind === "ref") { + // Check if this is a dynamic array or string in memory + return pipe() + .peek((state, builder) => { + // Check if value is in memory + const isInMemory = + objectId in state.memory.allocations || + state.stack.findIndex(({ irValue }) => irValue === objectId) > -1; + + if (isInMemory || objectType.location === "memory") { + // Memory data: length is stored at the pointer location + // First word contains length + return builder + .then(loadValue(inst.object, { debug }), { as: "offset" }) + .then(MLOAD({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })); + } else { + // Storage data: length is packed with data if short, or in slot if long + // For simplicity, assume it's stored at the slot (long string/bytes) + // The length is stored as 2 * length + 1 in the slot for long strings + return ( + builder + .then(loadValue(inst.object, { debug }), { as: "key" }) + .then(SLOAD({ debug }), { as: "b" }) + // Extract length from storage format + // For long strings: (value - 1) / 2 + .then(PUSHn(1n, { debug }), { as: "a" }) + .then(SUB({ debug }), { as: "value" }) + .then(PUSHn(1n, { debug }), { as: "shift" }) + .then(SHR({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + ); + } + }) + .done(); + } + + // For scalars, we might have fixed-size data + if (objectType.kind === "scalar") { + // Fixed-size data - this shouldn't normally happen for length instruction + // but we could return the size in bytes + return pipe() + .then(PUSHn(BigInt(objectType.size), { debug })) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + + // This should never happen as we've covered all type kinds + // But TypeScript doesn't know that, so we need to handle it + throw new Error( + ErrorCode.UNSUPPORTED_INSTRUCTION, + `length operation not supported for type`, + ); +} diff --git a/packages/bugc/src/evmgen/generation/instructions/storage.ts b/packages/bugc/src/evmgen/generation/instructions/storage.ts new file mode 100644 index 00000000..49ad7b15 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/storage.ts @@ -0,0 +1,183 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import { type Transition, rebrand, pipe, operations } from "#evmgen/operations"; + +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { + SWAP1, + PUSHn, + SLOAD, + SSTORE, + MLOAD, + MSTORE, + SHL, + SHR, + AND, + OR, + NOT, + SUB, + DUP1, +} = operations; + +/** + * Generate code for the new unified read instruction + */ +export function generateRead( + inst: Ir.Instruction.Read, +): Transition { + const debug = inst.operationDebug; + + // Handle storage reads + if (inst.location === "storage" && inst.slot) { + const offset = inst.offset?.kind === "const" ? inst.offset.value : 0n; + const length = inst.length?.kind === "const" ? inst.length.value : 32n; + + if (offset === 0n && length === 32n) { + // Full slot read - simple SLOAD + return pipe() + .then(loadValue(inst.slot, { debug }), { as: "key" }) + .then(SLOAD({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } else { + // Partial read - need to extract specific bytes + return ( + pipe() + .then(loadValue(inst.slot, { debug }), { as: "key" }) + .then(SLOAD({ debug }), { as: "value" }) + + // Shift right to move desired bytes to the right (low) end + // We shift by (32 - offset - length) * 8 bits + .then( + PUSHn((32n - BigInt(offset) - BigInt(length)) * 8n, { debug }), + { + as: "shift", + }, + ) + .then(SHR({ debug }), { as: "shiftedValue" }) + .then(PUSHn(1n, { debug }), { as: "b" }) + + // Mask to keep only the desired length + // mask = (1 << (length * 8)) - 1 + .then(PUSHn(1n, { debug }), { as: "value" }) + .then(PUSHn(BigInt(length) * 8n, { debug }), { as: "shift" }) + .then(SHL({ debug }), { as: "a" }) // (1 << (length * 8)) + .then(SUB({ debug }), { as: "mask" }) // ((1 << (length * 8)) - 1) + .then(rebrand<"mask", "a", "shiftedValue", "b">({ 1: "a", 2: "b" })) + + // Apply mask: shiftedValue & mask + .then(AND({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done() + ); + } + } + + // Handle memory reads + if (inst.location === "memory" && inst.offset) { + return pipe() + .then(loadValue(inst.offset, { debug }), { as: "offset" }) + .then(MLOAD({ debug }), { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); + } + + // TODO: Handle other locations (calldata, returndata) + // For unsupported locations, push a dummy value to maintain stack typing + return pipe().then(PUSHn(0n, { debug }), { as: "value" }).done(); +} + +/** + * Generate code for the new unified write instruction + */ +export function generateWrite( + inst: Ir.Instruction.Write, +): Transition { + const debug = inst.operationDebug; + + // Handle storage writes + if (inst.location === "storage" && inst.slot && inst.value) { + // Check if this is a partial write (offset != 0 or length != 32) + const offset = inst.offset?.kind === "const" ? inst.offset.value : 0n; + const length = inst.length?.kind === "const" ? inst.length.value : 32n; + + if (offset === 0n && length === 32n) { + // Full slot write - simple SSTORE + return pipe() + .then(loadValue(inst.value, { debug }), { as: "value" }) + .then(loadValue(inst.slot, { debug }), { as: "key" }) + .then(SSTORE({ debug })) + .done(); + } else { + // Partial write - need to do read-modify-write with masking + return ( + pipe() + // Load the slot key and duplicate for later SSTORE + .then(loadValue(inst.slot, { debug }), { as: "key" }) + .then(DUP1({ debug })) + + // Load current value from storage + .then(SLOAD({ debug }), { as: "current" }) + + // Create mask to clear the bits we're updating + // First create: (1 << (length * 8)) - 1 + .then(PUSHn(1n, { debug }), { as: "b" }) + .then(PUSHn(1n, { debug }), { as: "value" }) + .then(PUSHn(BigInt(length) * 8n, { debug }), { as: "shift" }) + .then(SHL({ debug }), { as: "a" }) // (1 << (length * 8)) + .then(SUB({ debug }), { as: "lengthMask" }) // ((1 << (length * 8)) - 1) + + // Then shift it left by offset: ((1 << (length * 8)) - 1) << (offset * 8) + .then(PUSHn(BigInt(offset) * 8n, { debug }), { as: "bitOffset" }) + .then( + rebrand<"bitOffset", "shift", "lengthMask", "value">({ + 1: "shift", + 2: "value", + }), + ) + .then(SHL({ debug }), { as: "a" }) + + // Invert to get clear mask: ~(((1 << (length * 8)) - 1) << (offset * 8)) + .then(NOT({ debug }), { as: "clearMask" }) + .then(rebrand<"clearMask", "a", "current", "b">({ 1: "a", 2: "b" })) + + // Clear the bits in the current value: current & clearMask + .then(AND({ debug }), { as: "clearedCurrent" }) + + // Prepare the new value (shift to correct position) + .then(loadValue(inst.value, { debug }), { as: "value" }) + .then(PUSHn(BigInt(offset) * 8n, { debug }), { as: "shift" }) + .then(SHL({ debug }), { as: "shiftedValue" }) + + .then( + rebrand<"shiftedValue", "a", "clearedCurrent", "b">({ + 1: "a", + 2: "b", + }), + ) + + // Combine: clearedCurrent | shiftedValue + .then(OR({ debug }), { as: "value" }) + .then(SWAP1({ debug })) + + // Store the result (key is already on stack from DUP1) + .then(SSTORE({ debug })) + .done() + ); + } + } + + // Handle memory writes + if (inst.location === "memory" && inst.offset && inst.value) { + return pipe() + .then(loadValue(inst.value, { debug }), { as: "value" }) + .then(loadValue(inst.offset, { debug }), { as: "offset" }) + .then(MSTORE({ debug })) + .done(); + } + + // TODO: Handle other locations + return (state) => state; +} diff --git a/packages/bugc/src/evmgen/generation/instructions/unary.ts b/packages/bugc/src/evmgen/generation/instructions/unary.ts new file mode 100644 index 00000000..41f7f3ae --- /dev/null +++ b/packages/bugc/src/evmgen/generation/instructions/unary.ts @@ -0,0 +1,41 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; + +import type { State } from "#evmgen/state"; +import { + type Transition, + pipe, + operations, + rebrandTop, +} from "#evmgen/operations"; +import { loadValue, storeValueIfNeeded } from "../values/index.js"; + +const { NOT, PUSHn, SUB } = operations; + +/** + * Generate code for unary operations + */ +export function generateUnary( + inst: Ir.Instruction.UnaryOp, +): Transition { + const debug = inst.operationDebug; + + const map: { + [O in Ir.Instruction.UnaryOp["op"]]: ( + state: State, + ) => State; + } = { + not: NOT({ debug }), + neg: pipe() + .then(rebrandTop("b")) + .then(PUSHn(0n, { debug }), { as: "a" }) + .then(SUB({ debug })) + .done(), + }; + + return pipe() + .then(loadValue(inst.operand, { debug }), { as: "a" }) + .then(map[inst.op], { as: "value" }) + .then(storeValueIfNeeded(inst.dest, { debug })) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/memory/allocate.ts b/packages/bugc/src/evmgen/generation/memory/allocate.ts new file mode 100644 index 00000000..7297d598 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/memory/allocate.ts @@ -0,0 +1,59 @@ +import * as Evm from "#evm"; +import type { Stack } from "#evm"; +import { type Transition, operations, pipe } from "#evmgen/operations"; +import { Memory } from "#evmgen/analysis"; + +/** + * Allocate memory dynamically at runtime + * Loads the free memory pointer, returns it, and updates it + * + * Stack: [...] -> [allocatedOffset, ...] + * + * Note: This is a simplified version. A proper implementation would + * need to handle stack types more carefully. + */ +export function allocateMemory( + sizeBytes: bigint, + options?: Evm.InstructionOptions, +): Transition { + const { PUSHn } = operations; + + return pipe() + .then(PUSHn(sizeBytes, options), { as: "size" }) + .then(allocateMemoryDynamic(options)) + .done(); +} + +/** + * Allocate memory dynamically (runtime-determined size) + * Takes size from stack, returns offset + */ +export function allocateMemoryDynamic( + options?: Evm.InstructionOptions, +): Transition { + const { PUSHn, SWAP1, DUP2, MLOAD, ADD, MSTORE } = operations; + + return ( + pipe() + // Load current free memory pointer from 0x40 + .then(PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER), options), { + as: "offset", + }) + .then(MLOAD(options), { as: "offset" }) + .then(SWAP1(options), { as: "b" }) + // Stack: [size, current_fmp, ...] + // Save current for return, calculate new = current + size + .then(DUP2(options), { as: "a" }) + // Stack: [current_fmp, size, current_fmp, ...] + .then(ADD(options), { as: "value" }) + // Stack: [new_fmp, current_fmp, ...] + + // Store new free pointer + .then(PUSHn(BigInt(Memory.regions.FREE_MEMORY_POINTER), options), { + as: "offset", + }) + .then(MSTORE(options)) + // Stack: [current_fmp(allocated), ...] + .done() + ); +} diff --git a/packages/bugc/src/evmgen/generation/memory/index.ts b/packages/bugc/src/evmgen/generation/memory/index.ts new file mode 100644 index 00000000..ad76d7fa --- /dev/null +++ b/packages/bugc/src/evmgen/generation/memory/index.ts @@ -0,0 +1,6 @@ +export { allocateMemory, allocateMemoryDynamic } from "./allocate.js"; +export { + getTypeSize, + getSliceElementSize, + getSliceDataOffset, +} from "./slice.js"; diff --git a/packages/bugc/src/evmgen/generation/memory/slice.ts b/packages/bugc/src/evmgen/generation/memory/slice.ts new file mode 100644 index 00000000..37661bc6 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/memory/slice.ts @@ -0,0 +1,52 @@ +import * as Ir from "#ir"; + +/** + * Get the size of a type in bytes + */ +export function getTypeSize(type: Ir.Type): bigint { + switch (type.kind) { + case "scalar": + return BigInt(type.size); + case "ref": + return 32n; // references are always 32 bytes (pointer/slot) + default: + return 32n; // default to word size + } +} + +/** + * Get the element size for sliceable types + * Returns the size of each element in bytes + */ +export function getSliceElementSize(type: Ir.Type): bigint { + // With the new type system, we need to examine the origin + // to understand what kind of data this is + // For now, we'll use conservative defaults + if (type.kind === "ref") { + // References to arrays/strings/bytes + // Arrays: elements are 32-byte padded + // Strings/bytes: elements are 1 byte + // Without origin information, default to 1 byte (conservative for slicing) + return 1n; + } + throw new Error(`Cannot slice type ${type.kind}`); +} +/** + * Get the offset where actual data starts for sliceable types. + * For dynamic bytes/strings in memory, data starts after the 32-byte length field. + * For fixed-size bytes and arrays, data starts immediately. + */ +export function getSliceDataOffset(type: Ir.Type): bigint { + // With the new type system, dynamic data (refs) have a length field + if (type.kind === "ref") { + // Dynamic data in memory has a 32-byte length field before the data + return 32n; + } + if (type.kind === "scalar") { + // Fixed-size data has no length field + return 0n; + } + // This should never happen as we've covered all type kinds + // But TypeScript doesn't know that, so we need to handle it + throw new Error(`Cannot get data offset for unknown type`); +} diff --git a/packages/bugc/src/evmgen/generation/module.test.ts b/packages/bugc/src/evmgen/generation/module.test.ts new file mode 100644 index 00000000..76819209 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/module.test.ts @@ -0,0 +1,485 @@ +import { describe, it, expect } from "vitest"; + +import * as Ir from "#ir"; + +import { generate } from "./module.js"; + +describe("Module.generate", () => { + it("should generate runtime bytecode for module without constructor", () => { + const module: Ir.Module = { + name: "Test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }, + }; + + const memoryLayouts = { + main: { + allocations: {}, + nextStaticOffset: 0x80, + }, + functions: {}, + }; + + const blockLayouts = { + main: { + order: ["entry"], + offsets: new Map(), + }, + functions: {}, + }; + + const result = generate(module, memoryLayouts, blockLayouts); + + expect(result.runtime).toBeDefined(); + expect(result.create).toBeDefined(); // Always generates constructor + // Runtime now includes memory initialization (5 bytes) even for empty function + // PUSH1 0x80 (2) + PUSH1 0x40 (2) + MSTORE (1) = 5 bytes minimum + expect(result.runtime.length).toBeGreaterThanOrEqual(5); + expect(result.create!.length).toBeGreaterThan(0); // Constructor needs deployment code + }); + + it("should generate deployment bytecode with constructor", () => { + const module: Ir.Module = { + name: "Test", + functions: new Map(), + create: { + name: "create", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }, + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }, + }; + + const memoryLayouts = { + create: { + allocations: {}, + nextStaticOffset: 0x80, + }, + main: { + allocations: {}, + nextStaticOffset: 0x80, + }, + functions: {}, + }; + + const blockLayouts = { + create: { + order: ["entry"], + offsets: new Map(), + }, + main: { + order: ["entry"], + offsets: new Map(), + }, + functions: {}, + }; + + const result = generate(module, memoryLayouts, blockLayouts); + + expect(result.runtime).toBeDefined(); + expect(result.create).toBeDefined(); + + // Deployment bytecode should be longer (includes constructor + runtime) + expect(result.create!.length).toBeGreaterThan(result.runtime.length); + + // Should have CODECOPY and RETURN instructions for deployment + expect( + result.createInstructions?.some((inst) => inst.mnemonic === "CODECOPY"), + ).toBe(true); + expect( + result.createInstructions?.some((inst) => inst.mnemonic === "RETURN"), + ).toBe(true); + }); + + describe("Bytecode Optimization", () => { + it("should use optimal PUSH opcodes based on value size", () => { + const module: Ir.Module = { + name: "Test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value: 42n, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + }; + + const result = generate( + module, + { + main: { + allocations: { "%1": { offset: 0x80, size: 32 } }, + nextStaticOffset: 0xa0, + }, + functions: {}, + }, + { + main: { order: ["entry"], offsets: new Map() }, + functions: {}, + }, + ); + + const deployment = result.create!; + + // For small runtime (< 256 bytes), should use PUSH1 not PUSH2 + // Find the PUSH opcodes that push runtime length + const runtimeLength = result.runtime.length; + expect(runtimeLength).toBeLessThan(256); + + // Count PUSH1 vs PUSH2 opcodes + let push1Count = 0; + let push2Count = 0; + + for (let i = 0; i < deployment.length; i++) { + if (deployment[i] === 0x60) { + // PUSH1 + const value = deployment[i + 1]; + if (value === runtimeLength) push1Count++; + } else if (deployment[i] === 0x61) { + // PUSH2 + const value = (deployment[i + 1] << 8) | deployment[i + 2]; + if (value === runtimeLength) push2Count++; + } + } + + // Should use PUSH1 for small values + expect(push1Count).toBeGreaterThan(0); + expect(push2Count).toBe(0); + }); + + it("should use PUSH3 for offsets larger than 65535", () => { + // Create a module that will have deployment bytecode > 65535 + // We can simulate this by having a large runtime bytecode + // Instead of 20000 instructions, we'll create a mock scenario + const instructions = Array.from({ length: 1000 }, (_, i) => ({ + kind: "const" as const, + value: BigInt(0xffffff), // Large constant to generate more bytes + type: Ir.Type.Scalar.uint256, + dest: `%${i}`, + operationDebug: {}, + })); + + const module: Ir.Module = { + name: "LargeContract", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions, + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + }; + + const allocations = new Map( + instructions.map((inst, i) => [inst.dest, i * 32]), + ); + + const result = generate( + module, + { + main: { + allocations: Object.fromEntries( + Array.from(allocations.entries()).map(([k, v]) => [ + k, + { offset: v, size: 32 }, + ]), + ), + nextStaticOffset: 32000, + }, + functions: {}, + }, + { + main: { order: ["entry"], offsets: new Map() }, + functions: {}, + }, + ); + + // The test just verifies we can handle memory-allocated values + // without stack overflow (which we fixed) + expect(result.runtime.length).toBeGreaterThan(0); + expect(result.create).toBeDefined(); + + // Check that deployment bytecode uses appropriate PUSH opcodes + const deployment = result.create!; + + // Find what PUSH opcodes are used for the runtime offset + let hasOptimalPush = false; + const runtimeOffset = deployment.length - result.runtime.length; + + for (let i = 0; i < deployment.length - 3; i++) { + if (deployment[i] >= 0x60 && deployment[i] <= 0x7f) { + // PUSH1-PUSH32 + const pushSize = deployment[i] - 0x5f; + if (pushSize > 0 && i + pushSize < deployment.length) { + let value = 0; + for (let j = 1; j <= pushSize; j++) { + value = (value << 8) | deployment[i + j]; + } + if (value === runtimeOffset) { + // Check if this is the optimal size + const optimalSize = + runtimeOffset === 0 + ? 1 + : runtimeOffset < 256 + ? 1 + : runtimeOffset < 65536 + ? 2 + : 3; + hasOptimalPush = pushSize === optimalSize; + break; + } + } + } + } + + expect(hasOptimalPush).toBe(true); + }); + + it("should calculate deployment size correctly with optimal PUSH opcodes", () => { + const module: Ir.Module = { + name: "Test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + }; + + const result = generate( + module, + { + main: { allocations: {}, nextStaticOffset: 0x80 }, + functions: {}, + }, + { + main: { order: ["entry"], offsets: new Map() }, + functions: {}, + }, + ); + + const deployment = result.create!; + const runtime = result.runtime; + + // Verify runtime is embedded at the end of deployment + const deploymentEnd = + runtime.length > 0 ? deployment.slice(-runtime.length) : []; + expect(Array.from(deploymentEnd)).toEqual(Array.from(runtime)); + + // Verify CODECOPY copies from correct offset + const expectedOffset = deployment.length - runtime.length; + + // Find CODECOPY and verify the offset pushed before it + const codecopyIndex = deployment.indexOf(0x39); + + // Check what value is pushed before CODECOPY (after the length push) + // It should be the offset where runtime starts + let actualOffset = -1; + + // Look backwards from CODECOPY for PUSH opcodes + for (let i = codecopyIndex - 1; i >= 0; i--) { + if (deployment[i] >= 0x60 && deployment[i] <= 0x7f) { + // PUSH1-PUSH32 + const pushSize = deployment[i] - 0x5f; + if (i + pushSize < codecopyIndex) { + // This could be our offset push + actualOffset = 0; + for (let j = 1; j <= pushSize; j++) { + actualOffset = (actualOffset << 8) | deployment[i + j]; + } + // Verify this is the runtime offset + if (actualOffset === expectedOffset) { + break; + } + } + } + } + + expect(actualOffset).toBe(expectedOffset); + }); + + it("should not truncate values when generating PUSH instructions", () => { + // Test that we correctly handle all value sizes + const testCases = [ + { value: 0n, expectedPushSize: 1 }, // PUSH0 or PUSH1 0x00 + { value: 255n, expectedPushSize: 1 }, // PUSH1 + { value: 256n, expectedPushSize: 2 }, // PUSH2 + { value: 65535n, expectedPushSize: 2 }, // PUSH2 + { value: 65536n, expectedPushSize: 3 }, // PUSH3 + ]; + + for (const { value, expectedPushSize } of testCases) { + const module: Ir.Module = { + name: "Test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [ + { + kind: "const", + value, + type: Ir.Type.Scalar.uint256, + dest: "%1", + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + }; + + const result = generate( + module, + { + main: { + allocations: { "%1": { offset: 0x80, size: 32 } }, + nextStaticOffset: 0xa0, + }, + functions: {}, + }, + { + main: { order: ["entry"], offsets: new Map() }, + functions: {}, + }, + ); + + // Find the PUSH instruction for our constant + const runtime = result.runtime; + + // Look for PUSH instructions (no JUMPDEST for entry with no predecessors) + let found = false; + for (let i = 0; i < runtime.length; i++) { + if (runtime[i] >= 0x5f && runtime[i] <= 0x7f) { + // PUSH0-PUSH32 + const pushOpcode = runtime[i]; + const pushSize = pushOpcode === 0x5f ? 0 : pushOpcode - 0x5f; + + if (pushSize > 0) { + // Read the value + let pushedValue = 0n; + for (let j = 1; j <= pushSize; j++) { + pushedValue = (pushedValue << 8n) | BigInt(runtime[i + j]); + } + + if (pushedValue === value) { + found = true; + expect(pushSize).toBe(expectedPushSize); + break; + } + } else if (value === 0n) { + found = true; + expect(pushOpcode).toBe(0x5f); // PUSH0 + break; + } + } + } + + expect(found).toBe(true); + } + }); + }); +}); diff --git a/packages/bugc/src/evmgen/generation/module.ts b/packages/bugc/src/evmgen/generation/module.ts new file mode 100644 index 00000000..ba24e717 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/module.ts @@ -0,0 +1,232 @@ +/** + * EVM Bytecode Generator with strongly-typed state management + */ + +import type * as Ir from "#ir"; +import type * as Evm from "#evm"; + +import type { State } from "#evmgen/state"; +import { pipe, operations } from "#evmgen/operations"; +import { Memory, Layout } from "#evmgen/analysis"; +import { serialize, calculateSize } from "#evmgen/serialize"; +import type { Error } from "#evmgen/errors"; + +import * as Function from "./function.js"; + +/** + * Generate bytecode for entire module + */ +export function generate( + module: Ir.Module, + memory: Memory.Module.Info, + blocks: Layout.Module.Info, +): { + create?: number[]; + runtime: number[]; + createInstructions?: Evm.Instruction[]; + runtimeInstructions: Evm.Instruction[]; + warnings: Error[]; +} { + // Generate runtime main function + const runtimeResult = Function.generate( + module.main, + memory.main, + blocks.main, + ); + + // Collect all warnings + let allWarnings: Error[] = [...runtimeResult.warnings]; + + // Generate user-defined functions and build function registry + const functionResults: Array<{ + name: string; + bytecode: number[]; + instructions: Evm.Instruction[]; + patches: typeof runtimeResult.patches; + }> = []; + + for (const [name, func] of module.functions.entries()) { + const funcMemory = memory.functions?.[name]; + const funcLayout = blocks.functions?.[name]; + + if (funcMemory && funcLayout) { + const funcResult = Function.generate(func, funcMemory, funcLayout, { + isUserFunction: true, + }); + functionResults.push({ + name, + bytecode: funcResult.bytecode, + instructions: funcResult.instructions, + patches: funcResult.patches, + }); + allWarnings = [...allWarnings, ...funcResult.warnings]; + } + } + + // Build function registry with offsets + const functionRegistry: Record = {}; + let currentOffset = runtimeResult.bytecode.length; + for (const funcResult of functionResults) { + functionRegistry[funcResult.name] = currentOffset; + currentOffset += funcResult.bytecode.length; + } + + // Patch function calls in runtime bytecode + const patchedRuntime = Function.patchFunctionCalls( + runtimeResult.bytecode, + runtimeResult.instructions, + runtimeResult.patches, + functionRegistry, + ); + + // Patch function calls in user-defined functions + const patchedFunctions = functionResults.map((funcResult) => + Function.patchFunctionCalls( + funcResult.bytecode, + funcResult.instructions, + funcResult.patches, + functionRegistry, + ), + ); + + // Combine runtime with user functions + const allRuntimeBytes = [ + ...patchedRuntime.bytecode, + ...patchedFunctions.flatMap((f) => f.bytecode), + ]; + const allRuntimeInstructions = [ + ...patchedRuntime.instructions, + ...patchedFunctions.flatMap((f) => f.instructions), + ]; + + // Generate constructor function if present + let createBytes: number[] = []; + let allCreateInstructions: Evm.Instruction[] = []; + if (module.create && memory.create && blocks.create) { + const createResult = Function.generate( + module.create, + memory.create, + blocks.create, + ); + createBytes = createResult.bytecode; + allCreateInstructions = [...createResult.instructions]; + allWarnings = [...allWarnings, ...createResult.warnings]; + } + + // Build complete deployment bytecode and get deployment wrapper instructions + const { deployBytes, deploymentWrapperInstructions } = + buildDeploymentInstructions(createBytes, allRuntimeBytes); + + // Combine constructor instructions with deployment wrapper + const finalCreateInstructions = + allCreateInstructions.length > 0 || deploymentWrapperInstructions.length > 0 + ? [...allCreateInstructions, ...deploymentWrapperInstructions] + : undefined; + + return { + create: deployBytes, + runtime: allRuntimeBytes, + createInstructions: finalCreateInstructions, + runtimeInstructions: allRuntimeInstructions, + warnings: allWarnings, + }; +} + +/** + * Calculate the size of deployment bytecode with proper PUSH sizing + */ +function calculateDeploymentSize( + createBytesLength: number, + runtimeBytesLength: number, +): number { + // Initial state just for calculating push sizes + const state: State<[]> = { + brands: [], + stack: [], + instructions: [], + memory: { allocations: {}, nextStaticOffset: 0x80 }, + nextId: 0, + patches: [], + blockOffsets: {}, + warnings: [], + functionRegistry: {}, + callStackPointer: 0x60, + }; + + let deploymentPrefixSize = 0; + let lastSize = -1; + + // Iterate until size stabilizes + while (deploymentPrefixSize !== lastSize) { + lastSize = deploymentPrefixSize; + + // Calculate size based on current estimate + const result = deploymentTransition( + BigInt(createBytesLength + deploymentPrefixSize), + BigInt(runtimeBytesLength), + )(state); + + deploymentPrefixSize = calculateSize(result.instructions); + } + + return createBytesLength + deploymentPrefixSize; +} + +/** + * Build deployment bytecode and instructions (constructor + runtime deployment wrapper) + */ +function buildDeploymentInstructions( + createBytes: number[], + runtimeBytes: number[], +): { deployBytes: number[]; deploymentWrapperInstructions: Evm.Instruction[] } { + const state: State<[]> = { + brands: [], + stack: [], + instructions: [], + memory: { allocations: {}, nextStaticOffset: 0x80 }, + nextId: 0, + patches: [], + blockOffsets: {}, + warnings: [], + functionRegistry: {}, + callStackPointer: 0x60, + }; + + const deploymentSize = calculateDeploymentSize( + createBytes.length, + runtimeBytes.length, + ); + const runtimeOffset = BigInt(deploymentSize); + const runtimeLength = BigInt(runtimeBytes.length); + + // Build deployment wrapper + const result = deploymentTransition(runtimeOffset, runtimeLength)(state); + + const deploymentWrapperBytes = serialize(result.instructions); + + // Combine everything + const deployBytes = [ + ...createBytes, + ...deploymentWrapperBytes, + ...runtimeBytes, + ]; + + return { + deployBytes, + deploymentWrapperInstructions: result.instructions, + }; +} + +function deploymentTransition(runtimeOffset: bigint, runtimeLength: bigint) { + const { PUSHn, CODECOPY, RETURN } = operations; + + return pipe() + .then(PUSHn(runtimeLength), { as: "size" }) + .then(PUSHn(runtimeOffset), { as: "offset" }) + .then(PUSHn(0n), { as: "destOffset" }) + .then(CODECOPY()) + .then(PUSHn(runtimeLength), { as: "size" }) + .then(PUSHn(0n), { as: "offset" }) + .then(RETURN()) + .done(); +} diff --git a/packages/bugc/src/evmgen/generation/values/identify.ts b/packages/bugc/src/evmgen/generation/values/identify.ts new file mode 100644 index 00000000..2b2d641d --- /dev/null +++ b/packages/bugc/src/evmgen/generation/values/identify.ts @@ -0,0 +1,40 @@ +import type * as Ir from "#ir"; +import type { Stack } from "#evm"; +import type { State } from "#evmgen/state"; + +/** + * Get the ID for a value + */ +export function valueId(val: Ir.Value): string { + if (val.kind === "const") { + // Constants don't have stable IDs, we'll handle them specially + return `$const_${val.value}`; + } else if (val.kind === "temp") { + return val.id; + } else { + // @ts-expect-error should be exhausted + throw new Error(`Unknown value kind: ${val.kind}`); + } +} + +/** + * Annotate the top stack item with an IR value + */ +export const annotateTop = + (irValue: string) => + (state: State): State => { + if (state.stack.length === 0) { + throw new Error("Cannot annotate empty stack"); + } + + const newStack = [...state.stack]; + newStack[0] = { + ...newStack[0], + irValue, + }; + + return { + ...state, + stack: newStack, + }; + }; diff --git a/packages/bugc/src/evmgen/generation/values/index.ts b/packages/bugc/src/evmgen/generation/values/index.ts new file mode 100644 index 00000000..c99b21c7 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/values/index.ts @@ -0,0 +1,3 @@ +export { valueId, annotateTop } from "./identify.js"; +export { loadValue } from "./load.js"; +export { storeValueIfNeeded } from "./store.js"; diff --git a/packages/bugc/src/evmgen/generation/values/load.ts b/packages/bugc/src/evmgen/generation/values/load.ts new file mode 100644 index 00000000..ecfe7d88 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/values/load.ts @@ -0,0 +1,47 @@ +import type * as Ir from "#ir"; +import * as Evm from "#evm"; +import type { Stack } from "#evm"; +import { type Transition, operations, pipe } from "#evmgen/operations"; + +import { valueId, annotateTop } from "./identify.js"; + +/** + * Load a value onto the stack + */ +export const loadValue = ( + value: Ir.Value, + options?: Evm.InstructionOptions, +): Transition => { + const { PUSHn, DUPn, MLOAD } = operations; + + const id = valueId(value); + + if (value.kind === "const") { + return pipe() + .then(PUSHn(BigInt(value.value), options)) + .then(annotateTop(id)) + .done(); + } + + return pipe() + .peek((state, builder) => { + // Check if value is on stack + // Note addition because DUP uses 1-based indexing + const stackPos = + state.stack.findIndex(({ irValue }) => irValue === id) + 1; + if (stackPos > 0 && stackPos <= 16) { + return builder.then(DUPn(stackPos, options), { as: "value" }); + } + // Check if in memory + if (id in state.memory.allocations) { + const offset = state.memory.allocations[id].offset; + return builder + .then(PUSHn(BigInt(offset), options), { as: "offset" }) + .then(MLOAD(options)) + .then(annotateTop(id)); + } + + throw new Error(`Cannot load value ${id} - not in stack or memory`); + }) + .done(); +}; diff --git a/packages/bugc/src/evmgen/generation/values/store.ts b/packages/bugc/src/evmgen/generation/values/store.ts new file mode 100644 index 00000000..eb813a34 --- /dev/null +++ b/packages/bugc/src/evmgen/generation/values/store.ts @@ -0,0 +1,33 @@ +import * as Evm from "#evm"; +import type { Stack } from "#evm"; +import { type Transition, operations, pipe } from "#evmgen/operations"; + +import { annotateTop } from "./identify.js"; + +/** + * Store a value to memory if it needs to be persisted + */ +export const storeValueIfNeeded = ( + destId: string, + options?: Evm.InstructionOptions, +): Transition => { + const { PUSHn, DUP2, SWAP1, MSTORE } = operations; + + return ( + pipe() + // First annotate the top value with the destination ID + .then(annotateTop(destId)) + .peek((state, builder) => { + const allocation = state.memory.allocations[destId]; + if (allocation === undefined) { + return builder; + } + return builder + .then(PUSHn(BigInt(allocation.offset), options), { as: "offset" }) + .then(DUP2(options)) + .then(SWAP1(options)) + .then(MSTORE(options)); + }) + .done() + ); +}; diff --git a/packages/bugc/src/evmgen/index.ts b/packages/bugc/src/evmgen/index.ts new file mode 100644 index 00000000..eb4a78cf --- /dev/null +++ b/packages/bugc/src/evmgen/index.ts @@ -0,0 +1,17 @@ +/** + * EVM Code Generation Module + * + * A self-contained EVM backend that transforms IR to EVM bytecode with + * careful stack and memory management. Includes analysis, generation, + * and operation utilities. + */ + +// Main generation entry point + +import { Module } from "#evmgen/generation"; +export const generateModule = Module.generate; + +// Error handling +export { Error, ErrorCode } from "./errors.js"; + +export * as Analysis from "#evmgen/analysis"; diff --git a/packages/bugc/src/evmgen/integration.test.ts b/packages/bugc/src/evmgen/integration.test.ts new file mode 100644 index 00000000..0542812f --- /dev/null +++ b/packages/bugc/src/evmgen/integration.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from "vitest"; +import { bytecodeSequence, buildSequence } from "#compiler"; + +describe("EVM Generation Integration", () => { + it("should compile minimal.bug to bytecode", async () => { + const source = `name Minimal; + +storage { + [0] value: uint256; +} + +code { + value = 1; +}`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.value.bytecode).toBeDefined(); + expect(result.value.bytecode.runtime).toBeInstanceOf(Uint8Array); + expect(result.value.bytecode.runtime.length).toBeGreaterThan(0); + + // Should have constructor bytecode (even if no create block) + expect(result.value.bytecode.create).toBeInstanceOf(Uint8Array); + expect(result.value.bytecode.create!.length).toBeGreaterThan( + result.value.bytecode.runtime.length, + ); + } + }); + + it("should compile counter with arithmetic", async () => { + const source = `name Counter; + +storage { + [0] count: uint256; +} + +code { + count = count + 1; +}`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + expect(result.success).toBe(true); + if (result.success) { + const { runtime } = result.value.bytecode; + + // Check for expected opcodes + const bytecode = Array.from(runtime); + + // Should contain SLOAD (0x54) + expect(bytecode).toContain(0x54); + + // Should contain ADD (0x01) + expect(bytecode).toContain(0x01); + + // Should contain SSTORE (0x55) + expect(bytecode).toContain(0x55); + } + }); + + it("should compile with constructor", async () => { + const source = `name WithConstructor; + +storage { + [0] owner: address; +} + +create { + owner = msg.sender; +} + +code { + if (msg.sender == owner) { + owner = 0x0000000000000000000000000000000000000000; + } +}`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + expect(result.success).toBe(true); + if (result.success) { + const { runtime, create } = result.value.bytecode; + + expect(runtime).toBeInstanceOf(Uint8Array); + expect(create).toBeInstanceOf(Uint8Array); + + // Constructor should be larger (includes runtime + deployment logic) + expect(create!.length).toBeGreaterThan(runtime.length); + + // Should have CODECOPY in constructor + const createBytes = Array.from(create!); + expect(createBytes).toContain(0x39); // CODECOPY + expect(createBytes).toContain(0xf3); // RETURN + } + }); +}); diff --git a/packages/bugc/src/evmgen/operations.ts b/packages/bugc/src/evmgen/operations.ts new file mode 100644 index 00000000..bb6132f1 --- /dev/null +++ b/packages/bugc/src/evmgen/operations.ts @@ -0,0 +1,102 @@ +import * as Evm from "#evm"; +import type { _ } from "#evm"; + +import { type State, type StackItem, controls } from "#evmgen/state"; + +type UnsafeState = State<_ & Evm.Stack>; +type UnsafeItem = StackItem & { brand: _ & Evm.Stack.Brand }; + +const rebrands: ReturnType> = + Evm.makeRebrands(controls); +export const rebrand: typeof rebrands.rebrand = rebrands.rebrand; +export const rebrandTop: typeof rebrands.rebrandTop = rebrands.rebrandTop; + +export type Transition< + X extends Evm.Stack, + Y extends Evm.Stack, +> = Evm.Transition; + +export const pipe = Evm.makePipe(controls); + +export type RawOperations = Evm.Operations; + +export const rawOperations: RawOperations = Evm.makeOperations< + UnsafeState, + UnsafeItem +>(controls); + +export type Operations = typeof rawOperations & { + DUPn: ( + position: number, + options?: Evm.InstructionOptions, + ) => Transition; + PUSHn: ( + value: bigint, + options?: Evm.InstructionOptions, + ) => Transition; +}; + +export const operations: Operations = { + ...rawOperations, + + DUPn: ( + position: number, + options?: Evm.InstructionOptions, + ): Transition => { + if (position < 1 || position > 16) { + throw new Error(`Cannot reach stack position ${position}`); + } + + type DUPn = { + [O in keyof RawOperations]: O extends `DUP${infer _N}` ? O : never; + }[keyof RawOperations]; + + const DUP = rawOperations[`DUP${position}` as DUPn] as unknown as ( + options?: Evm.InstructionOptions, + ) => Transition; + + return pipe() + .peek((state, builder) => { + // Check if stack has enough elements + if (state.stack.length < position) { + throw new Error("Stack too short"); + } + + return builder; + }) + .then(DUP(options), { as: "value" }) + .done(); + }, + + PUSHn: ( + value: bigint, + options?: Evm.InstructionOptions, + ): Transition => { + if (value === 0n) { + return rawOperations.PUSH0(options); + } + + const immediates = bigintToBytes(value); + + type PUSHn = { + [O in keyof RawOperations]: O extends `PUSH${infer _N}` ? O : never; + }[keyof RawOperations]; + const PUSH = rawOperations[`PUSH${immediates.length}` as PUSHn]; + + return PUSH(immediates, options); + }, +}; + +function bigintToBytes(value: bigint): number[] { + if (value === 0n) return []; + + const hex = value.toString(16); + const padded = hex.length % 2 ? "0" + hex : hex; + const bytes: number[] = []; + + for (let i = 0; i < padded.length; i += 2) { + bytes.push(parseInt(padded.substr(i, 2), 16)); + } + + return bytes; +} diff --git a/packages/bugc/src/evmgen/pass.ts b/packages/bugc/src/evmgen/pass.ts new file mode 100644 index 00000000..9b6039e5 --- /dev/null +++ b/packages/bugc/src/evmgen/pass.ts @@ -0,0 +1,118 @@ +import * as Format from "@ethdebug/format"; +import { Result } from "#result"; +import type { Pass } from "#compiler"; +import type * as Ir from "#ir"; +import type * as Evm from "#evm"; + +import { Module } from "#evmgen/generation"; +import { Error as EvmgenError, ErrorCode } from "#evmgen/errors"; +import { buildProgram } from "#evmgen/program-builder"; + +import { Layout, Liveness, Memory } from "#evmgen/analysis"; + +/** + * Output produced by the EVM generation pass + */ +export interface EvmGenerationOutput { + /** Runtime bytecode */ + runtime: Uint8Array; + /** Constructor bytecode (optional) */ + create?: Uint8Array; + /** Runtime instructions */ + runtimeInstructions: Evm.Instruction[]; + /** Constructor instructions (optional) */ + createInstructions?: Evm.Instruction[]; + /** Runtime program with debug info (ethdebug/format) */ + runtimeProgram: Format.Program; + /** Create program with debug info (ethdebug/format, optional) */ + createProgram?: Format.Program; +} + +/** + * EVM code generation pass + */ +const pass: Pass<{ + needs: { + ir: Ir.Module; + }; + adds: { + bytecode: EvmGenerationOutput; + }; + error: EvmgenError; +}> = { + async run({ ir }) { + try { + // Analyze liveness + const liveness = Liveness.Module.analyze(ir); + + // Analyze memory requirements + const memoryResult = Memory.Module.plan(ir, liveness); + if (!memoryResult.success) { + return Result.err( + new EvmgenError( + ErrorCode.INTERNAL_ERROR, + memoryResult.messages.error?.[0]?.message ?? + "Memory analysis failed", + ), + ); + } + + // Analyze block layout + const blockResult = Layout.Module.perform(ir); + if (!blockResult.success) { + return Result.err( + new EvmgenError( + ErrorCode.INTERNAL_ERROR, + blockResult.messages.error?.[0]?.message ?? + "Block layout analysis failed", + ), + ); + } + + // Generate bytecode + const result = Module.generate(ir, memoryResult.value, blockResult.value); + + // Convert to Uint8Array + const runtime = new Uint8Array(result.runtime); + const create = result.create ? new Uint8Array(result.create) : undefined; + + // Build Format.Program objects + const runtimeProgram = buildProgram( + result.runtimeInstructions, + "call", + ir, + ); + const createProgram = result.createInstructions + ? buildProgram(result.createInstructions, "create", ir) + : undefined; + + return Result.okWith( + { + bytecode: { + runtime, + create, + runtimeInstructions: result.runtimeInstructions, + createInstructions: result.createInstructions, + runtimeProgram, + createProgram, + }, + }, + result.warnings, + ); + } catch (error) { + if (error instanceof EvmgenError) { + return Result.err(error); + } + + // Wrap unexpected errors + return Result.err( + new EvmgenError( + ErrorCode.INTERNAL_ERROR, + error instanceof Error ? error.message : String(error), + ), + ); + } + }, +}; + +export default pass; diff --git a/packages/bugc/src/evmgen/program-builder.ts b/packages/bugc/src/evmgen/program-builder.ts new file mode 100644 index 00000000..ea42254b --- /dev/null +++ b/packages/bugc/src/evmgen/program-builder.ts @@ -0,0 +1,96 @@ +/** + * Build Format.Program objects from EVM generation output + */ + +import type * as Format from "@ethdebug/format"; +import type * as Evm from "#evm"; +import type * as Ir from "#ir"; + +/** + * Convert Evm.Instruction to Format.Program.Instruction + */ +function toFormatInstruction( + instr: Evm.Instruction, + offset: number, +): Format.Program.Instruction { + const result: Format.Program.Instruction = { + offset, + }; + + // Add operation info + if (instr.mnemonic) { + const operation: Format.Program.Instruction.Operation = { + mnemonic: instr.mnemonic, + }; + + // Add immediates as arguments if present + if (instr.immediates && instr.immediates.length > 0) { + // Convert immediates to hex string + const hex = + "0x" + + instr.immediates.map((b) => b.toString(16).padStart(2, "0")).join(""); + operation.arguments = [hex]; + } + + result.operation = operation; + } + + // Add debug context if present + if (instr.debug?.context) { + result.context = instr.debug.context; + } + + return result; +} + +/** + * Compute byte offsets for each instruction + */ +function computeOffsets(instructions: Evm.Instruction[]): number[] { + const offsets: number[] = []; + let offset = 0; + + for (const instr of instructions) { + offsets.push(offset); + offset += 1 + (instr.immediates?.length ?? 0); + } + + return offsets; +} + +/** + * Build a Format.Program from EVM instructions and IR module info + */ +export function buildProgram( + instructions: Evm.Instruction[], + environment: "call" | "create", + ir: Ir.Module, +): Format.Program { + const offsets = computeOffsets(instructions); + + const formatInstructions = instructions.map((instr, i) => + toFormatInstruction(instr, offsets[i]), + ); + + // Build contract info from IR module + const contract: Format.Program.Contract = { + name: ir.name, + definition: { + source: { id: "0" }, // TODO: Get actual source ID + range: ir.loc ?? { offset: 0, length: 0 }, + }, + }; + + const program: Format.Program = { + contract, + environment, + instructions: formatInstructions, + }; + + // Add program-level context if available + if (ir.debugContext) { + program.context = ir.debugContext; + } + + return program; +} diff --git a/packages/bugc/src/evmgen/serialize.ts b/packages/bugc/src/evmgen/serialize.ts new file mode 100644 index 00000000..3541b950 --- /dev/null +++ b/packages/bugc/src/evmgen/serialize.ts @@ -0,0 +1,44 @@ +/** + * Serialization module for converting Instructions to raw EVM bytecode + */ + +import type * as Evm from "#evm"; + +/** + * Convert an array of Instructions to raw bytecode bytes + */ +export function serialize(instructions: Evm.Instruction[]): number[] { + const bytes: number[] = []; + + for (const instruction of instructions) { + // Add the opcode + bytes.push(instruction.opcode); + + // Add any immediates + if (instruction.immediates) { + bytes.push(...instruction.immediates); + } + } + + return bytes; +} + +/** + * Calculate the size in bytes that an instruction will occupy + */ +export function instructionSize(instruction: Evm.Instruction): number { + let size = 1; // opcode + + if (instruction.immediates) { + size += instruction.immediates.length; + } + + return size; +} + +/** + * Calculate total size of multiple instructions + */ +export function calculateSize(instructions: Evm.Instruction[]): number { + return instructions.reduce((acc, inst) => acc + instructionSize(inst), 0); +} diff --git a/packages/bugc/src/evmgen/state.ts b/packages/bugc/src/evmgen/state.ts new file mode 100644 index 00000000..f4473ea8 --- /dev/null +++ b/packages/bugc/src/evmgen/state.ts @@ -0,0 +1,101 @@ +import * as Format from "@ethdebug/format"; + +import * as Evm from "#evm"; +import { type _ } from "#evm"; + +import * as Analysis from "#evmgen/analysis"; +import type { Error } from "#evmgen/errors"; + +// Debug context type for EVM instructions +export type DebugContext = { + context?: Format.Program.Context; +}; + +// Track stack at type level +export interface State { + brands: S; + stack: StackItem[]; + nextId: number; // For generating unique IDs + instructions: Evm.Instruction[]; + memory: Analysis.Memory.Function.Info; + blockOffsets: Record; + patches: Patch[]; + warnings: Error[]; + functionRegistry: Record; // Function name -> bytecode offset + callStackPointer: number; // Memory location for call stack (0x60) +} + +export type Patch = + | { + type?: "block"; // Default type for backward compatibility + index: number; + target: string; + } + | { + type: "function"; + index: number; + target: string; // Function name + } + | { + type: "continuation"; + index: number; + target: string; // Block name + }; + +export interface StackItem { + id: string; + irValue?: string; // Optional IR value ID (e.g., "t1", "t2") +} + +type UnsafeState = State<_ & Evm.Stack>; +type UnsafeItem = StackItem & { brand: _ & Evm.Stack.Brand }; + +const unsafe: Evm.Unsafe.StateControls = { + slice: (state, ...args) => ({ + ...state, + stack: state.stack.slice(...args), + brands: state.brands.slice(...args), + }), + prepend: (state, item) => ({ + ...state, + stack: [{ id: item.id }, ...state.stack], + brands: [item.brand, ...state.brands], + }), + create: (id, brand) => ({ + id, + brand, + }), + duplicate: (item, id) => ({ + ...item, + id, + }), + rebrand: (item, brand) => ({ + ...item, + brand, + }), + readTop: (state, num) => { + // Return the top N stack items with their IDs and brands + const items = []; + for (let i = 0; i < num && i < state.stack.length; i++) { + items.push({ + ...state.stack[i], // Preserves id and irValue + brand: state.brands[i], + }); + } + return items; + }, + generateId: (state, prefix = "id") => ({ + id: `${prefix}_${state.nextId}`, + state: { + ...state, + nextId: state.nextId + 1, + }, + }), + emit: (state, instruction) => ({ + ...state, + instructions: [...state.instructions, instruction], + }), +}; + +export const controls: Evm.State.Controls = + Evm.State.makeControls(unsafe); diff --git a/packages/bugc/src/evmgen/storage-verification.test.ts b/packages/bugc/src/evmgen/storage-verification.test.ts new file mode 100644 index 00000000..304e90f9 --- /dev/null +++ b/packages/bugc/src/evmgen/storage-verification.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect } from "vitest"; +import { compile } from "#compiler"; + +describe.skip("Storage verification (requires local Ethereum node)", () => { + it("should store array values correctly in constructor", async () => { + const source = ` + name ConstructorArray; + + storage { + [0] items: array; + } + + create { + items[0] = 1005; + items[1] = 1006; + items[2] = 1007; + } + + code {} + `; + + const result = await compile({ to: "bytecode", source }); + expect(result.success).toBe(true); + + if (!result.success) return; + + const { create: creationBytecode } = result.value.bytecode; + expect(creationBytecode).toBeDefined(); + + // Deploy and check storage + const deployResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_sendTransaction", + params: [ + { + from: "0xa0e20c11276b8ab803a7034d0945797afb4d060b", + data: "0x" + Buffer.from(creationBytecode!).toString("hex"), + gas: "0x100000", + }, + ], + id: 1, + }), + }); + + const deployResult = await deployResponse.json(); + const txHash = deployResult.result; + + // Get receipt to find contract address + const receiptResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getTransactionReceipt", + params: [txHash], + id: 2, + }), + }); + + const receipt = await receiptResponse.json(); + const contractAddress = receipt.result.contractAddress; + + // Check storage slots + const expectedValues = [ + { slot: "0x0", value: 1005 }, + { slot: "0x1", value: 1006 }, + { slot: "0x2", value: 1007 }, + ]; + + for (const { slot, value } of expectedValues) { + const storageResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getStorageAt", + params: [contractAddress, slot, "latest"], + id: 3, + }), + }); + + const storageResult = await storageResponse.json(); + const storedValue = BigInt(storageResult.result); + + expect(storedValue).toBe(BigInt(value)); + } + }); + + it("should store direct storage values correctly", async () => { + const source = ` + name DirectStorage; + + storage { + [0] a: uint256; + [1] b: uint256; + [2] c: uint256; + } + + create { + a = 100; + b = 200; + c = 300; + } + + code {} + `; + + const result = await compile({ to: "bytecode", source }); + expect(result.success).toBe(true); + + if (!result.success) return; + + const { create: creationBytecode } = result.value.bytecode; + + // Deploy + const deployResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_sendTransaction", + params: [ + { + from: "0xa0e20c11276b8ab803a7034d0945797afb4d060b", + data: "0x" + Buffer.from(creationBytecode!).toString("hex"), + gas: "0x100000", + }, + ], + id: 1, + }), + }); + + const deployResult = await deployResponse.json(); + const txHash = deployResult.result; + + // Get receipt + const receiptResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getTransactionReceipt", + params: [txHash], + id: 2, + }), + }); + + const receipt = await receiptResponse.json(); + const contractAddress = receipt.result.contractAddress; + + // Check storage slots + const expectedValues = [ + { slot: "0x0", value: 100 }, + { slot: "0x1", value: 200 }, + { slot: "0x2", value: 300 }, + ]; + + for (const { slot, value } of expectedValues) { + const storageResponse = await fetch("http://localhost:8545", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getStorageAt", + params: [contractAddress, slot, "latest"], + id: 3, + }), + }); + + const storageResult = await storageResponse.json(); + const storedValue = BigInt(storageResult.result); + + expect(storedValue).toBe(BigInt(value)); + } + }); +}); diff --git a/packages/bugc/src/index.ts b/packages/bugc/src/index.ts new file mode 100644 index 00000000..31d58126 --- /dev/null +++ b/packages/bugc/src/index.ts @@ -0,0 +1,31 @@ +export const VERSION = "0.1.0"; + +export * as Ast from "#ast"; +export * as Ir from "#ir"; +export * as Evm from "#evm"; +export * as Parser from "#parser"; + +// Re-export type checker functionality +export * as TypeChecker from "#typechecker"; + +// Re-export type system +export { Type } from "#types"; + +// Re-export IR generation functionality +export { generateModule } from "#irgen"; + +// Re-export optimizer functionality +export { optimizeIr } from "#optimizer"; +export type { OptimizationLevel } from "#optimizer"; + +// Re-export error handling utilities +export * from "#errors"; + +// Re-export result type +export * from "#result"; + +// Re-export compiler interfaces +export { compile, type CompileOptions } from "#compiler"; + +// CLI utilities are not exported to avoid browser compatibility issues +// They should be imported directly from ./cli when needed in Node.js environments diff --git a/packages/bugc/src/ir/analysis/formatter.test.ts b/packages/bugc/src/ir/analysis/formatter.test.ts new file mode 100644 index 00000000..4c8a37cb --- /dev/null +++ b/packages/bugc/src/ir/analysis/formatter.test.ts @@ -0,0 +1,295 @@ +import { describe, it, expect } from "vitest"; +import * as Ir from "#ir"; +import { Formatter } from "./formatter.js"; + +describe("IrFormatter", () => { + it("should format phi nodes with predecessor labels", () => { + const module: Ir.Module = { + name: "TestModule", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + instructions: [], + terminator: { + kind: "branch", + condition: { + kind: "temp", + id: "t0", + type: Ir.Type.Scalar.bool, + }, + trueTarget: "then", + falseTarget: "else", + operationDebug: {}, + }, + predecessors: new Set(), + phis: [], + debug: {}, + }, + ], + [ + "then", + { + id: "then", + instructions: [ + { + kind: "const", + value: 20n, + type: Ir.Type.Scalar.uint256, + dest: "t1", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "merge", + operationDebug: {}, + }, + predecessors: new Set(["entry"]), + phis: [], + debug: {}, + }, + ], + [ + "else", + { + id: "else", + instructions: [ + { + kind: "const", + value: 30n, + type: Ir.Type.Scalar.uint256, + dest: "t2", + operationDebug: {}, + }, + ], + terminator: { + kind: "jump", + target: "merge", + operationDebug: {}, + }, + predecessors: new Set(["entry"]), + phis: [], + debug: {}, + }, + ], + [ + "merge", + { + id: "merge", + instructions: [], + terminator: { + kind: "return", + operationDebug: {}, + }, + predecessors: new Set(["then", "else"]), + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "then", + { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + ], + [ + "else", + { + kind: "temp", + id: "t2", + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "t3", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + debug: {}, + }, + ], + ]), + }, + }; + + const formatter = new Formatter(); + const formatted = formatter.format(module); + + // Check that phi node is formatted + expect(formatted).toContain("phi"); + + // Check that phi node shows predecessor blocks + expect(formatted).toContain("[then:"); + expect(formatted).toContain("[else:"); + + // Check that phi node shows the correct sources + expect(formatted).toContain("%t1"); + expect(formatted).toContain("%t2"); + + // Check that phi node shows the destination + expect(formatted).toContain("%t3"); + }); + + it("should format multiple phi nodes in a block", () => { + const module: Ir.Module = { + name: "TestModule", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "merge", + { + id: "merge", + instructions: [], + terminator: { + kind: "return", + operationDebug: {}, + }, + predecessors: new Set(["pred1", "pred2"]), + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "pred1", + { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + ], + [ + "pred2", + { + kind: "temp", + id: "t2", + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "t3", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + { + kind: "phi", + sources: new Map([ + [ + "pred1", + { kind: "const", value: true, type: Ir.Type.Scalar.bool }, + ], + [ + "pred2", + { + kind: "const", + value: false, + type: Ir.Type.Scalar.bool, + }, + ], + ]), + dest: "t4", + type: Ir.Type.Scalar.bool, + operationDebug: {}, + }, + ], + debug: {}, + }, + ], + ]), + }, + }; + + const formatter = new Formatter(); + const formatted = formatter.format(module); + + // Should format both phi nodes + const phiLines = formatted + .split("\n") + .filter((line) => line.includes("phi")); + expect(phiLines.length).toBe(2); + + // Check for both destinations + expect(formatted).toContain("%t3"); + expect(formatted).toContain("%t4"); + }); + + it("should show block predecessors when there are phi nodes", () => { + const module: Ir.Module = { + name: "TestModule", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "merge", + { + id: "merge", + instructions: [], + terminator: { + kind: "return", + operationDebug: {}, + }, + predecessors: new Set(["block1", "block2", "block3"]), + phis: [ + { + kind: "phi", + sources: new Map([ + [ + "block1", + { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + ], + [ + "block2", + { + kind: "temp", + id: "t2", + type: Ir.Type.Scalar.uint256, + }, + ], + [ + "block3", + { + kind: "temp", + id: "t3", + type: Ir.Type.Scalar.uint256, + }, + ], + ]), + dest: "t4", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + debug: {}, + }, + ], + ]), + }, + }; + + const formatter = new Formatter(); + const formatted = formatter.format(module); + + // Should show predecessors in the block header + expect(formatted).toMatch(/merge\s+preds=\[block1, block2, block3\]/); + }); +}); diff --git a/packages/bugc/src/ir/analysis/formatter.ts b/packages/bugc/src/ir/analysis/formatter.ts new file mode 100644 index 00000000..e6c4f3f1 --- /dev/null +++ b/packages/bugc/src/ir/analysis/formatter.ts @@ -0,0 +1,609 @@ +/** + * IR formatter for human-readable text output + */ + +import * as Format from "@ethdebug/format"; + +import * as Ir from "#ir/spec"; +import { Analysis as AstAnalysis } from "#ast"; + +export class Formatter { + private indent = 0; + private output: string[] = []; + private commentedValues: Set = new Set(); + private source?: string; + + format(module: Ir.Module, source?: string): string { + this.output = []; + this.indent = 0; + this.source = source; + + // Module declaration with name (no quotes) + this.line(`module ${module.name} {`); + this.indent++; + + // Format create function first if present + if (module.create) { + this.line("@create"); + this.formatFunction(module.create); + this.line(""); + } + + // Format main function next + this.line("@main"); + this.formatFunction(module.main); + + // Format user-defined functions last + if (module.functions && module.functions.size > 0) { + this.line(""); + for (const func of module.functions.values()) { + this.formatFunction(func); + this.line(""); + } + } + + this.indent--; + this.line("}"); + + return this.output.join("\n"); + } + + private formatFunction(func: Ir.Function): void { + // Format function signature with parameters + const params: string[] = []; + for (const param of func.parameters) { + params.push(`^${param.tempId}: ${this.formatType(param.type)}`); + } + this.line(`function ${func.name}(${params.join(", ")}) {`); + this.indent++; + + // Get blocks in topological order + const sortedBlocks = this.topologicalSort(func); + + // Format each block + for (const blockId of sortedBlocks) { + const block = func.blocks.get(blockId); + if (!block) continue; // Skip blocks that don't exist + this.formatBlock(blockId, block); + } + + this.indent--; + this.line("}"); + } + + private formatBlock(id: string, block: Ir.Block): void { + // Reset commented values for each block + this.commentedValues = new Set(); + + // Block header - only show predecessors for merge points (multiple preds) + // or if block has phi nodes (which indicates it's a merge point) + const showPreds = + block.predecessors?.size > 1 || (block.phis && block.phis.length > 0); + const predsStr = + showPreds && block.predecessors?.size > 0 + ? ` preds=[${Array.from(block.predecessors).sort().join(", ")}]` + : ""; + this.line(`${id}${predsStr}:`); + this.indent++; + + // Phi nodes + if (block.phis && block.phis.length > 0) { + for (const phi of block.phis) { + const phiSourceComment = this.formatSourceComment(phi.operationDebug); + if (phiSourceComment) { + // Handle multi-line comments for pick contexts + for (const line of phiSourceComment.split("\n")) { + this.line(line); + } + } + this.line(this.formatPhiInstruction(phi)); + } + } + + // Instructions + for (const inst of block.instructions) { + const sourceComment = this.formatSourceComment(inst.operationDebug); + if (sourceComment) { + // Handle multi-line comments for pick contexts + for (const line of sourceComment.split("\n")) { + this.line(line); + } + } + this.line(this.formatInstruction(inst)); + } + + // Terminator + const terminatorSourceComment = this.formatSourceComment( + block.terminator.operationDebug, + ); + if (terminatorSourceComment) { + // Handle multi-line comments for pick contexts + for (const line of terminatorSourceComment.split("\n")) { + this.line(line); + } + } + this.line(this.formatTerminator(block.terminator)); + + this.indent--; + this.line(""); + } + + private formatPhiInstruction(inst: Ir.Block.Phi): string { + const sources: string[] = []; + for (const [block, value] of inst.sources) { + sources.push(`[${block}: ${this.formatValue(value)}]`); + } + // Add appropriate prefix for destinations in phi nodes + const dest = inst.dest.startsWith("t") ? `%${inst.dest}` : `^${inst.dest}`; + const typeStr = inst.type ? `: ${this.formatType(inst.type)}` : ""; + return `${dest}${typeStr} = phi ${sources.join(", ")}`; + } + private formatInstruction(inst: Ir.Instruction): string { + // Helper to add type annotation to dest + const destWithType = (dest: string, type?: Ir.Type): string => { + // Add appropriate prefix for destinations + const formattedDest = dest.startsWith("t") ? `%${dest}` : `^${dest}`; + return type + ? `${formattedDest}: ${this.formatType(type)}` + : formattedDest; + }; + + switch (inst.kind) { + case "const": + return `${destWithType(inst.dest, inst.type)} = const ${this.formatConstValue(inst.value, inst.type)}`; + + case "allocate": + return `${destWithType(inst.dest, Ir.Type.Scalar.uint256)} = allocate.${inst.location}, size=${this.formatValue(inst.size)}`; + + case "binary": + return `${destWithType(inst.dest)} = ${inst.op} ${this.formatValue(inst.left)}, ${this.formatValue(inst.right)}`; + + case "unary": + return `${destWithType(inst.dest)} = ${inst.op} ${this.formatValue(inst.operand)}`; + + case "env": + return `${destWithType(inst.dest)} = env ${inst.op}`; + + case "hash": + return `${destWithType(inst.dest)} = hash ${this.formatValue(inst.value)}`; + + case "cast": + return `${destWithType(inst.dest, inst.targetType)} = cast ${this.formatValue(inst.value)} to ${this.formatType(inst.targetType)}`; + + case "compute_slot": { + const base = this.formatValue(inst.base); + let slotExpr: string; + + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + const key = this.formatValue(inst.key); + slotExpr = `slot[${base}].mapping[${key}]`; + } else if (Ir.Instruction.ComputeSlot.isArray(inst)) { + // Array compute_slot computes the first slot of the array + slotExpr = `slot[${base}].array`; + } else if (Ir.Instruction.ComputeSlot.isField(inst)) { + // Just show the offset, no field name + slotExpr = `slot[${base}].field[${inst.fieldOffset}]`; + } else { + // Shouldn't happen with proper typing + slotExpr = `slot[${base}]`; + } + + return `${destWithType(inst.dest, Ir.Type.Scalar.uint256)} = ${slotExpr}`; + } + + // Call instruction removed - calls are now block terminators + + case "length": + return `${destWithType(inst.dest)} = length ${this.formatValue(inst.object)}`; + + // NEW: unified read instruction + case "read": { + const location = inst.location; + + // Check if we're using defaults + const isDefaultOffset = + !inst.offset || + (inst.offset.kind === "const" && inst.offset.value === 0n); + const isDefaultLength = + !inst.length || + (inst.length.kind === "const" && inst.length.value === 32n); + + let locationStr: string; + if (location === "storage" || location === "transient") { + // For storage/transient, slot is required + const slot = inst.slot ? this.formatValue(inst.slot) : "0"; + + if (isDefaultOffset && isDefaultLength) { + // Only slot - compact syntax with * to indicate word-sized operation + locationStr = `${location}[${slot}*]`; + } else { + // Multiple fields - use named syntax + const parts: string[] = [`slot: ${slot}`]; + if (!isDefaultOffset && inst.offset) { + parts.push(`offset: ${this.formatValue(inst.offset)}`); + } + if (!isDefaultLength && inst.length) { + parts.push(`length: ${this.formatValue(inst.length)}`); + } + locationStr = `${location}[${parts.join(", ")}]`; + } + } else { + // For memory/calldata/returndata + if (inst.offset) { + const offset = this.formatValue(inst.offset); + if (isDefaultLength) { + // Only offset - compact syntax with * to indicate word-sized operation + locationStr = `${location}[${offset}*]`; + } else { + // Multiple fields - use named syntax + const length = inst.length ? this.formatValue(inst.length) : "32"; + locationStr = `${location}[offset: ${offset}, length: ${length}]`; + } + } else { + // No offset specified + locationStr = `${location}[]`; + } + } + + return `${destWithType(inst.dest, inst.type)} = ${locationStr}`; + } + + // NEW: unified write instruction + case "write": { + const location = inst.location; + const value = this.formatValue(inst.value); + + // Check if we're using defaults + const isDefaultOffset = + !inst.offset || + (inst.offset.kind === "const" && inst.offset.value === 0n); + const isDefaultLength = + !inst.length || + (inst.length.kind === "const" && inst.length.value === 32n); + + if (location === "storage" || location === "transient") { + // For storage/transient, slot is required + const slot = inst.slot ? this.formatValue(inst.slot) : "0"; + + if (isDefaultOffset && isDefaultLength) { + // Only slot - compact syntax with * to indicate word-sized operation + return `${location}[${slot}*] = ${value}`; + } else { + // Multiple fields - use named syntax + const parts: string[] = [`slot: ${slot}`]; + if (!isDefaultOffset && inst.offset) { + parts.push(`offset: ${this.formatValue(inst.offset)}`); + } + if (!isDefaultLength && inst.length) { + parts.push(`length: ${this.formatValue(inst.length)}`); + } + return `${location}[${parts.join(", ")}] = ${value}`; + } + } else { + // For memory/calldata/returndata + if (inst.offset) { + const offset = this.formatValue(inst.offset); + if (isDefaultLength) { + // Only offset - compact syntax with * to indicate word-sized operation + return `${location}[${offset}*] = ${value}`; + } else { + // Multiple fields - use named syntax + const length = inst.length ? this.formatValue(inst.length) : "32"; + return `${location}[offset: ${offset}, length: ${length}] = ${value}`; + } + } else { + // No offset specified + return `${location}[] = ${value}`; + } + } + } + + // NEW: unified compute offset + case "compute_offset": { + const base = this.formatValue(inst.base); + let offsetExpr: string; + + if (Ir.Instruction.ComputeOffset.isArray(inst)) { + const index = this.formatValue(inst.index); + if (inst.stride === 32) { + // Default stride - single param syntax + offsetExpr = `offset[${base}].array[${index}]`; + } else { + // Non-default stride - named syntax + offsetExpr = `offset[${base}].array[index: ${index}, stride: ${inst.stride}]`; + } + } else if (Ir.Instruction.ComputeOffset.isField(inst)) { + // Field just shows the offset + offsetExpr = `offset[${base}].field[${inst.fieldOffset}]`; + } else if (Ir.Instruction.ComputeOffset.isByte(inst)) { + const offset = this.formatValue(inst.offset); + offsetExpr = `offset[${base}].byte[${offset}]`; + } else { + // Shouldn't happen with proper typing + offsetExpr = `offset[${base}]`; + } + + // Add % prefix for temp destinations + const dest = inst.dest.startsWith("t") ? `%${inst.dest}` : inst.dest; + return `${dest} = ${offsetExpr}`; + } + + default: + return `; unknown instruction: ${(inst as unknown as { kind: string }).kind}`; + } + } + + private formatTerminator(term: Ir.Block.Terminator): string { + switch (term.kind) { + case "jump": + return `jump ${term.target}`; + + case "branch": + return `branch ${this.formatValue(term.condition)} ? ${term.trueTarget} : ${term.falseTarget}`; + + case "return": + return term.value + ? `return ${this.formatValue(term.value)}` + : "return void"; + + case "call": { + const args = term.arguments + .map((arg) => this.formatValue(arg)) + .join(", "); + const callPart = term.dest + ? `${term.dest} = call ${term.function}(${args})` + : `call ${term.function}(${args})`; + return `${callPart} -> ${term.continuation}`; + } + + default: + return `; unknown terminator: ${(term as unknown as { kind: string }).kind}`; + } + } + + private formatValue( + value: Ir.Value | bigint | string | boolean, + includeType: boolean = false, + ): string { + if (typeof value === "bigint") { + return value.toString(); + } + if (typeof value === "string") { + // If it's a hex string (starts with 0x), return without quotes + if (value.startsWith("0x")) { + return value; + } + return JSON.stringify(value); + } + if (typeof value === "boolean") { + return value.toString(); + } + + const baseFormat = (() => { + switch (value.kind) { + case "const": + // Pass type information to formatConstValue for proper hex formatting + return this.formatConstValue(value.value, value.type); + case "temp": + return `%${value.id}`; // Add % prefix for temps for clarity + default: + return "?"; + } + })(); + + // Only add type information if requested (to avoid redundancy) + if (includeType && value.type) { + const typeStr = this.formatType(value.type); + return `${baseFormat}: ${typeStr}`; + } + return baseFormat; + } + + private formatConstValue( + value: bigint | string | boolean, + type?: Ir.Type, + ): string { + if (typeof value === "bigint") { + // If we have type information and it's a bytes type, format as hex + if (type && type.kind === "scalar" && type.size <= 32) { + // Convert to hex string with 0x prefix + const hex = value.toString(16); + // Pad to even number of characters (2 per byte) + const padded = hex.length % 2 === 0 ? hex : "0" + hex; + const hexStr = `0x${padded}`; + + // Add decimal comment for meaningful values (not tiny single-digit values) + const shouldAddComment = + value >= 10n && + // Small values (less than 4 bytes) + (value <= 0xffffffffn || + // Common round numbers + value % 10000n === 0n || + // Powers of 10 + value === 10n || + value === 100n || + value === 1000n || + value === 10000n || + value === 100000n || + value === 1000000n || + // Powers of 2 up to 2^16 + ((value & (value - 1n)) === 0n && value <= 65536n)); + + if (shouldAddComment) { + // Check if we've already commented this value in this block + const valueKey = value.toString(); + if (!this.commentedValues.has(valueKey)) { + this.commentedValues.add(valueKey); + return `${hexStr} /* ${value} */`; + } + } + return hexStr; + } + return value.toString(); + } + if (typeof value === "string") { + // If it's already a hex string (starts with 0x), return without quotes + if (value.startsWith("0x")) { + return value; + } + // Otherwise, use JSON.stringify for proper escaping + return JSON.stringify(value); + } + return value.toString(); + } + + private formatType(type: Ir.Type): string { + switch (type.kind) { + case "scalar": + // Format scalar types based on size and origin + if (type.origin === "synthetic") { + return `scalar${type.size}`; + } + // Common scalar sizes + if (type.size === 32) return "uint256"; + if (type.size === 20) return "address"; + if (type.size === 1) return "bool"; + return `bytes${type.size}`; + case "ref": + // Format reference types based on location + return `ref<${type.location}>`; + default: + return "unknown"; + } + } + + private line(text: string): void { + const indentStr = " ".repeat(this.indent); + this.output.push(indentStr + text); + } + + private topologicalSort(func: Ir.Function): string[] { + const visited = new Set(); + const result: string[] = []; + + const visit = (blockId: string): void => { + if (visited.has(blockId)) return; + visited.add(blockId); + + // Add current block first (pre-order) + result.push(blockId); + + const block = func.blocks.get(blockId); + if (!block) return; + + // Then visit successors + const successors = this.getSuccessors(block); + for (const succ of successors) { + visit(succ); + } + }; + + // Start from entry + visit(func.entry); + + // Visit any unreachable blocks + for (const blockId of func.blocks.keys()) { + visit(blockId); + } + + return result; + } + + private getSuccessors(block: Ir.Block): string[] { + switch (block.terminator.kind) { + case "jump": + return [block.terminator.target]; + case "branch": + return [block.terminator.trueTarget, block.terminator.falseTarget]; + case "call": + return [block.terminator.continuation]; + case "return": + return []; + default: + return []; + } + } + + private formatSourceComment( + debug?: Ir.Instruction.Debug | Ir.Block.Debug, + ): string { + if (!debug?.context || !this.source) { + return ""; + } + + const context = debug.context; + + // Handle pick context - show all source locations on one line + if ("pick" in context && Array.isArray(context.pick)) { + const locations: string[] = []; + for (const pickContext of context.pick) { + const location = this.extractSourceLocation(pickContext); + if (location) { + locations.push(location); + } + } + if (locations.length > 0) { + return `; source: ${locations.join(" or ")}`; + } + return ""; + } + + // Handle direct code context + return this.formatContextSourceComment(context); + } + + private formatContextSourceComment( + context: Format.Program.Context | undefined, + ): string { + if (!context || !this.source) { + return ""; + } + + // Check for code.range (correct path according to ethdebug format) + if (Format.Program.Context.isCode(context) && context.code.range) { + const range = context.code.range; + if ( + typeof range.offset === "number" && + typeof range.length === "number" + ) { + // Convert to AST SourceLocation format for the existing formatter + const loc = { + offset: range.offset, + length: range.length, + }; + return AstAnalysis.formatSourceComment(loc, this.source); + } + } + + return ""; + } + + private extractSourceLocation( + context: Format.Program.Context | undefined, + ): string | null { + if (!context || !this.source) { + return null; + } + + // Check for code.range (correct path according to ethdebug format) + if (Format.Program.Context.isCode(context) && context.code.range) { + const range = context.code.range; + if ( + typeof range.offset === "number" && + typeof range.length === "number" + ) { + // Convert to AST SourceLocation format and extract just the location part + const loc = { + offset: range.offset, + length: range.length, + }; + const fullComment = AstAnalysis.formatSourceComment(loc, this.source); + // Extract just the location part (e.g., "16:3-11" from "; source: 16:3-11") + const match = fullComment.match(/; source: (.+)/); + return match ? match[1] : null; + } + } + + return null; + } +} diff --git a/packages/bugc/src/ir/analysis/index.ts b/packages/bugc/src/ir/analysis/index.ts new file mode 100644 index 00000000..5bd21e6b --- /dev/null +++ b/packages/bugc/src/ir/analysis/index.ts @@ -0,0 +1,7 @@ +/** + * IR utilities exports + */ + +export { Formatter } from "./formatter.js"; +export { Validator } from "./validator.js"; +export { Statistics } from "./stats.js"; diff --git a/packages/bugc/src/ir/analysis/stats.test.ts b/packages/bugc/src/ir/analysis/stats.test.ts new file mode 100644 index 00000000..4c13dc7e --- /dev/null +++ b/packages/bugc/src/ir/analysis/stats.test.ts @@ -0,0 +1,250 @@ +import { describe, it, expect } from "vitest"; + +import { compile } from "#compiler"; + +import * as Ir from "#ir/spec"; + +import { Statistics } from "./stats.js"; + +describe("Statistics", () => { + const compileToIr = async (source: string): Promise => { + const result = await compile({ to: "ir", source, sourcePath: "test.bug" }); + + if (!result.success) { + throw new Error("Compilation failed"); + } + + return result.value.ir; + }; + + describe("computeDominatorTree", () => { + it("should compute correct dominators for simple if-else", async () => { + const source = ` + name Test; + storage { [0] x: uint256; } + code { + if (x > 0) { + x = 1; + } else { + x = 2; + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + // Check dominator relationships + const dom = analysis.dominatorTree; + + // Entry dominates all blocks + expect(dom.entry).toBe(null); // Entry has no dominator + + // The then and else blocks should be dominated by entry + const thenBlock = Object.keys(dom).find((b) => b.includes("then")); + const elseBlock = Object.keys(dom).find((b) => b.includes("else")); + const mergeBlock = Object.keys(dom).find((b) => b.includes("merge")); + + expect(thenBlock).toBeDefined(); + expect(elseBlock).toBeDefined(); + expect(mergeBlock).toBeDefined(); + + expect(dom[thenBlock!]).toBe("entry"); + expect(dom[elseBlock!]).toBe("entry"); + expect(dom[mergeBlock!]).toBe("entry"); + }); + + it("should compute correct dominators for nested control flow", async () => { + const source = ` + name Test; + storage { [0] x: uint256; } + code { + if (x > 0) { + if (x < 10) { + x = 1; + } + x = x + 1; + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + const dom = analysis.dominatorTree; + + // Find the nested blocks + const blocks = Object.keys(dom); + const outerThen = blocks.find((b) => b === "then_1"); + const innerThen = blocks.find((b) => b === "then_3"); + const innerMerge = blocks.find((b) => b === "merge_4"); + + expect(outerThen).toBeDefined(); + expect(innerThen).toBeDefined(); + expect(innerMerge).toBeDefined(); + + // Outer then is dominated by entry + expect(dom[outerThen!]).toBe("entry"); + + // Inner blocks are dominated by outer then + expect(dom[innerThen!]).toBe(outerThen); + expect(dom[innerMerge!]).toBe(outerThen); + }); + + it("should detect loops correctly with proper dominators", async () => { + const source = ` + name Test; + storage { [0] x: uint256; } + code { + for (let i = 0; i < 10; i = i + 1) { + x = x + i; + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + // Check that loops are detected + expect(analysis.loopInfo).toHaveLength(1); + + const loop = analysis.loopInfo[0]; + expect(loop.header).toContain("for_header"); + expect(loop.blocks.length).toBeGreaterThan(2); // header, body, update at minimum + + // Verify dominator relationships in loop + const dom = analysis.dominatorTree; + const header = loop.header; + const body = Object.keys(dom).find((b) => b.includes("for_body")); + const update = Object.keys(dom).find((b) => b.includes("for_update")); + + expect(body).toBeDefined(); + expect(update).toBeDefined(); + + // Header dominates body and update + expect(dom[body!]).toBe(header); + expect(dom[update!]).toBe(body); // Body dominates update in for loops + }); + + it("should handle complex control flow with multiple merge points", async () => { + const source = ` + name Test; + storage { [0] x: uint256; [1] y: uint256; } + code { + if (x > 0) { + if (y > 0) { + x = 1; + } else { + x = 2; + } + } else { + if (y < 0) { + x = 3; + } else { + x = 4; + } + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + const dom = analysis.dominatorTree; + + // All blocks should have proper dominators + for (const [block, dominator] of Object.entries(dom)) { + if (block !== "entry") { + expect(dominator).toBeDefined(); + expect(typeof dominator).toBe("string"); + } + } + + // No loops should be detected in this example + expect(analysis.loopInfo).toHaveLength(0); + }); + }); + + describe("detectLoops", () => { + it("should detect nested loops", async () => { + const source = ` + name Test; + storage { [0] x: uint256; } + code { + for (let i = 0; i < 10; i = i + 1) { + for (let j = 0; j < 5; j = j + 1) { + x = x + i + j; + } + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + // Should detect both loops + expect(analysis.loopInfo.length).toBeGreaterThanOrEqual(2); + + // Find the loops by their headers + const outerLoop = analysis.loopInfo.find( + (l) => l.header === "for_header_1", + ); + const innerLoop = analysis.loopInfo.find( + (l) => l.header === "for_header_5", + ); + + expect(outerLoop).toBeDefined(); + expect(innerLoop).toBeDefined(); + + // Verify loop blocks + expect(outerLoop!.blocks.length).toBeGreaterThan(3); + expect(innerLoop!.blocks.length).toBeGreaterThan(2); + + // Verify loop depths + expect(outerLoop!.depth).toBe(1); // Outer loop has depth 1 + expect(innerLoop!.depth).toBe(2); // Inner loop has depth 2 + }); + + it("should detect deeply nested loops with correct depths", async () => { + const source = ` + name Test; + storage { [0] x: uint256; } + code { + for (let i = 0; i < 10; i = i + 1) { + for (let j = 0; j < 5; j = j + 1) { + for (let k = 0; k < 3; k = k + 1) { + x = x + i + j + k; + } + } + } + return; + } + `; + + const ir = await compileToIr(source); + const stats = new Statistics.Analyzer(); + const analysis = stats.analyze(ir); + + // Should detect all three loops + expect(analysis.loopInfo.length).toBeGreaterThanOrEqual(3); + + // Sort loops by depth to make testing easier + const loopsByDepth = analysis.loopInfo.sort((a, b) => a.depth - b.depth); + + // Check depths + expect(loopsByDepth[0].depth).toBe(1); // Outermost loop + expect(loopsByDepth[1].depth).toBe(2); // Middle loop + expect(loopsByDepth[2].depth).toBe(3); // Innermost loop + }); + }); +}); diff --git a/packages/bugc/src/ir/analysis/stats.ts b/packages/bugc/src/ir/analysis/stats.ts new file mode 100644 index 00000000..af4ee8db --- /dev/null +++ b/packages/bugc/src/ir/analysis/stats.ts @@ -0,0 +1,432 @@ +/** + * IR Statistics analyzer + */ + +import * as Ir from "#ir/spec"; + +export interface Statistics { + blockCount: number; + instructionCount: number; + tempCount: number; + parameterCount: number; + maxBlockSize: number; + avgBlockSize: number; + cfgEdges: number; + instructionTypes: Record; + criticalPaths: Statistics.PathInfo[]; + dominatorTree: Record; + loopInfo: Statistics.LoopInfo[]; +} + +export namespace Statistics { + export interface PathInfo { + from: string; + to: string; + length: number; + blocks: string[]; + } + + export interface LoopInfo { + header: string; + blocks: string[]; + depth: number; + } + + export class Analyzer { + analyze(module: Ir.Module): Statistics { + const func = module.main; + const blocks = Array.from(func.blocks.values()); + + // Basic counts + const blockCount = blocks.length; + const instructions = blocks.flatMap((b) => b.instructions); + const instructionCount = instructions.length; + + // Instruction type distribution + const instructionTypes: Record = {}; + for (const inst of instructions) { + instructionTypes[inst.kind] = (instructionTypes[inst.kind] || 0) + 1; + } + + // Block sizes + const blockSizes = blocks.map((b) => b.instructions.length + 1); // +1 for terminator + const maxBlockSize = Math.max(...blockSizes); + const avgBlockSize = blockSizes.reduce((a, b) => a + b, 0) / blockCount; + + // Count temporaries and parameters + const tempCount = this.countTemporaries(func); + const parameterCount = func.parameters.length; + + // CFG edges + const cfgEdges = this.countCfgEdges(func); + + // Advanced analyses + const dominatorTree = this.computeDominatorTree(func); + const criticalPaths = this.findCriticalPaths(func); + const loopInfo = this.detectLoops(func, dominatorTree); + + return { + blockCount, + instructionCount, + tempCount, + parameterCount, + maxBlockSize, + avgBlockSize: Math.round(avgBlockSize * 10) / 10, + cfgEdges, + instructionTypes, + criticalPaths, + dominatorTree, + loopInfo, + }; + } + + private countTemporaries(func: Ir.Function): number { + const temps = new Set(); + + for (const block of func.blocks.values()) { + for (const inst of block.instructions) { + // Check destinations + if ("dest" in inst && inst.dest) { + temps.add(inst.dest); + } + + // Check value uses + this.collectTempsFromValue(inst, temps); + } + + // Check terminator + if (block.terminator.kind === "branch") { + this.collectTempsFromValueRef(block.terminator.condition, temps); + } else if ( + block.terminator.kind === "return" && + block.terminator.value + ) { + this.collectTempsFromValueRef(block.terminator.value, temps); + } + } + + return temps.size; + } + + private collectTempsFromValue( + inst: Ir.Instruction, + temps: Set, + ): void { + const valueFields = [ + "value", + "left", + "right", + "operand", + "object", + "array", + "index", + "key", + ]; + + for (const field of valueFields) { + if (field in inst) { + const fieldValue = (inst as unknown as Record)[ + field + ]; + if (fieldValue) { + this.collectTempsFromValueRef(fieldValue, temps); + } + } + } + } + + private collectTempsFromValueRef( + value: Ir.Value | unknown, + temps: Set, + ): void { + if ( + typeof value === "object" && + value && + "kind" in value && + value.kind === "temp" + ) { + temps.add((value as { kind: "temp"; id: string }).id); + } + } + + private countCfgEdges(func: Ir.Function): number { + let edges = 0; + + for (const block of func.blocks.values()) { + switch (block.terminator.kind) { + case "jump": + edges += 1; + break; + case "branch": + edges += 2; + break; + // return has no edges + } + } + + return edges; + } + + private computeDominatorTree( + func: Ir.Function, + ): Record { + // Implement the standard iterative dominator tree algorithm + const dominators: Record = {}; + const blockIds = Array.from(func.blocks.keys()); + + // Build predecessor map for efficiency + const predecessors: Record = {}; + for (const blockId of blockIds) { + predecessors[blockId] = Array.from( + func.blocks.get(blockId)?.predecessors || [], + ); + } + + // Entry block dominates itself (has no dominator) + dominators[func.entry] = null; + + // Initialize all other blocks to undefined (not yet computed) + for (const blockId of blockIds) { + if (blockId !== func.entry) { + dominators[blockId] = undefined!; // Will be computed + } + } + + // Iterative algorithm - repeat until fixed point + let changed = true; + while (changed) { + changed = false; + + // Process blocks in a reasonable order (BFS from entry) + const worklist = [func.entry]; + const processed = new Set([func.entry]); + + while (worklist.length > 0) { + const current = worklist.shift()!; + const block = func.blocks.get(current); + if (!block) continue; + + // Add successors to worklist + const successors = this.getSuccessors(block); + for (const succ of successors) { + if (!processed.has(succ)) { + worklist.push(succ); + processed.add(succ); + } + } + + // Skip entry block + if (current === func.entry) continue; + + // Find immediate dominator + const preds = predecessors[current] || []; + if (preds.length === 0) continue; // Unreachable block + + // Find first predecessor with a dominator + let newDom: string | undefined; + for (const pred of preds) { + if (dominators[pred] !== undefined) { + newDom = pred; + break; + } + } + + if (newDom === undefined) continue; // No processed predecessors yet + + // Intersect with other predecessors + for (const pred of preds) { + if (pred !== newDom && dominators[pred] !== undefined) { + newDom = this.intersectDominators(pred, newDom, dominators); + } + } + + // Update if changed + if (dominators[current] !== newDom) { + dominators[current] = newDom; + changed = true; + } + } + } + + return dominators; + } + + private intersectDominators( + b1: string, + b2: string, + dominators: Record, + ): string { + // Find common dominator of b1 and b2 + let finger1: string | null = b1; + let finger2: string | null = b2; + + // Create paths from both blocks to entry + const path1 = new Set(); + while (finger1 !== null) { + path1.add(finger1); + finger1 = dominators[finger1] ?? null; + } + + // Find first common ancestor + while (finger2 !== null) { + if (path1.has(finger2)) { + return finger2; + } + finger2 = dominators[finger2] ?? null; + } + + // Should never happen if CFG is well-formed + throw new Error("No common dominator found - CFG may be disconnected"); + } + + private findCriticalPaths(func: Ir.Function): PathInfo[] { + const paths: PathInfo[] = []; + const visited = new Set(); + + // Find longest paths from entry to returns + const findPaths = ( + blockId: string, + path: string[], + length: number, + ): void => { + if (visited.has(blockId)) return; + + const block = func.blocks.get(blockId); + if (!block) return; + + const newPath = [...path, blockId]; + const newLength = length + block.instructions.length + 1; + + if (block.terminator.kind === "return") { + paths.push({ + from: func.entry, + to: blockId, + length: newLength, + blocks: newPath, + }); + return; + } + + visited.add(blockId); + + // Explore successors + const successors = this.getSuccessors(block); + for (const succ of successors) { + findPaths(succ, newPath, newLength); + } + + visited.delete(blockId); + }; + + findPaths(func.entry, [], 0); + + // Sort by length and return top 3 + return paths.sort((a, b) => b.length - a.length).slice(0, 3); + } + + private detectLoops( + func: Ir.Function, + dominators: Record, + ): LoopInfo[] { + const loops: LoopInfo[] = []; + + // Find back edges (proper loop detection) + for (const [blockId, block] of func.blocks.entries()) { + const successors = this.getSuccessors(block); + + for (const succ of successors) { + // Check if this is a back edge + if (this.dominates(succ, blockId, dominators)) { + // Found a loop with header at succ + const loopBlocks = this.findLoopBlocks(succ, blockId, func); + loops.push({ + header: succ, + blocks: loopBlocks, + depth: 0, // Will be computed later + }); + } + } + } + + // Compute loop depths by checking containment + this.computeLoopDepths(loops); + + return loops; + } + + private computeLoopDepths(loops: LoopInfo[]): void { + // For each loop, count how many other loops contain it + for (let i = 0; i < loops.length; i++) { + let depth = 1; // Base depth is 1 + + // Check if this loop is contained within any other loop + for (let j = 0; j < loops.length; j++) { + if (i !== j && this.isLoopContainedIn(loops[i], loops[j])) { + depth++; + } + } + + loops[i].depth = depth; + } + } + + private isLoopContainedIn(inner: LoopInfo, outer: LoopInfo): boolean { + // A loop is contained in another if its header is part of the outer loop's blocks + return outer.blocks.includes(inner.header); + } + + private dominates( + a: string, + b: string, + dominators: Record, + ): boolean { + // Check if a dominates b + let current: string | null = b; + while (current !== null) { + if (current === a) return true; + current = dominators[current] || null; + } + return false; + } + + private findLoopBlocks( + header: string, + tail: string, + func: Ir.Function, + ): string[] { + // Find all blocks in the loop (simplified) + const loopBlocks = new Set([header, tail]); + const worklist = [tail]; + + while (worklist.length > 0) { + const blockId = worklist.pop()!; + const block = func.blocks.get(blockId); + if (!block) continue; + + for (const pred of block.predecessors) { + if (!loopBlocks.has(pred)) { + loopBlocks.add(pred); + if (pred !== header) { + worklist.push(pred); + } + } + } + } + + return Array.from(loopBlocks); + } + + private getSuccessors(block: Ir.Block): string[] { + switch (block.terminator.kind) { + case "jump": + return [block.terminator.target]; + case "branch": + return [block.terminator.trueTarget, block.terminator.falseTarget]; + case "return": + return []; + default: + return []; + } + } + } +} diff --git a/packages/bugc/src/ir/analysis/validator.ts b/packages/bugc/src/ir/analysis/validator.ts new file mode 100644 index 00000000..51e819f0 --- /dev/null +++ b/packages/bugc/src/ir/analysis/validator.ts @@ -0,0 +1,479 @@ +/** + * IR Validator - checks IR consistency and correctness + */ + +import * as Ir from "#ir/spec"; + +export class Validator { + private errors: string[] = []; + private warnings: string[] = []; + private tempDefs: Set = new Set(); + private tempUses: Set = new Set(); + private blockIds: Set = new Set(); + + validate(module: Ir.Module): Validator.Result { + this.errors = []; + this.warnings = []; + this.tempDefs = new Set(); + this.tempUses = new Set(); + this.blockIds = new Set(); + + // Validate module structure + this.validateModule(module); + + // Check for undefined temporaries + this.checkUndefinedTemporaries(); + + return { + isValid: this.errors.length === 0, + errors: this.errors, + warnings: this.warnings, + }; + } + + private validateModule(module: Ir.Module): void { + // Check module has a name + if (!module.name) { + this.error("Module must have a name"); + } + + // Validate main function + if (!module.main) { + this.error("Module must have a main function"); + } else { + this.validateFunction(module.main); + } + } + + private validateFunction(func: Ir.Function): void { + // Collect all block IDs + for (const blockId of func.blocks.keys()) { + this.blockIds.add(blockId); + } + + // Check entry block exists + if (!func.blocks.has(func.entry)) { + this.error(`Entry block '${func.entry}' not found in function`); + } + + // Validate parameters + for (const param of func.parameters) { + if (!param.tempId || !param.name) { + this.error("Parameter must have tempId and name"); + } + this.tempDefs.add(param.tempId); + this.validateType(param.type); + } + + // Validate each block + for (const [blockId, block] of func.blocks.entries()) { + this.validateBlock(blockId, block, func); + } + + // Check for unreachable blocks + this.checkUnreachableBlocks(func); + + // Check predecessor consistency + this.checkPredecessorConsistency(func); + } + + private validateBlock( + blockId: string, + block: Ir.Block, + _func: Ir.Function, + ): void { + // Validate instructions + for (const inst of block.instructions) { + this.validateInstruction(inst); + } + + // Validate terminator + this.validateTerminator(block.terminator); + + // Check terminator targets exist + const targets = this.getTerminatorTargets(block.terminator); + for (const target of targets) { + if (!this.blockIds.has(target)) { + this.error( + `Block '${blockId}' jumps to non-existent block '${target}'`, + ); + } + } + } + + private validateInstruction(inst: Ir.Instruction): void { + // Check instruction has required fields + if (!inst.kind) { + this.error("Instruction must have a kind"); + return; + } + + // Validate based on instruction type + switch (inst.kind) { + case "const": + this.validateConstInstruction(inst); + break; + case "binary": + this.validateBinaryInstruction(inst); + break; + case "unary": + this.validateUnaryInstruction(inst); + break; + case "env": + this.validateEnvInstruction(inst); + break; + case "compute_slot": + this.validateComputeSlotInstruction(inst); + break; + case "hash": + this.validateHashInstruction(inst); + break; + // Add more instruction validations as needed + } + } + + private validateConstInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "const") return; + + if (!inst.dest) { + this.error("Const instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (inst.value === undefined) { + this.error("Const instruction must have a value"); + } + + this.validateType(inst.type); + } + + private validateBinaryInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "binary") return; + + if (!inst.dest) { + this.error("Binary instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (!inst.op) { + this.error("Binary instruction must have an operator"); + } + + if (!inst.left) { + this.error("Binary instruction must have a left operand"); + } else { + this.validateValue(inst.left); + } + + if (!inst.right) { + this.error("Binary instruction must have a right operand"); + } else { + this.validateValue(inst.right); + } + } + + private validateUnaryInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "unary") return; + + if (!inst.dest) { + this.error("Unary instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (!inst.op) { + this.error("Unary instruction must have an operator"); + } + + if (!inst.operand) { + this.error("Unary instruction must have an operand"); + } else { + this.validateValue(inst.operand); + } + } + + private validateEnvInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "env") return; + + if (!inst.dest) { + this.error("Env instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (!inst.op) { + this.error("Env instruction must have an operation"); + } + + const validOps = [ + "msg_sender", + "msg_value", + "block_number", + "block_timestamp", + ]; + if (!validOps.includes(inst.op)) { + this.error(`Invalid env operation '${inst.op}'`); + } + } + + private validateComputeSlotInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "compute_slot") return; + + if (!inst.dest) { + this.error("Compute slot instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (!inst.base) { + this.error("Compute slot instruction must have a base"); + } else { + this.validateValue(inst.base); + } + + // Validate based on slot kind + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + if (!inst.key) { + this.error("Mapping compute_slot must have a key"); + } else { + this.validateValue(inst.key); + } + if (!inst.keyType) { + this.error("Mapping compute_slot must have a keyType"); + } else { + this.validateType(inst.keyType); + } + } else if (Ir.Instruction.ComputeSlot.isArray(inst)) { + // Array compute_slot now only computes the first slot (no index needed) + } else if (Ir.Instruction.ComputeSlot.isField(inst)) { + if (inst.fieldOffset === undefined) { + this.error("Field compute_slot must have a fieldOffset"); + } + } else { + // This should never be reached due to exhaustive type checking + const _exhaustive: never = inst; + void _exhaustive; + this.error(`Unknown compute_slot kind`); + } + } + + private validateHashInstruction(inst: Ir.Instruction): void { + if (inst.kind !== "hash") return; + + if (!inst.dest) { + this.error("Hash instruction must have a destination"); + } else { + this.tempDefs.add(inst.dest); + } + + if (!inst.value) { + this.error("Hash instruction must have a value"); + } else { + this.validateValue(inst.value); + } + } + + private validateTerminator(term: Ir.Block["terminator"]): void { + if (!term.kind) { + this.error("Terminator must have a kind"); + return; + } + + switch (term.kind) { + case "jump": + if (!term.target) { + this.error("Jump terminator must have a target"); + } + break; + + case "branch": + if (!term.condition) { + this.error("Branch terminator must have a condition"); + } else { + this.validateValue(term.condition); + } + if (!term.trueTarget) { + this.error("Branch terminator must have a true target"); + } + if (!term.falseTarget) { + this.error("Branch terminator must have a false target"); + } + break; + + case "return": + if (term.value) { + this.validateValue(term.value); + } + break; + + default: + this.error( + `Unknown terminator kind '${(term as unknown as { kind: string }).kind}'`, + ); + } + } + + private validateValue(value: Ir.Value): void { + if (!value || typeof value !== "object") return; + + switch (value.kind) { + case "temp": + if (!value.id) { + this.error("Temp value must have an id"); + } else { + this.tempUses.add(value.id); + } + break; + + case "const": + if (value.value === undefined) { + this.error("Const value must have a value"); + } + break; + + default: + this.error( + `Unknown value kind '${(value as unknown as { kind: string }).kind}'`, + ); + } + + if (value.type) { + this.validateType(value.type); + } + } + + private validateType(type: Ir.Type): void { + if (!type || !type.kind) { + this.error("Type must have a kind"); + return; + } + + switch (type.kind) { + case "scalar": + if (!type.size || type.size < 1 || type.size > 32) { + this.error(`Invalid scalar size: ${type.size}`); + } + if (!type.origin) { + this.error("Scalar type must have an origin"); + } + break; + + case "ref": + if (!type.location) { + this.error("Reference type must have a location"); + } else if ( + !["memory", "storage", "calldata", "returndata"].includes( + type.location, + ) + ) { + this.error(`Invalid reference location: ${type.location}`); + } + if (!type.origin) { + this.error("Reference type must have an origin"); + } + break; + + default: + this.error( + `Unknown type kind '${(type as unknown as { kind: string }).kind}'`, + ); + } + } + + private checkUndefinedTemporaries(): void { + for (const tempId of this.tempUses) { + if (!this.tempDefs.has(tempId)) { + this.error(`Use of undefined temporary '${tempId}'`); + } + } + } + + private checkUnreachableBlocks(func: Ir.Function): void { + const reachable = new Set(); + const worklist = [func.entry]; + + while (worklist.length > 0) { + const blockId = worklist.pop()!; + if (reachable.has(blockId)) continue; + + reachable.add(blockId); + const block = func.blocks.get(blockId); + if (!block) continue; + + const targets = this.getTerminatorTargets(block.terminator); + worklist.push(...targets); + } + + for (const blockId of func.blocks.keys()) { + if (!reachable.has(blockId)) { + this.warning(`Block '${blockId}' is unreachable`); + } + } + } + + private checkPredecessorConsistency(func: Ir.Function): void { + // Build actual predecessor sets + const actualPreds = new Map>(); + + for (const [blockId, block] of func.blocks.entries()) { + const targets = this.getTerminatorTargets(block.terminator); + for (const target of targets) { + if (!actualPreds.has(target)) { + actualPreds.set(target, new Set()); + } + actualPreds.get(target)!.add(blockId); + } + } + + // Check consistency + for (const [blockId, block] of func.blocks.entries()) { + const expected = actualPreds.get(blockId) || new Set(); + const recorded = block.predecessors; + + // Check for missing predecessors + for (const pred of expected) { + if (!recorded.has(pred)) { + this.error(`Block '${blockId}' missing predecessor '${pred}'`); + } + } + + // Check for extra predecessors + for (const pred of recorded) { + if (!expected.has(pred)) { + this.error(`Block '${blockId}' has invalid predecessor '${pred}'`); + } + } + } + } + + private getTerminatorTargets(term: Ir.Block["terminator"]): string[] { + switch (term.kind) { + case "jump": + return [term.target]; + case "branch": + return [term.trueTarget, term.falseTarget]; + case "return": + return []; + default: + return []; + } + } + + private error(message: string): void { + this.errors.push(message); + } + + private warning(message: string): void { + this.warnings.push(message); + } +} + +export namespace Validator { + export interface Result { + isValid: boolean; + errors: string[]; + warnings: string[]; + } +} diff --git a/packages/bugc/src/ir/index.ts b/packages/bugc/src/ir/index.ts new file mode 100644 index 00000000..6c5ebaa7 --- /dev/null +++ b/packages/bugc/src/ir/index.ts @@ -0,0 +1,10 @@ +/** + * BUG-IR (Intermediate Representation) module + * + * This module provides the intermediate representation used between + * the AST and final code generation phases. + */ + +export * from "./spec/index.js"; +export * as Analysis from "./analysis/index.js"; +export * as Utils from "./utils/index.js"; diff --git a/packages/bugc/src/ir/spec/block.ts b/packages/bugc/src/ir/spec/block.ts new file mode 100644 index 00000000..e1f72aa9 --- /dev/null +++ b/packages/bugc/src/ir/spec/block.ts @@ -0,0 +1,75 @@ +import type * as Format from "@ethdebug/format"; + +import { Value } from "./value.js"; +import type { Type } from "./type.js"; +import type { Instruction } from "./instruction.js"; + +/** + * Basic block - sequence of instructions with single entry/exit + */ +export interface Block { + /** Unique block ID */ + id: string; + /** Phi nodes must be at the beginning of the block */ + phis: Block.Phi[]; + /** Instructions in execution order (after phi nodes) */ + instructions: Instruction[]; + /** Terminal instruction (jump, conditional jump, or return) */ + terminator: Block.Terminator; + /** Predecessor block IDs (for CFG construction) */ + predecessors: Set; + /** Debug information (e.g., for if/while blocks) */ + debug: Block.Debug; +} + +export namespace Block { + /** + * Debug information for blocks, terminators, and phi nodes + */ + export interface Debug { + context?: Format.Program.Context; + } + + /** + * Block terminator instructions + */ + export type Terminator = + | { kind: "jump"; target: string; operationDebug: Block.Debug } + | { + kind: "branch"; + condition: Value; + conditionDebug?: Block.Debug; + trueTarget: string; + falseTarget: string; + operationDebug: Block.Debug; + } + | { + kind: "return"; + value?: Value; + valueDebug?: Block.Debug; + operationDebug: Block.Debug; + } + | { + kind: "call"; + function: string; + arguments: Value[]; + argumentsDebug?: Block.Debug[]; + dest?: string; + continuation: string; + operationDebug: Block.Debug; + }; + + export interface Phi { + kind: "phi"; + /** Map from predecessor block ID to value */ + sources: Map; + /** Map from predecessor block ID to debug context for that source */ + sourcesDebug?: Map; + /** Destination temp to assign the phi result */ + dest: string; + /** Type of the phi node (all sources must have same type) */ + type: Type; + /** Debug context for the phi operation itself */ + operationDebug: Block.Debug; + } +} diff --git a/packages/bugc/src/ir/spec/function.ts b/packages/bugc/src/ir/spec/function.ts new file mode 100644 index 00000000..df58c6ac --- /dev/null +++ b/packages/bugc/src/ir/spec/function.ts @@ -0,0 +1,52 @@ +import type * as Ast from "#ast"; + +import type { Type } from "./type.js"; +import type { Block } from "./block.js"; + +/** + * Ir function containing basic blocks + */ +export interface Function { + /** Function name (for debugging) */ + name: string; + /** Function parameters as temps (in SSA form) */ + parameters: Function.Parameter[]; + /** Entry block ID */ + entry: string; + /** All basic blocks in the function */ + blocks: Map; + /** SSA variable metadata mapping temp IDs to original variables */ + ssaVariables?: Map; +} + +export namespace Function { + /** + * Function parameter in SSA form + */ + export interface Parameter { + /** Parameter name (for debugging) */ + name: string; + /** Parameter type */ + type: Type; + /** Temp ID for this parameter */ + tempId: string; + /** Source location of declaration */ + loc?: Ast.SourceLocation; + } + + /** + * SSA variable metadata + */ + export interface SsaVariable { + /** Original variable name */ + name: string; + /** Scope identifier (to handle shadowing) */ + scopeId: string; + /** Type of the variable */ + type: Type; + /** Version number for this SSA instance */ + version: number; + /** Source location of declaration */ + loc?: Ast.SourceLocation; + } +} diff --git a/packages/bugc/src/ir/spec/index.ts b/packages/bugc/src/ir/spec/index.ts new file mode 100644 index 00000000..e048d94d --- /dev/null +++ b/packages/bugc/src/ir/spec/index.ts @@ -0,0 +1,7 @@ +export { Type } from "./type.js"; +export { Value } from "./value.js"; +export type { ValueDebug } from "./value.js"; +export * from "./instruction.js"; +export type { Block } from "./block.js"; +export type { Function } from "./function.js"; +export type { Module } from "./module.js"; diff --git a/packages/bugc/src/ir/spec/instruction.ts b/packages/bugc/src/ir/spec/instruction.ts new file mode 100644 index 00000000..2720c10c --- /dev/null +++ b/packages/bugc/src/ir/spec/instruction.ts @@ -0,0 +1,380 @@ +import * as Format from "@ethdebug/format"; + +import type { Type } from "./type.js"; +import { Value } from "./value.js"; + +export type Instruction = + // Constants + | Instruction.Const + // Memory management + | Instruction.Allocate + // Unified read/write operations + | Instruction.Read + | Instruction.Write + // Storage slot computation + | Instruction.ComputeSlot + // Unified compute operations + | Instruction.ComputeOffset + // Arithmetic and logic + | Instruction.BinaryOp + | Instruction.UnaryOp + // Environment access + | Instruction.Env + // Type operations + | Instruction.Hash + | Instruction.Cast + // Length operations + | Instruction.Length; + +export namespace Instruction { + export interface Base { + kind: string; + /** + * Debug context for the operation itself (not operands). + * Renamed from `debug` to clarify that this tracks only the operation, + * while operands have their own debug contexts. + */ + operationDebug: Instruction.Debug; + } + + export interface Debug { + context?: Format.Program.Context; + } + + // Location types for unified read/write + export type Location = + | "storage" + | "transient" + | "memory" + | "calldata" + | "returndata" + | "code" + | "local"; + + // NEW: Unified Read instruction + export interface Read extends Instruction.Base { + kind: "read"; + location: Location; + // For storage/transient (segment-based) + slot?: Value; + slotDebug?: Instruction.Debug; + // For all locations that need offset + offset?: Value; + offsetDebug?: Instruction.Debug; + // Length in bytes + length?: Value; + lengthDebug?: Instruction.Debug; + // For local variables + name?: string; + // Destination and type + dest: string; + type: Type; + } + + // NEW: Unified Write instruction + export interface Write extends Instruction.Base { + kind: "write"; + location: Exclude; // No writes to read-only locations + // For storage/transient (segment-based) + slot?: Value; + slotDebug?: Instruction.Debug; + // For all locations that need offset + offset?: Value; + offsetDebug?: Instruction.Debug; + // Length in bytes + length?: Value; + lengthDebug?: Instruction.Debug; + // For local variables + name?: string; + // Value to write + value: Value; + valueDebug?: Instruction.Debug; + } + + // NEW: Unified compute offset instruction + export type ComputeOffset = + | ComputeOffset.Array + | ComputeOffset.Field + | ComputeOffset.Byte; + + export namespace ComputeOffset { + export interface Base extends Instruction.Base { + kind: "compute_offset"; + offsetKind: "array" | "field" | "byte"; + location: "memory" | "calldata" | "returndata" | "code"; + base: Value; + baseDebug?: Instruction.Debug; + dest: string; + } + + export interface Array extends ComputeOffset.Base { + offsetKind: "array"; + index: Value; + indexDebug?: Instruction.Debug; + stride: number; + } + + export const isArray = (inst: ComputeOffset): inst is ComputeOffset.Array => + inst.offsetKind === "array"; + + export const array = ( + location: "memory" | "calldata" | "returndata" | "code", + base: Value, + index: Value, + stride: number, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + indexDebug?: Instruction.Debug, + ): ComputeOffset.Array => ({ + kind: "compute_offset", + offsetKind: "array", + location, + base, + baseDebug, + index, + indexDebug, + stride, + dest, + operationDebug, + }); + + export interface Field extends ComputeOffset.Base { + offsetKind: "field"; + field: string; + fieldOffset: number; + } + + export const isField = (inst: ComputeOffset): inst is ComputeOffset.Field => + inst.offsetKind === "field"; + + export const field = ( + location: "memory" | "calldata" | "returndata" | "code", + base: Value, + field: string, + fieldOffset: number, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + ): ComputeOffset.Field => ({ + kind: "compute_offset", + offsetKind: "field", + location, + base, + baseDebug, + field, + fieldOffset, + dest, + operationDebug, + }); + + export interface Byte extends ComputeOffset.Base { + offsetKind: "byte"; + offset: Value; + offsetDebug?: Instruction.Debug; + } + + export const isByte = (inst: ComputeOffset): inst is ComputeOffset.Byte => + inst.offsetKind === "byte"; + + export const byte = ( + location: "memory" | "calldata" | "returndata" | "code", + base: Value, + offset: Value, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + offsetDebug?: Instruction.Debug, + ): ComputeOffset.Byte => ({ + kind: "compute_offset", + offsetKind: "byte", + location, + base, + baseDebug, + offset, + offsetDebug, + dest, + operationDebug, + }); + } + + export interface Const extends Instruction.Base { + kind: "const"; + value: bigint | string | boolean; + valueDebug?: Instruction.Debug; + type: Type; + dest: string; + } + + // Memory allocation instruction + export interface Allocate extends Instruction.Base { + kind: "allocate"; + location: "memory"; // For now, only memory allocation + size: Value; // Size in bytes to allocate + sizeDebug?: Instruction.Debug; + dest: string; // Destination temp for the allocated pointer + } + + export type ComputeSlot = + | ComputeSlot.Mapping + | ComputeSlot.Array + | ComputeSlot.Field; + + export namespace ComputeSlot { + export interface Base extends Instruction.Base { + kind: "compute_slot"; + slotKind: "mapping" | "array" | "field"; + base: Value; + baseDebug?: Instruction.Debug; + dest: string; + } + + export interface Mapping extends ComputeSlot.Base { + slotKind: "mapping"; + key: Value; + keyDebug?: Instruction.Debug; + keyType: Type; + } + + export const isMapping = (inst: ComputeSlot): inst is ComputeSlot.Mapping => + inst.slotKind === "mapping"; + + export const mapping = ( + base: Value, + key: Value, + keyType: Type, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + keyDebug?: Instruction.Debug, + ): ComputeSlot.Mapping => ({ + kind: "compute_slot", + slotKind: "mapping", + base, + baseDebug, + key, + keyDebug, + keyType, + dest, + operationDebug, + }); + + export interface Array extends ComputeSlot.Base { + slotKind: "array"; + // No index - just computes the first slot of the array + } + + export const isArray = (inst: ComputeSlot): inst is ComputeSlot.Array => + inst.slotKind === "array"; + + export const array = ( + base: Value, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + ): ComputeSlot.Array => ({ + kind: "compute_slot", + slotKind: "array", + base, + baseDebug, + dest, + operationDebug, + }); + + export interface Field extends ComputeSlot.Base { + slotKind: "field"; + fieldOffset: number; // Byte offset from struct base + } + + export const isField = (inst: ComputeSlot): inst is ComputeSlot.Field => + inst.slotKind === "field"; + + export const field = ( + base: Value, + fieldOffset: number, + dest: string, + operationDebug: Instruction.Debug, + baseDebug?: Instruction.Debug, + ): ComputeSlot.Field => ({ + kind: "compute_slot", + slotKind: "field", + base, + baseDebug, + fieldOffset, + dest, + operationDebug, + }); + } + + export interface BinaryOp extends Instruction.Base { + kind: "binary"; + op: // Arithmetic + | "add" + | "sub" + | "mul" + | "div" + | "mod" + // Bitwise + | "shl" + | "shr" + // Comparison + | "eq" + | "ne" + | "lt" + | "le" + | "gt" + | "ge" + // Logical + | "and" + | "or"; + left: Value; + leftDebug?: Instruction.Debug; + right: Value; + rightDebug?: Instruction.Debug; + dest: string; + } + + export interface UnaryOp extends Instruction.Base { + kind: "unary"; + op: "not" | "neg"; + operand: Value; + operandDebug?: Instruction.Debug; + dest: string; + } + + export interface Env extends Instruction.Base { + kind: "env"; + op: + | "msg_sender" + | "msg_value" + | "msg_data" + | "block_number" + | "block_timestamp"; + + dest: string; + } + + export interface Hash extends Instruction.Base { + kind: "hash"; + value: Value; + valueDebug?: Instruction.Debug; + dest: string; + } + + export interface Cast extends Instruction.Base { + kind: "cast"; + value: Value; + valueDebug?: Instruction.Debug; + targetType: Type; + dest: string; + } + + // Call instruction removed - calls are now block terminators + + export interface Length extends Instruction.Base { + kind: "length"; + object: Value; + objectDebug?: Instruction.Debug; + dest: string; + } +} diff --git a/packages/bugc/src/ir/spec/module.ts b/packages/bugc/src/ir/spec/module.ts new file mode 100644 index 00000000..6d6abc3f --- /dev/null +++ b/packages/bugc/src/ir/spec/module.ts @@ -0,0 +1,22 @@ +import type * as Ast from "#ast"; +import type * as Format from "@ethdebug/format"; + +import type { Function as IrFunction } from "./function.js"; + +/** + * Top-level Ir module representing a complete BUG program + */ +export interface Module { + /** Program name from 'name' declaration */ + name: string; + /** User-defined functions */ + functions: Map; + /** Constructor function (optional, for contract creation) */ + create?: IrFunction; + /** The main code function (runtime code) */ + main: IrFunction; + /** Source location of the program */ + loc?: Ast.SourceLocation; + /** Program-level debug context (storage variables, etc.) */ + debugContext?: Format.Program.Context; +} diff --git a/packages/bugc/src/ir/spec/type.ts b/packages/bugc/src/ir/spec/type.ts new file mode 100644 index 00000000..7307a501 --- /dev/null +++ b/packages/bugc/src/ir/spec/type.ts @@ -0,0 +1,261 @@ +/** + * IR Type System + * + * A minimal type system that accurately represents EVM semantics. + * + * Core principles: + * 1. The EVM stack only holds 32-byte values + * 2. Dynamic data lives in memory/storage/calldata and is accessed via references + * 3. All types are either scalar values or references to data + * 4. Type safety comes from knowing data size and location + * 5. All types track their origin from the Bug type system + */ + +import { Type as BugType } from "#types"; + +/** + * The complete IR type system + */ +export type Type = Type.Scalar | Type.Ref; + +export namespace Type { + /** + * Base interface for all types + */ + export interface Base { + kind: string; + } + + export const isBase = (type: unknown): type is Type.Base => + typeof type === "object" && + !!type && + "kind" in type && + typeof type.kind === "string"; + + /** + * Origin type - where this IR type came from + */ + export type Origin = BugType | "synthetic"; + + /** + * Scalar types - raw byte values on the stack + * Size ranges from 1 to 32 bytes + */ + export interface Scalar extends Type.Base { + kind: "scalar"; + size: + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25 + | 26 + | 27 + | 28 + | 29 + | 30 + | 31 + | 32; + origin: Type.Origin; + } + + /** + * Create a scalar type of the specified size + */ + export const scalar = ( + size: Type.Scalar["size"], + origin: Type.Origin, + ): Type.Scalar => ({ + kind: "scalar", + size, + origin, + }); + + export const isScalar = (type: Type.Base): type is Type.Scalar => + type.kind === "scalar" && + "size" in type && + typeof (type as Type.Scalar).size === "number" && + (type as Type.Scalar).size >= 1 && + (type as Type.Scalar).size <= 32 && + "origin" in type; + + export namespace Scalar { + /** + * Export the size type for external use + */ + export type Size = Type.Scalar["size"]; + + /** + * Create synthetic scalar types (for IR-generated values) + */ + export const synthetic = (size: Size): Type.Scalar => + Type.scalar(size, "synthetic"); + + /** + * Common synthetic scalar types + */ + export const bytes1 = synthetic(1); + export const bytes4 = synthetic(4); + export const bytes8 = synthetic(8); + export const bytes20 = synthetic(20); // address size + export const bytes32 = synthetic(32); // word size + + /** + * Semantic aliases for synthetic types + */ + export const uint8 = bytes1; + export const uint32 = bytes4; + export const uint64 = bytes8; + export const uint256 = bytes32; + export const int8 = bytes1; + export const int256 = bytes32; + export const address = bytes20; + export const bool = bytes1; + export const word = bytes32; + + /** + * Check equality + */ + export const equals = (a: Type.Scalar, b: Type.Scalar): boolean => + a.size === b.size && a.origin === b.origin; // Note: this is reference equality for Bug types + + /** + * Format for display + */ + export const format = (type: Type.Scalar): string => { + const sizeStr = `scalar${type.size}`; + if (type.origin === "synthetic") { + return sizeStr; + } + // Could format with Bug type info if desired + return `${sizeStr}[${BugType.format(type.origin)}]`; + }; + } + + /** + * Reference types - pointers to data in various locations + */ + export interface Ref extends Type.Base { + kind: "ref"; + location: Type.Ref.Location; + origin: Type.Origin; + } + + /** + * Create a reference type + */ + export const ref = ( + location: Type.Ref.Location, + origin: Type.Origin, + ): Type.Ref => ({ + kind: "ref", + location, + origin, + }); + + export const isRef = (type: Type.Base): type is Type.Ref => + type.kind === "ref" && + "location" in type && + typeof (type as Type.Ref).location === "string" && + ["memory", "storage", "calldata", "returndata", "transient"].includes( + (type as Type.Ref).location, + ) && + "origin" in type; + + export namespace Ref { + /** + * Where the referenced data lives + */ + export type Location = + | "memory" + | "storage" + | "calldata" + | "returndata" + | "transient"; + + /** + * Create synthetic reference types (for IR-generated references) + */ + export const synthetic = (location: Type.Ref.Location): Type.Ref => + Type.ref(location, "synthetic"); + + /** + * Common synthetic reference types + */ + export const memory = () => synthetic("memory"); + export const storage = () => synthetic("storage"); + export const calldata = () => synthetic("calldata"); + export const returndata = () => synthetic("returndata"); + export const transient = () => synthetic("transient"); + + /** + * Check equality + */ + export const equals = (a: Type.Ref, b: Type.Ref): boolean => + a.location === b.location && a.origin === b.origin; // Note: this is reference equality for Bug types + + /** + * Format for display + */ + export const format = (type: Type.Ref): string => { + const locStr = `ref<${type.location}>`; + if (type.origin === "synthetic") { + return locStr; + } + // Include Bug type info + return `${locStr}[${BugType.format(type.origin)}]`; + }; + } + + /** + * Type equality check + */ + export const equals = (a: Type, b: Type): boolean => { + if (a.kind !== b.kind) return false; + + if (Type.isScalar(a) && Type.isScalar(b)) { + return Type.Scalar.equals(a, b); + } + + if (Type.isRef(a) && Type.isRef(b)) { + return Type.Ref.equals(a, b); + } + + return false; + }; + + /** + * Format type for display + */ + export const format = (type: Type): string => { + if (Type.isScalar(type)) { + return Type.Scalar.format(type); + } + + if (Type.isRef(type)) { + return Type.Ref.format(type); + } + + return "unknown"; + }; +} diff --git a/packages/bugc/src/ir/spec/value.ts b/packages/bugc/src/ir/spec/value.ts new file mode 100644 index 00000000..cfa9c79c --- /dev/null +++ b/packages/bugc/src/ir/spec/value.ts @@ -0,0 +1,45 @@ +import type { Type } from "./type.js"; +import type * as Format from "@ethdebug/format"; + +/** + * Debug context for values (operands) + */ +export interface ValueDebug { + context?: Format.Program.Context; +} + +/** + * Ir value - either a constant or a reference to a temporary + * + * Each value can now carry its own debug context to track where the value + * originated in the source code. This enables sub-instruction level debug + * tracking for optimizer passes. + */ +export type Value = + | { + kind: "const"; + value: bigint | string | boolean; + type: Type; + debug?: ValueDebug; + } + | { kind: "temp"; id: string; type: Type; debug?: ValueDebug }; + +export namespace Value { + /** + * Helper to create temporary value references + */ + export function temp(id: string, type: Type, debug?: ValueDebug): Value { + return { kind: "temp", id, type, debug }; + } + + /** + * Helper to create constant values + */ + export function constant( + value: bigint | string | boolean, + type: Type, + debug?: ValueDebug, + ): Value { + return { kind: "const", value, type, debug }; + } +} diff --git a/packages/bugc/src/ir/utils/debug.ts b/packages/bugc/src/ir/utils/debug.ts new file mode 100644 index 00000000..5feb48b9 --- /dev/null +++ b/packages/bugc/src/ir/utils/debug.ts @@ -0,0 +1,352 @@ +import type * as Ir from "#ir"; +import type * as Format from "@ethdebug/format"; + +/** + * Combine multiple debug contexts into a single context. + * If multiple contexts have source information, creates a pick context. + * Filters out empty contexts. + */ +export function combineDebugContexts( + ...debugs: (Ir.Instruction.Debug | Ir.Block.Debug | undefined)[] +): Ir.Instruction.Debug { + // Filter out undefined and empty debug objects + const contexts = debugs + .filter((d): d is Ir.Instruction.Debug | Ir.Block.Debug => d !== undefined) + .map((d) => d.context) + .filter((c): c is Format.Program.Context => c !== undefined); + + if (contexts.length === 0) { + return {}; + } + + // Flatten pick contexts - if a context has a pick, extract its children + const flattenedContexts: Format.Program.Context[] = []; + for (const context of contexts) { + if ("pick" in context && Array.isArray(context.pick)) { + flattenedContexts.push(...context.pick); + } else { + flattenedContexts.push(context); + } + } + + // Deduplicate contexts by checking structural equality + const uniqueContexts: Format.Program.Context[] = []; + const contextStrings = new Set(); + + for (const context of flattenedContexts) { + // Create a string representation for comparison + // We need to handle the structure carefully since it might have nested objects + const contextStr = JSON.stringify(context, (_key, value) => { + // Sort object keys to ensure consistent stringification + if (value && typeof value === "object" && !Array.isArray(value)) { + return Object.keys(value) + .sort() + .reduce( + (sorted, key) => { + sorted[key] = value[key]; + return sorted; + }, + {} as Record, + ); + } + return value; + }); + + if (!contextStrings.has(contextStr)) { + contextStrings.add(contextStr); + uniqueContexts.push(context); + } + } + + if (uniqueContexts.length === 0) { + return {}; + } + + if (uniqueContexts.length === 1) { + return { context: uniqueContexts[0] }; + } + + // Multiple unique contexts - create a pick context + return { + context: { + pick: uniqueContexts, + } as Format.Program.Context, + }; +} + +/** + * Preserve debug context from the original instruction when creating a replacement. + * Optionally combine with additional debug contexts. + * + * Works with both old-style (debug field) and new-style (operationDebug field). + */ +export function preserveDebug( + original: + | { debug?: Ir.Instruction.Debug | Ir.Block.Debug } + | { operationDebug?: Ir.Instruction.Debug | Ir.Block.Debug } + | Ir.Instruction + | Ir.Block.Phi + | Ir.Block.Terminator, + ...additional: (Ir.Instruction.Debug | Ir.Block.Debug | undefined)[] +): Ir.Instruction.Debug { + // Extract debug context from various possible locations + let debugContext: Ir.Instruction.Debug | Ir.Block.Debug | undefined; + + if ("operationDebug" in original) { + debugContext = original.operationDebug; + } else if ("debug" in original) { + debugContext = original.debug; + } + + return combineDebugContexts(debugContext, ...additional); +} + +/** + * Extract contexts from debug objects for transformation tracking. + * Works with both old-style (debug) and new-style (operationDebug) fields. + */ +export function extractContexts( + ...items: ( + | { debug?: Ir.Instruction.Debug | Ir.Block.Debug } + | { operationDebug?: Ir.Instruction.Debug | Ir.Block.Debug } + | Ir.Instruction + | Ir.Block.Phi + | Ir.Block.Terminator + | undefined + )[] +): Format.Program.Context[] { + const contexts: Format.Program.Context[] = []; + + for (const item of items) { + if (!item) continue; + + let debugContext: Ir.Instruction.Debug | Ir.Block.Debug | undefined; + + if ("operationDebug" in item) { + debugContext = item.operationDebug; + } else if ("debug" in item) { + debugContext = item.debug; + } + + if (debugContext?.context) { + contexts.push(debugContext.context); + } + } + + return contexts; +} + +/** + * Combine operation debug with field/operand debug contexts. + * This is used to create a comprehensive debug context for EVM emission + * that preserves all information from sub-instruction level tracking. + * + * The result combines: + * - The operation's debug context + * - Debug contexts from all operands/fields + * + * Uses pick contexts to preserve all distinct debug information. + */ +export function combineSubInstructionContexts( + operationDebug: Ir.Instruction.Debug | Ir.Block.Debug | undefined, + fieldsDebug: Record< + string, + Ir.Instruction.Debug | Ir.Block.Debug | Ir.ValueDebug | undefined + >, +): Ir.Instruction.Debug { + const allDebugContexts: ( + | Ir.Instruction.Debug + | Ir.Block.Debug + | Ir.ValueDebug + | undefined + )[] = [operationDebug, ...Object.values(fieldsDebug)]; + + return combineDebugContexts(...allDebugContexts); +} + +/** + * Extract all debug contexts from an instruction, including operation + * and all field/operand contexts. + * + * This is useful for transformation tracking in optimizer passes. + */ +export function extractSubInstructionContexts( + instruction: Ir.Instruction, +): Format.Program.Context[] { + const contexts: Format.Program.Context[] = []; + + // Add operation debug + if (instruction.operationDebug?.context) { + contexts.push(instruction.operationDebug.context); + } + + // Add field-specific debug contexts based on instruction kind + switch (instruction.kind) { + case "binary": + if (instruction.leftDebug?.context) { + contexts.push(instruction.leftDebug.context); + } + if (instruction.rightDebug?.context) { + contexts.push(instruction.rightDebug.context); + } + break; + + case "unary": + if (instruction.operandDebug?.context) { + contexts.push(instruction.operandDebug.context); + } + break; + + case "read": + case "write": + if (instruction.slotDebug?.context) { + contexts.push(instruction.slotDebug.context); + } + if (instruction.offsetDebug?.context) { + contexts.push(instruction.offsetDebug.context); + } + if (instruction.lengthDebug?.context) { + contexts.push(instruction.lengthDebug.context); + } + if (instruction.kind === "write" && instruction.valueDebug?.context) { + contexts.push(instruction.valueDebug.context); + } + break; + + case "compute_offset": + if (instruction.baseDebug?.context) { + contexts.push(instruction.baseDebug.context); + } + if ( + instruction.offsetKind === "array" && + instruction.indexDebug?.context + ) { + contexts.push(instruction.indexDebug.context); + } + if ( + instruction.offsetKind === "byte" && + instruction.offsetDebug?.context + ) { + contexts.push(instruction.offsetDebug.context); + } + break; + + case "compute_slot": + if (instruction.baseDebug?.context) { + contexts.push(instruction.baseDebug.context); + } + if (instruction.slotKind === "mapping" && instruction.keyDebug?.context) { + contexts.push(instruction.keyDebug.context); + } + break; + + case "const": + if (instruction.valueDebug?.context) { + contexts.push(instruction.valueDebug.context); + } + break; + + case "allocate": + if (instruction.sizeDebug?.context) { + contexts.push(instruction.sizeDebug.context); + } + break; + + case "hash": + case "cast": + if (instruction.valueDebug?.context) { + contexts.push(instruction.valueDebug.context); + } + break; + + case "length": + if (instruction.objectDebug?.context) { + contexts.push(instruction.objectDebug.context); + } + break; + + // env has no operands + case "env": + break; + } + + // Also check if operands have their own debug contexts + // (from the Value.debug field) + const addValueDebug = (value?: Ir.Value) => { + if (value?.debug?.context) { + contexts.push(value.debug.context); + } + }; + + switch (instruction.kind) { + case "binary": + addValueDebug(instruction.left); + addValueDebug(instruction.right); + break; + case "unary": + addValueDebug(instruction.operand); + break; + case "read": + addValueDebug(instruction.slot); + addValueDebug(instruction.offset); + addValueDebug(instruction.length); + break; + case "write": + addValueDebug(instruction.slot); + addValueDebug(instruction.offset); + addValueDebug(instruction.length); + addValueDebug(instruction.value); + break; + case "compute_offset": + addValueDebug(instruction.base); + if (instruction.offsetKind === "array") { + addValueDebug(instruction.index); + } + if (instruction.offsetKind === "byte") { + addValueDebug(instruction.offset); + } + break; + case "compute_slot": + addValueDebug(instruction.base); + if (instruction.slotKind === "mapping") { + addValueDebug(instruction.key); + } + break; + case "allocate": + addValueDebug(instruction.size); + break; + case "hash": + case "cast": + addValueDebug(instruction.value); + break; + case "length": + addValueDebug(instruction.object); + break; + } + + return contexts; +} + +/** + * Preserve debug context from the original instruction when creating a + * replacement, taking into account sub-instruction level debug. + * + * This extracts all debug contexts (operation + fields + value debugs) + * from the original instruction and combines them with any additional + * debug contexts provided. + */ +export function preserveSubInstructionDebug( + original: Ir.Instruction, + ...additional: (Ir.Instruction.Debug | Ir.Block.Debug | undefined)[] +): Ir.Instruction.Debug { + const originalContexts = extractSubInstructionContexts(original); + const additionalContexts = additional + .filter((d): d is Ir.Instruction.Debug | Ir.Block.Debug => d !== undefined) + .map((d) => d.context) + .filter((c): c is Format.Program.Context => c !== undefined); + + return combineDebugContexts( + ...originalContexts.map((c) => ({ context: c })), + ...additionalContexts.map((c) => ({ context: c })), + ); +} diff --git a/packages/bugc/src/ir/utils/index.ts b/packages/bugc/src/ir/utils/index.ts new file mode 100644 index 00000000..8144839c --- /dev/null +++ b/packages/bugc/src/ir/utils/index.ts @@ -0,0 +1 @@ +export * from "./debug.js"; diff --git a/packages/bugc/src/irgen/create-block.test.ts b/packages/bugc/src/irgen/create-block.test.ts new file mode 100644 index 00000000..be467746 --- /dev/null +++ b/packages/bugc/src/irgen/create-block.test.ts @@ -0,0 +1,196 @@ +import { describe, it, expect } from "vitest"; +import { parse } from "#parser"; +import * as TypeChecker from "#typechecker"; +import { generateModule } from "./generator.js"; + +describe("IR generation for create blocks", () => { + it("generates separate IR for create and main functions", () => { + const code = ` + name Token; + + storage { + [0] totalSupply: uint256; + [1] owner: address; + } + + create { + totalSupply = 1000000; + owner = msg.sender; + } + + code { + let supply = totalSupply; + } + `; + + const parseResult = parse(code); + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeResult = TypeChecker.checkProgram(parseResult.value); + expect(typeResult.success).toBe(true); + if (!typeResult.success) return; + + const irResult = generateModule(parseResult.value, typeResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + + // Check that both functions exist + expect(module.create).toBeDefined(); + expect(module.main).toBeDefined(); + + // Check create function + expect(module.create!.name).toBe("create"); + expect(module.create!.blocks.size).toBeGreaterThan(0); + + const createEntry = module.create!.blocks.get("entry"); + expect(createEntry).toBeDefined(); + expect(createEntry!.instructions.length).toBeGreaterThan(0); + + // Should have write storage instructions for totalSupply and owner + const storeInstructions = createEntry!.instructions.filter( + (inst) => inst.kind === "write" && inst.location === "storage", + ); + expect(storeInstructions).toHaveLength(2); + + // Check main function + expect(module.main.name).toBe("main"); + const mainEntry = module.main.blocks.get("entry"); + expect(mainEntry).toBeDefined(); + + // Should have read storage for totalSupply + const loadInstructions = mainEntry!.instructions.filter( + (inst) => inst.kind === "read" && inst.location === "storage", + ); + expect(loadInstructions).toHaveLength(1); + }); + + it("handles empty create block", () => { + const code = ` + name EmptyCreate; + + create { + // No initialization + } + + code { + let x = 42; + } + `; + + const parseResult = parse(code); + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeResult = TypeChecker.checkProgram(parseResult.value); + expect(typeResult.success).toBe(true); + if (!typeResult.success) return; + + const irResult = generateModule(parseResult.value, typeResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + + // Empty create block doesn't generate a create function + expect(module.create).toBeUndefined(); + }); + + it("handles control flow in create block", () => { + const code = ` + name ControlFlowCreate; + + storage { + [0] initialized: bool; + [1] value: uint256; + } + + create { + if (msg.value > 0) { + value = msg.value; + initialized = true; + } else { + value = 1000; + initialized = false; + } + } + + code { + let v = value; + } + `; + + const parseResult = parse(code); + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeResult = TypeChecker.checkProgram(parseResult.value); + expect(typeResult.success).toBe(true); + if (!typeResult.success) return; + + const irResult = generateModule(parseResult.value, typeResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + + // Create function should have multiple blocks for if/else + expect(module.create!.blocks.size).toBeGreaterThan(1); + + // Should have conditional branch + const entryBlock = module.create!.blocks.get("entry")!; + expect(entryBlock.terminator.kind).toBe("branch"); + }); + + it("maintains separate local variables for create and main", () => { + const code = ` + name SeparateLocals; + + create { + let x = 100; + let y = 200; + } + + code { + let x = 300; // Different x than in create + let z = 400; + } + `; + + const parseResult = parse(code); + expect(parseResult.success).toBe(true); + if (!parseResult.success) return; + + const typeResult = TypeChecker.checkProgram(parseResult.value); + expect(typeResult.success).toBe(true); + if (!typeResult.success) return; + + const irResult = generateModule(parseResult.value, typeResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) return; + + const module = irResult.value; + + // Create function should have assignments for x and y + const createEntry = module.create!.blocks.get("entry")!; + const createConsts = createEntry.instructions.filter( + (inst) => inst.kind === "const", + ); + // Variables are now tracked as temps with SSA - should have const 100 and const 200 + expect(createConsts.length).toBe(2); + expect(createConsts[0]).toMatchObject({ kind: "const", value: 100n }); + expect(createConsts[1]).toMatchObject({ kind: "const", value: 200n }); + + // Main function should have assignments for x and z + const mainEntry = module.main.blocks.get("entry")!; + const mainConsts = mainEntry.instructions.filter( + (inst) => inst.kind === "const", + ); + // Should have const 300 and const 400 + expect(mainConsts.length).toBe(2); + expect(mainConsts[0]).toMatchObject({ kind: "const", value: 300n }); + expect(mainConsts[1]).toMatchObject({ kind: "const", value: 400n }); + }); +}); diff --git a/packages/bugc/src/irgen/debug/pointers.ts b/packages/bugc/src/irgen/debug/pointers.ts new file mode 100644 index 00000000..3b015d1a --- /dev/null +++ b/packages/bugc/src/irgen/debug/pointers.ts @@ -0,0 +1,239 @@ +/** + * Pointer generation utilities for ethdebug/format integration + * + * Converts runtime variable locations to ethdebug/format pointer + * expressions + */ +import * as Format from "@ethdebug/format"; + +import type * as Ir from "#ir/spec"; + +import type { ComputeSlotChain } from "./storage-analysis.js"; + +/** + * Variable location information + */ +export type VariableLocation = + | { kind: "storage"; slot: number | bigint } + | { kind: "storage-computed"; expression: Format.Pointer.Expression } + | { + kind: "memory"; + offset: number | bigint; + length: number | bigint; + } + | { + kind: "memory-computed"; + offsetExpression: Format.Pointer.Expression; + lengthExpression: Format.Pointer.Expression; + } + | { + kind: "calldata"; + offset: number | bigint; + length: number | bigint; + } + | { kind: "stack"; slot: number } + | { kind: "transient"; slot: number | bigint } + | { kind: "unknown" }; + +/** + * Generate an ethdebug/format pointer for a variable location + */ +export function generatePointer( + location: VariableLocation, +): Format.Pointer | undefined { + switch (location.kind) { + case "storage": + return { + location: "storage", + slot: Number(location.slot), + }; + + case "storage-computed": + return { + location: "storage", + slot: location.expression, + }; + + case "memory": + return { + location: "memory", + offset: Number(location.offset), + length: Number(location.length), + }; + + case "memory-computed": + return { + location: "memory", + offset: location.offsetExpression, + length: location.lengthExpression, + }; + + case "calldata": + return { + location: "calldata", + offset: Number(location.offset), + length: Number(location.length), + }; + + case "transient": + return { + location: "transient", + slot: Number(location.slot), + }; + + case "stack": + // Stack-based SSA temps don't have concrete runtime locations yet + // at IR generation time. They only get stack positions during + // EVM code generation. So we can't generate pointers for them here. + return undefined; + + case "unknown": + return undefined; + } +} + +/** + * Translate a compute_slot chain to an ethdebug/format pointer expression + * + * Takes a chain of compute_slot instructions and generates the corresponding + * pointer expression using $keccak256, $sum, and other operations. + * + * SAFETY: Handles unknown patterns gracefully by returning simple expressions. + * Never crashes - just returns best-effort pointer. + * + * @param chain The compute_slot chain from storage-analysis + * @returns A pointer expression (simple number or complex expression) + */ +export function translateComputeSlotChain( + chain: ComputeSlotChain, +): Format.Pointer.Expression { + // Start with base slot + if (chain.baseSlot === null) { + // Couldn't determine base - this shouldn't happen if chain is valid + // Return 0 as fallback + return 0; + } + + let expr: Format.Pointer.Expression = chain.baseSlot; + + // Process each step in the chain + for (const step of chain.steps) { + const inst = step.instruction; + + if (inst.slotKind === "mapping") { + // Mapping access: keccak256(wordsized(key), slot) + // Try to convert key to expression + const keyExpr = valueToExpression(step.key); + if (keyExpr !== null) { + expr = { + $keccak256: [{ $wordsized: keyExpr }, expr], + }; + } + // If we can't convert the key, skip this step (use current expr) + } else if (inst.slotKind === "array") { + // Array base: keccak256(slot) + // Note: actual element access is done with binary.add afterward + // which we don't see in the compute_slot chain + expr = { + $keccak256: [expr], + }; + } else if (inst.slotKind === "field") { + // Struct field: slot + fieldSlotOffset + const slotOffset = step.fieldSlotOffset ?? 0; + if (slotOffset > 0) { + expr = { + $sum: [expr, slotOffset], + }; + } + // If offset is 0, no change needed + } + // Unknown slotKind: skip (shouldn't happen) + } + + return expr; +} + +/** + * Convert an IR value to a pointer expression + * + * SAFETY: Returns null if we can't convert the value + * For now, only handles constants. Future: handle temp references. + */ +function valueToExpression( + value: Ir.Value | undefined, +): Format.Pointer.Expression | null { + if (!value) { + return null; + } + + // Handle constant values + if (value.kind === "const" && typeof value.value === "bigint") { + return Number(value.value); + } + + // Handle temp references + // For now, we can't represent these in pointer expressions + // Future: could use region references or symbolic names + if (value.kind === "temp") { + // Can't represent temp in pointer expression yet + return null; + } + + return null; +} + +/** + * Helper to create pointer expression for mapping access + * + * Generates: keccak256(concat(key, slot)) + */ +export function mappingAccess( + slot: number | Format.Pointer.Expression, + key: Format.Pointer.Expression, +): Format.Pointer.Expression { + return { + $keccak256: [{ $wordsized: key }, slot], + }; +} + +/** + * Helper to create pointer expression for array element access + * + * For dynamic arrays: slot for length, keccak256(slot) + index for elements + * For fixed arrays: slot + index + */ +export function arrayElementAccess( + baseSlot: number | Format.Pointer.Expression, + index: number | Format.Pointer.Expression, + isDynamic: boolean, +): Format.Pointer.Expression { + if (isDynamic) { + // Dynamic array: keccak256(slot) + index + return { + $sum: [{ $keccak256: [baseSlot] }, index], + }; + } else { + // Fixed array: slot + index + return { + $sum: [baseSlot, index], + }; + } +} + +/** + * Helper to create pointer expression for struct field access + * + * Generates: slot + fieldOffset + */ +export function structFieldAccess( + baseSlot: number | Format.Pointer.Expression, + fieldOffset: number, +): Format.Pointer.Expression { + if (fieldOffset === 0) { + return baseSlot; + } + + return { + $sum: [baseSlot, fieldOffset], + }; +} diff --git a/packages/bugc/src/irgen/debug/storage-analysis.test.ts b/packages/bugc/src/irgen/debug/storage-analysis.test.ts new file mode 100644 index 00000000..37c86f8a --- /dev/null +++ b/packages/bugc/src/irgen/debug/storage-analysis.test.ts @@ -0,0 +1,552 @@ +/** + * Unit tests for storage slot computation analysis + * + * Tests the chain walking and pointer generation for sophisticated + * storage access patterns (mappings, arrays, structs) + */ + +import { describe, it, expect } from "vitest"; +import * as Ir from "#ir"; +import * as Ast from "#ast"; +import { + findComputeSlotChain, + findBaseStorageVariable, + analyzeStorageSlot, + type ComputeSlotChain, +} from "./storage-analysis.js"; +import { translateComputeSlotChain } from "./pointers.js"; + +/** + * Helper to create a minimal IR block with compute_slot instructions + */ +function createBlockWithInstructions(instructions: Ir.Instruction[]): Ir.Block { + return { + id: "test_block", + phis: [], + instructions, + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }; +} + +/** + * Helper to create a compute_slot instruction for mapping access + */ +function createMappingSlot( + dest: string, + base: Ir.Value, + key: Ir.Value, +): Ir.Instruction.ComputeSlot { + return { + kind: "compute_slot", + dest, + slotKind: "mapping", + base, + key, + keyType: Ir.Type.Scalar.uint256, + operationDebug: {}, + }; +} + +/** + * Helper to create a compute_slot instruction for array access + */ +function createArraySlot( + dest: string, + base: Ir.Value, +): Ir.Instruction.ComputeSlot { + return { + kind: "compute_slot", + dest, + slotKind: "array", + base, + operationDebug: {}, + }; +} + +/** + * Helper to create a compute_slot instruction for struct field access + */ +function createFieldSlot( + dest: string, + base: Ir.Value, + fieldOffset: number, +): Ir.Instruction.ComputeSlot { + return { + kind: "compute_slot", + dest, + slotKind: "field", + base, + fieldOffset, + operationDebug: {}, + }; +} + +/** + * Helper to create a const value + */ +function constValue(value: number | bigint): Ir.Value { + return { + kind: "const", + value: typeof value === "number" ? BigInt(value) : value, + type: Ir.Type.Scalar.uint256, + }; +} + +/** + * Helper to create a temp reference + */ +function tempValue(id: string): Ir.Value { + return { + kind: "temp", + id, + type: Ir.Type.Scalar.uint256, + }; +} + +describe("storage-analysis", () => { + describe("findComputeSlotChain", () => { + it("should find simple mapping access chain", () => { + // balances[addr] where balances is at slot 0 + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(0), constValue(0x1234)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + expect(chain!.baseSlot).toBe(0); + expect(chain!.steps).toHaveLength(1); + expect(chain!.steps[0].instruction.slotKind).toBe("mapping"); + expect(chain!.steps[0].destTemp).toBe("t1"); + }); + + it("should find nested mapping access chain", () => { + // allowances[sender][spender] where allowances is at slot 1 + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(1), constValue(0xaaaa)), + createMappingSlot("t2", tempValue("t1"), constValue(0xbbbb)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t2"); + + expect(chain).not.toBeNull(); + expect(chain!.baseSlot).toBe(1); + expect(chain!.steps).toHaveLength(2); + expect(chain!.steps[0].instruction.slotKind).toBe("mapping"); + expect(chain!.steps[1].instruction.slotKind).toBe("mapping"); + }); + + it("should find array element access chain", () => { + // items[5] where items is at slot 2 + const instructions: Ir.Instruction[] = [ + createArraySlot("t1", constValue(2)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + expect(chain!.baseSlot).toBe(2); + expect(chain!.steps).toHaveLength(1); + expect(chain!.steps[0].instruction.slotKind).toBe("array"); + }); + + it("should find struct field access chain", () => { + // account.balance where account is at slot 3, balance at offset 32 + const instructions: Ir.Instruction[] = [ + createFieldSlot("t1", constValue(3), 32), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + expect(chain!.baseSlot).toBe(3); + expect(chain!.steps).toHaveLength(1); + expect(chain!.steps[0].instruction.slotKind).toBe("field"); + expect(chain!.steps[0].fieldSlotOffset).toBe(1); // 32 bytes / 32 + }); + + it("should find complex nested access chain", () => { + // accounts[user].balance where accounts is at slot 4 + // First: mapping access for accounts[user] + // Then: field access for .balance (offset 64) + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(4), constValue(0xabcd)), + createFieldSlot("t2", tempValue("t1"), 64), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t2"); + + expect(chain).not.toBeNull(); + expect(chain!.baseSlot).toBe(4); + expect(chain!.steps).toHaveLength(2); + expect(chain!.steps[0].instruction.slotKind).toBe("mapping"); + expect(chain!.steps[1].instruction.slotKind).toBe("field"); + expect(chain!.steps[1].fieldSlotOffset).toBe(2); // 64 bytes / 32 + }); + + it("should return null if temp not found", () => { + const block = createBlockWithInstructions([]); + const chain = findComputeSlotChain(block, "nonexistent"); + + expect(chain).toBeNull(); + }); + + it("should return null if temp is not from compute_slot", () => { + // Create a different instruction kind that produces a temp + const instructions: Ir.Instruction[] = [ + { + kind: "binary", + dest: "t1", + op: "add", + left: constValue(1), + right: constValue(2), + operationDebug: {}, + }, + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + // Should return chain with no steps and null baseSlot + // because the temp comes from binary.add, not compute_slot + expect(chain).not.toBeNull(); + expect(chain!.steps).toHaveLength(0); + expect(chain!.baseSlot).toBeNull(); + }); + + it("should handle field offset = 0", () => { + // First field in a struct (offset 0) + const instructions: Ir.Instruction[] = [ + createFieldSlot("t1", constValue(5), 0), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + expect(chain!.steps[0].fieldSlotOffset).toBe(0); + }); + + it("should limit chain depth", () => { + // Create a chain longer than MAX_CHAIN_DEPTH (10) + const instructions: Ir.Instruction[] = []; + for (let i = 0; i < 15; i++) { + const base = i === 0 ? constValue(0) : tempValue(`t${i}`); + instructions.push(createMappingSlot(`t${i + 1}`, base, constValue(i))); + } + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t15"); + + // Should stop at MAX_CHAIN_DEPTH + expect(chain).not.toBeNull(); + expect(chain!.steps.length).toBeLessThanOrEqual(10); + }); + }); + + describe("findBaseStorageVariable", () => { + it("should find storage variable by slot", () => { + const storageDecls: Ast.Declaration.Storage[] = [ + { + kind: "declaration:storage", + id: "balances_decl", + name: "balances", + slot: 0, + type: {} as Ast.Type, + loc: null, + }, + { + kind: "declaration:storage", + id: "allowances_decl", + name: "allowances", + slot: 1, + type: {} as Ast.Type, + loc: null, + }, + ]; + + const chain: ComputeSlotChain = { + baseSlot: 0, + baseVariableName: null, + steps: [], + }; + + const result = findBaseStorageVariable(chain, storageDecls); + + expect(result).not.toBeNull(); + expect(result!.name).toBe("balances"); + expect(result!.slot).toBe(0); + }); + + it("should return null if slot not found", () => { + const storageDecls: Ast.Declaration.Storage[] = [ + { + kind: "declaration:storage", + id: "balances_decl", + name: "balances", + slot: 0, + type: {} as Ast.Type, + loc: null, + }, + ]; + + const chain: ComputeSlotChain = { + baseSlot: 99, + baseVariableName: null, + steps: [], + }; + + const result = findBaseStorageVariable(chain, storageDecls); + + expect(result).toBeNull(); + }); + + it("should return null if baseSlot is null", () => { + const storageDecls: Ast.Declaration.Storage[] = []; + + const chain: ComputeSlotChain = { + baseSlot: null, + baseVariableName: null, + steps: [], + }; + + const result = findBaseStorageVariable(chain, storageDecls); + + expect(result).toBeNull(); + }); + }); + + describe("analyzeStorageSlot", () => { + it("should analyze direct constant slot access", () => { + const storageDecls: Ast.Declaration.Storage[] = [ + { + kind: "declaration:storage", + id: "owner_decl", + name: "owner", + slot: 0, + type: {} as Ast.Type, + loc: null, + }, + ]; + + const block = createBlockWithInstructions([]); + const slotValue = constValue(0); + + const result = analyzeStorageSlot(slotValue, block, storageDecls); + + expect(result).not.toBeNull(); + expect(result!.variableName).toBe("owner"); + expect(result!.pointer).toMatchObject({ + location: "storage", + slot: 0, + }); + expect(result!.chain).toBeUndefined(); + }); + + it("should analyze computed slot access", () => { + const storageDecls: Ast.Declaration.Storage[] = [ + { + kind: "declaration:storage", + id: "balances_decl", + name: "balances", + slot: 0, + type: {} as Ast.Type, + loc: null, + }, + ]; + + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(0), constValue(0x1234)), + ]; + + const block = createBlockWithInstructions(instructions); + const slotValue = tempValue("t1"); + + const result = analyzeStorageSlot(slotValue, block, storageDecls); + + expect(result).not.toBeNull(); + expect(result!.variableName).toBe("balances"); + expect(result!.pointer).toMatchObject({ + location: "storage", + }); + expect(result!.chain).toBeDefined(); + expect(result!.chain!.steps).toHaveLength(1); + }); + + it("should return null for unknown storage slot", () => { + const storageDecls: Ast.Declaration.Storage[] = []; + + const block = createBlockWithInstructions([]); + const slotValue = constValue(99); + + const result = analyzeStorageSlot(slotValue, block, storageDecls); + + expect(result).toBeNull(); + }); + + it("should return null if chain cannot be analyzed", () => { + const storageDecls: Ast.Declaration.Storage[] = [ + { + kind: "declaration:storage", + id: "balances_decl", + name: "balances", + slot: 0, + type: {} as Ast.Type, + loc: null, + }, + ]; + + const block = createBlockWithInstructions([]); + const slotValue = tempValue("nonexistent"); + + const result = analyzeStorageSlot(slotValue, block, storageDecls); + + expect(result).toBeNull(); + }); + }); + + describe("translateComputeSlotChain (integration)", () => { + it("should generate pointer for simple mapping", () => { + // balances[0x1234] + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(0), constValue(0x1234)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be: keccak256(wordsized(0x1234), 0) + expect(pointer).toEqual({ + $keccak256: [{ $wordsized: 0x1234 }, 0], + }); + }); + + it("should generate pointer for nested mapping", () => { + // allowances[0xaaaa][0xbbbb] + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(1), constValue(0xaaaa)), + createMappingSlot("t2", tempValue("t1"), constValue(0xbbbb)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t2"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be: keccak256(wordsized(0xbbbb), keccak256(wordsized(0xaaaa), 1)) + expect(pointer).toEqual({ + $keccak256: [ + { $wordsized: 0xbbbb }, + { + $keccak256: [{ $wordsized: 0xaaaa }, 1], + }, + ], + }); + }); + + it("should generate pointer for array base", () => { + // items (dynamic array base) + const instructions: Ir.Instruction[] = [ + createArraySlot("t1", constValue(2)), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be: keccak256(2) + expect(pointer).toEqual({ + $keccak256: [2], + }); + }); + + it("should generate pointer for struct field", () => { + // account.balance (field at offset 32 = slot offset 1) + const instructions: Ir.Instruction[] = [ + createFieldSlot("t1", constValue(3), 32), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be: sum(3, 1) + expect(pointer).toEqual({ + $sum: [3, 1], + }); + }); + + it("should generate pointer for complex nested access", () => { + // accounts[0xuser].balance (offset 64 = slot offset 2) + const instructions: Ir.Instruction[] = [ + createMappingSlot("t1", constValue(4), constValue(0xaaaa)), + createFieldSlot("t2", tempValue("t1"), 64), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t2"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be: sum(keccak256(wordsized(0xaaaa), 4), 2) + expect(pointer).toEqual({ + $sum: [ + { + $keccak256: [{ $wordsized: 0xaaaa }, 4], + }, + 2, + ], + }); + }); + + it("should handle field offset of 0", () => { + // First field - no offset needed + const instructions: Ir.Instruction[] = [ + createFieldSlot("t1", constValue(5), 0), + ]; + + const block = createBlockWithInstructions(instructions); + const chain = findComputeSlotChain(block, "t1"); + + expect(chain).not.toBeNull(); + + const pointer = translateComputeSlotChain(chain!); + + // Should be just the base slot (no sum) + expect(pointer).toBe(5); + }); + + it("should return 0 for chain with null baseSlot", () => { + const chain: ComputeSlotChain = { + baseSlot: null, + baseVariableName: null, + steps: [], + }; + + const pointer = translateComputeSlotChain(chain); + + expect(pointer).toBe(0); + }); + }); +}); diff --git a/packages/bugc/src/irgen/debug/storage-analysis.ts b/packages/bugc/src/irgen/debug/storage-analysis.ts new file mode 100644 index 00000000..7849e7df --- /dev/null +++ b/packages/bugc/src/irgen/debug/storage-analysis.ts @@ -0,0 +1,258 @@ +/** + * Storage slot computation analysis for ethdebug/format integration + * + * Analyzes IR instruction chains to reconstruct storage slot computations + * and generate accurate pointer expressions. + * + * SAFETY: This module uses defensive programming patterns throughout: + * - Returns null on any uncertainty rather than guessing + * - Validates all lookups before use + * - Handles incomplete/unexpected IR patterns gracefully + * - Never crashes - just skips what we can't analyze + */ + +import * as Ir from "#ir"; +import * as Ast from "#ast"; +import type * as Format from "@ethdebug/format"; + +/** + * A compute_slot instruction in a chain, with its position and semantics + */ +export interface ComputeSlotStep { + /** The instruction itself */ + instruction: Ir.Instruction.ComputeSlot; + + /** The temp this instruction produces */ + destTemp: string; + + /** For mapping access: the key being hashed */ + key?: Ir.Value; + + /** For struct field: the slot offset */ + fieldSlotOffset?: number; +} + +/** + * A complete chain of compute_slot operations + */ +export interface ComputeSlotChain { + /** The base slot number (if we can determine it) */ + baseSlot: number | null; + + /** The base storage variable name (if found) */ + baseVariableName: string | null; + + /** The sequence of compute_slot steps */ + steps: ComputeSlotStep[]; +} + +/** + * Find the compute_slot instruction chain that produces a given temp + * + * Walks backward through the block's instructions to trace how a temp + * was computed from storage slot operations. + * + * SAFETY: Only traces within the current block. Returns null if: + * - The temp doesn't exist + * - The temp wasn't produced by compute_slot + * - The chain spans multiple blocks (not yet supported) + * - Any step in the chain is ambiguous + * + * @param block The block to search in + * @param slotTemp The temp ID to trace back from + * @returns The complete chain, or null if we can't analyze it + */ +export function findComputeSlotChain( + block: Ir.Block, + slotTemp: string, +): ComputeSlotChain | null { + const steps: ComputeSlotStep[] = []; + let currentTemp = slotTemp; + let baseSlot: number | null = null; + + // Walk backward through the chain + // Safety: limit iterations to prevent infinite loops + const MAX_CHAIN_DEPTH = 10; + for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth++) { + // Find the instruction that produces currentTemp + const inst = block.instructions.find( + (i) => "dest" in i && i.dest === currentTemp, + ); + + if (!inst) { + // Temp not found in this block + // Could be from a predecessor block or phi node + // For now, we only handle single-block chains + return null; + } + + // Check if it's a compute_slot instruction + if (inst.kind !== "compute_slot") { + // The temp comes from some other operation + // This is the end of the chain + break; + } + + // TypeScript narrowing - we know it's ComputeSlot now + const computeSlotInst = inst as Ir.Instruction.ComputeSlot; + + // Extract step information based on slotKind + const step: ComputeSlotStep = { + instruction: computeSlotInst, + destTemp: computeSlotInst.dest, + }; + + if (Ir.Instruction.ComputeSlot.isMapping(computeSlotInst)) { + step.key = computeSlotInst.key; + } else if (Ir.Instruction.ComputeSlot.isField(computeSlotInst)) { + // Convert byte offset to slot offset + step.fieldSlotOffset = Math.floor(computeSlotInst.fieldOffset / 32); + } + // Array kind has no additional data needed + + steps.push(step); + + // Continue tracing from the base + if (computeSlotInst.base.kind === "const") { + // Found the base slot! + baseSlot = Number(computeSlotInst.base.value); + break; + } else if (computeSlotInst.base.kind === "temp") { + // Continue following the chain + currentTemp = computeSlotInst.base.id; + } else { + // Unexpected base value kind + return null; + } + } + + // Reverse steps to get forward order (base -> ... -> final) + steps.reverse(); + + return { + baseSlot, + baseVariableName: null, // Will be filled in by caller + steps, + }; +} + +/** + * Find the base storage variable for a compute_slot chain + * + * Matches the base slot number against storage declarations to find + * the variable name. + * + * SAFETY: Returns null if base slot is unknown or not found in declarations + * + * @param chain The compute_slot chain + * @param storageDecls Storage variable declarations + * @returns Storage info or null + */ +export function findBaseStorageVariable( + chain: ComputeSlotChain, + storageDecls: Ast.Declaration.Storage[], +): { slot: number; name: string; declaration: Ast.Declaration.Storage } | null { + if (chain.baseSlot === null) { + return null; + } + + const decl = storageDecls.find((d) => d.slot === chain.baseSlot); + if (!decl) { + return null; + } + + return { + slot: chain.baseSlot, + name: decl.name, + declaration: decl, + }; +} + +/** + * Information about a storage access we've successfully analyzed + */ +export interface StorageAccessInfo { + /** The base storage variable name */ + variableName: string; + + /** The storage variable declaration */ + declaration: Ast.Declaration.Storage; + + /** The pointer expression (simple or computed) */ + pointer: Format.Pointer; + + /** The access chain (if computed) */ + chain?: ComputeSlotChain; +} + +/** + * Analyze a storage slot value to determine what variable it accesses + * + * This is the main entry point for understanding storage accesses. + * Given a slot value (from a read/write instruction), determines: + * - What storage variable is being accessed + * - The pointer expression to reach that location + * + * SAFETY: Returns null for anything we can't confidently analyze + * + * @param slotValue The slot value from a read/write instruction + * @param block The current block + * @param storageDecls Storage variable declarations + * @returns Storage access info or null + */ +export function analyzeStorageSlot( + slotValue: Ir.Value, + block: Ir.Block, + storageDecls: Ast.Declaration.Storage[], +): StorageAccessInfo | null { + // Case 1: Direct constant slot (simple variable access) + if (slotValue.kind === "const") { + const slot = Number(slotValue.value); + const decl = storageDecls.find((d) => d.slot === slot); + + if (!decl) { + // Unknown storage slot + return null; + } + + return { + variableName: decl.name, + declaration: decl, + pointer: { + location: "storage", + slot, + }, + }; + } + + // Case 2: Computed slot (mapping/array/struct access) + if (slotValue.kind === "temp") { + const chain = findComputeSlotChain(block, slotValue.id); + if (!chain) { + // Can't analyze the chain + return null; + } + + const base = findBaseStorageVariable(chain, storageDecls); + if (!base) { + // Can't find base variable + return null; + } + + // We have a complete chain! But we need to translate it to a pointer + // For now, just return the simple base pointer + // The translation will be added in the next phase + return { + variableName: base.name, + declaration: base.declaration, + pointer: { + location: "storage", + slot: base.slot, + }, + chain, + }; + } + + // Unknown slot value kind + return null; +} diff --git a/packages/bugc/src/irgen/debug/types.ts b/packages/bugc/src/irgen/debug/types.ts new file mode 100644 index 00000000..9a9ace4d --- /dev/null +++ b/packages/bugc/src/irgen/debug/types.ts @@ -0,0 +1,203 @@ +/** + * Type conversion utilities for ethdebug/format integration + * + * Converts IR types to ethdebug/format type schemas for variable contexts + */ + +import * as Format from "@ethdebug/format"; +import * as Ir from "#ir"; +import { Type as BugType } from "#types"; + +/** + * Convert an IR type to an ethdebug/format type + * + * The IR type system is minimal (scalars and refs), but each IR type + * carries its origin from the Bug type system, which contains rich + * semantic information we can use to generate proper ethdebug types. + */ +export function convertToEthDebugType( + irType: Ir.Type, +): Format.Type | undefined { + // If we have a Bug type origin, use it for rich type information + if (irType.origin !== "synthetic" && BugType.isBase(irType.origin)) { + return convertBugType(irType.origin); + } + + // For synthetic types (IR-generated), infer from IR type structure + return convertSyntheticType(irType); +} + +/** + * Convert a Bug type to ethdebug/format type + * + * Bug types contain full semantic information about the original + * source language type + */ +export function convertBugType(bugType: BugType): Format.Type | undefined { + // Elementary types + if (BugType.isElementary(bugType)) { + return convertElementaryType(bugType); + } + + // Array types + if (BugType.isArray(bugType)) { + const elementType = convertBugType(bugType.element); + if (!elementType) { + return undefined; + } + + return { + kind: "array", + contains: { + type: elementType, + }, + ...(bugType.size !== undefined && { length: bugType.size }), + }; + } + + // Mapping types + if (BugType.isMapping(bugType)) { + const keyType = convertBugType(bugType.key); + const valueType = convertBugType(bugType.value); + + if (!keyType || !valueType) { + return undefined; + } + + return { + kind: "mapping", + contains: { + key: { type: keyType }, + value: { type: valueType }, + }, + }; + } + + // Struct types + if (BugType.isStruct(bugType)) { + const contains: Array<{ type: Format.Type; name?: string }> = []; + + for (const [fieldName, fieldType] of bugType.fields) { + const convertedField = convertBugType(fieldType); + if (convertedField) { + contains.push({ + name: fieldName, + type: convertedField, + }); + } + } + + return { + kind: "struct", + contains, + }; + } + + // Function types (if needed for function pointers) + if (BugType.isFunction(bugType)) { + // ethdebug/format may not have full function type support yet + // For now, represent as an opaque type or skip + return undefined; + } + + return undefined; +} + +/** + * Convert an elementary Bug type to ethdebug/format + */ +function convertElementaryType( + elementary: BugType.Elementary, +): Format.Type | undefined { + const { kind } = elementary; + + switch (kind) { + case "uint": + return { + kind: "uint", + bits: elementary.bits, + }; + + case "int": + return { + kind: "int", + bits: elementary.bits, + }; + + case "address": + return { + kind: "address", + // payable is optional, omit if unknown + }; + + case "bool": + return { + kind: "bool", + }; + + case "bytes": { + if (elementary.size !== undefined) { + // Fixed-size bytes (bytes1 - bytes32) + return { + kind: "bytes", + size: elementary.size, + }; + } else { + // Dynamic bytes + return { + kind: "bytes", + }; + } + } + + case "string": + return { + kind: "string", + }; + + default: + return undefined; + } +} + +/** + * Convert a synthetic IR type to ethdebug/format + * + * For IR-generated types without Bug type origin, we infer what we can + * from the IR type structure + */ +function convertSyntheticType(irType: Ir.Type): Format.Type | undefined { + if (Ir.Type.isScalar(irType)) { + // Scalar types - infer based on size + const bits = irType.size * 8; + + // Common patterns for synthetic scalars + if (irType.size === 32) { + // Most 32-byte scalars are uint256 + return { kind: "uint", bits: 256 }; + } + + if (irType.size === 20) { + // 20-byte scalars are addresses + return { kind: "address" }; + } + + if (irType.size === 1) { + // 1-byte scalars could be bool or uint8 + // Default to uint8 for safety + return { kind: "uint", bits: 8 }; + } + + // For other sizes, use uint with appropriate bit size + return { kind: "uint", bits }; + } + + if (Ir.Type.isRef(irType)) { + // Reference types don't directly map to ethdebug types + // They represent pointers, not the data itself + // The actual type would need to come from Bug type origin + return undefined; + } + + return undefined; +} diff --git a/packages/bugc/src/irgen/debug/variables.ts b/packages/bugc/src/irgen/debug/variables.ts new file mode 100644 index 00000000..60fa687d --- /dev/null +++ b/packages/bugc/src/irgen/debug/variables.ts @@ -0,0 +1,344 @@ +/** + * Variable collection utilities for ethdebug/format integration + * + * Collects variable information for generating variables contexts + */ + +import * as Format from "@ethdebug/format"; +import type { State } from "../generate/state.js"; +import { generatePointer, type VariableLocation } from "./pointers.js"; +import { Type } from "#types"; +import { convertBugType } from "./types.js"; + +/** + * Information about a variable available for debug contexts + */ +export interface VariableInfo { + /** Variable identifier (name) */ + identifier: string; + + /** Type information */ + type?: Format.Type; + + /** Runtime location pointer */ + pointer?: Format.Pointer; + + /** Declaration location in source */ + declaration?: Format.Materials.SourceRange; +} + +/** + * Get the size in bytes for a type + */ +function getTypeSize(bugType: Type): number { + if (Type.isElementary(bugType)) { + switch (bugType.kind) { + case "uint": + case "int": + return (bugType.bits || 256) / 8; + case "address": + return 20; + case "bool": + return 1; + case "bytes": + return bugType.size || 32; // Dynamic bytes default to full slot + case "string": + return 32; // Dynamic string default to full slot + default: + return 32; + } + } + // For complex types, default to full slot + return 32; +} + +/** + * Generate a sophisticated pointer for a storage variable based on its type + */ +function generateStoragePointer( + baseSlot: number, + bugType: Type, + byteOffset: number = 0, +): Format.Pointer | undefined { + // For structs, generate a group pointer with each field + if (Type.isStruct(bugType)) { + const group: Array = []; + + for (const [fieldName, fieldType] of bugType.fields) { + const layout = bugType.layout.get(fieldName); + if (!layout) continue; + + const absoluteOffset = byteOffset + layout.byteOffset; + const fieldSlot = baseSlot + Math.floor(absoluteOffset / 32); + const fieldOffset = absoluteOffset % 32; + + const fieldPointer = generateStoragePointer( + fieldSlot, + fieldType, + fieldOffset, + ); + if (!fieldPointer) continue; + + group.push({ ...fieldPointer, name: fieldName }); + } + + if (group.length === 0) { + return undefined; + } + + return { group }; + } + + // For arrays, generate a list pointer + if (Type.isArray(bugType)) { + const elementType = bugType.element; + const elementSize = getTypeSize(elementType); + + // Check if this is a fixed-size or dynamic array + if (bugType.size !== undefined) { + // Fixed-size array: elements stored sequentially from base slot + // For fixed arrays, elements are at baseSlot + floor((i * elementSize) / 32) + // with offset (i * elementSize) % 32 + const elementSlotExpression: Format.Pointer.Expression = + elementSize >= 32 + ? // Full slots: baseSlot + i * (elementSize / 32) + { + $sum: [baseSlot, { $product: ["i", elementSize / 32] }], + } + : // Packed elements: baseSlot + floor((i * elementSize) / 32) + { + $sum: [ + baseSlot, + { + $quotient: [{ $product: ["i", elementSize] }, 32], + }, + ], + }; + + const elementPointer: Format.Pointer = { + name: "element", + location: "storage", + slot: elementSlotExpression, + }; + + // Add offset for packed elements + if (elementSize < 32) { + elementPointer.offset = { + $remainder: [{ $product: ["i", elementSize] }, 32], + }; + elementPointer.length = elementSize; + } + + // Recursively handle complex element types + const refinedPointer = + Type.isStruct(elementType) || Type.isArray(elementType) + ? generateStoragePointer(0, elementType, 0) + : elementPointer; + + return { + list: { + count: bugType.size, + each: "i", + is: refinedPointer || elementPointer, + }, + }; + } else { + // Dynamic array: length at base slot, elements at keccak256(slot) + index + // Elements start at keccak256(baseSlot) + // Note: baseSlot must be wordsized for proper 32-byte keccak256 input + const elementSlotExpression: Format.Pointer.Expression = + elementSize >= 32 + ? // Full slots: keccak256(baseSlot) + i * (elementSize / 32) + { + $sum: [ + { $keccak256: [{ $wordsized: baseSlot }] }, + { $product: ["i", elementSize / 32] }, + ], + } + : // Packed elements: keccak256(baseSlot) + floor((i * elementSize) / 32) + { + $sum: [ + { $keccak256: [{ $wordsized: baseSlot }] }, + { + $quotient: [{ $product: ["i", elementSize] }, 32], + }, + ], + }; + + const elementPointer: Format.Pointer = { + name: "element", + location: "storage", + slot: elementSlotExpression, + }; + + // Add offset for packed elements + if (elementSize < 32) { + elementPointer.offset = { + $remainder: [{ $product: ["i", elementSize] }, 32], + }; + elementPointer.length = elementSize; + } + + // Recursively handle complex element types + const refinedPointer = + Type.isStruct(elementType) || Type.isArray(elementType) + ? generateStoragePointer(0, elementType, 0) + : elementPointer; + + // For dynamic arrays, we use a group to declare both the length region + // and the list of elements + // Note: "array-length" avoids conflict with Array.prototype.length + const lengthRegion: Format.Pointer = { + name: "array-length", + location: "storage", + slot: baseSlot, + }; + if (byteOffset > 0) { + lengthRegion.offset = byteOffset; + } + + return { + group: [ + lengthRegion, + { + list: { + count: { $read: "array-length" }, + each: "i", + is: refinedPointer || elementPointer, + }, + }, + ], + }; + } + } + + // For mappings, we can't represent them without keys + // Just return the base slot pointer with offset/length + if (Type.isMapping(bugType)) { + const pointer: Format.Pointer = { + location: "storage", + slot: baseSlot, + }; + if (byteOffset > 0) { + pointer.offset = byteOffset; + } + return pointer; + } + + // For elementary types, generate pointer with offset and length + const size = getTypeSize(bugType); + const pointer: Format.Pointer = { + location: "storage", + slot: baseSlot, + }; + + if (byteOffset > 0) { + pointer.offset = byteOffset; + } + + if (size < 32) { + pointer.length = size; + } + + return pointer; +} + +/** + * Collect all variables with determinable locations from current state + * + * At IR generation time, we can only include variables that have + * concrete runtime locations: + * - Storage variables (fixed or computed slots) + * - Memory allocations (if tracked) + * + * SSA temps are NOT included because they don't have concrete runtime + * locations until EVM code generation. + */ +export function collectVariablesWithLocations( + state: State, + sourceId: string, +): VariableInfo[] { + const variables: VariableInfo[] = []; + + // Collect storage variables - these have fixed/known slots + for (const storageDecl of state.module.storageDeclarations) { + // Get the resolved BugType from the typechecker + const bugType = state.types.get(storageDecl.id); + if (!bugType) { + // Fallback to simple pointer if no type info + const location: VariableLocation = { + kind: "storage", + slot: storageDecl.slot, + }; + const pointer = generatePointer(location); + if (pointer) { + variables.push({ + identifier: storageDecl.name, + pointer, + declaration: storageDecl.loc + ? { + source: { id: sourceId }, + range: storageDecl.loc, + } + : undefined, + }); + } + continue; + } + + // Generate sophisticated pointer based on type + const pointer = generateStoragePointer(storageDecl.slot, bugType); + if (!pointer) continue; + + // Convert Bug type to ethdebug format type + const type = convertBugType(bugType); + + const declaration: Format.Materials.SourceRange | undefined = + storageDecl.loc + ? { + source: { id: sourceId }, + range: storageDecl.loc, + } + : undefined; + + variables.push({ + identifier: storageDecl.name, + type, + pointer, + declaration, + }); + } + + // TODO: Add memory-allocated variables when we track memory allocations + // For now, we skip memory variables as we don't track their offsets yet + + // Note: We do NOT include SSA temps here because they don't have + // concrete runtime locations (stack positions) until EVM codegen + + return variables; +} + +/** + * Convert VariableInfo to ethdebug/format variable context entry + */ +export function toVariableContextEntry( + variable: VariableInfo, +): Format.Program.Context.Variables["variables"][number] { + const entry: Format.Program.Context.Variables["variables"][number] = { + identifier: variable.identifier, + }; + + if (variable.type) { + entry.type = variable.type; + } + + if (variable.pointer) { + entry.pointer = variable.pointer; + } + + if (variable.declaration) { + entry.declaration = variable.declaration; + } + + return entry; +} diff --git a/packages/bugc/src/irgen/errors.ts b/packages/bugc/src/irgen/errors.ts new file mode 100644 index 00000000..41c252b3 --- /dev/null +++ b/packages/bugc/src/irgen/errors.ts @@ -0,0 +1,59 @@ +/** + * IR generation errors and error codes + */ + +import { BugError } from "#errors"; +import { Severity } from "#result"; +import type { SourceLocation } from "#ast"; + +/** + * Error codes for IR errors + */ +export enum ErrorCode { + INVALID_NODE = "IR001", + UNKNOWN_IDENTIFIER = "IR002", + INTERNAL_ERROR = "IR003", + UNSUPPORTED_FEATURE = "IR004", + INVALID_LVALUE = "IR005", + STORAGE_ACCESS_ERROR = "IR006", + UNKNOWN_TYPE = "IR007", + INVALID_ARGUMENT_COUNT = "IR008", + MISSING_RETURN = "IR009", + GENERAL = "IR_ERROR", // Legacy support +} + +/** + * IR error message templates + */ +export const ErrorMessages = { + UNKNOWN_IDENTIFIER: (name: string) => `Unknown identifier: ${name}`, + STORAGE_MODIFICATION_ERROR: (varName: string, typeName: string) => + `Cannot modify storage through local variable '${varName}' of type ${typeName}. Direct storage access required for persistent changes.`, + UNSUPPORTED_STORAGE_PATTERN: (pattern: string) => + `${pattern} in storage access not yet supported`, +} as const; + +/** + * IR generation errors + */ +class IrgenError extends BugError { + constructor( + message: string, + location?: SourceLocation, + severity: Severity = Severity.Error, + code: ErrorCode = ErrorCode.GENERAL, + ) { + super(message, code, location, severity); + } +} + +export { IrgenError as Error }; + +export function assertExhausted(_: never): never { + throw new IrgenError( + `Unexpected code path; expected exhaustive conditionals`, + undefined, + Severity.Error, + ErrorCode.INTERNAL_ERROR, + ); +} diff --git a/packages/bugc/src/irgen/generate/expressions/access.ts b/packages/bugc/src/irgen/generate/expressions/access.ts new file mode 100644 index 00000000..a568d6fc --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/access.ts @@ -0,0 +1,523 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; +import { Type } from "#types"; + +import { Error as IrgenError, assertExhausted } from "#irgen/errors"; + +import { Process } from "../process.js"; +import type { Context } from "./context.js"; +import { fromBugType } from "#irgen/type"; +import { + type StorageAccessChain, + findStorageAccessChain, + emitStorageChainLoad, +} from "../storage.js"; + +/** + * Build an access expression (array/member access) + */ +export const makeBuildAccess = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => { + // findStorageAccessChain is now imported directly + const buildIndexAccess = makeBuildIndexAccess( + buildExpression, + findStorageAccessChain, + ); + const buildMemberAccess = makeBuildMemberAccess( + buildExpression, + findStorageAccessChain, + ); + const buildSliceAccess = makeBuildSliceAccess(buildExpression); + + return function* buildAccess( + expr: Ast.Expression.Access, + context: Context, + ): Process { + switch (expr.kind) { + case "expression:access:member": + return yield* buildMemberAccess( + expr as Ast.Expression.Access.Member, + context, + ); + + case "expression:access:slice": + return yield* buildSliceAccess( + expr as Ast.Expression.Access.Slice, + context, + ); + + case "expression:access:index": + return yield* buildIndexAccess( + expr as Ast.Expression.Access.Index, + context, + ); + + default: + assertExhausted(expr); + } + }; +}; + +const makeBuildMemberAccess = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, + findStorageAccessChain: ( + node: Ast.Expression, + ) => Process, +) => + function* buildMemberAccess( + expr: Ast.Expression.Access.Member, + _context: Context, + ): Process { + // Check if this is a .length property access + if (expr.property === "length") { + const objectType = yield* Process.Types.nodeType(expr.object); + + // Verify that the object type supports .length (arrays, bytes, string) + if ( + objectType && + (Type.isArray(objectType) || + (Type.isElementary(objectType) && + (Type.Elementary.isBytes(objectType) || + Type.Elementary.isString(objectType)))) + ) { + const resultType: Ir.Type = Ir.Type.Scalar.uint256; + const tempId = yield* Process.Variables.newTemp(); + + // For fixed-size arrays, emit a constant with the known size + if (Type.isArray(objectType) && objectType.size !== undefined) { + yield* Process.Instructions.emit({ + kind: "const", + value: BigInt(objectType.size), + type: resultType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + return Ir.Value.temp(tempId, resultType); + } + + // For dynamic arrays/bytes/strings, emit length instruction + const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + yield* Process.Instructions.emit({ + kind: "length", + object, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + return Ir.Value.temp(tempId, resultType); + } + } + + // First check if this is accessing a storage chain (e.g., accounts[user].balance) + const chain = yield* findStorageAccessChain(expr); + if (chain) { + const nodeType = yield* Process.Types.nodeType(expr); + if (nodeType) { + const valueType = fromBugType(nodeType); + return yield* emitStorageChainLoad(chain, valueType, expr); + } + } + + // Reading through local variables is allowed, no diagnostic needed + + // Otherwise, handle regular struct field access + const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + const objectType = yield* Process.Types.nodeType(expr.object); + + if (objectType && Type.isStruct(objectType)) { + const fieldType = objectType.fields.get(expr.property); + if (fieldType) { + const fieldIndex = Array.from(objectType.fields.keys()).indexOf( + expr.property, + ); + const irFieldType = fromBugType(fieldType); + + // First compute the offset for the field + const offsetTemp = yield* Process.Variables.newTemp(); + // Calculate field offset - assuming 32 bytes per field for now + const fieldOffset = fieldIndex * 32; + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.field( + "memory", + object, + expr.property, + fieldOffset, + offsetTemp, + yield* Process.Debug.forAstNode(expr), + ), + ); + + // Then read from that offset + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + type: irFieldType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, irFieldType); + } + } + + throw new IrgenError( + "Invalid member access expression", + expr.loc ?? undefined, + Severity.Error, + ); + }; + +const makeBuildSliceAccess = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => + function* buildSliceAccess( + expr: Ast.Expression.Access.Slice, + _context: Context, + ): Process { + // Slice access - start:end + const objectType = yield* Process.Types.nodeType(expr.object); + if ( + objectType && + Type.isElementary(objectType) && + Type.Elementary.isBytes(objectType) + ) { + const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + const start = yield* buildExpression(expr.start, { kind: "rvalue" }); + const end = yield* buildExpression(expr.end, { kind: "rvalue" }); + + // Slicing bytes returns dynamic bytes (memory reference) + const resultType: Ir.Type = Ir.Type.Ref.memory(); + + // Calculate the length of the slice + const lengthTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "sub", + left: end, + right: start, + dest: lengthTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + const length = Ir.Value.temp(lengthTemp, Ir.Type.Scalar.uint256); + + // Allocate memory for the slice result (length + 32 for length prefix) + const allocSizeTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: length, + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: allocSizeTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + const destTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "allocate", + location: "memory", + size: Ir.Value.temp(allocSizeTemp, Ir.Type.Scalar.uint256), + dest: destTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Store the length at the beginning of the allocated memory + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(destTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: length, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Write); + + // Compute source offset (skip length prefix + start offset) + const sourceOffsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: object, + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: sourceOffsetTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + const adjustedSourceTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(sourceOffsetTemp, Ir.Type.Scalar.uint256), + right: start, + dest: adjustedSourceTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Read the slice data from source + const dataTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "memory", + offset: Ir.Value.temp(adjustedSourceTemp, Ir.Type.Scalar.uint256), + length, + type: resultType, + dest: dataTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + // Calculate destination offset (skip length prefix) + const destDataOffsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(destTemp, Ir.Type.Scalar.uint256), + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: destDataOffsetTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Write the slice data to destination + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(destDataOffsetTemp, Ir.Type.Scalar.uint256), + length, + value: Ir.Value.temp(dataTemp, resultType), + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Write); + + return Ir.Value.temp(destTemp, resultType); + } + + throw new IrgenError( + "Only bytes types can be sliced", + expr.loc ?? undefined, + Severity.Error, + ); + }; + +const makeBuildIndexAccess = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, + findStorageAccessChain: ( + node: Ast.Expression, + ) => Process, +) => + function* buildIndexAccess( + expr: Ast.Expression.Access.Index, + _context: Context, + ): Process { + // Array/mapping/bytes index access + // First check if we're indexing into bytes (not part of storage chain) + const nodeType = yield* Process.Types.nodeType(expr); + const objectType = yield* Process.Types.nodeType(expr.object); + if ( + objectType && + Type.isElementary(objectType) && + Type.Elementary.isBytes(objectType) + ) { + // Fixed-size bytes types (bytes1, bytes4, etc.) cannot be indexed + // They are atomic values, not arrays + if (objectType.size !== undefined) { + throw new IrgenError( + `Cannot index into fixed-size bytes type 'bytes${objectType.size}'`, + expr.loc ?? undefined, + Severity.Error, + ); + } + + // Dynamic bytes can be indexed + const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + const index = yield* buildExpression(expr.index, { kind: "rvalue" }); + // Bytes indexing returns uint8 + const elementType: Ir.Type = Ir.Type.scalar(1, "synthetic"); + + // Compute offset for the byte at the index using byte offset + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.byte( + "memory", + object, + index, + offsetTemp, + yield* Process.Debug.forAstNode(expr), + ), + ); + + // Read the byte at that offset + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(1n, Ir.Type.Scalar.uint256), + type: elementType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, elementType); + } + + // Check if it's a memory array first (to avoid storage chain check for local arrays) + if (objectType && Type.isArray(objectType)) { + // Check if it's a local (memory) array + if (Ast.Expression.isIdentifier(expr.object)) { + const varName = expr.object.name; + const localVar = yield* Process.Variables.lookup(varName); + if (localVar) { + // It's a local memory array - handle it directly + const object = yield* buildExpression(expr.object, { + kind: "rvalue", + }); + const index = yield* buildExpression(expr.index, { kind: "rvalue" }); + const elementType = fromBugType(objectType.element); + + // Calculate base + 32 to skip length field + const elementsBaseTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: object, + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: elementsBaseTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Compute offset for array element + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.array( + "memory", + Ir.Value.temp(elementsBaseTemp, Ir.Type.Scalar.uint256), + index, + 32, // array elements are 32 bytes each + offsetTemp, + yield* Process.Debug.forAstNode(expr), + ), + ); + + // Read the element at that offset + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + type: elementType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, elementType); + } + } + } + + // For non-bytes, non-memory-array types, try to find a complete storage access chain + const chain = yield* findStorageAccessChain(expr); + if (chain && nodeType) { + const valueType = fromBugType(nodeType); + return yield* emitStorageChainLoad(chain, valueType, expr); + } + + // If no storage chain, handle remaining cases + const object = yield* buildExpression(expr.object, { kind: "rvalue" }); + const index = yield* buildExpression(expr.index, { kind: "rvalue" }); + + if (objectType && Type.isArray(objectType)) { + // This would be for complex array access (e.g., returned from function) + const elementType = fromBugType(objectType.element); + + // Compute offset for array element (no need to add 32 here as the object + // should already point to the elements section) + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.array( + "memory", + object, + index, + 32, // array elements are 32 bytes each + offsetTemp, + yield* Process.Debug.forAstNode(expr), + ), + ); + + // Read the element at that offset + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + type: elementType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, elementType); + } + + if ( + objectType && + Type.isMapping(objectType) && + Ast.Expression.isIdentifier(expr.object) + ) { + // Simple mapping access - compute slot then read + const storageVar = yield* Process.Storage.findSlot(expr.object.name); + if (storageVar) { + const valueType = fromBugType(objectType.value); + + // First compute the slot for the mapping key + const slotTempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "mapping", + base: Ir.Value.constant( + BigInt(storageVar.slot), + Ir.Type.Scalar.uint256, + ), + key: index, + keyType: fromBugType(objectType.key), + dest: slotTempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.ComputeSlot); + + // Then read from that computed slot + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "storage", + slot: Ir.Value.temp(slotTempId, Ir.Type.Scalar.uint256), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + type: valueType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + + return Ir.Value.temp(tempId, valueType); + } + } + + throw new IrgenError( + "Invalid index access expression", + expr.loc ?? undefined, + Severity.Error, + ); + }; diff --git a/packages/bugc/src/irgen/generate/expressions/array.ts b/packages/bugc/src/irgen/generate/expressions/array.ts new file mode 100644 index 00000000..3af5b452 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/array.ts @@ -0,0 +1,223 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Type } from "#types"; +import { Process } from "../process.js"; +import type { Context } from "./context.js"; +import { buildExpression } from "./expression.js"; + +/** + * Build IR for an array expression. + * The behavior depends on the evaluation context: + * - rvalue: allocate memory and initialize + * - lvalue-storage: handled specially in assignment + * - lvalue-memory: allocate and initialize in memory + */ +export function* buildArray( + expr: Ast.Expression.Array, + context: Context, +): Process { + switch (context.kind) { + case "lvalue-storage": { + // Storage array assignment - expand to individual storage writes + // First, store the array length at the base slot + const lengthValue = Ir.Value.constant( + BigInt(expr.elements.length), + Ir.Type.Scalar.uint256, + ); + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot: Ir.Value.constant(BigInt(context.slot), Ir.Type.Scalar.uint256), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: lengthValue, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Write); + + // Then write each element + for (let i = 0; i < expr.elements.length; i++) { + // Generate the value for this element + const elementValue = yield* buildExpression(expr.elements[i], { + kind: "rvalue", + }); + + // Generate the index value + const indexValue = Ir.Value.constant(BigInt(i), Ir.Type.Scalar.uint256); + + // Compute the first slot for the array + const firstSlotTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeSlot.array( + Ir.Value.constant(BigInt(context.slot), Ir.Type.Scalar.uint256), + firstSlotTemp, + yield* Process.Debug.forAstNode(expr), + ), + ); + + // Add the index to get the actual element slot + const slotTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(firstSlotTemp, Ir.Type.Scalar.uint256), + right: indexValue, + dest: slotTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.BinaryOp); + + // Write to storage + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot: Ir.Value.temp(slotTemp, Ir.Type.Scalar.uint256), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: elementValue, + operationDebug: yield* Process.Debug.forAstNode(expr.elements[i]), + } as Ir.Instruction.Write); + } + + // Return a marker value since storage arrays don't have a memory address + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + case "lvalue-memory": + case "rvalue": { + // For memory contexts (both lvalue and rvalue), allocate and initialize + const arrayType = + context.kind === "lvalue-memory" + ? context.type + : yield* Process.Types.nodeType(expr); + + if (!arrayType || !Type.isArray(arrayType)) { + // Fallback if type inference fails + const elementCount = BigInt(expr.elements.length); + const totalSize = 32n + elementCount * 32n; + + const basePtr = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "allocate", + location: "memory", + size: Ir.Value.constant(totalSize, Ir.Type.Scalar.uint256), + dest: basePtr, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Allocate); + + // Store length + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: Ir.Value.constant(elementCount, Ir.Type.Scalar.uint256), + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Write); + + // Calculate elements base (skip length field) + const elementsBaseTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256), + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: elementsBaseTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Store each element + for (let i = 0; i < expr.elements.length; i++) { + const elementValue = yield* buildExpression(expr.elements[i], { + kind: "rvalue", + }); + + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.array( + "memory", + Ir.Value.temp(elementsBaseTemp, Ir.Type.Scalar.uint256), + Ir.Value.constant(BigInt(i), Ir.Type.Scalar.uint256), + 32, + offsetTemp, + yield* Process.Debug.forAstNode(expr.elements[i]), + ), + ); + + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: elementValue, + operationDebug: yield* Process.Debug.forAstNode(expr.elements[i]), + } as Ir.Instruction.Write); + } + + return Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256); + } + + // Same implementation as above but with proper type + const elementCount = BigInt(expr.elements.length); + const totalSize = 32n + elementCount * 32n; + + const basePtr = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "allocate", + location: "memory", + size: Ir.Value.constant(totalSize, Ir.Type.Scalar.uint256), + dest: basePtr, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Allocate); + + // Store length + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: Ir.Value.constant(elementCount, Ir.Type.Scalar.uint256), + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Write); + + // Calculate elements base + const elementsBaseTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256), + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: elementsBaseTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + // Store each element + for (let i = 0; i < expr.elements.length; i++) { + const elementValue = yield* buildExpression(expr.elements[i], { + kind: "rvalue", + }); + + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit( + Ir.Instruction.ComputeOffset.array( + "memory", + Ir.Value.temp(elementsBaseTemp, Ir.Type.Scalar.uint256), + Ir.Value.constant(BigInt(i), Ir.Type.Scalar.uint256), + 32, + offsetTemp, + yield* Process.Debug.forAstNode(expr.elements[i]), + ), + ); + + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: elementValue, + operationDebug: yield* Process.Debug.forAstNode(expr.elements[i]), + } as Ir.Instruction.Write); + } + + return Ir.Value.temp(basePtr, Ir.Type.Scalar.uint256); + } + } +} diff --git a/packages/bugc/src/irgen/generate/expressions/call.ts b/packages/bugc/src/irgen/generate/expressions/call.ts new file mode 100644 index 00000000..fcfc3af7 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/call.ts @@ -0,0 +1,131 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; +import { Type } from "#types"; + +import { Error as IrgenError } from "#irgen/errors"; +import { Process } from "../process.js"; +import type { Context } from "./context.js"; +import { fromBugType } from "#irgen/type"; + +/** + * Build a call expression + */ +export const makeBuildCall = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => + function* buildCall( + expr: Ast.Expression.Call, + _context: Context, + ): Process { + // Check if this is a built-in function call + if ( + expr.callee.kind === "expression:identifier" && + (expr.callee as Ast.Expression.Identifier).name === "keccak256" + ) { + // keccak256 built-in function + if (expr.arguments.length !== 1) { + yield* Process.Errors.report( + new IrgenError( + "keccak256 expects exactly 1 argument", + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.bytes32); + } + + // Evaluate the argument + const argValue = yield* buildExpression(expr.arguments[0], { + kind: "rvalue", + }); + + // Generate hash instruction + const resultType: Ir.Type = Ir.Type.Scalar.bytes32; + const resultTemp = yield* Process.Variables.newTemp(); + + yield* Process.Instructions.emit({ + kind: "hash", + value: argValue, + dest: resultTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction); + + return Ir.Value.temp(resultTemp, resultType); + } + + // Handle user-defined function calls + if (expr.callee.kind === "expression:identifier") { + const functionName = (expr.callee as Ast.Expression.Identifier).name; + + // Get the function type from the type checker + const callType = yield* Process.Types.nodeType(expr); + + if (!callType) { + yield* Process.Errors.report( + new IrgenError( + `Unknown function: ${functionName}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + // Evaluate arguments + const argValues: Ir.Value[] = []; + for (const arg of expr.arguments) { + argValues.push(yield* buildExpression(arg, { kind: "rvalue" })); + } + + // Generate call terminator and split block + const irType = fromBugType(callType); + let dest: string | undefined; + + // Only create a destination if the function returns a value + // Check if it's a void function by checking if the type is a failure with "void function" message + const isVoidFunction = + Type.isFailure(callType) && + (callType as Type.Failure).reason === "void function"; + + if (!isVoidFunction) { + dest = yield* Process.Variables.newTemp(); + } + + // Create a continuation block for after the call + const continuationBlockId = yield* Process.Blocks.create("call_cont"); + + // Terminate current block with call terminator + yield* Process.Blocks.terminate({ + kind: "call", + function: functionName, + arguments: argValues, + dest, + continuation: continuationBlockId, + operationDebug: yield* Process.Debug.forAstNode(expr), + }); + + // Switch to the continuation block + yield* Process.Blocks.switchTo(continuationBlockId); + + // Return the result value or a dummy value for void functions + if (dest) { + return Ir.Value.temp(dest, irType); + } + // Void function - return a dummy value + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + // Other forms of function calls not supported + yield* Process.Errors.report( + new IrgenError( + "Complex function call expressions not yet supported", + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + }; diff --git a/packages/bugc/src/irgen/generate/expressions/cast.ts b/packages/bugc/src/irgen/generate/expressions/cast.ts new file mode 100644 index 00000000..9ae72748 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/cast.ts @@ -0,0 +1,58 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; + +import { Process } from "../process.js"; +import type { Context } from "./context.js"; + +/** + * Build a cast expression + */ +export const makeBuildCast = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => + function* buildCast( + expr: Ast.Expression.Cast, + _context: Context, + ): Process { + // Evaluate the expression being cast + const exprValue = yield* buildExpression(expr.expression, { + kind: "rvalue", + }); + + // Get the target type from the type checker + const targetType = yield* Process.Types.nodeType(expr); + + if (!targetType) { + yield* Process.Errors.report( + new IrgenError( + "Cannot determine target type for cast expression", + expr.loc ?? undefined, + Severity.Error, + ), + ); + return exprValue; // Return the original value + } + + const targetIrType = fromBugType(targetType); + + // For now, we'll generate a cast instruction that will be handled during bytecode generation + // In many cases, the cast is a no-op at the IR level (e.g., uint256 to address) + const resultTemp = yield* Process.Variables.newTemp(); + + yield* Process.Instructions.emit({ + kind: "cast", + value: exprValue, + targetType: targetIrType, + dest: resultTemp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Cast); + + return Ir.Value.temp(resultTemp, targetIrType); + }; diff --git a/packages/bugc/src/irgen/generate/expressions/context.ts b/packages/bugc/src/irgen/generate/expressions/context.ts new file mode 100644 index 00000000..28bb07bb --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/context.ts @@ -0,0 +1,15 @@ +import type { Type } from "#types"; + +/** + * Evaluation context for expressions + * Tells the expression builder how the result will be used + */ +export type Context = + | { kind: "rvalue" } // Normal evaluation - get the value + | { kind: "lvalue-storage"; slot: number; type: Type } // Assigning to storage + | { kind: "lvalue-memory"; type: Type }; // Assigning to memory variable + +/** + * Default context for most expression evaluations + */ +export const rvalue: Context = { kind: "rvalue" }; diff --git a/packages/bugc/src/irgen/generate/expressions/expression.ts b/packages/bugc/src/irgen/generate/expressions/expression.ts new file mode 100644 index 00000000..1a778d24 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/expression.ts @@ -0,0 +1,64 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; + +import { assertExhausted } from "#irgen/errors"; +import { type Process } from "../process.js"; +import type { Context } from "./context.js"; + +import { buildIdentifier } from "./identifier.js"; +import { buildLiteral } from "./literal.js"; +import { makeBuildOperator } from "./operator.js"; +import { makeBuildAccess } from "./access.js"; +import { makeBuildCall } from "./call.js"; +import { makeBuildCast } from "./cast.js"; +import { buildSpecial } from "./special.js"; +import { buildArray } from "./array.js"; + +const buildOperator = makeBuildOperator(buildExpression); +const buildAccess = makeBuildAccess(buildExpression); +const buildCall = makeBuildCall(buildExpression); +const buildCast = makeBuildCast(buildExpression); + +/** + * Build an expression and return the resulting IR value + */ +export function* buildExpression( + expr: Ast.Expression, + context: Context, +): Process { + switch (expr.kind) { + case "expression:identifier": + return yield* buildIdentifier(expr as Ast.Expression.Identifier); + case "expression:literal:number": + case "expression:literal:string": + case "expression:literal:boolean": + case "expression:literal:address": + case "expression:literal:hex": + return yield* buildLiteral(expr as Ast.Expression.Literal); + case "expression:operator": + return yield* buildOperator(expr as Ast.Expression.Operator, context); + case "expression:access:member": + case "expression:access:slice": + case "expression:access:index": + return yield* buildAccess(expr as Ast.Expression.Access, context); + case "expression:call": + return yield* buildCall(expr as Ast.Expression.Call, context); + case "expression:cast": + return yield* buildCast(expr as Ast.Expression.Cast, context); + case "expression:special:msg.sender": + case "expression:special:msg.value": + case "expression:special:msg.data": + case "expression:special:block.timestamp": + case "expression:special:block.number": + return yield* buildSpecial(expr as Ast.Expression.Special); + case "expression:array": + return yield* buildArray(expr as Ast.Expression.Array, context); + case "expression:struct": + // TODO: Implement struct expression generation + throw new Error( + "Struct expressions not yet implemented in IR generation", + ); + default: + assertExhausted(expr); + } +} diff --git a/packages/bugc/src/irgen/generate/expressions/identifier.ts b/packages/bugc/src/irgen/generate/expressions/identifier.ts new file mode 100644 index 00000000..d3bf38e2 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/identifier.ts @@ -0,0 +1,67 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError, ErrorMessages } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; + +import { Process } from "../process.js"; + +/** + * Build an identifier expression + */ +export function* buildIdentifier( + expr: Ast.Expression.Identifier, +): Process { + const ssaVar = yield* Process.Variables.lookup(expr.name); + + if (ssaVar) { + // Check if we need a phi node for this variable + const phiTemp = yield* Process.Variables.checkAndInsertPhi( + expr.name, + ssaVar, + ); + if (phiTemp) { + return Ir.Value.temp(phiTemp, ssaVar.type); + } + + // Return the current SSA temp for this variable + return Ir.Value.temp(ssaVar.currentTempId, ssaVar.type); + } + + // Check if it's a storage variable + const storageSlot = yield* Process.Storage.findSlot(expr.name); + + if (storageSlot) { + // Get the type from the type checker + const storageType = yield* Process.Types.nodeType(storageSlot.declaration); + const irType = storageType + ? fromBugType(storageType) + : Ir.Type.Scalar.uint256; + + // Build storage load using new unified read instruction + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "storage", + slot: Ir.Value.constant(BigInt(storageSlot.slot), Ir.Type.Scalar.uint256), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), // 32 bytes for uint256 + type: irType, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Read); + return Ir.Value.temp(tempId, irType); + } + + // Unknown identifier - add error and return default value + yield* Process.Errors.report( + new IrgenError( + ErrorMessages.UNKNOWN_IDENTIFIER(expr.name), + expr.loc ?? undefined, + Severity.Error, + ), + ); + + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); +} diff --git a/packages/bugc/src/irgen/generate/expressions/index.ts b/packages/bugc/src/irgen/generate/expressions/index.ts new file mode 100644 index 00000000..efc4bf49 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/index.ts @@ -0,0 +1 @@ +export { buildExpression } from "./expression.js"; diff --git a/packages/bugc/src/irgen/generate/expressions/literal.ts b/packages/bugc/src/irgen/generate/expressions/literal.ts new file mode 100644 index 00000000..6a4adc15 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/literal.ts @@ -0,0 +1,74 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError, assertExhausted } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; + +import { Process } from "../process.js"; + +/** + * Build a literal expression + */ +export function* buildLiteral(expr: Ast.Expression.Literal): Process { + // Get the type from the context + const nodeType = yield* Process.Types.nodeType(expr); + + if (!nodeType) { + yield* Process.Errors.report( + new IrgenError( + `Cannot determine type for literal: ${expr.value}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + // Return a default value to allow compilation to continue + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + const type = fromBugType(nodeType); + + // Parse the literal value based on its kind + let value: bigint | string | boolean; + switch (expr.kind) { + case "expression:literal:number": + value = BigInt(expr.value); + break; + case "expression:literal:hex": { + // For hex literals, check if they fit in a BigInt (up to 32 bytes / 256 bits) + const hexValue = expr.value.startsWith("0x") + ? expr.value.slice(2) + : expr.value; + + // If the hex value is longer than 64 characters (32 bytes), + // store it as a string with 0x prefix + if (hexValue.length > 64) { + value = expr.value.startsWith("0x") ? expr.value : `0x${expr.value}`; + } else { + value = BigInt(expr.value); + } + break; + } + case "expression:literal:address": + case "expression:literal:string": + value = expr.value; + break; + case "expression:literal:boolean": + value = expr.value === "true"; + break; + default: + assertExhausted(expr); + } + + const tempId = yield* Process.Variables.newTemp(); + + yield* Process.Instructions.emit({ + kind: "const", + dest: tempId, + value, + type, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Const); + + return Ir.Value.temp(tempId, type); +} diff --git a/packages/bugc/src/irgen/generate/expressions/operator.ts b/packages/bugc/src/irgen/generate/expressions/operator.ts new file mode 100644 index 00000000..bc062b7d --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/operator.ts @@ -0,0 +1,176 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError, assertExhausted } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; + +import { Process } from "../process.js"; +import type { Context } from "./context.js"; + +/** + * Build an operator expression (unary or binary) + */ +export const makeBuildOperator = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => { + const buildUnaryOperator = makeBuildUnaryOperator(buildExpression); + const buildBinaryOperator = makeBuildBinaryOperator(buildExpression); + + return function* buildOperator( + expr: Ast.Expression.Operator, + context: Context, + ): Process { + // Get the type from the context + const nodeType = yield* Process.Types.nodeType(expr); + + if (!nodeType) { + yield* Process.Errors.report( + new IrgenError( + `Cannot determine type for operator expression: ${expr.operator}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + switch (expr.operands.length) { + case 1: + return yield* buildUnaryOperator(expr, context); + case 2: + return yield* buildBinaryOperator( + expr as typeof expr & { operands: { length: 2 } }, + context, + ); + default: + assertExhausted(expr.operands); + } + }; +}; +/** + * Build a unary operator expression + */ +const makeBuildUnaryOperator = ( + buildExpression: ( + expr: Ast.Expression, + context: Context, + ) => Process, +) => + function* buildUnaryOperator( + expr: Ast.Expression.Operator, + _context: Context, + ): Process { + // Get the result type from the context + const nodeType = yield* Process.Types.nodeType(expr); + + if (!nodeType) { + yield* Process.Errors.report( + new IrgenError( + `Cannot determine type for unary operator: ${expr.operator}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + const resultType = fromBugType(nodeType); + + // Evaluate operand + const operandVal = yield* buildExpression(expr.operands[0], { + kind: "rvalue", + }); + + // Generate temp for result + const tempId = yield* Process.Variables.newTemp(); + + // Map operator (matching generator.ts logic) + const op = expr.operator === "!" ? "not" : "neg"; + + // Emit unary operation + yield* Process.Instructions.emit({ + kind: "unary", + op, + operand: operandVal, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.UnaryOp); + + return Ir.Value.temp(tempId, resultType); + }; + +/** + * Build a binary operator expression + */ +const makeBuildBinaryOperator = ( + buildExpression: ( + node: Ast.Expression, + context: Context, + ) => Process, +) => + function* buildBinaryOperator( + expr: Ast.Expression.Operator & { operands: { length: 2 } }, + _context: Context, + ): Process { + // Get the result type from the context + const nodeType = yield* Process.Types.nodeType(expr); + + if (!nodeType) { + yield* Process.Errors.report( + new IrgenError( + `Cannot determine type for binary operator: ${expr.operator}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + const resultType = fromBugType(nodeType); + + // Evaluate operands + const leftVal = yield* buildExpression(expr.operands[0], { + kind: "rvalue", + }); + const rightVal = yield* buildExpression(expr.operands[1], { + kind: "rvalue", + }); + + // Generate temp for result + const tempId = yield* Process.Variables.newTemp(); + + // Emit binary operation + yield* Process.Instructions.emit({ + kind: "binary", + op: mapBinaryOp(expr.operator), + left: leftVal, + right: rightVal, + dest: tempId, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.BinaryOp); + + return Ir.Value.temp(tempId, resultType); + }; + +function mapBinaryOp(op: string): Ir.Instruction.BinaryOp["op"] { + const opMap: Record = { + "+": "add", + "-": "sub", + "*": "mul", + "/": "div", + "%": "mod", + "==": "eq", + "!=": "ne", + "<": "lt", + "<=": "le", + ">": "gt", + ">=": "ge", + "&&": "and", + "||": "or", + }; + return opMap[op] || "add"; +} diff --git a/packages/bugc/src/irgen/generate/expressions/special.ts b/packages/bugc/src/irgen/generate/expressions/special.ts new file mode 100644 index 00000000..7a135397 --- /dev/null +++ b/packages/bugc/src/irgen/generate/expressions/special.ts @@ -0,0 +1,59 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError, assertExhausted } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; +import { Process } from "../process.js"; +/** + * Build a special expression (msg.sender, block.number, etc.) + */ +export function* buildSpecial(expr: Ast.Expression.Special): Process { + // Get the type from the type checker + const nodeType = yield* Process.Types.nodeType(expr); + + if (!nodeType) { + yield* Process.Errors.report( + new IrgenError( + `Cannot determine type for special expression: ${expr.kind}`, + expr.loc ?? undefined, + Severity.Error, + ), + ); + // Return a default value to allow compilation to continue + return Ir.Value.constant(0n, Ir.Type.Scalar.uint256); + } + + const resultType = fromBugType(nodeType); + const temp = yield* Process.Variables.newTemp(); + + let op: Ir.Instruction.Env["op"]; + switch (expr.kind) { + case "expression:special:msg.sender": + op = "msg_sender"; + break; + case "expression:special:msg.value": + op = "msg_value"; + break; + case "expression:special:msg.data": + op = "msg_data"; + break; + case "expression:special:block.timestamp": + op = "block_timestamp"; + break; + case "expression:special:block.number": + op = "block_number"; + break; + default: + assertExhausted(expr); + } + + yield* Process.Instructions.emit({ + kind: "env", + op, + dest: temp, + operationDebug: yield* Process.Debug.forAstNode(expr), + } as Ir.Instruction.Env); + + return Ir.Value.temp(temp, resultType); +} diff --git a/packages/bugc/src/irgen/generate/function.ts b/packages/bugc/src/irgen/generate/function.ts new file mode 100644 index 00000000..7e39aacf --- /dev/null +++ b/packages/bugc/src/irgen/generate/function.ts @@ -0,0 +1,107 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import { buildBlock } from "./statements/index.js"; + +import { Process } from "./process.js"; + +/** + * Build a function + */ +export function* buildFunction( + name: string, + parameters: { + name: string; + type: Ir.Type; + }[], + body: Ast.Block, +): Process { + // Initialize function context + yield* Process.Functions.initialize(name, parameters); + + // Build function body + yield* buildBlock(body); + + // Ensure function has a terminator + { + const terminator = yield* Process.Blocks.currentTerminator(); + if (!terminator) { + // Add implicit return + yield* Process.Blocks.terminate({ + kind: "return", + value: undefined, + // No debug context - compiler-generated implicit return + operationDebug: {}, + }); + } + } + + // Sync final block + yield* Process.Blocks.syncCurrent(); + + // Compute predecessors from the control flow graph + const blocksBeforeCompute = yield* Process.Functions.currentBlocks(); + const blocks = computePredecessors(blocksBeforeCompute); + const params = yield* Process.Functions.currentParameters(); + + // Collect SSA variable metadata + const ssaVariables = yield* Process.Functions.collectSsaMetadata(); + + const function_: Ir.Function = { + name, + parameters: params, + entry: "entry", + blocks, + ssaVariables: ssaVariables.size > 0 ? ssaVariables : undefined, + }; + + return function_; +} + +/** + * Compute predecessors for all blocks based on their terminators + */ +function computePredecessors( + blocks: Map, +): Map { + // Create new blocks with fresh predecessor sets + const result = new Map(); + + // First pass: create all blocks with empty predecessors + for (const [id, block] of blocks) { + result.set(id, { + ...block, + predecessors: new Set(), + }); + } + + // Second pass: add predecessors based on terminators + for (const [sourceId, block] of blocks) { + const terminator = block.terminator; + if (!terminator) continue; + + // Add edges based on terminator type + switch (terminator.kind) { + case "jump": { + const targetBlock = result.get(terminator.target); + if (targetBlock) { + targetBlock.predecessors.add(sourceId); + } + break; + } + case "branch": { + const trueBlock = result.get(terminator.trueTarget); + if (trueBlock) { + trueBlock.predecessors.add(sourceId); + } + const falseBlock = result.get(terminator.falseTarget); + if (falseBlock) { + falseBlock.predecessors.add(sourceId); + } + break; + } + // "return" and "unreachable" have no successors + } + } + + return result; +} diff --git a/packages/bugc/src/irgen/generate/index.ts b/packages/bugc/src/irgen/generate/index.ts new file mode 100644 index 00000000..aae19609 --- /dev/null +++ b/packages/bugc/src/irgen/generate/index.ts @@ -0,0 +1 @@ +export { buildModule } from "./module.js"; diff --git a/packages/bugc/src/irgen/generate/module.ts b/packages/bugc/src/irgen/generate/module.ts new file mode 100644 index 00000000..5f12d894 --- /dev/null +++ b/packages/bugc/src/irgen/generate/module.ts @@ -0,0 +1,178 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import type { Types } from "#types"; +import { Type } from "#types"; +import { Severity } from "#result"; + +import { Error as IrgenError } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; +import { + collectVariablesWithLocations, + toVariableContextEntry, +} from "#irgen/debug/variables"; + +import { State } from "./state.js"; +import { buildFunction } from "./function.js"; +import { Process } from "./process.js"; + +/** + * Generate IR from an AST program (generator version) + */ +export function* buildModule( + program: Ast.Program, + types: Types, +): Process { + // Build constructor if present + if (program.create) { + const func = yield* withErrorHandling( + buildFunction("create", [], program.create), + ); + if (func && !isEmptyCreateFunction(func)) { + yield* Process.Functions.addToModule(State.Module.create, func); + } + } + + // Build main function if present + if (program.body) { + const func = yield* withErrorHandling( + buildFunction("main", [], program.body), + ); + if (func) { + yield* Process.Functions.addToModule(State.Module.main, func); + } + } + + // Build user-defined functions + for (const decl of program.definitions?.items ?? []) { + if (decl.kind === "declaration:function") { + const funcDecl = decl as Ast.Declaration.Function; + + // Map parameters to include their resolved types + const funcType = types.get(funcDecl.id); + + // We expect the type checker to have validated this function + if (!funcType || !Type.isFunction(funcType)) { + yield* Process.Errors.report( + new IrgenError( + `Missing type information for function: ${funcDecl.name}`, + funcDecl.loc ?? undefined, + Severity.Error, + ), + ); + continue; + } + + // Type checker has the function type - use it + const parameters = funcDecl.parameters.map((param, index) => ({ + name: param.name, + type: fromBugType(funcType.parameters[index]), + })); + + const func = yield* withErrorHandling( + buildFunction(funcDecl.name, parameters, funcDecl.body), + ); + if (func) { + yield* Process.Functions.addToModule(funcDecl.name, func); + } + } + } + + // Check if there are any errors + const hasErrors = (yield* Process.Errors.count()) > 0; + if (hasErrors) { + return undefined; + } + + // Get module state to build final IR module + const module_ = yield* Process.Modules.current(); + + // Build program-level debug context with storage variables + const state: State = yield { type: "peek" }; + const sourceId = "0"; // TODO: Get actual source ID + const variables = collectVariablesWithLocations(state, sourceId); + const debugContext = + variables.length > 0 + ? { variables: variables.map(toVariableContextEntry) } + : undefined; + + // Return the module, ensuring main exists + const result: Ir.Module = { + name: module_.name, + functions: module_.functions, + main: module_.main || createEmptyFunction("main"), + debugContext, + }; + + if (module_.create) { + result.create = module_.create; + } + + return result; +} + +/** + * Error handling wrapper for generators + */ +function* withErrorHandling(gen: Process): Process { + const startCount = yield* Process.Errors.count(); + + // Run the generator + const result = yield* gen; + + // Check if new errors were added + const endCount = yield* Process.Errors.count(); + const hasNewErrors = endCount > startCount; + + if (hasNewErrors) { + // If there were errors during execution, return undefined + return undefined; + } + + return result; +} + +/** + * Create an empty function for cases where main is missing + */ +function createEmptyFunction(name: string): Ir.Function { + return { + name, + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + instructions: [], + phis: [], + terminator: { + kind: "return", + value: undefined, + // No debug context - compiler-generated empty function + operationDebug: {}, + }, + predecessors: new Set(), + // No debug context - compiler-generated empty block + debug: {}, + }, + ], + ]), + }; +} + +/** + * Check if a create function is effectively empty + */ +function isEmptyCreateFunction(func: Ir.Function): boolean { + const { blocks } = func; + const entry = blocks.get("entry"); + + return ( + blocks.size === 1 && + !!entry && + entry.instructions.length === 0 && + entry.terminator.kind === "return" && + !entry.terminator.value + ); +} diff --git a/packages/bugc/src/irgen/generate/process.ts b/packages/bugc/src/irgen/generate/process.ts new file mode 100644 index 00000000..51b15ba0 --- /dev/null +++ b/packages/bugc/src/irgen/generate/process.ts @@ -0,0 +1,1272 @@ +import * as Format from "@ethdebug/format"; + +import type * as Ast from "#ast"; +import * as Ir from "#ir"; + +import { assertExhausted } from "#irgen/errors"; + +import { State, type Modify, type Read, isModify, isRead } from "./state.js"; +import { + collectVariablesWithLocations, + toVariableContextEntry, +} from "../debug/variables.js"; + +/** + * Generator type for IR operations + * - Yields IrOperation commands + * - Returns final value of type T + * - Receives State back after peek operations + */ +export type Process = Generator; + +export namespace Process { + /** + * Operation types that can be yielded from generators + */ + export type Action = + | { type: "modify"; fn: (state: State) => State } + | { type: "peek" } + | { type: "value"; value: unknown }; + + export namespace Types { + export const nodeType = lift(State.Types.nodeType); + } + + export namespace Instructions { + /** + * Emit an instruction to the current block + */ + export const emit = lift(State.Block.emit); + } + + /** + * Block operations for managing basic blocks in the IR + */ + export namespace Blocks { + /** + * Set the terminator for the current block and update predecessors + */ + export function* terminate(terminator: Ir.Block.Terminator): Process { + yield* lift(State.Block.setTerminator)(terminator); + + // Track predecessors for target blocks + const state: State = yield { type: "peek" }; + const currentBlockId = state.block.id; + + switch (terminator.kind) { + case "jump": + yield* addPredecessorToBlock(terminator.target, currentBlockId); + break; + case "branch": + yield* addPredecessorToBlock(terminator.trueTarget, currentBlockId); + yield* addPredecessorToBlock(terminator.falseTarget, currentBlockId); + break; + case "call": + yield* addPredecessorToBlock(terminator.continuation, currentBlockId); + break; + } + } + + /** + * Add a predecessor to a block (creating it if needed) + */ + const addPredecessorToBlock = function* ( + targetBlockId: string, + predId: string, + ): Process { + const state: State = yield { type: "peek" }; + const existingBlock = state.function.blocks.get(targetBlockId); + + if (existingBlock) { + // Update existing block with new predecessor + const updatedBlock: Ir.Block = { + ...existingBlock, + predecessors: new Set([...existingBlock.predecessors, predId]), + }; + + // Update function with the updated block + yield { + type: "modify", + fn: (s: State) => ({ + ...s, + function: { + ...s.function, + blocks: new Map([ + ...s.function.blocks, + [targetBlockId, updatedBlock], + ]), + }, + }), + }; + } else { + // Create placeholder block with predecessor + const placeholderBlock: Ir.Block = { + id: targetBlockId, + instructions: [], + // No debug context - compiler-generated placeholder terminator + terminator: { + kind: "jump", + target: targetBlockId, + operationDebug: {}, + }, + predecessors: new Set([predId]), + phis: [], + // No debug context - compiler-generated placeholder block + debug: {}, + }; + + // Add the new block to the function + yield { + type: "modify", + fn: (s: State) => ({ + ...s, + function: { + ...s.function, + blocks: new Map([ + ...s.function.blocks, + [targetBlockId, placeholderBlock], + ]), + }, + }), + }; + } + }; + + export const currentTerminator = lift(State.Block.terminator); + + /** + * Create a new block with a generated ID + */ + export function* create(prefix: string): Process { + const state: State = yield { type: "peek" }; + const id = `${prefix}_${state.counters.block}`; + yield* lift(State.Counters.consumeBlock)(); + return id; + } + + /** + * Switch to a different block, syncing the current block to the function + */ + export function* switchTo(blockId: string): Process { + // First sync current block to function + yield* syncCurrent(); + + // Check if block already exists + const state: State = yield { type: "peek" }; + const existingBlock = state.function.blocks.get(blockId); + + if (existingBlock) { + // Switch to existing block, preserving its contents + // Check if it has a placeholder terminator (self-jump) + const isPlaceholder = + existingBlock.terminator && + existingBlock.terminator.kind === "jump" && + existingBlock.terminator.target === blockId; + + yield { + type: "modify", + fn: (state: State) => ({ + ...state, + block: { + id: existingBlock.id, + instructions: [...existingBlock.instructions], + terminator: isPlaceholder ? undefined : existingBlock.terminator, + predecessors: new Set(existingBlock.predecessors), + phis: [...existingBlock.phis], + }, + }), + }; + } else { + // Create new empty block + const newBlock: State.Block = { + id: blockId, + instructions: [], + terminator: undefined, + predecessors: new Set(), + phis: [], + }; + + yield { + type: "modify", + fn: (state: State) => ({ + ...state, + block: newBlock, + }), + }; + } + } + + /** + * Sync current block to the function + */ + export function* syncCurrent(): Process { + const state: State = yield { type: "peek" }; + const block = state.block; + + // Only sync if block has a terminator + if (block.terminator) { + const completeBlock: Ir.Block = { + id: block.id, + instructions: block.instructions, + terminator: block.terminator, + predecessors: block.predecessors, + phis: block.phis, + // No debug context - block-level context not currently tracked + debug: {}, + }; + + yield* lift(State.Function.addBlock)(block.id, completeBlock); + } + } + } + + /** + * Variable and scope management + */ + export namespace Variables { + /** + * Declare a new SSA variable in the current scope + */ + export function* declare( + name: string, + type: Ir.Type, + loc?: Ast.SourceLocation, + ): Process { + const scope = yield* lift(State.Scopes.current)(); + const tempId = yield* newTemp(); + + const version = (scope.ssaVars.get(name)?.version ?? -1) + 1; + const ssaVar: State.SsaVariable = { + name, + currentTempId: tempId, + type, + version, + }; + + // Update scope with new SSA variable + const newScope = { + ...scope, + ssaVars: new Map([...scope.ssaVars, [name, ssaVar]]), + usedNames: new Map([...scope.usedNames, [name, version + 1]]), + }; + + // Update scopes + yield* lift(State.Scopes.setCurrent)(newScope); + + // Track SSA metadata for phi insertion + const scopeIndex = yield* lift(State.Scopes.extract)( + (s) => s.stack.length - 1, + ); + const scopeId = `scope_${scopeIndex}_${name}`; + yield* addSsaMetadata(tempId, name, scopeId, type, version, loc); + + return ssaVar; + } + + /** + * Declare a new SSA variable with an existing temp ID + */ + export function* declareWithExistingTemp( + name: string, + type: Ir.Type, + tempId: string, + loc?: Ast.SourceLocation, + ): Process { + const scope = yield* lift(State.Scopes.current)(); + + const version = (scope.ssaVars.get(name)?.version ?? -1) + 1; + const ssaVar: State.SsaVariable = { + name, + currentTempId: tempId, + type, + version, + }; + + // Update scope with new SSA variable + const newScope = { + ...scope, + ssaVars: new Map([...scope.ssaVars, [name, ssaVar]]), + usedNames: new Map([...scope.usedNames, [name, version + 1]]), + }; + + // Update scopes + yield* lift(State.Scopes.setCurrent)(newScope); + + // Track SSA metadata for the existing temp + const scopeIndex = yield* lift(State.Scopes.extract)( + (s) => s.stack.length - 1, + ); + const scopeId = `scope_${scopeIndex}_${name}`; + yield* addSsaMetadata(tempId, name, scopeId, type, version, loc); + + return ssaVar; + } + + /** + * Create a new SSA version for a variable (for assignments) + */ + export function* assignSsa( + name: string, + type: Ir.Type, + ): Process { + const tempId = yield* newTemp(); + + // Find the current scope that has this variable + const scopes = yield* lift(State.Scopes.extract)((s) => s.stack); + let scopeIndex = -1; + + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].ssaVars.has(name)) { + scopeIndex = i; + break; + } + } + + if (scopeIndex === -1) { + // Variable doesn't exist, create it in current scope + return yield* declare(name, type); + } + + const targetScope = scopes[scopeIndex]; + const currentVar = targetScope.ssaVars.get(name)!; + const newVersion = currentVar.version + 1; + + const ssaVar: State.SsaVariable = { + name, + currentTempId: tempId, + type, + version: newVersion, + }; + + // Update the target scope + const updatedScope = { + ...targetScope, + ssaVars: new Map([...targetScope.ssaVars, [name, ssaVar]]), + }; + + // Rebuild the scope stack + const newStack = [ + ...scopes.slice(0, scopeIndex), + updatedScope, + ...scopes.slice(scopeIndex + 1), + ]; + + yield* lift(State.Scopes.update)(() => ({ stack: newStack })); + + // Track SSA metadata for phi insertion + const scopeId = `scope_${scopeIndex}_${name}`; + yield* addSsaMetadata(tempId, name, scopeId, type, newVersion); + + return ssaVar; + } + + /** + * Add SSA metadata for a temp ID + */ + const addSsaMetadata = function* ( + tempId: string, + name: string, + scopeId: string, + type: Ir.Type, + version: number, + loc?: Ast.SourceLocation, + ): Process { + const state: State = yield { type: "peek" }; + const currentMetadata = state.function.ssaMetadata || new Map(); + + const newMetadata = new Map(currentMetadata); + newMetadata.set(tempId, { + name, + scopeId, + type, + version, + loc, + }); + + // Update function with new metadata + yield { + type: "modify", + fn: (s: State) => ({ + ...s, + function: { + ...s.function, + ssaMetadata: newMetadata, + }, + }), + }; + }; + + /** + * Look up a variable by name in the scope chain + */ + export const lookup = lift(State.Scopes.lookupVariable); + + /** + * Check if we need a phi node for this variable and insert if needed + */ + export function* checkAndInsertPhi( + varName: string, + ssaVar: State.SsaVariable, + ): Process { + const state: State = yield { type: "peek" }; + const currentBlock = state.block; + + // Only consider phi nodes if we have multiple predecessors + if (currentBlock.predecessors.size <= 1) { + return null; + } + + // Check if we already have a phi node for this variable + const existingPhi = currentBlock.phis.find((phi) => { + // Check if this phi is for the same logical variable + const metadata = state.function.ssaMetadata?.get(phi.dest); + return metadata && metadata.name === varName; + }); + + if (existingPhi) { + return existingPhi.dest; + } + + // Check if different predecessors have different temps for this variable + const predTemps = new Map(); + let needsPhi = false; + let firstTemp: string | null = null; + + // We need to look at the SSA metadata to find which temps each predecessor uses + for (const predId of currentBlock.predecessors) { + // Look up what temp this predecessor uses for this variable + const predBlock = state.function.blocks.get(predId); + if (!predBlock) continue; + + // Find the last assignment to this variable in the predecessor + let lastTemp: string | null = null; + + // Check all temps in our metadata to find ones that match this variable + // and were defined in this predecessor block + if (state.function.ssaMetadata) { + for (const [tempId, metadata] of state.function.ssaMetadata) { + if (metadata.name === varName) { + // Check if this temp is defined in the predecessor block + for (const inst of predBlock.instructions) { + if ("dest" in inst && inst.dest === tempId) { + lastTemp = tempId; + // Keep looking for later assignments + } + } + } + } + } + + // If no assignment in block, use the value from the predecessor's entry + // (this would be the value that flows through the block) + if (!lastTemp && varName === ssaVar.name) { + // Use the current SSA temp as it flows through + lastTemp = ssaVar.currentTempId; + } + + if (lastTemp) { + predTemps.set(predId, lastTemp); + if (firstTemp === null) { + firstTemp = lastTemp; + } else if (firstTemp !== lastTemp) { + needsPhi = true; + } + } + } + + // If all predecessors use the same temp (or variable isn't defined), no phi needed + if (!needsPhi || predTemps.size === 0) { + return null; + } + + // Create a new temp for the phi destination + const phiDest = yield* newTemp(); + + // Build phi sources map + const sources = new Map(); + for (const [predId, tempId] of predTemps) { + const metadata = state.function.ssaMetadata?.get(tempId); + sources.set( + predId, + Ir.Value.temp(tempId, metadata?.type || ssaVar.type), + ); + } + + // Add phi node to the block + const phi: Ir.Block.Phi = { + kind: "phi", + dest: phiDest, + sources, + type: ssaVar.type, + // No debug context - compiler-generated phi node (SSA merge point) + operationDebug: {}, + }; + + yield* lift(State.Block.addPhi)(phi); + + // Track SSA metadata for the new phi temp + const scopeIndex = yield* lift(State.Scopes.extract)( + (s) => s.stack.length - 1, + ); + const scopeId = `scope_${scopeIndex}_${varName}`; + yield* addSsaMetadata( + phiDest, + varName, + scopeId, + ssaVar.type, + ssaVar.version + 1, + ); + + // Update the SSA variable to use the phi result + yield* updateSsaTemp(varName, phiDest); + + return phiDest; + } + + /** + * Update the current temp for an SSA variable + */ + const updateSsaTemp = function* ( + name: string, + newTempId: string, + ): Process { + const scopes = yield* lift(State.Scopes.extract)((s) => s.stack); + let scopeIndex = -1; + + // Find which scope has this variable + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].ssaVars.has(name)) { + scopeIndex = i; + break; + } + } + + if (scopeIndex === -1) return; + + const targetScope = scopes[scopeIndex]; + const currentVar = targetScope.ssaVars.get(name)!; + + // Update with new temp + const updatedVar: State.SsaVariable = { + ...currentVar, + currentTempId: newTempId, + }; + + const updatedScope = { + ...targetScope, + ssaVars: new Map([...targetScope.ssaVars, [name, updatedVar]]), + }; + + // Rebuild the scope stack + const newStack = [ + ...scopes.slice(0, scopeIndex), + updatedScope, + ...scopes.slice(scopeIndex + 1), + ]; + + yield* lift(State.Scopes.update)(() => ({ stack: newStack })); + }; + + /** + * Update an SSA variable to point to an existing temp without creating a new one + */ + export function* updateSsaToExistingTemp( + name: string, + existingTempId: string, + type: Ir.Type, + ): Process { + const scopes = yield* lift(State.Scopes.extract)((s) => s.stack); + let scopeIndex = -1; + + // Find which scope has this variable + for (let i = scopes.length - 1; i >= 0; i--) { + if (scopes[i].ssaVars.has(name)) { + scopeIndex = i; + break; + } + } + + if (scopeIndex === -1) { + // Variable doesn't exist, create it in current scope + // But use the existing temp instead of creating a new one + const scope = yield* lift(State.Scopes.current)(); + const version = (scope.ssaVars.get(name)?.version ?? -1) + 1; + + const ssaVar: State.SsaVariable = { + name, + currentTempId: existingTempId, + type, + version, + }; + + // Update scope with new SSA variable + const newScope = { + ...scope, + ssaVars: new Map([...scope.ssaVars, [name, ssaVar]]), + usedNames: new Map([...scope.usedNames, [name, version + 1]]), + }; + + yield* lift(State.Scopes.setCurrent)(newScope); + + // Track SSA metadata for the existing temp + const scopeId = `scope_${scopeIndex === -1 ? 0 : scopeIndex}_${name}`; + yield* addSsaMetadata(existingTempId, name, scopeId, type, version); + return; + } + + const targetScope = scopes[scopeIndex]; + const currentVar = targetScope.ssaVars.get(name)!; + const newVersion = currentVar.version + 1; + + // Update with the existing temp instead of creating a new one + const updatedVar: State.SsaVariable = { + name, + currentTempId: existingTempId, + type, + version: newVersion, + }; + + const updatedScope = { + ...targetScope, + ssaVars: new Map([...targetScope.ssaVars, [name, updatedVar]]), + }; + + // Rebuild the scope stack + const newStack = [ + ...scopes.slice(0, scopeIndex), + updatedScope, + ...scopes.slice(scopeIndex + 1), + ]; + + yield* lift(State.Scopes.update)(() => ({ stack: newStack })); + + // Track SSA metadata for the existing temp + const scopeId = `scope_${scopeIndex}_${name}`; + yield* addSsaMetadata(existingTempId, name, scopeId, type, newVersion); + } + + /** + * Generate a new temporary variable ID + */ + export function* newTemp(): Process { + const temp = yield* lift(State.Counters.nextTemp)(); + const id = `t${temp}`; + yield* lift(State.Counters.consumeTemp)(); + return id; + } + + /** + * Enter a new scope + */ + export const enterScope = lift(State.Scopes.push); + + /** + * Exit the current scope + */ + export const exitScope = lift(State.Scopes.pop); + + /** + * Capture current state of all variables for loop phi insertion + */ + export function* captureCurrentVariables(): Process< + Map + > { + const scopes = yield* lift(State.Scopes.extract)((s) => s.stack); + const result = new Map(); + + // Capture all variables from all scopes + for (const scope of scopes) { + for (const [name, ssaVar] of scope.ssaVars) { + // Only capture the innermost definition of each variable + if (!result.has(name)) { + result.set(name, { + tempId: ssaVar.currentTempId, + type: ssaVar.type, + }); + } + } + } + + return result; + } + + /** + * Create phi nodes for loop variables at loop header (deprecated - use createAndInsertLoopPhis) + */ + export function createLoopPhis( + preLoopVars: Map, + _headerBlockId: string, + ): Map< + string, + { phiTemp: string; varName: string; type: Ir.Type; initialTemp: string } + > { + const loopPhis = new Map< + string, + { phiTemp: string; varName: string; type: Ir.Type; initialTemp: string } + >(); + + // Track which variables exist before the loop + for (const [varName, { tempId, type }] of preLoopVars) { + // We'll track this for creating phi nodes later + loopPhis.set(varName, { + phiTemp: "", // Will be set when we create the actual phi + varName, + type, + initialTemp: tempId, // The temp value before entering the loop + }); + } + + return loopPhis; + } + + /** + * Create and insert phi nodes for loop variables at loop header + * This creates placeholder phis with initial values, to be updated later + */ + export function* createAndInsertLoopPhis( + preLoopVars: Map, + headerBlockId: string, + ): Process< + Map< + string, + { phiTemp: string; varName: string; type: Ir.Type; initialTemp: string } + > + > { + const loopPhis = new Map< + string, + { phiTemp: string; varName: string; type: Ir.Type; initialTemp: string } + >(); + + const state: State = yield { type: "peek" }; + const headerBlock = state.function.blocks.get(headerBlockId); + if (!headerBlock) { + return loopPhis; + } + + // Find the entry predecessor (the block before the loop) + const entryPredecessor = Array.from(headerBlock.predecessors).find( + (pred) => pred !== headerBlockId, + ); + + if (!entryPredecessor) { + return loopPhis; + } + + // We should already be in the header block when this is called + // Create phi nodes for all variables that exist before the loop + for (const [varName, { tempId, type }] of preLoopVars) { + const phiTemp = yield* newTemp(); + + // Create a placeholder phi with only the entry value for now + // The loop-back value will be added later in updateLoopPhis + const sources = new Map(); + sources.set(entryPredecessor, Ir.Value.temp(tempId, type)); + + const phi: Ir.Block.Phi = { + kind: "phi", + dest: phiTemp, + sources, + type, + operationDebug: {}, + }; + + // Add the phi to the current block (header block) state + yield* lift(State.Block.addPhi)(phi); + + // Track SSA metadata for the phi + const scopeIndex = yield* lift(State.Scopes.extract)( + (s) => s.stack.length - 1, + ); + const scopeId = `scope_${scopeIndex}_${varName}`; + yield* addSsaMetadata(phiTemp, varName, scopeId, type, 0); + + // Update the SSA variable to use the phi result + // This is crucial: from this point forward in the header block, + // all uses of this variable will reference the phi node + yield* updateSsaTemp(varName, phiTemp); + + // Track the phi for later update + loopPhis.set(varName, { + phiTemp, + varName, + type, + initialTemp: tempId, + }); + } + + return loopPhis; + } + + /** + * Update loop phi nodes with values from the loop body + * This adds the loop-back edge to the phi nodes created by createAndInsertLoopPhis + */ + export function* updateLoopPhis( + loopPhis: Map< + string, + { phiTemp: string; varName: string; type: Ir.Type; initialTemp: string } + >, + fromBlockId: string, + headerBlockId: string, + ): Process { + const state: State = yield { type: "peek" }; + const headerBlock = state.function.blocks.get(headerBlockId); + if (!headerBlock) return; + + // Get current values of all loop variables + const currentVars = yield* captureCurrentVariables(); + + for (const [varName, loopPhi] of loopPhis) { + const currentVar = currentVars.get(varName); + if (!currentVar) continue; + + // Find the existing phi node for this variable + const existingPhi = headerBlock.phis.find( + (phi) => phi.dest === loopPhi.phiTemp, + ); + + if (existingPhi) { + // Update the existing phi node with the loop-back value + yield { + type: "modify", + fn: (s: State) => { + const updatedBlock = s.function.blocks.get(headerBlockId); + if (!updatedBlock) return s; + + const updatedPhis = updatedBlock.phis.map((phi) => { + if (phi.dest === loopPhi.phiTemp) { + // Add the loop-back source + const newSources = new Map(phi.sources); + newSources.set( + fromBlockId, + Ir.Value.temp(currentVar.tempId, loopPhi.type), + ); + return { + ...phi, + sources: newSources, + }; + } + return phi; + }); + + return { + ...s, + function: { + ...s.function, + blocks: new Map([ + ...s.function.blocks, + [ + headerBlockId, + { + ...updatedBlock, + phis: updatedPhis, + }, + ], + ]), + }, + }; + }, + }; + } + } + } + } + + /** + * Control flow context management + */ + export namespace ControlFlow { + /** + * Enter a loop context + */ + export const enterLoop = lift(State.Loops.push); + + /** + * Exit the current loop context + */ + export const exitLoop = lift(State.Loops.pop); + + /** + * Get the current loop context + */ + export function* currentLoop(): Process { + const state: State = yield { type: "peek" }; + const loop = state.loops.stack[state.loops.stack.length - 1]; + return loop || null; + } + } + + /** + * Function building operations + */ + export namespace Functions { + /** + * Initialize a new function context + */ + export function* initialize( + name: string, + parameters: { name: string; type: Ir.Type }[], + ): Process { + // Convert parameters to SSA form + const ssaParams: Ir.Function.Parameter[] = []; + const paramSsaVars = new Map(); + const ssaMetadata = new Map(); + + for (const param of parameters) { + const tempId = `t${ssaParams.length}`; + const ssaParam: Ir.Function.Parameter = { + name: param.name, + type: param.type, + tempId, + }; + ssaParams.push(ssaParam); + + // Track SSA variable for the parameter + paramSsaVars.set(param.name, { + name: param.name, + currentTempId: tempId, + type: param.type, + version: 0, + }); + + // Track SSA metadata for phi insertion + ssaMetadata.set(tempId, { + name: param.name, + scopeId: "param", + type: param.type, + version: 0, + }); + } + + // Create function context + const functionContext: State.Function = { + id: name, + parameters: ssaParams, + blocks: new Map(), + ssaMetadata, + }; + + // Create initial block context + const blockContext: State.Block = { + id: "entry", + instructions: [], + terminator: undefined, + predecessors: new Set(), + phis: [], + }; + + // Update state with new contexts + yield { + type: "modify", + fn: (state: State) => ({ + ...state, + function: functionContext, + block: blockContext, + scopes: { stack: [{ ssaVars: paramSsaVars, usedNames: new Map() }] }, + loops: { stack: [] }, + counters: { ...state.counters, block: 1, temp: parameters.length }, + }), + }; + } + + /** + * Get the current function's blocks + */ + export function* currentBlocks(): Process> { + const state: State = yield { type: "peek" }; + return state.function.blocks; + } + + /** + * Get the current function's parameters + */ + export function* currentParameters(): Process { + const state: State = yield { type: "peek" }; + return state.function.parameters; + } + + /** + * Finalize the current function + */ + export function* finalize(): Process { + // Sync final block + yield* Blocks.syncCurrent(); + + const state: State = yield { type: "peek" }; + const func = state.function; + + return { + name: func.id, + parameters: func.parameters, + entry: "entry", + blocks: func.blocks, + }; + } + + /** + * Collect SSA variable metadata for phi insertion + */ + export function* collectSsaMetadata(): Process< + Map + > { + const state: State = yield { type: "peek" }; + // Return the SSA metadata that we've been tracking in the function state + return state.function.ssaMetadata || new Map(); + } + + /** + * Add a function to the module + */ + export const addToModule = lift(State.Module.addFunction); + } + + export namespace Modules { + export function* current(): Process { + const state: State = yield { type: "peek" }; + return state.module; + } + } + + export namespace Debug { + /** + * Generate debug context for an AST node + * + * Includes: + * - Code source location (if node has loc) + * - Variables context (all storage variables with known locations) + * + * This automatically attaches all available debug information to instructions. + */ + export function* forAstNode(node: Ast.Node): Process { + const variablesContext = yield* withStorageVariables(); + + if (!node.loc) { + // No code location, but might have variables + return variablesContext; + } + + const { offset, length } = node.loc; + const id = (yield* Process.Modules.current()).name; + + // Combine code and variables in a single context object + // No need for gather since the keys don't conflict + const context: Format.Program.Context = { + code: { + source: { id }, + range: { offset, length }, + }, + // Add variables if available + ...(Format.Program.Context.isVariables(variablesContext.context) + ? { variables: variablesContext.context.variables } + : {}), + }; + + return { context }; + } + + /** + * Generate variables context for all storage variables + * + * This includes variables with determinable runtime locations: + * - Storage variables (with fixed slots) + * - Memory allocations (when tracked) + * + * SSA temps are NOT included as they don't have concrete locations + * until EVM code generation. + */ + export function* withStorageVariables(): Process { + const state: State = yield { type: "peek" }; + const id = state.module.name; + + const variables = collectVariablesWithLocations(state, id); + + if (variables.length === 0) { + return {}; + } + + return { + context: { + variables: variables.map(toVariableContextEntry), + }, + }; + } + } + + /** + * Storage operations + */ + export namespace Storage { + /** + * Find a storage slot by name + */ + export function* findSlot(name: string): Process<{ + slot: number; + name: string; + declaration: Ast.Declaration.Storage; + } | null> { + const state: State = yield { type: "peek" }; + const storageDecl = state.module.storageDeclarations.find( + (decl) => decl.name === name, + ); + + if (!storageDecl) return null; + + return { + slot: storageDecl.slot, + name: storageDecl.name, + declaration: storageDecl, + }; + } + + /** + * Emit a compute_slot instruction + */ + export function* computeSlot( + baseSlot: Ir.Value, + key: Ir.Value, + node?: Ast.Node, + ): Process { + const tempId = yield* Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "mapping", + base: baseSlot, + key, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.ComputeSlot); + return Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); + } + + /** + * Emit a load_storage instruction + */ + export function* load( + slot: Ir.Value, + type: Ir.Type, + node?: Ast.Node, + ): Process { + const tempId = yield* Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "storage", + slot, + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + type, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.Read); + return Ir.Value.temp(tempId, type); + } + + /** + * Emit a write instruction for storage + */ + export function* store( + slot: Ir.Value, + value: Ir.Value, + node?: Ast.Node, + ): Process { + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot, + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.Write); + } + } + + /** + * Error handling + */ + export namespace Errors { + /** + * Report an error + */ + export const report = lift(State.Errors.append); + + export const count = lift(State.Errors.count); + + /** + * Report a warning + */ + export const warning = lift(State.Warnings.append); + + /** + * Attempt an operation, catching IrgenErrors + */ + export const attempt = lift(State.Errors.attempt); + } + + /** + * Run a process with an initial state + */ + export function run( + process: Process, + initialState: State, + ): { state: State; value: T } { + let state = initialState; + let next = process.next(); + + while (!next.done) { + const action = next.value; + + switch (action.type) { + case "modify": { + state = action.fn(state); + next = process.next(state); + break; + } + case "peek": { + next = process.next(state); + break; + } + case "value": { + // This is for returning values without state changes + next = process.next(state); + break; + } + default: + assertExhausted(action); + } + } + + return { state, value: next.value }; + } +} + +// Overloaded signatures for different return types +function lift( + fn: (...args: A) => Modify, +): (...args: A) => Process; + +function lift( + fn: (...args: A) => Read, +): (...args: A) => Process; + +// Implementation +function lift( + fn: (...args: A) => Modify | Read, +) { + return function* (...args: A): Process { + const result = fn(...args); + + if (isModify(result)) { + yield { + type: "modify", + fn: result, + }; + return; + } + + if (isRead(result)) { + return result(yield { type: "peek" }); + } + + assertExhausted(result); + }; +} diff --git a/packages/bugc/src/irgen/generate/state.ts b/packages/bugc/src/irgen/generate/state.ts new file mode 100644 index 00000000..86d8adf9 --- /dev/null +++ b/packages/bugc/src/irgen/generate/state.ts @@ -0,0 +1,375 @@ +import type * as Ast from "#ast"; +import type * as Ir from "#ir"; +import type { Types } from "#types"; +import { Severity } from "#result"; + +import { Error as IrgenError } from "#irgen/errors"; + +/** + * Main state for IR generation - immutable and passed through all operations + */ +export interface State { + readonly types: Types; // Type information (read-only) + + readonly module: State.Module; // Module being built incrementally + readonly function: State.Function; // Current function context + readonly block: State.Block; // Current block context + readonly scopes: State.Scopes; // Variable scoping for name resolution + readonly loops: State.Loops; // Loop contexts for break/continue + readonly counters: State.Counters; // ID generation counters + + readonly errors: State.Errors; // Accumulated errors + readonly warnings: State.Warnings; // Accumulated warnings +} + +export namespace State { + export namespace Types { + const extract = makeExtract((read) => (state) => read(state.types)); + + export const nodeType = (node: Ast.Node) => + extract((types) => types.get(node.id)); + } + + /** + * Partially built module + */ + export interface Module { + readonly name: string; + readonly functions: Map; + readonly main?: Ir.Function; + readonly create?: Ir.Function; + readonly storageDeclarations: Ast.Declaration.Storage[]; + } + + export namespace Module { + const update = makeUpdate((modify) => (state) => ({ + ...state, + module: modify(state.module), + })); + + export const main = Symbol("main"); + export const create = Symbol("create"); + + export const addFunction = ( + name: string | typeof main | typeof create, + function_: Ir.Function, + ) => + update((module) => ({ + ...module, + ...(name !== main && name !== create + ? { + functions: new Map([ + ...module.functions, + [name.toString(), function_], + ]), + } + : {}), + ...(name === main ? { main: function_ } : {}), + ...(name === create ? { create: function_ } : {}), + })); + } + + /** + * Current function being built + */ + export interface Function { + readonly id: string; + readonly parameters: Ir.Function.Parameter[]; // Function parameters + readonly blocks: Map; // All blocks in function + readonly ssaMetadata?: Map; // SSA variable metadata for phi insertion + } + + export namespace Function { + const update = makeUpdate((modify) => (state) => ({ + ...state, + function: modify(state.function), + })); + + export const addParameter = (param: Ir.Function.Parameter) => + update((function_) => ({ + ...function_, + parameters: [...function_.parameters, param], + })); + + export const addBlock = (id: string, block: Ir.Block) => + update((function_) => ({ + ...function_, + blocks: new Map([...function_.blocks, [id, block]]), + })); + } + + /** + * Current block being built - incomplete until terminator is set + */ + export interface Block { + readonly id: string; + readonly instructions: Ir.Instruction[]; + readonly terminator?: Ir.Block.Terminator; // Optional during building + readonly predecessors: Set; + readonly phis: Ir.Block.Phi[]; // Phi nodes for the block + /** Track which temp each predecessor uses for each variable */ + readonly predecessorTemps?: Map>; // varName -> (predBlock -> tempId) + } + + export namespace Block { + const update = makeUpdate((modify) => (state) => ({ + ...state, + block: modify(state.block), + })); + + const extract = makeExtract( + (read) => + ({ block }) => + read(block), + ); + + export const emit = (instruction: Ir.Instruction) => + update((block) => ({ + ...block, + instructions: [...block.instructions, instruction], + })); + + export const terminator = () => extract((block) => block.terminator); + + export const setTerminator = (terminator: Ir.Block.Terminator) => + State.Errors.attempt( + update((block) => { + if (block.terminator) { + throw new IrgenError( + `Block ${block.id} already has terminator`, + undefined, + Severity.Warning, + ); + } + + return { + ...block, + terminator, + }; + }), + ); + + export const addPhi = (phi: Ir.Block.Phi) => + update((block) => ({ + ...block, + phis: [...block.phis, phi], + })); + } + + /** + * Variable scoping stack + */ + export interface Scopes { + readonly stack: State.Scope[]; + } + + export interface Scope { + readonly ssaVars: Map; // SSA variable tracking + readonly usedNames: Map; // For handling shadowing + } + + /** + * SSA variable information + */ + export interface SsaVariable { + readonly name: string; // Original variable name + readonly currentTempId: string; // Current SSA temp + readonly type: Ir.Type; + readonly version: number; // SSA version number + } + + export namespace Scopes { + export const update = makeUpdate((modify) => (state) => ({ + ...state, + scopes: modify(state.scopes), + })); + + export const extract = makeExtract( + (read) => (state) => read(state.scopes), + ); + + export const push = () => + update((scopes) => ({ + stack: [...scopes.stack, { ssaVars: new Map(), usedNames: new Map() }], + })); + + export const pop = () => + State.Errors.attempt( + update((scopes) => { + if (scopes.stack.length <= 1) { + throw new IrgenError( + "Cannot pop last scope", + undefined, + Severity.Error, + ); + } + + return { + stack: scopes.stack.slice(0, -1), + }; + }), + ); + + export const current = () => extract((scopes) => scopes.stack.at(-1)!); + + export const setCurrent = (scope: State.Scope) => + update((scopes) => ({ + stack: [...scopes.stack.slice(0, -1), scope], + })); + + export const lookupVariable = (name: string) => + extract((scopes) => { + // Search from innermost to outermost scope + for (let i = scopes.stack.length - 1; i >= 0; i--) { + const ssaVar = scopes.stack[i].ssaVars.get(name); + if (ssaVar) { + return ssaVar; + } + } + return null; + }); + } + + export interface Loop { + readonly continueTarget: string; // Block ID for continue + readonly breakTarget: string; // Block ID for break + } + + export interface Loops { + readonly stack: State.Loop[]; + } + + export namespace Loops { + const update = makeUpdate((modify) => (state) => ({ + ...state, + loops: modify(state.loops), + })); + + export const push = (continueTarget: string, breakTarget: string) => + update((loops) => ({ + stack: [...loops.stack, { continueTarget, breakTarget }], + })); + + export const pop = () => + State.Errors.attempt( + update((loops) => { + if (loops.stack.length < 1) { + throw new IrgenError( + "Cannot exit loop if not currently inside a loop", + undefined, + Severity.Error, + ); + } + + return { + stack: loops.stack.slice(0, -1), + }; + }), + ); + } + + /** + * Counters for ID generation + */ + export interface Counters { + readonly temp: number; // For temporary IDs (t0, t1, ...) + readonly block: number; // For block IDs (block_1, block_2, ...) + } + + export namespace Counters { + const update = makeUpdate((modify) => (state) => ({ + ...state, + counters: modify(state.counters), + })); + + const extract = makeExtract( + (read) => (state) => read(state.counters), + ); + + export const nextTemp = () => extract(({ temp }) => temp); + export const consumeTemp = () => + update((counters) => ({ ...counters, temp: counters.temp + 1 })); + + export const nextBlock = () => extract(({ block }) => block); + export const consumeBlock = () => + update((counters) => ({ ...counters, block: counters.block + 1 })); + } + + export type Errors = IrgenError[]; + + export namespace Errors { + const update = makeUpdate((modify) => (state) => ({ + ...state, + errors: modify(state.errors), + })); + + const extract = makeExtract( + (read) => (state) => read(state.errors), + ); + + export const count = () => extract((errors) => errors.length); + + export const append = (error: IrgenError) => + update((errors) => [...errors, error]); + + export const attempt = makeUpdate((modify) => (state) => { + try { + return modify(state); + } catch (error) { + if (error instanceof IrgenError) { + return State.Errors.append(error)(state); + } + throw error; + } + }); + } + + export type Warnings = IrgenError[]; + + export namespace Warnings { + const update = makeUpdate((modify) => (state) => ({ + ...state, + warnings: modify(state.warnings), + })); + + export const append = (warning: IrgenError) => + update((warnings) => [...warnings, warning]); + } +} + +// Brand symbols for runtime type detection +const MODIFY_BRAND = Symbol("modify"); +const READ_BRAND = Symbol("read"); + +export type Modify = ((substate: S) => S) & { [MODIFY_BRAND]: true }; +export type Read = ((substate: S) => T) & { [READ_BRAND]: true }; + +export type Update = (modify: (substate: S) => S) => Modify; +export type Extract = (read: (substate: S) => T) => Read; + +export function makeUpdate( + update: (modify: (substate: S) => S) => (state: State) => State, +): Update { + return (modify) => { + const function_ = update(modify); + return Object.assign(function_, { [MODIFY_BRAND]: true as const }); + }; +} + +export function makeExtract( + extract: (read: (substate: S) => T) => (state: State) => T, +): Extract { + return (read) => { + const function_ = extract(read); + return Object.assign(function_, { [READ_BRAND]: true as const }); + }; +} + +// Type guards that check for brands +export function isModify(fn: unknown): fn is Modify { + return typeof fn === "function" && MODIFY_BRAND in fn; +} + +export function isRead(fn: unknown): fn is Read { + return typeof fn === "function" && READ_BRAND in fn; +} diff --git a/packages/bugc/src/irgen/generate/statements/assign.ts b/packages/bugc/src/irgen/generate/statements/assign.ts new file mode 100644 index 00000000..d3c2d6fe --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/assign.ts @@ -0,0 +1,290 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Type } from "#types"; +import { Error as IrgenError } from "#irgen/errors"; +import { Severity } from "#result"; +import { buildExpression } from "../expressions/index.js"; +import { Process } from "../process.js"; +import type { Context } from "../expressions/context.js"; + +import { findStorageAccessChain, emitStorageChainStore } from "../storage.js"; + +/** + * Build an assignment statement + */ +export function* buildAssignmentStatement( + stmt: Ast.Statement.Assign, +): Process { + yield* buildLValue(stmt.target, stmt.value); +} + +/** + * Handle lvalue assignment + * @param target The target expression to assign to + * @param valueExpr The value expression being assigned + */ +function* buildLValue( + target: Ast.Expression, + valueExpr: Ast.Expression, +): Process { + // Determine the evaluation context based on the target + let context: Context = { kind: "rvalue" }; + + if (target.kind === "expression:identifier") { + const targetName = (target as Ast.Expression.Identifier).name; + + // Check if it's storage + const storageSlot = yield* Process.Storage.findSlot(targetName); + if (storageSlot) { + const targetType = yield* Process.Types.nodeType(target); + if (targetType) { + context = { + kind: "lvalue-storage", + slot: storageSlot.slot, + type: targetType, + }; + } + } else { + // Check if it's a local variable + const variable = yield* Process.Variables.lookup(targetName); + if (variable) { + const targetType = yield* Process.Types.nodeType(target); + if (targetType) { + context = { kind: "lvalue-memory", type: targetType }; + } + } + } + } + + // Evaluate the value expression with the appropriate context + const value = yield* buildExpression(valueExpr, context); + + // For storage array assignments, the array expression will have already + // expanded to storage writes, so we don't need to do anything else + if ( + context.kind === "lvalue-storage" && + valueExpr.kind === "expression:array" + ) { + return; + } + + // Otherwise assign the computed value to the target + yield* assignToTarget(target, value); +} + +/** + * Assign a value to a target expression (identifier or access expression) + */ +function* assignToTarget(node: Ast.Expression, value: Ir.Value): Process { + if (node.kind === "expression:identifier") { + const name = (node as Ast.Expression.Identifier).name; + + // Check if it's a variable + const ssaVar = yield* Process.Variables.lookup(name); + if (ssaVar) { + // Instead of creating a new SSA version with a new temp, + // we can just update the SSA variable to point to the value's temp + if (value.kind === "temp") { + // If the value is already a temp, we can just update the SSA variable + // to point to it directly + yield* Process.Variables.updateSsaToExistingTemp( + name, + value.id, + ssaVar.type, + ); + } else { + // For non-temp values (constants, etc.), we still need to create a new temp + const newSsaVar = yield* Process.Variables.assignSsa(name, ssaVar.type); + + // Copy the non-temp value to the new SSA temp + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: value, + right: Ir.Value.constant(0n, ssaVar.type), + dest: newSsaVar.currentTempId, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction); + } + return; + } + + // Check if it's storage + const storageSlot = yield* Process.Storage.findSlot(name); + if (storageSlot) { + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot: Ir.Value.constant( + BigInt(storageSlot.slot), + Ir.Type.Scalar.uint256, + ), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), // 32 bytes for uint256 + value, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.Write); + return; + } + + yield* Process.Errors.report( + new IrgenError( + `Unknown identifier: ${name}`, + node.loc || undefined, + Severity.Error, + ), + ); + return; + } + + if ( + node.kind === "expression:access:member" || + node.kind === "expression:access:slice" || + node.kind === "expression:access:index" + ) { + const accessNode = node as Ast.Expression.Access; + + if (accessNode.kind === "expression:access:member") { + // First check if this is a storage chain assignment + const chain = yield* findStorageAccessChain(node); + if (chain) { + yield* emitStorageChainStore(chain, value, accessNode); + return; + } + + // Otherwise, handle regular struct field assignment + const object = yield* buildExpression( + (accessNode as Ast.Expression.Access.Member).object, + { + kind: "rvalue", + }, + ); + const objectType = yield* Process.Types.nodeType( + (accessNode as Ast.Expression.Access.Member).object, + ); + + if (objectType && Type.isStruct(objectType)) { + const fieldName = (accessNode as Ast.Expression.Access.Member) + .property as string; + const fieldType = objectType.fields.get(fieldName); + if (fieldType) { + // Find field index + let fieldIndex = 0; + for (const [name] of objectType.fields) { + if (name === fieldName) break; + fieldIndex++; + } + + // First compute the offset for the field + const offsetTemp = yield* Process.Variables.newTemp(); + // Calculate field offset - assuming 32 bytes per field for now + const fieldOffset = fieldIndex * 32; + yield* Process.Instructions.emit({ + kind: "compute_offset", + location: "memory", + base: object, + field: fieldName, + fieldOffset, + dest: offsetTemp, + operationDebug: yield* Process.Debug.forAstNode(accessNode), + } as Ir.Instruction.ComputeOffset); + + // Then write to that offset + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value, + operationDebug: yield* Process.Debug.forAstNode(accessNode), + } as Ir.Instruction.Write); + return; + } + } + } else if (accessNode.kind === "expression:access:index") { + // Array/mapping/bytes assignment + // First check if we're assigning to bytes + const objectType = yield* Process.Types.nodeType(accessNode.object); + if ( + objectType && + Type.isElementary(objectType) && + Type.Elementary.isBytes(objectType) + ) { + // Handle bytes indexing directly + const object = yield* buildExpression(accessNode.object, { + kind: "rvalue", + }); + const index = yield* buildExpression(accessNode.index, { + kind: "rvalue", + }); + + // Compute offset for the byte at the index + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_offset", + location: "memory", + base: object, + index, + stride: 1, // bytes are 1 byte each + dest: offsetTemp, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.ComputeOffset); + + // Write the byte at that offset + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(1n, Ir.Type.Scalar.uint256), + value, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.Write); + return; + } + + // For non-bytes types, try to find a complete storage access chain + const chain = yield* findStorageAccessChain(node); + if (chain) { + yield* emitStorageChainStore(chain, value, accessNode); + return; + } + + // If no storage chain, handle regular array/mapping access + const object = yield* buildExpression(accessNode.object, { + kind: "rvalue", + }); + const index = yield* buildExpression(accessNode.index, { + kind: "rvalue", + }); + + if (objectType && Type.isArray(objectType)) { + // Compute offset for array element + const offsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_offset", + location: "memory", + base: object, + index, + stride: 32, // array elements are 32 bytes each + dest: offsetTemp, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.ComputeOffset); + + // Write the element at that offset + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(offsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value, + operationDebug: yield* Process.Debug.forAstNode(node), + } as Ir.Instruction.Write); + return; + } + } + } + + yield* Process.Errors.report( + new IrgenError("Invalid lvalue", node.loc || undefined, Severity.Error), + ); +} diff --git a/packages/bugc/src/irgen/generate/statements/block.ts b/packages/bugc/src/irgen/generate/statements/block.ts new file mode 100644 index 00000000..762b72a5 --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/block.ts @@ -0,0 +1,20 @@ +import * as Ast from "#ast"; +import { Process } from "../process.js"; + +/** + * Build a block of statements + */ +export const makeBuildBlock = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => + function* buildBlock(block: Ast.Block): Process { + yield* Process.Variables.enterScope(); + + for (const item of block.items) { + if (Ast.isStatement(item)) { + yield* buildStatement(item); + } + } + + yield* Process.Variables.exitScope(); + }; diff --git a/packages/bugc/src/irgen/generate/statements/control-flow.ts b/packages/bugc/src/irgen/generate/statements/control-flow.ts new file mode 100644 index 00000000..f89fdb6c --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/control-flow.ts @@ -0,0 +1,357 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Error as IrgenError, assertExhausted } from "#irgen/errors"; +import { Severity } from "#result"; +import { buildExpression } from "../expressions/index.js"; + +import { makeBuildBlock } from "./block.js"; + +import { Process } from "../process.js"; +import type { State } from "../state.js"; + +/** + * Build a control flow statement + */ +export const makeBuildControlFlowStatement = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => { + const buildIfStatement = makeBuildIfStatement(buildStatement); + const buildWhileStatement = makeBuildWhileStatement(buildStatement); + const buildForStatement = makeBuildForStatement(buildStatement); + + return function* buildControlFlowStatement( + stmt: Ast.Statement.ControlFlow, + ): Process { + switch (stmt.kind) { + case "statement:control-flow:if": + return yield* buildIfStatement(stmt as Ast.Statement.ControlFlow.If); + case "statement:control-flow:while": + return yield* buildWhileStatement( + stmt as Ast.Statement.ControlFlow.While, + ); + case "statement:control-flow:for": + return yield* buildForStatement(stmt as Ast.Statement.ControlFlow.For); + case "statement:control-flow:return": + return yield* buildReturnStatement( + stmt as Ast.Statement.ControlFlow.Return, + ); + case "statement:control-flow:break": + return yield* buildBreakStatement( + stmt as Ast.Statement.ControlFlow.Break, + ); + case "statement:control-flow:continue": + return yield* buildContinueStatement( + stmt as Ast.Statement.ControlFlow.Continue, + ); + default: + assertExhausted(stmt); + } + }; +}; + +/** + * Build an if statement + */ +export const makeBuildIfStatement = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => { + const buildBlock = makeBuildBlock(buildStatement); + return function* buildIfStatement( + stmt: Ast.Statement.ControlFlow.If, + ): Process { + const thenBlock = yield* Process.Blocks.create("then"); + const elseBlock = stmt.alternate + ? yield* Process.Blocks.create("else") + : yield* Process.Blocks.create("merge"); + const mergeBlock = stmt.alternate + ? yield* Process.Blocks.create("merge") + : elseBlock; // For no-else case, elseBlock IS the merge block + + // Evaluate condition + const condVal = yield* buildExpression(stmt.condition!, { kind: "rvalue" }); + + // Branch to then or else/merge + yield* Process.Blocks.terminate({ + kind: "branch", + condition: condVal, + trueTarget: thenBlock, + falseTarget: elseBlock, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); + + // Build then block + yield* Process.Blocks.switchTo(thenBlock); + yield* buildBlock(stmt.body!); + + { + const terminator = yield* Process.Blocks.currentTerminator(); + // Only set terminator if block doesn't have one + if (!terminator) { + yield* Process.Blocks.terminate({ + kind: "jump", + target: mergeBlock, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); + } + } + + // Build else block if it exists + if (stmt.alternate) { + yield* Process.Blocks.switchTo(elseBlock); + yield* buildBlock(stmt.alternate); + + const terminator = yield* Process.Blocks.currentTerminator(); + if (!terminator) { + yield* Process.Blocks.terminate({ + kind: "jump", + target: mergeBlock, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); + } + } + + // Continue in merge block + yield* Process.Blocks.switchTo(mergeBlock); + }; +}; + +/** + * Unified loop builder for while and for loops + */ +const makeBuildLoop = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => + function* buildLoop(config: { + init?: Ast.Statement; + condition?: Ast.Expression; + update?: Ast.Statement; + body: Ast.Block; + prefix: string; + node?: Ast.Node; + }): Process { + const buildBlock = makeBuildBlock(buildStatement); + + // Execute init statement if present (for loops) + if (config.init) { + yield* buildStatement(config.init); + } + + // Create blocks + const headerBlock = yield* Process.Blocks.create(`${config.prefix}_header`); + const bodyBlock = yield* Process.Blocks.create(`${config.prefix}_body`); + const exitBlock = yield* Process.Blocks.create(`${config.prefix}_exit`); + + // For 'for' loops, we need an update block + const updateBlock = config.update + ? yield* Process.Blocks.create(`${config.prefix}_update`) + : null; + + // Track variables before entering loop for phi insertion + const preLoopVars = yield* Process.Variables.captureCurrentVariables(); + + // Jump to header + yield* Process.Blocks.terminate({ + kind: "jump", + target: headerBlock, + operationDebug: config.node + ? yield* Process.Debug.forAstNode(config.node) + : {}, + }); + + // Header: evaluate condition and branch + yield* Process.Blocks.switchTo(headerBlock); + + // On first entry to header, create phi nodes for loop variables + // We need to create placeholder phis NOW so that the condition uses them + const loopPhis = yield* Process.Variables.createAndInsertLoopPhis( + preLoopVars, + headerBlock, + ); + + const condVal = config.condition + ? yield* buildExpression(config.condition, { kind: "rvalue" }) + : Ir.Value.constant(1n, Ir.Type.Scalar.bool); // infinite loop if no condition + + yield* Process.Blocks.terminate({ + kind: "branch", + condition: condVal, + trueTarget: bodyBlock, + falseTarget: exitBlock, + operationDebug: config.node + ? yield* Process.Debug.forAstNode(config.node) + : {}, + }); + + // Body: execute loop body + yield* Process.Blocks.switchTo(bodyBlock); + + // Set up loop context (continue target depends on whether we have update) + const continueTarget = updateBlock || headerBlock; + yield* Process.ControlFlow.enterLoop(continueTarget, exitBlock); + + yield* buildBlock(config.body); + + yield* Process.ControlFlow.exitLoop(); + + // Jump to update block (for loop) or header (while loop) + { + const terminator = yield* Process.Blocks.currentTerminator(); + if (!terminator) { + yield* Process.Blocks.terminate({ + kind: "jump", + target: continueTarget, + operationDebug: config.node + ? yield* Process.Debug.forAstNode(config.node) + : {}, + }); + } + } + + // Update block (only for 'for' loops) + if (updateBlock && config.update) { + yield* Process.Blocks.switchTo(updateBlock); + yield* buildStatement(config.update); + + // Before jumping back to header, update phi sources with loop values + yield* Process.Variables.updateLoopPhis( + loopPhis, + updateBlock, + headerBlock, + ); + + const terminator = yield* Process.Blocks.currentTerminator(); + if (!terminator) { + yield* Process.Blocks.terminate({ + kind: "jump", + target: headerBlock, + operationDebug: config.node + ? yield* Process.Debug.forAstNode(config.node) + : {}, + }); + } + } else if (!config.update) { + // For while loops, update phis before jumping from body to header + // This needs to happen at the end of the body block + const state: State = yield { type: "peek" }; + const currentBlockId = state.block.id; + yield* Process.Variables.updateLoopPhis( + loopPhis, + currentBlockId, + headerBlock, + ); + } + + // Continue from exit block + yield* Process.Blocks.switchTo(exitBlock); + }; + +/** + * Build a while statement + */ +export const makeBuildWhileStatement = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => { + const buildLoop = makeBuildLoop(buildStatement); + return function* buildWhileStatement( + stmt: Ast.Statement.ControlFlow.While, + ): Process { + yield* buildLoop({ + condition: stmt.condition, + body: stmt.body, + prefix: "while", + node: stmt, + }); + }; +}; + +/** + * Build a for statement + */ +export const makeBuildForStatement = ( + buildStatement: (stmt: Ast.Statement) => Process, +) => { + const buildLoop = makeBuildLoop(buildStatement); + return function* buildForStatement( + stmt: Ast.Statement.ControlFlow.For, + ): Process { + yield* buildLoop({ + init: stmt.init, + condition: stmt.condition, + update: stmt.update, + body: stmt.body, + prefix: "for", + node: stmt, + }); + }; +}; + +/** + * Build a return statement + */ +function* buildReturnStatement( + stmt: Ast.Statement.ControlFlow.Return, +): Process { + const value = stmt.value + ? yield* buildExpression(stmt.value, { kind: "rvalue" }) + : undefined; + + yield* Process.Blocks.terminate({ + kind: "return", + value, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); +} + +/** + * Build a break statement + */ +function* buildBreakStatement( + stmt: Ast.Statement.ControlFlow.Break, +): Process { + const loop = yield* Process.ControlFlow.currentLoop(); + + if (!loop) { + yield* Process.Errors.report( + new IrgenError( + "Break outside loop", + stmt.loc ?? undefined, + Severity.Error, + ), + ); + + return; + } + + yield* Process.Blocks.terminate({ + kind: "jump", + target: loop.breakTarget, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); +} + +/** + * Build a continue statement + */ +function* buildContinueStatement( + stmt: Ast.Statement.ControlFlow.Continue, +): Process { + const loop = yield* Process.ControlFlow.currentLoop(); + + if (!loop) { + yield* Process.Errors.report( + new IrgenError( + "Continue outside loop", + stmt.loc ?? undefined, + Severity.Error, + ), + ); + + return; + } + + yield* Process.Blocks.terminate({ + kind: "jump", + target: loop.continueTarget, + operationDebug: yield* Process.Debug.forAstNode(stmt), + }); +} diff --git a/packages/bugc/src/irgen/generate/statements/declare.ts b/packages/bugc/src/irgen/generate/statements/declare.ts new file mode 100644 index 00000000..fc011901 --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/declare.ts @@ -0,0 +1,220 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; + +import { Error as IrgenError } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; + +import { buildExpression } from "../expressions/index.js"; +import { Process } from "../process.js"; + +/** + * Build a declaration statement + */ +export function* buildDeclarationStatement( + stmt: Ast.Statement.Declare, +): Process { + const decl = stmt.declaration; + + switch (decl.kind) { + case "declaration:variable": + return yield* buildVariableDeclaration(decl as Ast.Declaration.Variable); + case "declaration:function": + // Function declarations are handled at module level + return; + case "declaration:parameter": + // Parameter declarations are part of function handling + return; + case "declaration:struct": + // Struct declarations are handled at module level + return; + case "declaration:storage": + // Storage declarations are handled at module level + return; + default: + return yield* Process.Errors.report( + new IrgenError( + `Unsupported declaration kind: ${decl.kind}`, + stmt.loc ?? undefined, + Severity.Error, + ), + ); + } +} + +/** + * Build a variable declaration + */ +function* buildVariableDeclaration( + decl: Ast.Declaration.Variable, +): Process { + // Infer type from the types map or use default + const type = yield* Process.Types.nodeType(decl); + const irType = type ? fromBugType(type) : Ir.Type.Scalar.uint256; + + // Check if this is a reference type that needs memory allocation + const needsMemoryAllocation = irType.kind === "ref"; + + if (needsMemoryAllocation) { + // For types that need memory allocation + + // For reference types, calculate size needed + let sizeValue: Ir.Value; + + // Check if we have an initializer to determine size + if (decl.initializer) { + // For hex literals, calculate actual size needed + if ( + Ast.Expression.isLiteral(decl.initializer) && + Ast.Expression.Literal.isHex(decl.initializer) + ) { + const hexLiteral = decl.initializer as Ast.Expression.Literal; + const hexValue = hexLiteral.value.startsWith("0x") + ? hexLiteral.value.slice(2) + : hexLiteral.value; + const byteSize = hexValue.length / 2; + // Add 32 bytes for length prefix + sizeValue = Ir.Value.constant( + BigInt(byteSize + 32), + Ir.Type.Scalar.uint256, + ); + } else { + // Default size for other initializers + sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256); + } + } else { + // Default size when no initializer + sizeValue = Ir.Value.constant(64n, Ir.Type.Scalar.uint256); + } + + // Allocate memory + const allocTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "allocate", + location: "memory", + size: sizeValue, + dest: allocTemp, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction); + + // Declare the SSA variable and directly use the allocTemp as its value + yield* Process.Variables.declareWithExistingTemp( + decl.name, + irType, + allocTemp, + ); + + // If there's an initializer, store the value in memory + if (decl.initializer) { + const value = yield* buildExpression(decl.initializer, { + kind: "rvalue", + }); + + // For reference types, we need to handle initialization + // Check the initializer type to determine how to store it + if (Ast.Expression.isLiteral(decl.initializer)) { + const hexLiteral = decl.initializer as Ast.Expression.Literal; + if (Ast.Expression.Literal.isHex(hexLiteral)) { + const hexValue = hexLiteral.value.startsWith("0x") + ? hexLiteral.value.slice(2) + : hexLiteral.value; + const byteSize = hexValue.length / 2; + + // Store length at the beginning + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256), + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.Write); + + // Store the actual bytes data after the length + const dataOffsetTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(allocTemp, Ir.Type.Scalar.uint256), + right: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + dest: dataOffsetTemp, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.BinaryOp); + + yield* Process.Instructions.emit({ + kind: "write", + location: "memory", + offset: Ir.Value.temp(dataOffsetTemp, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(BigInt(byteSize), Ir.Type.Scalar.uint256), + value: value, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.Write); + } + } else { + // For slice expressions and other bytes operations, + // the value is already a reference to memory + // We need to copy the slice result to the new allocation + // This is a simplified version - a full implementation would need to + // handle different cases more carefully + + // If the value is a temp, update the SSA variable to point to it + if (value.kind === "temp") { + yield* Process.Variables.updateSsaToExistingTemp( + decl.name, + value.id, + irType, + ); + } else { + // For non-temp values, we need to copy it + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: value, + right: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + dest: allocTemp, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.BinaryOp); + } + } + } + } else { + // Original logic for non-memory types + if (decl.initializer) { + const value = yield* buildExpression(decl.initializer, { + kind: "rvalue", + }); + const ssaVar = yield* Process.Variables.declare(decl.name, irType); + + // Generate assignment to the new SSA temp + if (value.kind === "temp") { + // If value is already a temp, just update SSA to use it + if (value.id !== ssaVar.currentTempId) { + yield* Process.Variables.updateSsaToExistingTemp( + decl.name, + value.id, + irType, + ); + } + } else if (value.kind === "const") { + // Create const instruction for constants + yield* Process.Instructions.emit({ + kind: "const", + value: value.value, + type: irType, + dest: ssaVar.currentTempId, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.Const); + } + } else { + // No initializer - declare with default value + const ssaVar = yield* Process.Variables.declare(decl.name, irType); + yield* Process.Instructions.emit({ + kind: "const", + value: 0n, + type: irType, + dest: ssaVar.currentTempId, + operationDebug: yield* Process.Debug.forAstNode(decl), + } as Ir.Instruction.Const); + } + } +} diff --git a/packages/bugc/src/irgen/generate/statements/express.ts b/packages/bugc/src/irgen/generate/statements/express.ts new file mode 100644 index 00000000..51a532e7 --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/express.ts @@ -0,0 +1,12 @@ +import type * as Ast from "#ast"; +import { type Process } from "../process.js"; +import { buildExpression } from "../expressions/index.js"; + +/** + * Build an expression statement + */ +export function* buildExpressionStatement( + stmt: Ast.Statement.Express, +): Process { + yield* buildExpression(stmt.expression, { kind: "rvalue" }); +} diff --git a/packages/bugc/src/irgen/generate/statements/index.ts b/packages/bugc/src/irgen/generate/statements/index.ts new file mode 100644 index 00000000..05de1f56 --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/index.ts @@ -0,0 +1 @@ +export { buildStatement, buildBlock } from "./statement.js"; diff --git a/packages/bugc/src/irgen/generate/statements/statement.ts b/packages/bugc/src/irgen/generate/statements/statement.ts new file mode 100644 index 00000000..e93bbdf8 --- /dev/null +++ b/packages/bugc/src/irgen/generate/statements/statement.ts @@ -0,0 +1,40 @@ +import type * as Ast from "#ast"; +import { assertExhausted } from "#irgen/errors"; + +import { Process } from "../process.js"; + +import { makeBuildBlock } from "./block.js"; + +import { buildExpressionStatement } from "./express.js"; +import { buildDeclarationStatement } from "./declare.js"; +import { makeBuildControlFlowStatement } from "./control-flow.js"; +import { buildAssignmentStatement } from "./assign.js"; + +const buildControlFlowStatement = makeBuildControlFlowStatement(buildStatement); + +export const buildBlock = makeBuildBlock(buildStatement); + +/** + * Build a statement + */ +export function* buildStatement(stmt: Ast.Statement): Process { + switch (stmt.kind) { + case "statement:declare": + return yield* buildDeclarationStatement(stmt as Ast.Statement.Declare); + case "statement:assign": + return yield* buildAssignmentStatement(stmt as Ast.Statement.Assign); + case "statement:control-flow:if": + case "statement:control-flow:for": + case "statement:control-flow:while": + case "statement:control-flow:return": + case "statement:control-flow:break": + case "statement:control-flow:continue": + return yield* buildControlFlowStatement( + stmt as Ast.Statement.ControlFlow, + ); + case "statement:express": + return yield* buildExpressionStatement(stmt as Ast.Statement.Express); + default: + assertExhausted(stmt); + } +} diff --git a/packages/bugc/src/irgen/generate/storage.ts b/packages/bugc/src/irgen/generate/storage.ts new file mode 100644 index 00000000..a84dfddc --- /dev/null +++ b/packages/bugc/src/irgen/generate/storage.ts @@ -0,0 +1,616 @@ +import * as Ast from "#ast"; +import * as Ir from "#ir"; +import { Severity } from "#result"; +import { Type } from "#types"; + +import { Error as IrgenError } from "#irgen/errors"; +import { fromBugType } from "#irgen/type"; +import { Process } from "./process.js"; +import { buildExpression } from "./expressions/index.js"; +import type { Context } from "./expressions/context.js"; + +/** + * Local storage information for IR generation + */ +interface StorageInfo { + slot: number; + name: string; + declaration: Ast.Declaration.Storage; +} + +export interface StorageAccessChain { + slot: StorageInfo; + accesses: Array<{ + kind: "index" | "member"; + key?: Ir.Value; + fieldName?: string; + fieldOffset?: number; + fieldType?: Ir.Type; + }>; +} + +/** + * Try to extract a complete storage access chain from an expression. + * Returns undefined if the expression isn't a pure storage access. + */ +export function* findStorageAccessChain( + expr: Ast.Expression, +): Process { + // Handle different expression types + if (Ast.Expression.isIdentifier(expr)) { + // Check if this is a storage variable + const storageSlot = yield* Process.Storage.findSlot(expr.name); + if (storageSlot) { + return { + slot: storageSlot, + accesses: [], + }; + } + return undefined; + } + + if (Ast.Expression.isAccess(expr) && Ast.Expression.Access.isIndex(expr)) { + // array[index] or mapping[key] + const indexExpr = expr as Ast.Expression.Access.Index; + const baseChain = yield* findStorageAccessChain(indexExpr.object); + if (!baseChain) return undefined; + + // Build the index value + const key = yield* buildExpression(indexExpr.index, { kind: "rvalue" }); + + baseChain.accesses.push({ + kind: "index", + key, + }); + return baseChain; + } + + if (Ast.Expression.isAccess(expr) && Ast.Expression.Access.isMember(expr)) { + // struct.field + const memberExpr = expr as Ast.Expression.Access.Member; + const baseChain = yield* findStorageAccessChain(memberExpr.object); + if (!baseChain) return undefined; + + baseChain.accesses.push({ + kind: "member", + fieldName: memberExpr.property, + }); + return baseChain; + } + + return undefined; +} + +/** + * Emit instructions to read from a storage location by following + * an access chain (e.g., accounts[user].balance) + */ +export function* emitStorageChainAccess( + expr: Ast.Expression, + _context: Context, +): Process { + const chain = yield* findStorageAccessChain(expr); + if (!chain) return undefined; + + // Get the type of the expression from the type checker + const exprType = yield* Process.Types.nodeType(expr); + const irType = exprType ? fromBugType(exprType) : Ir.Type.Scalar.uint256; + + // Build the expression to load from storage + const value = yield* emitStorageChainLoad(chain, irType, expr); + + return value; +} + +/** + * Determines field size in bytes based on type + */ +function getFieldSize(type: Ir.Type): number { + // Check origin to get semantic type info + if (type.origin !== "synthetic") { + if (Type.Elementary.isAddress(type.origin)) { + return 20; // addresses are 20 bytes + } else if (Type.Elementary.isBool(type.origin)) { + return 1; // bools are 1 byte + } else if (Type.Elementary.isBytes(type.origin) && type.origin.size) { + return type.origin.size; // fixed bytes + } else if (Type.Elementary.isUint(type.origin)) { + return (type.origin.bits || 256) / 8; + } + } + + // For scalars, use the size directly + if (type.kind === "scalar") { + return type.size; + } + + // Default to full slot for references and unknown types + return 32; +} + +/** + * Emit a storage chain load + */ +export function* emitStorageChainLoad( + chain: StorageAccessChain, + valueType: Ir.Type, + node: Ast.Node | undefined, +): Process { + // Get the Bug type from the type checker + const bugType = yield* Process.Types.nodeType(chain.slot.declaration); + + let currentSlot = Ir.Value.constant( + BigInt(chain.slot.slot), + Ir.Type.Scalar.uint256, + ); + + // Track the Bug type for semantic information + let currentOrigin = bugType; + + // Process each access in the chain + for (const access of chain.accesses) { + if (access.kind === "index" && access.key) { + // For mapping/array access + const tempId = yield* Process.Variables.newTemp(); + + // Check the origin to determine if it's a mapping or array + if (currentOrigin && Type.isMapping(currentOrigin)) { + // Mapping access - get key and value types from Bug type + const keyIrType = fromBugType(currentOrigin.key); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "mapping", + base: currentSlot, + key: access.key, + keyType: keyIrType, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.ComputeSlot); + // Update to the value type + currentOrigin = currentOrigin.value; + } else if (currentOrigin && Type.isArray(currentOrigin)) { + // Array access - first compute the array's first slot (hash of base) + const firstSlotTempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "array", + base: currentSlot, + dest: firstSlotTempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.ComputeSlot); + + // Then add the index to get the actual element slot + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(firstSlotTempId, Ir.Type.Scalar.uint256), + right: access.key, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.BinaryOp); + // Update to the element type + currentOrigin = currentOrigin.element; + } + + currentSlot = Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); + } else if (access.kind === "member" && access.fieldName) { + // For struct field access + if (currentOrigin && Type.isStruct(currentOrigin)) { + // Access struct information from the Bug type origin + const fieldType = currentOrigin.fields.get(access.fieldName); + const layout = currentOrigin.layout.get(access.fieldName); + + if (!fieldType || !layout) { + throw new Error( + `Field ${access.fieldName} not found in struct ${currentOrigin.name}`, + ); + } + + // For structs in mappings, we need to generate compute_slot.field + // to compute the field's slot offset + const fieldSlotOffset = Math.floor(layout.byteOffset / 32); + + if (fieldSlotOffset > 0) { + // Field is in a different slot, generate compute_slot.field + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "field", + base: currentSlot, + fieldOffset: fieldSlotOffset, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + }); + currentSlot = Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); + } + + // Store field info for later use in read/write + access.fieldOffset = layout.byteOffset; + // Store the field type so we know the correct size + access.fieldType = fromBugType(fieldType); + currentOrigin = fieldType; + } + } + } + + // Check if the last access was a struct field to get packed field info + let byteOffset = 0; + let fieldSize = 32; // Default to full slot + const lastAccess = chain.accesses[chain.accesses.length - 1]; + if ( + lastAccess && + lastAccess.kind === "member" && + lastAccess.fieldOffset !== undefined + ) { + // Calculate the byte offset within the slot + byteOffset = lastAccess.fieldOffset % 32; + + // Determine field size from type + if (lastAccess.fieldType) { + fieldSize = getFieldSize(lastAccess.fieldType); + } + } + + // Generate the final read instruction using new unified format + const loadTempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "read", + location: "storage", + slot: currentSlot, + offset: Ir.Value.constant(BigInt(byteOffset), Ir.Type.Scalar.uint256), + length: Ir.Value.constant(BigInt(fieldSize), Ir.Type.Scalar.uint256), + type: valueType, + dest: loadTempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.Read); + + return Ir.Value.temp(loadTempId, valueType); +} + +/** + * Emit a storage write for an access chain + */ +export function* emitStorageChainStore( + chain: StorageAccessChain, + value: Ir.Value, + node: Ast.Node | undefined, +): Process { + // Handle direct storage variable assignment (no accesses) + if (chain.accesses.length === 0) { + // Direct storage assignment using new unified format + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot: Ir.Value.constant(BigInt(chain.slot.slot), Ir.Type.Scalar.uint256), + offset: Ir.Value.constant(0n, Ir.Type.Scalar.uint256), + length: Ir.Value.constant(32n, Ir.Type.Scalar.uint256), + value, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.Write); + return; + } + + // Get the Bug type from the type checker + const bugType = yield* Process.Types.nodeType(chain.slot.declaration); + + // Compute the final storage slot through the chain + let currentSlot: Ir.Value = Ir.Value.constant( + BigInt(chain.slot.slot), + Ir.Type.Scalar.uint256, + ); + let currentOrigin = bugType; + + // Process each access in the chain + for (const access of chain.accesses) { + if (access.kind === "index" && access.key) { + // For mapping/array access + if (currentOrigin && Type.isMapping(currentOrigin)) { + // Mapping access + const slotTemp = yield* Process.Variables.newTemp(); + const keyIrType = fromBugType(currentOrigin.key); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "mapping", + base: currentSlot, + key: access.key, + keyType: keyIrType, + dest: slotTemp, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.ComputeSlot); + currentSlot = Ir.Value.temp(slotTemp, Ir.Type.Scalar.uint256); + currentOrigin = currentOrigin.value; + } else if (currentOrigin && Type.isArray(currentOrigin)) { + // Array access - first compute the array's first slot (hash of base) + const firstSlotTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "array", + base: currentSlot, + dest: firstSlotTemp, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + }); + + // Then add the index to get the actual element slot + const slotTemp = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "binary", + op: "add", + left: Ir.Value.temp(firstSlotTemp, Ir.Type.Scalar.uint256), + right: access.key, + dest: slotTemp, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + }); + currentSlot = Ir.Value.temp(slotTemp, Ir.Type.Scalar.uint256); + currentOrigin = currentOrigin.element; + } + } else if (access.kind === "member" && access.fieldName) { + // For struct field access + if (currentOrigin && Type.isStruct(currentOrigin)) { + const fieldType = currentOrigin.fields.get(access.fieldName); + const layout = currentOrigin.layout.get(access.fieldName); + + if (fieldType && layout) { + // Calculate the slot offset for the field + const fieldSlotOffset = Math.floor(layout.byteOffset / 32); + + if (fieldSlotOffset > 0) { + // Field is in a different slot + const tempId = yield* Process.Variables.newTemp(); + yield* Process.Instructions.emit({ + kind: "compute_slot", + slotKind: "field", + base: currentSlot, + fieldOffset: fieldSlotOffset, + dest: tempId, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + }); + currentSlot = Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); + } + + // Store field info for later use in write + access.fieldOffset = layout.byteOffset; + // Store the field type so we know the correct size + access.fieldType = fromBugType(fieldType); + currentOrigin = fieldType; + } else { + yield* Process.Errors.report( + new IrgenError( + `Field ${access.fieldName} not found in struct`, + node?.loc ?? undefined, + Severity.Error, + ), + ); + } + } + } + } + + // Check if the last access was a struct field to handle packed fields + let byteOffset = 0; + let fieldSize: number | undefined; + const lastAccess = chain.accesses[chain.accesses.length - 1]; + if ( + lastAccess && + lastAccess.kind === "member" && + lastAccess.fieldOffset !== undefined + ) { + // Calculate the byte offset within the slot + byteOffset = lastAccess.fieldOffset % 32; + + // Determine field size from type + if (lastAccess.fieldType) { + fieldSize = getFieldSize(lastAccess.fieldType); + } + } + + // Determine the actual field size to write + const actualFieldSize = fieldSize !== undefined ? fieldSize : 32; + + // Store to the computed slot using new unified format + yield* Process.Instructions.emit({ + kind: "write", + location: "storage", + slot: currentSlot, + offset: Ir.Value.constant(BigInt(byteOffset), Ir.Type.Scalar.uint256), + length: Ir.Value.constant(BigInt(actualFieldSize), Ir.Type.Scalar.uint256), + value, + operationDebug: node ? yield* Process.Debug.forAstNode(node) : {}, + } as Ir.Instruction.Write); +} + +// The rest of the file contains commented-out old implementations +// which we can keep for reference... + +// /** +// * Generate storage access for a complete chain (e.g., accounts[user].balance) +// */ +// export function* generateStorageAccess( +// chain: StorageAccessChain, +// context: Context, +// ): Process { +// const { +// emit, +// newTemp, +// getNodeType, +// } = yield* Process.all(); +// +// // Start with the base storage slot +// let currentSlot = Ir.Value.constant(BigInt(chain.slot.slot), { +// kind: "uint", +// bits: 256, +// }); +// let currentType = chain.slot.type; +// +// // Process each access in the chain +// for (const access of chain.accesses) { +// if (access.kind === "index" && access.key) { +// // For mapping/array access, compute the slot +// const tempId = yield* newTemp(); +// +// yield* emit({ +// kind: "compute_slot", +// baseSlot: currentSlot, +// key: access.key, +// dest: tempId, +// loc, +// } as Ir.Instruction); +// +// currentSlot = Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); +// +// // Update type based on mapping/array element type +// if (currentType.kind === "mapping") { +// currentType = currentType.value || Ir.Type.Scalar.uint256; +// } else if (currentType.kind === "array") { +// currentType = currentType.element || Ir.Type.Scalar.uint256; +// } +// } else if (access.kind === "member" && access.fieldName) { +// // For struct field access +// if (currentType.kind === "struct") { +// const fieldIndex = +// currentType.fields.findIndex( +// ({ name }) => name === access.fieldName, +// ) ?? 0; +// const tempId = yield* newTemp(); +// +// yield* emit({ +// kind: "compute_field_offset", +// baseSlot: currentSlot, +// fieldIndex, +// dest: tempId, +// loc, +// } as Ir.Instruction); +// +// currentSlot = Ir.Value.temp(tempId, Ir.Type.Scalar.uint256); +// currentType = currentType.fields[fieldIndex]?.type || { +// kind: "uint", +// bits: 256, +// }; +// } +// } +// } +// +// // Load from the final slot +// const loadTempId = yield* newTemp(); +// yield* emit({ +// kind: "load_storage", +// slot: currentSlot, +// dest: loadTempId, +// loc, +// } as Ir.Instruction); +// +// return Ir.Value.temp(loadTempId, currentType); +// } + +/** + * Emit storage chain store for assignment + */ +// export function* generateStorageStore( +// chain: StorageAccessChain, +// value: Ir.Value, +// loc: Ast.SourceLocation | undefined, +// ): Process { +// const { emit, newTemp } = yield* Process.all(); +// +// // Handle direct storage variable assignment (no accesses) +// if (chain.accesses.length === 0) { +// yield* emit({ +// kind: "store_storage", +// slot: Ir.Value.constant(BigInt(chain.slot.slot), { +// kind: "uint", +// bits: 256, +// }), +// value, +// loc, +// } as Ir.Instruction); +// return; +// } +// +// // Compute the final storage slot through the chain +// let currentSlot: Ir.Value = Ir.Value.constant(BigInt(chain.slot.slot), { +// kind: "uint", +// bits: 256, +// }); +// let currentType = chain.slot.type; +// +// // Process each access in the chain +// for (const access of chain.accesses) { +// if (access.kind === "index" && access.key) { +// // For mapping/array access +// if (currentType.kind === "mapping") { +// // Mapping access +// const slotTemp = yield* newTemp(); +// yield* emit({ +// kind: "compute_slot", +// baseSlot: currentSlot, +// key: access.key, +// dest: slotTemp, +// loc, +// } as Ir.Instruction); +// currentSlot = Ir.Value.temp(slotTemp, Ir.Type.Scalar.uint256); +// currentType = (currentType as { kind: "mapping"; value: Ir.Type }) +// .value; +// } else if (currentType.kind === "array") { +// // Array access +// const baseSlotTemp = yield* newTemp(); +// yield* emit({ +// kind: "compute_array_slot", +// baseSlot: currentSlot, +// dest: baseSlotTemp, +// loc, +// } as Ir.Instruction); +// +// // Add the index to get the final slot +// const finalSlotTemp = yield* newTemp(); +// yield* emit({ +// kind: "binary", +// op: "add", +// left: Ir.Value.temp(baseSlotTemp, Ir.Type.Scalar.uint256), +// right: access.key, +// dest: finalSlotTemp, +// loc, +// } as Ir.Instruction); +// +// currentSlot = Ir.Value.temp(finalSlotTemp, { +// kind: "uint", +// bits: 256, +// }); +// currentType = (currentType as { kind: "array"; element: Ir.Type }) +// .element; +// } +// } else if (access.kind === "member" && access.fieldName) { +// // For struct field access +// if (currentType.kind === "struct") { +// const fieldIndex = +// currentType.fields.findIndex( +// ({ name }) => name === access.fieldName, +// ) ?? 0; +// const slotTemp = yield* newTemp(); +// +// yield* emit({ +// kind: "compute_field_offset", +// baseSlot: currentSlot, +// fieldIndex, +// dest: slotTemp, +// loc, +// } as Ir.Instruction); +// +// currentSlot = Ir.Value.temp(slotTemp, { +// kind: "uint", +// bits: 256, +// }); +// currentType = currentType.fields[fieldIndex]?.type || { +// kind: "uint", +// bits: 256, +// }; +// } +// } +// } +// +// // Store to the final slot +// yield* emit({ +// kind: "store_storage", +// slot: currentSlot, +// value, +// loc, +// } as Ir.Instruction); +// } diff --git a/packages/bugc/src/irgen/generator.error.test.ts b/packages/bugc/src/irgen/generator.error.test.ts new file mode 100644 index 00000000..7e7a1fed --- /dev/null +++ b/packages/bugc/src/irgen/generator.error.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect } from "vitest"; +import { compile } from "#compiler"; +import type * as Ir from "#ir"; +import { Result, Severity } from "#result"; +import type { BugError } from "#errors"; +import "#test/matchers"; + +describe("generateModule error handling", () => { + const compileTest = async ( + source: string, + ): Promise> => { + return Result.map( + await compile({ to: "ir", source, sourcePath: "test.bug" }), + ({ ir }) => ir, + ); + }; + + it("should propagate type errors instead of defaulting to uint256", async () => { + // This test verifies that when type checking fails, the IR generator + // reports errors rather than silently defaulting to uint256 + const source = ` + name Test; + storage { + [0] x: uint256; + } + code { + // This should fail type checking because y is not declared + let z = x + y; + return; + } + `; + + const result = await compileTest(source); + + expect(result.success).toBe(false); + expect(Result.hasErrors(result)).toBe(true); + + // Check that we get a proper type error, not an IR error + const typeErrors = Result.findMessages(result, { + severity: Severity.Error, + }).filter((d) => d.code.startsWith("TYPE")); + expect(typeErrors.length).toBeGreaterThan(0); + }); + + it("should handle missing types gracefully in IR generation", async () => { + // Even if type checking somehow passes an expression without a type, + // the IR generator should report a clear error + const source = ` + name Test; + storage { + [0] x: uint256; + } + code { + // This creates a scenario where getType might return null + let a = 42; + let b = a; // This should work + return; + } + `; + + const result = await compileTest(source); + + if (!result.success) { + // If it fails, it should be with clear error messages + const irErrors = Result.findMessages(result, { + severity: Severity.Error, + }).filter((d) => d.code === "IR_ERROR"); + for (const error of irErrors) { + expect(error.message).not.toContain("Cannot read properties of null"); + expect(error.message).not.toContain( + "Cannot read properties of undefined", + ); + } + } + }); + + it("should provide meaningful errors for type conversion failures", async () => { + const source = ` + name Test; + + define { + struct Custom { + field: uint256; + }; + } + + storage { + [0] data: Custom; + } + + code { + // This should work - accessing struct field + let x = data.field; + return; + } + `; + + const result = await compileTest(source); + + // This should compile successfully + expect(result.success).toBe(true); + + // No warnings about defaulting to uint256 + const warnings = Result.warnings(result) || []; + const defaultingWarnings = warnings.filter( + (w) => w.message.includes("defaulting") || w.message.includes("uint256"), + ); + expect(defaultingWarnings).toHaveLength(0); + }); +}); diff --git a/packages/bugc/src/irgen/generator.test.ts b/packages/bugc/src/irgen/generator.test.ts new file mode 100644 index 00000000..a448bfca --- /dev/null +++ b/packages/bugc/src/irgen/generator.test.ts @@ -0,0 +1,695 @@ +import { describe, expect, it } from "vitest"; +import { parse } from "#parser"; +import * as TypeChecker from "#typechecker"; +import { generateModule } from "./generator.js"; +import * as Ir from "#ir"; +import { Result, Severity } from "#result"; +import "#test/matchers"; + +describe("generateModule", () => { + function buildIR(source: string): Ir.Module { + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + `Parse error: ${Result.firstError(parseResult)?.message || "Unknown error"}`, + ); + } + const ast = parseResult.value; + const typeCheckResult = TypeChecker.checkProgram(ast); + + if (!typeCheckResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeCheckResult)?.message || "Unknown error"), + ); + } + + const buildResult = generateModule(ast, typeCheckResult.value.types); + + if (!buildResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(buildResult)?.message || "Unknown error"), + ); + } + + return buildResult.value; + } + + describe("basic programs", () => { + it("should build IR for empty program", () => { + const source = ` + name Empty; + storage {} + code {} + `; + + const ir = buildIR(source); + + expect(ir.name).toBe("Empty"); + expect(ir.main.blocks.size).toBe(1); + expect(ir.main.blocks.get("entry")).toBeDefined(); + }); + }); + + describe("expressions", () => { + it("should generate IR for arithmetic expressions", () => { + const source = ` + name Arithmetic; + storage {} + code { + let x = 5 + 3 * 2; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + // Should have: const 5, const 3, const 2, mul, add (no more "add 0" for assignment) + expect(entry.instructions).toHaveLength(5); + + // Check constants (5 is loaded first as left operand of +) + expect(entry.instructions[0]).toMatchObject({ + kind: "const", + value: 5n, + }); + expect(entry.instructions[1]).toMatchObject({ + kind: "const", + value: 3n, + }); + expect(entry.instructions[2]).toMatchObject({ + kind: "const", + value: 2n, + }); + + // Check multiplication (3 * 2) + expect(entry.instructions[3]).toMatchObject({ + kind: "binary", + op: "mul", + }); + + // Check addition (5 + result) + expect(entry.instructions[4]).toMatchObject({ + kind: "binary", + op: "add", + }); + + // We no longer generate "add 0" for assignments - the result is directly used + }); + + it("should generate IR for comparison expressions", () => { + const source = ` + name Comparison; + storage {} + code { + let result = 10 > 5 && 3 <= 3; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + // Find the comparison instructions + const gtInst = entry.instructions.find( + (i: Ir.Instruction) => i.kind === "binary" && i.op === "gt", + ); + const leInst = entry.instructions.find( + (i: Ir.Instruction) => i.kind === "binary" && i.op === "le", + ); + const andInst = entry.instructions.find( + (i: Ir.Instruction) => i.kind === "binary" && i.op === "and", + ); + + expect(gtInst).toBeDefined(); + expect(leInst).toBeDefined(); + expect(andInst).toBeDefined(); + }); + }); + + describe("control flow", () => { + it("should generate basic blocks for if statements", () => { + const source = ` + name IfStatement; + storage {} + code { + let x = 10; + if (x > 5) { + x = 20; + } + } + `; + + const ir = buildIR(source); + + // Should have: entry, then_1, merge_2 + expect(ir.main.blocks.size).toBe(3); + expect(Array.from(ir.main.blocks.keys())).toContain("entry"); + expect(Array.from(ir.main.blocks.keys())).toContain("then_1"); + expect(Array.from(ir.main.blocks.keys())).toContain("merge_2"); + + // Check branch in entry block + const entry = ir.main.blocks.get("entry")!; + expect(entry.terminator).toMatchObject({ + kind: "branch", + trueTarget: "then_1", + falseTarget: "merge_2", + }); + }); + + it("should generate basic blocks for if-else statements", () => { + const source = ` + name IfElse; + storage {} + code { + let x = 10; + if (x > 5) { + x = 20; + } else { + x = 30; + } + } + `; + + const ir = buildIR(source); + + // Should have: entry, then, else, merge blocks + expect(ir.main.blocks.size).toBe(4); + const blockIds = Array.from(ir.main.blocks.keys()); + expect(blockIds).toContain("entry"); + expect(blockIds.some((id) => id.startsWith("then_"))).toBe(true); + expect(blockIds.some((id) => id.startsWith("else_"))).toBe(true); + expect(blockIds.some((id) => id.startsWith("merge_"))).toBe(true); + }); + + it("should generate basic blocks for for loops", () => { + const source = ` + name ForLoop; + storage {} + code { + for (let i = 0; i < 10; i = i + 1) { + } + } + `; + + const ir = buildIR(source); + + // Should have: entry, for_header, for_body, for_update, for_exit + expect(ir.main.blocks.size).toBeGreaterThanOrEqual(4); + const blockIds = Array.from(ir.main.blocks.keys()); + expect(blockIds.some((id) => id.includes("for_header"))).toBe(true); + expect(blockIds.some((id) => id.includes("for_body"))).toBe(true); + expect(blockIds.some((id) => id.includes("for_exit"))).toBe(true); + }); + + it("should handle break in loops", () => { + const source = ` + name BreakLoop; + storage {} + code { + for (let i = 0; i < 100; i = i + 1) { + if (i >= 10) { + break; + } + } + } + `; + + const ir = buildIR(source); + + // Find blocks with break jumps + let hasBreakJump = false; + + for (const block of ir.main.blocks.values()) { + if (block.terminator.kind === "jump") { + if (block.terminator.target.includes("for_exit")) { + hasBreakJump = true; + } + } + } + + expect(hasBreakJump).toBe(true); + }); + + it("should handle return statements", () => { + const source = ` + name Return; + storage {} + code { + if (true) { + return; + } + let x = 10; + } + `; + + const ir = buildIR(source); + + // Find the then block + const thenBlock = Array.from(ir.main.blocks.values()).find((b) => + b.id.startsWith("then_"), + ); + + expect(thenBlock).toBeDefined(); + expect(thenBlock!.terminator).toMatchObject({ + kind: "return", + }); + }); + }); + + describe("storage access", () => { + it("should generate load_storage for reading storage", () => { + const source = ` + name LoadStorage; + storage { + [0] value: uint256; + } + code { + let x = value; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + const loadInst = entry.instructions.find( + (i) => i.kind === "read" && i.location === "storage", + ); + expect(loadInst).toMatchObject({ + kind: "read", + location: "storage", + slot: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + offset: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + length: { + kind: "const", + value: 32n, + type: Ir.Type.Scalar.uint256, + }, + dest: expect.stringMatching(/^t\d+$/), + }); + }); + + it("should generate store_storage for writing storage", () => { + const source = ` + name StoreStorage; + storage { + [0] value: uint256; + } + code { + value = 42; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + const storeInst = entry.instructions.find( + (i) => i.kind === "write" && i.location === "storage", + ); + expect(storeInst).toMatchObject({ + kind: "write", + location: "storage", + slot: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + offset: { + kind: "const", + value: 0n, + type: Ir.Type.Scalar.uint256, + }, + length: { + kind: "const", + value: 32n, + type: Ir.Type.Scalar.uint256, + }, + }); + }); + }); + + describe("special expressions", () => { + it("should generate env instructions for msg properties", () => { + const source = ` + name MsgProperties; + storage {} + code { + let sender = msg.sender; + let value = msg.value; + let data = msg.data; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + const senderInst = entry.instructions.find( + (i) => i.kind === "env" && i.op === "msg_sender", + ); + const valueInst = entry.instructions.find( + (i) => i.kind === "env" && i.op === "msg_value", + ); + const dataInst = entry.instructions.find( + (i) => i.kind === "env" && i.op === "msg_data", + ); + + expect(senderInst).toBeDefined(); + expect(valueInst).toBeDefined(); + expect(dataInst).toBeDefined(); + }); + + it("should generate env instructions for block properties", () => { + const source = ` + name BlockProperties; + storage {} + code { + let num = block.number; + let time = block.timestamp; + } + `; + + const ir = buildIR(source); + const entry = ir.main.blocks.get("entry")!; + + const numberInst = entry.instructions.find( + (i) => i.kind === "env" && i.op === "block_number", + ); + const timestampInst = entry.instructions.find( + (i) => i.kind === "env" && i.op === "block_timestamp", + ); + + expect(numberInst).toBeDefined(); + expect(timestampInst).toBeDefined(); + }); + }); + + describe("complex programs", () => { + it("should build IR for counter example", () => { + const source = ` + name Counter; + storage { + [0] count: uint256; + [1] owner: address; + } + code { + if (msg.sender != owner) { + return; + } + count = count + 1; + } + `; + + const ir = buildIR(source); + + // Should have multiple basic blocks + expect(ir.main.blocks.size).toBeGreaterThan(1); + + // Should have env instruction for msg.sender + let hasMsgSender = false; + for (const block of ir.main.blocks.values()) { + if ( + block.instructions.some( + (i) => i.kind === "env" && i.op === "msg_sender", + ) + ) { + hasMsgSender = true; + break; + } + } + expect(hasMsgSender).toBe(true); + + // Should have storage operations + let hasStorageOps = false; + for (const block of ir.main.blocks.values()) { + if ( + block.instructions.some( + (i) => + (i.kind === "read" && i.location === "storage") || + (i.kind === "write" && i.location === "storage"), + ) + ) { + hasStorageOps = true; + break; + } + } + expect(hasStorageOps).toBe(true); + }); + }); + + describe("Complex storage access patterns", () => { + it("should handle nested mapping access with warnings", () => { + const source = ` + name NestedMappings; + + storage { + [0] allowances: mapping>; + } + + code { + allowances[msg.sender][0x1234567890123456789012345678901234567890] = 1000; + let amount = allowances[msg.sender][0x1234567890123456789012345678901234567890]; + } + `; + + // Build IR using custom function that collects diagnostics + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + `Parse error: ${Result.firstError(parseResult)?.message || "Unknown error"}`, + ); + } + const ast = parseResult.value; + const typeCheckResult = TypeChecker.checkProgram(ast); + + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + const buildResult = generateModule(ast, typeCheckResult.value.types); + + expect(buildResult.success).toBe(true); + if (!buildResult.success) return; + + const ir = buildResult.value; + const warnings = Result.findMessages(buildResult, { + severity: Severity.Warning, + }).filter((d) => d.severity === "warning"); + + // Should no longer have warnings about simplified IR + expect(warnings.length).toBe(0); + + // Should generate compute_slot and read/write storage with dynamic slots + let hasComputeSlot = false; + let hasReadStorageDynamic = false; + let hasWriteStorageDynamic = false; + for (const block of ir.main.blocks.values()) { + for (const inst of block.instructions) { + if (inst.kind === "compute_slot") hasComputeSlot = true; + if ( + inst.kind === "read" && + inst.location === "storage" && + inst.slot && + inst.slot.kind !== "const" + ) + hasReadStorageDynamic = true; + if ( + inst.kind === "write" && + inst.location === "storage" && + inst.slot && + inst.slot.kind !== "const" + ) + hasWriteStorageDynamic = true; + } + } + expect(hasComputeSlot).toBe(true); + expect(hasReadStorageDynamic).toBe(true); + expect(hasWriteStorageDynamic).toBe(true); + }); + + it("should handle struct field access in mappings", () => { + const source = ` + name StructInMapping; + + define { + struct Account { + balance: uint256; + nonce: uint256; + }; + } + + storage { + [0] accounts: mapping; + } + + code { + accounts[msg.sender].balance = 100; + accounts[msg.sender].nonce = accounts[msg.sender].nonce + 1; + let bal = accounts[msg.sender].balance; + } + `; + + const ir = buildIR(source); + + // Should generate compute_slot for both mapping and field operations + let hasMappingSlot = false; + let hasFieldSlot = false; + let hasReadStorageDynamic = false; + let hasWriteStorageDynamic = false; + + for (const block of ir.main.blocks.values()) { + for (const inst of block.instructions) { + if (inst.kind === "compute_slot") { + if (inst.slotKind === "mapping") hasMappingSlot = true; + if (inst.slotKind === "field") hasFieldSlot = true; + } + if ( + inst.kind === "read" && + inst.location === "storage" && + inst.slot && + inst.slot.kind !== "const" + ) + hasReadStorageDynamic = true; + if ( + inst.kind === "write" && + inst.location === "storage" && + inst.slot && + inst.slot.kind !== "const" + ) + hasWriteStorageDynamic = true; + } + } + + expect(hasMappingSlot).toBe(true); + expect(hasFieldSlot).toBe(true); + expect(hasReadStorageDynamic).toBe(true); + expect(hasWriteStorageDynamic).toBe(true); + }); + + it("should handle triple nested mappings", () => { + const source = ` + name ComplexPatterns; + + storage { + [0] data: mapping>>; + } + + code { + // Triple nested mapping + data[msg.sender][1][2] = 42; + } + `; + + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + `Parse error: ${Result.firstError(parseResult)?.message || "Unknown error"}`, + ); + } + const ast = parseResult.value; + const typeCheckResult = TypeChecker.checkProgram(ast); + + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + const buildResult = generateModule(ast, typeCheckResult.value.types); + + expect(buildResult.success).toBe(true); + if (!buildResult.success) return; + + // Verify that IR is generated for triple nested mappings + let instructionCount = 0; + let computeSlotCount = 0; + + for (const block of buildResult.value.main.blocks.values()) { + instructionCount += block.instructions.length; + for (const inst of block.instructions) { + if (inst.kind === "compute_slot") { + computeSlotCount++; + } + } + } + + // Should generate instructions including 3 compute_slot operations + expect(instructionCount).toBeGreaterThan(0); + expect(computeSlotCount).toBe(3); + }); + + it("should handle array element access in mappings", () => { + const source = ` + name ArrayInMapping; + + storage { + [0] counts: mapping>; + } + + code { + // Array element access in mapping - currently generates + // load_mapping followed by load_index/store_index + // but doesn't store array back (incorrect!) + counts[2][3] = 42; + let count = counts[2][3]; + } + `; + + // With our storage chain detection, it should emit warnings + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + `Parse error: ${Result.firstError(parseResult)?.message || "Unknown error"}`, + ); + } + const ast = parseResult.value; + const typeCheckResult = TypeChecker.checkProgram(ast); + expect(typeCheckResult.success).toBe(true); + if (!typeCheckResult.success) return; + + const buildResult = generateModule(ast, typeCheckResult.value.types); + expect(buildResult.success).toBe(true); + if (!buildResult.success) return; + + const warnings = Result.findMessages(buildResult, { + severity: Severity.Warning, + }).filter((d) => d.severity === "warning"); + + // Should no longer have warnings + expect(warnings.length).toBe(0); + + // Check that the proper instructions are generated + let hasMappingSlot = false; + let hasArraySlot = false; + let hasBinaryAdd = false; + let hasStorageDynamic = false; + + for (const block of buildResult.value.main.blocks.values()) { + for (const inst of block.instructions) { + if (inst.kind === "compute_slot") { + if (inst.slotKind === "mapping") hasMappingSlot = true; + if (inst.slotKind === "array") hasArraySlot = true; + } + if (inst.kind === "binary" && inst.op === "add") hasBinaryAdd = true; + if ( + ((inst.kind === "read" && inst.location === "storage") || + (inst.kind === "write" && inst.location === "storage")) && + inst.slot && + inst.slot.kind !== "const" + ) { + hasStorageDynamic = true; + } + } + } + + expect(hasMappingSlot).toBe(true); + // Both fixed and dynamic arrays now use compute_slot with kind="array" for proper storage layout + expect(hasArraySlot).toBe(true); + // We now generate "add" instructions to compute array element slots + expect(hasBinaryAdd).toBe(true); + expect(hasStorageDynamic).toBe(true); + }); + }); +}); diff --git a/packages/bugc/src/irgen/generator.ts b/packages/bugc/src/irgen/generator.ts new file mode 100644 index 00000000..2393a997 --- /dev/null +++ b/packages/bugc/src/irgen/generator.ts @@ -0,0 +1,112 @@ +import type * as Ast from "#ast"; +import * as Ir from "#ir"; +import type { Types } from "#types"; +import { Result, Severity } from "#result"; + +import { Error as IrgenError } from "#irgen/errors"; + +import type { State } from "./generate/state.js"; +import { Process } from "./generate/process.js"; +import { buildModule } from "#irgen/generate"; + +/** + * Generate IR from an AST program (public API) + */ +export function generateModule( + program: Ast.Program, + types: Types, +): Result { + // Create initial state + const initialState = createInitialState(program, types); + + // Run the generator + const result = Process.run(buildModule(program, types), initialState); + const { state, value: module } = result; + + // Check if there are any errors + const hasErrors = state.errors.length > 0; + + // Build messages object + const messages: Result["messages"] = {}; + + if (state.errors.length > 0) { + messages[Severity.Error] = state.errors; + } + + if (state.warnings.length > 0) { + messages[Severity.Warning] = state.warnings; + } + + // Return Result based on whether there were errors + if (hasErrors || !module) { + return { + success: false, + messages, + }; + } + + return { + success: true, + value: module, + messages, + }; +} + +/** + * Create the initial IR generation state + */ +function createInitialState(program: Ast.Program, types: Types): State { + // Create errors array to collect any type resolution errors + const errors: IrgenError[] = []; + + // Create initial module + const module: State.Module = { + name: program.name, + functions: new Map(), + storageDeclarations: program.storage ?? [], + }; + + // Create empty function context (will be replaced when building functions) + const function_: State.Function = { + id: "", + parameters: [], + blocks: new Map(), + }; + + // Create initial block context + const block = { + id: "entry", + instructions: [], + terminator: undefined, + predecessors: new Set(), + phis: [], + }; + + // Create initial scope + const scopes = { + stack: [{ ssaVars: new Map(), usedNames: new Map() }], + }; + + // Create initial counters + const counters = { + temp: 0, + block: 1, // Start at 1 to match test expectations + }; + + // Create empty loop stack + const loops = { + stack: [], + }; + + return { + module, + function: function_, + block, + scopes, + loops, + counters, + types, + errors, + warnings: [], + }; +} diff --git a/packages/bugc/src/irgen/index.ts b/packages/bugc/src/irgen/index.ts new file mode 100644 index 00000000..dead05fe --- /dev/null +++ b/packages/bugc/src/irgen/index.ts @@ -0,0 +1,2 @@ +export { generateModule } from "./generator.js"; +export { Error, ErrorCode } from "./errors.js"; diff --git a/packages/bugc/src/irgen/length.test.ts b/packages/bugc/src/irgen/length.test.ts new file mode 100644 index 00000000..3bb1d36b --- /dev/null +++ b/packages/bugc/src/irgen/length.test.ts @@ -0,0 +1,264 @@ +import { describe, expect, it } from "vitest"; +import { parse } from "#parser"; +import * as TypeChecker from "#typechecker"; +import { generateModule } from "./generator.js"; + +describe("IR Builder - Length Instructions", () => { + it("should generate const for fixed-size array length", () => { + const source = ` + name ArrayLength; + + storage { + [0] arr: array; + [1] len: uint256; + } + + code { + len = arr.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + const main = ir.value.main; + const entryBlock = main.blocks.get(main.entry)!; + + // For fixed-size arrays, length is a compile-time constant + const constInst = entryBlock.instructions.find( + (inst) => inst.kind === "const" && inst.value === 10n, + ); + + expect(constInst).toBeDefined(); + expect(constInst?.kind).toBe("const"); + } + }); + + it("should generate length instruction for msg.data", () => { + const source = ` + name DataLength; + + storage { + [0] dataSize: uint256; + } + + code { + dataSize = msg.data.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + const main = ir.value.main; + const entryBlock = main.blocks.get(main.entry)!; + + // Should have: msg_data, length, store_storage + const instructions = entryBlock.instructions; + + const msgDataInst = instructions.find((inst) => inst.kind === "env"); + expect(msgDataInst).toBeDefined(); + + const lengthInst = instructions.find((inst) => inst.kind === "length"); + expect(lengthInst).toBeDefined(); + expect(lengthInst?.kind).toBe("length"); + + if (lengthInst && lengthInst.kind === "length") { + expect(lengthInst.object.type?.kind).toBe("ref"); + if (lengthInst.object.type?.kind === "ref") { + expect(lengthInst.object.type.location).toBe("memory"); + } + } + } + }); + + it("should generate length in conditional expressions", () => { + const source = ` + name LengthCondition; + + storage { + [0] isLarge: bool; + } + + code { + if (msg.data.length > 100) { + isLarge = true; + } else { + isLarge = false; + } + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + const main = ir.value.main; + const entryBlock = main.blocks.get(main.entry)!; + + // Should have msg_data, length, const 100, gt comparison + const lengthInst = entryBlock.instructions.find( + (inst) => inst.kind === "length", + ); + expect(lengthInst).toBeDefined(); + + const compareInst = entryBlock.instructions.find( + (inst) => inst.kind === "binary" && inst.op === "gt", + ); + expect(compareInst).toBeDefined(); + } + }); + + it("should generate const for fixed-size array loop bounds", () => { + const source = ` + name LengthLoop; + + storage { + [0] arr: array; + } + + code { + for (let i = 0; i < arr.length; i = i + 1) { + arr[i] = i * 2; + } + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + const main = ir.value.main; + + // For fixed-size arrays, length should be a compile-time constant (5) + let constInstructionFound = false; + for (const [, block] of main.blocks) { + const constInst = block.instructions.find( + (inst) => inst.kind === "const" && inst.value === 5n, + ); + if (constInst) { + constInstructionFound = true; + break; + } + } + + expect(constInstructionFound).toBe(true); + } + }); + + it("should handle nested access with fixed-size length", () => { + const source = ` + name NestedLength; + + storage { + [0] matrix: array, 2>; + [1] result: uint256; + } + + code { + result = matrix[0].length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + const main = ir.value.main; + const entryBlock = main.blocks.get(main.entry)!; + + // For fixed-size inner arrays, length is a compile-time constant (3) + // We don't need to load from storage to get the length + const constInst = entryBlock.instructions.find( + (inst) => inst.kind === "const" && inst.value === 3n, + ); + expect(constInst).toBeDefined(); + } + }); + + it("should handle string length in functions", () => { + const source = ` + name StringLength; + + define { + function checkString(s: string) -> bool { + return s.length > 0; + }; + } + + code { + let hasData = checkString("test"); + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const checkResult = TypeChecker.checkProgram(ast.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) return; + + const ir = generateModule(ast.value, checkResult.value.types); + expect(ir.success).toBe(true); + + if (ir.success) { + // Check the function has a length instruction + const checkStringFunc = ir.value.functions?.get("checkString"); + expect(checkStringFunc).toBeDefined(); + + if (checkStringFunc) { + let lengthFound = false; + for (const [, block] of checkStringFunc.blocks) { + if (block.instructions.some((inst) => inst.kind === "length")) { + lengthFound = true; + break; + } + } + expect(lengthFound).toBe(true); + } + } + }); +}); diff --git a/packages/bugc/src/irgen/pass.ts b/packages/bugc/src/irgen/pass.ts new file mode 100644 index 00000000..fc8728fb --- /dev/null +++ b/packages/bugc/src/irgen/pass.ts @@ -0,0 +1,29 @@ +import type { Program } from "#ast"; +import type { Types } from "#types"; +import type * as Ir from "#ir"; +import { Result } from "#result"; +import type { Pass } from "#compiler"; + +import { Error as IrgenError } from "./errors.js"; +import { generateModule } from "./generator.js"; + +/** + * IR generation pass - converts typed AST to intermediate representation + * and inserts phi nodes for proper SSA form + */ +const pass: Pass<{ + needs: { + ast: Program; + types: Types; + }; + adds: { + ir: Ir.Module; + }; + error: IrgenError; +}> = { + async run({ ast, types }) { + return Result.map(generateModule(ast, types), (ir) => ({ ir })); + }, +}; + +export default pass; diff --git a/packages/bugc/src/irgen/phi-generation.test.ts b/packages/bugc/src/irgen/phi-generation.test.ts new file mode 100644 index 00000000..2577a617 --- /dev/null +++ b/packages/bugc/src/irgen/phi-generation.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from "vitest"; +import { parse } from "#parser"; +import { checkProgram } from "#typechecker"; +import { generateModule } from "#irgen"; +import { Formatter } from "#ir/analysis"; + +describe("Phi Node Generation", () => { + it("should generate phi nodes for values used across branches", () => { + const source = `name PhiTest; +storage { + [0] x: uint256; +} +code { + let result: uint256 = 0; + + if (x > 5) { + result = 20; + } else { + result = 30; + } + + // Use result after the merge - should require a phi node + x = result; +}`; + + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error("Parse failed"); + } + expect(parseResult.success).toBe(true); + + if (!parseResult.value) { + throw new Error("Parse result has no value"); + } + const checkResult = checkProgram(parseResult.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) throw new Error("Type check failed"); + + if (!checkResult.value) { + throw new Error("Check result has no value"); + } + const irResult = generateModule(parseResult.value, checkResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) throw new Error("IR generation failed"); + + if (!irResult.value) { + throw new Error("IR result has no value"); + } + const ir = irResult.value; + + // Format the IR to check for phi nodes + const formatter = new Formatter(); + const formatted = formatter.format(ir); + + // Should contain phi nodes + // console.debug("Formatted IR:", formatted); + + // Check that merge blocks have phi nodes + let foundPhiInMergeBlock = false; + for (const [_blockId, _block] of ir.main.blocks.entries()) { + // console.debug(`Block ${_blockId}: phis = ${_block.phis.length}`); + if (_blockId.includes("merge") && _block.phis.length > 0) { + foundPhiInMergeBlock = true; + } + } + + expect(formatted).toContain("phi"); + expect(foundPhiInMergeBlock).toBe(true); + }); + + it("should generate phi nodes for nested conditionals", () => { + const source = `name NestedPhiTest; +storage { + [0] x: uint256; +} +code { + let result: uint256 = 100; + + if (x < 10) { + result = 1; + } else { + if (x < 20) { + result = 2; + } else { + result = 3; + } + } + + // Use result after all merges + x = result; +}`; + + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error("Parse failed"); + } + expect(parseResult.success).toBe(true); + + if (!parseResult.value) { + throw new Error("Parse result has no value"); + } + const checkResult = checkProgram(parseResult.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) throw new Error("Type check failed"); + + if (!checkResult.value) { + throw new Error("Check result has no value"); + } + const irResult = generateModule(parseResult.value, checkResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) throw new Error("IR generation failed"); + + if (!irResult.value) { + throw new Error("IR result has no value"); + } + const ir = irResult.value; + + // Count phi nodes across all blocks + let totalPhiNodes = 0; + for (const block of ir.main.blocks.values()) { + totalPhiNodes += block.phis.length; + } + + // Should have phi nodes for the nested structure + // console.debug("Total phi nodes found:", totalPhiNodes); + for (const [_blockId, _block] of ir.main.blocks.entries()) { + // console.debug(`Block ${blockId}: phis = ${block.phis.length}`); + } + expect(totalPhiNodes).toBeGreaterThan(0); + }); + + it("should generate phi nodes for loop variables", () => { + const source = `name LoopPhiTest; +storage { + [0] x: uint256; +} +code { + let sum: uint256 = 0; + + for (let i: uint256 = 0; i < 10; i = i + 1) { + sum = sum + i; + } + + x = sum; +}`; + + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error("Parse failed"); + } + expect(parseResult.success).toBe(true); + + if (!parseResult.value) { + throw new Error("Parse result has no value"); + } + const checkResult = checkProgram(parseResult.value); + expect(checkResult.success).toBe(true); + if (!checkResult.success) throw new Error("Type check failed"); + + if (!checkResult.value) { + throw new Error("Check result has no value"); + } + const irResult = generateModule(parseResult.value, checkResult.value.types); + expect(irResult.success).toBe(true); + if (!irResult.success) throw new Error("IR generation failed"); + + if (!irResult.value) { + throw new Error("IR result has no value"); + } + const ir = irResult.value; + + // Loop headers should have phi nodes for loop-carried values + let foundLoopPhi = false; + for (const [_blockId, _block] of ir.main.blocks.entries()) { + if ( + (_blockId.includes("loop") || _blockId.includes("header")) && + _block.phis.length > 0 + ) { + foundLoopPhi = true; + } + } + + // Loops should definitely have phi nodes + expect(foundLoopPhi).toBe(true); + }); +}); diff --git a/packages/bugc/src/irgen/slice.test.ts b/packages/bugc/src/irgen/slice.test.ts new file mode 100644 index 00000000..1373e9c8 --- /dev/null +++ b/packages/bugc/src/irgen/slice.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "vitest"; +import { parse } from "#parser"; +import * as TypeChecker from "#typechecker"; +import { generateModule } from "./generator.js"; +import { Severity } from "#result"; +import "#test/matchers"; + +describe("IR slice generation", () => { + test("generates slice IR for msg.data", () => { + const result = parse(` + name Test; + code { + let slice = msg.data[0:4]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const typeResult = TypeChecker.checkProgram(result.value); + expect(typeResult.success).toBe(true); + + if (typeResult.success) { + const irResult = generateModule(result.value, typeResult.value.types); + expect(irResult.success).toBe(true); + + if (irResult.success) { + const ir = irResult.value; + expect(ir.main).toBeDefined(); + + // Find the decomposed slice operations + const mainBlocks = Array.from(ir.main.blocks.values()); + const allInsts = mainBlocks.flatMap((block) => block.instructions); + + // Should have operations for: sub (length calc), add (size calc), + // allocate, write (length), add (source offset), add (adjusted source), + // read, add (dest offset), write (data) + const subInsts = allInsts.filter( + (inst) => inst.kind === "binary" && inst.op === "sub", + ); + const allocInsts = allInsts.filter((inst) => inst.kind === "allocate"); + const readInsts = allInsts.filter((inst) => inst.kind === "read"); + const writeInsts = allInsts.filter((inst) => inst.kind === "write"); + + // Should have decomposed the slice into multiple operations + expect(subInsts.length).toBeGreaterThan(0); // Length calculation + expect(allocInsts.length).toBeGreaterThan(0); // Memory allocation(s) - may allocate for result + expect(readInsts.length).toBeGreaterThan(0); // Read slice data + expect(writeInsts.length).toBeGreaterThan(0); // Write length and data + } + } + }); + + test("rejects slice of non-bytes type in IR", () => { + const result = parse(` + name Test; + storage { + [0] numbers: array; + } + code { + numbers[0:4]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const typeResult = TypeChecker.checkProgram(result.value); + + if (typeResult.success) { + const irResult = generateModule(result.value, typeResult.value.types); + expect(irResult.success).toBe(false); + expect(irResult).toHaveMessage({ + severity: Severity.Error, + message: "Only bytes types can be sliced", + }); + } + }); +}); diff --git a/packages/bugc/src/irgen/type.ts b/packages/bugc/src/irgen/type.ts new file mode 100644 index 00000000..d506e7ac --- /dev/null +++ b/packages/bugc/src/irgen/type.ts @@ -0,0 +1,68 @@ +import * as Ir from "#ir"; +import { Type as BugType } from "#types"; +import { Error as IrgenError, ErrorCode, assertExhausted } from "./errors.js"; +import { Severity } from "#result"; + +export function fromBugType(type: BugType): Ir.Type { + if (BugType.isFailure(type) || BugType.isFunction(type)) { + // Error type should already have diagnostics added elsewhere + throw new IrgenError( + `Cannot convert type with kind ${type.kind} to IR type`, + undefined, + Severity.Error, + ErrorCode.UNKNOWN_TYPE, + ); + } + + // Arrays, mappings, structs, and dynamic types become references + if (BugType.isArray(type)) { + // Arrays are memory references when not in storage + return Ir.Type.ref("memory", type); + } + + if (BugType.isMapping(type)) { + // Mappings are always storage references + return Ir.Type.ref("storage", type); + } + + if (BugType.isStruct(type)) { + // Structs are memory references when not in storage + return Ir.Type.ref("memory", type); + } + + if (BugType.isElementary(type)) { + switch (type.kind) { + case "uint": { + // Uints become scalars + const bits = type.bits || 256; + return Ir.Type.scalar((bits / 8) as Ir.Type.Scalar.Size, type); + } + case "int": { + // BUG language doesn't have signed ints, treat as uint + const intBits = type.bits || 256; + return Ir.Type.scalar((intBits / 8) as Ir.Type.Scalar.Size, type); + } + case "address": + // Addresses are 20-byte scalars + return Ir.Type.scalar(20, type); + case "bool": + // Bools are 1-byte scalars + return Ir.Type.scalar(1, type); + case "bytes": + if (type.size) { + // Fixed-size bytes are scalars + return Ir.Type.scalar(type.size as Ir.Type.Scalar.Size, type); + } else { + // Dynamic bytes are memory references + return Ir.Type.ref("memory", type); + } + case "string": + // Strings are always memory references + return Ir.Type.ref("memory", type); + default: + assertExhausted(type); + } + } + + assertExhausted(type); +} diff --git a/packages/bugc/src/optimizer/index.ts b/packages/bugc/src/optimizer/index.ts new file mode 100644 index 00000000..65706f68 --- /dev/null +++ b/packages/bugc/src/optimizer/index.ts @@ -0,0 +1,3 @@ +// Export the simple optimizer for now +export { optimizeIr } from "./simple-optimizer.js"; +export type { OptimizationLevel } from "./optimizer.js"; diff --git a/packages/bugc/src/optimizer/optimizer.property.test.ts b/packages/bugc/src/optimizer/optimizer.property.test.ts new file mode 100644 index 00000000..4435a238 --- /dev/null +++ b/packages/bugc/src/optimizer/optimizer.property.test.ts @@ -0,0 +1,658 @@ +/** + * Property-based tests for the IR optimizer + * + * These tests verify that optimizations preserve program semantics + * and maintain important invariants. + */ + +import { describe, it, expect } from "vitest"; +import * as fc from "fast-check"; +import * as Ir from "#ir"; + +import { optimizeIr as optimize } from "./simple-optimizer.js"; + +// Simple IR validator for testing +function validateIr(module: Ir.Module): { isValid: boolean } { + // Basic validation - check that module has required fields + return { + isValid: + module.name !== undefined && + module.main !== undefined && + module.main.blocks.size > 0, + }; +} + +describe("Optimizer Property Tests", () => { + // Property: Optimization should preserve program semantics + describe("Semantic Preservation", () => { + it("constant folding preserves module structure", () => { + fc.assert( + fc.property( + fc.bigInt({ min: 1n, max: 100n }), + fc.bigInt({ min: 1n, max: 100n }), + fc.constantFrom("add", "sub", "mul"), + (a, b, op) => { + // Create IR with binary operation on constants + const module = createModuleWithBinaryOp(a, b, op); + + // Optimize at level 1 (constant folding) + const optimized = optimize(module, 1); + + // Basic structural checks + expect(optimized).toBeDefined(); + expect(optimized.name).toBe(module.name); + expect(optimized.main).toBeDefined(); + expect(optimized.main.blocks.size).toBeGreaterThan(0); + + // The optimization should preserve the basic block structure + expect(optimized.main.entry).toBe(module.main.entry); + }, + ), + { numRuns: 20 }, + ); + }); + + it("dead code elimination preserves module structure", () => { + fc.assert( + fc.property( + fc.array(fc.boolean(), { minLength: 3, maxLength: 10 }), + (useFlags) => { + // Create module with multiple computations, some unused + const module = createModuleWithDeadCode(useFlags); + + // Optimize with DCE + const optimized = optimize(module, 1); + + // Basic structural checks + expect(optimized).toBeDefined(); + expect(optimized.main).toBeDefined(); + expect(optimized.main.blocks.size).toBeGreaterThan(0); + + // Get instructions before and after + const instructionsBefore = getAllInstructions(module); + const instructionsAfter = getAllInstructions(optimized); + + // DCE should not increase instruction count + expect(instructionsAfter.length).toBeLessThanOrEqual( + instructionsBefore.length, + ); + }, + ), + { numRuns: 20 }, + ); + }); + }); + + // Property: Optimizations should maintain IR invariants + describe("IR Invariants", () => { + it("optimization preserves valid IR structure", () => { + fc.assert( + fc.property(generateRandomModule(), (module) => { + // Optimize at each level + for (let level = 0; level <= 3; level++) { + const optimized = optimize(module, level); + + // Validate IR structure + const validation = validateIr(optimized); + expect(validation.isValid).toBe(true); + + // Check specific invariants + expect(hasValidControlFlow(optimized)).toBe(true); + expect(hasValidPhiNodes(optimized)).toBe(true); + expect(allJumpTargetsExist(optimized)).toBe(true); + } + }), + { numRuns: 20 }, + ); + }); + + it("optimization maintains SSA form", () => { + fc.assert( + fc.property(generateRandomModule(), (module) => { + const optimized = optimize(module, 3); + + // Each temp should be assigned exactly once + const assignments = new Map(); + + const countAssignments = (func: { + blocks: Map; + }): void => { + for (const block of func.blocks.values()) { + // Count phi assignments + if (block.phis) { + for (const phi of block.phis) { + assignments.set( + phi.dest, + (assignments.get(phi.dest) || 0) + 1, + ); + } + } + + // Count instruction assignments + for (const inst of block.instructions) { + if ("dest" in inst && inst.dest) { + assignments.set( + inst.dest, + (assignments.get(inst.dest) || 0) + 1, + ); + } + } + } + }; + + countAssignments(optimized.main); + for (const func of optimized.functions.values()) { + countAssignments(func); + } + + // Each temp should be assigned exactly once + for (const [, count] of assignments) { + expect(count).toBe(1); + } + }), + { numRuns: 20 }, + ); + }); + }); + + // Property: Optimization levels should be monotonic + describe("Optimization Monotonicity", () => { + it("higher optimization levels produce smaller or equal code", () => { + fc.assert( + fc.property(generateRandomModule(), (module) => { + const sizes: number[] = []; + + for (let level = 0; level <= 3; level++) { + const optimized = optimize(module, level); + const size = countInstructions(optimized); + sizes.push(size); + } + + // Each level should produce same or smaller code + for (let i = 1; i < sizes.length; i++) { + expect(sizes[i]).toBeLessThanOrEqual(sizes[i - 1]); + } + }), + { numRuns: 30 }, + ); + }); + }); + + // Property: Specific optimization correctness + describe("Optimization Correctness", () => { + it("CSE produces equivalent expressions", () => { + fc.assert( + fc.property( + fc.bigInt({ min: 0n, max: 100n }), + fc.bigInt({ min: 0n, max: 100n }), + (a, b) => { + // Create module with duplicate expressions + const module = createModuleWithDuplicateExpressions(a, b); + + // Optimize with CSE (level 2) + const optimized = optimize(module, 2); + + // Count binary operations + const originalOps = countBinaryOps(module); + const optimizedOps = countBinaryOps(optimized); + + // CSE should reduce duplicate operations + expect(optimizedOps).toBeLessThan(originalOps); + + // But the final result should be the same + // (Would need execution to verify, checking structure for now) + expect(hasValidStructure(optimized)).toBe(true); + }, + ), + { numRuns: 50 }, + ); + }); + + it("block merging preserves execution paths", () => { + fc.assert( + fc.property(fc.boolean(), fc.boolean(), (cond1, cond2) => { + // Create module with mergeable blocks + const module = createModuleWithMergeableBlocks(cond1, cond2); + + // Count blocks before and after + const blocksBefore = countBlocks(module); + const optimized = optimize(module, 3); + const blocksAfter = countBlocks(optimized); + + // Should have fewer blocks after merging + expect(blocksAfter).toBeLessThanOrEqual(blocksBefore); + + // All original paths should still be possible + expect(hasAllPaths(module, optimized)).toBe(true); + }), + { numRuns: 20 }, + ); + }); + }); +}); + +// Helper functions for creating test modules + +function createModuleWithBinaryOp(a: bigint, b: bigint, op: string): Ir.Module { + return { + name: "Test", + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + instructions: [ + { + kind: "const", + value: a, + dest: "t0", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + { + kind: "const", + value: b, + dest: "t1", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + { + kind: "binary", + op: op as + | "add" + | "sub" + | "mul" + | "div" + | "mod" + | "lt" + | "gt" + | "eq" + | "ne" + | "and" + | "or", + left: { + kind: "temp", + id: "t0", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + dest: "t2", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + phis: [], + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + functions: new Map(), + }; +} + +function createModuleWithDeadCode(useFlags: boolean[]): Ir.Module { + const instructions: Ir.Instruction[] = []; + + // Create computations + useFlags.forEach((_, index) => { + instructions.push({ + kind: "const", + value: BigInt(index), + dest: `t${index}`, + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }); + }); + + // Use some values + const usedInstructions: Ir.Instruction[] = []; + useFlags.forEach((used, index) => { + if (used && index > 0) { + usedInstructions.push({ + kind: "binary", + op: "add", + left: { + kind: "temp", + id: `t${index}`, + type: Ir.Type.Scalar.uint256, + }, + right: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + dest: `result${index}`, + operationDebug: {}, + }); + } + }); + + return { + name: "Test", + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + instructions: [...instructions, ...usedInstructions], + terminator: { kind: "return", operationDebug: {} }, + phis: [], + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + functions: new Map(), + }; +} + +function createModuleWithDuplicateExpressions(a: bigint, b: bigint): Ir.Module { + return { + name: "Test", + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + instructions: [ + { + kind: "const", + value: a, + dest: "t0", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + { + kind: "const", + value: b, + dest: "t1", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + // First computation + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "t0", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + dest: "t2", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + // Duplicate computation + { + kind: "binary", + op: "add", + left: { + kind: "temp", + id: "t0", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "t1", + type: Ir.Type.Scalar.uint256, + }, + dest: "t3", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + // Use both results + { + kind: "binary", + op: "mul", + left: { + kind: "temp", + id: "t2", + type: Ir.Type.Scalar.uint256, + }, + right: { + kind: "temp", + id: "t3", + type: Ir.Type.Scalar.uint256, + }, + dest: "t4", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + phis: [], + predecessors: new Set(), + debug: {}, + }, + ], + ]), + }, + functions: new Map(), + }; +} + +function createModuleWithMergeableBlocks( + cond1: boolean, + _cond2: boolean, +): Ir.Module { + const blocks = new Map(); + + // Entry block + blocks.set("entry", { + id: "entry", + instructions: [ + { + kind: "const", + value: cond1 ? 1n : 0n, + dest: "t0", + type: Ir.Type.Scalar.bool, + operationDebug: {}, + }, + ], + terminator: { + kind: "branch", + condition: { kind: "temp", id: "t0", type: Ir.Type.Scalar.bool }, + trueTarget: "block1", + falseTarget: "block2", + operationDebug: {}, + }, + phis: [], + predecessors: new Set(), + debug: {}, + }); + + // Intermediate blocks that could be merged + blocks.set("block1", { + id: "block1", + instructions: [], + terminator: { kind: "jump", target: "final", operationDebug: {} }, + phis: [], + predecessors: new Set(["entry"]), + debug: {}, + }); + + blocks.set("block2", { + id: "block2", + instructions: [], + terminator: { kind: "jump", target: "final", operationDebug: {} }, + phis: [], + predecessors: new Set(["entry"]), + debug: {}, + }); + + // Final block + blocks.set("final", { + id: "final", + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + phis: [], + predecessors: new Set(["block1", "block2"]), + debug: {}, + }); + + return { + name: "Test", + main: { name: "main", parameters: [], entry: "entry", blocks }, + functions: new Map(), + }; +} + +// Generator for random IR modules +function generateRandomModule(): fc.Arbitrary { + return fc.record({ + name: fc.constant("Test"), + main: generateRandomFunction(), + functions: fc.constant(new Map()), + }); +} + +function generateRandomFunction(): fc.Arbitrary<{ + name: string; + parameters: never[]; + entry: string; + blocks: Map; +}> { + return fc.record({ + name: fc.constant("main"), + parameters: fc.constant([]), + entry: fc.constant("entry"), + blocks: fc.constant(createSimpleBlocks()), + }); +} + +function createSimpleBlocks(): Map { + // Create a simple but valid control flow graph + const blocks = new Map(); + + blocks.set("entry", { + id: "entry", + instructions: [ + { + kind: "const", + value: 42n, + dest: "t0", + type: Ir.Type.Scalar.uint256, + operationDebug: {}, + }, + ], + terminator: { kind: "return", operationDebug: {} }, + phis: [], + predecessors: new Set(), + debug: {}, + }); + + return blocks; +} + +// Utility functions + +function getAllInstructions(module: Ir.Module): Ir.Instruction[] { + const instructions: Ir.Instruction[] = []; + + const collectFromFunction = (func: { + blocks: Map; + }): void => { + for (const block of func.blocks.values()) { + instructions.push(...block.instructions); + } + }; + + collectFromFunction(module.main); + for (const func of module.functions.values()) { + collectFromFunction(func); + } + + return instructions; +} + +function hasValidControlFlow(module: Ir.Module): boolean { + // Check that all blocks have valid terminators + for (const block of module.main.blocks.values()) { + if (!block.terminator) return false; + } + return true; +} + +function hasValidPhiNodes(module: Ir.Module): boolean { + // Phi nodes should only appear at block entry + for (const block of module.main.blocks.values()) { + if (block.phis && !Array.isArray(block.phis)) return false; + } + return true; +} + +function allJumpTargetsExist(module: Ir.Module): boolean { + const blockIds = new Set(module.main.blocks.keys()); + + for (const block of module.main.blocks.values()) { + if (block.terminator.kind === "jump") { + if (!blockIds.has(block.terminator.target)) return false; + } else if (block.terminator.kind === "branch") { + if (!blockIds.has(block.terminator.trueTarget)) return false; + if (!blockIds.has(block.terminator.falseTarget)) return false; + } + } + + return true; +} + +function countInstructions(module: Ir.Module): number { + let count = 0; + + const countInFunction = (func: { blocks: Map }): void => { + for (const block of func.blocks.values()) { + count += block.instructions.length; + if (block.phis) count += block.phis.length; + } + }; + + countInFunction(module.main); + for (const func of module.functions.values()) { + countInFunction(func); + } + + return count; +} + +function countBinaryOps(module: Ir.Module): number { + return getAllInstructions(module).filter((inst) => inst.kind === "binary") + .length; +} + +function hasValidStructure(module: Ir.Module): boolean { + return module.main.blocks.size > 0 && hasValidControlFlow(module); +} + +function countBlocks(module: Ir.Module): number { + let count = module.main.blocks.size; + for (const func of module.functions.values()) { + count += func.blocks.size; + } + return count; +} + +function hasAllPaths(_original: Ir.Module, _optimized: Ir.Module): boolean { + // Simplified check - in real implementation would trace all paths + return true; +} diff --git a/packages/bugc/src/optimizer/optimizer.ts b/packages/bugc/src/optimizer/optimizer.ts new file mode 100644 index 00000000..0e287a8e --- /dev/null +++ b/packages/bugc/src/optimizer/optimizer.ts @@ -0,0 +1,269 @@ +import * as Ir from "#ir"; +import type * as Format from "@ethdebug/format"; + +export type OptimizationLevel = 0 | 1 | 2 | 3; + +export interface OptimizationStep { + name: string; + run(module: Ir.Module, context: OptimizationContext): Ir.Module; +} + +export interface OptimizationContext { + trackTransformation(transform: SourceTransform): void; + getTransformations(): SourceTransform[]; + + // For passes that need to share analysis results + getAnalysis(key: string): T | undefined; + setAnalysis(key: string, value: T): void; +} + +export type TransformationType = + | "move" + | "merge" + | "delete" + | "split" + | "replace"; + +export interface SourceTransform { + type: TransformationType; + pass: string; + original: Format.Program.Context[]; + result: Format.Program.Context[]; + reason: string; +} + +export interface OptimizationStats { + passName: string; + instructionsRemoved: number; + instructionsAdded: number; + blocksRemoved: number; + blocksAdded: number; + transformations: number; +} + +export interface OptimizationResult { + module: Ir.Module; + stats: OptimizationStats[]; + transformations: SourceTransform[]; +} + +export class OptimizationContextImpl implements OptimizationContext { + private transformations: SourceTransform[] = []; + private analysisCache = new Map(); + + trackTransformation(transform: SourceTransform): void { + this.transformations.push(transform); + } + + getTransformations(): SourceTransform[] { + return this.transformations; + } + + getAnalysis(key: string): T | undefined { + return this.analysisCache.get(key) as T | undefined; + } + + setAnalysis(key: string, value: T): void { + this.analysisCache.set(key, value); + } +} + +export class OptimizationPipeline { + constructor(private steps: OptimizationStep[]) {} + + optimize(module: Ir.Module): OptimizationResult { + const context = new OptimizationContextImpl(); + const stats: OptimizationStats[] = []; + + let currentModule = module; + + for (const step of this.steps) { + const startInstructions = this.countInstructions(currentModule); + const startBlocks = this.countBlocks(currentModule); + const startTransforms = context.getTransformations().length; + + currentModule = step.run(currentModule, context); + + const endInstructions = this.countInstructions(currentModule); + const endBlocks = this.countBlocks(currentModule); + const endTransforms = context.getTransformations().length; + + stats.push({ + passName: step.name, + instructionsRemoved: Math.max(0, startInstructions - endInstructions), + instructionsAdded: Math.max(0, endInstructions - startInstructions), + blocksRemoved: Math.max(0, startBlocks - endBlocks), + blocksAdded: Math.max(0, endBlocks - startBlocks), + transformations: endTransforms - startTransforms, + }); + } + + return { + module: currentModule, + stats, + transformations: context.getTransformations(), + }; + } + + private countInstructions(module: Ir.Module): number { + let count = 0; + + // Count main function instructions + for (const block of module.main.blocks.values()) { + count += block.instructions.length; + count += 1; // terminator + } + + // Count create function instructions if present + if (module.create) { + for (const block of module.create.blocks.values()) { + count += block.instructions.length; + count += 1; // terminator + } + } + + // Count user-defined function instructions + if (module.functions) { + for (const func of module.functions.values()) { + for (const block of func.blocks.values()) { + count += block.instructions.length; + count += 1; // terminator + } + } + } + + return count; + } + + private countBlocks(module: Ir.Module): number { + let count = module.main.blocks.size; + + if (module.create) { + count += module.create.blocks.size; + } + + // Count user-defined function blocks + if (module.functions) { + for (const func of module.functions.values()) { + count += func.blocks.size; + } + } + + return count; + } +} + +// Base class for optimization passes +export abstract class BaseOptimizationStep implements OptimizationStep { + abstract name: string; + + abstract run(module: Ir.Module, context: OptimizationContext): Ir.Module; + + /** + * Apply optimization to all functions (main, create, and user-defined) + */ + protected processAllFunctions( + module: Ir.Module, + processor: (func: Ir.Function, funcName: string) => void, + ): void { + // Process main function + processor(module.main, "main"); + + // Process create function if present + if (module.create) { + processor(module.create, "create"); + } + + // Process user-defined functions + if (module.functions) { + for (const [name, func] of module.functions.entries()) { + processor(func, name); + } + } + } + + protected cloneModule(module: Ir.Module): Ir.Module { + // Clone main function + const clonedMain = this.cloneFunction(module.main); + + // Clone create function if present + let clonedCreate: Ir.Function | undefined; + if (module.create) { + clonedCreate = this.cloneFunction(module.create); + } + + // Clone user-defined functions + const clonedFunctions = new Map(); + if (module.functions) { + for (const [name, func] of module.functions.entries()) { + clonedFunctions.set(name, this.cloneFunction(func)); + } + } + + return { + name: module.name, + functions: clonedFunctions, + create: clonedCreate, + main: clonedMain, + loc: module.loc, + }; + } + + protected cloneFunction(func: Ir.Function): Ir.Function { + // Deep clone that preserves Map structure + const clonedBlocks = new Map(); + + for (const [id, block] of func.blocks.entries()) { + clonedBlocks.set(id, { + id: block.id, + phis: block.phis ? [...block.phis] : [], + instructions: [...block.instructions], + terminator: { ...block.terminator }, + predecessors: new Set(block.predecessors), + debug: block.debug, + }); + } + + return { + name: func.name, + parameters: [...func.parameters], + entry: func.entry, + blocks: clonedBlocks, + }; + } + + protected replaceInstruction( + instructions: Ir.Instruction[], + index: number, + newInstruction: Ir.Instruction | null, + context: OptimizationContext, + reason: string, + ): Ir.Instruction[] { + const result = [...instructions]; + const original = instructions[index]; + + if (newInstruction === null) { + // Delete instruction + result.splice(index, 1); + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(original), + result: [], + reason, + }); + } else { + // Replace instruction + result[index] = newInstruction; + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(original), + result: Ir.Utils.extractContexts(newInstruction), + reason, + }); + } + + return result; + } +} diff --git a/packages/bugc/src/optimizer/pass.ts b/packages/bugc/src/optimizer/pass.ts new file mode 100644 index 00000000..082fc6a1 --- /dev/null +++ b/packages/bugc/src/optimizer/pass.ts @@ -0,0 +1,27 @@ +import type * as Ir from "#ir"; +import { optimizeIr } from "./simple-optimizer.js"; +import { type OptimizationLevel } from "./optimizer.js"; +import { Result } from "#result"; +import type { Pass } from "#compiler"; + +/** + * Optimization pass - optimizes intermediate representation + */ +export const pass: Pass<{ + needs: { + ir: Ir.Module; + optimizer?: { + level?: OptimizationLevel; + }; + }; + adds: { + ir: Ir.Module; + }; + error: never; +}> = { + async run({ ir, optimizer: { level = 0 } = {} }) { + return Result.ok({ + ir: optimizeIr(ir, level), + }); + }, +}; diff --git a/packages/bugc/src/optimizer/simple-optimizer.ts b/packages/bugc/src/optimizer/simple-optimizer.ts new file mode 100644 index 00000000..b078c963 --- /dev/null +++ b/packages/bugc/src/optimizer/simple-optimizer.ts @@ -0,0 +1,80 @@ +/** + * Simplified optimizer implementation that uses the optimization step architecture + */ + +import * as Ir from "#ir"; +import { OptimizationPipeline, type OptimizationStep } from "./optimizer.js"; +import { + ConstantFoldingStep, + DeadCodeEliminationStep, + CommonSubexpressionEliminationStep, + ConstantPropagationStep, + JumpOptimizationStep, + BlockMergingStep, + ReturnMergingStep, + ReadWriteMergingStep, + TailCallOptimizationStep, +} from "./steps/index.js"; + +/** + * Apply all optimizations based on the specified level + */ +export function optimizeIr(module: Ir.Module, level: number): Ir.Module { + if (level === 0) return module; + + const steps = createOptimizationPipeline(level); + const pipeline = new OptimizationPipeline(steps); + + let current = module; + let previousHash = ""; + + // Run optimization steps until fixpoint for level 2+ + do { + const currentHash = JSON.stringify(current); + if (currentHash === previousHash) break; // Reached fixpoint + previousHash = currentHash; + + const result = pipeline.optimize(current); + current = result.module; + } while (level >= 2); + + return current; +} + +/** + * Create optimization pipeline for a given level + */ +function createOptimizationPipeline(level: number): OptimizationStep[] { + const steps: OptimizationStep[] = []; + + if (level === 0) return steps; + + // Level 1: Basic optimizations + if (level >= 1) { + steps.push( + new ConstantFoldingStep(), + new ConstantPropagationStep(), + new DeadCodeEliminationStep(), + ); + } + + // Level 2: Add CSE, tail call optimization, and jump optimization + if (level >= 2) { + steps.push( + new CommonSubexpressionEliminationStep(), + new TailCallOptimizationStep(), + new JumpOptimizationStep(), + ); + } + + // Level 3: Add block merging and read/write merging + if (level >= 3) { + steps.push( + new BlockMergingStep(), + new ReturnMergingStep(), + new ReadWriteMergingStep(), + ); + } + + return steps; +} diff --git a/packages/bugc/src/optimizer/steps/block-merging.ts b/packages/bugc/src/optimizer/steps/block-merging.ts new file mode 100644 index 00000000..baa26d55 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/block-merging.ts @@ -0,0 +1,91 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class BlockMergingStep extends BaseOptimizationStep { + name = "block-merging"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Find candidates for merging + const mergeMap = new Map(); // maps block to its merge target + + for (const [blockId, block] of func.blocks) { + if (block.terminator.kind === "jump" && block.predecessors.size === 1) { + const targetBlock = func.blocks.get(block.terminator.target); + + // Only merge if target has single predecessor + if (targetBlock && targetBlock.predecessors.size === 1) { + mergeMap.set(block.terminator.target, blockId); + } + } + } + + // Perform merging + for (const [toMerge, mergeInto] of mergeMap) { + const sourceBlock = func.blocks.get(toMerge); + const targetBlock = func.blocks.get(mergeInto); + + if (!sourceBlock || !targetBlock) continue; + + // Merge phi nodes from source to target + if (sourceBlock.phis && targetBlock.phis) { + targetBlock.phis.push(...sourceBlock.phis); + } + + // Append instructions from source to target + targetBlock.instructions.push(...sourceBlock.instructions); + targetBlock.terminator = sourceBlock.terminator; + + // Combine debug contexts from both blocks + targetBlock.debug = Ir.Utils.combineDebugContexts( + targetBlock.debug, + sourceBlock.debug, + ); + + // Track the merge transformation + context.trackTransformation({ + type: "merge", + pass: this.name, + original: Ir.Utils.extractContexts(targetBlock, sourceBlock), + result: Ir.Utils.extractContexts(targetBlock), + reason: `Merged block ${toMerge} into ${mergeInto}`, + }); + + // Remove the merged block + func.blocks.delete(toMerge); + + // Update predecessors and jump targets + for (const block of func.blocks.values()) { + // Update predecessors + if (block.predecessors.has(toMerge)) { + block.predecessors.delete(toMerge); + block.predecessors.add(mergeInto); + } + + // Update jump targets in terminators + if ( + block.terminator.kind === "jump" && + block.terminator.target === toMerge + ) { + block.terminator.target = mergeInto; + } else if (block.terminator.kind === "branch") { + if (block.terminator.trueTarget === toMerge) { + block.terminator.trueTarget = mergeInto; + } + if (block.terminator.falseTarget === toMerge) { + block.terminator.falseTarget = mergeInto; + } + } + } + } + }); + + return optimized; + } +} diff --git a/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts new file mode 100644 index 00000000..6f8b8273 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/common-subexpression-elimination.ts @@ -0,0 +1,371 @@ +import * as Ir from "#ir"; + +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class CommonSubexpressionEliminationStep extends BaseOptimizationStep { + name = "common-subexpression-elimination"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Compute dominator tree for this function + const analyzer = new Ir.Analysis.Statistics.Analyzer(); + const analysis = analyzer.analyze({ ...module, main: func }); + const dominators = analysis.dominatorTree; + + // Global replacements map for the entire function + const globalReplacements = new Map(); + // Pure expressions that persist across side effects + // Maps expression -> {instruction, blockId} + const pureExpressions = new Map< + string, + { instruction: Ir.Instruction; block: string } + >(); + + // Process blocks in topological order to ensure dominators are + // processed first + const orderedBlocks = this.topologicalSort(func); + + for (const blockId of orderedBlocks) { + const block = func.blocks.get(blockId); + if (!block) continue; + // Map of expression -> instruction that computes it + // (cleared on side effects) + const expressions = new Map(); + const newInstructions: Ir.Instruction[] = []; + + for (const inst of block.instructions) { + // Apply any replacements to this instruction + const processedInst = this.applyReplacements( + inst, + globalReplacements, + ); + + if ( + processedInst.kind === "binary" || + processedInst.kind === "unary" || + processedInst.kind === "compute_slot" || + processedInst.kind === "env" + ) { + // Create a canonical representation of the expression + const exprKey = this.getExpressionKey(processedInst); + const isPure = + processedInst.kind === "compute_slot" || + processedInst.kind === "env"; + + // Check if we've seen this expression before + let existingInst: Ir.Instruction | undefined; + if (isPure) { + // For pure expressions, check if we have a dominating + // definition + const pureEntry = pureExpressions.get(exprKey); + if ( + pureEntry && + this.dominates(pureEntry.block, blockId, dominators) + ) { + existingInst = pureEntry.instruction; + } else { + // Fall back to local expressions in this block + existingInst = expressions.get(exprKey); + } + } else { + // Non-pure expressions only within the same block + existingInst = expressions.get(exprKey); + } + + if ( + existingInst && + "dest" in processedInst && + "dest" in existingInst + ) { + // This is a duplicate - combine debug contexts + existingInst.operationDebug = Ir.Utils.combineDebugContexts( + existingInst.operationDebug, + processedInst.operationDebug, + ); + + // Map this temp to the existing one + globalReplacements.set(processedInst.dest, existingInst.dest); + + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(processedInst), + result: [], + reason: `Eliminated duplicate computation: ${exprKey}`, + }); + // Don't emit this instruction + } else { + // First time seeing this expression + if ("dest" in processedInst && exprKey) { + expressions.set(exprKey, processedInst); + if (isPure) { + pureExpressions.set(exprKey, { + instruction: processedInst, + block: blockId, + }); + } + } + newInstructions.push(processedInst); + } + } else if (this.hasSideEffects(processedInst)) { + // Instructions with side effects invalidate our expression + // tracking + expressions.clear(); + newInstructions.push(processedInst); + } else { + newInstructions.push(processedInst); + } + } + + block.instructions = newInstructions; + } + + // Now apply replacements to phi nodes and terminators in a second + // pass + for (const block of func.blocks.values()) { + // Apply replacements to phi nodes + for (const phi of block.phis) { + if (phi.sources) { + for (const [blockId, value] of phi.sources) { + const newValue = this.applyValueReplacement( + value, + globalReplacements, + ); + phi.sources.set(blockId, newValue); + } + } + } + + // Also apply replacements to the terminator + if (block.terminator.kind === "branch") { + block.terminator.condition = this.applyValueReplacement( + block.terminator.condition, + globalReplacements, + ); + } else if ( + block.terminator.kind === "return" && + block.terminator.value + ) { + block.terminator.value = this.applyValueReplacement( + block.terminator.value, + globalReplacements, + ); + } + } + }); + + return optimized; + } + + private applyValueReplacement( + value: Ir.Value, + replacements: Map, + ): Ir.Value { + if (value.kind === "temp" && replacements.has(value.id)) { + return { + kind: "temp", + id: replacements.get(value.id)!, + type: value.type, + }; + } + return value; + } + + private applyReplacements( + inst: Ir.Instruction, + replacements: Map, + ): Ir.Instruction { + // Clone the instruction and replace any temp references + const result = { ...inst }; + + // Helper to replace a value + const replaceValue = (value: Ir.Value): Ir.Value => { + if (value.kind === "temp" && replacements.has(value.id)) { + return { + kind: "temp", + id: replacements.get(value.id)!, + type: value.type, + }; + } + return value; + }; + + // Apply replacements based on instruction type + switch (result.kind) { + case "binary": + result.left = replaceValue(result.left); + result.right = replaceValue(result.right); + break; + case "unary": + result.operand = replaceValue(result.operand); + break; + case "write": + if (result.slot) result.slot = replaceValue(result.slot); + if (result.value) result.value = replaceValue(result.value); + if (result.offset) result.offset = replaceValue(result.offset); + if (result.length) result.length = replaceValue(result.length); + break; + case "read": + if (result.slot) result.slot = replaceValue(result.slot); + if (result.offset) result.offset = replaceValue(result.offset); + if (result.length) result.length = replaceValue(result.length); + break; + case "compute_slot": + result.base = replaceValue(result.base); + if (Ir.Instruction.ComputeSlot.isMapping(result)) { + result.key = replaceValue(result.key); + } else if (Ir.Instruction.ComputeSlot.isArray(result)) { + // Array compute_slot no longer has index field + } + break; + case "hash": + result.value = replaceValue(result.value); + break; + case "length": + result.object = replaceValue(result.object); + break; + } + + return result; + } + + private getExpressionKey(inst: Ir.Instruction): string { + if (inst.kind === "binary") { + const leftKey = this.getValueKey(inst.left); + const rightKey = this.getValueKey(inst.right); + const leftTypeKey = this.getTypeKey(inst.left.type); + const rightTypeKey = this.getTypeKey(inst.right.type); + + // For commutative operations, normalize the order + if (this.isCommutative(inst.op) && leftKey > rightKey) { + return `${inst.op}(${rightKey}:${rightTypeKey},${leftKey}:${leftTypeKey})`; + } + return `${inst.op}(${leftKey}:${leftTypeKey},${rightKey}:${rightTypeKey})`; + } else if (inst.kind === "unary") { + const operandKey = this.getValueKey(inst.operand); + const typeKey = this.getTypeKey(inst.operand.type); + return `${inst.op}(${operandKey}:${typeKey})`; + } else if (inst.kind === "compute_slot") { + // Create a unique key for compute_slot instructions + const baseKey = this.getValueKey(inst.base); + + if (inst.slotKind === "field") { + return `compute_slot:field(${baseKey},${inst.fieldOffset})`; + } else if (inst.slotKind === "mapping") { + const keyKey = this.getValueKey(inst.key); + const keyTypeKey = inst.keyType + ? this.getTypeKey(inst.keyType) + : "unknown"; + return `compute_slot:mapping(${baseKey},${keyKey}:${keyTypeKey})`; + } else if (inst.slotKind === "array") { + // Array compute_slot only depends on base now + return `compute_slot:array(${baseKey})`; + } + } else if (inst.kind === "env") { + // Environment values are constant during execution + return `env:${inst.op}`; + } + return ""; + } + + private getValueKey(value: Ir.Value): string { + if (value.kind === "const") { + return `const:${value.value}`; + } else if (value.kind === "temp") { + return `temp:${value.id}`; + } + return "unknown"; + } + + private getTypeKey(type: Ir.Value["type"]): string { + if (!type) return "unknown"; + switch (type.kind) { + case "scalar": + return `scalar:${type.size}`; + case "ref": + return `ref:${type.location}`; + default: + return "unknown"; + } + } + + private isCommutative(op: string): boolean { + return ["add", "mul", "eq", "ne", "and", "or"].includes(op); + } + + private hasSideEffects(inst: Ir.Instruction): boolean { + switch (inst.kind) { + case "write": + return true; + default: + return false; + } + } + + private topologicalSort(func: Ir.Function): string[] { + const visited = new Set(); + const result: string[] = []; + + const visit = (blockId: string): void => { + if (visited.has(blockId)) return; + visited.add(blockId); + + // Add current block first (pre-order) + result.push(blockId); + + const block = func.blocks.get(blockId); + if (!block) return; + + // Then visit successors + const successors = this.getSuccessors(block); + for (const succ of successors) { + visit(succ); + } + }; + + // Start from entry + visit(func.entry); + + // Visit any unreachable blocks + for (const blockId of func.blocks.keys()) { + visit(blockId); + } + + return result; + } + + private getSuccessors(block: Ir.Block): string[] { + switch (block.terminator.kind) { + case "jump": + return [block.terminator.target]; + case "branch": + return [block.terminator.trueTarget, block.terminator.falseTarget]; + case "call": + return [block.terminator.continuation]; + case "return": + return []; + default: + return []; + } + } + + private dominates( + a: string, + b: string, + dominators: Record, + ): boolean { + // Check if block a dominates block b + let current: string | null = b; + while (current !== null) { + if (current === a) return true; + current = dominators[current] || null; + } + return false; + } +} diff --git a/packages/bugc/src/optimizer/steps/constant-folding.test.ts b/packages/bugc/src/optimizer/steps/constant-folding.test.ts new file mode 100644 index 00000000..1124ac46 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/constant-folding.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect } from "vitest"; + +import * as Ir from "#ir"; + +import { ConstantFoldingStep } from "./constant-folding.js"; +import { type OptimizationContext } from "../optimizer.js"; + +describe("ConstantFoldingStep", () => { + const step = new ConstantFoldingStep(); + + function createTestModule(instructions: Ir.Instruction[]): Ir.Module { + const block: Ir.Block = { + id: "entry", + phis: [], + instructions, + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + }; + + return { + name: "test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([["entry", block]]), + }, + }; + } + + it("should fold keccak256 on string constants", () => { + const module = createTestModule([ + { + kind: "const", + value: "transfer(address,uint256)", + type: Ir.Type.Scalar.uint256, + dest: "t0", + operationDebug: {}, + }, + { + kind: "hash", + value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + dest: "t1", + operationDebug: {}, + }, + ]); + + const context: OptimizationContext = { + trackTransformation: () => {}, + getTransformations: () => [], + getAnalysis: () => undefined, + setAnalysis: () => {}, + }; + + const optimized = step.run(module, context); + const block = optimized.main.blocks.get("entry")!; + + expect(block.instructions).toHaveLength(2); + expect(block.instructions[1]).toMatchObject({ + kind: "const", + // This is keccak256("transfer(address,uint256)") + value: + 76450787364331811106618268332334209071204572358820727073668507032443496760475n, + type: Ir.Type.Scalar.bytes32, + dest: "t1", + }); + }); + + it("should not fold keccak256 on non-constant values", () => { + const module = createTestModule([ + { + kind: "const", + value: 123n, + type: Ir.Type.Scalar.uint256, + dest: "t0", + operationDebug: {}, + }, + { + kind: "hash", + value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + dest: "t1", + operationDebug: {}, + }, + ]); + + const context: OptimizationContext = { + trackTransformation: () => {}, + getTransformations: () => [], + getAnalysis: () => undefined, + setAnalysis: () => {}, + }; + + const optimized = step.run(module, context); + const block = optimized.main.blocks.get("entry")!; + + expect(block.instructions).toHaveLength(2); + expect(block.instructions[1]).toMatchObject({ + kind: "hash", + value: { kind: "temp", id: "t0" }, + dest: "t1", + }); + }); + + it("should fold multiple hash operations", () => { + const module = createTestModule([ + { + kind: "const", + value: "pause()", + type: Ir.Type.Scalar.uint256, + dest: "t0", + operationDebug: {}, + }, + { + kind: "hash", + value: { kind: "temp", id: "t0", type: Ir.Type.Scalar.uint256 }, + dest: "t1", + operationDebug: {}, + }, + { + kind: "const", + value: "unpause()", + type: Ir.Type.Scalar.uint256, + dest: "t2", + operationDebug: {}, + }, + { + kind: "hash", + value: { kind: "temp", id: "t2", type: Ir.Type.Scalar.uint256 }, + dest: "t3", + operationDebug: {}, + }, + ]); + + const context: OptimizationContext = { + trackTransformation: () => {}, + getTransformations: () => [], + getAnalysis: () => undefined, + setAnalysis: () => {}, + }; + + const optimized = step.run(module, context); + const block = optimized.main.blocks.get("entry")!; + + expect(block.instructions).toHaveLength(4); + + // Check that both hash instructions were folded + expect(block.instructions[1]).toMatchObject({ + kind: "const", + type: Ir.Type.Scalar.bytes32, + dest: "t1", + }); + + expect(block.instructions[3]).toMatchObject({ + kind: "const", + type: Ir.Type.Scalar.bytes32, + dest: "t3", + }); + }); +}); diff --git a/packages/bugc/src/optimizer/steps/constant-folding.ts b/packages/bugc/src/optimizer/steps/constant-folding.ts new file mode 100644 index 00000000..20f99d58 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/constant-folding.ts @@ -0,0 +1,289 @@ +import { keccak256 } from "ethereum-cryptography/keccak"; + +import * as Ir from "#ir"; + +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class ConstantFoldingStep extends BaseOptimizationStep { + name = "constant-folding"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process all functions in the module + this.processAllFunctions(optimized, (func) => { + // Track constant values per function + const constants = new Map(); + + for (const block of func.blocks.values()) { + const newInstructions: Ir.Instruction[] = []; + + for (let i = 0; i < block.instructions.length; i++) { + const inst = block.instructions[i]; + + if (inst.kind === "const") { + // Track constant values + if ("dest" in inst) { + constants.set(inst.dest, inst.value); + } + newInstructions.push(inst); + } else if ( + inst.kind === "binary" && + this.canFoldBinary(inst, constants) + ) { + // Try to fold binary operation + const folded = this.foldBinary(inst, constants); + if (folded) { + newInstructions.push(folded); + if (folded.kind === "const") { + constants.set(folded.dest, folded.value); + } + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(inst), + result: Ir.Utils.extractContexts(folded), + reason: `Folded ${inst.op} operation on constants`, + }); + } else { + newInstructions.push(inst); + } + } else if ( + inst.kind === "hash" && + this.canFoldHash(inst, constants) + ) { + // Try to fold hash operation + const folded = this.foldHash(inst, constants); + if (folded) { + newInstructions.push(folded); + if (folded.kind === "const") { + constants.set(folded.dest, folded.value); + } + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(inst), + result: Ir.Utils.extractContexts(folded), + reason: `Evaluated keccak256 on constant`, + }); + } else { + newInstructions.push(inst); + } + } else if (inst.kind === "length" && this.canFoldLength(inst)) { + // Try to fold length operation + const folded = this.foldLength(inst); + if (folded) { + newInstructions.push(folded); + if (folded.kind === "const") { + constants.set(folded.dest, folded.value); + } + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(inst), + result: Ir.Utils.extractContexts(folded), + reason: `Evaluated length of fixed-size array`, + }); + } else { + newInstructions.push(inst); + } + } else { + newInstructions.push(inst); + } + } + + block.instructions = newInstructions; + } + }); + + return optimized; + } + + private canFoldBinary( + inst: Ir.Instruction, + constants: Map, + ): boolean { + if (inst.kind !== "binary") return false; + + const leftValue = this.getConstantValue(inst.left, constants); + const rightValue = this.getConstantValue(inst.right, constants); + + return leftValue !== undefined && rightValue !== undefined; + } + + private foldBinary( + inst: Ir.Instruction & { kind: "binary" }, + constants: Map, + ): Ir.Instruction | null { + const leftValue = this.getConstantValue(inst.left, constants); + const rightValue = this.getConstantValue(inst.right, constants); + + if (leftValue === undefined || rightValue === undefined) return null; + + const result = this.evaluateBinary(inst.op, leftValue, rightValue); + if (result === undefined) return null; + + return { + kind: "const", + value: result, + type: this.getResultType(inst.op, typeof result), + dest: inst.dest, + operationDebug: Ir.Utils.preserveDebug(inst), + }; + } + + private getConstantValue( + value: Ir.Value, + constants: Map, + ): bigint | boolean | string | undefined { + if (value.kind === "const") { + return value.value; + } else if (value.kind === "temp") { + return constants.get(value.id); + } + return undefined; + } + + private evaluateBinary( + op: string, + left: bigint | boolean | string, + right: bigint | boolean | string, + ): bigint | boolean | undefined { + if (typeof left === "bigint" && typeof right === "bigint") { + switch (op) { + case "add": + return left + right; + case "sub": + return left - right; + case "mul": + return left * right; + case "div": + return right !== 0n ? left / right : undefined; + case "mod": + return right !== 0n ? left % right : undefined; + case "shl": + return left << right; + case "shr": + return left >> right; + case "lt": + return left < right; + case "gt": + return left > right; + case "le": + return left <= right; + case "ge": + return left >= right; + case "eq": + return left === right; + case "ne": + return left !== right; + } + } + + if (typeof left === "boolean" && typeof right === "boolean") { + switch (op) { + case "and": + return left && right; + case "or": + return left || right; + case "eq": + return left === right; + case "ne": + return left !== right; + } + } + + // Handle boolean as bigint for bitwise operations + if (op === "or" || op === "shl" || op === "shr") { + const leftBigint = typeof left === "boolean" ? (left ? 1n : 0n) : left; + const rightBigint = + typeof right === "boolean" ? (right ? 1n : 0n) : right; + + if (typeof leftBigint === "bigint" && typeof rightBigint === "bigint") { + switch (op) { + case "or": + return leftBigint | rightBigint; + case "shl": + return leftBigint << rightBigint; + case "shr": + return leftBigint >> rightBigint; + } + } + } + + return undefined; + } + + private getResultType(_op: string, resultType: string): Ir.Type { + if (resultType === "boolean") { + return Ir.Type.Scalar.bool; + } else if (resultType === "bigint") { + return Ir.Type.Scalar.uint256; + } + return Ir.Type.Scalar.bool; + } + + private canFoldHash( + inst: Ir.Instruction, + constants: Map, + ): boolean { + if (inst.kind !== "hash") return false; + + const inputValue = this.getConstantValue(inst.value, constants); + // We can only fold if the input is a constant string + return typeof inputValue === "string"; + } + + private foldHash( + inst: Ir.Instruction & { kind: "hash" }, + constants: Map, + ): Ir.Instruction | null { + const inputValue = this.getConstantValue(inst.value, constants); + + if (typeof inputValue !== "string") return null; + + // Convert string to bytes + const encoder = new TextEncoder(); + const inputBytes = encoder.encode(inputValue); + + // Compute keccak256 hash + const hashBytes = keccak256(inputBytes); + + // Convert hash bytes to bigint (bytes32 value) + let hashValue = 0n; + for (let i = 0; i < hashBytes.length; i++) { + hashValue = (hashValue << 8n) | BigInt(hashBytes[i]); + } + + return { + kind: "const", + value: hashValue, + type: Ir.Type.Scalar.bytes32, + dest: inst.dest, + operationDebug: Ir.Utils.preserveDebug(inst), + }; + } + + private canFoldLength(inst: Ir.Instruction): boolean { + if (inst.kind !== "length") return false; + + // In the new type system, we can't easily fold length without Bug type info + // For now, disable length folding + return false; + } + + private foldLength( + _inst: Ir.Instruction & { kind: "length" }, + ): Ir.Instruction | null { + // Length folding disabled for now with new type system + // Would need to check origin Bug type for array info + + return null; + } +} diff --git a/packages/bugc/src/optimizer/steps/constant-propagation.ts b/packages/bugc/src/optimizer/steps/constant-propagation.ts new file mode 100644 index 00000000..1fb5f860 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/constant-propagation.ts @@ -0,0 +1,181 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class ConstantPropagationStep extends BaseOptimizationStep { + name = "constant-propagation"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Track known constant values and their debug info across the function + const constants = new Map(); + const constantDebug = new Map(); + + for (const block of func.blocks.values()) { + const newInstructions: Ir.Instruction[] = []; + + for (const inst of block.instructions) { + let newInst = inst; + + // Track constant assignments and their debug info + if (inst.kind === "const" && "dest" in inst) { + constants.set(inst.dest, inst.value); + if (inst.operationDebug) { + constantDebug.set(inst.dest, inst.operationDebug); + } + } else { + // Try to propagate constants into instruction operands + const propagated = this.propagateConstantsIntoInstruction( + inst, + constants, + constantDebug, + ); + if (propagated !== inst) { + newInst = propagated; + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(inst), + result: Ir.Utils.extractContexts(newInst), + reason: "Propagated constants into instruction operands", + }); + } + + // Clear constant info if instruction has side effects + if (this.hasSideEffects(inst)) { + // Conservative: clear all constant info + // A more sophisticated analysis would track what's invalidated + constants.clear(); + constantDebug.clear(); + } + } + + newInstructions.push(newInst); + } + + block.instructions = newInstructions; + } + }); + + return optimized; + } + + private propagateConstantsIntoInstruction( + inst: Ir.Instruction, + constants: Map, + constantDebug: Map, + ): Ir.Instruction { + // Clone instruction and replace temp operands with constants where possible + const result = { ...inst }; + const propagatedDebugContexts: Ir.Instruction.Debug[] = []; + + const propagateValue = (value: Ir.Value): Ir.Value => { + if (value.kind === "temp") { + const constValue = constants.get(value.id); + if (constValue !== undefined) { + // Track debug info from the constant definition + const debug = constantDebug.get(value.id); + if (debug) { + propagatedDebugContexts.push(debug); + } + return { + kind: "const", + value: constValue, + type: value.type || this.getTypeForValue(constValue), + }; + } + } + return value; + }; + + // Apply propagation based on instruction type + switch (result.kind) { + case "binary": + result.left = propagateValue(result.left); + result.right = propagateValue(result.right); + break; + case "unary": + result.operand = propagateValue(result.operand); + break; + case "write": + if (result.slot) result.slot = propagateValue(result.slot); + if (result.value) result.value = propagateValue(result.value); + if (result.offset) result.offset = propagateValue(result.offset); + if (result.length) result.length = propagateValue(result.length); + break; + case "read": + if (result.slot) result.slot = propagateValue(result.slot); + if (result.offset) result.offset = propagateValue(result.offset); + if (result.length) result.length = propagateValue(result.length); + break; + case "compute_slot": + result.base = propagateValue(result.base); + if (Ir.Instruction.ComputeSlot.isMapping(result)) { + result.key = propagateValue(result.key); + } else if (Ir.Instruction.ComputeSlot.isArray(result)) { + // Array compute_slot no longer has index field + } + break; + case "hash": + result.value = propagateValue(result.value); + break; + case "cast": + result.value = propagateValue(result.value); + break; + case "compute_offset": + result.base = propagateValue(result.base); + if (Ir.Instruction.ComputeOffset.isArray(result)) { + result.index = propagateValue(result.index); + } else if (Ir.Instruction.ComputeOffset.isByte(result)) { + result.offset = propagateValue(result.offset); + } + // Field type doesn't have any Values to propagate (fieldOffset is a number) + break; + case "allocate": + result.size = propagateValue(result.size); + break; + case "length": + result.object = propagateValue(result.object); + break; + } + + // Check if we actually changed anything + const changed = propagatedDebugContexts.length > 0; + + // If we propagated constants, combine debug contexts + if (changed) { + result.operationDebug = Ir.Utils.combineDebugContexts( + inst.operationDebug, + ...propagatedDebugContexts, + ); + } + + return changed ? result : inst; + } + + private getTypeForValue(value: bigint | boolean | string): Ir.Type { + if (typeof value === "boolean") { + return Ir.Type.Scalar.bool; + } else if (typeof value === "bigint") { + return Ir.Type.Scalar.uint256; + } else { + // Strings are references in the new type system + return Ir.Type.Ref.memory(); + } + } + + private hasSideEffects(inst: Ir.Instruction): boolean { + switch (inst.kind) { + case "write": + return true; + default: + return false; + } + } +} diff --git a/packages/bugc/src/optimizer/steps/dead-code-elimination.ts b/packages/bugc/src/optimizer/steps/dead-code-elimination.ts new file mode 100644 index 00000000..88eada2f --- /dev/null +++ b/packages/bugc/src/optimizer/steps/dead-code-elimination.ts @@ -0,0 +1,174 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class DeadCodeEliminationStep extends BaseOptimizationStep { + name = "dead-code-elimination"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Collect all used values for this function + const usedValues = new Set(); + + for (const block of func.blocks.values()) { + // Analyze phi uses + if (block.phis) { + for (const phi of block.phis) { + this.collectUsedValues(phi, usedValues); + } + } + + // Analyze instruction uses + for (const inst of block.instructions) { + this.collectUsedValues(inst, usedValues); + } + + // Analyze terminator uses + if (block.terminator.kind === "branch") { + this.collectValueUse(block.terminator.condition, usedValues); + } else if ( + block.terminator.kind === "return" && + block.terminator.value + ) { + this.collectValueUse(block.terminator.value, usedValues); + } else if (block.terminator.kind === "call") { + // Collect argument uses + for (const arg of block.terminator.arguments) { + this.collectValueUse(arg, usedValues); + } + } + } + + // Remove dead instructions + for (const block of func.blocks.values()) { + // Remove dead phi nodes + if (block.phis) { + const newPhis = block.phis.filter((phi) => { + if (!usedValues.has(phi.dest)) { + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(phi), + result: [], + reason: `Removed unused phi node: ${phi.dest}`, + }); + return false; + } + return true; + }); + block.phis = newPhis; + } + + // Remove dead instructions + const newInstructions: Ir.Instruction[] = []; + + for (const inst of block.instructions) { + if (this.hasSideEffects(inst)) { + newInstructions.push(inst); // Keep instructions with side effects + } else if ( + "dest" in inst && + inst.dest && + !usedValues.has(inst.dest) + ) { + // Dead instruction - track its removal + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(inst), + result: [], + reason: `Removed unused instruction: ${inst.kind} -> ${inst.dest}`, + }); + } else { + newInstructions.push(inst); + } + } + + block.instructions = newInstructions; + } + }); + + return optimized; + } + + private collectUsedValues( + inst: Ir.Block.Phi | Ir.Instruction, + used: Set, + ): void { + switch (inst.kind) { + case "binary": + this.collectValueUse(inst.left, used); + this.collectValueUse(inst.right, used); + break; + case "unary": + this.collectValueUse(inst.operand, used); + break; + case "write": + if (inst.slot) this.collectValueUse(inst.slot, used); + if (inst.value) this.collectValueUse(inst.value, used); + if (inst.offset) this.collectValueUse(inst.offset, used); + if (inst.length) this.collectValueUse(inst.length, used); + break; + case "read": + if (inst.slot) this.collectValueUse(inst.slot, used); + if (inst.offset) this.collectValueUse(inst.offset, used); + if (inst.length) this.collectValueUse(inst.length, used); + break; + case "compute_slot": + this.collectValueUse(inst.base, used); + if (Ir.Instruction.ComputeSlot.isMapping(inst)) { + this.collectValueUse(inst.key, used); + } else if (Ir.Instruction.ComputeSlot.isArray(inst)) { + // Array compute_slot no longer has index field + } + break; + case "hash": + this.collectValueUse(inst.value, used); + break; + case "cast": + this.collectValueUse(inst.value, used); + break; + // Call instruction removed - calls are now block terminators + case "length": + this.collectValueUse(inst.object, used); + break; + case "compute_offset": + this.collectValueUse(inst.base, used); + if (Ir.Instruction.ComputeOffset.isArray(inst)) { + this.collectValueUse(inst.index, used); + } else if (Ir.Instruction.ComputeOffset.isByte(inst)) { + this.collectValueUse(inst.offset, used); + } + // Field type doesn't have any Values to collect (fieldOffset is a number) + break; + case "allocate": + this.collectValueUse(inst.size, used); + break; + case "phi": + for (const value of inst.sources.values()) { + this.collectValueUse(value, used); + } + break; + } + } + + private collectValueUse(value: Ir.Value, used: Set): void { + if (value.kind === "temp") { + used.add(value.id); + } + } + + private hasSideEffects(inst: Ir.Instruction): boolean { + switch (inst.kind) { + case "write": + case "allocate": // Allocate modifies the free memory pointer + return true; + default: + return false; + } + } +} diff --git a/packages/bugc/src/optimizer/steps/index.ts b/packages/bugc/src/optimizer/steps/index.ts new file mode 100644 index 00000000..75a02833 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/index.ts @@ -0,0 +1,9 @@ +export { ConstantFoldingStep } from "./constant-folding.js"; +export { DeadCodeEliminationStep } from "./dead-code-elimination.js"; +export { CommonSubexpressionEliminationStep } from "./common-subexpression-elimination.js"; +export { ConstantPropagationStep } from "./constant-propagation.js"; +export { JumpOptimizationStep } from "./jump-optimization.js"; +export { BlockMergingStep } from "./block-merging.js"; +export { ReturnMergingStep } from "./return-merging.js"; +export { ReadWriteMergingStep } from "./read-write-merging.js"; +export { TailCallOptimizationStep } from "./tail-call-optimization.js"; diff --git a/packages/bugc/src/optimizer/steps/jump-optimization.ts b/packages/bugc/src/optimizer/steps/jump-optimization.ts new file mode 100644 index 00000000..ccb4e058 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/jump-optimization.ts @@ -0,0 +1,232 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class JumpOptimizationStep extends BaseOptimizationStep { + name = "jump-optimization"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Find blocks that are just jumps to other blocks + const jumpTargets = new Map(); + + for (const [blockId, block] of func.blocks) { + if ( + block.instructions.length === 0 && + block.terminator.kind === "jump" + ) { + jumpTargets.set(blockId, block.terminator.target); + } + } + + // Track jump redirections for phi updates + const redirections = new Map>(); + + // Update all references to skip intermediate jumps + for (const [blockId, block] of func.blocks) { + if (block.terminator.kind === "jump") { + const originalTarget = block.terminator.target; + const finalTarget = this.resolveJumpChain( + originalTarget, + jumpTargets, + ); + if (finalTarget !== originalTarget) { + // Track this redirection: blockId was going to originalTarget, now goes to finalTarget + if (!redirections.has(finalTarget)) { + redirections.set(finalTarget, new Map()); + } + redirections.get(finalTarget)!.set(blockId, originalTarget); + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Optimized jump chain: ${originalTarget} -> ${finalTarget}`, + }); + block.terminator.target = finalTarget; + } + } else if (block.terminator.kind === "branch") { + const originalTrue = block.terminator.trueTarget; + const trueFinal = this.resolveJumpChain(originalTrue, jumpTargets); + const originalFalse = block.terminator.falseTarget; + const falseFinal = this.resolveJumpChain(originalFalse, jumpTargets); + + if (trueFinal !== originalTrue) { + // Track this redirection + if (!redirections.has(trueFinal)) { + redirections.set(trueFinal, new Map()); + } + redirections.get(trueFinal)!.set(blockId, originalTrue); + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Optimized true branch jump chain: ${originalTrue} -> ${trueFinal}`, + }); + block.terminator.trueTarget = trueFinal; + } + if (falseFinal !== originalFalse) { + // Track this redirection + if (!redirections.has(falseFinal)) { + redirections.set(falseFinal, new Map()); + } + redirections.get(falseFinal)!.set(blockId, originalFalse); + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Optimized false branch jump chain: ${originalFalse} -> ${falseFinal}`, + }); + block.terminator.falseTarget = falseFinal; + } + } + } + + // Update phi nodes for redirected jumps + this.updatePhisForRedirections(func, redirections, jumpTargets); + + // Remove unreachable blocks + const reachable = this.findReachableBlocks(func); + const blocksToRemove: string[] = []; + + for (const blockId of func.blocks.keys()) { + if (!reachable.has(blockId)) { + blocksToRemove.push(blockId); + const block = func.blocks.get(blockId)!; + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: [], + reason: `Removed unreachable block ${blockId}`, + }); + } + } + + for (const blockId of blocksToRemove) { + func.blocks.delete(blockId); + } + }); + + return optimized; + } + + private resolveJumpChain( + target: string, + jumpTargets: Map, + ): string { + const visited = new Set(); + let current = target; + + while (jumpTargets.has(current) && !visited.has(current)) { + visited.add(current); + current = jumpTargets.get(current)!; + } + + return current; + } + + private updatePhisForRedirections( + func: Ir.Function, + redirections: Map>, + jumpTargets: Map, + ): void { + // For each block that has redirected jumps coming into it + for (const [targetBlock, sourceMap] of redirections) { + const block = func.blocks.get(targetBlock); + if (!block) continue; + + // Update phi nodes in this block + for (const phi of block.phis) { + const newSources = new Map(phi.sources); + + // For each redirection (newSource was going to oldSource, now goes here) + for (const [newSource, oldSource] of sourceMap) { + // If the phi had a value from oldSource, we need to update it + if (phi.sources.has(oldSource)) { + // Get the value that was coming from oldSource + const value = phi.sources.get(oldSource)!; + + // Check if oldSource was a jump-only block that might have phi nodes + const oldBlock = func.blocks.get(oldSource); + if (oldBlock && oldBlock.phis.length > 0) { + // We need to thread through the phi values from the intermediate block + // This is complex - for now, we'll just copy the value + // A more sophisticated approach would thread through phi values + newSources.set(newSource, value); + } else { + // Simple case: just redirect the source + newSources.set(newSource, value); + } + + // If oldSource is being removed (it's a jump-only block), remove it from phi + if (jumpTargets.has(oldSource)) { + newSources.delete(oldSource); + } + } + } + + phi.sources = newSources; + } + + // Update predecessors list as well + const newPredecessors = new Set(); + for (const pred of block.predecessors) { + // If this predecessor was redirected, use the original source + let found = false; + for (const [newSource, oldSource] of sourceMap) { + if (oldSource === pred) { + newPredecessors.add(newSource); + found = true; + } + } + if (!found && !jumpTargets.has(pred)) { + // Keep predecessors that weren't redirected and aren't being removed + newPredecessors.add(pred); + } + } + // Add any new predecessors from redirections + for (const newSource of sourceMap.keys()) { + newPredecessors.add(newSource); + } + block.predecessors = newPredecessors; + } + } + + private findReachableBlocks(func: Ir.Function): Set { + const reachable = new Set(); + const worklist = [func.entry]; + + while (worklist.length > 0) { + const blockId = worklist.pop()!; + if (reachable.has(blockId)) continue; + + reachable.add(blockId); + const block = func.blocks.get(blockId); + if (!block) continue; + + // Add successors to worklist + if (block.terminator.kind === "jump") { + worklist.push(block.terminator.target); + } else if (block.terminator.kind === "branch") { + worklist.push(block.terminator.trueTarget); + worklist.push(block.terminator.falseTarget); + } else if (block.terminator.kind === "call") { + // Call instructions have a continuation block + worklist.push(block.terminator.continuation); + } + } + + return reachable; + } +} diff --git a/packages/bugc/src/optimizer/steps/read-write-merging.ts b/packages/bugc/src/optimizer/steps/read-write-merging.ts new file mode 100644 index 00000000..1ac2ecde --- /dev/null +++ b/packages/bugc/src/optimizer/steps/read-write-merging.ts @@ -0,0 +1,396 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class ReadWriteMergingStep extends BaseOptimizationStep { + name = "read-write-merging"; + private nextTempCounter = 0; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Find the highest temp number in the module + this.nextTempCounter = this.findHighestTempNumber(optimized) + 1; + + this.processAllFunctions(optimized, (func) => { + for (const block of func.blocks.values()) { + const newInstructions: Ir.Instruction[] = []; + let i = 0; + + while (i < block.instructions.length) { + const inst = block.instructions[i]; + + if (inst.kind === "write") { + // Try to find consecutive writes that can be merged + const mergeGroup = this.findMergeableWrites(block.instructions, i); + + if (mergeGroup.length > 1) { + // Merge the writes + const merged = this.mergeWrites(mergeGroup, context); + newInstructions.push(...merged); + i += mergeGroup.length; + } else { + newInstructions.push(inst); + i++; + } + } else { + newInstructions.push(inst); + i++; + } + } + + block.instructions = newInstructions; + } + }); + + return optimized; + } + + /** + * Find the highest temp number in the module + */ + private findHighestTempNumber(module: Ir.Module): number { + let highest = -1; + + const checkValue = (value: Ir.Value) => { + if (value.kind === "temp") { + const match = value.id.match(/^t(\d+)$/); + if (match) { + highest = Math.max(highest, parseInt(match[1], 10)); + } + } + }; + + const checkInstruction = (inst: Ir.Instruction) => { + // Check dest + if ("dest" in inst && typeof inst.dest === "string") { + const match = inst.dest.match(/^t(\d+)$/); + if (match) { + highest = Math.max(highest, parseInt(match[1], 10)); + } + } + + // Check values in instruction + if ("left" in inst && inst.left && typeof inst.left === "object") + checkValue(inst.left); + if ("right" in inst && inst.right && typeof inst.right === "object") + checkValue(inst.right); + if ("value" in inst && inst.value && typeof inst.value === "object") + checkValue(inst.value); + if ("operand" in inst && inst.operand && typeof inst.operand === "object") + checkValue(inst.operand); + if ("slot" in inst && inst.slot && typeof inst.slot === "object") + checkValue(inst.slot); + if ("offset" in inst && inst.offset && typeof inst.offset === "object") + checkValue(inst.offset); + if ("length" in inst && inst.length && typeof inst.length === "object") + checkValue(inst.length); + if ("base" in inst && inst.base && typeof inst.base === "object") + checkValue(inst.base); + if ("key" in inst && inst.key && typeof inst.key === "object") + checkValue(inst.key); + if ("index" in inst && inst.index && typeof inst.index === "object") + checkValue(inst.index); + if ("size" in inst && inst.size && typeof inst.size === "object") + checkValue(inst.size); + if ("object" in inst && inst.object && typeof inst.object === "object") + checkValue(inst.object); + }; + + this.processAllFunctions(module, (func) => { + for (const block of func.blocks.values()) { + // Check phis + if (block.phis) { + for (const phi of block.phis) { + const match = phi.dest.match(/^t(\d+)$/); + if (match) { + highest = Math.max(highest, parseInt(match[1], 10)); + } + for (const value of phi.sources.values()) { + checkValue(value); + } + } + } + + // Check instructions + for (const inst of block.instructions) { + checkInstruction(inst); + } + + // Check terminator + if ("condition" in block.terminator && block.terminator.condition) { + checkValue(block.terminator.condition); + } + if ("value" in block.terminator && block.terminator.value) { + checkValue(block.terminator.value); + } + } + }); + + return highest; + } + + /** + * Find consecutive write instructions that can be merged + */ + private findMergeableWrites( + instructions: Ir.Instruction[], + startIndex: number, + ): Ir.Instruction.Write[] { + const writes: Ir.Instruction.Write[] = []; + const firstWrite = instructions[startIndex]; + + if (firstWrite.kind !== "write") return writes; + + writes.push(firstWrite); + + // Look ahead for more writes to the same location + for (let i = startIndex + 1; i < instructions.length; i++) { + const inst = instructions[i]; + + // Stop if we hit a non-write instruction + if (inst.kind !== "write") break; + + // Check if this write can be merged with the group + if (this.canMergeWith(firstWrite, inst)) { + writes.push(inst); + } else { + break; + } + } + + return writes; + } + + /** + * Check if two writes can be merged + */ + private canMergeWith( + first: Ir.Instruction.Write, + second: Ir.Instruction.Write, + ): boolean { + // Must be same location type + if (first.location !== second.location) return false; + + // For storage/transient: must have same slot + if (first.location === "storage" || first.location === "transient") { + if (!first.slot || !second.slot) return false; + if (!this.isSameValue(first.slot, second.slot)) return false; + + // Both must have constant offsets and lengths for now + if ( + first.offset?.kind !== "const" || + first.length?.kind !== "const" || + second.offset?.kind !== "const" || + second.length?.kind !== "const" + ) { + return false; + } + + return true; + } + + // For memory: would need to check if addresses are related + // For now, only support storage/transient merging + return false; + } + + /** + * Check if two values are the same + */ + private isSameValue(a: Ir.Value, b: Ir.Value): boolean { + if (a.kind !== b.kind) return false; + + if (a.kind === "const" && b.kind === "const") { + return a.value === b.value; + } + + if (a.kind === "temp" && b.kind === "temp") { + return a.id === b.id; + } + + return false; + } + + /** + * Merge multiple write instructions into optimized sequence + */ + private mergeWrites( + writes: Ir.Instruction.Write[], + context: OptimizationContext, + ): Ir.Instruction[] { + if (writes.length === 1) return writes; + + // Extract offset and length info + type WriteInfo = { + write: Ir.Instruction.Write; + offset: bigint; + length: bigint; + }; + + const writeInfos: WriteInfo[] = writes.map((write) => ({ + write, + offset: write.offset?.kind === "const" ? BigInt(write.offset.value) : 0n, + length: write.length?.kind === "const" ? BigInt(write.length.value) : 32n, + })); + + // Sort by offset + writeInfos.sort((a, b) => Number(a.offset - b.offset)); + + // Check if writes are adjacent or overlapping (for simple merging) + const canSimpleMerge = this.areWritesAdjacent(writeInfos); + + if (!canSimpleMerge) { + // For non-adjacent writes, keep them separate for now + return writes; + } + + // Generate instructions to combine the values + const instructions: Ir.Instruction[] = []; + let tempCounter = this.nextTempCounter; + let combinedValue: Ir.Value | null = null; + let combinedDebug: Ir.Instruction.Debug = {}; + + for (let i = 0; i < writeInfos.length; i++) { + const info = writeInfos[i]; + const shiftBits = info.offset * 8n; + + // Track debug contexts as we process writes + combinedDebug = Ir.Utils.combineDebugContexts( + combinedDebug, + info.write.operationDebug, + ); + + if (shiftBits > 0n) { + // Need to shift the value + const shiftTemp = `t${tempCounter++}`; + instructions.push({ + kind: "binary", + op: "shl", + left: info.write.value, + right: { + kind: "const", + value: shiftBits, + type: Ir.Type.Scalar.uint256, + }, + dest: shiftTemp, + operationDebug: combinedDebug, + }); + + const shiftedValue: Ir.Value = { + kind: "temp", + id: shiftTemp, + type: Ir.Type.Scalar.uint256, + }; + + if (combinedValue === null) { + combinedValue = shiftedValue; + } else { + // OR with previous combined value + const orTemp = `t${tempCounter++}`; + instructions.push({ + kind: "binary", + op: "or", + left: combinedValue, + right: shiftedValue, + dest: orTemp, + operationDebug: combinedDebug, + }); + combinedValue = { + kind: "temp", + id: orTemp, + type: Ir.Type.Scalar.uint256, + }; + } + } else { + // No shift needed + if (combinedValue === null) { + combinedValue = info.write.value; + } else { + // OR with previous combined value + const orTemp = `t${tempCounter++}`; + instructions.push({ + kind: "binary", + op: "or", + left: combinedValue, + right: info.write.value, + dest: orTemp, + operationDebug: combinedDebug, + }); + combinedValue = { + kind: "temp", + id: orTemp, + type: Ir.Type.Scalar.uint256, + }; + } + } + } + + // Calculate the merged write parameters + const minOffset = writeInfos[0].offset; + const maxEnd = writeInfos.reduce((max, info) => { + const end = info.offset + info.length; + return end > max ? end : max; + }, minOffset + writeInfos[0].length); + const totalLength = maxEnd - minOffset; + + // Create merged write instruction + const mergedWrite: Ir.Instruction.Write = { + kind: "write", + location: writes[0].location, + slot: writes[0].slot, + offset: + minOffset > 0n + ? { kind: "const", value: minOffset, type: Ir.Type.Scalar.uint256 } + : undefined, + length: { + kind: "const", + value: totalLength, + type: Ir.Type.Scalar.uint256, + }, + value: combinedValue!, + operationDebug: combinedDebug, + }; + + instructions.push(mergedWrite); + + // Update the temp counter for next time + this.nextTempCounter = tempCounter; + + // Track transformation + context.trackTransformation({ + type: "merge", + pass: this.name, + original: Ir.Utils.extractContexts(...writes), + result: Ir.Utils.extractContexts(...instructions), + reason: `Merged ${writes.length} writes to same location`, + }); + + return instructions; + } + + /** + * Check if writes are adjacent or can be easily combined + */ + private areWritesAdjacent(writeInfos: WriteInfo[]): boolean { + for (let i = 1; i < writeInfos.length; i++) { + const prev = writeInfos[i - 1]; + const curr = writeInfos[i]; + + // Check if current write starts at or before previous write ends + if (curr.offset > prev.offset + prev.length) { + // Gap between writes - not adjacent + return false; + } + } + return true; + } +} + +type WriteInfo = { + write: Ir.Instruction.Write; + offset: bigint; + length: bigint; +}; diff --git a/packages/bugc/src/optimizer/steps/return-merging.test.ts b/packages/bugc/src/optimizer/steps/return-merging.test.ts new file mode 100644 index 00000000..e5e7d890 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/return-merging.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect } from "vitest"; + +import * as Irgen from "#irgen"; +import { parse } from "#parser"; +import { Result } from "#result"; +import * as TypeChecker from "#typechecker"; + +import { ReturnMergingStep } from "./return-merging.js"; +import { OptimizationContextImpl } from "../optimizer.js"; + +describe("ReturnMergingStep", () => { + it("merges multiple return void blocks into one", () => { + const source = ` + name TestReturnMerge; + + storage { + [0] x: uint256; + } + + code { + if (x == 0) { + return; + } + + if (x == 1) { + return; + } + + if (x == 2) { + return; + } + + x = x + 1; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + const mainFunc = module.main; + + // Count return blocks before optimization + let returnBlocksBefore = 0; + for (const [, block] of mainFunc.blocks) { + if ( + block.terminator.kind === "return" && + block.instructions.length === 0 + ) { + returnBlocksBefore++; + } + } + + // Should have multiple return blocks initially + expect(returnBlocksBefore).toBeGreaterThan(1); + + // Apply return merging + const pass = new ReturnMergingStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.main; + + // Count return blocks after optimization + let returnBlocksAfter = 0; + let mergedReturnBlockId: string | null = null; + for (const [blockId, block] of optimizedFunc.blocks) { + if ( + block.terminator.kind === "return" && + block.instructions.length === 0 + ) { + returnBlocksAfter++; + mergedReturnBlockId = blockId; + } + } + + // Should have only one return block after optimization + expect(returnBlocksAfter).toBe(1); + expect(mergedReturnBlockId).not.toBeNull(); + + // Check that multiple blocks now jump to the merged return block + let jumpsToReturn = 0; + for (const [, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "branch") { + if (block.terminator.trueTarget === mergedReturnBlockId) + jumpsToReturn++; + if (block.terminator.falseTarget === mergedReturnBlockId) + jumpsToReturn++; + } else if ( + block.terminator.kind === "jump" && + block.terminator.target === mergedReturnBlockId + ) { + jumpsToReturn++; + } + } + + // Should have multiple jumps to the single return block + expect(jumpsToReturn).toBeGreaterThan(1); + }); + + it("does not merge return blocks with different values", () => { + const source = ` + name TestDifferentReturns; + + define { + function getValue(x: uint256) -> uint256 { + if (x == 0) { + return 10; + } + return 20; + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + const getValueFunc = module.functions.get("getValue")!; + + // Count return blocks before + let returnBlocksBefore = 0; + for (const [, block] of getValueFunc.blocks) { + if (block.terminator.kind === "return") { + returnBlocksBefore++; + } + } + + // Apply return merging + const pass = new ReturnMergingStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("getValue")!; + + // Count return blocks after + let returnBlocksAfter = 0; + for (const [, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "return") { + returnBlocksAfter++; + } + } + + // Should not merge returns with different values + expect(returnBlocksAfter).toBe(returnBlocksBefore); + }); +}); diff --git a/packages/bugc/src/optimizer/steps/return-merging.ts b/packages/bugc/src/optimizer/steps/return-merging.ts new file mode 100644 index 00000000..d51c6602 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/return-merging.ts @@ -0,0 +1,133 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +export class ReturnMergingStep extends BaseOptimizationStep { + name = "return-merging"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + // Process each function separately + this.processAllFunctions(optimized, (func) => { + // Group blocks by their return value (or lack thereof) + const returnGroups = new Map(); // signature -> block IDs + + for (const [blockId, block] of func.blocks) { + if (block.terminator.kind === "return") { + // Create a signature for the return statement + const signature = this.getReturnSignature(block.terminator.value); + + if (!returnGroups.has(signature)) { + returnGroups.set(signature, []); + } + returnGroups.get(signature)!.push(blockId); + } + } + + // For each group with multiple blocks, merge them + for (const [, blockIds] of returnGroups) { + if (blockIds.length > 1) { + // Keep the first block as the target + const targetBlockId = blockIds[0]; + const targetBlock = func.blocks.get(targetBlockId)!; + + // Redirect all other blocks to jump to the target + for (let i = 1; i < blockIds.length; i++) { + const sourceBlockId = blockIds[i]; + const sourceBlock = func.blocks.get(sourceBlockId)!; + + // Only merge if the block contains only the return statement + if (sourceBlock.instructions.length === 0) { + // Update all references to the source block to point to target + for (const [, block] of func.blocks) { + if ( + block.terminator.kind === "jump" && + block.terminator.target === sourceBlockId + ) { + block.terminator.target = targetBlockId; + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Redirected jump from ${sourceBlockId} to ${targetBlockId} (return merging)`, + }); + } else if (block.terminator.kind === "branch") { + if (block.terminator.trueTarget === sourceBlockId) { + block.terminator.trueTarget = targetBlockId; + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Redirected true branch from ${sourceBlockId} to ${targetBlockId} (return merging)`, + }); + } + if (block.terminator.falseTarget === sourceBlockId) { + block.terminator.falseTarget = targetBlockId; + + context.trackTransformation({ + type: "replace", + pass: this.name, + original: Ir.Utils.extractContexts(block), + result: Ir.Utils.extractContexts(block), + reason: `Redirected false branch from ${sourceBlockId} to ${targetBlockId} (return merging)`, + }); + } + } + } + + // Update predecessors in the target block + for (const pred of sourceBlock.predecessors) { + targetBlock.predecessors.add(pred); + } + targetBlock.predecessors.delete(sourceBlockId); + + // Update all blocks that reference the source block + for (const [, block] of func.blocks) { + if (block.predecessors.has(sourceBlockId)) { + block.predecessors.delete(sourceBlockId); + block.predecessors.add(targetBlockId); + } + } + + // Remove the merged block + func.blocks.delete(sourceBlockId); + + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(sourceBlock), + result: [], + reason: `Merged return block ${sourceBlockId} into ${targetBlockId}`, + }); + } + } + } + } + }); + + return optimized; + } + + private getReturnSignature(value?: Ir.Value): string { + if (!value) { + return "void"; + } + + // For now, we only merge void returns + // In the future, we could extend this to merge returns with identical constant values + if (value.kind === "const") { + return `const:${value.type.kind}:${value.value}`; + } else if (value.kind === "temp") { + return `temp:${value.id}`; + } + + return "unknown"; + } +} diff --git a/packages/bugc/src/optimizer/steps/tail-call-optimization.test.ts b/packages/bugc/src/optimizer/steps/tail-call-optimization.test.ts new file mode 100644 index 00000000..ce888a24 --- /dev/null +++ b/packages/bugc/src/optimizer/steps/tail-call-optimization.test.ts @@ -0,0 +1,399 @@ +import { describe, it, expect } from "vitest"; + +import * as Irgen from "#irgen"; +import { parse } from "#parser"; +import { Result } from "#result"; +import * as TypeChecker from "#typechecker"; + +import { TailCallOptimizationStep } from "./tail-call-optimization.js"; +import { OptimizationContextImpl } from "../optimizer.js"; + +describe("TailCallOptimizationStep", () => { + it("optimizes simple tail-recursive factorial", () => { + const source = ` + name TestTailRecursion; + + define { + function factorial(n: uint256, acc: uint256) -> uint256 { + if (n == 0) { + return acc; + } + return factorial(n - 1, acc * n); + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + const factorialFunc = module.functions.get("factorial")!; + + // Count call terminators before optimization + let callsBeforeCount = 0; + for (const [, block] of factorialFunc.blocks) { + if (block.terminator.kind === "call") { + callsBeforeCount++; + } + } + + // Should have at least one call before optimization + expect(callsBeforeCount).toBeGreaterThan(0); + + // Apply tail call optimization + const pass = new TailCallOptimizationStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("factorial")!; + + // Count call terminators after optimization + let callsAfterCount = 0; + let hasLoopHeader = false; + + for (const [blockId, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "call") { + callsAfterCount++; + } + // Look for the loop header block + if (blockId.includes("_loop")) { + hasLoopHeader = true; + // Loop header should have phi nodes for parameters + expect(block.phis.length).toBe(factorialFunc.parameters.length); + } + } + + // Tail-recursive calls should be eliminated + expect(callsAfterCount).toBe(0); + + // Should have created a loop header + expect(hasLoopHeader).toBe(true); + + // Should have recorded transformations + const transformations = context.getTransformations(); + expect(transformations.length).toBeGreaterThan(0); + expect( + transformations.some((t) => t.reason.includes("tail-recursive")), + ).toBe(true); + }); + + it("does not optimize non-tail calls", () => { + const source = ` + name TestNonTailRecursion; + + define { + function fibonacci(n: uint256) -> uint256 { + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + return fibonacci(n - 1) + fibonacci(n - 2); + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + const fibFunc = module.functions.get("fibonacci")!; + + // Count blocks before optimization + const blocksBefore = fibFunc.blocks.size; + + // Apply tail call optimization + const pass = new TailCallOptimizationStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("fibonacci")!; + + // Count blocks after optimization + const blocksAfter = optimizedFunc.blocks.size; + + // Should not have changed the structure (non-tail recursion) + expect(blocksAfter).toBe(blocksBefore); + + // Should have no transformations + const transformations = context.getTransformations(); + expect(transformations.length).toBe(0); + }); + + it("optimizes multiple tail-recursive calls in different branches", () => { + const source = ` + name TestMultipleTailCalls; + + define { + function search(n: uint256, target: uint256) -> bool { + if (n == target) { + return true; + } + if (n > target) { + return search(n - 1, target); + } + return false; + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + + // Apply tail call optimization + const pass = new TailCallOptimizationStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("search")!; + + // Count call terminators after optimization + let callsAfter = 0; + for (const [, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "call") { + callsAfter++; + } + } + + // Should have eliminated tail calls + expect(callsAfter).toBe(0); + + // Should have recorded transformations + const transformations = context.getTransformations(); + expect(transformations.length).toBeGreaterThan(0); + }); + + it("preserves debug information during optimization", () => { + const source = ` + name TestDebugPreservation; + + define { + function countdown(n: uint256) -> uint256 { + if (n == 0) { + return 0; + } + return countdown(n - 1); + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + + // Apply tail call optimization + const pass = new TailCallOptimizationStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("countdown")!; + + // Verify tail calls were eliminated + let callsAfter = 0; + for (const [, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "call") { + callsAfter++; + } + } + expect(callsAfter).toBe(0); + + // Check that transformations were tracked (debug preservation) + const transformations = context.getTransformations(); + expect(transformations.length).toBeGreaterThan(0); + + // Each transformation should have original and result contexts + for (const transform of transformations) { + expect(transform.pass).toBe("tail-call-optimization"); + expect(transform.reason).toBeTruthy(); + // Original should have contexts (may be empty array for deletions) + expect(Array.isArray(transform.original)).toBe(true); + expect(Array.isArray(transform.result)).toBe(true); + } + }); + + it("handles functions with no parameters", () => { + const source = ` + name TestNoParams; + + define { + function alwaysZero() -> uint256 { + if (true) { + return 0; + } + return alwaysZero(); + }; + } + + code { + return; + } + `; + + // Parse and type check + const parseResult = parse(source); + if (!parseResult.success) { + throw new Error( + "Parse failed: " + + (Result.firstError(parseResult)?.message || "Unknown error"), + ); + } + + const typeResult = TypeChecker.checkProgram(parseResult.value); + if (!typeResult.success) { + throw new Error( + "Type check failed: " + + (Result.firstError(typeResult)?.message || "Unknown error"), + ); + } + + // Build IR + const irResult = Irgen.generateModule( + parseResult.value, + typeResult.value.types, + ); + if (!irResult.success) { + throw new Error( + "IR build failed: " + + (Result.firstError(irResult)?.message || "Unknown error"), + ); + } + + const module = irResult.value; + + // Apply tail call optimization + const pass = new TailCallOptimizationStep(); + const context = new OptimizationContextImpl(); + const optimized = pass.run(module, context); + const optimizedFunc = optimized.functions.get("alwaysZero")!; + + // Should still work with no parameters + let callsAfter = 0; + for (const [, block] of optimizedFunc.blocks) { + if (block.terminator.kind === "call") { + callsAfter++; + } + } + + expect(callsAfter).toBe(0); + }); +}); diff --git a/packages/bugc/src/optimizer/steps/tail-call-optimization.ts b/packages/bugc/src/optimizer/steps/tail-call-optimization.ts new file mode 100644 index 00000000..88961dde --- /dev/null +++ b/packages/bugc/src/optimizer/steps/tail-call-optimization.ts @@ -0,0 +1,203 @@ +import * as Ir from "#ir"; +import { + BaseOptimizationStep, + type OptimizationContext, +} from "../optimizer.js"; + +/** + * Tail Call Optimization + * + * Optimizes tail-recursive calls by transforming them into loops with phi + * nodes. This eliminates the call overhead and prevents stack growth. + * + * A tail call is eligible for optimization when: + * 1. The call is to the current function (recursion) + * 2. The continuation block immediately returns the call result + * 3. The continuation block has no other instructions or phis + * + * Transformation approach: + * - Create a loop header block with phi nodes for parameters + * - Redirect the tail-recursive call to jump to the loop header + * - The phi nodes select between initial parameters and recursive arguments + */ +export class TailCallOptimizationStep extends BaseOptimizationStep { + name = "tail-call-optimization"; + + run(module: Ir.Module, context: OptimizationContext): Ir.Module { + const optimized = this.cloneModule(module); + + this.processAllFunctions(optimized, (func, funcName) => { + const blocksToRemove = new Set(); + + // First pass: identify all tail-recursive calls + const tailCallBlocks: string[] = []; + + for (const [blockId, block] of func.blocks) { + // Only process call terminators + if (block.terminator.kind !== "call") { + continue; + } + + const callTerm = block.terminator; + + // Check if this is a recursive call to the current function + if (callTerm.function !== funcName) { + continue; + } + + // Get the continuation block + const contBlock = func.blocks.get(callTerm.continuation); + if (!contBlock) { + continue; + } + + // Check if continuation block is a tail position: + // - No phis + // - No instructions + // - Returns the call result (or void if no result) + if ( + contBlock.phis.length > 0 || + contBlock.instructions.length > 0 || + contBlock.terminator.kind !== "return" + ) { + continue; + } + + const returnTerm = contBlock.terminator; + + // Verify the return value matches the call destination + const returnMatchesCall = + (callTerm.dest === undefined && returnTerm.value === undefined) || + (callTerm.dest !== undefined && + returnTerm.value !== undefined && + returnTerm.value.kind === "temp" && + returnTerm.value.id === callTerm.dest); + + if (!returnMatchesCall) { + continue; + } + + tailCallBlocks.push(blockId); + } + + // If we found tail calls, create a loop structure + if (tailCallBlocks.length > 0) { + // Create a new loop header block that will contain phis for parameters + const loopHeaderId = `${func.entry}_loop`; + const originalEntry = func.blocks.get(func.entry); + + if (!originalEntry) { + return; // Should not happen + } + + // Create phi nodes for each parameter + const paramPhis: Ir.Block.Phi[] = []; + for (let i = 0; i < func.parameters.length; i++) { + const param = func.parameters[i]; + const phiSources = new Map(); + + // Initial value from function entry + phiSources.set(func.entry, { + kind: "temp", + id: param.tempId, + type: param.type, + }); + + paramPhis.push({ + kind: "phi", + sources: phiSources, + dest: `${param.tempId}_loop`, + type: param.type, + operationDebug: { context: param.loc ? undefined : undefined }, + }); + } + + // Create the loop header block + const loopHeader: Ir.Block = { + id: loopHeaderId, + phis: paramPhis, + instructions: [], + terminator: { + kind: "jump", + target: func.entry, + operationDebug: {}, + }, + predecessors: new Set([func.entry, ...tailCallBlocks]), + debug: {}, + }; + + func.blocks.set(loopHeaderId, loopHeader); + + // Transform each tail call + for (const blockId of tailCallBlocks) { + const block = func.blocks.get(blockId)!; + const callTerm = block.terminator as Ir.Block.Terminator & { + kind: "call"; + }; + const contBlock = func.blocks.get(callTerm.continuation)!; + + // Update phi sources with arguments from this tail call + for (let i = 0; i < func.parameters.length; i++) { + if (i < callTerm.arguments.length) { + paramPhis[i].sources.set(blockId, callTerm.arguments[i]); + } + } + + // Replace call with jump to loop header + block.terminator = { + kind: "jump", + target: loopHeaderId, + operationDebug: callTerm.operationDebug, + }; + + // Track the transformation + context.trackTransformation({ + type: "replace", + pass: this.name, + original: [ + ...Ir.Utils.extractContexts(block), + ...Ir.Utils.extractContexts(contBlock), + ], + result: Ir.Utils.extractContexts(block), + reason: `Optimized tail-recursive call to ${funcName} into loop`, + }); + + // Mark continuation block for removal if it has no other + // predecessors + const otherPredecessors = Array.from(contBlock.predecessors).filter( + (pred) => pred !== blockId, + ); + + if (otherPredecessors.length === 0) { + blocksToRemove.add(callTerm.continuation); + + context.trackTransformation({ + type: "delete", + pass: this.name, + original: Ir.Utils.extractContexts(contBlock), + result: [], + reason: `Removed unused continuation block ${callTerm.continuation}`, + }); + } else { + // Update predecessors + contBlock.predecessors.delete(blockId); + } + } + } + + // Remove marked blocks + for (const blockId of blocksToRemove) { + func.blocks.delete(blockId); + } + + // Update all predecessor sets to remove deleted blocks + for (const block of func.blocks.values()) { + for (const deletedBlock of blocksToRemove) { + block.predecessors.delete(deletedBlock); + } + } + }); + + return optimized; + } +} diff --git a/packages/bugc/src/parser/create-block.test.ts b/packages/bugc/src/parser/create-block.test.ts new file mode 100644 index 00000000..91dea399 --- /dev/null +++ b/packages/bugc/src/parser/create-block.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect } from "vitest"; +import { Severity } from "#result"; +import { parse } from "./parser.js"; +import "#test/matchers"; + +describe("Create block parsing", () => { + it("parses program with create block", () => { + const code = ` + name TokenContract; + + storage { + [0] totalSupply: uint256; + [1] owner: address; + } + + create { + totalSupply = 1000000; + owner = msg.sender; + } + + code { + let balance = totalSupply; + } + `; + + const result = parse(code); + expect(result.success).toBe(true); + if (!result.success) return; + + const ast = result.value; + expect(ast.name).toBe("TokenContract"); + expect(ast.create).toBeDefined(); + expect(ast.create?.kind).toBe("block:statements"); + expect(ast.create?.items).toHaveLength(2); + expect(ast.body?.kind).toBe("block:statements"); + expect(ast.body?.items).toHaveLength(1); + }); + + it("parses program without create block", () => { + const code = ` + name SimpleProgram; + + code { + let x = 100; + } + `; + + const result = parse(code); + expect(result.success).toBe(true); + if (!result.success) return; + + const ast = result.value; + expect(ast.name).toBe("SimpleProgram"); + expect(ast.create).toBeUndefined(); + expect(ast.body?.kind).toBe("block:statements"); + expect(ast.body?.items).toHaveLength(1); + }); + + it("parses create block with various statements", () => { + const code = ` + name ComplexContract; + + define { + struct Config { + maxSupply: uint256; + paused: bool; + }; + } + + storage { + [0] config: Config; + [1] balances: mapping; + } + + create { + // Initialize configuration + config.maxSupply = 1000000; + config.paused = false; + + // Give creator initial balance + balances[msg.sender] = 1000; + + // Conditional initialization + if (msg.value > 0) { + balances[msg.sender] = balances[msg.sender] + msg.value; + } + } + + code { + // Runtime code + let currentSupply = config.maxSupply; + } + `; + + const result = parse(code); + expect(result.success).toBe(true); + if (!result.success) return; + + const ast = result.value; + expect(ast.create).toBeDefined(); + expect(ast.create?.items).toHaveLength(4); // 2 config assignments + 1 balance assignment + 1 if statement + }); + + it("parses empty create block", () => { + const code = ` + name EmptyCreate; + + create { + // No initialization needed + } + + code { + let x = 42; + } + `; + + const result = parse(code); + expect(result.success).toBe(true); + if (!result.success) return; + + const ast = result.value; + expect(ast.create).toBeDefined(); + expect(ast.create?.items).toHaveLength(0); + }); + + it("requires create block before code block", () => { + const code = ` + name WrongOrder; + + code { + let x = 42; + } + + create { + // This should fail - create must come before code + } + `; + + const result = parse(code); + expect(result.success).toBe(false); + }); + + it("allows create block to use all statement types", () => { + const code = ` + name FullFeatures; + + storage { + [0] counter: uint256; + [1] values: array; + } + + create { + counter = 0; + + for (let i = 0; i < 10; i = i + 1) { + values[i] = i * i; + } + + if (counter == 0) { + counter = 5; + } + + if (counter == 5) { + return; + } + } + + code { + counter = counter + 1; + } + `; + + const result = parse(code); + expect(result.success).toBe(true); + if (!result.success) return; + + const ast = result.value; + expect(ast.create).toBeDefined(); + expect(ast.create?.items).toHaveLength(4); // assignment + for + if + if + }); + + it("disallows 'create' as identifier", () => { + const code = ` + name BadIdentifier; + + code { + let create = 100; // Should fail - create is reserved + } + `; + + const result = parse(code); + expect(result.success).toBe(false); + if (result.success) return; + + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Cannot use keyword 'create' as identifier", + }); + }); +}); diff --git a/packages/bugc/src/parser/define-block.test.ts b/packages/bugc/src/parser/define-block.test.ts new file mode 100644 index 00000000..75f0ca49 --- /dev/null +++ b/packages/bugc/src/parser/define-block.test.ts @@ -0,0 +1,322 @@ +import { describe, it, expect } from "vitest"; +import * as Ast from "#ast"; +import { Severity } from "#result"; +import { parse } from "./parser.js"; +import "#test/matchers"; + +describe("Define Block Parser", () => { + describe("Basic define block parsing", () => { + it("should parse empty define block", () => { + const input = ` + name EmptyDefine; + + define { + } + + storage { + } + + code { + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.definitions?.items ?? []).toHaveLength(0); + }); + + it("should parse define block with single struct", () => { + const input = ` + name SingleStruct; + + define { + struct User { + addr: address; + balance: uint256; + }; + } + + storage { + [0] owner: User; + } + + code { + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + // Should have 1 struct from define block, and 1 storage declaration + expect(result.value.definitions?.items ?? []).toHaveLength(1); + expect(result.value.storage ?? []).toHaveLength(1); + + const structDecl = result.value.definitions!.items[0]; + if (!Ast.Declaration.isStruct(structDecl)) { + throw new Error("Should receive a struct declaration"); + } + expect(structDecl.kind).toBe("declaration:struct"); + expect(structDecl.name).toBe("User"); + expect(structDecl.fields).toHaveLength(2); + + const storageDecl = result.value.storage![0]; + expect(storageDecl.kind).toBe("declaration:storage"); + expect(storageDecl.name).toBe("owner"); + }); + + it("should parse define block with multiple structs", () => { + const input = ` + name MultipleStructs; + + define { + struct User { + addr: address; + balance: uint256; + }; + + struct Transaction { + from: address; + to: address; + amount: uint256; + }; + } + + storage { + [0] owner: User; + [1] lastTx: Transaction; + } + + code { + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + // Should have 2 structs from define block, and 2 storage declarations + expect(result.value.definitions?.items ?? []).toHaveLength(2); + expect(result.value.storage ?? []).toHaveLength(2); + + const userStruct = result.value.definitions!.items[0]; + expect(userStruct.kind).toBe("declaration:struct"); + expect(userStruct.name).toBe("User"); + + const txStruct = result.value.definitions!.items[1]; + expect(txStruct.kind).toBe("declaration:struct"); + expect(txStruct.name).toBe("Transaction"); + }); + + it("should work without define block", () => { + const input = ` + name NoDefineBlock; + + storage { + [0] count: uint256; + } + + code { + count = count + 1; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + // Should only have storage declaration, no definitions + expect(result.value.definitions?.items ?? []).toHaveLength(0); + expect(result.value.storage ?? []).toHaveLength(1); + expect(result.value.storage![0].kind).toBe("declaration:storage"); + }); + }); + + describe("Syntax requirements", () => { + it("should require semicolons after struct declarations in define block", () => { + const input = ` + name MissingSemicolon; + + define { + struct User { + addr: address; + balance: uint256; + } + struct Transaction { + from: address; + }; + } + + storage {} + code {} + `; + + const result = parse(input); + expect(result.success).toBe(false); + }); + + it("should reject struct declarations outside define block", () => { + const input = ` + name StructOutsideDefine; + + struct User { + addr: address; + } + + define { + } + + storage {} + code {} + `; + + const result = parse(input); + expect(result.success).toBe(false); + }); + + it("should reject define keyword as identifier", () => { + const input = ` + name DefineAsIdentifier; + + storage { + [0] define: uint256; + } + + code {} + `; + + const result = parse(input); + expect(result.success).toBe(false); + if (result.success) throw new Error("Parse should have failed"); + + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Cannot use keyword 'define' as identifier", + }); + }); + }); + + describe("Complex scenarios", () => { + it("should parse nested struct references in define block", () => { + const input = ` + name NestedStructs; + + define { + struct Point { + x: uint256; + y: uint256; + }; + + struct Shape { + center: Point; + radius: uint256; + }; + } + + storage { + [0] circle: Shape; + } + + code { + circle.center.x = 100; + circle.center.y = 200; + circle.radius = 50; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.definitions?.items ?? []).toHaveLength(2); // 2 structs from define + expect(result.value.storage ?? []).toHaveLength(1); // 1 storage + }); + + it("should parse define block with empty structs", () => { + const input = ` + name EmptyStructs; + + define { + struct Empty { + }; + + struct AlsoEmpty { + }; + } + + storage {} + code {} + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const declarations = result.value.definitions?.items ?? []; + + expect(declarations).toHaveLength(2); + if (!declarations.every(Ast.Declaration.isStruct)) { + throw new Error("Expected only struct declarations"); + } + + expect(declarations[0].fields).toHaveLength(0); + expect(declarations[1].fields).toHaveLength(0); + }); + + it("should handle define block with comments", () => { + const input = ` + name DefineWithComments; + + define { + // User account structure + struct User { + addr: address; // account address + balance: uint256; // balance in wei + }; + + /* Transaction record */ + struct Transaction { + from: address; + to: address; + amount: uint256; + }; + } + + storage {} + code {} + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.definitions?.items ?? []).toHaveLength(2); + }); + }); + + describe("Source locations", () => { + it("should track source locations for define block", () => { + const input = `name DefineLocation; + +define { + struct User { + addr: address; + }; +} + +storage {} +code {}`; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const structDecl = result.value.definitions!.items[0]; + expect(structDecl.loc).not.toBeNull(); + expect(structDecl.loc?.offset).toBeGreaterThan(0); + expect(structDecl.loc?.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/packages/bugc/src/parser/edge-cases.test.ts b/packages/bugc/src/parser/edge-cases.test.ts new file mode 100644 index 00000000..5aef862d --- /dev/null +++ b/packages/bugc/src/parser/edge-cases.test.ts @@ -0,0 +1,950 @@ +import { describe, test, expect } from "vitest"; +import * as Ast from "#ast"; +import { parse } from "./parser.js"; + +describe("Parser Edge Cases", () => { + describe("Hex Literals", () => { + test("parses hex literals with various lengths", () => { + const cases = [ + { input: "0x0", expected: "0x0" }, + { input: "0x00", expected: "0x00" }, + { input: "0xFF", expected: "0xFF" }, + { input: "0xff", expected: "0xff" }, + { input: "0xDEADBEEF", expected: "0xDEADBEEF" }, + { input: "0xdeadbeef", expected: "0xdeadbeef" }, + { input: "0x123456789ABCDEF", expected: "0x123456789ABCDEF" }, + // 64 character hex literal (bytes32) + { input: "0x" + "F".repeat(64), expected: "0x" + "F".repeat(64) }, + { input: "0x" + "0".repeat(64), expected: "0x" + "0".repeat(64) }, + ]; + + for (const { input, expected } of cases) { + const source = ` + name HexTest; + storage { + [0] value: uint256; + } + code { + value = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const assignment = result.body?.items[0] as Ast.Statement.Assign; + const literal = assignment.value as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:hex"); + expect(literal.value).toBe(expected); + } + }); + + test("parses 64-character hex literal as hex, not address", () => { + const longHex = "0x" + "A".repeat(64); + const source = ` + name Test; + storage {} + code { + let x = ${longHex}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + expect(stmt?.kind).toBe("statement:declare"); + const decl = (stmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(decl)) { + throw new Error("Expected variable declaration"); + } + const literal = decl.initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:hex"); + expect(literal.value).toBe(longHex); + }); + }); + + describe("Address Literals", () => { + test("parses valid address literals", () => { + const cases = [ + "0x1234567890123456789012345678901234567890", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "0x0000000000000000000000000000000000000000", + "0xDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEf", + ]; + + for (const addr of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${addr}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(decl)) { + throw new Error("Expected variable declaration"); + } + const literal = decl.initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:address"); + expect(literal.value).toBe(addr.toLowerCase()); + } + }); + + test("normalizes address case to lowercase", () => { + const source = ` + name Test; + storage {} + code { + let x = 0xABCDEF1234567890123456789012345678901234; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(decl)) { + throw new Error("Expected variable declaration"); + } + const literal = decl.initializer as Ast.Expression.Literal; + expect(literal.value).toBe("0xabcdef1234567890123456789012345678901234"); + }); + + test("distinguishes between address and shorter hex", () => { + const source = ` + name Test; + storage {} + code { + let addr = 0x1234567890123456789012345678901234567890; + let hex = 0x123456789012345678901234567890123456789; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [addrStmt, hexStmt] = result.body?.items || []; + + const addrDecl = (addrStmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(addrDecl)) { + throw new Error("Expected variable declaration"); + } + const addrLiteral = addrDecl.initializer as Ast.Expression.Literal; + expect(addrLiteral.kind).toBe("expression:literal:address"); + + const hexDecl = (hexStmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(hexDecl)) { + throw new Error("Expected variable declaration"); + } + const hexLiteral = hexDecl.initializer as Ast.Expression.Literal; + expect(hexLiteral.kind).toBe("expression:literal:hex"); + }); + }); + + describe("Number Literals", () => { + test("parses various number formats", () => { + const cases = [ + { input: "0", expected: "0" }, + { input: "1", expected: "1" }, + { input: "42", expected: "42" }, + { input: "123456789", expected: "123456789" }, + { input: "999999999999999999", expected: "999999999999999999" }, + ]; + + for (const { input, expected } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + if (!Ast.Declaration.isVariable(decl)) { + throw new Error("Expected variable declaration"); + } + const literal = decl.initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:number"); + expect(literal.value).toBe(expected); + } + }); + + test("parses numbers with leading zeros", () => { + const source = ` + name Test; + storage {} + code { + let x = 007; + let y = 000123; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [xStmt, yStmt] = result.body?.items || []; + + const xDecl = (xStmt as Ast.Statement.Declare).declaration; + const xLiteral = (xDecl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(xLiteral.value).toBe("007"); + + const yDecl = (yStmt as Ast.Statement.Declare).declaration; + const yLiteral = (yDecl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(yLiteral.value).toBe("000123"); + }); + }); + + describe("Wei Literals", () => { + test("parses wei unit literals", () => { + const cases = [ + { input: "1 wei", value: "1", unit: "wei" }, + { input: "100 wei", value: "100", unit: "wei" }, + { input: "1000000 wei", value: "1000000", unit: "wei" }, + ]; + + for (const { input, value } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + const literal = (decl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:number"); + expect(literal.value).toBe(value); + // Unit is now handled at the parser level, not stored in AST + } + }); + + test("parses finney unit literals", () => { + const cases = [ + { input: "1 finney", value: "1", unit: "finney" }, + { input: "50 finney", value: "50", unit: "finney" }, + { input: "999 finney", value: "999", unit: "finney" }, + ]; + + for (const { input, value } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + const literal = (decl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:number"); + expect(literal.value).toBe(value); + // Unit is now handled at the parser level, not stored in AST + } + }); + + test("parses ether unit literals", () => { + const cases = [ + { input: "1 ether", value: "1", unit: "ether" }, + { input: "100 ether", value: "100", unit: "ether" }, + { input: "1000000 ether", value: "1000000", unit: "ether" }, + ]; + + for (const { input, value } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + const literal = (decl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:number"); + expect(literal.value).toBe(value); + // Unit is now handled at the parser level, not stored in AST + } + }); + + test("parses wei literals in expressions", () => { + const source = ` + name Test; + storage { + [0] balance: uint256; + } + code { + balance = balance + 10 ether; + if (balance >= 100 ether) { + balance = 0 wei; + } + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(2); + }); + }); + + describe("String Literals", () => { + test("parses basic string literals", () => { + const cases = [ + { input: '""', expected: "" }, + { input: '"hello"', expected: "hello" }, + { input: '"Hello, World!"', expected: "Hello, World!" }, + { input: '"123"', expected: "123" }, + { input: '"with spaces"', expected: "with spaces" }, + ]; + + for (const { input, expected } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + const literal = (decl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(literal.kind).toBe("expression:literal:string"); + expect(literal.value).toBe(expected); + } + }); + + test("parses string literals with escape sequences", () => { + const cases = [ + { input: '"\\n"', expected: "\n" }, + { input: '"\\t"', expected: "\t" }, + { input: '"\\r"', expected: "\r" }, + { input: '"\\\\"', expected: "\\" }, + { input: '"\\""', expected: '"' }, + { input: '"line1\\nline2"', expected: "line1\nline2" }, + ]; + + for (const { input, expected } of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmt = result.body?.items[0]; + const decl = (stmt as Ast.Statement.Declare).declaration; + const literal = (decl as Ast.Declaration.Variable) + .initializer as Ast.Expression.Literal; + expect(literal.value).toBe(expected); + } + }); + + test("parses strings with special characters", () => { + const cases = [ + '"@#$%^&*()"', + '"[]{}"', + '"<>?/|"', + '"1 + 2 = 3"', + '"msg.sender"', + ]; + + for (const input of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(1); + } + }); + + test("fails on invalid string literals", () => { + const cases = [ + '"unterminated', + '"unterminated\\', + '"invalid \\x escape"', + ]; + + for (const input of cases) { + const source = ` + name Test; + storage {} + code { + let x = ${input}; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + }); + + describe("Comments", () => { + test("parses single-line comments", () => { + const source = ` + name Test; // This is the name + storage {} // Empty storage + code { + // This is a comment + let x = 42; // Inline comment + // Another comment + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.name).toBe("Test"); + expect(result.body?.items).toHaveLength(1); + }); + + test("parses multi-line comments", () => { + const source = ` + name Test; /* Multi-line + comment here */ + storage { + /* Storage field */ + [0] x: uint256; + } + code { + /* This is a + multi-line + comment */ + let x = /* inline */ 42; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.storage ?? []).toHaveLength(1); + expect(result.body?.items).toHaveLength(1); + }); + + test("handles nested comment-like content", () => { + const source = ` + name Test; + storage {} + code { + let s1 = "// not a comment"; + let s2 = "/* also not a comment */"; + // Real comment with /* nested */ syntax + /* Real comment with // nested syntax */ + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(2); + }); + + test("parses comments with special characters", () => { + const source = ` + name Test; + storage {} + code { + // Comment with special chars: @#$%^&*() + /* Comment with + unicode: ñ é ü ß */ + let x = 42; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(1); + }); + + test("handles unterminated multi-line comments", () => { + const source = ` + name Test; + storage {} + code { + /* Unterminated comment + let x = 42; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + }); + + test("handles comments at end of file", () => { + const source = ` + name Test; + storage {} + code {} + // Final comment`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.name).toBe("Test"); + }); + }); + + describe("Empty Constructs", () => { + test("parses empty storage block", () => { + const source = ` + name Test; + storage {} + code { + let x = 42; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.definitions?.items ?? []).toEqual([]); + }); + + test("parses empty code block", () => { + const source = ` + name Test; + storage { + [0] x: uint256; + } + code {} + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toEqual([]); + }); + + test("parses empty structs", () => { + const source = ` + name Test; + define { + struct Empty {}; + } + storage {} + code {} + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const struct = result.definitions!.items[0]; + expect(struct.kind).toBe("declaration:struct"); + expect((struct as Ast.Declaration.Struct).fields).toEqual([]); + }); + + test("parses empty control flow blocks", () => { + const source = ` + name Test; + storage {} + code { + if (true) {} + for (let i = 0; i < 10; i = i + 1) {} + if (false) {} else {} + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(3); + }); + + test("parses minimal valid program", () => { + const source = `name X; storage{} code{}`; + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.name).toBe("X"); + }); + + test("parses program with only whitespace and newlines", () => { + const source = ` + name Test; + + + storage { + + } + + code { + + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.name).toBe("Test"); + }); + + test("parses empty statements", () => { + const source = ` + name Test; + storage {} + code { + ; + let x = 42;; + ;; + } + `; + + // Empty statements should be ignored by parser + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + }); + }); + + describe("Parser Error Scenarios", () => { + test("handles missing semicolons", () => { + const source = ` + name Test; + storage { + [0] x: uint256 + } + code {} + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + }); + + test("handles unclosed braces", () => { + const cases = [ + `name Test; storage { code {}`, + `name Test; storage {} code {`, + `name Test; storage {} code { if (true) { }`, + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + + test("handles invalid syntax", () => { + const cases = [ + `storage {} code {}`, // missing name + `name 123; storage {} code {}`, // invalid name + `code {}`, // missing name + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + + test("handles invalid type syntax", () => { + const cases = [ + `name Test; storage { [0] x: mapping<> } code {}`, + `name Test; storage { [0] x: array<> } code {}`, + `name Test; storage { [0] x: array } code {}`, + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + + test("handles invalid expressions", () => { + const cases = [ + `name Test; storage {} code { let x = ; }`, + `name Test; storage {} code { let x = 1 + ; }`, + `name Test; storage {} code { let x = + 1; }`, + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + + test("handles invalid statements", () => { + const cases = [ + `name Test; storage {} code { if () {} }`, + `name Test; storage {} code { for () {} }`, + `name Test; storage {} code { return return; }`, + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + + test("handles mismatched brackets", () => { + const source = ` + name Test; + storage { + [0] x: array { + test("parses comparison operators correctly", () => { + const source = ` + name Test; + storage {} + code { + let a = 5 > 3; + let b = 5 >= 3; + let c = 3 < 5; + let d = 3 <= 5; + let e = 5 == 5; + let f = 3 != 5; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(6); + }); + + test("parses chained comparisons", () => { + // Note: These parse but may not be semantically valid + const source = ` + name Test; + storage {} + code { + let a = 1 < 2 < 3; + let b = 5 > 4 > 3; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(2); + }); + + test("parses nested operators", () => { + const source = ` + name Test; + storage {} + code { + let a = !(true && false); + let b = -(-(-1)); + let c = !(!true); + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(3); + }); + + test("parses complex operator precedence", () => { + const source = ` + name Test; + storage {} + code { + let a = 1 + 2 * 3; + let b = (1 + 2) * 3; + let c = 1 * 2 + 3; + let d = 1 * (2 + 3); + let e = true || false && true; + let f = (true || false) && true; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(6); + }); + + test("handles operators with whitespace variations", () => { + const source = ` + name Test; + storage {} + code { + let a = 1+2; + let b = 1 + 2; + let c = 1 + 2; + let d = 1 + + + 2; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(4); + }); + + test("distinguishes between >= and > followed by =", () => { + const source = ` + name Test; + storage { + [0] balance: uint256; + } + code { + if (balance >= 100 ether) { + balance = 0; + } + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(1); + }); + + test("handles operator-like sequences in strings", () => { + const source = ` + name Test; + storage {} + code { + let s1 = ">="; + let s2 = "&&"; + let s3 = "!="; + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(3); + }); + + test("parses unary minus with numbers", () => { + const source = ` + name Test; + storage {} + code { + let a = -1; + let b = -42; + let c = -(1 + 2); + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(3); + }); + + test("parses boolean not operator", () => { + const source = ` + name Test; + storage {} + code { + let a = !true; + let b = !false; + let c = !(1 > 2); + } + `; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.body?.items).toHaveLength(3); + }); + + test("handles invalid operator usage", () => { + const cases = [ + `name Test; storage {} code { let x = 1 ! 2; }`, + `name Test; storage {} code { let x = && true; }`, + `name Test; storage {} code { let x = 1 ||; }`, + ]; + + for (const source of cases) { + const parseResult = parse(source); + expect(parseResult.success).toBe(false); + } + }); + }); +}); diff --git a/packages/bugc/src/parser/errors.ts b/packages/bugc/src/parser/errors.ts new file mode 100644 index 00000000..6a1e397c --- /dev/null +++ b/packages/bugc/src/parser/errors.ts @@ -0,0 +1,20 @@ +/** + * Parser-specific errors and error codes + */ + +import { BugError } from "#errors"; +import type { SourceLocation } from "#ast"; + +/** + * Parse errors + */ +class ParserError extends BugError { + public readonly expected?: string[]; + + constructor(message: string, location: SourceLocation, expected?: string[]) { + super(message, "PARSE_ERROR", location); + this.expected = expected; + } +} + +export { ParserError as Error }; diff --git a/packages/bugc/src/parser/function.test.ts b/packages/bugc/src/parser/function.test.ts new file mode 100644 index 00000000..9d46b609 --- /dev/null +++ b/packages/bugc/src/parser/function.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect } from "vitest"; +import type * as Ast from "#ast"; +import { Severity } from "#result"; +import { parse } from "./parser.js"; +import "#test/matchers"; + +describe("Function declarations", () => { + it("parses function with parameters and return type", () => { + if (!parse) { + throw new Error("parse function is not imported"); + } + const input = ` + name FunctionTest; + + define { + function add(a: uint256, b: uint256) -> uint256 { + return a + b; + }; + } + + code {} + `; + + const result = parse(input); + expect(result).toBeDefined(); + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Parse failed"); + } + + const program = result.value; + expect(program.definitions?.items).toHaveLength(1); + + const funcDecl = program.definitions!.items[0]; + expect(funcDecl.kind).toBe("declaration:function"); + expect(funcDecl.name).toBe("add"); + const func = funcDecl as Ast.Declaration.Function; + expect(func.parameters).toHaveLength(2); + expect(func.parameters[0].name).toBe("a"); + expect(func.parameters[1].name).toBe("b"); + expect(func.returnType?.kind).toBe("type:elementary:uint"); + }); + + it("parses void function without return type", () => { + const input = ` + name VoidFunction; + + define { + function doSomething(x: uint256) { + let y = x + 1; + }; + } + + code {} + `; + + const result = parse(input); + expect(result).toBeDefined(); + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Parse failed"); + } + + const program = result.value; + const funcDecl = program.definitions!.items[0]; + expect(funcDecl.kind).toBe("declaration:function"); + expect(funcDecl.name).toBe("doSomething"); + const func = funcDecl as Ast.Declaration.Function; + expect(func.returnType).toBeUndefined(); + }); + + it("parses function calls", () => { + const input = ` + name CallTest; + + define { + function multiply(x: uint256, y: uint256) -> uint256 { + return x * y; + }; + } + + code { + let result = multiply(10, 20); + } + `; + + const result = parse(input); + expect(result).toBeDefined(); + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Parse failed"); + } + + const program = result.value; + const codeBlock = program.body; + const letStmt = codeBlock?.items[0]; + + expect(letStmt?.kind).toBe("statement:declare"); + if (letStmt?.kind === "statement:declare") { + const decl = (letStmt as Ast.Statement.Declare) + .declaration as Ast.Declaration.Variable; + const init = decl.initializer; + expect(init?.kind).toBe("expression:call"); + if (init?.kind === "expression:call") { + const callExpr = init as Ast.Expression.Call; + expect(callExpr.callee.kind).toBe("expression:identifier"); + if (callExpr.callee.kind === "expression:identifier") { + expect((callExpr.callee as Ast.Expression.Identifier).name).toBe( + "multiply", + ); + } + expect(callExpr.arguments).toHaveLength(2); + } + } + }); + + it("rejects function as identifier", () => { + const input = ` + name BadIdentifier; + code { + let function = 5; + } + `; + + const result = parse(input); + expect(result).toBeDefined(); + expect(result.success).toBe(false); + if (!result.success) { + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "function", + }); + } + }); +}); diff --git a/packages/bugc/src/parser/index.ts b/packages/bugc/src/parser/index.ts new file mode 100644 index 00000000..e40e6d33 --- /dev/null +++ b/packages/bugc/src/parser/index.ts @@ -0,0 +1,8 @@ +/** + * Main parser module for BUG language + * + * Exports the parser implementation + */ + +export { parse } from "./parser.js"; +export { Error } from "./errors.js"; diff --git a/packages/bugc/src/parser/optional-storage.test.ts b/packages/bugc/src/parser/optional-storage.test.ts new file mode 100644 index 00000000..00cb0678 --- /dev/null +++ b/packages/bugc/src/parser/optional-storage.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from "vitest"; + +import { parse } from "./parser.js"; + +describe("Optional Storage Block", () => { + it("should parse program without storage block", () => { + const input = ` + name NoStorage; + + code { + let x = 42; + let y = x + 1; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.name).toBe("NoStorage"); + expect(result.value.storage ?? []).toHaveLength(0); + expect(result.value.body?.items).toHaveLength(2); + }); + + it("should parse program with define but no storage", () => { + const input = ` + name DefineNoStorage; + + define { + struct Point { + x: uint256; + y: uint256; + }; + } + + code { + let p = 100; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.storage ?? []).toHaveLength(0); + expect(result.value.definitions!.items[0].kind).toBe("declaration:struct"); + expect(result.value.definitions!.items[0].name).toBe("Point"); + }); + + it("should still parse program with storage block", () => { + const input = ` + name WithStorage; + + storage { + [0] count: uint256; + } + + code { + count = count + 1; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.storage ?? []).toHaveLength(1); + expect(result.value.storage![0].kind).toBe("declaration:storage"); + }); + + it("should parse empty storage block", () => { + const input = ` + name EmptyStorage; + + storage { + } + + code { + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.storage ?? []).toHaveLength(0); + }); + + it("should parse minimal program without storage", () => { + const input = ` + name Minimal; + code {} + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.name).toBe("Minimal"); + expect(result.value.storage ?? []).toHaveLength(0); + expect(result.value.body?.items).toHaveLength(0); + }); + + it("should parse program with all optional blocks", () => { + const input = ` + name AllOptional; + + define { + struct User { + id: uint256; + }; + } + + storage { + [0] owner: User; + } + + code { + owner.id = 1; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.storage ?? []).toHaveLength(1); + expect(result.value.definitions!.items).toHaveLength(1); + expect(result.value.definitions!.items[0].kind).toBe("declaration:struct"); + }); + + it("should handle whitespace and comments correctly", () => { + const input = ` + // Program without storage + name NoStorageWithComments; + + /* No storage block needed + for this simple program */ + + code { + // Just some calculations + let result = 1 + 2 + 3; + } + `; + + const result = parse(input); + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + expect(result.value.storage ?? []).toHaveLength(0); + }); +}); diff --git a/packages/bugc/src/parser/parser-integration.test.ts b/packages/bugc/src/parser/parser-integration.test.ts new file mode 100644 index 00000000..ecc2cfe6 --- /dev/null +++ b/packages/bugc/src/parser/parser-integration.test.ts @@ -0,0 +1,275 @@ +import { describe, it, expect } from "vitest"; + +import type * as Ast from "#ast"; + +import { parse } from "./parser.js"; + +describe("Parser Integration Tests", () => { + describe("Complete Example Programs", () => { + it("should parse counter.bug example", () => { + const source = ` +name Counter; + +storage { + [0] count: uint256; + [1] owner: address; +} + +code { + if (msg.sender != owner) { + return; + } + count = count + 1; +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.kind).toBe("program"); + expect(ast.name).toBe("Counter"); + expect(ast.storage).toHaveLength(2); + expect(ast.body?.items).toHaveLength(2); + + const ifStmt = ast.body?.items[0] as Ast.Statement.ControlFlow; + expect(ifStmt.kind).toBe("statement:control-flow:if"); + }); + + it("should parse simple-storage.bug example", () => { + const source = ` +name SimpleStorage; + +define { + struct User { + id: uint256; + balance: uint256; + active: bool; + }; +} + +storage { + [0] users: mapping; + [1] totalUsers: uint256; + [2] admin: address; +} + +code { + let sender = msg.sender; + + if (sender == admin) { + let user = users[sender]; + user.balance = user.balance + msg.value; + totalUsers = totalUsers + 1; + } +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.kind).toBe("program"); + expect(ast.name).toBe("SimpleStorage"); + expect(ast.storage ?? []).toHaveLength(3); // 3 storage + expect(ast.definitions?.items ?? []).toHaveLength(1); // 1 struct + + const struct = ast.definitions?.items[0]; + expect(struct?.kind).toBe("declaration:struct"); + expect(struct?.name).toBe("User"); + expect((struct as Ast.Declaration.Struct).fields).toHaveLength(3); + }); + + it("should parse auction.bug example", () => { + const source = ` +name Auction; + +define { + struct Bid { + bidder: address; + amount: uint256; + timestamp: uint256; + }; +} + +storage { + [0] highestBid: Bid; + [1] beneficiary: address; + [2] endTime: uint256; + [3] ended: bool; +} + +code { + if (ended) { + return 0; + } + + let bid = highestBid; + if (msg.value > bid.amount) { + highestBid.bidder = msg.sender; + highestBid.amount = msg.value; + return 1; + } + + return 0; +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.kind).toBe("program"); + expect(ast.storage ?? []).toHaveLength(4); // 4 storage + expect(ast.definitions?.items ?? []).toHaveLength(1); // 1 struct + expect(ast.body?.items).toHaveLength(4); // if, let, if, return + }); + }); + + describe("Complex Expression Parsing", () => { + it("should parse nested mappings", () => { + const source = ` +name NestedMappings; + +storage { + [0] balances: mapping>; +} + +code { + let addr = 0x1234567890123456789012345678901234567890; + let tokenId = 42; + let balance = balances[addr][tokenId]; + balances[addr][tokenId] = balance + 1; +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.storage ?? []).toHaveLength(1); + + const balances = ast.storage![0] as Ast.Declaration.Storage; + expect(balances.type.kind.startsWith("type:complex:")).toBe(true); + }); + + it("should parse complex arithmetic expressions", () => { + const source = ` +name ComplexMath; + +storage { + [0] result: uint256; +} + +code { + let a = 10; + let b = 20; + let c = 30; + + result = a + b * c - (a + b) / c; + result = ((a + b) * c + a * (b + c)) / (a + b + c); +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.body?.items).toHaveLength(5); + }); + }); + + it.skip("should parse with error recovery enabled", () => { + const source = ` +name ErrorRecovery; + +storage { + [0] x: uint256 // Missing semicolon + [1] y: uint256; +} + +code { + let a = 10 + let b = 20; // Should recover from previous error + x = a + b; +} +`; + + const result = parse(source); + expect(result.success).toBe(false); + // Would check for multiple errors if error recovery was implemented + }); + + describe("Edge Cases", () => { + it("should parse empty program", () => { + const source = ` +name Empty; +storage {} +code {} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.name).toBe("Empty"); + expect(ast.storage ?? []).toEqual([]); + expect(ast.body?.items).toEqual([]); + }); + + it("should parse program with only storage", () => { + const source = ` +name StorageOnly; + +storage { + [0] x: uint256; + [1] y: address; +} + +code {} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + expect(ast.storage ?? []).toHaveLength(2); + expect(ast.body?.items).toEqual([]); + }); + + it("should parse program with complex control flow", () => { + const source = ` +name ControlFlow; + +storage { + [0] counter: uint256; +} + +code { + for (let i = 0; i < 10; i = i + 1) { + if (i > 5) { + break; + } + counter = counter + i; + } + + if (counter > 15) { + return 1; + } else { + return 0; + } +} +`; + + const parseResult = parse(source); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const ast = parseResult.value; + const forLoop = ast.body?.items[0] as Ast.Statement.ControlFlow; + expect(forLoop.kind).toBe("statement:control-flow:for"); + expect( + (forLoop as Ast.Statement.ControlFlow.For).body.items, + ).toHaveLength(2); + }); + }); +}); diff --git a/packages/bugc/src/parser/parser.error.test.ts b/packages/bugc/src/parser/parser.error.test.ts new file mode 100644 index 00000000..97346846 --- /dev/null +++ b/packages/bugc/src/parser/parser.error.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from "vitest"; +import { Result } from "#result"; +import "#test/matchers"; +import { parse } from "./parser.js"; + +describe("Parser error handling", () => { + it("should return ParseError for invalid syntax", () => { + // Test various parse failures + const invalidPrograms = [ + { + source: `name Test + code { return; }`, // Missing semicolon after name + expectedError: "Expected", + }, + { + source: `name Test; + storage { + [0] x: InvalidType; + } + code { return; }`, // Invalid type (will be treated as struct reference) + expectedError: "compilation", + }, + { + source: `name Test; + code { + let x = 1 ** 2; // Invalid operator + return; + }`, + expectedError: "Expected", + }, + ]; + + for (const { source } of invalidPrograms) { + const result = parse(source); + + if (result.success) { + // Some invalid programs might parse but fail type checking + // That's OK - we're testing parser error handling, not type checking + continue; + } + + const error = Result.firstError(result); + // Should be a ParseError with proper structure + expect(error).toBeDefined(); + expect(error?.code).toBe("PARSE_ERROR"); + expect(error?.message).toBeDefined(); + expect(error?.message).toContain("Parse error"); + // Location might not always be present for EOF errors + if (error?.location) { + expect(error.location.offset).toBeGreaterThanOrEqual(0); + expect(error.location.length).toBeGreaterThanOrEqual(0); + } + } + }); + + it("should parse all valid elementary types", () => { + const validProgram = ` + name Test; + storage { + [0] a: uint256; + [1] b: uint128; + [2] c: uint64; + [3] d: uint32; + [4] e: uint16; + [5] f: uint8; + [6] g: int256; + [7] h: int128; + [8] i: int64; + [9] j: int32; + [10] k: int16; + [11] l: int8; + [12] m: address; + [13] n: bool; + [14] o: bytes32; + [15] p: bytes16; + [16] q: bytes8; + [17] r: bytes4; + [18] s: bytes; + [19] t: string; + } + code { return; } + `; + + const result = parse(validProgram); + expect(result.success).toBe(true); + }); + + it("should handle parser internal errors appropriately", () => { + // The elementaryType parser has a defensive throw for unknown types, + // but this is unreachable in normal operation since Lang.elementaryType + // only matches valid type keywords. The throw is there as a safety net + // for parser bugs, not for user errors. + + // Test that normal parsing doesn't trigger internal errors + const programs = [ + `name Test; storage { [0] x: uint512; } code { return; }`, // Invalid size + `name Test; storage { [0] x: uint; } code { return; }`, // Missing size + `name Test; storage { [0] x: unsigned; } code { return; }`, // Wrong keyword + ]; + + for (const source of programs) { + const result = parse(source); + // These should all parse (treated as struct references) but may fail type checking + // The important thing is they don't throw internal errors + expect(() => result).not.toThrow(); + } + }); +}); diff --git a/packages/bugc/src/parser/parser.overflow.test.ts b/packages/bugc/src/parser/parser.overflow.test.ts new file mode 100644 index 00000000..59ee8688 --- /dev/null +++ b/packages/bugc/src/parser/parser.overflow.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, it } from "vitest"; +import { parse } from "./parser.js"; +import type * as Ast from "#ast"; +import { Result, Severity } from "#result"; +import "#test/matchers"; + +describe("Parser overflow validation", () => { + describe("Array size validation", () => { + it("should accept array size at MAX_SAFE_INTEGER", () => { + const source = `name Test; +storage { + [0] arr: array; +} +code { + arr[0] = 1; +}`; + const result = parse(source); + expect(result.success).toBe(true); + }); + + it("should reject array size exceeding MAX_SAFE_INTEGER", () => { + const source = `name Test; +storage { + [0] arr: array; +} +code { + arr[0] = 1; +}`; + const result = parse(source); + expect(result.success).toBe(false); + if (!result.success) { + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "exceeds maximum safe integer", + }); + } + }); + + it("should reject negative array size", () => { + const source = `name Test; +storage { + [0] arr: array; +} +code { + arr[0] = 1; +}`; + const result = parse(source); + expect(result.success).toBe(false); + // Negative numbers won't match the numberString parser, so we get a different error + if (!result.success) { + expect(Result.hasErrors(result)).toBe(true); + } + }); + + it("should reject zero array size", () => { + const source = `name Test; +storage { + [0] arr: array; +} +code { + arr[0] = 1; +}`; + const result = parse(source); + expect(result.success).toBe(false); + if (!result.success) { + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Array size must be positive", + }); + } + }); + }); + + describe("Storage slot validation", () => { + it("should accept storage slot at MAX_SAFE_INTEGER", () => { + const source = `name Test; +storage { + [9007199254740991] a: uint256; +} +code { + a = 1; +}`; + const result = parse(source); + expect(result.success).toBe(true); + }); + + it("should reject storage slot exceeding MAX_SAFE_INTEGER", () => { + const source = `name Test; +storage { + [9007199254740992] a: uint256; +} +code { + a = 1; +}`; + const result = parse(source); + expect(result.success).toBe(false); + if (!result.success) { + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "exceeds maximum safe integer", + }); + } + }); + + it("should reject negative storage slot", () => { + const source = `name Test; +storage { + [-1] a: uint256; +} +code { + a = 1; +}`; + const result = parse(source); + expect(result.success).toBe(false); + // Negative numbers won't match the numberString parser + if (!result.success) { + expect(Result.hasErrors(result)).toBe(true); + } + }); + }); + + describe("Type validation", () => { + it("should accept valid uint types", () => { + const validTypes = [ + "uint8", + "uint16", + "uint32", + "uint64", + "uint128", + "uint256", + ]; + for (const type of validTypes) { + const source = `name Test; +storage { + [0] a: ${type}; +} +code { + a = 1; +}`; + const result = parse(source); + expect(result.success).toBe(true); + } + }); + + it("should treat invalid uint types as reference types", () => { + // Invalid uint types like uint512 are parsed as reference types + // The type checker will catch them as undefined structs + const source = `name Test; +storage { + [0] a: uint512; +} +code { + a = 1; +}`; + const result = parse(source); + expect(result.success).toBe(true); // Parser succeeds + + // Check that it's parsed as a reference type + if (result.success) { + const storage = result.value.storage?.find( + (d) => d.kind === "declaration:storage", + ) as Ast.Declaration.Storage; + if (storage) { + expect(storage.type.kind).toBe("type:reference"); + expect((storage.type as Ast.Type.Reference).name).toBe("uint512"); + } + } + }); + }); +}); diff --git a/packages/bugc/src/parser/parser.test.ts b/packages/bugc/src/parser/parser.test.ts new file mode 100644 index 00000000..82f77421 --- /dev/null +++ b/packages/bugc/src/parser/parser.test.ts @@ -0,0 +1,665 @@ +import { describe, it, expect } from "vitest"; +import "#test/matchers"; +import * as Ast from "#ast"; +import { Severity } from "#result"; +import { parse } from "./parser.js"; + +describe("Normalized Parser", () => { + describe("Basic Parsing", () => { + it("should parse minimal program", () => { + const input = ` + name Test; + storage {} + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.kind).toBe("program"); + expect(result.name).toBe("Test"); + expect(result.storage ?? []).toEqual([]); + expect(result.body?.kind).toBe("block:statements"); + expect(result.body?.items).toEqual([]); + }); + + it("should include source locations", () => { + const input = `name Test; +storage {} +code {}`; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.loc).not.toBeNull(); + expect(result.loc?.offset).toBe(0); + expect(result.loc?.length).toBe(input.length); + }); + + it("should parse program without storage block", () => { + const input = ` + name NoStorage; + code { + let x = 10; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + expect(result.kind).toBe("program"); + expect(result.name).toBe("NoStorage"); + expect(result.storage ?? []).toEqual([]); + expect(result.body?.items).toHaveLength(1); + }); + }); + + describe("Type Parsing", () => { + it("should parse primitive types", () => { + const input = ` + name Test; + storage { + [0] balance: uint256; + [1] owner: address; + [2] flag: bool; + } + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [balance, owner, flag] = + result.storage as Ast.Declaration.Storage[]; + + expect(balance.type.kind).toBe("type:elementary:uint"); + const balanceType = balance.type as Ast.Type.Elementary.Uint; + expect(balanceType.kind).toBe("type:elementary:uint"); + expect(balanceType.bits).toBe(256); + + const ownerType = owner.type as Ast.Type.Elementary.Address; + expect(ownerType.kind).toBe("type:elementary:address"); + + const flagType = flag.type as Ast.Type.Elementary.Bool; + expect(flagType.kind).toBe("type:elementary:bool"); + }); + + it("should parse array types", () => { + const input = ` + name Test; + storage { + [0] nums: array; + [1] fixed: array; + } + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [nums, fixed] = result.storage as Ast.Declaration.Storage[]; + + expect(nums.type.kind).toBe("type:complex:array"); + const numsType = nums.type as Ast.Type.Complex.Array; + expect(numsType.kind).toBe("type:complex:array"); + expect(numsType.size).toBeUndefined(); + expect(numsType.element).toBeDefined(); + + const fixedType = fixed.type as Ast.Type.Complex.Array; + expect(fixedType.kind).toBe("type:complex:array"); + expect(fixedType.size).toBe(10); + }); + + it("should parse mapping types", () => { + const input = ` + name Test; + storage { + [0] balances: mapping; + } + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const mapping = result.storage![0] as Ast.Declaration.Storage; + + const mapType = mapping.type as Ast.Type.Complex.Mapping; + expect(mapType.kind).toBe("type:complex:mapping"); + expect(mapType.key).toBeDefined(); + expect(mapType.value).toBeDefined(); + + const keyType = mapType.key as Ast.Type.Elementary.Address; + expect(keyType.kind).toBe("type:elementary:address"); + + const valueType = mapType.value as Ast.Type.Elementary.Uint; + expect(valueType.kind).toBe("type:elementary:uint"); + expect(valueType.bits).toBe(256); + }); + + it("should parse reference types", () => { + const input = ` + name Test; + define { + struct Point { x: uint256; y: uint256; }; + } + storage { + [0] position: Point; + } + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const position = result.storage?.find( + (d: Ast.Declaration.Storage) => d.name === "position", + ) as Ast.Declaration.Storage; + + expect(position?.type.kind).toBe("type:reference"); + expect((position?.type as Ast.Type.Reference).name).toBe("Point"); + }); + }); + + describe("Declaration Parsing", () => { + it("should parse struct declarations", () => { + const input = ` + name Test; + define { + struct Point { + x: uint256; + y: uint256; + }; + } + storage {} + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const struct = result.definitions?.items[0]; + + expect(struct?.kind).toBe("declaration:struct"); + expect(struct?.name).toBe("Point"); + const structDecl = struct as Ast.Declaration.Struct; + expect(structDecl.fields).toHaveLength(2); + + const [x, y] = structDecl.fields; + expect(x.kind).toBe("declaration:field"); + expect(x.name).toBe("x"); + expect(y.name).toBe("y"); + }); + + it("should parse storage declarations", () => { + const input = ` + name Test; + storage { + [0] balance: uint256; + [42] data: bytes32; + } + code {} + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [balance, data] = result.storage as Ast.Declaration.Storage[]; + + expect(balance.kind).toBe("declaration:storage"); + expect(balance.slot).toBe(0); + expect(data.slot).toBe(42); + }); + + it("should parse variable declarations", () => { + const input = ` + name Test; + storage {} + code { + let x = 42; + let flag = true; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [letX, letFlag] = result.body?.items as Ast.Statement.Declare[]; + + expect(letX.kind).toBe("statement:declare"); + expect(letX.declaration.kind).toBe("declaration:variable"); + expect(letX.declaration.name).toBe("x"); + + const xDecl = letX.declaration as Ast.Declaration.Variable; + const xInit = xDecl.initializer as Ast.Expression.Literal; + expect(xInit.kind).toBe("expression:literal:number"); + expect(xInit.value).toBe("42"); + + const flagDecl = letFlag.declaration as Ast.Declaration.Variable; + const flagInit = flagDecl.initializer as Ast.Expression.Literal; + expect(flagInit.kind).toBe("expression:literal:boolean"); + expect(flagInit.value).toBe("true"); + }); + }); + + describe("Expression Parsing", () => { + it("should parse literal expressions", () => { + const input = ` + name Test; + storage {} + code { + 42; + 0x1234; + "hello"; + true; + false; + 0x1234567890123456789012345678901234567890; + 100 ether; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + const exprs = stmts.map((s) => s.expression as Ast.Expression.Literal); + + expect(exprs[0].kind).toBe("expression:literal:number"); + expect(exprs[0].value).toBe("42"); + + expect(exprs[1].kind).toBe("expression:literal:hex"); + expect(exprs[1].value).toBe("0x1234"); + + expect(exprs[2].kind).toBe("expression:literal:string"); + expect(exprs[2].value).toBe("hello"); + + expect(exprs[3].kind).toBe("expression:literal:boolean"); + expect(exprs[3].value).toBe("true"); + + expect(exprs[4].kind).toBe("expression:literal:boolean"); + expect(exprs[4].value).toBe("false"); + + expect(exprs[5].kind).toBe("expression:literal:address"); + expect(exprs[5].value).toBe("0x1234567890123456789012345678901234567890"); + + expect(exprs[6].kind).toBe("expression:literal:number"); + expect(exprs[6].value).toBe("100"); + expect((exprs[6] as Ast.Expression.Literal.Number).unit).toBe("ether"); + }); + + it("should parse identifier expressions", () => { + const input = ` + name Test; + storage {} + code { + x; + balance; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + const [x, balance] = stmts.map( + (s) => s.expression as Ast.Expression.Identifier, + ); + + expect(x.kind).toBe("expression:identifier"); + expect(x.name).toBe("x"); + expect(balance.name).toBe("balance"); + }); + + it("should parse operator expressions", () => { + const input = ` + name Test; + storage {} + code { + x + y; + a * b; + !flag; + -value; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + const exprs = stmts.map((s) => s.expression as Ast.Expression.Operator); + + expect(exprs[0].operator).toBe("+"); + expect(exprs[0].operands).toHaveLength(2); + + expect(exprs[1].operator).toBe("*"); + + expect(exprs[2].operator).toBe("!"); + expect(exprs[2].operands).toHaveLength(1); + + expect(exprs[3].operator).toBe("-"); + expect(exprs[3].operands).toHaveLength(1); + }); + + it("should parse access expressions", () => { + const input = ` + name Test; + storage {} + code { + point.x; + arr[0]; + nested.field.subfield; + matrix[i][j]; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + const exprs = stmts.map((s) => s.expression as Ast.Expression.Access); + + if (!Ast.Expression.Access.isMember(exprs[0])) { + throw new Error("Expected member access"); + } + expect(exprs[0].property).toBe("x"); + + if (!Ast.Expression.Access.isIndex(exprs[1])) { + throw new Error("Expected index access"); + } + if (!Ast.Expression.isLiteral(exprs[1].index)) { + throw new Error("Expected literal in index"); + } + expect(exprs[1].index.value).toBe("0"); + + // nested.field.subfield is two member accesses + const nested = exprs[2]; + if (!Ast.Expression.Access.isMember(nested)) { + throw new Error("Expected member access"); + } + expect(nested.property).toBe("subfield"); + + const nestedObj = nested.object; + if ( + !Ast.Expression.isAccess(nestedObj) || + !Ast.Expression.Access.isMember(nestedObj) + ) { + throw new Error("expected member access"); + } + expect(nestedObj.kind).toBe("expression:access:member"); + expect(nestedObj.property).toBe("field"); + + // matrix[i][j] is two index accesses + const matrix = exprs[3]; + expect(matrix.kind).toBe("expression:access:index"); + }); + + it("should parse special expressions", () => { + const input = ` + name Test; + storage {} + code { + msg.sender; + msg.value; + msg.data; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + const [sender, value, data] = stmts.map( + (s) => s.expression as Ast.Expression.Special, + ); + + expect(sender.kind).toBe("expression:special:msg.sender"); + + expect(value.kind).toBe("expression:special:msg.value"); + + expect(data.kind).toBe("expression:special:msg.data"); + }); + + it("should parse complex expressions with correct precedence", () => { + const input = ` + name Test; + storage {} + code { + a + b * c; + x == y && z != w; + !flag || value > 0; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Express[]; + + // a + b * c should be a + (b * c) + const expr1 = stmts[0].expression as Ast.Expression.Operator; + expect(expr1.operator).toBe("+"); + const right1 = expr1.operands[1] as Ast.Expression.Operator; + expect(right1.operator).toBe("*"); + + // x == y && z != w should be (x == y) && (z != w) + const expr2 = stmts[1].expression as Ast.Expression.Operator; + expect(expr2.operator).toBe("&&"); + const left2 = expr2.operands[0] as Ast.Expression.Operator; + expect(left2.operator).toBe("=="); + const right2 = expr2.operands[1] as Ast.Expression.Operator; + expect(right2.operator).toBe("!="); + }); + }); + + describe("Statement Parsing", () => { + it("should parse assignment statements", () => { + const input = ` + name Test; + storage {} + code { + x = 42; + point.x = 100; + arr[0] = value; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.Assign[]; + + expect(stmts[0].kind).toBe("statement:assign"); + expect((stmts[0].target as Ast.Expression.Identifier).name).toBe("x"); + expect((stmts[0].value as Ast.Expression.Literal).value).toBe("42"); + + expect((stmts[1].target as Ast.Expression.Access).kind).toBe( + "expression:access:member", + ); + expect((stmts[2].target as Ast.Expression.Access).kind).toBe( + "expression:access:index", + ); + }); + + it("should parse control flow statements", () => { + const input = ` + name Test; + storage {} + code { + if (x > 0) { + return x; + } + + if (flag) { + break; + } else { + return 0; + } + + for (let i = 0; i < 10; i = i + 1) { + x = x + i; + } + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const [if1, if2, forLoop] = result.body + ?.items as Ast.Statement.ControlFlow[]; + + expect(if1.kind).toBe("statement:control-flow:if"); + const if1Cast = if1 as Ast.Statement.ControlFlow.If; + expect(if1Cast.condition).toBeDefined(); + expect(if1Cast.body.items).toHaveLength(1); + expect(if1Cast.alternate).toBeUndefined(); + + expect(if2.kind).toBe("statement:control-flow:if"); + const if2Cast = if2 as Ast.Statement.ControlFlow.If; + expect(if2Cast.body.items[0].kind).toBe("statement:control-flow:break"); + expect(if2Cast.alternate).toBeDefined(); + + expect(forLoop.kind).toBe("statement:control-flow:for"); + const forLoopCast = forLoop as Ast.Statement.ControlFlow.For; + expect(forLoopCast.init?.kind).toBe("statement:declare"); + expect(forLoopCast.condition).toBeDefined(); + expect(forLoopCast.update?.kind).toBe("statement:assign"); + expect(forLoopCast.body.items).toHaveLength(1); + }); + + it("should parse return statements", () => { + const input = ` + name Test; + storage {} + code { + return; + return 42; + return x + y; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + const stmts = result.body?.items as Ast.Statement.ControlFlow[]; + + expect(stmts[0].kind).toBe("statement:control-flow:return"); + expect( + (stmts[0] as Ast.Statement.ControlFlow.Return).value, + ).toBeUndefined(); + + expect(stmts[1].kind).toBe("statement:control-flow:return"); + expect( + ( + (stmts[1] as Ast.Statement.ControlFlow.Return) + .value as Ast.Expression.Literal + ).value, + ).toBe("42"); + + expect(stmts[2].kind).toBe("statement:control-flow:return"); + expect( + ( + (stmts[2] as Ast.Statement.ControlFlow.Return) + .value as Ast.Expression.Operator + ).operator, + ).toBe("+"); + }); + }); + + describe("Complex Programs", () => { + it("should parse complete program", () => { + const input = ` + name SimpleStorage; + + define { + struct User { + username: string; + balance: uint256; + }; + } + + storage { + [0] owner: address; + [1] users: mapping; + [2] totalSupply: uint256; + } + + code { + let sender = msg.sender; + + if (sender == owner) { + users[sender].balance = users[sender].balance + msg.value; + totalSupply = totalSupply + msg.value; + } else { + return 0; + } + + return users[sender].balance; + } + `; + + const parseResult = parse(input); + expect(parseResult.success).toBe(true); + if (!parseResult.success) throw new Error("Parse failed"); + const result = parseResult.value; + + expect(result.name).toBe("SimpleStorage"); + expect(result.storage ?? []).toHaveLength(3); // 3 storage + expect(result.definitions?.items ?? []).toHaveLength(1); // 1 struct + + const struct = result.definitions?.items[0]; + expect(struct?.kind).toBe("declaration:struct"); + expect((struct as Ast.Declaration.Struct).fields).toHaveLength(2); + + const codeStmts = result.body?.items; + expect(codeStmts).toHaveLength(3); // let, if, return + + const ifStmt = codeStmts?.[1] as Ast.Statement.ControlFlow.If; + expect(ifStmt.body.items).toHaveLength(2); // two assignments + expect(ifStmt.alternate?.items).toHaveLength(1); // one return + }); + }); + + describe("Error Handling", () => { + it("should handle parse errors gracefully", () => { + const result = parse("invalid syntax"); + expect(result.success).toBe(false); + }); + + it("should provide helpful error messages", () => { + const result = parse(` + name Test; + storage { + 0: x uint256; + } + code {} + `); + expect(result.success).toBe(false); + if (!result.success) { + // Error occurs at column 11 where parser expects "[" for storage slot syntax + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Parse error at line 4, column 11", + }); + } + }); + }); +}); diff --git a/packages/bugc/src/parser/parser.ts b/packages/bugc/src/parser/parser.ts new file mode 100644 index 00000000..005ee909 --- /dev/null +++ b/packages/bugc/src/parser/parser.ts @@ -0,0 +1,1059 @@ +/** + * BUG parser implementation that generates AST + */ + +import P from "parsimmon"; +import * as Ast from "#ast"; +import { Result } from "#result"; +import { Error as ParseError } from "./errors.js"; + +/** + * Placeholder ID used for nodes before location-based IDs are assigned + */ +const PENDING_ID = "" as Ast.Id; + +/** + * Parse a BUG program and return Result + */ +export function parse(input: string): Result { + return runParser(parser, input); +} + +/** + * Parser utilities and base definitions + */ + +/** + * Convert Parsimmon's Mark to our SourceLocation format + */ +function toSourceLocation(mark: P.Mark): Ast.SourceLocation { + return { + offset: mark.start.offset, + length: mark.end.offset - mark.start.offset, + }; +} + +/** + * Basic parsers + */ + +// Comments +const comment = P.alt( + P.regexp(/\/\/[^\n]*/), // Single line + P.regexp(/\/\*[^]*?\*\//), // Multi line +); + +// Whitespace and comments +const whitespaceOrComment = P.alt(P.whitespace, comment); + +// Whitespace (including comments) +const _ = whitespaceOrComment.many().result(undefined); +const __ = whitespaceOrComment.atLeast(1).result(undefined); + +// Lexeme: consume trailing whitespace/comments +const lexeme = (p: P.Parser): P.Parser => p.skip(_); + +// Keywords - must not be followed by identifier chars +const keyword = (name: string): P.Parser => + lexeme( + P.string(name) + .notFollowedBy(P.regexp(/[a-zA-Z0-9_]/)) + .desc(`keyword '${name}'`), + ); + +// Operators +const operator = (op: string): P.Parser => + P.string(op).desc(`operator '${op}'`); + +// Tokens with automatic whitespace consumption +const token = (s: string): P.Parser => lexeme(P.string(s)); + +// Language tokens +const Lang = { + // Whitespace + _, + __, + + // Comments + comment, + + // Lexeme + lexeme, + + // Keywords + keyword, + + // Operators + operator, + + // Tokens + token, + + // Parentheses, brackets, braces + lparen: token("("), + rparen: token(")"), + lbracket: token("["), + rbracket: token("]"), + lbrace: token("{"), + rbrace: token("}"), + semicolon: token(";"), + comma: token(","), + colon: token(":"), + dot: token("."), + + // Assignment + equals: token("="), + arrow: token("->"), + + // Binary operators + plus: token("+"), + minus: token("-"), + multiply: token("*"), + divide: token("/"), + lt: token("<"), + gt: token(">"), + lte: token("<="), + gte: token(">="), + eq: token("=="), + neq: token("!="), + and: token("&&"), + or: token("||"), + + // Unary operators + not: token("!"), + + // Type cast operator + as: token("as"), + + // Identifiers (must not be keywords) + identifier: lexeme( + P.regexp(/[a-zA-Z_][a-zA-Z0-9_]*/).chain((name: string) => { + // Check if it's a reserved keyword + const keywords = [ + "let", + "if", + "else", + "for", + "while", + "return", + "break", + "continue", + "struct", + "mapping", + "array", + "function", + "storage", + "code", + "create", + "define", + "msg", + "true", + "false", + "wei", + "finney", + "ether", + "as", + "uint256", + "uint128", + "uint64", + "uint32", + "uint16", + "uint8", + "int256", + "int128", + "int64", + "int32", + "int16", + "int8", + "address", + "bool", + "bytes32", + "bytes16", + "bytes8", + "bytes4", + "bytes", + "string", + ]; + + if (keywords.includes(name)) { + return P.fail(`Cannot use keyword '${name}' as identifier`); + } + return P.succeed(name); + }), + ), + + // Number literals + number: lexeme( + P.regexp(/0x[0-9a-fA-F]+|[0-9]+/) + .desc("number") + .map((str) => { + if (str.startsWith("0x")) { + return BigInt(str); + } + return BigInt(str); + }), + ), + + // String literals + string: lexeme( + P.regexp(/"([^"\\\n\r]|\\[ntr"\\])*"/) + .desc("string literal") + .map((str) => { + // Remove quotes and process escape sequences + const content = str.slice(1, -1); + return content + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\r/g, "\r") + .replace(/\\\\/g, "\\") + .replace(/\\"/g, '"'); + }), + ), + + // Boolean literals + boolean: lexeme( + P.alt(P.string("true").result(true), P.string("false").result(false)).desc( + "boolean literal", + ), + ), + + // Address literal (0x followed by exactly 40 hex chars, not followed by more hex chars) + address: lexeme( + P.regexp(/0x[0-9a-fA-F]{40}/) + .notFollowedBy(P.regexp(/[0-9a-fA-F]/)) + .desc("address literal"), + ), + + // Wei units + weiUnit: P.alt(keyword("ether"), keyword("finney"), keyword("wei")), + + // Elementary type names - now includes signed integers + elementaryType: P.alt( + // Unsigned integers + keyword("uint256"), + keyword("uint128"), + keyword("uint64"), + keyword("uint32"), + keyword("uint16"), + keyword("uint8"), + // Signed integers + keyword("int256"), + keyword("int128"), + keyword("int64"), + keyword("int32"), + keyword("int16"), + keyword("int8"), + // Address and bool + keyword("address"), + keyword("bool"), + // Bytes types + keyword("bytes32"), + keyword("bytes16"), + keyword("bytes8"), + keyword("bytes4"), + keyword("bytes"), + // String + keyword("string"), + ), + + // Keywords that can't be identifiers + reservedWord: P.alt( + // Statement keywords + keyword("let"), + keyword("if"), + keyword("else"), + keyword("for"), + keyword("while"), + keyword("return"), + keyword("break"), + keyword("continue"), + // Type keywords + keyword("struct"), + keyword("mapping"), + keyword("array"), + keyword("function"), + // Section keywords + keyword("name"), + keyword("storage"), + keyword("code"), + // Special + keyword("msg"), + // Boolean values (already handled as literals) + keyword("true"), + keyword("false"), + ), +}; + +/** + * Helper to create a parser that captures source location + */ +function located( + parser: P.Parser, +): P.Parser { + return parser.mark().map((mark) => { + const loc = toSourceLocation(mark); + const id = `${loc.offset}_${loc.length}` as Ast.Id; + return { + ...mark.value, + id, // Replace the pending ID with location-based ID + loc, + }; + }); +} + +/** + * Helper to run a parser and convert to Result type + */ +function runParser( + parser: P.Parser, + input: string, +): Result { + const result = parser.parse(input); + + if (result.status) { + return Result.ok(result.value); + } else { + // Convert Parsimmon position to SourceLocation + const location: Ast.SourceLocation = { + offset: result.index.offset, + length: 1, // We don't know the exact length, so use 1 + }; + + // Check if we have a custom error message + let message = `Parse error at line ${result.index.line}, column ${result.index.column}`; + + // First check if there's a custom message in the expected array + if (result.expected && result.expected.length > 0) { + // Look for our custom validation messages in the expected array + for (const expectedMsg of result.expected) { + if ( + expectedMsg && + (expectedMsg.includes("exceeds maximum safe integer") || + expectedMsg.includes("must be positive") || + expectedMsg.includes("must be non-negative") || + expectedMsg.includes("must be an integer") || + expectedMsg.includes("Cannot use keyword")) + ) { + message = expectedMsg; + break; + } + } + } + + const error = new ParseError(message, location, result.expected); + + return Result.err(error); + } +} + +// Forward declarations for recursive parsers +// eslint-disable-next-line prefer-const +let typeExpression: P.Parser; +// eslint-disable-next-line prefer-const +let expression: P.Parser; +// eslint-disable-next-line prefer-const +let statement: P.Parser; + +/** + * Type Parsers + */ + +// Elementary types with location +const elementaryType = located( + Lang.elementaryType.map((name: string) => { + // Parse the type name to extract kind and bits + if (name.startsWith("uint")) { + const bits = parseInt(name.substring(4), 10); + return Ast.Type.Elementary.uint(PENDING_ID, bits); + } else if (name.startsWith("int")) { + const bits = parseInt(name.substring(3), 10); + return Ast.Type.Elementary.int(PENDING_ID, bits); + } else if (name.startsWith("bytes") && name !== "bytes") { + const size = parseInt(name.substring(5), 10); + return Ast.Type.Elementary.bytes(PENDING_ID, size); + } else if (name === "address") { + return Ast.Type.Elementary.address(PENDING_ID); + } else if (name === "bool") { + return Ast.Type.Elementary.bool(PENDING_ID); + } else if (name === "string") { + return Ast.Type.Elementary.string(PENDING_ID); + } else if (name === "bytes") { + return Ast.Type.Elementary.bytes(PENDING_ID); + } + // This should never happen as elementaryTypeName parser ensures valid names + throw new Error(`Unknown elementary type: ${name}`); + }), +); + +// Reference type (identifier in type position) +const referenceType = located( + Lang.identifier.map((name: string) => Ast.Type.reference(PENDING_ID, name)), +); + +// Array type: array or array +const arrayType = P.lazy(() => + located( + P.seq( + Lang.keyword("array"), + Lang.lt, + typeExpression, + P.seq(Lang.comma, numberString) + .map(([_, n]) => n) + .fallback(null), + Lang.gt, + ).chain(([_, __, elementType, size, ___]) => { + if (size) { + const sizeNum = Number(size); + if (sizeNum > Number.MAX_SAFE_INTEGER) { + return P.fail( + `Array size ${size} exceeds maximum safe integer (${Number.MAX_SAFE_INTEGER})`, + ); + } + if (sizeNum <= 0) { + return P.fail(`Array size must be positive, got ${sizeNum}`); + } + } + return P.succeed( + Ast.Type.Complex.array( + PENDING_ID, + elementType, + size ? Number(size) : undefined, + ), + ); + }), + ), +); + +// Mapping type: mapping +const mappingType = P.lazy(() => + located( + P.seq( + Lang.keyword("mapping"), + Lang.lt, + typeExpression, + Lang.comma, + typeExpression, + Lang.gt, + ).map(([_, __, keyType, ___, valueType, ____]) => + Ast.Type.Complex.mapping(PENDING_ID, keyType, valueType), + ), + ), +); + +// Complete type expression +typeExpression = P.alt( + arrayType, + mappingType, + elementaryType, + referenceType, // Must come after keywords +); + +/** + * Expression Parsers + */ + +// Identifier expression +const identifier = located( + Lang.identifier.map((name: string) => + Ast.Expression.identifier(PENDING_ID, name), + ), +); + +// Number parser that returns string (not BigInt) +const numberString = Lang.lexeme(P.regexp(/[0-9]+/).desc("number")); + +// Hex parser +const hexString = Lang.lexeme(P.regexp(/0x[0-9a-fA-F]+/).desc("hex literal")); + +// Literal expressions +const numberLiteral = located( + numberString.map((value: string) => + Ast.Expression.Literal.number(PENDING_ID, value), + ), +); + +const hexLiteral = located( + hexString.map((value: string) => + Ast.Expression.Literal.hex(PENDING_ID, value), + ), +); + +const booleanLiteral = located( + P.alt(Lang.keyword("true"), Lang.keyword("false")).map((value: string) => + Ast.Expression.Literal.boolean(PENDING_ID, value), + ), +); + +const stringLiteral = located( + Lang.string.map((value: string) => + Ast.Expression.Literal.string(PENDING_ID, value), + ), +); + +// Address literal (0x followed by exactly 40 hex chars) +const addressLiteral = located( + P.regex(/0x[0-9a-fA-F]{40}/) + .desc("address literal") + .notFollowedBy(P.regex(/[0-9a-fA-F]/)) + .map((value: string) => + Ast.Expression.Literal.address(PENDING_ID, value.toLowerCase()), + ), +); + +// Wei literal (number followed by wei/finney/ether) +const weiLiteral = located( + P.seq( + numberString, + Lang._, + P.alt(Lang.keyword("wei"), Lang.keyword("finney"), Lang.keyword("ether")), + ).map(([value, _, unit]) => + Ast.Expression.Literal.number(PENDING_ID, value, unit), + ), +); + +// msg.sender, msg.value, and msg.data as special expressions +const msgExpression = located( + P.seq( + Lang.keyword("msg"), + Lang.dot, + P.alt(Lang.keyword("sender"), Lang.keyword("value"), Lang.keyword("data")), + ).map(([_, __, property]) => { + const kind = + property === "sender" + ? "msg.sender" + : property === "value" + ? "msg.value" + : "msg.data"; + if (kind === "msg.sender") { + return Ast.Expression.Special.msgSender(PENDING_ID); + } else if (kind === "msg.value") { + return Ast.Expression.Special.msgValue(PENDING_ID); + } else { + return Ast.Expression.Special.msgData(PENDING_ID); + } + }), +); + +// block.timestamp and block.number as special expressions +const blockExpression = located( + P.seq( + Lang.keyword("block"), + Lang.dot, + P.alt(Lang.keyword("timestamp"), Lang.keyword("number")), + ).map(([_, __, property]) => { + const kind = property === "timestamp" ? "block.timestamp" : "block.number"; + if (kind === "block.timestamp") { + return Ast.Expression.Special.blockTimestamp(PENDING_ID); + } else { + return Ast.Expression.Special.blockNumber(PENDING_ID); + } + }), +); + +// Array literal: [expr1, expr2, ...] +const arrayLiteral = P.lazy(() => + P.seq(Lang.lbracket, P.sepBy(expression, Lang.comma), Lang.rbracket).map( + ([_, elements, __]) => Ast.Expression.array(PENDING_ID, elements), + ), +); + +// Struct literal: StructName { field1: expr1, field2: expr2, ... } +const structLiteral = P.lazy(() => { + const fieldInit = P.seq(Lang.identifier, Lang.colon, expression).map( + ([name, _, value]) => ({ name, value }), + ); + + // Only named struct literals for now to avoid ambiguity + return P.seq( + Lang.identifier, + Lang.lbrace, + P.sepBy(fieldInit, Lang.comma), + Lang.rbrace, + ).map(([structName, _, fields, __]) => + Ast.Expression.struct(PENDING_ID, fields, structName), + ); +}); + +// Primary expressions (atoms) +const primaryExpression = P.lazy(() => + P.alt( + weiLiteral, + addressLiteral, + hexLiteral, + numberLiteral, + booleanLiteral, + stringLiteral, + arrayLiteral, + structLiteral, + msgExpression, + blockExpression, + identifier, + Lang.lparen.then(expression).skip(Lang.rparen), // Parenthesized + ), +); + +// Postfix expression handles both member and index access +const postfixExpression = P.lazy(() => { + const memberSuffix = P.seq(Lang.dot, Lang.identifier).map(([_, prop]) => ({ + type: "member" as const, + property: prop, + })); + + // Support both index access [expr] and slice access [start:end] + const indexSuffix = P.seq( + Lang.lbracket, + expression, + P.seq(Lang.colon, expression).or(P.succeed(null)), + Lang.rbracket, + ).map(([_, start, endPart, __]) => { + if (endPart) { + // Slice access: [start:end] + return { + type: "slice" as const, + property: start, + end: endPart[1], + }; + } else { + // Index access: [expr] + return { + type: "index" as const, + property: start, + }; + } + }); + + // Function call suffix: (arg1, arg2, ...) + const callSuffix = P.seq( + Lang.lparen, + P.sepBy(expression, Lang.comma), + Lang.rparen, + ).map(([_, args, __]) => ({ + type: "call" as const, + arguments: args, + })); + + // Type cast suffix: as Type + const castSuffix = P.seq(Lang.as, typeExpression).map(([_, targetType]) => ({ + type: "cast" as const, + targetType: targetType, + })); + + const suffix = P.alt(memberSuffix, indexSuffix, callSuffix, castSuffix); + + // Split into two parsers: inner for creating located intermediate expressions, + // outer for the main parsing logic + const innerPostfix = P.seq(primaryExpression, suffix.many()).map( + ([base, suffixes]) => { + return suffixes.reduce((obj, suffix) => { + // Use located for each intermediate expression + if (suffix.type === "member") { + return located( + P.succeed( + Ast.Expression.Access.member(PENDING_ID, obj, suffix.property), + ), + ).tryParse(""); + } else if (suffix.type === "slice") { + return located( + P.succeed( + Ast.Expression.Access.slice( + PENDING_ID, + obj, + suffix.property, + suffix.end, + ), + ), + ).tryParse(""); + } else if (suffix.type === "call") { + return located( + P.succeed(Ast.Expression.call(PENDING_ID, obj, suffix.arguments)), + ).tryParse(""); + } else if (suffix.type === "cast") { + return located( + P.succeed(Ast.Expression.cast(PENDING_ID, obj, suffix.targetType)), + ).tryParse(""); + } else { + return located( + P.succeed( + Ast.Expression.Access.index(PENDING_ID, obj, suffix.property), + ), + ).tryParse(""); + } + }, base); + }, + ); + + return located(innerPostfix); +}); + +// Unary expressions +const unaryExpression: P.Parser = P.lazy(() => + P.alt( + located( + P.seq(P.alt(Lang.not, Lang.minus), unaryExpression).map( + ([op, expr]: [string, Ast.Expression]) => + Ast.Expression.operator(PENDING_ID, op, [expr]), + ), + ), + postfixExpression, + ), +); + +// Binary expression with precedence climbing +const binaryOperators = [ + ["||"], + ["&&"], + ["==", "!="], + ["<", ">", "<=", ">="], + ["+", "-"], + ["*", "/"], +]; + +// Build precedence parser +function precedenceParser( + precedence: number, + nextParser: P.Parser, +): P.Parser { + if (precedence >= binaryOperators.length) { + return nextParser; + } + + const operators = binaryOperators[precedence]; + // Sort operators by length (descending) to match longer operators first + const sortedOps = [...operators].sort((a, b) => b.length - a.length); + const operatorParsers = sortedOps.map((op) => + Lang._.then(P.string(op)).skip(Lang._), + ); + + return P.lazy(() => + located( + P.seq( + precedenceParser(precedence + 1, nextParser), + P.seq( + P.alt(...operatorParsers), + precedenceParser(precedence + 1, nextParser), + ).many(), + ).map(([first, rest]) => { + return rest.reduce((left, [op, right]) => { + const loc = left.loc && + right.loc && { + offset: left.loc.offset, + length: + Number(right.loc.offset) + + Number(right.loc.length) - + Number(left.loc.offset), + }; + return Ast.Expression.operator( + PENDING_ID, + op, + [left, right], + loc || undefined, + ); + }, first); + }), + ), + ); +} + +// Complete expression parser +expression = precedenceParser(0, unaryExpression); + +/** + * Statement Parsers + */ + +// Variable declaration: let x = expr; or let x: Type = expr; +const letStatement = located( + P.seq( + Lang.keyword("let"), + located( + P.seq( + Lang.identifier, + // Optional type annotation + P.seq(Lang.colon, typeExpression) + .map(([_, type]) => type) + .fallback(undefined), + Lang.equals, + expression, + ).map(([name, declaredType, __, init]) => + Ast.Declaration.variable(PENDING_ID, name, declaredType, init), + ), + ), + Lang.semicolon, + ).map(([_, declaration, __]) => + Ast.Statement.declare(PENDING_ID, declaration), + ), +); + +// Assignment: lvalue = expr; +const assignmentStatement = P.lazy(() => + located( + P.seq(expression, Lang.equals, expression, Lang.semicolon).map( + ([target, _, value, __]) => + Ast.Statement.assign(PENDING_ID, target, value), + ), + ), +); + +// Expression statement: expr; +const expressionStatement = located( + P.seq(expression, Lang.semicolon).map(([expr, _]) => + Ast.Statement.express(PENDING_ID, expr), + ), +); + +// Return statement: return expr?; +const returnStatement = located( + P.seq(Lang.keyword("return"), expression.fallback(null), Lang.semicolon).map( + ([_, value, __]) => + Ast.Statement.ControlFlow.return_(PENDING_ID, value || undefined), + ), +); + +// Break statement: break; +const breakStatement = located( + P.seq(Lang.keyword("break"), Lang.semicolon).map(() => + Ast.Statement.ControlFlow.break_(PENDING_ID), + ), +); + +// Block of statements +const blockStatements = located( + P.lazy(() => + P.seq(Lang.lbrace, statement.many(), Lang.rbrace).map(([_, stmts, __]) => + Ast.Block.statements(PENDING_ID, stmts), + ), + ), +); + +// If statement +const ifStatement = P.lazy(() => + located( + P.seq( + Lang.keyword("if"), + Lang.lparen, + expression, + Lang.rparen, + blockStatements, + P.seq(Lang.keyword("else"), blockStatements) + .map(([_, block]) => block) + .fallback(undefined), + ).map(([_, __, condition, ___, thenBlock, elseBlock]) => + Ast.Statement.ControlFlow.if_( + PENDING_ID, + condition, + thenBlock, + elseBlock, + ), + ), + ), +); + +// For statement +const forStatement = P.lazy(() => + located( + P.seq( + Lang.keyword("for"), + Lang.lparen, + letStatement, + expression, + Lang.semicolon, + // Update is an assignment without semicolon + located( + P.seq(expression, Lang.equals, expression).map(([target, _, value]) => + Ast.Statement.assign(PENDING_ID, target, value), + ), + ), + Lang.rparen, + blockStatements, + ).map( + ( + parts: readonly [ + unknown, + unknown, + Ast.Statement, + Ast.Expression, + unknown, + Ast.Statement, + unknown, + Ast.Block, + ], + ) => { + const init = parts[2]; + const condition = parts[3]; + const update = parts[5]; + const body = parts[7]; + return Ast.Statement.ControlFlow.for_( + PENDING_ID, + body, + init, + condition, + update, + ); + }, + ), + ), +); + +// All statements +statement = P.alt( + letStatement, + ifStatement, + forStatement, + returnStatement, + breakStatement, + assignmentStatement, + expressionStatement, +); + +/** + * Top-level Parsers + */ + +// Field declaration: name: Type +const fieldDeclaration = located( + P.seq(Lang.identifier, Lang.colon, typeExpression).map( + ([name, _, fieldType]) => + Ast.Declaration.field(PENDING_ID, name, fieldType), + ), +); + +// Struct declaration +const structDeclaration = located( + P.seq( + Lang.keyword("struct"), + Lang.identifier, + Lang.lbrace, + fieldDeclaration + .sepBy(Lang.semicolon) + .skip(Lang.semicolon.or(P.succeed(null))), + Lang.rbrace, + ).map(([_, name, __, fields, ___]) => + Ast.Declaration.struct(PENDING_ID, name, fields), + ), +); + +// Function parameter: name: Type +const functionParameter = located( + P.seq(Lang.identifier, Lang.colon, typeExpression).map( + ([name, _, paramType]) => + Ast.Declaration.parameter(PENDING_ID, name, paramType), + ), +); + +// Function declaration +const functionDeclaration = P.lazy(() => + located( + P.seq( + Lang.keyword("function"), + Lang.identifier, + Lang.lparen, + functionParameter.sepBy(Lang.comma), + Lang.rparen, + P.seq(Lang.arrow, typeExpression) + .map(([_, returnType]) => returnType) + .fallback(undefined), + blockStatements, + ).map(([_, name, __, params, ___, returnType, body]) => + Ast.Declaration.function_(PENDING_ID, name, params, returnType, body), + ), + ), +); + +// Storage declaration: [slot] name: Type +const storageDeclaration = located( + P.seq( + Lang.lbracket, + numberString, + Lang.rbracket, + Lang.identifier, + Lang.colon, + typeExpression, + ).chain(([_, slot, __, name, ___, storageType]) => { + const slotNum = Number(slot); + if (slotNum > Number.MAX_SAFE_INTEGER) { + return P.fail( + `Storage slot ${slot} exceeds maximum safe integer (${Number.MAX_SAFE_INTEGER})`, + ); + } + if (slotNum < 0) { + return P.fail(`Storage slot must be non-negative, got ${slotNum}`); + } + if (!Number.isInteger(slotNum)) { + return P.fail(`Storage slot must be an integer, got ${slotNum}`); + } + return P.succeed( + Ast.Declaration.storage(PENDING_ID, name, storageType, slotNum), + ); + }), +); + +// Define block - optional, contains user-defined types and functions +const defineBlock = located( + P.seq( + Lang.keyword("define"), + Lang.lbrace, + P.alt( + // Non-empty define block - each declaration must end with semicolon + P.alt(structDeclaration, functionDeclaration) + .skip(Lang.semicolon) + .atLeast(1), + // Empty define block + P.succeed([]), + ), + Lang.rbrace, + ).map(([_, __, declarations, ___]) => + Ast.Block.definitions(PENDING_ID, declarations), + ), +); + +// Storage block - optional +const storageBlock = P.seq( + Lang.keyword("storage"), + Lang.lbrace, + P.alt( + // Non-empty storage block - each declaration must end with semicolon + storageDeclaration.skip(Lang.semicolon).atLeast(1), + // Empty storage block + P.succeed([]), + ), + Lang.rbrace, +).map(([_, __, declarations, ___]) => declarations); + +// Create block (constructor code) +const createBlock = located( + P.seq(Lang.keyword("create"), Lang.lbrace, statement.many(), Lang.rbrace).map( + ([_, __, stmts, ___]) => Ast.Block.statements(PENDING_ID, stmts), + ), +); + +// Code block (runtime code) +const codeBlock = located( + P.seq(Lang.keyword("code"), Lang.lbrace, statement.many(), Lang.rbrace).map( + ([_, __, stmts, ___]) => Ast.Block.statements(PENDING_ID, stmts), + ), +); + +// Program +const program = located( + P.seq( + Lang.keyword("name"), + Lang.identifier, + Lang.semicolon, + defineBlock.or(P.succeed(null)), + storageBlock.or(P.succeed([])), + createBlock.or(P.succeed(null)), + codeBlock.or(P.succeed(null)), + ).map(([_, name, __, defineBlockNode, storageDecls, create, body]) => { + return Ast.program( + PENDING_ID, + name, + storageDecls, + defineBlockNode || undefined, + body || undefined, + create || undefined, + ); + }), +); + +// Export the parser wrapped with whitespace handling +const parser = P.seq(Lang._, program, Lang._).map(([_, prog, __]) => prog); diff --git a/packages/bugc/src/parser/pass.ts b/packages/bugc/src/parser/pass.ts new file mode 100644 index 00000000..bc5c5f0c --- /dev/null +++ b/packages/bugc/src/parser/pass.ts @@ -0,0 +1,27 @@ +import type * as Ast from "#ast"; +import type { Pass } from "#compiler"; +import { Result } from "#result"; + +import type { Error as ParseError } from "./errors.js"; +import { parse } from "./parser.js"; + +/** + * Parsing pass - converts source code to AST + */ +const pass: Pass<{ + needs: { + source: string; + sourcePath?: string; + }; + adds: { + ast: Ast.Program; + }; + error: ParseError; +}> = { + async run({ source }) { + const result = parse(source); + return Result.map(result, (ast) => ({ ast })); + }, +}; + +export default pass; diff --git a/packages/bugc/src/parser/slice.test.ts b/packages/bugc/src/parser/slice.test.ts new file mode 100644 index 00000000..f51c38dd --- /dev/null +++ b/packages/bugc/src/parser/slice.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test } from "vitest"; + +import * as Ast from "#ast"; + +import { parse } from "./parser.js"; + +describe("Slice expressions", () => { + test("parses simple slice syntax", () => { + const result = parse(` + name Test; + code { + data[0:4]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) { + throw new Error("Parse failed"); + } + const program = result.value; + const exprStmt = program.body?.items[0]; + expect(exprStmt?.kind).toBe("statement:express"); + + if (exprStmt?.kind === "statement:express") { + const slice = (exprStmt as Ast.Statement.Express) + .expression as Ast.Expression.Access; + if (!Ast.Expression.Access.isSlice(slice)) { + throw new Error("Expected slice access"); + } + + expect(slice.kind).toBe("expression:access:slice"); + expect(slice.start).toMatchObject({ + kind: "expression:literal:number", + value: "0", + }); + expect(slice.end).toMatchObject({ + kind: "expression:literal:number", + value: "4", + }); + } + }); + + test("parses slice with complex expressions", () => { + const result = parse(` + name Test; + code { + msg.data[offset:offset + 32]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + const program = result.value; + const exprStmt = program.body?.items[0]; + + if (exprStmt?.kind === "statement:express") { + const slice = (exprStmt as Ast.Statement.Express) + .expression as Ast.Expression.Access; + expect(slice.kind).toBe("expression:access:slice"); + expect(slice.object).toMatchObject({ + kind: "expression:special:msg.data", + }); + } + }); + + test("distinguishes slice from index access", () => { + const result = parse(` + name Test; + code { + data[5]; + data[0:5]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + const program = result.value; + + const indexedStmt = program.body?.items[0]; + const slicedStmt = program.body?.items[1]; + + if ( + indexedStmt?.kind === "statement:express" && + slicedStmt?.kind === "statement:express" + ) { + const indexed = (indexedStmt as Ast.Statement.Express) + .expression as Ast.Expression.Access; + const sliced = (slicedStmt as Ast.Statement.Express) + .expression as Ast.Expression.Access; + + expect(indexed.kind).toBe("expression:access:index"); + expect(sliced.kind).toBe("expression:access:slice"); + } + }); +}); diff --git a/packages/bugc/src/result.ts b/packages/bugc/src/result.ts new file mode 100644 index 00000000..b25f8bb0 --- /dev/null +++ b/packages/bugc/src/result.ts @@ -0,0 +1,315 @@ +/** + * Result type for compiler operations with severity-based message organization + */ + +import type { BugError } from "#errors"; + +/** + * Message severity levels + */ +export enum Severity { + Error = "error", + Warning = "warning", + // Easy to extend with: Info = "info", Debug = "debug", etc. +} + +/** + * Messages organized by severity level + */ +export type MessagesBySeverity = { + [K in Severity]?: E[]; +}; + +/** + * Result type that tracks success/failure with categorized messages + */ +export type Result = + | { + success: true; + value: T; + messages: MessagesBySeverity; + } + | { + success: false; + messages: MessagesBySeverity; + }; + +/** + * Helper functions for working with Results + */ +export const Result = { + /** + * Create a successful result with no messages + */ + ok(value: T): Result { + return { success: true, value, messages: {} }; + }, + + /** + * Create a successful result with messages + */ + okWith(value: T, messages: E[]): Result { + return Result.addMessages(Result.ok(value), messages); + }, + + /** + * Create a failed result with error messages + */ + err(errors: E | E[]): Result { + const errorArray = Array.isArray(errors) ? errors : [errors]; + return { + success: false, + messages: { [Severity.Error]: errorArray }, + }; + }, + + /** + * Transform the value of a successful result + */ + map( + result: Result, + transform: (value: T) => U, + ): Result { + if (!result.success) { + return result as Result; + } + + return { + ...result, + value: transform(result.value), + }; + }, + + /** + * Add messages to an existing result + */ + addMessages( + result: Result, + newMessages: E[], + ): Result { + const messages = { ...result.messages }; + + for (const msg of newMessages) { + const severity = msg.severity; + if (!messages[severity]) { + messages[severity] = []; + } + messages[severity]!.push(msg); + } + + return { ...result, messages }; + }, + + /** + * Merge messages from two MessagesBySeverity objects + */ + mergeMessages( + a: MessagesBySeverity, + b: MessagesBySeverity, + ): MessagesBySeverity { + const merged: MessagesBySeverity = { ...a }; + + for (const [severity, messages] of Object.entries(b) as [Severity, E[]][]) { + if (!merged[severity]) { + merged[severity] = []; + } + merged[severity]!.push(...messages); + } + + return merged; + }, + + /** + * Check if a result has error messages + */ + hasErrors(result: Result): boolean { + return (result.messages[Severity.Error]?.length ?? 0) > 0; + }, + + /** + * Check if a result has warning messages + */ + hasWarnings(result: Result): boolean { + return (result.messages[Severity.Warning]?.length ?? 0) > 0; + }, + + /** + * Get all messages from a result as a flat array + */ + allMessages(result: Result): E[] { + return Object.values(result.messages).flat() as E[]; + }, + + /** + * Get messages by severity + */ + getMessages( + result: Result, + severity: Severity, + ): E[] { + return result.messages[severity] || []; + }, + + /** + * Get the first error from a result (useful for tests) + */ + firstError(result: Result): E | undefined { + const errors = result.messages[Severity.Error]; + return errors?.[0]; + }, + + /** + * Get the first message of any severity + */ + firstMessage(result: Result): E | undefined { + const allMessages = Result.allMessages(result); + return allMessages[0]; + }, + + /** + * Count messages by severity + */ + countBySeverity( + result: Result, + severity: Severity, + ): number { + return result.messages[severity]?.length ?? 0; + }, + + /** + * Count messages matching criteria + */ + countMessages( + result: Result, + match?: { + severity?: Severity; + code?: string; + }, + ): number { + if (!match) { + return Result.allMessages(result).length; + } + return Result.findMessages(result, match).length; + }, + + /** + * Find a single message matching criteria + */ + findMessage( + result: Result, + match: { + severity?: Severity; + code?: string; + message?: string | RegExp; + location?: { offset: number; length?: number }; + }, + ): E | undefined { + return Result.allMessages(result).find((e) => { + if (match.severity !== undefined && e.severity !== match.severity) + return false; + if (match.code && e.code !== match.code) return false; + + if (match.message) { + const matches = + typeof match.message === "string" + ? e.message.includes(match.message) + : match.message.test(e.message); + if (!matches) return false; + } + + if (match.location) { + if (!e.location) return false; + if (e.location.offset !== match.location.offset) return false; + if ( + match.location.length !== undefined && + e.location.length !== match.location.length + ) + return false; + } + + return true; + }); + }, + + /** + * Find all messages matching criteria + */ + findMessages( + result: Result, + match: { + severity?: Severity; + code?: string; + message?: string | RegExp; + location?: { offset: number; length?: number }; + }, + ): E[] { + return Result.allMessages(result).filter((e) => { + if (match.severity !== undefined && e.severity !== match.severity) + return false; + if (match.code && e.code !== match.code) return false; + + if (match.message) { + const matches = + typeof match.message === "string" + ? e.message.includes(match.message) + : match.message.test(e.message); + if (!matches) return false; + } + + if (match.location) { + if (!e.location) return false; + if (e.location.offset !== match.location.offset) return false; + if ( + match.location.length !== undefined && + e.location.length !== match.location.length + ) + return false; + } + + return true; + }); + }, + + /** + * Check if any message matches criteria + */ + hasMessage( + result: Result, + match: { + severity?: Severity; + code?: string; + message?: string | RegExp; + location?: { offset: number; length?: number }; + }, + ): boolean { + return Result.findMessage(result, match) !== undefined; + }, + + /** + * Check if result has any messages + */ + hasMessages(result: Result): boolean { + return Result.countMessages(result) > 0; + }, + + /** + * Get errors only (convenience for common pattern) + */ + errors(result: Result): E[] { + return Result.getMessages(result, Severity.Error); + }, + + /** + * Get warnings only (convenience for common pattern) + */ + warnings(result: Result): E[] { + return Result.getMessages(result, Severity.Warning); + }, + + /** + * Count errors in the result + */ + countErrors(result: Result): number { + return Result.errors(result).length; + }, +}; diff --git a/packages/bugc/src/typechecker/assignable.ts b/packages/bugc/src/typechecker/assignable.ts new file mode 100644 index 00000000..eae8d573 --- /dev/null +++ b/packages/bugc/src/typechecker/assignable.ts @@ -0,0 +1,58 @@ +import { Type } from "#types"; + +/** + * Checks if a value type can be assigned to a target type. + * Handles implicit conversions and type compatibility. + */ +export function isAssignable(target: Type, value: Type): boolean { + if (Type.isFailure(target) || Type.isFailure(value)) { + return true; + } + if (Type.equals(target, value)) { + return true; + } + + // Numeric types can be implicitly converted (with range checks) + if ( + Type.isElementary(target) && + Type.isElementary(value) && + Type.Elementary.isNumeric(target) && + Type.Elementary.isNumeric(value) + ) { + // Only allow same signedness + if (Type.Elementary.isUint(target) && Type.Elementary.isUint(value)) { + return true; + } + if (Type.Elementary.isInt(target) && Type.Elementary.isInt(value)) { + return true; + } + } + + return false; +} + +/** + * Finds the common type for binary operations. + * Returns the larger of two compatible types. + */ +export function commonType(type1: Type, type2: Type): Type | null { + if (Type.equals(type1, type2)) { + return type1; + } + + // For numeric types, return the larger type + if (Type.isElementary(type1) && Type.isElementary(type2)) { + if (Type.Elementary.isUint(type1) && Type.Elementary.isUint(type2)) { + const size1 = type1.bits || 256; + const size2 = type2.bits || 256; + return size1 >= size2 ? type1 : type2; + } + if (Type.Elementary.isInt(type1) && Type.Elementary.isInt(type2)) { + const size1 = type1.bits || 256; + const size2 = type2.bits || 256; + return size1 >= size2 ? type1 : type2; + } + } + + return null; +} diff --git a/packages/bugc/src/typechecker/blocks.ts b/packages/bugc/src/typechecker/blocks.ts new file mode 100644 index 00000000..7f208dff --- /dev/null +++ b/packages/bugc/src/typechecker/blocks.ts @@ -0,0 +1,417 @@ +import * as Ast from "#ast"; +import { Type } from "#types"; +import type { Visitor } from "#ast"; +import type { Symbol as BugSymbol } from "./symbols.js"; +import type { Context, Report } from "./context.js"; +import { enterFunctionScope } from "./symbols.js"; +import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; +import { resolveTypeWithBindings } from "./declarations.js"; +import { isAssignable } from "./assignable.js"; + +/** + * Type checker for block-level constructs: + * - program + * - blocks + * - declarations + */ +export const blockChecker: Pick< + Visitor, + "program" | "block" | "declaration" +> = { + program(node: Ast.Program, context: Context): Report { + // Note: First two passes (collecting structs/functions and storage) + // are already done in collectDeclarations() and buildInitialSymbols() + // We only need to handle the third pass and main body processing + + let currentSymbols = context.symbols; + let currentNodeTypes = context.nodeTypes; + let currentBindings = context.bindings; + const allErrors: TypeError[] = []; + + // Process storage declarations to set their types in nodeTypes + if (node.storage) { + for (const storageDecl of node.storage) { + const declContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + visitor: context.visitor, + }; + const declResult = Ast.visit( + declContext.visitor, + storageDecl, + declContext, + ); + currentNodeTypes = declResult.nodeTypes; + currentBindings = declResult.bindings; + allErrors.push(...declResult.errors); + } + } + + // Visit all definitions to ensure they get types in nodeTypes map + if (node.definitions) { + for (let i = 0; i < node.definitions.items.length; i++) { + const decl = node.definitions.items[i]; + // Visit function and struct declarations to set their types in nodeTypes + // and record bindings for type references + if ( + decl.kind === "declaration:function" || + decl.kind === "declaration:struct" + ) { + const declContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + visitor: context.visitor, + }; + const declResult = Ast.visit(declContext.visitor, decl, declContext); + currentNodeTypes = declResult.nodeTypes; + currentBindings = declResult.bindings; + allErrors.push(...declResult.errors); + + // For structs, also visit their fields to record type reference bindings + if (decl.kind === "declaration:struct") { + for (const field of decl.fields) { + const fieldResult = Ast.visit(declContext.visitor, field, { + ...declContext, + bindings: currentBindings, + }); + currentBindings = fieldResult.bindings; + allErrors.push(...fieldResult.errors); + } + } + } + } + + // Third pass: type check function bodies + for (let i = 0; i < node.definitions.items.length; i++) { + const decl = node.definitions.items[i]; + if (Ast.Declaration.isFunction(decl)) { + // Look up the function type + const funcType = currentSymbols.lookup(decl.name) + ?.type as Type.Function; + if (funcType) { + // Create a new scope with function parameters + const funcSymbols = enterFunctionScope( + currentSymbols, + decl, + funcType, + ); + + // Create context for function body with return type set + const funcContext: Context = { + ...context, + symbols: funcSymbols, + currentReturnType: funcType.return || undefined, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + visitor: context.visitor, + }; + + // Type check the function body + const bodyResult = Ast.visit( + funcContext.visitor, + decl.body, + funcContext, + ); + + // Exit function scope - we don't propagate function-local symbols + // so we keep currentSymbols unchanged (it still points to the pre-function scope) + currentNodeTypes = bodyResult.nodeTypes; + currentBindings = bodyResult.bindings; + allErrors.push(...bodyResult.errors); + } + } + } + } + + // Process create block if present + if (node.create) { + const createContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + }; + const createResult = Ast.visit( + createContext.visitor, + node.create, + createContext, + ); + currentSymbols = createResult.symbols; + currentNodeTypes = createResult.nodeTypes; + currentBindings = createResult.bindings; + allErrors.push(...createResult.errors); + } + + // Process main code block + if (node.body) { + const bodyContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + }; + const bodyResult = Ast.visit(bodyContext.visitor, node.body, bodyContext); + currentSymbols = bodyResult.symbols; + currentNodeTypes = bodyResult.nodeTypes; + currentBindings = bodyResult.bindings; + allErrors.push(...bodyResult.errors); + } + + return { + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + errors: allErrors, + }; + }, + + block(node: Ast.Block, context: Context): Report { + // Statement blocks need scope management + if (node.kind === "block:statements") { + // Enter new scope + let currentSymbols = context.symbols.enterScope(); + let currentNodeTypes = context.nodeTypes; + let currentBindings = context.bindings; + const allErrors: TypeError[] = []; + + // Process each item in the block + for (let i = 0; i < node.items.length; i++) { + const item = node.items[i]; + const itemContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + }; + + const itemResult = Ast.visit(itemContext.visitor, item, itemContext); + + // Thread the results to the next item + currentSymbols = itemResult.symbols; + currentNodeTypes = itemResult.nodeTypes; + currentBindings = itemResult.bindings; + allErrors.push(...itemResult.errors); + } + + // Exit scope + return { + symbols: currentSymbols.exitScope(), + nodeTypes: currentNodeTypes, + bindings: currentBindings, + errors: allErrors, + }; + } + + // Definition blocks don't need scope management, just process items + if (node.kind === "block:definitions") { + let currentSymbols = context.symbols; + let currentNodeTypes = context.nodeTypes; + let currentBindings = context.bindings; + const allErrors: TypeError[] = []; + + // Process each declaration in the block + for (let i = 0; i < node.items.length; i++) { + const item = node.items[i]; + const itemContext: Context = { + ...context, + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + }; + + const itemResult = Ast.visit(itemContext.visitor, item, itemContext); + + // Thread the results to the next item + currentSymbols = itemResult.symbols; + currentNodeTypes = itemResult.nodeTypes; + currentBindings = itemResult.bindings; + allErrors.push(...itemResult.errors); + } + + return { + symbols: currentSymbols, + nodeTypes: currentNodeTypes, + bindings: currentBindings, + errors: allErrors, + }; + } + + // Should not reach here, but return unchanged if we do + return { + symbols: context.symbols, + nodeTypes: context.nodeTypes, + bindings: context.bindings, + errors: [], + }; + }, + + declaration(node: Ast.Declaration, context: Context): Report { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + switch (node.kind) { + case "declaration:struct": + // Already processed in collectDeclarations phase + return { symbols, nodeTypes, bindings, errors }; + + case "declaration:function": { + // Function declarations are already in the symbol table from buildInitialSymbols + // We just need to set the type on the node and record any type reference bindings + const symbol = symbols.lookup(node.name); + if (symbol) { + nodeTypes.set(node.id, symbol.type); + } + + // Process parameter types to record bindings for type references + for (const param of node.parameters) { + if (param.type) { + const typeResult = resolveTypeWithBindings( + param.type, + context.structs, + bindings, + ); + bindings = typeResult.bindings; + } + } + + // Process return type to record bindings for type references + if (node.returnType) { + const typeResult = resolveTypeWithBindings( + node.returnType, + context.structs, + bindings, + ); + bindings = typeResult.bindings; + } + + return { type: symbol?.type, symbols, nodeTypes, bindings, errors }; + } + + case "declaration:storage": { + // Storage declarations are already in the symbol table from buildInitialSymbols + // We just need to set the type on the node and record any type reference bindings + const symbol = symbols.lookup(node.name); + if (symbol) { + nodeTypes.set(node.id, symbol.type); + } + + // Also process the type node to record bindings for type references + if (node.type) { + const typeResult = resolveTypeWithBindings( + node.type, + context.structs, + bindings, + ); + bindings = typeResult.bindings; + } + + return { type: symbol?.type, symbols, nodeTypes, bindings, errors }; + } + + case "declaration:variable": { + if (!node.initializer) { + const error = new TypeError( + `Variable ${node.name} must have an initializer`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.MISSING_INITIALIZER, + ); + errors.push(error); + + // Still define the variable with error type + const errorType = Type.failure("missing initializer"); + const symbol: BugSymbol = { + name: node.name, + type: errorType, + mutable: true, + location: "memory", + declaration: node, + }; + symbols = symbols.define(symbol); + nodeTypes.set(node.id, errorType); + return { type: errorType, symbols, nodeTypes, bindings, errors }; + } + + // Type check the initializer + const initContext: Context = { + ...context, + nodeTypes, + bindings, + }; + const initResult = Ast.visit( + initContext.visitor, + node.initializer, + initContext, + ); + nodeTypes = initResult.nodeTypes; + bindings = initResult.bindings; + errors.push(...initResult.errors); + + // Determine the variable's type + let type: Type; + if (node.type) { + // If a type is explicitly declared, use it and record bindings + const typeResult = resolveTypeWithBindings( + node.type, + context.structs, + bindings, + ); + type = typeResult.type; + bindings = typeResult.bindings; + + // Check that the initializer is compatible with the declared type + if (initResult.type && !isAssignable(type, initResult.type)) { + const error = new TypeError( + ErrorMessages.TYPE_MISMATCH( + Type.format(type), + Type.format(initResult.type), + ), + node.initializer.loc || undefined, + Type.format(type), + Type.format(initResult.type), + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } else { + // Otherwise, infer the type from the initializer + type = initResult.type || Type.failure("invalid initializer"); + } + + const symbol: BugSymbol = { + name: node.name, + type, + mutable: true, + location: "memory", + declaration: node, + }; + symbols = symbols.define(symbol); + nodeTypes.set(node.id, type); + return { type, symbols, nodeTypes, bindings, errors }; + } + + case "declaration:field": + // Fields are handled as part of struct processing, + // but we still need to record bindings for type references + if (node.type) { + const typeResult = resolveTypeWithBindings( + node.type, + context.structs, + bindings, + ); + bindings = typeResult.bindings; + } + return { symbols, nodeTypes, bindings, errors }; + + default: + return { symbols, nodeTypes, bindings, errors }; + } + }, +}; diff --git a/packages/bugc/src/typechecker/checker.test.ts b/packages/bugc/src/typechecker/checker.test.ts new file mode 100644 index 00000000..9746de34 --- /dev/null +++ b/packages/bugc/src/typechecker/checker.test.ts @@ -0,0 +1,399 @@ +import { describe, it, expect } from "vitest"; + +import { parse } from "#parser"; +import { Result, Severity } from "#result"; +import type { Types } from "#types"; + +import { checkProgram } from "./checker.js"; +import type { Error as BugTypeError } from "./errors.js"; + +import "#test/matchers"; + +describe("checkProgram", () => { + function check(source: string): Result { + const parseResult = parse(source); + if (!parseResult.success) { + const firstError = Result.firstError(parseResult); + throw new Error(`Parse error: ${firstError?.message || "Unknown error"}`); + } + const ast = parseResult.value; + const result = checkProgram(ast); + return Result.map(result, ({ types }) => types); + } + + describe("Variable Declarations", () => { + it("should type check variable declarations", () => { + const result = check(` + name Test; + storage {} + code { + let x = 42; + let y = true; + let z = "hello"; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + // Variables are local to the code block and not accessible after type checking + }); + + it("should report error for undefined variables", () => { + const result = check(` + name Test; + storage {} + code { + x = 42; + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Undefined variable: x", + }); + }); + }); + + describe("Type Assignments", () => { + it("should allow numeric assignments", () => { + const result = check(` + name Test; + storage {} + code { + let x = 42; + x = 100; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should report type mismatch", () => { + const result = check(` + name Test; + storage {} + code { + let x = 42; + x = true; + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Type mismatch", + }); + }); + }); + + describe("Operators", () => { + it("should type check arithmetic operators", () => { + const result = check(` + name Test; + storage {} + code { + let x = 10 + 20; + let y = x * 2; + let z = y - x; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check comparison operators", () => { + const result = check(` + name Test; + storage {} + code { + let x = 10; + let b1 = x > 5; + let b2 = x <= 20; + let b3 = x == 10; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check logical operators", () => { + const result = check(` + name Test; + storage {} + code { + let a = true; + let b = false; + let c = a && b; + let d = a || b; + let e = !a; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should report operator type errors", () => { + const result = check(` + name Test; + storage {} + code { + let x = true + false; + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "requires numeric operands", + }); + }); + }); + + describe("Structs", () => { + it("should type check struct field access", () => { + const result = check(` + name Test; + define { + struct Point { + x: uint256; + y: uint256; + }; + } + storage { + [0] point: Point; + } + code { + let x = point.x; + point.y = 100; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should report undefined struct fields", () => { + const result = check(` + name Test; + define { + struct Point { + x: uint256; + y: uint256; + }; + } + storage { + [0] point: Point; + } + code { + let z = point.z; + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "has no field z", + }); + }); + }); + + describe("Arrays and Mappings", () => { + it("should type check array access", () => { + const result = check(` + name Test; + storage { + [0] nums: array; + } + code { + let x = nums[0]; + nums[1] = 42; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check mapping access", () => { + const result = check(` + name Test; + storage { + [0] balances: mapping; + } + code { + let addr = 0x1234567890123456789012345678901234567890; + let bal = balances[addr]; + balances[addr] = 100; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should report invalid array index type", () => { + const result = check(` + name Test; + storage { + [0] nums: array; + } + code { + let x = nums[true]; + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "Array index must be numeric", + }); + }); + }); + + describe("Control Flow", () => { + it("should type check if statements", () => { + const result = check(` + name Test; + storage {} + code { + let x = 10; + if (x > 5) { + x = 20; + } else { + x = 0; + } + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check for loops", () => { + const result = check(` + name Test; + storage {} + code { + let sum = 0; + for (let i = 0; i < 10; i = i + 1) { + sum = sum + i; + } + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should report non-boolean conditions", () => { + const result = check(` + name Test; + storage {} + code { + if (42) { + let x = 1; + } + } + `); + + expect(result.success).toBe(false); + expect(Result.countErrors(result)).toBe(1); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "condition must be boolean", + }); + }); + }); + + describe("Special Expressions", () => { + it("should type check msg.sender", () => { + const result = check(` + name Test; + storage { + [0] owner: address; + } + code { + owner = msg.sender; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check msg.value", () => { + const result = check(` + name Test; + storage { + [0] balance: uint256; + } + code { + balance = balance + msg.value; + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + + it("should type check msg.data", () => { + const result = check(` + name Test; + storage { + [0] calldataHash: bytes32; + } + code { + let data = msg.data; + // Note: bytes type is dynamic, can be used in let statements + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + }); + + describe("Complex Programs", () => { + it("should type check complete program", () => { + const result = check(` + name SimpleStorage; + + define { + struct User { + addr: address; + balance: uint256; + }; + } + + storage { + [0] owner: address; + [1] users: mapping; + [2] totalSupply: uint256; + } + + code { + let sender = msg.sender; + + if (sender == owner) { + let user = users[sender]; + user.balance = user.balance + msg.value; + totalSupply = totalSupply + msg.value; + } + } + `); + + expect(result.success).toBe(true); + expect(Result.hasMessages(result)).toBe(false); + }); + }); +}); diff --git a/packages/bugc/src/typechecker/checker.ts b/packages/bugc/src/typechecker/checker.ts new file mode 100644 index 00000000..63bc4c58 --- /dev/null +++ b/packages/bugc/src/typechecker/checker.ts @@ -0,0 +1,64 @@ +import * as Ast from "#ast"; +import { type Types, type Bindings, emptyBindings } from "#types"; +import { Result } from "#result"; +import { collectDeclarations } from "./declarations.js"; +import { buildInitialSymbols } from "./symbols.js"; +import { expressionChecker } from "./expressions.js"; +import { statementChecker } from "./statements.js"; +import { blockChecker } from "./blocks.js"; +import { typeNodeChecker } from "./type-nodes.js"; +import { Error as TypeError } from "./errors.js"; +import type { Report, Context } from "./context.js"; + +/** + * Compose the full type checker from modular parts + */ +const typeChecker: Ast.Visitor = [ + expressionChecker, + statementChecker, + blockChecker, + typeNodeChecker, +].reduce((a, b) => ({ ...a, ...b }), {}) as Ast.Visitor; + +/** + * Main type checking function. + * + * Process: + * 1. Collect all type declarations (structs, functions) + * 2. Build initial symbol table with globals + * 3. Traverse AST with composed visitor + * 4. Return symbols and type map, or errors + */ +export function checkProgram( + program: Ast.Program, +): Result<{ types: Types; bindings: Bindings }, TypeError> { + // 1. Collect declarations (structs, functions) + const declResult = collectDeclarations(program); + if (!declResult.success) { + return declResult; + } + + // 2. Build initial symbol table with functions and storage + const symbolResult = buildInitialSymbols(program, declResult.value); + if (!symbolResult.success) { + return symbolResult; + } + + // 3. Type check with visitor traversal + const context: Context = { + symbols: symbolResult.value, + structs: declResult.value.structs, + nodeTypes: new Map(), + bindings: emptyBindings(), + visitor: typeChecker, + }; + + const report = Ast.visit(typeChecker, program, context); + + // 4. Return result + if (report.errors.length > 0) { + return Result.err([...report.errors]); // Convert readonly to mutable + } + + return Result.ok({ types: report.nodeTypes, bindings: report.bindings }); +} diff --git a/packages/bugc/src/typechecker/context.ts b/packages/bugc/src/typechecker/context.ts new file mode 100644 index 00000000..9d4fc683 --- /dev/null +++ b/packages/bugc/src/typechecker/context.ts @@ -0,0 +1,50 @@ +import { Type, type Types, type Bindings } from "#types"; +import type { Visitor } from "#ast"; +import type { Declaration } from "./declarations.js"; +import { type Symbols } from "./symbols.js"; +import { type Error as TypeError } from "./errors.js"; + +/** + * Context passed DOWN the tree during type checking. + * Contains environment information needed to type check a node. + */ +export interface Context { + /** Symbol table with all visible symbols at this point */ + readonly symbols: Symbols; + + /** All struct type definitions */ + readonly structs: Map; + + /** Return type of the current function (if inside one) */ + readonly currentReturnType?: Type; + + /** Accumulated type information for nodes */ + readonly nodeTypes: Types; + + /** Accumulated bindings from identifiers to declarations */ + readonly bindings: Bindings; + + /** The visitor itself for recursive calls */ + readonly visitor: Visitor; +} + +/** + * Report passed UP the tree during type checking. + * Contains results and any updates from checking a subtree. + */ +export interface Report { + /** The type of this specific node (if it has one) */ + readonly type?: Type; + + /** Updated symbol table (with any new symbols defined) */ + readonly symbols: Symbols; + + /** Updated node type map (with this node's type added) */ + readonly nodeTypes: Types; + + /** Updated bindings map (with new identifier->declaration mappings) */ + readonly bindings: Bindings; + + /** Any type errors found in this subtree */ + readonly errors: readonly TypeError[]; +} diff --git a/packages/bugc/src/typechecker/declarations.ts b/packages/bugc/src/typechecker/declarations.ts new file mode 100644 index 00000000..60f13b73 --- /dev/null +++ b/packages/bugc/src/typechecker/declarations.ts @@ -0,0 +1,340 @@ +import * as Ast from "#ast"; +import { Type, recordBinding } from "#types"; +import type { Bindings } from "#types"; +import { Result } from "#result"; +import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; +import { computeStructLayout } from "./layout.js"; + +export interface Declarations { + readonly structs: Map; + readonly functions: Map; +} + +export interface Declaration { + node: N; + type: T; +} + +export namespace Declaration { + export type Struct = Declaration; + export type Function = Declaration; +} + +/** + * Collects all type declarations from a program without traversing expressions. + * This includes struct definitions and function signatures. + */ +export function collectDeclarations( + program: Ast.Program, +): Result { + const structs = new Map(); + const functions = new Map(); + const errors: TypeError[] = []; + + // First pass: collect all struct types + for (const decl of program.definitions?.items || []) { + if (Ast.Declaration.isStruct(decl)) { + try { + const structType = buildStructType(decl, structs); + structs.set(decl.name, { + node: decl, + type: structType, + }); + } catch (e) { + if (e instanceof TypeError) { + errors.push(e); + } + } + } + } + + // Second pass: collect function signatures (may reference structs) + for (const decl of program.definitions?.items || []) { + if (Ast.Declaration.isFunction(decl)) { + try { + const funcType = buildFunctionSignature(decl, structs); + functions.set(decl.name, { + node: decl, + type: funcType, + }); + } catch (e) { + if (e instanceof TypeError) { + errors.push(e); + } + } + } + } + + if (errors.length > 0) { + return Result.err(errors); + } + return Result.ok({ structs, functions }); +} + +/** + * Builds a Type.Struct from a struct declaration + */ +function buildStructType( + decl: Ast.Declaration.Struct, + existingStructs: Map, +): Type.Struct { + const fields = new Map(); + + for (const field of decl.fields) { + if (Ast.Declaration.isField(field) && field.type) { + const fieldType = resolveType(field.type, existingStructs); + fields.set(field.name, fieldType); + } + } + + // Compute storage layout for the struct + const layout = computeStructLayout(fields); + + return Type.struct(decl.name, fields, layout); +} + +/** + * Builds a Type.Function from a function declaration + */ +function buildFunctionSignature( + decl: Ast.Declaration.Function, + structTypes: Map, +): Type.Function { + // Resolve parameter types + const parameterTypes: Type[] = []; + for (const param of decl.parameters) { + const paramType = resolveType(param.type, structTypes); + parameterTypes.push(paramType); + } + + // Resolve return type (null for void functions) + const returnType = decl.returnType + ? resolveType(decl.returnType, structTypes) + : null; + + return Type.function_(parameterTypes, returnType, decl.name); +} + +/** + * Resolves an AST type node to a Type object and records bindings + */ +export function resolveTypeWithBindings( + typeNode: Ast.Type, + structTypes: Map, + bindings: Bindings, +): { type: Type; bindings: Bindings } { + if (Ast.Type.isElementary(typeNode)) { + // Map elementary types based on kind and bits + if (Ast.Type.Elementary.isUint(typeNode)) { + const typeMap: Record = { + 256: Type.Elementary.uint(256), + 128: Type.Elementary.uint(128), + 64: Type.Elementary.uint(64), + 32: Type.Elementary.uint(32), + 16: Type.Elementary.uint(16), + 8: Type.Elementary.uint(8), + }; + return { + type: + typeMap[typeNode.bits || 256] || + Type.failure(`Unknown uint size: ${typeNode.bits}`), + bindings, + }; + } + + if (Ast.Type.Elementary.isInt(typeNode)) { + const typeMap: Record = { + 256: Type.Elementary.int(256), + 128: Type.Elementary.int(128), + 64: Type.Elementary.int(64), + 32: Type.Elementary.int(32), + 16: Type.Elementary.int(16), + 8: Type.Elementary.int(8), + }; + return { + type: + typeMap[typeNode.bits || 256] || + Type.failure(`Unknown int size: ${typeNode.bits}`), + bindings, + }; + } + + if (Ast.Type.Elementary.isBytes(typeNode)) { + if (!typeNode.size) { + return { type: Type.Elementary.bytes(), bindings }; // Dynamic bytes + } + // typeNode.bits now contains the byte size directly (e.g., 32 for bytes32) + const validSizes = [4, 8, 16, 32]; + if (validSizes.includes(typeNode.size)) { + return { type: Type.Elementary.bytes(typeNode.size), bindings }; + } else { + return { + type: Type.failure(`Unknown bytes size: ${typeNode.size}`), + bindings, + }; + } + } + if (Ast.Type.Elementary.isAddress(typeNode)) { + return { type: Type.Elementary.address(), bindings }; + } + if (Ast.Type.Elementary.isBool(typeNode)) { + return { type: Type.Elementary.bool(), bindings }; + } + if (Ast.Type.Elementary.isString(typeNode)) { + return { type: Type.Elementary.string(), bindings }; + } + return { + type: Type.failure(`Unknown elementary type: ${typeNode.kind}`), + bindings, + }; + } + + if (Ast.Type.isComplex(typeNode)) { + if (Ast.Type.Complex.isArray(typeNode)) { + const elementResult = resolveTypeWithBindings( + typeNode.element, + structTypes, + bindings, + ); + return { + type: Type.array(elementResult.type, typeNode.size), + bindings: elementResult.bindings, + }; + } + if (Ast.Type.Complex.isMapping(typeNode)) { + const keyResult = resolveTypeWithBindings( + typeNode.key, + structTypes, + bindings, + ); + const valueResult = resolveTypeWithBindings( + typeNode.value, + structTypes, + keyResult.bindings, + ); + return { + type: Type.mapping(keyResult.type, valueResult.type), + bindings: valueResult.bindings, + }; + } + return { + type: Type.failure(`Unsupported complex type: ${typeNode.kind}`), + bindings, + }; + } + + if (Ast.Type.isReference(typeNode)) { + const structType = structTypes.get(typeNode.name); + if (!structType) { + throw new TypeError( + ErrorMessages.UNDEFINED_TYPE(typeNode.name), + typeNode.loc || undefined, + undefined, + undefined, + ErrorCode.UNDEFINED_TYPE, + ); + } + // Record the binding from this type reference to the struct declaration + const updatedBindings = recordBinding( + bindings, + typeNode.id, + structType.node, + ); + return { type: structType.type, bindings: updatedBindings }; + } + + return { type: Type.failure("Unknown type"), bindings }; +} + +/** + * Resolves an AST type node to a Type object (legacy version without bindings) + */ +export function resolveType( + typeNode: Ast.Type, + structTypes: Map, +): Type { + if (Ast.Type.isElementary(typeNode)) { + // Map elementary types based on kind and bits + if (Ast.Type.Elementary.isUint(typeNode)) { + const typeMap: Record = { + 256: Type.Elementary.uint(256), + 128: Type.Elementary.uint(128), + 64: Type.Elementary.uint(64), + 32: Type.Elementary.uint(32), + 16: Type.Elementary.uint(16), + 8: Type.Elementary.uint(8), + }; + return ( + typeMap[typeNode.bits || 256] || + Type.failure(`Unknown uint size: ${typeNode.bits}`) + ); + } + + if (Ast.Type.Elementary.isInt(typeNode)) { + const typeMap: Record = { + 256: Type.Elementary.int(256), + 128: Type.Elementary.int(128), + 64: Type.Elementary.int(64), + 32: Type.Elementary.int(32), + 16: Type.Elementary.int(16), + 8: Type.Elementary.int(8), + }; + return ( + typeMap[typeNode.bits || 256] || + Type.failure(`Unknown int size: ${typeNode.bits}`) + ); + } + + if (Ast.Type.Elementary.isBytes(typeNode)) { + if (!typeNode.size) { + return Type.Elementary.bytes(); // Dynamic bytes + } + // typeNode.bits now contains the byte size directly (e.g., 32 for bytes32) + const validSizes = [4, 8, 16, 32]; + if (validSizes.includes(typeNode.size)) { + return Type.Elementary.bytes(typeNode.size); + } else { + return Type.failure(`Unknown bytes size: ${typeNode.size}`); + } + } + if (Ast.Type.Elementary.isAddress(typeNode)) { + return Type.Elementary.address(); + } + if (Ast.Type.Elementary.isBool(typeNode)) { + return Type.Elementary.bool(); + } + if (Ast.Type.Elementary.isString(typeNode)) { + return Type.Elementary.string(); + } + return Type.failure(`Unknown elementary type: ${typeNode.kind}`); + } + + if (Ast.Type.isComplex(typeNode)) { + if (Ast.Type.Complex.isArray(typeNode)) { + const elementType = resolveType(typeNode.element, structTypes); + return Type.array(elementType, typeNode.size); + } + if (Ast.Type.Complex.isMapping(typeNode)) { + const keyType = resolveType(typeNode.key, structTypes); + const valueType = resolveType(typeNode.value, structTypes); + return Type.mapping(keyType, valueType); + } + return Type.failure(`Unsupported complex type: ${typeNode.kind}`); + } + + if (Ast.Type.isReference(typeNode)) { + const structType = structTypes.get(typeNode.name); + if (!structType) { + throw new TypeError( + ErrorMessages.UNDEFINED_TYPE(typeNode.name), + typeNode.loc || undefined, + undefined, + undefined, + ErrorCode.UNDEFINED_TYPE, + ); + } + return structType.type; + } + + return Type.failure("Unknown type"); +} diff --git a/packages/bugc/src/typechecker/errors.ts b/packages/bugc/src/typechecker/errors.ts new file mode 100644 index 00000000..006e8271 --- /dev/null +++ b/packages/bugc/src/typechecker/errors.ts @@ -0,0 +1,73 @@ +/** + * Type checker errors and error codes + */ + +import { BugError } from "#errors"; +import { Severity } from "#result"; +import type { SourceLocation } from "#ast"; + +/** + * Error codes for type errors + */ +export enum ErrorCode { + TYPE_MISMATCH = "TYPE001", + UNDEFINED_VARIABLE = "TYPE002", + UNDEFINED_TYPE = "TYPE003", + INVALID_OPERATION = "TYPE004", + MISSING_INITIALIZER = "TYPE005", + INVALID_ASSIGNMENT = "TYPE006", + INVALID_CONDITION = "TYPE007", + INVALID_OPERAND = "TYPE008", + NO_SUCH_FIELD = "TYPE009", + INVALID_INDEX_TYPE = "TYPE010", + NOT_INDEXABLE = "TYPE011", + INVALID_ARGUMENT_COUNT = "TYPE012", + INVALID_TYPE_CAST = "TYPE013", + INTERNAL_ERROR = "TYPE014", + GENERAL = "TYPE_ERROR", // Legacy support +} + +/** + * Type error message templates + */ +export const ErrorMessages = { + TYPE_MISMATCH: (expected: string, actual: string) => + `Type mismatch: expected ${expected}, got ${actual}`, + UNDEFINED_VARIABLE: (name: string) => `Undefined variable: ${name}`, + UNDEFINED_TYPE: (name: string) => `Undefined type: ${name}`, + INVALID_UNARY_OP: (op: string, type: string) => + `Operator ${op} requires ${type} operand`, + INVALID_BINARY_OP: (op: string, type: string) => + `Operator ${op} requires ${type} operands`, + NO_SUCH_FIELD: (structName: string, fieldName: string) => + `Struct ${structName} has no field ${fieldName}`, + CANNOT_INDEX: (type: string) => `Cannot index ${type}`, +} as const; + +class TypeError extends BugError { + public readonly expectedType?: string; + public readonly actualType?: string; + + constructor( + message: string, + location?: SourceLocation, + expectedType?: string, + actualType?: string, + code: ErrorCode = ErrorCode.GENERAL, + ) { + super(message, code, location); + this.expectedType = expectedType; + this.actualType = actualType; + } +} + +export { TypeError as Error }; + +export function assertExhausted(_: never): never { + throw new TypeError( + `Unexpected code path; expected exhaustive conditionals`, + undefined, + Severity.Error, + ErrorCode.INTERNAL_ERROR, + ); +} diff --git a/packages/bugc/src/typechecker/expressions.ts b/packages/bugc/src/typechecker/expressions.ts new file mode 100644 index 00000000..a46d9865 --- /dev/null +++ b/packages/bugc/src/typechecker/expressions.ts @@ -0,0 +1,1088 @@ +import * as Ast from "#ast"; +import { Type, recordBinding } from "#types"; +import type { Visitor } from "#ast"; +import type { Context, Report } from "./context.js"; +import { + Error as TypeError, + ErrorCode, + ErrorMessages, + assertExhausted, +} from "./errors.js"; +import { isAssignable, commonType } from "./assignable.js"; + +/** + * Type checker for expression nodes. + * Each expression method computes the type of the expression + * and returns it in the report. + */ +export const expressionChecker: Pick, "expression"> = { + expression(node: Ast.Expression, context: Context): Report { + if (Ast.Expression.isIdentifier(node)) { + const errors: TypeError[] = []; + const nodeTypes = new Map(context.nodeTypes); + let bindings = context.bindings; + + const symbol = context.symbols.lookup(node.name); + if (!symbol) { + const error = new TypeError( + ErrorMessages.UNDEFINED_VARIABLE(node.name), + node.loc || undefined, + undefined, + undefined, + ErrorCode.UNDEFINED_VARIABLE, + ); + errors.push(error); + return { + type: undefined, + symbols: context.symbols, + nodeTypes, + bindings, + errors, + }; + } + + nodeTypes.set(node.id, symbol.type); + // Record the binding from this identifier to its declaration + bindings = recordBinding(bindings, node.id, symbol.declaration); + + return { + type: symbol.type, + symbols: context.symbols, + nodeTypes, + bindings, + errors, + }; + } + + if (Ast.Expression.isLiteral(node)) { + const nodeTypes = new Map(context.nodeTypes); + let type: Type | undefined; + + switch (node.kind) { + case "expression:literal:number": + type = Type.Elementary.uint(256); + break; + case "expression:literal:boolean": + type = Type.Elementary.bool(); + break; + case "expression:literal:string": + type = Type.Elementary.string(); + break; + case "expression:literal:address": + type = Type.Elementary.address(); + break; + case "expression:literal:hex": { + // Determine bytes type based on hex literal length + // Remove 0x prefix if present + const hexValue = node.value.startsWith("0x") + ? node.value.slice(2) + : node.value; + + // Each byte is 2 hex characters + const byteCount = Math.ceil(hexValue.length / 2); + + // For fixed-size bytes types (bytes1 to bytes32) + if (byteCount > 0 && byteCount <= 32) { + type = Type.Elementary.bytes(byteCount); + } else { + // For larger hex literals, use dynamic bytes + type = Type.Elementary.bytes(); + } + break; + } + } + + if (type) { + nodeTypes.set(node.id, type); + } + + return { + type, + symbols: context.symbols, + nodeTypes, + bindings: context.bindings, + errors: [], + }; + } + + if (Ast.Expression.isOperator(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // Type check all operands + const operandTypes: Type[] = []; + for (let i = 0; i < node.operands.length; i++) { + const operandContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const operandResult = Ast.visit( + context.visitor, + node.operands[i], + operandContext, + ); + nodeTypes = operandResult.nodeTypes; + symbols = operandResult.symbols; + bindings = operandResult.bindings; + errors.push(...operandResult.errors); + if (operandResult.type) { + operandTypes.push(operandResult.type); + } + } + + // If any operand failed to type check, bail out + if (operandTypes.length !== node.operands.length) { + return { symbols, nodeTypes, bindings, errors }; + } + + let resultType: Type | undefined; + + if (node.operands.length === 1) { + // Unary operator + const operandType = operandTypes[0]; + + switch (node.operator) { + case "!": + if (!Type.Elementary.isBool(operandType)) { + const error = new TypeError( + ErrorMessages.INVALID_UNARY_OP("!", "boolean"), + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERAND, + ); + errors.push(error); + } + resultType = Type.Elementary.bool(); + break; + + case "-": + if ( + !Type.isElementary(operandType) || + !Type.Elementary.isNumeric(operandType) + ) { + const error = new TypeError( + ErrorMessages.INVALID_UNARY_OP("-", "numeric"), + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERAND, + ); + errors.push(error); + } + resultType = operandType; + break; + + default: { + const error = new TypeError( + `Unknown unary operator: ${node.operator}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + break; + } + } + } else if (node.operands.length === 2) { + // Binary operator + const [leftType, rightType] = operandTypes; + + switch (node.operator) { + case "+": + case "-": + case "*": + case "/": + if ( + !Type.isElementary(leftType) || + !Type.isElementary(rightType) || + !Type.Elementary.isNumeric(leftType) || + !Type.Elementary.isNumeric(rightType) + ) { + const error = new TypeError( + ErrorMessages.INVALID_BINARY_OP(node.operator, "numeric"), + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERAND, + ); + errors.push(error); + } + resultType = commonType(leftType, rightType) || undefined; + break; + + case "<": + case ">": + case "<=": + case ">=": + if ( + !Type.isElementary(leftType) || + !Type.isElementary(rightType) || + !Type.Elementary.isNumeric(leftType) || + !Type.Elementary.isNumeric(rightType) + ) { + const error = new TypeError( + ErrorMessages.INVALID_BINARY_OP(node.operator, "numeric"), + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERAND, + ); + errors.push(error); + } + resultType = Type.Elementary.bool(); + break; + + case "==": + case "!=": + if (!isAssignable(leftType, rightType)) { + const error = new TypeError( + `Cannot compare ${Type.format(leftType)} with ${Type.format(rightType)}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + } + resultType = Type.Elementary.bool(); + break; + + case "&&": + case "||": + if ( + !Type.Elementary.isBool(leftType) || + !Type.Elementary.isBool(rightType) + ) { + const error = new TypeError( + ErrorMessages.INVALID_BINARY_OP(node.operator, "boolean"), + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERAND, + ); + errors.push(error); + } + resultType = Type.Elementary.bool(); + break; + + default: { + const error = new TypeError( + `Unknown binary operator: ${node.operator}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + break; + } + } + } else { + assertExhausted(node.operands); + } + + if (resultType) { + nodeTypes.set(node.id, resultType); + } + + return { + type: resultType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + if (Ast.Expression.isAccess(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // Type check the object being accessed + const objectContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const objectResult = Ast.visit( + context.visitor, + node.object, + objectContext, + ); + nodeTypes = objectResult.nodeTypes; + symbols = objectResult.symbols; + bindings = objectResult.bindings; + errors.push(...objectResult.errors); + + if (!objectResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + const objectType = objectResult.type; + let resultType: Type | undefined; + + if (Ast.Expression.Access.isMember(node)) { + const property = node.property as string; + + if (Type.isStruct(objectType)) { + const fieldType = objectType.fields.get(property); + if (!fieldType) { + const error = new TypeError( + ErrorMessages.NO_SUCH_FIELD(objectType.name, property), + node.loc || undefined, + undefined, + undefined, + ErrorCode.NO_SUCH_FIELD, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + resultType = fieldType; + } else if (property === "length") { + // Handle .length property for arrays and bytes types + if (Type.isArray(objectType)) { + // Array length is always uint256 + resultType = Type.Elementary.uint(256); + } else if ( + Type.isElementary(objectType) && + (Type.Elementary.isBytes(objectType) || + Type.Elementary.isString(objectType)) + ) { + // bytes and string length is uint256 + resultType = Type.Elementary.uint(256); + } else { + const error = new TypeError( + `Type ${Type.format(objectType)} does not have a length property`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + } else { + const error = new TypeError( + `Cannot access member ${property} on ${Type.format(objectType)}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + } else if (Ast.Expression.Access.isSlice(node)) { + // Slice access - start:end + const startExpr = node.start; + const endExpr = node.end; // slice always has end + + const startContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const startResult = Ast.visit(context.visitor, startExpr, startContext); + nodeTypes = startResult.nodeTypes; + symbols = startResult.symbols; + bindings = startResult.bindings; + errors.push(...startResult.errors); + + const endContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const endResult = Ast.visit(context.visitor, endExpr, endContext); + nodeTypes = endResult.nodeTypes; + symbols = endResult.symbols; + bindings = endResult.bindings; + errors.push(...endResult.errors); + + if (!startResult.type || !endResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + // Only bytes types can be sliced for now + if ( + Type.isElementary(objectType) && + Type.Elementary.isBytes(objectType) + ) { + if ( + !Type.isElementary(startResult.type) || + !Type.Elementary.isNumeric(startResult.type) + ) { + const error = new TypeError( + "Slice start index must be numeric", + startExpr.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_INDEX_TYPE, + ); + errors.push(error); + } + if ( + !Type.isElementary(endResult.type) || + !Type.Elementary.isNumeric(endResult.type) + ) { + const error = new TypeError( + "Slice end index must be numeric", + endExpr.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_INDEX_TYPE, + ); + errors.push(error); + } + // Slicing bytes returns dynamic bytes + resultType = Type.Elementary.bytes(); + } else { + const error = new TypeError( + `Cannot slice ${Type.format(objectType)} - only bytes types can be sliced`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + } else { + // Index access + const indexExpr = node.index; + + const indexContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const indexResult = Ast.visit(context.visitor, indexExpr, indexContext); + nodeTypes = indexResult.nodeTypes; + symbols = indexResult.symbols; + bindings = indexResult.bindings; + errors.push(...indexResult.errors); + + if (!indexResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + const indexType = indexResult.type; + + if (Type.isArray(objectType)) { + if ( + !Type.isElementary(indexType) || + !Type.Elementary.isNumeric(indexType) + ) { + const error = new TypeError( + "Array index must be numeric", + indexExpr.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_INDEX_TYPE, + ); + errors.push(error); + } + resultType = objectType.element; + } else if (Type.isMapping(objectType)) { + if (!isAssignable(objectType.key, indexType)) { + const error = new TypeError( + `Invalid mapping key: expected ${Type.format(objectType.key)}, got ${Type.format(indexType)}`, + indexExpr.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + resultType = objectType.value; + } else if ( + Type.isElementary(objectType) && + Type.Elementary.isBytes(objectType) + ) { + // Allow indexing into bytes types - returns uint8 + if (!isAssignable(Type.Elementary.uint(8), indexType)) { + const error = new TypeError( + `Bytes index must be a numeric type, got ${Type.format(indexType)}`, + indexExpr.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + // Bytes indexing returns uint8 + resultType = Type.Elementary.uint(8); + } else { + const error = new TypeError( + ErrorMessages.CANNOT_INDEX(Type.format(objectType)), + node.loc || undefined, + undefined, + undefined, + ErrorCode.NOT_INDEXABLE, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + } + + if (resultType) { + nodeTypes.set(node.id, resultType); + } + + return { + type: resultType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + if (Ast.Expression.isCall(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // Check if this is a built-in function call + if (node.callee.kind === "expression:identifier") { + const functionName = node.callee.name; + + // Handle keccak256 built-in function + if (functionName === "keccak256") { + if (node.arguments.length !== 1) { + const error = new TypeError( + "keccak256 expects exactly 1 argument", + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_ARGUMENT_COUNT, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + const argContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const argResult = Ast.visit( + context.visitor, + node.arguments[0], + argContext, + ); + nodeTypes = argResult.nodeTypes; + symbols = argResult.symbols; + bindings = argResult.bindings; + errors.push(...argResult.errors); + + if (!argResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + // keccak256 accepts bytes types and strings + if ( + !Type.Elementary.isBytes(argResult.type) && + !Type.Elementary.isString(argResult.type) + ) { + const error = new TypeError( + "keccak256 argument must be bytes or string type", + node.arguments[0].loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + // keccak256 returns bytes32 + const resultType = Type.Elementary.bytes(32); + nodeTypes.set(node.id, resultType); + return { + type: resultType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + // Handle user-defined function calls + const symbol = symbols.lookup(functionName); + if (!symbol) { + const error = new TypeError( + ErrorMessages.UNDEFINED_VARIABLE(functionName), + node.callee.loc || undefined, + undefined, + undefined, + ErrorCode.UNDEFINED_VARIABLE, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + // Record binding for the function identifier + bindings = recordBinding(bindings, node.callee.id, symbol.declaration); + + if (!Type.isFunction(symbol.type)) { + const error = new TypeError( + `${functionName} is not a function`, + node.callee.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + const funcType = symbol.type; + + // Check argument count + if (node.arguments.length !== funcType.parameters.length) { + const error = new TypeError( + `Function ${funcType.name} expects ${funcType.parameters.length} arguments but got ${node.arguments.length}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_ARGUMENT_COUNT, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + // Check argument types + for (let i = 0; i < node.arguments.length; i++) { + const argContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const argResult = Ast.visit( + context.visitor, + node.arguments[i], + argContext, + ); + nodeTypes = argResult.nodeTypes; + symbols = argResult.symbols; + bindings = argResult.bindings; + errors.push(...argResult.errors); + + if (!argResult.type) continue; + + const expectedType = funcType.parameters[i]; + if (!isAssignable(expectedType, argResult.type)) { + const error = new TypeError( + `Argument ${i + 1} type mismatch: expected ${Type.format(expectedType)}, got ${Type.format(argResult.type)}`, + node.arguments[i].loc || undefined, + Type.format(expectedType), + Type.format(argResult.type), + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } + + // Return the function's return type + const returnType = funcType.return || Type.failure("void function"); + nodeTypes.set(node.id, returnType); + return { + type: returnType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + // For now, other forms of function calls are not supported + const error = new TypeError( + "Complex function call expressions not yet supported", + node.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_OPERATION, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + if (Ast.Expression.isCast(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // Get the type of the expression being cast + const exprContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const exprResult = Ast.visit( + context.visitor, + node.expression, + exprContext, + ); + nodeTypes = exprResult.nodeTypes; + symbols = exprResult.symbols; + bindings = exprResult.bindings; + errors.push(...exprResult.errors); + + if (!exprResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + // Resolve the target type + const targetTypeContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const targetTypeResult = Ast.visit( + context.visitor, + node.targetType, + targetTypeContext, + ); + nodeTypes = targetTypeResult.nodeTypes; + symbols = targetTypeResult.symbols; + bindings = targetTypeResult.bindings; + errors.push(...targetTypeResult.errors); + + if (!targetTypeResult.type) { + return { symbols, nodeTypes, bindings, errors }; + } + + // Check if the cast is valid + if (!isValidCast(exprResult.type, targetTypeResult.type)) { + const error = new TypeError( + `Cannot cast from ${Type.format(exprResult.type)} to ${Type.format(targetTypeResult.type)}`, + node.loc || undefined, + Type.format(targetTypeResult.type), + Type.format(exprResult.type), + ErrorCode.INVALID_TYPE_CAST, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + // Set the type of the cast expression to the target type + nodeTypes.set(node.id, targetTypeResult.type); + return { + type: targetTypeResult.type, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + if (Ast.Expression.isArray(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // Type check all elements + const elementTypes: Type[] = []; + for (let i = 0; i < node.elements.length; i++) { + const elementContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const elementResult = Ast.visit( + context.visitor, + node.elements[i], + elementContext, + ); + nodeTypes = elementResult.nodeTypes; + symbols = elementResult.symbols; + bindings = elementResult.bindings; + errors.push(...elementResult.errors); + if (elementResult.type) { + elementTypes.push(elementResult.type); + } + } + + // If any element failed to type check, bail out + if (elementTypes.length !== node.elements.length) { + return { symbols, nodeTypes, bindings, errors }; + } + + // Determine common element type + let elementType: Type | undefined; + if (elementTypes.length > 0) { + elementType = elementTypes[0]; + for (let i = 1; i < elementTypes.length; i++) { + const common = commonType(elementType, elementTypes[i]); + if (!common) { + const error = new TypeError( + `Array elements must have compatible types. Element at index ${i} has type ${Type.format(elementTypes[i])} which is incompatible with ${Type.format(elementType)}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + elementType = common; + } + } else { + // Empty array - default to uint256[] + elementType = Type.Elementary.uint(256); + } + + // Create dynamic array type + const arrayType = Type.array(elementType); + nodeTypes.set(node.id, arrayType); + + return { + type: arrayType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + if (Ast.Expression.isStruct(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + // If a struct name is provided, look it up + let structType: Type | undefined; + if (node.structName) { + const symbol = symbols.lookup(node.structName); + if (!symbol || !Type.isStruct(symbol.type)) { + const error = new TypeError( + `${node.structName} is not a struct type`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + structType = symbol.type; + } + + // Type check all fields + const fieldTypes = new Map(); + for (let i = 0; i < node.fields.length; i++) { + const field = node.fields[i]; + const fieldContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const fieldResult = Ast.visit( + context.visitor, + field.value, + fieldContext, + ); + nodeTypes = fieldResult.nodeTypes; + symbols = fieldResult.symbols; + bindings = fieldResult.bindings; + errors.push(...fieldResult.errors); + + if (fieldResult.type) { + if (fieldTypes.has(field.name)) { + const error = new TypeError( + `Duplicate field ${field.name} in struct literal`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } else { + fieldTypes.set(field.name, fieldResult.type); + } + } + } + + // If a struct type was specified, validate fields match + if (structType && Type.isStruct(structType)) { + // Check all required fields are present + for (const [fieldName, fieldType] of structType.fields) { + const providedType = fieldTypes.get(fieldName); + if (!providedType) { + const error = new TypeError( + `Missing field ${fieldName} in struct literal`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } else if (!isAssignable(fieldType, providedType)) { + const error = new TypeError( + `Field ${fieldName} type mismatch: expected ${Type.format(fieldType)}, got ${Type.format(providedType)}`, + node.loc || undefined, + Type.format(fieldType), + Type.format(providedType), + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } + + // Check no extra fields + for (const fieldName of fieldTypes.keys()) { + if (!structType.fields.has(fieldName)) { + const error = new TypeError( + `Unknown field ${fieldName} in struct literal`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } + + nodeTypes.set(node.id, structType); + return { + type: structType, + symbols, + nodeTypes, + bindings, + errors, + }; + } else { + // Create anonymous struct type + // Use empty layout for now since we're creating a memory struct literal + const anonStructType = Type.struct("anonymous", fieldTypes, new Map()); + nodeTypes.set(node.id, anonStructType); + return { + type: anonStructType, + symbols, + nodeTypes, + bindings, + errors, + }; + } + } + + if (Ast.Expression.isSpecial(node)) { + // TODO: Handle special expressions (msg.sender, block.timestamp, etc.) + let type: Type | undefined; + + switch (node.kind) { + case "expression:special:msg.sender": + type = Type.Elementary.address(); + break; + case "expression:special:msg.value": + type = Type.Elementary.uint(256); + break; + case "expression:special:msg.data": + type = Type.Elementary.bytes(); + break; + case "expression:special:block.timestamp": + type = Type.Elementary.uint(256); + break; + case "expression:special:block.number": + type = Type.Elementary.uint(256); + break; + } + + const nodeTypes = new Map(context.nodeTypes); + if (type) { + nodeTypes.set(node.id, type); + } + + return { + type, + symbols: context.symbols, + nodeTypes, + bindings: context.bindings, + errors: [], + }; + } + + throw new Error("Unknown expression kind"); + }, +}; + +/** + * Helper function to check if a cast is valid between two types + */ +function isValidCast(fromType: Type, toType: Type): boolean { + // Allow casting between numeric types + if ( + Type.isElementary(fromType) && + Type.isElementary(toType) && + Type.Elementary.isNumeric(fromType) && + Type.Elementary.isNumeric(toType) + ) { + return true; + } + + // Allow casting from uint256 to address + if (Type.Elementary.isUint(fromType) && Type.Elementary.isAddress(toType)) { + return true; + } + + // Allow casting from address to uint256 + if (Type.Elementary.isAddress(fromType) && Type.Elementary.isUint(toType)) { + return true; + } + + // Allow casting between bytes types + if (Type.Elementary.isBytes(fromType) && Type.Elementary.isBytes(toType)) { + return true; + } + + // Allow casting from string to bytes (for slicing without UTF-8 concerns) + if (Type.Elementary.isString(fromType) && Type.Elementary.isBytes(toType)) { + return true; + } + + // Allow casting from bytes to string (reverse operation) + if (Type.Elementary.isBytes(fromType) && Type.Elementary.isString(toType)) { + return true; + } + + // Allow casting from bytes (including dynamic bytes) to address + if (Type.Elementary.isBytes(fromType) && Type.Elementary.isAddress(toType)) { + return true; + } + + // Allow casting from bytes (including dynamic bytes) to numeric types + if ( + Type.isElementary(fromType) && + Type.isElementary(toType) && + Type.Elementary.isBytes(fromType) && + Type.Elementary.isNumeric(toType) + ) { + return true; + } + + // No other casts are allowed + return false; +} diff --git a/packages/bugc/src/typechecker/index.ts b/packages/bugc/src/typechecker/index.ts new file mode 100644 index 00000000..6b15ed45 --- /dev/null +++ b/packages/bugc/src/typechecker/index.ts @@ -0,0 +1,6 @@ +/** + * Type checker module for BUG language + */ + +export { checkProgram } from "./checker.js"; +export { Error, ErrorCode } from "./errors.js"; diff --git a/packages/bugc/src/typechecker/layout.ts b/packages/bugc/src/typechecker/layout.ts new file mode 100644 index 00000000..428e44b7 --- /dev/null +++ b/packages/bugc/src/typechecker/layout.ts @@ -0,0 +1,107 @@ +import { Type } from "../types/index.js"; + +/** + * Compute storage layout for a struct's fields. + * Implements Solidity-style packing where fields are packed together + * if they fit within 32-byte slots. + */ +export function computeStructLayout( + fields: Map, +): Map { + const layout = new Map(); + let currentSlotOffset = 0; // Byte offset from start of struct + let currentSlotUsed = 0; // Bytes used in current slot + const SLOT_SIZE = 32; + + for (const [fieldName, fieldType] of fields) { + const size = getTypeSize(fieldType); + const isDynamic = isTypeDynamic(fieldType); + + // Dynamic types always start a new slot + if (isDynamic) { + // If we've used any of the current slot, move to next slot + if (currentSlotUsed > 0) { + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } + + layout.set(fieldName, { + byteOffset: currentSlotOffset, + size: SLOT_SIZE, // Dynamic types use full slot for reference + }); + + // Move to next slot + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } else { + // For non-dynamic types, try to pack them + // If this field doesn't fit in the current slot, start a new slot + if (currentSlotUsed + size > SLOT_SIZE) { + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } + + // Place field in current slot + layout.set(fieldName, { + byteOffset: currentSlotOffset + currentSlotUsed, + size: size, + }); + + currentSlotUsed += size; + + // If we've filled the slot exactly, prepare for next slot + if (currentSlotUsed >= SLOT_SIZE) { + currentSlotOffset += SLOT_SIZE; + currentSlotUsed = 0; + } + } + } + + return layout; +} + +/** + * Check if a type is dynamic (requires its own slot). + */ +function isTypeDynamic(type: Type): boolean { + switch (type.kind) { + case "string": + case "array": + case "mapping": + return true; + case "bytes": + // Dynamic bytes (no fixed size) are dynamic + return type.size === undefined; + case "struct": + // Structs are considered dynamic for simplicity + // (in reality, they could be packed if all fields are static) + return true; + default: + return false; + } +} + +/** + * Get the storage size of a type in bytes. + * For storage, values are padded to 32 bytes. + */ +function getTypeSize(type: Type): number { + switch (type.kind) { + case "bool": + return 1; + case "uint": + case "int": + return Math.ceil(type.bits / 8); + case "address": + return 20; + case "bytes": + return type.size || 32; // Fixed-size bytes or dynamic + case "string": + case "array": + case "mapping": + case "struct": + return 32; // Reference types take full slot + default: + return 32; + } +} diff --git a/packages/bugc/src/typechecker/length.test.ts b/packages/bugc/src/typechecker/length.test.ts new file mode 100644 index 00000000..b3dc3591 --- /dev/null +++ b/packages/bugc/src/typechecker/length.test.ts @@ -0,0 +1,261 @@ +import { describe, expect, it } from "vitest"; + +import { parse } from "#parser"; +import { Result, Severity } from "#result"; + +import { checkProgram } from "./checker.js"; + +import "#test/matchers"; + +describe("TypeChecker - Length Property", () => { + it("should type check array.length", () => { + const source = ` + name ArrayLength; + + storage { + [0] arr: array; + [1] len: uint256; + } + + code { + len = arr.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should type check msg.data.length", () => { + const source = ` + name DataLength; + + storage { + [0] dataSize: uint256; + } + + code { + dataSize = msg.data.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should type check length in conditions", () => { + const source = ` + name LengthCondition; + + storage { + [0] arr: array; + [1] hasElements: bool; + } + + code { + if (arr.length > 0) { + hasElements = true; + } + + if (msg.data.length >= 4) { + hasElements = true; + } + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should type check length in loops", () => { + const source = ` + name LengthLoop; + + storage { + [0] arr: array; + } + + code { + for (let i = 0; i < arr.length; i = i + 1) { + arr[i] = i; + } + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should type check string.length", () => { + const source = ` + name StringLength; + + define { + function getStringLen(s: string) -> uint256 { + return s.length; + }; + } + + code { + let len = getStringLen("hello"); + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should fail on length of non-array/bytes/string types", () => { + const source = ` + name InvalidLength; + + storage { + [0] num: uint256; + } + + code { + let len = num.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(false); + + if (!result.success) { + expect(Result.hasErrors(result)).toBe(true); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "does not have a length property", + }); + } + }); + + it("should fail on length of address type", () => { + const source = ` + name AddressLength; + + code { + let len = msg.sender.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(false); + + if (!result.success) { + expect(Result.hasErrors(result)).toBe(true); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "does not have a length property", + }); + } + }); + + it("should fail on length of mapping type", () => { + const source = ` + name MappingLength; + + storage { + [0] balances: mapping; + } + + code { + let len = balances.length; + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(false); + + if (!result.success) { + expect(Result.hasErrors(result)).toBe(true); + expect(result).toHaveMessage({ + severity: Severity.Error, + message: "does not have a length property", + }); + } + }); + + it("should type check nested array length", () => { + const source = ` + name NestedArrayLength; + + storage { + [0] matrix: array, 5>; + [1] rowCount: uint256; + [2] colCount: uint256; + } + + code { + rowCount = matrix.length; // Should be 5 + colCount = matrix[0].length; // Should be 10 + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); + + it("should type check bytes type length variations", () => { + const source = ` + name BytesLengthVariations; + + storage { + [0] fixedBytes: bytes32; + [1] len1: uint256; + [2] len2: uint256; + } + + code { + // Dynamic bytes (msg.data) + len1 = msg.data.length; + + // Fixed bytes don't have .length in our implementation + // This would need to be handled differently if we want to support it + } + `; + + const ast = parse(source); + expect(ast.success).toBe(true); + if (!ast.success) return; + + const result = checkProgram(ast.value); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/bugc/src/typechecker/pass.ts b/packages/bugc/src/typechecker/pass.ts new file mode 100644 index 00000000..9df3f9cd --- /dev/null +++ b/packages/bugc/src/typechecker/pass.ts @@ -0,0 +1,30 @@ +import type { Program } from "#ast"; +import { Result } from "#result"; +import type { Types, Bindings } from "#types"; +import type { Pass } from "#compiler"; + +import type { Error as TypeError } from "./errors.js"; +import { checkProgram } from "./checker.js"; + +/** + * Type checking pass - validates types and builds symbol table + */ +const pass: Pass<{ + needs: { + ast: Program; + }; + adds: { + types: Types; + bindings: Bindings; + }; + error: TypeError; +}> = { + async run({ ast }) { + return Result.map(checkProgram(ast), ({ types, bindings }) => ({ + types, + bindings, + })); + }, +}; + +export default pass; diff --git a/packages/bugc/src/typechecker/slice.test.ts b/packages/bugc/src/typechecker/slice.test.ts new file mode 100644 index 00000000..66dc3fed --- /dev/null +++ b/packages/bugc/src/typechecker/slice.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "vitest"; + +import * as Ast from "#ast"; +import { Type } from "#types"; +import { parse } from "#parser"; +import { Severity } from "#result"; + +import { checkProgram } from "./checker.js"; + +import "#test/matchers"; + +describe("Slice type checking", () => { + test("validates slice of msg.data", () => { + const result = parse(` + name Test; + code { + let slice = msg.data[0:4]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const typeResult = checkProgram(result.value); + expect(typeResult.success).toBe(true); + + if (typeResult.success) { + const { types } = typeResult.value; + const program = result.value; + const statement = program.body?.items[0]; + if (Ast.isStatement(statement) && Ast.Statement.isDeclare(statement)) { + const decl = statement.declaration; + if (!Ast.Declaration.isVariable(decl) || !decl.initializer) { + throw new Error("Expected initializer"); + } + const sliceType = types.get(decl.initializer.id); + if (!sliceType) { + throw new Error("Unexpected missing slice type"); + } + expect(Type.format(sliceType)).toBe("bytes"); + } + } + }); + + test("rejects slice of non-bytes type", () => { + const result = parse(` + name Test; + storage { + [0] numbers: array; + } + code { + let slice = numbers[0:4]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const typeResult = checkProgram(result.value); + expect(typeResult.success).toBe(false); + expect(typeResult).toHaveMessage({ + severity: Severity.Error, + message: "Cannot slice", + }); + }); + + test("validates slice indices are numeric", () => { + const result = parse(` + name Test; + code { + let slice = msg.data["start":"end"]; + } + `); + + expect(result.success).toBe(true); + if (!result.success) throw new Error("Parse failed"); + + const typeResult = checkProgram(result.value); + expect(typeResult.success).toBe(false); + expect(typeResult).toHaveMessage({ + severity: Severity.Error, + message: "Slice start index must be numeric", + }); + expect(typeResult).toHaveMessage({ + severity: Severity.Error, + message: "Slice end index must be numeric", + }); + }); +}); diff --git a/packages/bugc/src/typechecker/statements.ts b/packages/bugc/src/typechecker/statements.ts new file mode 100644 index 00000000..7cefdf16 --- /dev/null +++ b/packages/bugc/src/typechecker/statements.ts @@ -0,0 +1,342 @@ +import * as Ast from "#ast"; +import { Type } from "#types"; +import type { Visitor } from "#ast"; +import type { Context, Report } from "./context.js"; +import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; +import { isAssignable } from "./assignable.js"; + +/** + * Type checker for statement nodes. + * Each statement method handles type checking for that statement type + * and returns an updated report. + */ +export const statementChecker: Pick, "statement"> = { + statement(node: Ast.Statement, context: Context): Report { + if (Ast.Statement.isDeclare(node)) { + // Forward to the declaration visitor method + return Ast.visit(context.visitor, node.declaration, context); + } + if (Ast.Statement.isExpress(node)) { + // Type check the expression (for side effects) + const exprContext: Context = { + ...context, + }; + return Ast.visit(context.visitor, node.expression, exprContext); + } + + if (Ast.Statement.isAssign(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + if (!Ast.Expression.isAssignable(node.target)) { + const error = new TypeError( + "Invalid assignment target", + node.target.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_ASSIGNMENT, + ); + errors.push(error); + return { symbols, nodeTypes, bindings, errors }; + } + + // Type check target + const targetContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const targetResult = Ast.visit( + context.visitor, + node.target, + targetContext, + ); + nodeTypes = targetResult.nodeTypes; + symbols = targetResult.symbols; + bindings = targetResult.bindings; + errors.push(...targetResult.errors); + + // Type check value + const valueContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const valueResult = Ast.visit(context.visitor, node.value, valueContext); + nodeTypes = valueResult.nodeTypes; + symbols = valueResult.symbols; + bindings = valueResult.bindings; + errors.push(...valueResult.errors); + + // Check type compatibility + if ( + targetResult.type && + valueResult.type && + !isAssignable(targetResult.type, valueResult.type) + ) { + const error = new TypeError( + ErrorMessages.TYPE_MISMATCH( + Type.format(targetResult.type), + Type.format(valueResult.type), + ), + node.loc || undefined, + Type.format(targetResult.type), + Type.format(valueResult.type), + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + + return { symbols, nodeTypes, bindings, errors }; + } + + if (Ast.Statement.isControlFlow(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + + switch (node.kind) { + case "statement:control-flow:if": { + // Type check condition + if (node.condition) { + const condContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const condResult = Ast.visit( + context.visitor, + node.condition, + condContext, + ); + nodeTypes = condResult.nodeTypes; + symbols = condResult.symbols; + bindings = condResult.bindings; + errors.push(...condResult.errors); + + if (condResult.type && !Type.Elementary.isBool(condResult.type)) { + const error = new TypeError( + "If condition must be boolean", + node.condition.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_CONDITION, + ); + errors.push(error); + } + } + + // Type check body + if (node.body) { + const bodyContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const bodyResult = Ast.visit( + context.visitor, + node.body, + bodyContext, + ); + nodeTypes = bodyResult.nodeTypes; + symbols = bodyResult.symbols; + bindings = bodyResult.bindings; + errors.push(...bodyResult.errors); + } + + // Type check alternate (else branch) + if (node.alternate) { + const altContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const altResult = Ast.visit( + context.visitor, + node.alternate, + altContext, + ); + nodeTypes = altResult.nodeTypes; + symbols = altResult.symbols; + bindings = altResult.bindings; + errors.push(...altResult.errors); + } + + return { symbols, nodeTypes, bindings, errors }; + } + + case "statement:control-flow:for": { + // Enter new scope for loop + symbols = symbols.enterScope(); + + // Type check init + if (node.init) { + const initContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const initResult = Ast.visit( + context.visitor, + node.init, + initContext, + ); + nodeTypes = initResult.nodeTypes; + symbols = initResult.symbols; + bindings = initResult.bindings; + errors.push(...initResult.errors); + } + + // Type check condition + if (node.condition) { + const condContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const condResult = Ast.visit( + context.visitor, + node.condition, + condContext, + ); + nodeTypes = condResult.nodeTypes; + symbols = condResult.symbols; + bindings = condResult.bindings; + errors.push(...condResult.errors); + + if (condResult.type && !Type.Elementary.isBool(condResult.type)) { + const error = new TypeError( + "For condition must be boolean", + node.condition.loc || undefined, + undefined, + undefined, + ErrorCode.INVALID_CONDITION, + ); + errors.push(error); + } + } + + // Type check update + if (node.update) { + const updateContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const updateResult = Ast.visit( + context.visitor, + node.update, + updateContext, + ); + nodeTypes = updateResult.nodeTypes; + symbols = updateResult.symbols; + bindings = updateResult.bindings; + errors.push(...updateResult.errors); + } + + // Type check body + if (node.body) { + const bodyContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const bodyResult = Ast.visit( + context.visitor, + node.body, + bodyContext, + ); + nodeTypes = bodyResult.nodeTypes; + symbols = bodyResult.symbols; + bindings = bodyResult.bindings; + errors.push(...bodyResult.errors); + } + + // Exit scope (don't propagate local symbols) + symbols = symbols.exitScope(); + + return { symbols, nodeTypes, bindings, errors }; + } + + case "statement:control-flow:return": { + if (node.value) { + // Type check return value + const valueContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const valueResult = Ast.visit( + context.visitor, + node.value, + valueContext, + ); + nodeTypes = valueResult.nodeTypes; + symbols = valueResult.symbols; + bindings = valueResult.bindings; + errors.push(...valueResult.errors); + + if (valueResult.type && context.currentReturnType) { + if (!isAssignable(context.currentReturnType, valueResult.type)) { + const error = new TypeError( + ErrorMessages.TYPE_MISMATCH( + Type.format(context.currentReturnType), + Type.format(valueResult.type), + ), + node.loc || undefined, + Type.format(context.currentReturnType), + Type.format(valueResult.type), + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } else if (valueResult.type && !context.currentReturnType) { + const error = new TypeError( + "Cannot return a value from a void function", + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + } else if (context.currentReturnType) { + const error = new TypeError( + `Function must return a value of type ${Type.format(context.currentReturnType)}`, + node.loc || undefined, + undefined, + undefined, + ErrorCode.TYPE_MISMATCH, + ); + errors.push(error); + } + + return { symbols, nodeTypes, bindings, errors }; + } + + case "statement:control-flow:break": + // No type checking needed for break + return { symbols, nodeTypes, bindings, errors }; + + default: + // Unknown control flow + return { symbols, nodeTypes, bindings, errors }; + } + } + + throw new Error("Unexpected statement"); + }, +}; diff --git a/packages/bugc/src/typechecker/symbols.ts b/packages/bugc/src/typechecker/symbols.ts new file mode 100644 index 00000000..b443feb5 --- /dev/null +++ b/packages/bugc/src/typechecker/symbols.ts @@ -0,0 +1,151 @@ +import * as Ast from "#ast"; +import { Type } from "#types"; +import { Result } from "#result"; +import { resolveType, type Declarations } from "./declarations.js"; +import { Error as TypeError } from "./errors.js"; + +/** + * Immutable symbol table for functional typechecking. + * Each operation returns a new table rather than mutating. + */ +export class Symbols { + constructor( + private readonly scopes: readonly Map[] = [new Map()], + ) {} + + static empty(): Symbols { + return new Symbols(); + } + + enterScope(): Symbols { + return new Symbols([...this.scopes, new Map()]); + } + + exitScope(): Symbols { + if (this.scopes.length <= 1) { + return this; + } + return new Symbols(this.scopes.slice(0, -1)); + } + + define(symbol: BugSymbol): Symbols { + const newScopes = [...this.scopes]; + const lastScope = new Map(newScopes[newScopes.length - 1]); + lastScope.set(symbol.name, symbol); + newScopes[newScopes.length - 1] = lastScope; + return new Symbols(newScopes); + } + + lookup(name: string): BugSymbol | undefined { + // Search from innermost to outermost scope + for (let i = this.scopes.length - 1; i >= 0; i--) { + const symbol = this.scopes[i].get(name); + if (symbol) { + return symbol; + } + } + return undefined; + } + + isDefined(name: string): boolean { + return this.lookup(name) !== undefined; + } + + isDefinedInCurrentScope(name: string): boolean { + const currentScope = this.scopes[this.scopes.length - 1]; + return currentScope.has(name); + } +} + +// Symbol table entry +interface BugSymbol { + name: string; + type: Type; + mutable: boolean; + location: "storage" | "memory" | "builtin"; + slot?: number; // For storage variables + declaration: Ast.Declaration; +} + +export { type BugSymbol as Symbol }; + +/** + * Builds the initial symbol table from declarations without traversing expressions. + * This includes: + * - Function names in global scope + * - Storage variables in global scope + * + * Note: Function parameters and local variables are added during type checking + * when we actually traverse function bodies. + */ +export function buildInitialSymbols( + program: Ast.Program, + { functions, structs }: Declarations, +): Result { + let symbols = Symbols.empty(); + const errors: TypeError[] = []; + + // Add all function signatures to global scope + for (const [name, function_] of functions) { + const symbol: BugSymbol = { + name, + type: function_.type, + mutable: false, + location: "memory", + declaration: function_.node, + }; + + symbols = symbols.define(symbol); + } + + // Add all storage variables to global scope + for (const decl of program.storage || []) { + const type = decl.type + ? resolveType(decl.type, structs) + : Type.failure("missing type"); + + const symbol: BugSymbol = { + name: decl.name, + type, + mutable: true, + location: "storage", + slot: decl.slot, + declaration: decl, + }; + + symbols = symbols.define(symbol); + } + + if (errors.length > 0) { + return Result.err(errors); + } + return Result.ok(symbols); +} + +/** + * Creates a new symbol table scope with function parameters. + * Used when entering a function body during type checking. + */ +export function enterFunctionScope( + symbols: Symbols, + funcDecl: Ast.Declaration.Function, + funcType: Type.Function, +): Symbols { + let newSymbols = symbols.enterScope(); + + // Add parameters to the function scope + const parameters = funcDecl.parameters; + for (let i = 0; i < parameters.length; i++) { + const param = parameters[i]; + const symbol: BugSymbol = { + name: param.name, + type: funcType.parameters[i], + mutable: true, + location: "memory", + declaration: param, + }; + newSymbols = newSymbols.define(symbol); + } + + return newSymbols; +} diff --git a/packages/bugc/src/typechecker/type-nodes.ts b/packages/bugc/src/typechecker/type-nodes.ts new file mode 100644 index 00000000..00ed7f98 --- /dev/null +++ b/packages/bugc/src/typechecker/type-nodes.ts @@ -0,0 +1,183 @@ +import * as Ast from "#ast"; +import { Type } from "#types"; +import type { Visitor } from "#ast"; +import type { Context, Report } from "./context.js"; +import { Error as TypeError, ErrorCode, ErrorMessages } from "./errors.js"; + +/** + * Type checker for type AST nodes. + * These nodes appear in declarations and casts. + * They resolve to Type objects. + */ +export const typeNodeChecker: Pick, "type"> = { + type(node: Ast.Type, context: Context): Report { + if (Ast.Type.isElementary(node)) { + const errors: TypeError[] = []; + const nodeTypes = new Map(context.nodeTypes); + let type: Type | undefined; + + // Map elementary types based on kind and bits + if (Ast.Type.Elementary.isUint(node)) { + const typeMap: Record = { + 256: Type.Elementary.uint(256), + 128: Type.Elementary.uint(128), + 64: Type.Elementary.uint(64), + 32: Type.Elementary.uint(32), + 16: Type.Elementary.uint(16), + 8: Type.Elementary.uint(8), + }; + type = + typeMap[node.bits || 256] || + Type.failure(`Unknown uint size: ${node.bits}`); + } else if (Ast.Type.Elementary.isInt(node)) { + const typeMap: Record = { + 256: Type.Elementary.int(256), + 128: Type.Elementary.int(128), + 64: Type.Elementary.int(64), + 32: Type.Elementary.int(32), + 16: Type.Elementary.int(16), + 8: Type.Elementary.int(8), + }; + type = + typeMap[node.bits || 256] || + Type.failure(`Unknown int size: ${node.bits}`); + } else if (Ast.Type.Elementary.isBytes(node)) { + if (!node.size) { + type = Type.Elementary.bytes(); // Dynamic bytes + } else { + type = Type.Elementary.bytes(node.size); + } + } else if (Ast.Type.Elementary.isAddress(node)) { + type = Type.Elementary.address(); + } else if (Ast.Type.Elementary.isBool(node)) { + type = Type.Elementary.bool(); + } else if (Ast.Type.Elementary.isString(node)) { + type = Type.Elementary.string(); + } else { + type = Type.failure(`Unknown elementary type: ${node.kind}`); + } + + if (type) { + nodeTypes.set(node.id, type); + } + + return { + type, + symbols: context.symbols, + nodeTypes, + bindings: context.bindings, + errors, + }; + } + + if (Ast.Type.isComplex(node)) { + const errors: TypeError[] = []; + let nodeTypes = new Map(context.nodeTypes); + let symbols = context.symbols; + let bindings = context.bindings; + let type: Type | undefined; + + if (Ast.Type.Complex.isArray(node)) { + // Resolve element type + const elementContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const elementResult = Ast.visit( + context.visitor, + node.element, + elementContext, + ); + nodeTypes = elementResult.nodeTypes; + symbols = elementResult.symbols; + bindings = elementResult.bindings; + errors.push(...elementResult.errors); + + if (elementResult.type) { + type = Type.array(elementResult.type, node.size); + } + } else if (Ast.Type.Complex.isMapping(node)) { + // Resolve key type + const keyContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const keyResult = Ast.visit(context.visitor, node.key, keyContext); + nodeTypes = keyResult.nodeTypes; + symbols = keyResult.symbols; + bindings = keyResult.bindings; + errors.push(...keyResult.errors); + + // Resolve value type + const valueContext: Context = { + ...context, + nodeTypes, + symbols, + bindings, + }; + const valueResult = Ast.visit( + context.visitor, + node.value, + valueContext, + ); + nodeTypes = valueResult.nodeTypes; + symbols = valueResult.symbols; + bindings = valueResult.bindings; + errors.push(...valueResult.errors); + + if (keyResult.type && valueResult.type) { + type = Type.mapping(keyResult.type, valueResult.type); + } + } else { + type = Type.failure(`Unsupported complex type: ${node.kind}`); + } + + if (type) { + nodeTypes.set(node.id, type); + } + + return { + type, + symbols, + nodeTypes, + bindings, + errors, + }; + } + + const errors: TypeError[] = []; + const nodeTypes = new Map(context.nodeTypes); + let type: Type | undefined; + + const structType = context.structs.get(node.name); + if (!structType) { + const error = new TypeError( + ErrorMessages.UNDEFINED_TYPE(node.name), + node.loc || undefined, + undefined, + undefined, + ErrorCode.UNDEFINED_TYPE, + ); + errors.push(error); + type = Type.failure(`Undefined struct: ${node.name}`); + } else { + type = structType.type; + } + + if (type) { + nodeTypes.set(node.id, type); + } + + return { + type, + symbols: context.symbols, + nodeTypes, + bindings: context.bindings, + errors, + }; + }, +}; diff --git a/packages/bugc/src/types/analysis/formatter.ts b/packages/bugc/src/types/analysis/formatter.ts new file mode 100644 index 00000000..b9d86e6a --- /dev/null +++ b/packages/bugc/src/types/analysis/formatter.ts @@ -0,0 +1,300 @@ +import type * as Ast from "#ast"; +import { Analysis as AstAnalysis } from "#ast"; +import { Type, type Types, type Bindings } from "#types/spec"; + +export class Formatter { + private output: string[] = []; + private indent = 0; + private source?: string; + + format(types: Types, bindings: Bindings, source?: string): string { + this.output = []; + this.indent = 0; + this.source = source; + + this.line("=== Type Information ==="); + this.line(""); + + // Group types by their AST node kind + const groupedTypes = this.groupByNodeKind(types); + + // Format each group + for (const [kind, entries] of groupedTypes) { + this.formatGroup(kind, entries); + } + + // Format bindings + if (bindings.size > 0) { + this.line("=== Bindings Information ==="); + this.line(""); + this.formatBindings(bindings); + } + + return this.output.join("\n"); + } + + private groupByNodeKind(types: Types): Map> { + const groups = new Map>(); + + for (const [id, type] of types) { + const kind = this.getNodeKind(id); + if (!groups.has(kind)) { + groups.set(kind, []); + } + groups.get(kind)!.push([id, type]); + } + + // Sort groups for consistent output + return new Map( + [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)), + ); + } + + private getNodeKind(_id: Ast.Id): string { + // Extract the node kind from the ID + // IDs are numeric with underscore separator like "31_1" + // For now, group all as "TypedExpressions" + return "TypedExpressions"; + } + + private formatGroup(kind: string, entries: Array<[Ast.Id, Type]>) { + this.line(`${kind}:`); + this.indent++; + + // Sort entries by their position in the source + const sortedEntries = [...entries].sort(([a], [b]) => { + const posA = this.extractPosition(a); + const posB = this.extractPosition(b); + if (posA.line !== posB.line) { + return posA.line - posB.line; + } + return posA.col - posB.col; + }); + + for (const [id, type] of sortedEntries) { + this.formatEntry(id, type); + } + + this.indent--; + this.line(""); + } + + private extractPosition(id: Ast.Id): { line: number; col: number } { + // IDs are in format like "31_1" (byteOffset_length) + const parts = id.split("_"); + const byteOffset = parseInt(parts[0] || "0", 10); + + // Convert byte offset to line/column if we have source + if (this.source) { + const { line, col } = AstAnalysis.offsetToLineCol( + this.source, + byteOffset, + ); + return { line, col }; + } + + // Fallback: just use byte offset as line for sorting + return { line: byteOffset, col: 0 }; + } + + private formatEntry(id: Ast.Id, type: Type) { + // IDs are in format like "offset_length" + const parts = id.split("_"); + const offset = parseInt(parts[0] || "0", 10); + const length = parseInt(parts[1] || "0", 10); + + let position: string; + if (this.source) { + const start = AstAnalysis.offsetToLineCol(this.source, offset); + const end = AstAnalysis.offsetToLineCol(this.source, offset + length); + + if (start.line === end.line) { + // Same line: show as line:col1-col2 + position = `${start.line}:${start.col}-${end.col}`; + } else { + // Multiple lines: show full range + position = `${start.line}:${start.col}-${end.line}:${end.col}`; + } + } else { + position = `offset ${offset}, length ${length}`; + } + + // Format the type using the built-in formatter + const typeStr = Type.format(type); + + // Build the entry line + const entry = `${position}: ${typeStr}`; + + // Add additional details for complex types + if (Type.isStruct(type)) { + this.line(entry); + this.indent++; + this.formatStructDetails(type); + this.indent--; + } else if (Type.isFunction(type)) { + this.line(entry); + this.indent++; + this.formatFunctionDetails(type); + this.indent--; + } else if (Type.isArray(type)) { + this.line(entry); + this.indent++; + this.formatArrayDetails(type); + this.indent--; + } else if (Type.isMapping(type)) { + this.line(entry); + this.indent++; + this.formatMappingDetails(type); + this.indent--; + } else { + this.line(entry); + } + } + + private formatStructDetails(struct: Type.Struct) { + this.line("fields:"); + this.indent++; + for (const [fieldName, fieldType] of struct.fields) { + const layout = struct.layout.get(fieldName); + const layoutStr = layout + ? ` [offset: ${layout.byteOffset}, size: ${layout.size}]` + : ""; + this.line(`${fieldName}: ${Type.format(fieldType)}${layoutStr}`); + } + this.indent--; + } + + private formatFunctionDetails(func: Type.Function) { + if (func.parameters.length > 0) { + this.line("parameters:"); + this.indent++; + func.parameters.forEach((param, index) => { + this.line(`[${index}]: ${Type.format(param)}`); + }); + this.indent--; + } + + if (func.return !== null) { + this.line(`returns: ${Type.format(func.return)}`); + } else { + this.line("returns: void"); + } + } + + private formatArrayDetails(array: Type.Array) { + this.line(`element type: ${Type.format(array.element)}`); + if (array.size !== undefined) { + this.line(`size: ${array.size}`); + } else { + this.line("size: dynamic"); + } + } + + private formatMappingDetails(mapping: Type.Mapping) { + this.line(`key type: ${Type.format(mapping.key)}`); + this.line(`value type: ${Type.format(mapping.value)}`); + } + + private line(text: string) { + const indentStr = " ".repeat(this.indent); + this.output.push(indentStr + text); + } + + private formatBindings(bindings: Bindings) { + // Group bindings by declaration + const byDeclaration = new Map(); + for (const [id, decl] of bindings) { + if (!byDeclaration.has(decl)) { + byDeclaration.set(decl, []); + } + byDeclaration.get(decl)!.push(id); + } + + // Sort declarations by their position + const sortedDeclarations = [...byDeclaration.entries()].sort( + ([declA], [declB]) => { + const posA = this.extractPosition(declA.id); + const posB = this.extractPosition(declB.id); + if (posA.line !== posB.line) { + return posA.line - posB.line; + } + return posA.col - posB.col; + }, + ); + + this.line("Identifier Bindings:"); + this.indent++; + + for (const [decl, identifierIds] of sortedDeclarations) { + // Format the declaration location and type + const declPos = this.formatPosition(decl.id); + const declType = this.getDeclarationType(decl); + const declName = this.getDeclarationName(decl); + + this.line(`${declType} "${declName}" at ${declPos}:`); + this.indent++; + + // Sort and format all references to this declaration + const sortedIds = identifierIds.sort((a, b) => { + const posA = this.extractPosition(a); + const posB = this.extractPosition(b); + if (posA.line !== posB.line) { + return posA.line - posB.line; + } + return posA.col - posB.col; + }); + + for (const id of sortedIds) { + const refPos = this.formatPosition(id); + this.line(`referenced at ${refPos}`); + } + + this.indent--; + } + + this.indent--; + } + + private formatPosition(id: Ast.Id): string { + const parts = id.split("_"); + const offset = parseInt(parts[0] || "0", 10); + const length = parseInt(parts[1] || "0", 10); + + if (this.source) { + const start = AstAnalysis.offsetToLineCol(this.source, offset); + const end = AstAnalysis.offsetToLineCol(this.source, offset + length); + + if (start.line === end.line) { + return `${start.line}:${start.col}-${end.col}`; + } else { + return `${start.line}:${start.col}-${end.line}:${end.col}`; + } + } else { + return `offset ${offset}, length ${length}`; + } + } + + private getDeclarationType(decl: Ast.Declaration): string { + switch (decl.kind) { + case "declaration:variable": + return "Variable"; + case "declaration:function": + return "Function"; + case "declaration:storage": + return "Storage"; + case "declaration:struct": + return "Struct"; + case "declaration:field": + return "Field"; + default: + return "Declaration"; + } + } + + private getDeclarationName(decl: Ast.Declaration): string { + if ("name" in decl && typeof decl.name === "string") { + return decl.name; + } + return ""; + } +} diff --git a/packages/bugc/src/types/analysis/index.ts b/packages/bugc/src/types/analysis/index.ts new file mode 100644 index 00000000..bfeea41b --- /dev/null +++ b/packages/bugc/src/types/analysis/index.ts @@ -0,0 +1 @@ +export { Formatter } from "./formatter.js"; diff --git a/packages/bugc/src/types/index.ts b/packages/bugc/src/types/index.ts new file mode 100644 index 00000000..9a1e43df --- /dev/null +++ b/packages/bugc/src/types/index.ts @@ -0,0 +1,20 @@ +/** + * Type system for the BUG language + * + * This module contains the core type definitions used throughout the compiler. + * It is separate from the typechecker to allow other modules to use types + * without depending on the checking logic. + */ + +// Re-export all type definitions +export { + Type, + type Types, + type Bindings, + emptyBindings, + recordBinding, + mergeBindings, +} from "./spec.js"; + +// Export analysis tools +export * as Analysis from "./analysis/index.js"; diff --git a/packages/bugc/src/types/spec.ts b/packages/bugc/src/types/spec.ts new file mode 100644 index 00000000..44d013e1 --- /dev/null +++ b/packages/bugc/src/types/spec.ts @@ -0,0 +1,474 @@ +/** + * Type definitions for the BUG language + */ +import type * as Ast from "#ast"; + +export type Types = Map; + +export type Type = + | Type.Elementary + | Type.Array + | Type.Struct + | Type.Mapping + | Type.Function + | Type.Failure; + +export const isType = (type: unknown): type is Type => + Type.isBase(type) && + [ + Type.isElementary, + Type.isArray, + Type.isStruct, + Type.isMapping, + Type.isFunction, + Type.isFailure, + ].some((guard) => guard(type)); + +const Array_ = Array; + +export namespace Type { + export interface Base { + kind: string; + } + + export const equals = (a: Type, b: Type): boolean => { + if (a.kind !== b.kind) { + return false; + } + + if (Type.isElementary(a) && Type.isElementary(b)) { + return Type.Elementary.equals(a, b); + } + + const map = { + array: Type.Array.equals, + struct: Type.Struct.equals, + mapping: Type.Mapping.equals, + function: Type.Function.equals, + failure: Type.Failure.equals, + } as const; + + // @ts-expect-error typing this is too tricky + return map[a.kind](a, b); + }; + + export const format = (type: Type): string => { + if (Type.isElementary(type)) { + return Type.Elementary.format(type); + } + + const map = { + array: Type.Array, + struct: Type.Struct, + mapping: Type.Mapping, + function: Type.Function, + failure: Type.Failure, + } as const; + + // @ts-expect-error typing this is too tricky + return map[type.kind].format(type); + }; + + export const isBase = (type: unknown): type is Type.Base => + typeof type === "object" && + !!type && + "kind" in type && + typeof type.kind === "string" && + !!type.kind; + + export type Elementary = + | Type.Elementary.Uint + | Type.Elementary.Int + | Type.Elementary.Address + | Type.Elementary.Bool + | Type.Elementary.Bytes + | Type.Elementary.String; + + export const isElementary = (type: Type.Base): type is Type.Elementary => + [ + Type.Elementary.isUint, + Type.Elementary.isInt, + Type.Elementary.isAddress, + Type.Elementary.isBool, + Type.Elementary.isBytes, + Type.Elementary.isString, + ].some((guard) => guard(type)); + + export namespace Elementary { + export const equals = (a: Type.Elementary, b: Type.Elementary): boolean => { + if (a.kind !== b.kind) { + return false; + } + + const map = { + uint: Type.Elementary.Uint.equals, + int: Type.Elementary.Int.equals, + address: Type.Elementary.Address.equals, + bytes: Type.Elementary.Bytes.equals, + bool: Type.Elementary.Bool.equals, + string: Type.Elementary.String.equals, + } as const; + + // @ts-expect-error typing this is too tricky + return map[a.kind](a, b); + }; + + export const format = (type: T): string => { + const map = { + uint: Type.Elementary.Uint, + int: Type.Elementary.Int, + address: Type.Elementary.Address, + bytes: Type.Elementary.Bytes, + bool: Type.Elementary.Bool, + string: Type.Elementary.String, + } as const; + + // @ts-expect-error typing this is needlessly tricky + return map[type.kind].format(type); + }; + + export interface Uint { + kind: "uint"; + bits: number; + } + + export const uint = (bits: number): Type.Elementary.Uint => ({ + kind: "uint", + bits, + }); + + export namespace Uint { + export const equals = ( + a: Type.Elementary.Uint, + b: Type.Elementary.Uint, + ): boolean => a.bits === b.bits; + + export const format = (type: Type.Elementary.Uint): string => + `uint${type.bits}`; + } + + export interface Int { + kind: "int"; + bits: number; + } + + export const int = (bits: number): Type.Elementary.Int => ({ + kind: "int", + bits, + }); + + export namespace Int { + export const equals = ( + a: Type.Elementary.Int, + b: Type.Elementary.Int, + ): boolean => a.bits === b.bits; + + export const format = (type: Type.Elementary.Int): string => + `int${type.bits}`; + } + + export interface Address { + kind: "address"; + } + + export const address = (): Type.Elementary.Address => ({ + kind: "address", + }); + + export namespace Address { + export const equals = ( + _a: Type.Elementary.Address, + _b: Type.Elementary.Address, + ): boolean => true; + + export const format = (_type: Type.Elementary.Address): string => + `address`; + } + + export interface Bool { + kind: "bool"; + } + + export const bool = (): Type.Elementary.Bool => ({ + kind: "bool", + }); + + export namespace Bool { + export const equals = ( + _a: Type.Elementary.Bool, + _b: Type.Elementary.Bool, + ): boolean => true; + + export const format = (_type: Type.Elementary.Bool): string => `bool`; + } + + export interface Bytes { + kind: "bytes"; + size?: number; + } + + export const bytes = (size?: number): Type.Elementary.Bytes => ({ + kind: "bytes", + size, + }); + + export namespace Bytes { + export const isDynamic = ( + type: Type.Elementary.Bytes, + ): type is Type.Elementary.Bytes & { size?: undefined } => + !("size" in type) || type.size === undefined; + + export const equals = ( + a: Type.Elementary.Bytes, + b: Type.Elementary.Bytes, + ): boolean => a.size == b.size; + + export const format = (type: Type.Elementary.Bytes) => + `bytes${ + "size" in type && typeof type.size === "number" + ? type.size.toString() + : "" + }`; + } + + export interface String { + kind: "string"; + } + + export const string = (): Type.Elementary.String => ({ + kind: "string", + }); + + export namespace String { + export const equals = ( + _a: Type.Elementary.String, + _b: Type.Elementary.String, + ): boolean => true; + + export const format = (_type: Type.Elementary.String): string => `string`; + } + + const makeIsKind = + (kind: K) => + (type: Type.Base): type is Type.Base & { kind: K } => + type.kind === kind; + + export const isUint = makeIsKind("uint" as const); + export const isInt = makeIsKind("int" as const); + export const isAddress = makeIsKind("address" as const); + export const isBool = makeIsKind("bool" as const); + export const isBytes = makeIsKind("bytes" as const); + export const isString = makeIsKind("string" as const); + + export const isNumeric = (type: Type.Elementary) => + Type.Elementary.isUint(type) || Type.Elementary.isInt(type); + } + + export interface Array { + kind: "array"; + element: Type; + size?: number; + } + + export const isArray = (type: Type.Base): type is Type.Array => + type.kind === "array" && "element" in type && isType(type.element); + + export const array = (element: Type, size?: number): Type.Array => ({ + kind: "array", + element, + size, + }); + + export namespace Array { + export const equals = (a: Type.Array, b: Type.Array): boolean => + Type.equals(a.element, b.element) && a.size === b.size; + + export const format = (type: Type.Array): string => + `array<${Type.format(type.element)}${ + "size" in type && typeof type.size === "number" ? `, ${type.size}` : "" + }>`; + } + + export interface Mapping { + kind: "mapping"; + key: Type; + value: Type; + } + + export const mapping = (key: Type, value: Type): Type.Mapping => ({ + kind: "mapping", + key, + value, + }); + + export const isMapping = (type: Type.Base): type is Type.Mapping => + type.kind === "mapping" && + "key" in type && + "value" in type && + isType(type.key) && + isType(type.value); + + export namespace Mapping { + export const equals = (a: Type.Mapping, b: Type.Mapping): boolean => + Type.equals(a.key, b.key) && Type.equals(a.value, b.value); + + export const format = (type: Type.Mapping): string => + `mapping<${Type.format(type.key)}, ${Type.format(type.value)}>`; + } + + export interface FieldLayout { + byteOffset: number; // Byte offset from struct start + size: number; // Size in bytes + } + + export interface Struct { + kind: "struct"; + name: string; + fields: Map; + layout: Map; + } + + export const isStruct = (type: Type.Base): type is Type.Struct => + type.kind === "struct" && + "name" in type && + typeof type.name === "string" && + "fields" in type && + type.fields instanceof Map && + [...type.fields.values()].every(isType); + + export const struct = ( + name: string, + fields: Map, + layout: Map, + ): Type.Struct => ({ + kind: "struct", + name, + fields, + layout, + }); + + export namespace Struct { + export const equals = (a: Type.Struct, b: Type.Struct): boolean => + a.name === b.name && + a.fields.size == b.fields.size && + [...a.fields.entries()].every( + ([keyA, valueA], index) => + [...b.fields.keys()][index] === keyA && + [...b.fields.values()][index] === valueA, + ); + + export const format = (type: Type.Struct): string => type.name; + } + + export interface Function { + kind: "function"; + name?: string; + parameters: Type[]; + return: Type | null; // null for void functions + } + + export const function_ = ( + parameters: Type[], + return_: Type | null, + name?: string, + ): Type.Function => ({ + kind: "function", + parameters, + return: return_, + name, + }); + + export const isFunction = (type: Type.Base): type is Type.Function => + type.kind === "function" && + "parameters" in type && + type.parameters instanceof Array_ && + type.parameters.every(isType) && + "return" in type && + (type.return === null || isType(type.return)); + + export namespace Function { + export const equals = (a: Type.Function, b: Type.Function): boolean => + a.parameters.length === b.parameters.length && + a.parameters.every((type, index) => type == b.parameters[index]) && + ((a.return === null && b.return === null) || + (a.return !== null && + b.return !== null && + Type.equals(a.return, b.return))); + + export const format = (type: Type.Function): string => + `function ${"name" in type ? type.name : ""}(${type.parameters + .map((parameter) => Type.format(parameter)) + .join(", ")})${ + type.return !== null ? `-> ${Type.format(type.return)}` : "" + }`; + } + + export interface Failure { + kind: "fail"; + reason: string; + } + + export const failure = (reason: string): Type.Failure => ({ + kind: "fail", + reason, + }); + + export const isFailure = (type: Type.Base): type is Type.Failure => + type.kind === "fail" && + "reason" in type && + typeof type.reason === "string" && + !!type.reason; + + export namespace Failure { + export const equals = (a: Type.Failure, b: Type.Failure): boolean => + a.reason === b.reason; + + export const format = (type: Type.Failure): string => + `fail<"${type.reason}">`; + } +} + +/** + * Bindings map identifier AST nodes to their declaration sites. + * + * This is a flat, global mapping that records where each identifier + * in the program was declared. Unlike the symbol table, this is not + * scope-aware - it's just a simple lookup table. + * + * The keys are AST IDs of identifier nodes (where symbols are used). + * The values are declaration nodes (where symbols are declared). + */ +export type Bindings = Map; + +/** + * Create an empty bindings map + */ +export function emptyBindings(): Bindings { + return new Map(); +} + +/** + * Record a binding from an identifier use to its declaration + */ +export function recordBinding( + bindings: Bindings, + identifierId: Ast.Id, + declaration: Ast.Declaration, +): Bindings { + const updated = new Map(bindings); + updated.set(identifierId, declaration); + return updated; +} + +/** + * Merge multiple bindings maps + */ +export function mergeBindings(...bindingMaps: Bindings[]): Bindings { + const merged = new Map(); + for (const bindings of bindingMaps) { + for (const [id, decl] of bindings) { + merged.set(id, decl); + } + } + return merged; +} diff --git a/packages/bugc/test/debug-addr.ts b/packages/bugc/test/debug-addr.ts new file mode 100644 index 00000000..2ae750e6 --- /dev/null +++ b/packages/bugc/test/debug-addr.ts @@ -0,0 +1,48 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +async function main() { + const source = ` +name TestAddr; + +storage { + [0] val: uint256; +} + +create { + val = 123; +} + +code { + val = 456; +} +`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed"); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + console.log("Has create bytecode:", hasCreate); + + await executor.deploy(createCode); + + console.log("\nAfter deploy (should be 123):"); + console.log(" slot 0:", await executor.getStorage(0n)); + + await executor.execute({ data: "" }); + + console.log("\nAfter call (should be 456):"); + console.log(" slot 0:", await executor.getStorage(0n)); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-arrays.ts b/packages/bugc/test/debug-arrays.ts new file mode 100644 index 00000000..1aa67f24 --- /dev/null +++ b/packages/bugc/test/debug-arrays.ts @@ -0,0 +1,58 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import { keccak256 } from "ethereum-cryptography/keccak"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function main() { + const source = readFileSync( + resolve(__dirname, "../../../examples/intermediate/arrays.bug"), + "utf-8", + ); + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed"); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + await executor.deploy(createCode); + await executor.execute({ data: "" }); + + // Calculate keccak256(0) for array base + const slotBytes = new Uint8Array(32); + slotBytes[31] = 0; + const hash = keccak256(slotBytes); + const keccakSlot = BigInt("0x" + bytesToHex(hash)); + + console.log("Checking keccak-based array storage:"); + let sum = 0n; + for (let i = 0n; i < 10n; i++) { + const val = await executor.getStorage(keccakSlot + i); + console.log(` numbers[${i}] at keccak(0)+${i}: ${val}`); + sum += val; + } + console.log("Sum of array elements:", sum); + + // Check scalar values at their declared slots + console.log("\nScalar storage (at declared slots):"); + console.log(" slot 1 (sum):", await executor.getStorage(1n)); + console.log(" slot 2 (max):", await executor.getStorage(2n)); + console.log(" slot 3 (count):", await executor.getStorage(3n)); + console.log(" slot 4 (found):", await executor.getStorage(4n)); + console.log(" slot 5 (searchValue):", await executor.getStorage(5n)); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-arrlen.ts b/packages/bugc/test/debug-arrlen.ts new file mode 100644 index 00000000..bb334f4c --- /dev/null +++ b/packages/bugc/test/debug-arrlen.ts @@ -0,0 +1,53 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import { keccak256 } from "ethereum-cryptography/keccak"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function main() { + const source = readFileSync( + resolve(__dirname, "../../../examples/basic/array-length.bug"), + "utf-8", + ); + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed"); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + console.log("Has create bytecode:", hasCreate); + + await executor.deploy(createCode); + + // Calculate keccak256(0) for array base + const slotBytes = new Uint8Array(32); + slotBytes[31] = 0; + const hash = keccak256(slotBytes); + const keccakSlot = BigInt("0x" + bytesToHex(hash)); + + console.log( + "\nArray elements after DEPLOY (should be 0,2,4,6,8,10,12,14,16,18):", + ); + for (let i = 0n; i < 10n; i++) { + const val = await executor.getStorage(keccakSlot + i); + console.log(` fixedArray[${i}]: ${val}`); + } + + console.log("\nScalar values:"); + console.log(" arraySize (slot 1):", await executor.getStorage(1n)); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-loop-create.ts b/packages/bugc/test/debug-loop-create.ts new file mode 100644 index 00000000..0bf9c3e9 --- /dev/null +++ b/packages/bugc/test/debug-loop-create.ts @@ -0,0 +1,42 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +async function main() { + const source = ` +name LoopCreate; + +storage { + [0] counter: uint256; +} + +create { + counter = 0; + for (let i = 0; i < 5; i = i + 1) { + counter = counter + 1; + } +} +`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed"); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + console.log("Create bytecode:", createCode); + + await executor.deploy(createCode); + + console.log("\nAfter deploy (should be 5):"); + console.log(" slot 0 (counter):", await executor.getStorage(0n)); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-loop.ts b/packages/bugc/test/debug-loop.ts new file mode 100644 index 00000000..eadb9f9b --- /dev/null +++ b/packages/bugc/test/debug-loop.ts @@ -0,0 +1,55 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +async function main() { + const source = ` +name SimpleLoop; + +storage { + [0] counter: uint256; + [1] result: uint256; +} + +code { + counter = 0; + result = 99; + + for (let i = 0; i < 5; i = i + 1) { + counter = counter + 1; + } +} +`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed:", result); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + console.log("Bytecode:", bytesToHex(create ?? runtime)); + + await executor.deploy(createCode); + await executor.execute({ data: "" }); + + console.log("\nStorage after call:"); + console.log( + " slot 0 (counter):", + await executor.getStorage(0n), + "(expected: 5)", + ); + console.log( + " slot 1 (result):", + await executor.getStorage(1n), + "(expected: 99)", + ); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-noloop.ts b/packages/bugc/test/debug-noloop.ts new file mode 100644 index 00000000..43f7d1d8 --- /dev/null +++ b/packages/bugc/test/debug-noloop.ts @@ -0,0 +1,44 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +async function main() { + const source = ` +name NoLoop; + +storage { + [0] a: uint256; + [1] b: uint256; + [2] c: uint256; +} + +code { + a = 10; + b = 20; + c = a + b; +} +`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed:", result); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + await executor.deploy(createCode); + await executor.execute({ data: "" }); + + console.log("\nStorage after call:"); + console.log(" slot 0 (a):", await executor.getStorage(0n), "(expected: 10)"); + console.log(" slot 1 (b):", await executor.getStorage(1n), "(expected: 20)"); + console.log(" slot 2 (c):", await executor.getStorage(2n), "(expected: 30)"); +} + +main().catch(console.error); diff --git a/packages/bugc/test/debug-simple.ts b/packages/bugc/test/debug-simple.ts new file mode 100644 index 00000000..eb17c1b1 --- /dev/null +++ b/packages/bugc/test/debug-simple.ts @@ -0,0 +1,62 @@ +import { bytecodeSequence, buildSequence } from "../src/compiler/index.js"; +import { EvmExecutor } from "./evm/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils"; +import { keccak256 } from "ethereum-cryptography/keccak"; + +async function main() { + // Simpler test case + const source = ` +name SimpleArrayTest; + +storage { + [0] arr: array; + [10] result: uint256; +} + +code { + arr[0] = 100; + arr[1] = 200; + result = 42; +} +`; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + if (!result.success) { + console.log("Compilation failed:", result); + return; + } + + const executor = new EvmExecutor(); + const { runtime, create } = result.value.bytecode; + const hasCreate = create && create.length > 0; + const createCode = hasCreate ? bytesToHex(create) : bytesToHex(runtime); + + await executor.deploy(createCode); + await executor.execute({ data: "" }); + + // Calculate keccak256(0) - slot for dynamic array element 0 + const slotBytes = new Uint8Array(32); + slotBytes[31] = 0; // slot 0 + const hash = keccak256(slotBytes); + const keccakSlot = BigInt("0x" + bytesToHex(hash)); + + console.log("keccak256(0) =", keccakSlot.toString(16)); + + // Check storage at keccak256(0) slots + console.log("\nStorage at keccak-based slots:"); + console.log( + " keccak256(0) + 0:", + await executor.getStorage(keccakSlot + 0n), + ); + console.log( + " keccak256(0) + 1:", + await executor.getStorage(keccakSlot + 1n), + ); + + // Also check result + console.log("\nSlot 10 (result):", await executor.getStorage(10n)); +} + +main().catch(console.error); diff --git a/packages/bugc/test/evm/evm-executor.ts b/packages/bugc/test/evm/evm-executor.ts new file mode 100644 index 00000000..125f7646 --- /dev/null +++ b/packages/bugc/test/evm/evm-executor.ts @@ -0,0 +1,272 @@ +/** + * EVM Executor for testing generated bytecode + * Uses @ethereumjs/evm for in-process execution + */ + +/* eslint-disable no-console */ + +import { EVM } from "@ethereumjs/evm"; +import { SimpleStateManager } from "@ethereumjs/statemanager"; +import { Common, Mainnet } from "@ethereumjs/common"; +import { Address, Account } from "@ethereumjs/util"; +import { hexToBytes, bytesToHex } from "ethereum-cryptography/utils"; + +export interface ExecutionOptions { + value?: bigint; + data?: string; + origin?: Address; + caller?: Address; + gasLimit?: bigint; +} + +export interface ExecutionResult { + success: boolean; + gasUsed: bigint; + returnValue: Uint8Array; + logs: unknown[]; + error?: unknown; +} + +interface ExecResult { + exceptionError?: unknown; + executionGasUsed?: bigint; + returnValue?: Uint8Array; + logs?: unknown[]; +} + +interface ResultWithExec extends ExecResult { + execResult?: ExecResult; +} + +export class EvmExecutor { + private evm: EVM; + private stateManager: SimpleStateManager; + private contractAddress: Address; + private deployerAddress: Address; + + constructor() { + const common = new Common({ + chain: Mainnet, + hardfork: "shanghai", + }); + this.stateManager = new SimpleStateManager(); + this.evm = new EVM({ + common, + stateManager: this.stateManager, + }); + + // Use a fixed contract address for testing + this.contractAddress = new Address( + hexToBytes("1234567890123456789012345678901234567890"), + ); + + // Use a fixed deployer address + this.deployerAddress = new Address( + hexToBytes("0000000000000000000000000000000000000001"), + ); + } + + /** + * Get the deployer address used for deployment + */ + getDeployerAddress(): Address { + return this.deployerAddress; + } + + /** + * Deploy bytecode to the test contract address + */ + async deploy(bytecode: string): Promise { + // Execute the constructor bytecode to get the runtime bytecode + const code = hexToBytes(bytecode); + + // Initialize deployer account + const deployerAccount = new Account(0n, BigInt(10) ** BigInt(18)); + await this.stateManager.putAccount(this.deployerAddress, deployerAccount); + + // Initialize contract account before execution + const contractAccount = new Account(0n, 0n); + await this.stateManager.putAccount(this.contractAddress, contractAccount); + + // Use runCall with empty to address to simulate CREATE + const result = await this.evm.runCall({ + caller: this.deployerAddress, + origin: this.deployerAddress, + to: undefined, // undefined 'to' means contract creation + data: code, + gasLimit: 10_000_000n, + value: 0n, + }); + + const error = result.execResult?.exceptionError; + + if (error) { + console.error("Raw error:", error); + throw new Error(`Deployment failed: ${JSON.stringify(error)}`); + } + + // Get the created contract address + const createdAddress = result.createdAddress; + if (createdAddress) { + // Update our contract address to the created one + this.contractAddress = createdAddress; + } + } + + /** + * Execute deployed bytecode + */ + async execute( + options: ExecutionOptions = {}, + trace = false, + ): Promise { + const runCallOpts = { + to: this.contractAddress, + caller: options.caller ?? this.deployerAddress, + origin: options.origin ?? this.deployerAddress, + data: options.data ? hexToBytes(options.data) : new Uint8Array(), + value: options.value ?? 0n, + gasLimit: options.gasLimit ?? 10_000_000n, + }; + + if (trace) { + this.evm.events.on( + "step", + (step: { pc: number; opcode: { name: string }; stack: bigint[] }) => { + console.log( + `[TRACE] PC=${step.pc.toString(16).padStart(4, "0")} ${step.opcode.name} stack=[${step.stack + .slice(-3) + .map((s) => s.toString(16)) + .join(", ")}]`, + ); + }, + ); + } + + const result = await this.evm.runCall(runCallOpts); + + if (trace) { + this.evm.events.removeAllListeners("step"); + } + + // Access the execution result from the returned object + const rawResult = result as ResultWithExec; + const execResult = (rawResult.execResult || rawResult) as ExecResult; + + return { + success: execResult.exceptionError === undefined, + gasUsed: execResult.executionGasUsed || 0n, + returnValue: execResult.returnValue || new Uint8Array(), + logs: execResult.logs || [], + error: execResult.exceptionError, + }; + } + + /** + * Execute bytecode directly (without deployment) + */ + async executeCode( + bytecode: string, + options: ExecutionOptions = {}, + ): Promise { + const code = hexToBytes(bytecode); + + // For storage operations to work, we need an account context + // Create a temporary account with the code + const tempAddress = new Address( + hexToBytes("9999999999999999999999999999999999999999"), + ); + await this.stateManager.putCode(tempAddress, code); + await this.stateManager.putAccount(tempAddress, new Account(0n, 0n)); + + const runCodeOpts = { + code, + data: options.data ? hexToBytes(options.data) : new Uint8Array(), + gasLimit: options.gasLimit ?? 10_000_000n, + value: options.value ?? 0n, + origin: options.origin ?? new Address(Buffer.alloc(20)), + caller: options.caller ?? new Address(Buffer.alloc(20)), + address: tempAddress, // Add the address context + }; + + const result = await this.evm.runCode(runCodeOpts); + + // Access the execution result from the returned object + const rawResult = result as ResultWithExec; + const execResult = (rawResult.execResult || rawResult) as ExecResult; + + return { + success: execResult.exceptionError === undefined, + gasUsed: execResult.executionGasUsed || 0n, + returnValue: execResult.returnValue || new Uint8Array(), + logs: execResult.logs || [], + error: execResult.exceptionError, + }; + } + + /** + * Get storage value at a specific slot + */ + async getStorage(slot: bigint): Promise { + const slotBuffer = Buffer.alloc(32); + + // Convert bigint to hex string and pad to 64 characters (32 bytes) + const hex = slot.toString(16).padStart(64, "0"); + slotBuffer.write(hex, "hex"); + + const value = await this.stateManager.getStorage( + this.contractAddress, + slotBuffer, + ); + + // Convert Uint8Array to bigint + if (value.length === 0) return 0n; + return BigInt("0x" + bytesToHex(value)); + } + + /** + * Set storage value at a specific slot + */ + async setStorage(slot: bigint, value: bigint): Promise { + const slotBuffer = Buffer.alloc(32); + slotBuffer.writeBigUInt64BE(slot, 24); + + const valueBuffer = Buffer.alloc(32); + const hex = value.toString(16).padStart(64, "0"); + valueBuffer.write(hex, "hex"); + + await this.stateManager.putStorage( + this.contractAddress, + slotBuffer, + valueBuffer, + ); + } + + /** + * Get the code at the contract address + */ + async getCode(): Promise { + return this.stateManager.getCode(this.contractAddress); + } + + /** + * Get the contract address + */ + getContractAddress(): Address { + return this.contractAddress; + } + + /** + * Reset the EVM state + */ + async reset(): Promise { + this.stateManager = new SimpleStateManager(); + this.evm = new EVM({ + common: new Common({ + chain: Mainnet, + hardfork: "shanghai", + }), + stateManager: this.stateManager, + }); + } +} diff --git a/packages/bugc/test/evm/index.ts b/packages/bugc/test/evm/index.ts new file mode 100644 index 00000000..9a8197b2 --- /dev/null +++ b/packages/bugc/test/evm/index.ts @@ -0,0 +1,2 @@ +export { EvmExecutor } from "./evm-executor.js"; +export type { ExecutionOptions, ExecutionResult } from "./evm-executor.js"; diff --git a/packages/bugc/test/examples/annotations.ts b/packages/bugc/test/examples/annotations.ts new file mode 100644 index 00000000..3beb4cf8 --- /dev/null +++ b/packages/bugc/test/examples/annotations.ts @@ -0,0 +1,207 @@ +/** + * Test Block Parser + * + * Parses fenced YAML test blocks from .bug source files. + * Format: multi-line comment starting with @test, containing YAML. + */ + +import YAML from "yaml"; + +export interface VariableExpectation { + pointer?: unknown; // Expected pointer structure + value?: string | number | bigint; // Expected dereferenced scalar value + values?: RegionValues; // Expected values by region name + type?: unknown; // Expected type (future use) +} + +// Region values can be: +// - An object mapping region names to values/arrays +// - e.g., { length: 3, element: [100, 200, 300] } +export type RegionValues = Record< + string, + string | number | bigint | (string | number | bigint)[] +>; + +export interface VariablesTest { + atLine: number; + after?: "deploy" | "call"; // When to check values (default: deploy) + callData?: string; // Call data if after: call + variables: Record; +} + +export interface TestBlock { + name?: string; + raw: string; + parsed: VariablesTest; + expectFail?: string; // If set, test is expected to fail with this reason +} + +/** + * Determine which block (create or code) contains the given offset. + * Returns "deploy" for create block, "call" for code block. + */ +function inferAfterFromOffset( + source: string, + offset: number, +): "deploy" | "call" { + // Find create { ... } and code { ... } block boundaries + // Simple approach: find last occurrence of "create {" or "code {" before offset + const beforeOffset = source.slice(0, offset); + + const lastCreate = beforeOffset.lastIndexOf("create {"); + const lastCode = beforeOffset.lastIndexOf("code {"); + + // If we're after "code {" and it comes after "create {", we're in code block + if (lastCode > lastCreate) { + return "call"; + } + + // Otherwise assume create block (or default to deploy) + return "deploy"; +} + +/** + * Find the last non-empty, non-comment line before a given offset. + * This is used for "at: here" to find the code line above the test block. + */ +function findPrecedingCodeLine(source: string, offset: number): number { + // Get all text up to the offset + const textBefore = source.slice(0, offset); + + // Remove all multi-line comments (including nested test blocks) + // Replace with equivalent newlines to preserve line numbers + const withoutBlockComments = textBefore.replace( + /\/\*[\s\S]*?\*\//g, + (match) => match.replace(/[^\n]/g, " "), + ); + + const lines = withoutBlockComments.split("\n"); + + // Walk backwards to find the last line with actual code + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i].trim(); + // Skip empty lines and single-line comments + if (line && !line.startsWith("//")) { + return i + 1; // 1-indexed line numbers + } + } + + // Fallback to the line before the block + return Math.max(1, lines.length); +} + +/** + * Remove common leading indentation from a multi-line string. + * Also strips JSDoc-style " * " prefixes from lines. + */ +function dedent(text: string): string { + let lines = text.split("\n"); + + // Strip JSDoc-style " * " prefix from each line + const allHaveJsdocPrefix = lines.every( + (line) => !line.trim() || /^\s*\*\s?/.test(line), + ); + if (allHaveJsdocPrefix) { + lines = lines.map((line) => line.replace(/^\s*\*\s?/, "")); + } + + // Find minimum indentation (ignoring empty lines) + let minIndent = Infinity; + for (const line of lines) { + if (line.trim()) { + const indent = line.match(/^(\s*)/)?.[1].length ?? 0; + minIndent = Math.min(minIndent, indent); + } + } + + if (minIndent === Infinity || minIndent === 0) { + return lines.join("\n"); + } + + // Remove the common indentation + return lines.map((line) => line.slice(minIndent)).join("\n"); +} + +/** + * Parse all test blocks from a source file. + */ +export function parseTestBlocks(source: string): TestBlock[] { + const blocks: TestBlock[] = []; + + // Match /*@test or /**@test style blocks + const regex = /\/\*\*?@test\s*(\S*)?\n([\s\S]*?)\*\//g; + + let match; + while ((match = regex.exec(source)) !== null) { + const name = match[1] || undefined; + const yamlContent = dedent(match[2]).trim(); + const blockStartOffset = match.index; + + try { + const parsed = YAML.parse(yamlContent); + + // Must have (at-line or at: here) and variables + if (!isValidTest(parsed)) { + continue; + } + + const expectFail = extractExpectFail(parsed); + + blocks.push({ + name, + raw: yamlContent, + parsed: normalizeTest(parsed, source, blockStartOffset), + expectFail, + }); + } catch { + // Skip malformed test blocks + } + } + + return blocks; +} + +function isValidTest(parsed: unknown): boolean { + if (typeof parsed !== "object" || parsed === null) { + return false; + } + const obj = parsed as Record; + // Only require variables; location defaults to "here" (preceding line) + return "variables" in obj; +} + +function extractExpectFail( + parsed: Record, +): string | undefined { + if ("fails" in parsed) { + return typeof parsed.fails === "string" ? parsed.fails : "expected failure"; + } + return undefined; +} + +function normalizeTest( + parsed: Record, + source: string, + blockOffset: number, +): VariablesTest { + // Default to preceding code line ("here" behavior) + // Can override with explicit "at-line: N" + const atLine = + "at-line" in parsed + ? (parsed["at-line"] as number) + : findPrecedingCodeLine(source, blockOffset); + + // Default "after" based on which block the test is in + // create {} -> deploy, code {} -> call + const after = + "after" in parsed + ? (parsed.after as "deploy" | "call") + : inferAfterFromOffset(source, blockOffset); + + return { + atLine, + after, + callData: parsed["call-data"] as string | undefined, + variables: parsed.variables as Record, + }; +} diff --git a/packages/bugc/test/examples/examples.test.ts b/packages/bugc/test/examples/examples.test.ts new file mode 100644 index 00000000..fc7a0af9 --- /dev/null +++ b/packages/bugc/test/examples/examples.test.ts @@ -0,0 +1,244 @@ +/** + * Example Files Test Suite + * + * Automatically discovers and tests all .bug example files. + * + * Supports annotations for test behavior: + * // @wip - Skip test (work in progress) + * // @skip Reason - Skip with reason + * // @expect-parse-error - Expected to fail parsing + * // @expect-typecheck-error - Expected to fail typechecking + * // @expect-ir-error - Expected to fail IR generation + * // @expect-bytecode-error - Expected to fail bytecode generation + * + * Supports fenced YAML test blocks (see annotations.ts for format). + */ + +import { describe, it, expect } from "vitest"; +import { promises as fs } from "fs"; +import path from "path"; +import { glob } from "glob"; +import type * as Format from "@ethdebug/format"; + +import { bytecodeSequence, buildSequence } from "#compiler"; +import { Result } from "#result"; +import type { Instruction } from "#evm"; + +import { + parseTestBlocks, + type TestBlock, + type VariablesTest, +} from "./annotations.js"; +import { buildSourceMapping } from "./source-map.js"; +import { runVariablesTest } from "./runners.js"; + +const EXAMPLES_DIR = path.resolve(__dirname, "../../examples"); + +interface ExampleAnnotations { + wip: boolean; + skip: string | false; + expectParseError: boolean; + expectTypecheckError: boolean; + expectIrError: boolean; + expectBytecodeError: boolean; +} + +interface CompiledBytecode { + runtime: Uint8Array; + create?: Uint8Array; + runtimeInstructions: Instruction[]; + createInstructions?: Instruction[]; + runtimeProgram: Format.Program; + createProgram?: Format.Program; +} + +interface ExampleInfo { + relativePath: string; + fullPath: string; + source: string; + annotations: ExampleAnnotations; + testBlocks: TestBlock[]; +} + +// Cache for compiled examples +const compilationCache = new Map< + string, + { success: true; bytecode: CompiledBytecode } | { success: false } +>(); + +function parseAnnotations(source: string): ExampleAnnotations { + return { + wip: source.includes("// @wip"), + skip: source.match(/\/\/ @skip\s*(.*)/)?.[1] || false, + expectParseError: source.includes("// @expect-parse-error"), + expectTypecheckError: source.includes("// @expect-typecheck-error"), + expectIrError: source.includes("// @expect-ir-error"), + expectBytecodeError: source.includes("// @expect-bytecode-error"), + }; +} + +async function loadExamples(): Promise { + const files = await glob("**/*.bug", { cwd: EXAMPLES_DIR }); + const examples: ExampleInfo[] = []; + + for (const relativePath of files.sort()) { + const fullPath = path.join(EXAMPLES_DIR, relativePath); + const source = await fs.readFile(fullPath, "utf-8"); + + examples.push({ + relativePath, + fullPath, + source, + annotations: parseAnnotations(source), + testBlocks: parseTestBlocks(source), + }); + } + + return examples; +} + +function shouldSkip(annotations: ExampleAnnotations): boolean { + return annotations.wip || !!annotations.skip; +} + +function skipSuffix(annotations: ExampleAnnotations): string { + if (annotations.skip) return ` (skip: ${annotations.skip})`; + if (annotations.wip) return " (wip)"; + return ""; +} + +async function compileExample( + example: ExampleInfo, +): Promise<{ success: true; bytecode: CompiledBytecode } | { success: false }> { + const cached = compilationCache.get(example.relativePath); + if (cached) return cached; + + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source: example.source }); + + const cacheEntry = result.success + ? { success: true as const, bytecode: result.value.bytecode } + : { success: false as const }; + + compilationCache.set(example.relativePath, cacheEntry); + return cacheEntry; +} + +describe("Example Files", async () => { + const examples = await loadExamples(); + + // Collect variables tests + const variablesTests: Array<{ + example: ExampleInfo; + block: TestBlock; + test: VariablesTest; + }> = []; + + for (const example of examples) { + for (const block of example.testBlocks) { + variablesTests.push({ + example, + block, + test: block.parsed, + }); + } + } + + // === Compilation Tests === + describe("Compilation", () => { + for (const example of examples) { + const { relativePath, source, annotations } = example; + const skip = shouldSkip(annotations); + const itFn = skip ? it.skip : it; + + itFn(`${relativePath}${skipSuffix(annotations)}`, async () => { + const compiler = buildSequence(bytecodeSequence); + const result = await compiler.run({ source }); + + const expectAnyError = + annotations.expectParseError || + annotations.expectTypecheckError || + annotations.expectIrError || + annotations.expectBytecodeError; + + if (expectAnyError) { + expect(result.success).toBe(false); + } else { + if (!result.success) { + const errors = Result.errors(result); + const errorMessages = errors + .map( + (e) => `${e.code || "ERROR"}: ${e.message || "Unknown error"}`, + ) + .join("\n"); + expect.fail( + `Expected compilation to succeed but got errors:\n${errorMessages}`, + ); + } + expect(result.success).toBe(true); + + compilationCache.set(relativePath, { + success: true, + bytecode: result.value.bytecode, + }); + } + }); + } + }); + + // === Runtime Assertions === + if (variablesTests.length > 0) { + const byFile = new Map(); + for (const entry of variablesTests) { + const key = entry.example.relativePath; + if (!byFile.has(key)) byFile.set(key, []); + byFile.get(key)!.push(entry); + } + + describe("Runtime", () => { + for (const [relativePath, tests] of byFile) { + const { annotations, source } = tests[0].example; + const skip = shouldSkip(annotations); + const describeFn = skip ? describe.skip : describe; + + describeFn(`${relativePath}${skipSuffix(annotations)}`, () => { + for (const { example, block, test } of tests) { + const baseName = block.name || `line ${test.atLine}`; + + // Skip tests with expectFail annotation + if (block.expectFail) { + it.skip(`${baseName} (known issue: ${block.expectFail})`, () => { + // Skipped - known issue + }); + continue; + } + + it(baseName, async () => { + const compiled = await compileExample(example); + if (!compiled.success) { + throw new Error("Compilation failed - cannot run test"); + } + + const mapping = buildSourceMapping( + source, + compiled.bytecode.runtimeInstructions, + ); + + const result = await runVariablesTest( + compiled.bytecode, + compiled.bytecode.runtimeInstructions, + mapping, + test, + compiled.bytecode.runtimeProgram.context, + ); + + if (!result.passed) { + expect.fail(result.message); + } + }); + } + }); + } + }); + } +}); diff --git a/packages/bugc/test/examples/index.ts b/packages/bugc/test/examples/index.ts new file mode 100644 index 00000000..60e908b4 --- /dev/null +++ b/packages/bugc/test/examples/index.ts @@ -0,0 +1,8 @@ +/** + * Example Test Utilities + */ + +export * from "./annotations.js"; +export * from "./source-map.js"; +export * from "./runners.js"; +export * from "./machine-adapter.js"; diff --git a/packages/bugc/test/examples/machine-adapter.ts b/packages/bugc/test/examples/machine-adapter.ts new file mode 100644 index 00000000..2b1e50fe --- /dev/null +++ b/packages/bugc/test/examples/machine-adapter.ts @@ -0,0 +1,83 @@ +/** + * Machine.State Adapter for EvmExecutor + * + * Implements the @ethdebug/pointers Machine.State interface + * wrapping our EvmExecutor for pointer evaluation. + */ + +import type { Machine } from "@ethdebug/pointers"; +import { Data } from "@ethdebug/pointers"; +import type { EvmExecutor } from "../evm/index.js"; + +/** + * Create a Machine.State from an EvmExecutor. + * + * This adapter allows using @ethdebug/pointers dereference() + * to evaluate pointers against our EVM executor's storage state. + * + * Note: Only storage is fully implemented. Stack, memory, etc. + * return empty/zero values since we only have end-state access. + */ +export function createMachineState(executor: EvmExecutor): Machine.State { + return { + // Trace info (not meaningful for end-state) + traceIndex: Promise.resolve(0n), + programCounter: Promise.resolve(0n), + opcode: Promise.resolve("STOP"), + + // Stack - not available in end-state + stack: { + length: Promise.resolve(0n), + peek: async (): Promise => Data.zero(), + }, + + // Memory - not available in end-state + memory: { + length: Promise.resolve(0n), + read: async (): Promise => Data.zero(), + }, + + // Storage - fully implemented via executor + storage: { + async read({ slot, slice }): Promise { + const slotValue = slot.asUint(); + const value = await executor.getStorage(slotValue); + const data = Data.fromUint(value); + + if (slice) { + const padded = data.padUntilAtLeast(32); + const sliced = new Uint8Array(padded).slice( + Number(slice.offset), + Number(slice.offset + slice.length), + ); + return Data.fromBytes(sliced); + } + + return data.padUntilAtLeast(32); + }, + }, + + // Calldata - not available + calldata: { + length: Promise.resolve(0n), + read: async (): Promise => Data.zero(), + }, + + // Returndata - not available + returndata: { + length: Promise.resolve(0n), + read: async (): Promise => Data.zero(), + }, + + // Code - not available + code: { + length: Promise.resolve(0n), + read: async (): Promise => Data.zero(), + }, + + // Transient storage - not available + transient: { + read: async (): Promise => Data.zero(), + }, + }; +} diff --git a/packages/bugc/test/examples/runners.ts b/packages/bugc/test/examples/runners.ts new file mode 100644 index 00000000..b8e113f9 --- /dev/null +++ b/packages/bugc/test/examples/runners.ts @@ -0,0 +1,374 @@ +/** + * Test Runner + * + * Execute variable tests against compiled bytecode. + * Checks pointer structure and/or dereferenced values at source lines. + */ + +import type * as Evm from "#evm"; +import * as Format from "@ethdebug/format"; +import { dereference } from "@ethdebug/pointers"; +import { bytesToHex } from "ethereum-cryptography/utils"; + +import { EvmExecutor } from "../evm/index.js"; +import type { VariablesTest } from "./annotations.js"; +import type { SourceMapping } from "./source-map.js"; +import { findInstructionsAtLine } from "./source-map.js"; +import { createMachineState } from "./machine-adapter.js"; + +export interface TestResult { + passed: boolean; + message?: string; + expected?: unknown; + actual?: unknown; +} + +function toHex(bytes: Uint8Array): string { + return bytesToHex(bytes); +} + +/** + * Run a variables test: check pointer structure and/or dereferenced values. + */ +export async function runVariablesTest( + bytecode: { runtime: Uint8Array; create?: Uint8Array }, + instructions: Evm.Instruction[], + sourceMapping: SourceMapping, + test: VariablesTest, + programContext?: Format.Program.Context, +): Promise { + // Collect variables from program-level context first (storage variables) + const variables = new Map(); + + if (programContext) { + const vars = extractVariables(programContext); + for (const v of vars) { + if (v.identifier && v.pointer) { + variables.set(v.identifier, v.pointer); + } + } + } + + // Find instructions at the source line and augment with local variables + const instrIndices = findInstructionsAtLine(sourceMapping, test.atLine); + + for (const idx of instrIndices) { + const instr = instructions[idx]; + const context = instr.debug?.context; + + if (!context) { + continue; + } + + const vars = extractVariables(context); + for (const v of vars) { + if (v.identifier && v.pointer) { + variables.set(v.identifier, v.pointer); + } + } + } + + // Check each expected variable + for (const [name, expected] of Object.entries(test.variables)) { + const pointer = variables.get(name); + + if (!pointer) { + return { + passed: false, + message: `Variable "${name}" not found at line ${test.atLine}`, + }; + } + + // Check pointer structure if specified + if (expected.pointer !== undefined) { + const match = deepEqual(pointer, expected.pointer); + if (!match) { + return { + passed: false, + message: + `Variable "${name}" pointer mismatch\n` + + ` expected: ${JSON.stringify(expected.pointer)}\n` + + ` actual: ${JSON.stringify(pointer)}`, + expected: expected.pointer, + actual: pointer, + }; + } + } + + // Check dereferenced value if specified (scalar - first region) + if (expected.value !== undefined) { + const result = await checkScalarValue( + bytecode, + pointer, + name, + expected.value, + test.after, + test.callData, + ); + if (!result.passed) { + return result; + } + } + + // Check dereferenced values by region name if specified + if (expected.values !== undefined) { + const result = await checkRegionValues( + bytecode, + pointer, + name, + expected.values, + test.after, + test.callData, + ); + if (!result.passed) { + return result; + } + } + } + + return { passed: true }; +} + +/** + * Deploy contract and check a scalar value (first region). + */ +async function checkScalarValue( + bytecode: { runtime: Uint8Array; create?: Uint8Array }, + pointer: Format.Pointer, + name: string, + expectedValue: string | number | bigint, + after: "deploy" | "call" = "deploy", + callData?: string, +): Promise { + const executor = new EvmExecutor(); + + try { + // Deploy + const hasCreate = bytecode.create && bytecode.create.length > 0; + const createCode = hasCreate + ? toHex(bytecode.create!) + : toHex(bytecode.runtime); + await executor.deploy(createCode); + + // Call if needed + if (after === "call") { + const execResult = await executor.execute({ data: callData || "" }); + if (!execResult.success) { + return { + passed: false, + message: `Execution failed: ${JSON.stringify(execResult.error)}`, + }; + } + } + + // Dereference the pointer + const state = createMachineState(executor); + const cursor = await dereference(pointer, { state }); + const view = await cursor.view(state); + + if (view.regions.length === 0) { + return { + passed: false, + message: `No regions for pointer of variable "${name}"`, + }; + } + + const data = await view.read(view.regions[0]); + const actual = data.asUint(); + const expected = BigInt(expectedValue); + + if (actual !== expected) { + return { + passed: false, + message: `Variable "${name}": expected ${expected}, got ${actual}`, + expected, + actual, + }; + } + + return { passed: true }; + } catch (error) { + return { + passed: false, + message: + `Failed to evaluate "${name}": ` + + `${error instanceof Error ? error.message : String(error)}`, + }; + } +} + +/** + * Deploy contract and check values by region name. + * values is an object like { length: 3, element: [100, 200, 300] } + */ +async function checkRegionValues( + bytecode: { runtime: Uint8Array; create?: Uint8Array }, + pointer: Format.Pointer, + varName: string, + expectedValues: Record< + string, + string | number | bigint | (string | number | bigint)[] + >, + after: "deploy" | "call" = "deploy", + callData?: string, +): Promise { + const executor = new EvmExecutor(); + + try { + // Deploy + const hasCreate = bytecode.create && bytecode.create.length > 0; + const createCode = hasCreate + ? toHex(bytecode.create!) + : toHex(bytecode.runtime); + await executor.deploy(createCode); + + // Call if needed + if (after === "call") { + const execResult = await executor.execute({ data: callData || "" }); + if (!execResult.success) { + return { + passed: false, + message: `Execution failed: ${JSON.stringify(execResult.error)}`, + }; + } + } + + // Dereference the pointer + const state = createMachineState(executor); + const cursor = await dereference(pointer, { state }); + const view = await cursor.view(state); + + // Check each expected region + for (const [regionName, expectedValue] of Object.entries(expectedValues)) { + const regions = view.regions.named(regionName); + + if (regions.length === 0) { + return { + passed: false, + message: `Variable "${varName}": no regions named "${regionName}"`, + }; + } + + if (Array.isArray(expectedValue)) { + // Check multiple regions with same name + if (regions.length !== expectedValue.length) { + return { + passed: false, + message: + `Variable "${varName}.${regionName}": expected ` + + `${expectedValue.length} regions, got ${regions.length}`, + }; + } + + for (let i = 0; i < expectedValue.length; i++) { + const data = await view.read(regions[i]); + const actual = data.asUint(); + const expected = BigInt(expectedValue[i]); + + if (actual !== expected) { + return { + passed: false, + message: + `Variable "${varName}.${regionName}[${i}]": ` + + `expected ${expected}, got ${actual}`, + expected, + actual, + }; + } + } + } else { + // Check single region + const data = await view.read(regions[0]); + const actual = data.asUint(); + const expected = BigInt(expectedValue); + + if (actual !== expected) { + return { + passed: false, + message: + `Variable "${varName}.${regionName}": ` + + `expected ${expected}, got ${actual}`, + expected, + actual, + }; + } + } + } + + return { passed: true }; + } catch (error) { + return { + passed: false, + message: + `Failed to evaluate "${varName}": ` + + `${error instanceof Error ? error.message : String(error)}`, + }; + } +} + +/** + * Extract variables from a context (handles gather/pick/variables variants). + */ +function extractVariables( + context: Format.Program.Context, +): Format.Program.Context.Variables.Variable[] { + const result: Format.Program.Context.Variables.Variable[] = []; + + if (Format.Program.Context.isVariables(context)) { + result.push(...context.variables); + } + + if (Format.Program.Context.isGather(context)) { + for (const child of context.gather) { + result.push(...extractVariables(child)); + } + } + + if (Format.Program.Context.isPick(context)) { + for (const option of context.pick) { + result.push(...extractVariables(option)); + } + } + + return result; +} + +/** + * Deep equality check for pointer structures. + */ +function deepEqual(a: unknown, b: unknown): boolean { + if (a === b) { + return true; + } + + if (typeof a !== typeof b) { + return false; + } + + if (typeof a !== "object" || a === null || b === null) { + return false; + } + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + return a.every((val, i) => deepEqual(val, b[i])); + } + + if (Array.isArray(a) !== Array.isArray(b)) { + return false; + } + + const aKeys = Object.keys(a as Record); + const bKeys = Object.keys(b as Record); + + if (aKeys.length !== bKeys.length) { + return false; + } + + const aObj = a as Record; + const bObj = b as Record; + + return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); +} diff --git a/packages/bugc/test/examples/source-map.ts b/packages/bugc/test/examples/source-map.ts new file mode 100644 index 00000000..5a5899bc --- /dev/null +++ b/packages/bugc/test/examples/source-map.ts @@ -0,0 +1,170 @@ +/** + * Source Line Mapping + * + * Maps source code lines to bytecode instruction indices. + */ + +import type * as Evm from "#evm"; +import * as Format from "@ethdebug/format"; + +export interface SourceMapping { + /** Map from source line number (1-indexed) to instruction indices */ + lineToInstructions: Map; + /** Byte offset for each instruction index */ + instructionOffsets: number[]; +} + +/** + * Build a mapping from source lines to instruction indices. + */ +export function buildSourceMapping( + source: string, + instructions: Evm.Instruction[], +): SourceMapping { + const lineOffsets = buildLineOffsets(source); + const lineToInstructions = new Map(); + const instructionOffsets = computeInstructionOffsets(instructions); + + for (let i = 0; i < instructions.length; i++) { + const instr = instructions[i]; + const context = instr.debug?.context; + + if (!context) { + continue; + } + + // Get source range from code context + const codeRange = getCodeRange(context); + if (!codeRange) { + continue; + } + + // Map source byte range to line numbers + const lines = sourceRangeToLines(codeRange, lineOffsets); + + for (const line of lines) { + const existing = lineToInstructions.get(line) ?? []; + if (!existing.includes(i)) { + existing.push(i); + lineToInstructions.set(line, existing); + } + } + } + + return { lineToInstructions, instructionOffsets }; +} + +/** + * Find all instruction indices that cover a given source line. + */ +export function findInstructionsAtLine( + mapping: SourceMapping, + line: number, +): number[] { + return mapping.lineToInstructions.get(line) ?? []; +} + +/** + * Build a map from line number to [startOffset, endOffset]. + */ +function buildLineOffsets(source: string): Map { + const lines = source.split("\n"); + const lineOffsets = new Map(); + let offset = 0; + + for (let i = 0; i < lines.length; i++) { + const lineStart = offset; + const lineEnd = offset + lines[i].length; + lineOffsets.set(i + 1, [lineStart, lineEnd]); // 1-indexed + offset = lineEnd + 1; // +1 for newline + } + + return lineOffsets; +} + +/** + * Compute byte offset for each instruction. + */ +function computeInstructionOffsets(instructions: Evm.Instruction[]): number[] { + const offsets: number[] = []; + let offset = 0; + + for (const instr of instructions) { + offsets.push(offset); + offset += 1 + (instr.immediates?.length ?? 0); + } + + return offsets; +} + +/** + * Convert a Value (number or hex string) to a number. + */ +function valueToNumber(value: Format.Data.Value): number { + if (typeof value === "number") { + return value; + } + // Hex string + return parseInt(value, 16); +} + +/** + * Extract source range from a context (handles gather/pick/code variants). + */ +function getCodeRange( + context: Format.Program.Context, +): { offset: number; length: number } | null { + // Direct code context + if (Format.Program.Context.isCode(context)) { + const range = context.code.range; + if (!range) { + return null; + } + return { + offset: valueToNumber(range.offset), + length: valueToNumber(range.length), + }; + } + + // Gather context - check children + if (Format.Program.Context.isGather(context)) { + for (const child of context.gather) { + const range = getCodeRange(child); + if (range) { + return range; + } + } + } + + // Pick context - check all options + if (Format.Program.Context.isPick(context)) { + for (const option of context.pick) { + const range = getCodeRange(option); + if (range) { + return range; + } + } + } + + return null; +} + +/** + * Convert a source byte range to line numbers. + */ +function sourceRangeToLines( + range: { offset: number; length: number }, + lineOffsets: Map, +): number[] { + const lines: number[] = []; + const rangeEnd = range.offset + range.length; + + for (const [line, [start, end]] of lineOffsets) { + // Check if ranges overlap + if (range.offset <= end && rangeEnd >= start) { + lines.push(line); + } + } + + return lines; +} diff --git a/packages/bugc/test/matchers.ts b/packages/bugc/test/matchers.ts new file mode 100644 index 00000000..3f0bb892 --- /dev/null +++ b/packages/bugc/test/matchers.ts @@ -0,0 +1,147 @@ +/** + * Custom Vitest matchers for Result type assertions + */ +import { expect } from "vitest"; + +import { Result, Severity } from "#result"; +import type { BugError } from "#errors"; + +interface CustomMatchers { + toHaveMessage(match: { + severity?: Severity; + code?: string; + message?: string | RegExp; + location?: { offset: number; length?: number }; + }): R; + toHaveNoErrors(): R; + toHaveOnlyWarnings(): R; + toBeCleanSuccess(): R; +} + +declare module "vitest" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +expect.extend({ + toHaveMessage( + result: Result, + match: { + severity?: Severity; + code?: string; + message?: string | RegExp; + location?: { offset: number; length?: number }; + }, + ) { + const found = Result.findMessage(result, match); + + if (!found) { + const allMessages = Result.allMessages(result); + let details = ""; + + if (allMessages.length === 0) { + details = "no messages found"; + } else { + // Show messages of the requested severity (or all if not specified) + const relevant = match.severity + ? allMessages.filter((m) => m.severity === match.severity) + : allMessages; + + if (relevant.length === 0) { + details = `no ${match.severity} messages found`; + } else { + details = `found ${relevant.length} message(s):\n`; + details += relevant + .map( + (m) => + ` - [${m.severity}] ${m.code}: ${m.message}${m.location ? ` at offset ${m.location.offset}` : ""}`, + ) + .join("\n"); + } + } + + return { + pass: false, + message: () => + `Expected message matching ${JSON.stringify(match)}, but ${details}`, + }; + } + + return { + pass: true, + message: () => + `Expected not to have message matching ${JSON.stringify(match)}`, + }; + }, + + toHaveNoErrors(result: Result) { + const errors = Result.getMessages(result, Severity.Error); + + if (errors.length > 0) { + const summary = errors + .map((e) => ` - ${e.code}: ${e.message}`) + .join("\n"); + + return { + pass: false, + message: () => + `Expected no errors, but found ${errors.length}:\n${summary}`, + }; + } + + return { + pass: true, + message: () => "Expected to have errors", + }; + }, + + toHaveOnlyWarnings(result: Result) { + const errors = Result.getMessages(result, Severity.Error); + + const warnings = Result.getMessages(result, Severity.Warning); + + if (errors.length > 0) { + return { + pass: false, + message: () => + `Expected only warnings, but found ${errors.length} error(s)`, + }; + } + + if (warnings.length === 0) { + return { + pass: false, + message: () => "Expected warnings, but found none", + }; + } + + return { + pass: true, + message: () => "Expected to have errors or no warnings", + }; + }, + + toBeCleanSuccess(result: Result) { + if (!result.success) { + return { + pass: false, + message: () => "Expected successful result, but it failed", + }; + } + + const messageCount = Result.countMessages(result); + if (messageCount > 0) { + return { + pass: false, + message: () => + `Expected clean success with no messages, but found ${messageCount} message(s)`, + }; + } + + return { + pass: true, + message: () => "Expected failure or messages", + }; + }, +}); diff --git a/packages/bugc/tsconfig.json b/packages/bugc/tsconfig.json new file mode 100644 index 00000000..3b97f25f --- /dev/null +++ b/packages/bugc/tsconfig.json @@ -0,0 +1,63 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "./dist/", + "baseUrl": "./", + "paths": { + "#ast": ["./src/ast/index"], + "#ast/spec": ["./src/ast/spec"], + "#ast/visitor": ["./src/ast/visitor"], + "#ast/analysis": ["./src/ast/analysis/index"], + + "#cli": ["./src/cli/index"], + + "#compiler": ["./src/compiler/index"], + + "#errors": ["./src/errors/index"], + + "#evm": ["./src/evm/index"], + "#evm/spec": ["./src/evm/spec"], + "#evm/analysis": ["./src/evm/analysis"], + + "#evmgen": ["./src/evmgen/index"], + "#evmgen/analysis": ["./src/evmgen/analysis"], + "#evmgen/errors": ["./src/evmgen/errors"], + "#evmgen/state": ["./src/evmgen/state"], + "#evmgen/operations": ["./src/evmgen/operations"], + "#evmgen/generation": ["./src/evmgen/generation"], + "#evmgen/serialize": ["./src/evmgen/serialize"], + "#evmgen/pass": ["./src/evmgen/pass"], + + "#ir": ["./src/ir/index"], + "#ir/spec": ["./src/ir/spec"], + "#ir/analysis": ["./src/ir/analysis"], + + "#irgen": ["./src/irgen/index"], + "#irgen/pass": ["./src/irgen/pass"], + "#irgen/errors": ["./src/irgen/errors"], + "#irgen/type": ["./src/irgen/type"], + "#irgen/generate": ["./src/irgen/generate/index"], + + "#optimizer": ["./src/optimizer/index"], + "#optimizer/*": ["./src/optimizer/*"], + + "#result": ["./src/result"], + + "#parser": ["./src/parser/index"], + "#parser/pass": ["./src/parser/pass"], + + "#typechecker": ["./src/typechecker/index"], + "#typechecker/pass": ["./src/typechecker/pass"], + + "#types": ["./src/types/index"], + "#types/analysis": ["./src/types/analysis/index"], + "#types/spec": ["./src/types/spec"], + + "#test/*": ["./test/*"] + } + }, + "include": ["src/**/*", "bin/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"], + "references": [{ "path": "../format" }, { "path": "../pointers" }] +} diff --git a/packages/bugc/vitest.config.ts b/packages/bugc/vitest.config.ts new file mode 100644 index 00000000..da9c2437 --- /dev/null +++ b/packages/bugc/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + threads: false, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "dist/", "**/*.test.ts", "bin/**"], + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 1b46ddff..ed34f2ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2205,6 +2205,11 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@bcoe/v8-coverage@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" @@ -3142,86 +3147,171 @@ dependencies: tslib "^2.4.0" +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + "@esbuild/aix-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c" integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw== +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + "@esbuild/android-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57" integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA== +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + "@esbuild/android-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142" integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA== +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + "@esbuild/android-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2" integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A== +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + "@esbuild/darwin-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256" integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg== +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + "@esbuild/darwin-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509" integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA== +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + "@esbuild/freebsd-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c" integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g== +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + "@esbuild/freebsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb" integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA== +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + "@esbuild/linux-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb" integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw== +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + "@esbuild/linux-arm@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322" integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw== +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + "@esbuild/linux-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc" integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w== +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + "@esbuild/linux-loong64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a" integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg== +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + "@esbuild/linux-mips64el@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10" integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw== +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + "@esbuild/linux-ppc64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0" integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ== +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + "@esbuild/linux-riscv64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d" integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA== +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + "@esbuild/linux-s390x@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab" integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w== +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + "@esbuild/linux-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650" @@ -3232,6 +3322,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0" integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw== +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + "@esbuild/netbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272" @@ -3242,6 +3337,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e" integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA== +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + "@esbuild/openbsd-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a" @@ -3252,34 +3352,54 @@ resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f" integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag== +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + "@esbuild/sunos-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2" integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg== +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + "@esbuild/win32-arm64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a" integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg== +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + "@esbuild/win32-ia32@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5" integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ== +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + "@esbuild/win32-x64@0.27.2": version "0.27.2" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": version "4.12.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== @@ -3304,6 +3424,119 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@ethereumjs/binarytree@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/binarytree/-/binarytree-10.1.0.tgz#d5c7b19975fba762ab84937120ec56f6a657ccb3" + integrity sha512-54n24oYJqgDk6DtXVjjGohjQCFCDgcvI2+AeNGRZcsQuz6yE2HYvxWq8VrAAcR8ja/gNc9eVYL51DvU93qK8Eg== + dependencies: + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + "@noble/hashes" "^1.7.2" + debug "^4.4.0" + ethereum-cryptography "^3.2.0" + lru-cache "11.0.2" + +"@ethereumjs/block@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-10.1.0.tgz#18c19008881a910bd22ce7e519d7a5d5c4a4cee1" + integrity sha512-W2GR/ejYn/idfX4fxQ2DRbrbOF5U04Q2wDiMuxtvnOr8zdVCBSlVYCC348N73ufkrqY5ltTlEUZYWLWWobcS/Q== + dependencies: + "@ethereumjs/common" "^10.1.0" + "@ethereumjs/mpt" "^10.1.0" + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/tx" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + ethereum-cryptography "^3.2.0" + +"@ethereumjs/common@^10.0.0", "@ethereumjs/common@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-10.1.0.tgz#f80e5589feae32ba33a818c50b18a361d8bde527" + integrity sha512-zIHCy0i2LFmMDp+QkENyoPGxcoD3QzeNVhx6/vE4nJk4uWGNXzO8xJ2UC4gtGW4UJTAOXja8Z1yZMVeRc2/+Ew== + dependencies: + "@ethereumjs/util" "^10.1.0" + eventemitter3 "^5.0.1" + +"@ethereumjs/evm@^10.0.0", "@ethereumjs/evm@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/evm/-/evm-10.1.0.tgz#97ae3a688efd860d7a3bd2251c51ef00ed750d3e" + integrity sha512-RD6tjysXEWPfyBHmMxsg3s3T2JdbPVYvsDDDUR0laFaHWtHMnOeW/EIZ5njUx75nG+i72w34Sh6NhhkpkhMGDg== + dependencies: + "@ethereumjs/binarytree" "^10.1.0" + "@ethereumjs/common" "^10.1.0" + "@ethereumjs/statemanager" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + "@noble/curves" "^1.9.0" + debug "^4.4.0" + ethereum-cryptography "^3.2.0" + eventemitter3 "^5.0.1" + +"@ethereumjs/mpt@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/mpt/-/mpt-10.1.0.tgz#fe0472a8f1e3658e06bcd798a934fa876e00ebc4" + integrity sha512-gWxx8n1OB2Js3EFWNeSdsUdYvw1P5WvdKxHmeq+giO03mt5fGRbuSh3ruXzreG4JRugpNSW6Dwqk+StVwjrQ4Q== + dependencies: + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + debug "^4.4.0" + ethereum-cryptography "^3.2.0" + lru-cache "11.0.2" + +"@ethereumjs/rlp@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-10.1.0.tgz#fe681ed0fd2f55ed8623c0d445353d1411703b5d" + integrity sha512-r67BJbwilammAqYI4B5okA66cNdTlFzeWxPNJOolKV52ZS/flo0tUBf4x4gxWXBgh48OgsdFV1Qp5pRoSe8IhQ== + +"@ethereumjs/statemanager@^10.0.0", "@ethereumjs/statemanager@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/statemanager/-/statemanager-10.1.0.tgz#73b89b31b5ac0a116d5e7e0f9412ab1720e0bd16" + integrity sha512-n4AB8olDocXtq/V6SClbZ5zEDfJL7cw3MmkGUb/MVQwJBgYZ143oll4UfGaK7jWQfF5Gth68KEJZQay5Hh3h6g== + dependencies: + "@ethereumjs/binarytree" "^10.1.0" + "@ethereumjs/common" "^10.1.0" + "@ethereumjs/mpt" "^10.1.0" + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + "@js-sdsl/ordered-map" "^4.4.2" + "@noble/hashes" "^1.7.2" + debug "^4.4.0" + ethereum-cryptography "^3.2.0" + lru-cache "11.0.2" + +"@ethereumjs/tx@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-10.1.0.tgz#90b568f7a1a8c02a20b2531dbc0375be38dff573" + integrity sha512-svG6pyzUZDpunafszf2BaolA6Izuvo8ZTIETIegpKxAXYudV1hmzPQDdSI+d8nHCFyQfEFbQ6tfUq95lNArmmg== + dependencies: + "@ethereumjs/common" "^10.1.0" + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + ethereum-cryptography "^3.2.0" + +"@ethereumjs/util@^10.0.0", "@ethereumjs/util@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-10.1.0.tgz#56ba2abd5ca0030a1bb6d543bf205c27307cd592" + integrity sha512-GGTCkRu1kWXbz2JoUnIYtJBOoA9T5akzsYa91Bh+DZQ3Cj4qXj3hkNU0Rx6wZlbcmkmhQfrjZfVt52eJO/y2nA== + dependencies: + "@ethereumjs/rlp" "^10.1.0" + ethereum-cryptography "^3.2.0" + +"@ethereumjs/vm@^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-10.1.0.tgz#96a859946e8d85cd2759ea4b24d9c194d0a5d704" + integrity sha512-5OxHK4hdccDeSiBeSZj8WITl9uB6NbXA6/yf2wAPoflVxZKzOM5h6ssFrOAhz2faD130UoU62pnT23WClgrXSQ== + dependencies: + "@ethereumjs/block" "^10.1.0" + "@ethereumjs/common" "^10.1.0" + "@ethereumjs/evm" "^10.1.0" + "@ethereumjs/mpt" "^10.1.0" + "@ethereumjs/rlp" "^10.1.0" + "@ethereumjs/statemanager" "^10.1.0" + "@ethereumjs/tx" "^10.1.0" + "@ethereumjs/util" "^10.1.0" + debug "^4.4.0" + ethereum-cryptography "^3.2.0" + eventemitter3 "^5.0.1" + "@fortawesome/fontawesome-common-types@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470" @@ -3373,7 +3606,7 @@ resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperjump/browser@^1.3.1": +"@hyperjump/browser@^1.2.0", "@hyperjump/browser@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@hyperjump/browser/-/browser-1.3.1.tgz#e86542a260495269ceea91bde37b55b15f83952a" integrity sha512-Le5XZUjnVqVjkgLYv6yyWgALat/0HpB1XaCPuCZ+GCFki9NvXloSZITIJ0H+wRW7mb9At1SxvohKBbNQbrr/cw== @@ -3396,7 +3629,7 @@ "@hyperjump/uri" "^1.3.2" idn-hostname "^15.1.2" -"@hyperjump/json-schema@^1.17.3": +"@hyperjump/json-schema@^1.11.0", "@hyperjump/json-schema@^1.17.3": version "1.17.3" resolved "https://registry.yarnpkg.com/@hyperjump/json-schema/-/json-schema-1.17.3.tgz#18c6cace109f196e6475275b6a4997315a2ec677" integrity sha512-WaatDqGn5ZD6A9IiSvx82cZ5BYPLqNLVpgna95mA7T6TDNavdHvV3EhQZ9D4zP71/8/Lhxe2a5LRWvkD1t9PAg== @@ -3540,6 +3773,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -3572,6 +3810,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + "@jsonjoy.com/base64@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" @@ -3784,6 +4027,11 @@ "@emnapi/runtime" "^1.1.0" "@tybys/wasm-util" "^0.9.0" +"@noble/ciphers@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" + integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== + "@noble/curves@1.4.2": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -3791,6 +4039,20 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.0.tgz#13e0ca8be4a0ce66c113693a94514e5599f40cfc" + integrity sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/curves@^1.9.0", "@noble/curves@~1.9.0": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + "@noble/curves@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" @@ -3803,6 +4065,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.8.0", "@noble/hashes@^1.7.2", "@noble/hashes@~1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -4466,6 +4733,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== +"@scure/base@~1.2.5": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== + "@scure/bip32@1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" @@ -4475,6 +4747,15 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip32@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.7.0.tgz#b8683bab172369f988f1589640e53c4606984219" + integrity sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw== + dependencies: + "@noble/curves" "~1.9.0" + "@noble/hashes" "~1.8.0" + "@scure/base" "~1.2.5" + "@scure/bip39@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" @@ -4483,6 +4764,14 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip39@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.6.0.tgz#475970ace440d7be87a6086cbee77cb8f1a684f9" + integrity sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A== + dependencies: + "@noble/hashes" "~1.8.0" + "@scure/base" "~1.2.5" + "@shikijs/core@2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-2.5.0.tgz#e14d33961dfa3141393d4a76fc8923d0d1c4b62f" @@ -5064,7 +5353,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -5123,11 +5412,23 @@ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@^20.0.0": + version "20.19.29" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.29.tgz#49e9857d3e3f3220508d37904eb47cb3434d5b17" + integrity sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw== + dependencies: + undici-types "~6.21.0" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +"@types/parsimmon@^1.10.9": + version "1.10.9" + resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.9.tgz#14e60db223c1d213fea0e15985d480b5cfe1789a" + integrity sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A== + "@types/prismjs@^1.26.0": version "1.26.3" resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz" @@ -5205,6 +5506,11 @@ resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.1.tgz#1254750a4fec4aff2ebec088ccd0bb02e91fedb4" integrity sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw== +"@types/semver@^7.5.0": + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== + "@types/send@*": version "0.17.4" resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" @@ -5287,6 +5593,23 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/eslint-plugin@^8.21.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz#afb966c66a2fdc6158cf81118204a971a36d0fc5" @@ -5301,6 +5624,17 @@ natural-compare "^1.4.0" ts-api-utils "^2.4.0" +"@typescript-eslint/parser@^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + dependencies: + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + "@typescript-eslint/parser@^8.21.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.53.0.tgz#d8bed6f12dc74e03751e5f947510ff2b165990c6" @@ -5321,6 +5655,14 @@ "@typescript-eslint/types" "^8.53.0" debug "^4.4.3" +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager@8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz#f922fcbf0d42e72f065297af31779ccf19de9a97" @@ -5334,6 +5676,16 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz#105279d7969a7abdc8345cc9c57cff83cf910f8f" integrity sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== + dependencies: + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/type-utils@8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz#81a0de5c01fc68f6df0591d03cd8226bda01c91f" @@ -5345,11 +5697,30 @@ debug "^4.4.3" ts-api-utils "^2.4.0" +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + "@typescript-eslint/types@8.53.0", "@typescript-eslint/types@^8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.0.tgz#1adcad3fa32bc2c4cbf3785ba07a5e3151819efb" integrity sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/typescript-estree@8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz#7805b46b7a8ce97e91b7bb56fc8b1ba26ca8ef52" @@ -5365,6 +5736,19 @@ tinyglobby "^0.2.15" ts-api-utils "^2.4.0" +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + "@typescript-eslint/utils@8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.53.0.tgz#bf0a4e2edaf1afc9abce209fc02f8cab0b74af13" @@ -5375,6 +5759,14 @@ "@typescript-eslint/types" "8.53.0" "@typescript-eslint/typescript-estree" "8.53.0" +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + "@typescript-eslint/visitor-keys@8.53.0": version "8.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz#9a785664ddae7e3f7e570ad8166e48dbc9c6cf02" @@ -5398,6 +5790,24 @@ resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.1.0.tgz#066caee449b84079f33c7445fc862464fe10ec32" integrity sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w== +"@vitest/coverage-v8@^2.1.8": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz#060bebfe3705c1023bdc220e17fdea4bd9e2b24d" + integrity sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ== + dependencies: + "@ampproject/remapping" "^2.3.0" + "@bcoe/v8-coverage" "^0.2.3" + debug "^4.3.7" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.6" + istanbul-reports "^3.1.7" + magic-string "^0.30.12" + magicast "^0.3.5" + std-env "^3.8.0" + test-exclude "^7.0.1" + tinyrainbow "^1.2.0" + "@vitest/coverage-v8@^3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz#a2d8d040288c1956a1c7d0a0e2cdcfc7a3319f13" @@ -5417,6 +5827,16 @@ test-exclude "^7.0.1" tinyrainbow "^2.0.0" +"@vitest/expect@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8" + integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw== + dependencies: + "@vitest/spy" "2.1.9" + "@vitest/utils" "2.1.9" + chai "^5.1.2" + tinyrainbow "^1.2.0" + "@vitest/expect@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" @@ -5428,6 +5848,15 @@ chai "^5.2.0" tinyrainbow "^2.0.0" +"@vitest/mocker@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5" + integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg== + dependencies: + "@vitest/spy" "2.1.9" + estree-walker "^3.0.3" + magic-string "^0.30.12" + "@vitest/mocker@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" @@ -5437,6 +5866,13 @@ estree-walker "^3.0.3" magic-string "^0.30.17" +"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf" + integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ== + dependencies: + tinyrainbow "^1.2.0" + "@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" @@ -5444,6 +5880,14 @@ dependencies: tinyrainbow "^2.0.0" +"@vitest/runner@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6" + integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g== + dependencies: + "@vitest/utils" "2.1.9" + pathe "^1.1.2" + "@vitest/runner@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766" @@ -5453,6 +5897,15 @@ pathe "^2.0.3" strip-literal "^3.0.0" +"@vitest/snapshot@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91" + integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ== + dependencies: + "@vitest/pretty-format" "2.1.9" + magic-string "^0.30.12" + pathe "^1.1.2" + "@vitest/snapshot@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c" @@ -5462,6 +5915,13 @@ magic-string "^0.30.17" pathe "^2.0.3" +"@vitest/spy@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60" + integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ== + dependencies: + tinyspy "^3.0.2" + "@vitest/spy@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" @@ -5469,6 +5929,19 @@ dependencies: tinyspy "^4.0.3" +"@vitest/ui@^2.1.8": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-2.1.9.tgz#9e876cf3caf492dd6fddbd7f87b2d6bf7186a7a9" + integrity sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw== + dependencies: + "@vitest/utils" "2.1.9" + fflate "^0.8.2" + flatted "^3.3.1" + pathe "^1.1.2" + sirv "^3.0.0" + tinyglobby "^0.2.10" + tinyrainbow "^1.2.0" + "@vitest/ui@^3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-3.2.4.tgz#df8080537c1dcfeae353b2d3cb3301d9acafe04a" @@ -5482,6 +5955,15 @@ tinyglobby "^0.2.14" tinyrainbow "^2.0.0" +"@vitest/utils@2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1" + integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ== + dependencies: + "@vitest/pretty-format" "2.1.9" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + "@vitest/utils@3.2.4": version "3.2.4" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" @@ -6674,7 +7156,7 @@ ccount@^2.0.0: resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chai@^5.2.0: +chai@^5.1.2, chai@^5.2.0: version "5.3.3" resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== @@ -7581,7 +8063,7 @@ debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug dependencies: ms "2.1.2" -debug@^4.3.2, debug@^4.4.1, debug@^4.4.3: +debug@^4.3.2, debug@^4.3.7, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -8095,7 +8577,7 @@ es-module-lexer@^1.2.1: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== -es-module-lexer@^1.7.0: +es-module-lexer@^1.5.4, es-module-lexer@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== @@ -8122,6 +8604,35 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + esbuild@^0.27.0, esbuild@~0.27.0: version "0.27.2" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.2.tgz#d83ed2154d5813a5367376bb2292a9296fc83717" @@ -8220,7 +8731,7 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@^8.57.1: +eslint@^8.0.0, eslint@^8.57.1: version "8.57.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -8381,6 +8892,17 @@ ethereum-cryptography@^2.2.1: "@scure/bip32" "1.4.0" "@scure/bip39" "1.3.0" +ethereum-cryptography@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-3.2.0.tgz#42a04b57834bf536e552b50a70b9ee5057c71dc6" + integrity sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg== + dependencies: + "@noble/ciphers" "1.3.0" + "@noble/curves" "1.9.0" + "@noble/hashes" "1.8.0" + "@scure/bip32" "1.7.0" + "@scure/bip39" "1.6.0" + eval@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz" @@ -8454,7 +8976,7 @@ execa@^8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" -expect-type@^1.2.1: +expect-type@^1.1.0, expect-type@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== @@ -8522,6 +9044,13 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-check@^4.2.0: + version "4.5.3" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-4.5.3.tgz#55c0d2e9649499ad1aa03b897f48e4f9bcdca2ad" + integrity sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA== + dependencies: + pure-rand "^7.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -8705,7 +9234,7 @@ flat@^5.0.2: resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.2.9, flatted@^3.3.3: +flatted@^3.2.9, flatted@^3.3.1, flatted@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== @@ -8759,6 +9288,11 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +fp-ts@^2.16.11: + version "2.16.11" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.11.tgz#831a10514bf4e22adf12065732fc5a20c85d9623" + integrity sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w== + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" @@ -10770,7 +11304,7 @@ loupe@^3.1.0: resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== -loupe@^3.1.4: +loupe@^3.1.2, loupe@^3.1.4: version "3.2.1" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== @@ -10787,6 +11321,11 @@ lowercase-keys@^3.0.0: resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz" integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== +lru-cache@11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39" + integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": version "10.1.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz" @@ -10811,6 +11350,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.30.12: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + magic-string@^0.30.17: version "0.30.17" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" @@ -12752,6 +13298,11 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +parsimmon@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/parsimmon/-/parsimmon-1.18.1.tgz#d8dd9c28745647d02fc6566f217690897eed7709" + integrity sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw== + pascal-case@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" @@ -12850,6 +13401,11 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + pathe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" @@ -13531,7 +14087,7 @@ postcss@^8.4.24: picocolors "^1.0.0" source-map-js "^1.2.0" -postcss@^8.4.33, postcss@^8.5.4, postcss@^8.5.6: +postcss@^8.4.33, postcss@^8.4.43, postcss@^8.5.4, postcss@^8.5.6: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -13545,7 +14101,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^3.4.2: +prettier@^3.4.2, prettier@^3.5.3: version "3.7.4" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== @@ -13705,6 +14261,11 @@ pupa@^3.1.0: dependencies: escape-goat "^4.0.0" +pure-rand@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-7.0.1.tgz#6f53a5a9e3e4a47445822af96821ca509ed37566" + integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== + pvtsutils@^1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" @@ -14356,7 +14917,7 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" -rollup@^4.43.0: +rollup@^4.20.0, rollup@^4.43.0: version "4.55.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.55.1.tgz#4ec182828be440648e7ee6520dc35e9f20e05144" integrity sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A== @@ -14774,7 +15335,7 @@ sirv@^2.0.3: mrmime "^2.0.0" totalist "^3.0.0" -sirv@^3.0.1: +sirv@^3.0.0, sirv@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970" integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g== @@ -15045,7 +15606,7 @@ statuses@~2.0.1, statuses@~2.0.2: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== -std-env@^3.7.0, std-env@^3.9.0: +std-env@^3.7.0, std-env@^3.8.0, std-env@^3.9.0: version "3.10.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== @@ -15441,7 +16002,7 @@ tinybench@^2.9.0: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinyexec@^0.3.2: +tinyexec@^0.3.1, tinyexec@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== @@ -15454,7 +16015,7 @@ tinyglobby@0.2.12: fdir "^6.4.3" picomatch "^4.0.2" -tinyglobby@^0.2.14, tinyglobby@^0.2.15: +tinyglobby@^0.2.10, tinyglobby@^0.2.14, tinyglobby@^0.2.15: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== @@ -15462,21 +16023,31 @@ tinyglobby@^0.2.14, tinyglobby@^0.2.15: fdir "^6.5.0" picomatch "^4.0.3" +tinypool@^1.0.1, tinypool@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + tinypool@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== -tinypool@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" - integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== tinyrainbow@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + tinyspy@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" @@ -15560,6 +16131,11 @@ trough@^2.0.0: resolved "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-api-utils@^1.0.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== + ts-api-utils@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" @@ -15616,7 +16192,7 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsx@^4.21.0: +tsx@^4.19.4, tsx@^4.21.0: version "4.21.0" resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.21.0.tgz#32aa6cf17481e336f756195e6fe04dae3e6308b1" integrity sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw== @@ -15714,7 +16290,7 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -typescript@^5.9.3: +typescript@^5.0.0, typescript@^5.9.3: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== @@ -15734,6 +16310,11 @@ undici-types@~5.26.4: resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" @@ -16053,6 +16634,17 @@ vfile@^6.0.0, vfile@^6.0.1: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" +vite-node@2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f" + integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + vite-node@3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" @@ -16064,6 +16656,17 @@ vite-node@3.2.4: pathe "^2.0.3" vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" +vite@^5.0.0: + version "5.4.21" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" + integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + "vite@^5.0.0 || ^6.0.0 || ^7.0.0-0": version "7.3.1" resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" @@ -16078,6 +16681,32 @@ vite-node@3.2.4: optionalDependencies: fsevents "~2.3.3" +vitest@^2.1.8: + version "2.1.9" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7" + integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q== + dependencies: + "@vitest/expect" "2.1.9" + "@vitest/mocker" "2.1.9" + "@vitest/pretty-format" "^2.1.9" + "@vitest/runner" "2.1.9" + "@vitest/snapshot" "2.1.9" + "@vitest/spy" "2.1.9" + "@vitest/utils" "2.1.9" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.9" + why-is-node-running "^2.3.0" + vitest@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea"