Skip to content

Commit d65c000

Browse files
authored
feat(slang): encode require string literal message (#346)
Extract the string literal from the second argument of `require(cond, "msg")` and pass it into `sol.require`'s `msg` attribute. Previously the message was always hardcoded to an empty string, causing require reverts to lose the error reason. Newly passing tests: - `tests/solidity/simple/conditional/require.sol` (all)
1 parent 54a46a7 commit d65c000

11 files changed

Lines changed: 119 additions & 41 deletions

File tree

Cargo.lock

Lines changed: 25 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solx-mlir/sol_attr_stubs.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,11 @@ MlirType solxCreateContractType(MlirContext ctx, const char *name_ptr,
6969
return wrap(mlir::sol::ContractType::get(context, name, payable));
7070
}
7171

72+
MlirType solxCreateStringType(MlirContext ctx, uint32_t dataLocation) {
73+
if (dataLocation > 5) abort();
74+
auto *context = unwrap(ctx);
75+
auto location = static_cast<mlir::sol::DataLocation>(dataLocation);
76+
return wrap(mlir::sol::StringType::get(context, location));
77+
}
78+
7279
} /* extern "C" */

solx-mlir/src/context/builder/mod.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use crate::ods::sol::ReturnOperation;
5353
use crate::ods::sol::RevertOperation;
5454
use crate::ods::sol::StateVarOperation;
5555
use crate::ods::sol::StoreOperation;
56+
use crate::ods::sol::StringLitOperation;
5657
use crate::ods::sol::WhileOperation;
5758
use crate::ods::sol::YieldOperation;
5859

@@ -311,6 +312,31 @@ impl<'context> Builder<'context> {
311312
.into()
312313
}
313314

315+
// ==== String literals ====
316+
317+
/// Emits a `sol.string_lit` constant with a `!sol.string<Memory>` result.
318+
///
319+
/// # Panics
320+
///
321+
/// Panics if the MLIR operation cannot be constructed, indicating a bug in the builder.
322+
pub fn emit_sol_string_lit<'block, B>(&self, value: &str, block: &B) -> Value<'context, 'block>
323+
where
324+
B: BlockLike<'context, 'block>,
325+
'context: 'block,
326+
{
327+
block
328+
.append_operation(
329+
StringLitOperation::builder(self.context, self.unknown_location)
330+
.value(StringAttribute::new(self.context, value))
331+
.addr(self.types.sol_string_memory)
332+
.build()
333+
.into(),
334+
)
335+
.result(0)
336+
.expect("sol.string_lit always produces one result")
337+
.into()
338+
}
339+
314340
// ==== Terminators ====
315341

316342
/// Emits a `sol.revert` with an empty signature (no error data).
@@ -352,27 +378,31 @@ impl<'context> Builder<'context> {
352378
);
353379
}
354380

355-
/// Emits a `sol.require` conditional revert with an empty signature.
381+
/// Emits a `sol.require` conditional revert with an optional message.
356382
///
357-
/// Reverts if `condition` is false. Not a terminator — execution continues
358-
/// after this op when the condition is true.
383+
/// Reverts if `condition` is false. When `msg` is `Some`, the revert
384+
/// includes the string as a revert reason. Not a terminator — execution
385+
/// continues after this op when the condition is true.
359386
///
360387
/// # Panics
361388
///
362389
/// Panics if the MLIR operation cannot be constructed, indicating a bug in the builder.
363-
pub fn emit_sol_require<'block, B>(&self, condition: Value<'context, 'block>, block: &B)
364-
where
390+
pub fn emit_sol_require<'block, B>(
391+
&self,
392+
condition: Value<'context, 'block>,
393+
msg: Option<&str>,
394+
block: &B,
395+
) where
365396
B: BlockLike<'context, 'block>,
366397
'context: 'block,
367398
{
368-
block.append_operation(
369-
RequireOperation::builder(self.context, self.unknown_location)
370-
.cond(condition)
371-
.msg(StringAttribute::new(self.context, ""))
372-
.args(&[])
373-
.build()
374-
.into(),
375-
);
399+
let mut builder = RequireOperation::builder(self.context, self.unknown_location)
400+
.cond(condition)
401+
.args(&[]);
402+
if let Some(msg) = msg {
403+
builder = builder.msg(StringAttribute::new(self.context, msg));
404+
}
405+
block.append_operation(builder.build().into());
376406
}
377407

378408
/// Emits a `sol.return` terminator.

solx-mlir/src/context/builder/type_factory.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub struct TypeFactory<'context> {
2626
pub sol_address: Type<'context>,
2727
/// Sol storage pointer type (`!sol.ptr<ui256, Storage>`).
2828
pub sol_ptr_storage: Type<'context>,
29+
/// Sol memory string type (`!sol.string<Memory>`).
30+
pub sol_string_memory: Type<'context>,
2931
}
3032

3133
impl<'context> TypeFactory<'context> {
@@ -59,13 +61,22 @@ impl<'context> TypeFactory<'context> {
5961
solx_utils::DataLocation::Storage as u32,
6062
))
6163
};
64+
// SAFETY: `solxCreateStringType` returns a valid MlirType from
65+
// the C++ Sol dialect. The context pointer is valid.
66+
let sol_string_memory = unsafe {
67+
Type::from_raw(crate::ffi::solxCreateStringType(
68+
context.to_raw(),
69+
solx_utils::DataLocation::Memory as u32,
70+
))
71+
};
6272
Self {
6373
context,
6474
i1,
6575
ui160,
6676
ui256,
6777
sol_address,
6878
sol_ptr_storage,
79+
sol_string_memory,
6980
}
7081
}
7182

