Skip to content

refactor(typewriter): parse apps from solc output, tested with inline Solidity - #116

Merged
kyscott18 merged 10 commits into
mainfrom
refactor/sol-parse
Nov 2, 2026
Merged

kyscott18 merged 10 commits into
mainfrom
refactor/sol-parse

Conversation

@kyscott18

@kyscott18 kyscott18 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Solidity parsing is now a pure fundamental with a thin IO step in front:

  • sol-parse.ts: parseSolidity(solcOutput, path) takes solc's standard JSON output and returns { contractName, abi, bytecode, storageLayout, mutations }, or throws a ConfigError. It does no IO and doesn't use Effect. The file went from 829 to 338 lines.
  • sol-build.ts: loadSolidityApp(entrypoint) runs forge build --build-info, hands the solc output to parseSolidity, and refreshes the .sol.d.ts. This replaces the artifact globbing and import-graph walk. A full-project build still reproduces apps/token's committed Token.sol.d.ts byte for byte.
  • createSolidityTypewriterApp is gone. index.ts and the sim pass the parsed app straight to createTypewriterApp.

Behavior changes

  • Mutation params come from the XMutation library for each Mutation member X: it must declare struct X and execute(State storage, X memory, bytes32) internal. The parser no longer reads dispatch.
  • Apps: OrderBook renames executeX to execute and gives AddInstrument an accountID param (forge tests 24/24; OrderBook.sol.d.ts regenerated, AST ids only). Assessment moves its mutation logic from dispatch into AMutation.execute/BMutation.execute unchanged; the sim's state astId constant is updated. Token already conformed.
  • The app contract must inherit only Typewriter, not be abstract, declare Mutation inside itself, add no public/external functions or variables, and keep all storage in state.
  • Mutation params can be nested structs (tuple/tuple[]; abipg, ox, and executeInputSchema already support them), user-defined value types, and contracts/interfaces (address). uint/int canonicalize to uint256/int256. Before, a struct field written uint produced the ABI type uint.
  • New errors: a missing or duplicate mutation library, a wrong execute signature, public/external entrypoints or extra storage on the app contract, and apps that link an external library. The old bytecode check used non-strict Hex.validate, which only checks for 0x, so unlinked placeholders passed. Typewriter executes only the app contract, so a library call would also break offchain execution.
  • Error messages say how to fix the problem, e.g. AddMutation must declare `struct Add` with the mutation's params and `function execute(State storage state, Add memory mutation, bytes32 accountID) internal` .

What a valid typewriter contract needs, and the test for each

# Requirement Enforced by Test
1 The entrypoint file defines exactly one contract that inherits Typewriter parser none; two
2 It inherits only Typewriter parser inherits Ownable too
3 It isn't abstract parser abstract app
4 Its constructor takes exactly (address scheduler) parser (ABI) extra arg; scheduler fixed in the inheritance list
5 It has a storage variable state that is a struct parser (storage layout) not a struct; missing
6 It links no external libraries parser (bytecode) external library
7 It declares enum Mutation inside itself parser no enum; file-level enum
8 Mutation members don't use built-in names; at most 253 members parser built-in name; 254 members
9 Each Mutation.X has exactly one library XMutation in the entrypoint file or its imports parser library in imported file, non-imported file ignored; none; two
10 XMutation declares struct X (the params) and function execute(State storage, X memory, bytes32) internal returning nothing, State being the state variable's type parser no struct; no execute; executeAdd; no accountID; other State; calldata; public; returns a value
11 The app contract adds no public/external functions (incl. fallback/receive) or public variables, and no storage besides state (constants allowed) parser public, external, fallback, receive, public constant, State public state, extra variable, immutable; constants and internal/private helpers allowed
12 Each struct field has an ABI type parser (+ solc for mappings and fixed-point) every ABI type; nested tuples; function-type field
13 Nested struct params validate and encode at runtime executeInputSchema mutation-schema.test.ts nested structs

