@@ -307,6 +307,15 @@ inductive Stmt
307307 (resultVars : List String) -- local vars to bind return values to
308308 (externalName : String) -- name of the external function declaration
309309 (args : List Expr) -- call arguments
310+ /-- High-level ABI-encoded external call with return value binding.
311+ Compiles to: mstore(selector+args), call/staticcall, revert forwarding, returndatacopy+mload.
312+ Covers the common `call(gas(), target, 0, 0, calldataSize, 0, 32)` + decode pattern. -/
313+ | externalCallWithReturn
314+ (resultVar : String) -- local variable to bind the returned uint256
315+ (target : Expr) -- target contract address
316+ (selector : Nat) -- 4-byte function selector (e.g., 0xa035b1fe)
317+ (args : List Expr) -- ABI-encoded arguments (each occupies 32 bytes)
318+ (isStatic : Bool := false ) -- use staticcall instead of call
310319 deriving Repr
311320
312321structure FunctionSpec where
@@ -633,6 +642,8 @@ private partial def collectStmtNames : Stmt → List String
633642 collectExprListNames topics ++ collectExprNames dataOffset ++ collectExprNames dataSize
634643 | Stmt.externalCallBind resultVars externalName args =>
635644 resultVars ++ externalName :: collectExprListNames args
645+ | Stmt.externalCallWithReturn resultVar target _ args _ =>
646+ resultVar :: collectExprNames target ++ collectExprListNames args
636647
637648private partial def collectStmtListNames : List Stmt → List String
638649 | [] => []
@@ -1098,6 +1109,8 @@ private partial def stmtContainsUnsafeLogicalCallLike : Stmt → Bool
10981109 exprContainsUnsafeLogicalCallLike dataSize
10991110 | Stmt.externalCallBind _ _ args =>
11001111 args.any exprContainsUnsafeLogicalCallLike
1112+ | Stmt.externalCallWithReturn _ target _ args _ =>
1113+ exprContainsUnsafeLogicalCallLike target || args.any exprContainsUnsafeLogicalCallLike
11011114
11021115private partial def staticParamBindingNames (name : String) (ty : ParamType) : List String :=
11031116 match ty with
@@ -1327,6 +1340,14 @@ private partial def validateScopedStmtIdentifiers
13271340 | Stmt.externalCallBind resultVars _ args => do
13281341 args.forM (validateScopedExprIdentifiers context params paramScope dynamicParams localScope constructorArgCount)
13291342 pure (resultVars.reverse ++ localScope)
1343+ | Stmt.externalCallWithReturn resultVar target _ args _ => do
1344+ validateScopedExprIdentifiers context params paramScope dynamicParams localScope constructorArgCount target
1345+ args.forM (validateScopedExprIdentifiers context params paramScope dynamicParams localScope constructorArgCount)
1346+ if paramScope.contains resultVar then
1347+ throw s! "Compilation error: { context} uses Stmt.externalCallWithReturn with result variable '{ resultVar} ' that shadows a parameter"
1348+ if localScope.contains resultVar then
1349+ throw s! "Compilation error: { context} uses Stmt.externalCallWithReturn with result variable '{ resultVar} ' that redeclares an existing local variable"
1350+ pure (resultVar :: localScope)
13301351 | _ => pure localScope
13311352
13321353private partial def validateScopedStmtListIdentifiers
@@ -1563,6 +1584,8 @@ private partial def stmtWritesState : Stmt → Bool
15631584 args.any exprWritesState || true
15641585 | Stmt.externalCallBind _ _ args =>
15651586 args.any exprWritesState || true
1587+ | Stmt.externalCallWithReturn _ target _ args isStatic =>
1588+ exprWritesState target || args.any exprWritesState || !isStatic
15661589where
15671590 exprWritesState (expr : Expr) : Bool :=
15681591 match expr with
@@ -1639,6 +1662,8 @@ private partial def stmtReadsStateOrEnv : Stmt → Bool
16391662 args.any exprReadsStateOrEnv || true
16401663 | Stmt.externalCallBind _ _ args =>
16411664 args.any exprReadsStateOrEnv || true
1665+ | Stmt.externalCallWithReturn _ target _ args _ =>
1666+ exprReadsStateOrEnv target || args.any exprReadsStateOrEnv || true
16421667
16431668private def validateFunctionSpec (spec : FunctionSpec) : Except String Unit := do
16441669 if spec.isPayable && (spec.isView || spec.isPure) then
@@ -2233,6 +2258,9 @@ private partial def validateInteropStmt (context : String) : Stmt → Except Str
22332258 args.forM (validateInteropExpr context)
22342259 | Stmt.externalCallBind _ _ args =>
22352260 args.forM (validateInteropExpr context)
2261+ | Stmt.externalCallWithReturn _ target _ args _ => do
2262+ validateInteropExpr context target
2263+ args.forM (validateInteropExpr context)
22362264 | Stmt.returnValues values =>
22372265 values.forM (validateInteropExpr context)
22382266 | Stmt.rawLog topics dataOffset dataSize => do
@@ -2460,6 +2488,9 @@ private partial def validateInternalCallShapesInStmt
24602488 validateInternalCallShapesInExpr functions callerName dataSize
24612489 | Stmt.externalCallBind _resultVars _ args =>
24622490 args.forM (validateInternalCallShapesInExpr functions callerName)
2491+ | Stmt.externalCallWithReturn _ target _ args _ => do
2492+ validateInternalCallShapesInExpr functions callerName target
2493+ args.forM (validateInternalCallShapesInExpr functions callerName)
24632494 | _ =>
24642495 pure ()
24652496
@@ -2606,6 +2637,9 @@ private partial def validateExternalCallTargetsInStmt
26062637 else
26072638 checkDuplicateVars (name :: seen) rest
26082639 checkDuplicateVars [] resultVars
2640+ | Stmt.externalCallWithReturn _ target _ args _ => do
2641+ validateExternalCallTargetsInExpr externals context target
2642+ args.forM (validateExternalCallTargetsInExpr externals context)
26092643 | Stmt.returnValues values =>
26102644 values.forM (validateExternalCallTargetsInExpr externals context)
26112645 | Stmt.rawLog topics dataOffset dataSize => do
@@ -3751,6 +3785,52 @@ def compileStmt (fields : List Field) (events : List EventDef := [])
37513785 | Stmt.externalCallBind resultVars externalName args => do
37523786 let argExprs ← compileExprList fields dynamicSource args
37533787 pure [YulStmt.letMany resultVars (YulExpr.call externalName argExprs)]
3788+ | Stmt.externalCallWithReturn resultVar target selector args isStatic => do
3789+ let targetExpr ← compileExpr fields dynamicSource target
3790+ let argCompiledExprs ← compileExprList fields dynamicSource args
3791+ -- Step 1: store selector (left-shifted 224 bits) at memory offset 0
3792+ let selectorExpr := YulExpr.call "shl" [YulExpr.lit 224 , YulExpr.hex selector]
3793+ let storeSelector := YulStmt.expr (YulExpr.call "mstore" [YulExpr.lit 0 , selectorExpr])
3794+ -- Step 2: store each arg at offsets 4, 36, 68, ...
3795+ let storeArgs := argCompiledExprs.zipIdx.map fun (argExpr, i) =>
3796+ YulStmt.expr (YulExpr.call "mstore" [YulExpr.lit (4 + i * 32 ), argExpr])
3797+ -- Step 3: perform call/staticcall
3798+ let calldataSize := 4 + args.length * 32
3799+ let callExpr :=
3800+ if isStatic then
3801+ YulExpr.call "staticcall" [
3802+ YulExpr.call "gas" [],
3803+ targetExpr,
3804+ YulExpr.lit 0 , YulExpr.lit calldataSize,
3805+ YulExpr.lit 0 , YulExpr.lit 32
3806+ ]
3807+ else
3808+ YulExpr.call "call" [
3809+ YulExpr.call "gas" [],
3810+ targetExpr,
3811+ YulExpr.lit 0 ,
3812+ YulExpr.lit 0 , YulExpr.lit calldataSize,
3813+ YulExpr.lit 0 , YulExpr.lit 32
3814+ ]
3815+ let letSuccess := YulStmt.let_ "__ecwr_success" callExpr
3816+ -- Step 4: revert forwarding on failure
3817+ let revertBlock := YulStmt.if_ (YulExpr.call "iszero" [YulExpr.ident "__ecwr_success" ]) [
3818+ YulStmt.let_ "__ecwr_rds" (YulExpr.call "returndatasize" []),
3819+ YulStmt.expr (YulExpr.call "returndatacopy" [YulExpr.lit 0 , YulExpr.lit 0 , YulExpr.ident "__ecwr_rds" ]),
3820+ YulStmt.expr (YulExpr.call "revert" [YulExpr.lit 0 , YulExpr.ident "__ecwr_rds" ])
3821+ ]
3822+ -- Step 5: validate return data size ≥ 32
3823+ let sizeCheck := YulStmt.if_ (YulExpr.call "lt" [YulExpr.call "returndatasize" [], YulExpr.lit 32 ]) [
3824+ YulStmt.expr (YulExpr.call "revert" [YulExpr.lit 0 , YulExpr.lit 0 ])
3825+ ]
3826+ -- Wrap call + checks in a block so __ecwr_success is block-scoped.
3827+ -- This avoids duplicate let declarations when multiple externalCallWithReturn
3828+ -- statements appear in the same function body.
3829+ let callBlock := YulStmt.block ([storeSelector] ++ storeArgs ++ [letSuccess, revertBlock, sizeCheck])
3830+ -- Step 6: extract return value outside the block (call already copied returndata to memory[ 0..32 ] )
3831+ -- resultVar is flat-scoped so subsequent statements can reference it.
3832+ let bindResult := YulStmt.let_ resultVar (YulExpr.call "mload" [YulExpr.lit 0 ])
3833+ pure [callBlock, bindResult]
37543834 | Stmt.returnValues values => do
37553835 if isInternal then
37563836 if values.length != internalRetNames.length then
@@ -4019,6 +4099,7 @@ private partial def collectStmtBindNames : Stmt → List String
40194099 varName :: collectStmtListBindNames body
40204100 | Stmt.internalCallAssign names _ _ => names
40214101 | Stmt.externalCallBind resultVars _ _ => resultVars
4102+ | Stmt.externalCallWithReturn resultVar _ _ _ _ => [resultVar]
40224103 | _ => []
40234104
40244105private partial def collectStmtListBindNames : List Stmt → List String
@@ -4091,6 +4172,8 @@ private partial def stmtUsesArrayElement : Stmt → Bool
40914172 topics.any exprUsesArrayElement || exprUsesArrayElement dataOffset || exprUsesArrayElement dataSize
40924173 | Stmt.externalCallBind _ _ args =>
40934174 args.any exprUsesArrayElement
4175+ | Stmt.externalCallWithReturn _ target _ args _ =>
4176+ exprUsesArrayElement target || args.any exprUsesArrayElement
40944177 | _ => false
40954178
40964179private def functionUsesArrayElement (fn : FunctionSpec) : Bool :=
0 commit comments