Skip to content

Commit f3debfa

Browse files
authored
Merge pull request #32 from refcell/feat/abi-json-emission
feat: emit ABI JSON from edgec
2 parents 5270d31 + a5ddeab commit f3debfa

13 files changed

Lines changed: 358 additions & 13 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ resolver = "2"
44

55
[workspace.package]
66
description = "Edge Language Compiler Workspace"
7-
version = "0.1.16"
7+
version = "0.1.17"
88
edition = "2021"
99
rust-version = "1.85"
1010
license = "MIT"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ edgec parse examples/counter.edge
8080

8181
# Type-check only (no codegen)
8282
edgec check examples/counter.edge
83+
84+
# Print the contract ABI as JSON
85+
edgec --emit abi examples/counter.edge
86+
# [{"type":"function","name":"increment","inputs":[],"outputs":[],"stateMutability":"view"}, ...]
8387
```
8488

8589
### Example: counter contract

bin/edgec/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ clap = { workspace = true }
1717
anyhow = { workspace = true }
1818
tracing = { workspace = true }
1919
tracing-subscriber = { workspace = true, features = ["env-filter"] }
20+
serde_json = { workspace = true }
2021
edge-codegen = { workspace = true }
2122
edge-driver = { workspace = true }
2223
edge-ir = { workspace = true }

bin/edgec/src/cli.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ pub struct Cli {
2323
#[arg(short, long, requires = "file")]
2424
pub output: Option<PathBuf>,
2525

26-
/// What to emit: tokens, ast, ir, pretty-ir, asm, bytecode
27-
#[arg(long, value_parser = ["tokens", "ast", "ir", "pretty-ir", "asm", "bytecode"], default_value = "bytecode")]
26+
/// What to emit: tokens, ast, ir, pretty-ir, asm, abi, bytecode
27+
#[arg(long, value_parser = ["tokens", "ast", "ir", "pretty-ir", "asm", "abi", "bytecode"], default_value = "bytecode")]
2828
pub emit: String,
2929

3030
/// Optimization level (0-3)
@@ -106,6 +106,7 @@ impl Cli {
106106
"ir" => EmitKind::Ir,
107107
"pretty-ir" => EmitKind::PrettyIr,
108108
"asm" => EmitKind::Asm,
109+
"abi" => EmitKind::Abi,
109110
"bytecode" => EmitKind::Bytecode,
110111
_ => EmitKind::Bytecode,
111112
};
@@ -187,6 +188,11 @@ impl Cli {
187188
}
188189
}
189190
}
191+
EmitKind::Abi => {
192+
if let Some(ref abi) = result.abi {
193+
println!("{}", serde_json::to_string_pretty(abi).unwrap());
194+
}
195+
}
190196
EmitKind::Bytecode => {
191197
if let Some(ref bytecode) = result.bytecode {
192198
if bytecode.is_empty() {

crates/driver/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ source -> lexer -> parser -> AST -> typeck -> IR -> codegen -> driver -> bytecod
2020
## Key Types
2121

2222
- **`Compiler`** -- Main entry point; constructed with a `CompilerConfig`, drives `compile()`
23-
- **`CompileOutput`** -- Holds optional tokens, AST, or bytecode depending on emit mode
23+
- **`CompileOutput`** -- Holds optional tokens, AST, ABI, or bytecode depending on emit mode
2424
- **`CompileError`** -- Covers I/O, lex, parse, type-check, IR lowering, and codegen failures
2525
- **`CompilerConfig`** -- Input file path, output path, emit kind, optimization level, verbose flag
26-
- **`EmitKind`** -- What the compiler should produce: `Tokens`, `Ast`, or `Bytecode`
26+
- **`EmitKind`** -- What the compiler should produce: `Tokens`, `Ast`, `Abi`, or `Bytecode`
2727
- **`Session`** -- Per-compilation state: config, source text, and accumulated diagnostics
2828

2929
## Usage
@@ -39,6 +39,10 @@ let output = compiler.compile().unwrap();
3939
if let Some(bytecode) = output.bytecode {
4040
println!("{}", hex::encode(&bytecode));
4141
}
42+
43+
if let Some(abi) = &output.abi {
44+
println!("{}", serde_json::to_string_pretty(abi).unwrap());
45+
}
4246
```
4347