dispatch is the app's own code: the parser doesn't read it. Not yet checked: determinism (external calls, COINBASE, PREVRANDAO, …), the next PR.

Tests

sol-parse.test.ts compiles inline Solidity with solc-js 0.8.37 next to the real Typewriter.sol (~230ms per compile), runs parseSolidity, and checks the result with inline snapshots. There are 20 tests: every ABI type a param can have, library lookup across imports, every error path, and the declaration formatter. The forge fixture contracts are deleted. test/contracts/foundry.toml stays, since it runs Typewriter.t.sol (21/21 pass).

Test plan

  • bun test in packages/typewriter: 91 pass, 0 fail
  • tsc --noEmit, biome check .
  • Sim seed 0x41 passes end to end through the new forge loader
  • forge test in test/contracts (Typewriter.t.sol)

🤖 Generated with Claude Code

… Solidity

Split Solidity parsing into a pure fundamental and a thin build step:

- sol-parse.ts: parseSolidity(solcOutput, path) reads the app from solc's
  standard JSON output, with no IO. It matches dispatch branches and decodes
  by declaration ID instead of identifier names, so renamed dispatch params,
  reversed comparisons, `Mutation(mutation) == Mutation.X`, inline decodes,
  and an inherited `dispatch`/`Mutation` all work. Mutation params can now be
  nested structs, user-defined value types, and contracts, and `uint`/`int`
  canonicalize to uint256/int256. Duplicate branches are an error, and an app
  that links an external library is rejected (the old hex check was
  non-strict and never caught it).
- sol-build.ts: loadSolidityApp runs forge with --build-info and passes the
  solc output to parseSolidity, replacing the artifact globbing and
  import-graph walk.
- The tests compile inline Solidity with solc-js next to the real
  Typewriter.sol and check results and errors with inline snapshots. The
  forge fixture contracts are gone; test/contracts stays for Typewriter.t.sol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​solc@​0.8.379910010093100

View full report

kyscott18 and others added 7 commits November 2, 2026 19:05
Add the missing cases found by listing what a valid app must satisfy: a
scheduler fixed in the inheritance list, a missing state variable, a
file-level Mutation enum (now named in the error), a numeric comparison in
dispatch, and nested struct params through executeInputSchema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dispatch is the app's to write, so the parser no longer analyzes it.
Mutation.X takes its params from the one struct named X in the entrypoint
file or its imports, the convention the README already documents. The app
contract must inherit only Typewriter and declare Mutation inside itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…execute

Each Mutation member X needs exactly one library XMutation, in the
entrypoint file or its imports, that declares struct X (the params) and
function execute(State storage, X memory, bytes32) internal, where State is
the type of the app's state variable. A ...Mutation library without a
Mutation member is an error.

OrderBook renames its executeX functions to execute and gives
AddInstrument's an accountID param. Assessment moves its mutation logic out
of dispatch into AMutation.execute and BMutation.execute, unchanged, and its
sim storage layout constant takes the new state astId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app contract can't add public or external functions (including fallback
and receive) or public variables, and keeps all its storage in state;
constants are allowed. execute must return nothing. A library named
...Mutation without a Mutation member is no longer an error: the only
library check is one XMutation per Mutation member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop internals (revm, storage proxies, slot-write rehydration, preimages, the
reorg algorithm, forge/AST parsing, the scheduler-only execute ABI) and list
the app contract rules in one place. Fix two stale examples: the Concepts
mutation now follows the XMutation library convention, and the
FORCE_INCLUSION_DELAY constructor passes scheduler to Typewriter. The root
README had drifted to document features that no longer exist (safe status,
onFatalError, mutation id); it is now a copy of the package README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kyscott18 and others added 2 commits November 2, 2026 20:23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kyscott18 added a commit that referenced this pull request Nov 2, 2026
…ct rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyscott18
kyscott18 merged commit 3a67ca4 into main Nov 2, 2026
3 checks passed
@kyscott18
kyscott18 deleted the refactor/sol-parse branch November 2, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant