Skip to main content

Under the Hood

This section is for developers who want to understand how Skittles works internally. If you're just getting started with smart contracts, you can safely skip this — everything covered here happens automatically.

How Skittles Works

Skittles is a TypeScript to Solidity compiler. You write smart contracts as TypeScript classes, and Skittles compiles them into clean, readable Solidity source code. Hardhat (or any Solidity toolchain) then compiles that Solidity to EVM bytecode that runs on the blockchain.

TypeScript (.ts) → Parser → IR → Analysis → Codegen → Solidity (.sol) → Hardhat → ABI + Bytecode

The Four Stage Pipeline

  1. Parse — Your TypeScript is parsed using the official TypeScript compiler API. Classes become contracts, properties become state variables, methods become functions.

  2. Analyze — The parsed code is checked for common issues like unreachable code (statements after return or throw) and unused local variables. These are reported as warnings to help you catch mistakes early.

  3. Generate — The intermediate representation is converted to valid Solidity. Type mappings, visibility, state mutability inference, and optimizations are applied automatically.

  4. Compile — The generated Solidity is written to build/solidity/. Hardhat (or another Solidity toolchain) compiles it to ABI and EVM bytecode.

Source Maps

When Skittles generates Solidity, it also produces a source map file (.sol.map) alongside each .sol file. This maps every generated Solidity line back to the original TypeScript source file and line number.

For example, if Hardhat reports an error on line 12 of Token.sol, you can look up line 12 in Token.sol.map to find the corresponding TypeScript line:

build/solidity/Token.sol.map
{
"sourceFile": "contracts/Token.ts",
"mappings": {
"4": 1,
"5": 2,
"8": 5,
"12": 9
}
}

Each key is a Solidity line number and each value is the corresponding TypeScript line number. This makes it easy to trace any Solidity compiler error or runtime revert back to your TypeScript source.

Type Mappings

Here's how TypeScript types map to Solidity types:

TypeScriptSolidityNotes
numberuint256All numbers are unsigned 256-bit integers
stringstringUTF-8 strings
booleanbooltrue / false
addressaddressEthereum address
bytesbytesRaw byte data
bytes32bytes32Fixed-size 32-byte value (hashes, keys)
Record<K, V>mapping(K => V)Key-value storage
T[]T[]Dynamic arrays
Type aliasstructCustom data structures
InterfaceContract interfaceExternal API definition
EnumenumNamed constants

Visibility Mappings

TypeScriptSolidityNotes
public (or no modifier)publicGenerates an automatic getter
privateinternalMore gas-efficient than Solidity's private
protectedinternalSame as private in output
static readonlyconstantCompile-time constant
readonlyimmutableSet once at deployment

State Mutability Inference

Skittles analyzes each function body to determine its Solidity state mutability:

Access PatternSolidity Mutability
No this.* accesspure
Reads this.* onlyview
Writes this.*, emits events, or deletes state(default, no annotation)
Accesses msg.valuepayable

This inference propagates through call chains — if function A calls function B, and B writes state, then A is also marked as state-modifying. For external contract calls via Contract<T>(), the compiler respects already-known mutability annotations on interface methods: if all external calls in a function target methods annotated as view or pure (e.g. from property signatures or implements resolution), the wrapper function's mutability is computed from its own body. Unannotated interface methods are treated conservatively as state-modifying.

Automatic Optimizations

Skittles applies several optimizations to generate idiomatic Solidity:

  • if/throwrequire(): When an if block contains only throw new Error("message") with no else, it's converted to require() with the condition negated
  • String truthiness → length check: if (str) compiles to if (bytes(str).length > 0) and if (!str) compiles to if (bytes(str).length == 0) since Solidity doesn't support implicit string-to-bool conversion
  • privateinternal: TypeScript private maps to Solidity internal rather than private for better gas efficiency
  • virtual by default: All functions are marked virtual so child contracts can override them
  • Address wrapping: 42-character hex string literals are automatically wrapped in address(...)
  • Memory annotations: memory keywords are added to string and bytes parameters automatically
  • for...of desugaring: for...of loops over arrays are converted to index-based for loops
  • switch/caseif/else: Switch statements are converted to if/else chains (Solidity has no native switch)
  • Number.MAX_VALUEtype(uint256).max: Maximum integer value
  • Math.min(a, b) / Math.max(a, b) → helper functions: Auto-generates internal _min / _max helpers to ensure each argument is evaluated exactly once
  • Math.pow(a, b)a ** b: Uses Solidity's exponentiation operator
  • Math.sqrt(x) → Babylonian method: Auto-generates an internal _sqrt helper function
  • Template literals → string.concat(): Template strings are converted to Solidity string concatenation
  • **=x = x ** y: The **= compound assignment is desugared because Solidity has no **= operator
  • Local variable shadowing prevention: When a local variable has the same name as a state variable, it's automatically renamed with an underscore prefix (e.g. result_result) to avoid Solidity shadowing warnings
  • Parameter shadowing prevention: When a function parameter has the same name as another function in the contract (e.g. a setter parameter value shadowing a getter function value()), the parameter is automatically renamed to avoid Solidity shadowing warnings

Generated Solidity Example

Here's what a simple TypeScript contract looks like after compilation:

Input (TypeScript):

contracts/Token.ts
import { address, msg } from "skittles";

export class Token {
totalSupply: number = 0;
private balances: Record<address, number> = {};

constructor(supply: number) {
this.totalSupply = supply;
this.balances[msg.sender] = supply;
}

balanceOf(account: address): number {
return this.balances[account];
}

transfer(to: address, amount: number): boolean {
if (this.balances[msg.sender] < amount) {
throw new Error("Insufficient balance");
}
this.balances[msg.sender] -= amount;
this.balances[to] += amount;
return true;
}
}

Output (Solidity):

build/solidity/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Token {
uint256 public totalSupply;
mapping(address => uint256) internal balances;

constructor(uint256 supply) {
totalSupply = supply;
balances[msg.sender] = supply;
}

function balanceOf(address account) public view virtual returns (uint256) {
return balances[account];
}

function transfer(address to, uint256 amount) public virtual returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
return true;
}
}

Notice the automatic transformations:

  • numberuint256, Record<address, number>mapping(address => uint256)
  • privateinternal
  • balanceOf is marked view (only reads state)
  • if/throw is optimized to require()
  • Functions are marked virtual by default
  • String parameters get memory annotations

Why Solidity?

Skittles compiles to Solidity rather than directly to EVM bytecode for several important reasons:

  • Security audits: Solidity has the largest ecosystem of security auditors and automated analysis tools
  • Etherscan verification: You can verify your generated Solidity on Etherscan and other block explorers, making your contracts transparent
  • Tooling compatibility: Works with every existing Solidity tool — Hardhat, Foundry, Remix, Slither, and more
  • Readability: The generated code is human-readable, so you can always inspect what's being deployed
  • Trust: Users and auditors can verify the generated Solidity matches the TypeScript source