Skip to content

Commit 7f056e1

Browse files
authored
Language design axes: full implementation (#1731)
Merge the language design axes implementation for the v1.0.0 release.
1 parent 825107f commit 7f056e1

95 files changed

Lines changed: 7505 additions & 1184 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Compiler/ABI.lean

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ private def abiTypeString : ParamType → String
2222
| .tuple _ => "tuple"
2323
| .array t => abiTypeString t ++ "[]"
2424
| .fixedArray t n => abiTypeString t ++ "[" ++ toString n ++ "]"
25+
| .adt _ _ => "tuple" -- ADTs are ABI-encoded as static tuples
26+
| .newtypeOf _ baseType => abiTypeString baseType -- Erased to base type
2527

2628
-- Uses `fieldTypeToParamType` from CompilationModel (shared, not duplicated).
2729
-- Uses `isInteropEntrypointName` from CompilationModel for consistent filtering.
@@ -39,6 +41,13 @@ mutual
3941
| .tuple elems =>
4042
let rendered := elems.map (fun ty => renderParam "" ty none)
4143
some ("[" ++ String.intercalate ", " rendered ++ "]")
44+
| .adt _ maxFields =>
45+
-- ADTs encode as (uint8 tag, uint256 field₀, ..., uint256 fieldₙ)
46+
let tagComponent := renderParam "tag" .uint8 none
47+
let fieldComponents := List.range maxFields |>.map fun i =>
48+
renderParam s!"field{i}" .uint256 none
49+
some ("[" ++ String.intercalate ", " (tagComponent :: fieldComponents) ++ "]")
50+
| .newtypeOf _ baseType => abiComponents? baseType
4251
| .array t => abiComponents? t
4352
| .fixedArray t _ => abiComponents? t
4453
| _ => none
@@ -126,4 +135,50 @@ def writeContractABIFile (outDir : String) (spec : CompilationModel) : IO Unit :
126135
let path := s!"{outDir}/{spec.name}.abi.json"
127136
IO.FS.writeFile path (emitContractABIJson spec)
128137

138+
/-- Render the storage layout for a contract as a JSON object.
139+
Includes EIP-7201 namespace when present (#1730, Axis 4 Step 4d).
140+
The output is a JSON object with `"contract"`, `"storageNamespace"`,
141+
and `"fields"` keys. -/
142+
def emitContractStorageLayoutJson (spec : CompilationModel) : String :=
143+
let nsTerm := match spec.storageNamespace with
144+
| some ns => jsonString (toString ns)
145+
| none => "null"
146+
let fieldEntries := renderFields spec.fields 0
147+
"{" ++ joinJsonFields [
148+
s!"\"contract\": {jsonString spec.name}",
149+
s!"\"storageNamespace\": {nsTerm}",
150+
s!"\"fields\": [{String.intercalate ", " fieldEntries}]"
151+
] ++ "}\n"
152+
where
153+
renderFieldType : FieldType → String
154+
| .uint256 => "uint256"
155+
| .address => "address"
156+
| .adt name maxFields => s!"adt({name},{maxFields})"
157+
| .dynamicArray elemType => renderStorageArrayElemType elemType ++ "[]"
158+
| .mappingTyped _ => "mapping"
159+
| .mappingStruct _ _ => "mapping"
160+
| .mappingStruct2 _ _ _ => "mapping"
161+
renderStorageArrayElemType : StorageArrayElemType → String
162+
| .uint256 => "uint256"
163+
| .address => "address"
164+
| .bool => "bool"
165+
| .uint8 => "uint8"
166+
| .bytes32 => "bytes32"
167+
renderFields (fields : List Field) (idx : Nat) : List String :=
168+
match fields with
169+
| [] => []
170+
| f :: rest =>
171+
let slot := f.slot.getD idx
172+
let entry := "{" ++ joinJsonFields [
173+
s!"\"name\": {jsonString f.name}",
174+
s!"\"slot\": {jsonString (toString slot)}",
175+
s!"\"type\": {jsonString (renderFieldType f.ty)}"
176+
] ++ "}"
177+
entry :: renderFields rest (idx + 1)
178+
179+
def writeContractStorageLayoutFile (outDir : String) (spec : CompilationModel) : IO Unit := do
180+
IO.FS.createDirAll outDir
181+
let path := s!"{outDir}/{spec.name}.storage.json"
182+
IO.FS.writeFile path (emitContractStorageLayoutJson spec)
183+
129184
end Compiler.ABI

Compiler/CompilationModel.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import Compiler.CompilationModel.Types
88
import Compiler.CompilationModel.AbiHelpers
99
import Compiler.CompilationModel.AbiTypeLayout
10+
import Compiler.CompilationModel.AdtStorageLayout
1011
import Compiler.CompilationModel.AbiEncoding
1112
import Compiler.CompilationModel.DynamicData
1213
import Compiler.CompilationModel.EcmAxiomCollection

Compiler/CompilationModel/AbiEncoding.lean

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,25 @@ partial def compileUnindexedAbiEncode
250250
])
251251
], YulExpr.call "add" [YulExpr.lit 32, YulExpr.ident paddedName])
252252