4448
## Integration

crates/driver/src/compiler.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub struct CompileOutput {
3333
pub bytecodes: Option<IndexMap<String, Vec<u8>>>,
3434
/// Emitted assembly (if emit=asm), keyed by contract name
3535
pub asm: Option<Vec<(String, edge_codegen::AsmOutput)>>,
36+
/// Emitted ABI JSON entries (if emit=abi)
37+
pub abi: Option<Vec<edge_typeck::AbiEntry>>,
3638
}
3739

3840
/// Compiler errors
@@ -125,6 +127,7 @@ impl Compiler {
125127
bytecode: None,
126128
bytecodes: None,
127129
asm: None,
130+
abi: None,
128131
});
129132
}
130133

@@ -142,17 +145,35 @@ impl Compiler {
142145
bytecode: None,
143146
bytecodes: None,
144147
asm: None,
148+
abi: None,
145149
});
146150
}
147151

148152
// Type check pass
149-
let _checked = edge_typeck::TypeChecker::new().check(&ast).map_err(|e| {
153+
let checked = edge_typeck::TypeChecker::new().check(&ast).map_err(|e| {
150154
self.session
151155
.emit_error(Diagnostic::error(format!("type error: {e}")));
152156
self.session.report_diagnostics();
153157
CompileError::TypeCheckErrors
154158
})?;
155159

160+
// ABI extraction — return early if that's all the user requested
161+
if emit == EmitKind::Abi {
162+
let mut all_entries = Vec::new();
163+
for contract in &checked.contracts {
164+
all_entries.extend(edge_typeck::extract_abi(contract, &checked.events));
165+
}
166+
return Ok(CompileOutput {
167+
tokens: None,
168+
ast: None,
169+
ir: None,
170+
bytecode: None,
171+
bytecodes: None,
172+
asm: None,
173+
abi: Some(all_entries),
174+
});
175+
}
176+
156177
// IR lowering + optimization
157178
let ir_program = edge_ir::lower_and_optimize(
158179
&ast,
@@ -180,6 +201,7 @@ impl Compiler {
180201
bytecode: None,
181202
bytecodes: None,
182203
asm: None,
204+
abi: None,
183205
});
184206
}
185207

@@ -192,6 +214,7 @@ impl Compiler {
192214
bytecode: None,
193215
bytecodes: None,
194216
asm: None,
217+
abi: None,
195218
});
196219
}
197220

@@ -221,6 +244,7 @@ impl Compiler {
221244
bytecode: None,
222245
bytecodes: None,
223246
asm: Some(asm_outputs),
247+
abi: None,
224248
});
225249
}
226250

@@ -263,6 +287,7 @@ impl Compiler {
263287
bytecode: Some(bytecode),
264288
bytecodes: None,
265289
asm: None,
290+
abi: None,
266291
});
267292
}
268293

@@ -275,6 +300,7 @@ impl Compiler {
275300
bytecode: last_bytecode,
276301
bytecodes: Some(all_bytecodes),
277302
asm: None,
303+
abi: None,
278304
})
279305
}
280306

crates/driver/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub enum EmitKind {
1717
PrettyIr,
1818
/// Emit post-optimization assembly (disassembly with labeled blocks)
1919
Asm,
20+
/// Emit Ethereum-compatible ABI JSON
21+
Abi,
2022
/// Emit EVM bytecode (default)
2123
#[default]
2224
Bytecode,

crates/typeck/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ thiserror = { workspace = true }
1515
indexmap = { workspace = true }
1616
tiny-keccak = { workspace = true }
1717
alloy-primitives = { workspace = true }
18+
serde = { workspace = true, features = ["derive"] }
19+
20+
[dev-dependencies]
21+
edge-driver = { workspace = true }
22+
serde_json = { workspace = true }
1823

1924
[lints]
2025
workspace = true

crates/typeck/src/abi.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! ABI JSON types and extraction for Edge contracts.
2+
//!
3+
//! Produces Ethereum-compatible ABI descriptors from type-checked contract info.
4+
5+
use edge_ast::item::EventDecl;
6+
use serde::Serialize;
7+
8+
use crate::checker::{ContractInfo, TypeChecker};
9+
10+
/// State mutability of a function in the ABI.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12+
#[serde(rename_all = "camelCase")]
13+
pub enum StateMutability {
14+
/// Function does not read or write state
15+
Pure,
16+
/// Function reads but does not write state
17+
View,
18+
/// Function may write state (no ETH value accepted)
19+
NonPayable,
20+
/// Function accepts ETH value
21+
Payable,
22+
}
23+
24+
/// A single parameter in an ABI function or event.
25+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26+
#[serde(rename_all = "camelCase")]
27+
pub struct AbiParam {
28+
/// Parameter name (empty string for unnamed return values)
29+
pub name: String,
30+
/// Solidity ABI type string (e.g. "uint256", "address")
31+
#[serde(rename = "type")]
32+
pub ty: String,
33+
}
34+
35+
/// A single parameter in an ABI event.
36+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37+
#[serde(rename_all = "camelCase")]
38+
pub struct AbiEventParam {
39+
/// Parameter name
40+
pub name: String,
41+
/// Solidity ABI type string
42+
#[serde(rename = "type")]
43+
pub ty: String,
44+
/// Whether this parameter is indexed
45+
pub indexed: bool,
46+
}
47+
48+
/// A top-level ABI entry (function or event).
49+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50+
#[serde(tag = "type", rename_all = "camelCase")]
51+
pub enum AbiEntry {
52+
/// A function entry
53+
Function(AbiFunctionEntry),
54+
/// An event entry
55+
Event(AbiEventEntry),
56+
}
57+
58+
/// ABI descriptor for a function.
59+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
60+
#[serde(rename_all = "camelCase")]
61+
pub struct AbiFunctionEntry {
62+
/// Function name
63+
pub name: String,
64+
/// Input parameters
65+
pub inputs: Vec<AbiParam>,
66+
/// Output parameters
67+
pub outputs: Vec<AbiParam>,
68+
/// State mutability
69+
pub state_mutability: StateMutability,
70+
}
71+
72+
/// ABI descriptor for an event.
73+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
74+
#[serde(rename_all = "camelCase")]
75+
pub struct AbiEventEntry {
76+
/// Event name
77+
pub name: String,
78+
/// Event parameters
79+
pub inputs: Vec<AbiEventParam>,
80+
/// Whether the event is anonymous
81+
pub anonymous: bool,
82+
}
83+
84+
/// Extract an Ethereum-compatible ABI from a type-checked contract and top-level events.
85+
pub fn extract_abi(contract: &ContractInfo, events: &[EventDecl]) -> Vec<AbiEntry> {
86+
let mut entries = Vec::new();
87+
88+
// Functions: only include public functions
89+
for func in &contract.functions {
90+
if !func.is_pub {
91+
continue;
92+
}
93+
94+
let inputs = func
95+
.params
96+
.iter()
97+
.map(|(name, ty)| AbiParam {
98+
name: name.clone(),
99+
ty: TypeChecker::type_to_abi_string(ty),
100+
})
101+
.collect();
102+
103+
let outputs = func
104+
.returns
105+
.iter()
106+
.map(|ty| AbiParam {
107+
name: String::new(),
108+
ty: TypeChecker::type_to_abi_string(ty),
109+
})
110+
.collect();
111+
112+
let state_mutability = if func.is_mut {
113+
StateMutability::NonPayable
114+
} else {
115+
StateMutability::View
116+
};
117+
118+
entries.push(AbiEntry::Function(AbiFunctionEntry {
119+
name: func.name.clone(),
120+
inputs,
121+
outputs,
122+
state_mutability,
123+
}));
124+
}
125+
126+
// Events
127+
for event in events {
128+
let inputs = event
129+
.fields
130+
.iter()
131+
.map(|field| AbiEventParam {
132+
name: field.name.name.clone(),
133+
ty: TypeChecker::type_to_abi_string(&field.ty),
134+
indexed: field.indexed,
135+
})
136+
.collect();
137+
138+
entries.push(AbiEntry::Event(AbiEventEntry {
139+
name: event.name.name.clone(),
140+
inputs,
141+
anonymous: event.is_anon,
142+
}));
143+
}
144+
145+
entries
146+
}

0 commit comments

Comments
 (0)