solx-mlir/src/ffi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ unsafe extern "C" {
104104
payable: bool,
105105
) -> mlir_sys::MlirType;
106106

107+
/// Creates a `sol::StringType` with the given data location.
108+
///
109+
/// `data_location` maps to `mlir::sol::DataLocation` (0=Storage, 1=CallData,
110+
/// 2=Memory, 3=Stack, 4=Immutable, 5=Transient).
111+
pub fn solxCreateStringType(context: MlirContext, data_location: u32) -> mlir_sys::MlirType;
112+
107113
// ---- MLIR core (not in mlir-sys) ----
108114

109115
/// Returns the region that owns the given block.

solx-slang/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ semver.workspace = true
1616
# `__private_testing_utils` is required by `__private_backend_api` internals
1717
# (binder/types access in node_extensions). Both are unstable Slang APIs.
1818
# TODO: pin to a release tag instead of branch = "main"
19-
slang_solidity = { git = "https://github.com/NomicFoundation/slang.git", rev = "a01bc76b742173941cea1aff0b3b68a9006b38b8", features = ["__private_backend_api", "__private_testing_utils"] }
19+
slang_solidity = { git = "https://github.com/NomicFoundation/slang.git", rev = "6f892dff8e2d6d275cfe9737c3cb364486c41410", features = ["__private_backend_api", "__private_testing_utils"] }
2020
melior = { git = "https://github.com/NomicFoundation/melior", rev = "62a909e2ff67fcc19f7f42dc4fb70c3e03376bd2", features = ["ods-dialects", "helpers"] }
2121

2222
solx-codegen-evm = { path = "../solx-codegen-evm" }

solx-slang/src/ast/contract/function/expression/arithmetic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl<'state, 'context, 'block> ExpressionEmitter<'state, 'context, 'block> {
132132
.state
133133
.builder
134134
.emit_sol_cmp(value, zero, CmpPredicate::Eq, &block);
135-
let result_type = target_type.unwrap_or_else(|| self.state.builder.types.ui256);
135+
let result_type = target_type.unwrap_or(self.state.builder.types.ui256);
136136
let result = TypeConversion::from_target_type(result_type, &self.state.builder)
137137
.emit(cmp, &self.state.builder, &block);
138138
Ok((result, block))

solx-slang/src/ast/contract/function/expression/call/mod.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,18 @@ impl<'emitter, 'state, 'context, 'block> CallEmitter<'emitter, 'state, 'context,
102102
&& (positional_arguments.len() == 1
103103
|| positional_arguments.len() == Self::MAX_REQUIRE_ARGUMENTS)
104104
{
105-
// TODO: encode revert reason from second argument
106-
let first = positional_arguments
107-
.iter()
108-
.next()
109-
.expect("length checked above");
110-
let block = self.emit_require(&first, block)?;
105+
let mut args = positional_arguments.iter();
106+
let condition = args.next().expect("length checked above");
107+
let message_arg = args.next();
108+
let message = match &message_arg {
109+
Some(Expression::StringExpression(string_expression)) => {
110+
let bytes = string_expression.value();
111+
Some(String::from_utf8(bytes).expect("require message is valid UTF-8"))
112+
}
113+
Some(_) => anyhow::bail!("require message must be a string literal"),
114+
None => None,
115+
};
116+
let block = self.emit_require(&condition, message.as_deref(), block)?;
111117
return Ok((None, block));
112118
}
113119

@@ -242,10 +248,12 @@ impl<'emitter, 'state, 'context, 'block> CallEmitter<'emitter, 'state, 'context,
242248
Ok(block)
243249
}
244250

245-
/// Emits a `require(condition)` built-in via `sol.require`.
251+
/// Emits a `require(condition)` or `require(condition, "message")` built-in
252+
/// via `sol.require`.
246253
fn emit_require(
247254
&self,
248255
condition: &Expression,
256+
message: Option<&str>,
249257
block: BlockRef<'context, 'block>,
250258
) -> anyhow::Result<BlockRef<'context, 'block>> {
251259
let (condition_value, block) = self.expression_emitter.emit_value(condition, block)?;
@@ -256,7 +264,7 @@ impl<'emitter, 'state, 'context, 'block> CallEmitter<'emitter, 'state, 'context,
256264
self.expression_emitter
257265
.state
258266
.builder
259-
.emit_sol_require(condition_boolean, &block);
267+
.emit_sol_require(condition_boolean, message, &block);
260268

261269
Ok(block)
262270
}

solx-slang/src/ast/contract/function/expression/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ impl<'state, 'context, 'block> ExpressionEmitter<'state, 'context, 'block> {
154154
.into();
155155
Ok((Some(value), block))
156156
}
157+
Expression::StringExpression(string_expression) => {
158+
let bytes = string_expression.value();
159+
let text = std::str::from_utf8(&bytes).expect("string literal is valid UTF-8");
160+
let value = self.state.builder.emit_sol_string_lit(text, &block);
161+
Ok((Some(value), block))
162+
}
157163
Expression::Identifier(identifier) => {
158164
let name = identifier.name();
159165
match identifier.resolve_to_definition() {
@@ -302,7 +308,7 @@ impl<'state, 'context, 'block> ExpressionEmitter<'state, 'context, 'block> {
302308
Expression::ConditionalExpression(conditional) => {
303309
let result_type = self
304310
.resolve_expression_type(conditional.node_id())
305-
.unwrap_or_else(|| self.state.builder.types.ui256);
311+
.unwrap_or(self.state.builder.types.ui256);
306312
let condition = conditional.operand();
307313
let (condition_value, block) = self.emit_value(&condition, block)?;
308314
let condition_boolean = self.emit_is_nonzero(condition_value, &block);

0 commit comments

Comments
 (0)