253+
| ParamType.adt _ maxFields =>
254+
-- ADTs are ABI-encoded as (uint8 tag, uint256 field0, ..., uint256 fieldN)
255+
-- Tag word: load and mask to uint8
256+
let tagLoaded := dynamicWordLoad dynamicSource srcBase
257+
let tagStore := YulStmt.expr (YulExpr.call "mstore" [
258+
dstBase, YulExpr.call "and" [tagLoaded, YulExpr.lit 0xFF]
259+
])
260+
-- Field words: load consecutive words from source
261+
let fieldStores := (List.range maxFields).map fun i =>
262+
let srcOff := YulExpr.call "add" [srcBase, YulExpr.lit ((i + 1) * 32)]
263+
let dstOff := YulExpr.call "add" [dstBase, YulExpr.lit ((i + 1) * 32)]
264+
YulStmt.expr (YulExpr.call "mstore" [dstOff, dynamicWordLoad dynamicSource srcOff])
265+
let totalBytes := 32 * (1 + maxFields)
266+
pure (tagStore :: fieldStores, YulExpr.lit totalBytes)
267+
268+
| ParamType.newtypeOf _ baseType =>
269+
-- Newtypes erased to base type (#1727 Step 3b)
270+
compileUnindexedAbiEncode dynamicSource baseType srcBase dstBase stem
271+
253272
def revertWithCustomError (dynamicSource : DynamicDataSource)
254273
(errorDef : ErrorDef) (sourceArgs : List Expr) (args : List YulExpr) :
255274
Except String (List YulStmt) := do
@@ -282,6 +301,25 @@ def revertWithCustomError (dynamicSource : DynamicDataSource)
282301
| ParamType.uint256 | ParamType.int256 | ParamType.uint8 | ParamType.address | ParamType.bool | ParamType.bytes32 =>
283302
let encoded ← encodeStaticCustomErrorArg errorDef.name ty argExpr
284303
pure [YulStmt.expr (YulExpr.call "mstore" [YulExpr.lit headOffset, encoded])]
304+
| ParamType.adt _ maxFields =>
305+
match srcExpr with
306+
| Expr.param name =>
307+
let tagStore := YulStmt.expr (YulExpr.call "mstore" [
308+
YulExpr.lit headOffset,
309+
YulExpr.call "and" [argExpr, YulExpr.lit 0xFF]
310+
])
311+
let fieldStores := (List.range maxFields).map fun fieldIdx =>
312+
YulStmt.expr (YulExpr.call "mstore" [
313+
YulExpr.lit (headOffset + (fieldIdx + 1) * 32),
314+
YulExpr.ident s!"{name}_f{fieldIdx}"
315+
])
316+
pure (tagStore :: fieldStores)
317+
| _ =>
318+
throw s!"Compilation error: custom error '{errorDef.name}' parameter of type {repr ty} currently requires direct parameter reference ({issue586Ref})."
319+
| ParamType.newtypeOf _ baseType =>
320+
-- Newtypes erased to base type (#1727 Step 3b)
321+
let encoded ← encodeStaticCustomErrorArg errorDef.name baseType argExpr
322+
pure [YulStmt.expr (YulExpr.call "mstore" [YulExpr.lit headOffset, encoded])]
285323
| ParamType.tuple _ | ParamType.fixedArray _ _ =>
286324
match srcExpr with
287325
| Expr.param name =>

Compiler/CompilationModel/AbiHelpers.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ mutual
5656
| ParamType.array t => paramTypeToSolidityString t ++ "[]"
5757
| ParamType.fixedArray t n => paramTypeToSolidityString t ++ "[" ++ toString n ++ "]"
5858
| ParamType.bytes => "bytes"
59+
| ParamType.adt _name maxFields =>
60+
-- ABI-encoded as static tuple: (uint8, uint256, ..., uint256)
61+
"(" ++ String.intercalate "," ("uint8" :: List.replicate maxFields "uint256") ++ ")"
62+
| ParamType.newtypeOf _ baseType => paramTypeToSolidityString baseType -- Erased to base type
5963

6064
private def paramTypeListToSolidityStrings : List ParamType → List String
6165
| [] => []
@@ -79,6 +83,7 @@ def storageArrayElemTypeToParamType : StorageArrayElemType → ParamType
7983
def fieldTypeToParamType : FieldType → ParamType
8084
| FieldType.uint256 => ParamType.uint256
8185
| FieldType.address => ParamType.address
86+
| FieldType.adt name maxFields => ParamType.adt name maxFields
8287
| FieldType.dynamicArray elemType => ParamType.array (storageArrayElemTypeToParamType elemType)
8388
| FieldType.mappingTyped _ => ParamType.uint256
8489
| FieldType.mappingStruct _ _ => ParamType.uint256

Compiler/CompilationModel/AbiTypeLayout.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ mutual
1717
| ParamType.bytes => true
1818
| ParamType.fixedArray elemTy _ => isDynamicParamType elemTy
1919
| ParamType.tuple elemTys => isDynamicParamTypeList elemTys
20+
| ParamType.adt _ _ => false -- ADTs are statically-sized tagged unions
21+
| ParamType.newtypeOf _ baseType => isDynamicParamType baseType -- Erased to base type
2022
termination_by ty => sizeOf ty
2123

2224
def isDynamicParamTypeList : List ParamType → Bool
@@ -43,6 +45,8 @@ mutual
4345
if isDynamicParamType elemTy then 32 else n * paramHeadSize elemTy
4446
| ParamType.tuple elemTys =>
4547
if isDynamicParamTypeList elemTys then 32 else paramHeadSizeList elemTys
48+
| ParamType.adt _ maxFields => 32 * (1 + maxFields) -- ADTs: uint8 tag word + maxFields field words (#1727 Step 5e)
49+
| ParamType.newtypeOf _ baseType => paramHeadSize baseType -- Erased to base type
4650
termination_by ty => sizeOf ty
4751

4852
def paramHeadSizeList : List ParamType → Nat
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/-
2+
Compiler.CompilationModel.AdtStorageLayout: Storage layout helpers for ADTs
3+
4+
ADT storage encoding uses a tagged-union layout:
5+
- Slot 0 (relative): tag byte (uint8, identifies the variant)
6+
- Slots 1..N (relative): max-width fields in consecutive slots
7+
8+
The total storage footprint is 1 + max(variant field counts).
9+
All variants share the same field slots — unused trailing slots
10+
are simply not read/written for shorter variants.
11+
12+
(#1727, Phase 5 Steps 5c+5d)
13+
-/
14+
import Compiler.CompilationModel.Types
15+
16+
namespace Compiler.CompilationModel
17+
18+
open Compiler.Yul
19+
20+
/-- Look up an ADT type definition by name. -/
21+
def lookupAdtTypeDef (adtTypes : List AdtTypeDef) (name : String) :
22+
Except String AdtTypeDef :=
23+
match adtTypes.find? (·.name == name) with
24+
| some def_ => pure def_
25+
| none => throw s!"Compilation error: unknown ADT type '{name}'"
26+
27+
/-- Look up a variant within an ADT type definition by name. -/
28+
def lookupAdtVariant (def_ : AdtTypeDef) (variantName : String) :
29+
Except String AdtVariant :=
30+
match def_.variants.find? (·.name == variantName) with
31+
| some v => pure v
32+
| none => throw s!"Compilation error: unknown variant '{variantName}' in ADT '{def_.name}'"
33+
34+
/-- Maximum number of field slots across all variants. -/
35+
def adtMaxFieldSlots (def_ : AdtTypeDef) : Nat :=
36+
def_.variants.foldl (fun acc v => max acc v.fields.length) 0
37+
38+
/-! ### Yul compilation helpers for ADT storage operations
39+
40+
These generate Yul AST fragments for reading/writing ADT values
41+
stored in contract storage. The caller provides the base slot
42+
(resolved from the contract's field list) and the ADT type info.
43+
-/
44+
45+
/-- Read the tag byte from an ADT's storage slot.
46+
`baseSlot` is the Yul expression for the ADT field's first slot. -/
47+
def compileAdtTagRead (baseSlot : YulExpr) : YulExpr :=
48+
-- Tag is stored as a full word in the base slot (mask to uint8)
49+
YulExpr.call "and" [
50+
YulExpr.call "sload" [baseSlot],
51+
YulExpr.lit 0xFF
52+
]
53+
54+
/-- Write the tag byte to an ADT's storage base slot. -/
55+
def compileAdtTagWrite (baseSlot : YulExpr) (tagValue : Nat) : YulStmt :=
56+
YulStmt.expr (YulExpr.call "sstore" [baseSlot, YulExpr.lit tagValue])
57+
58+
/-- Read a field from an ADT variant in storage.
59+
`baseSlot` is the ADT's first slot, `fieldIndex` is 0-based within the variant.
60+
Fields occupy slots base+1, base+2, ... -/
61+
def compileAdtFieldRead (baseSlot : YulExpr) (fieldIndex : Nat) : YulExpr :=
62+
YulExpr.call "sload" [
63+
YulExpr.call "add" [baseSlot, YulExpr.lit (fieldIndex + 1)]
64+
]
65+
66+
/-- Write a field value into an ADT variant's storage slot.
67+
`baseSlot` is the ADT's first slot, `fieldIndex` is 0-based. -/
68+
def compileAdtFieldWrite (baseSlot : YulExpr) (fieldIndex : Nat)
69+
(valueExpr : YulExpr) : YulStmt :=
70+
YulStmt.expr (YulExpr.call "sstore" [
71+
YulExpr.call "add" [baseSlot, YulExpr.lit (fieldIndex + 1)],
72+
valueExpr
73+
])
74+
75+
end Compiler.CompilationModel

0 commit comments

Comments
 (0)