1use proc_macro::TokenStream;
24use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
25use quote::{quote, ToTokens};
26use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, FnArg, Ident};
27
28#[proc_macro_attribute]
39pub fn define_env(attr: TokenStream, item: TokenStream) -> TokenStream {
40 if !attr.is_empty() {
41 let msg = r#"Invalid `define_env` attribute macro: expected no attributes:
42 - `#[define_env]`"#;
43 let span = TokenStream2::from(attr).span();
44 return syn::Error::new(span, msg).to_compile_error().into();
45 }
46
47 let item = syn::parse_macro_input!(item as syn::ItemMod);
48
49 match EnvDef::try_from(item) {
50 Ok(mut def) => expand_env(&mut def).into(),
51 Err(e) => e.to_compile_error().into(),
52 }
53}
54
55struct EnvDef {
57 host_funcs: Vec<HostFn>,
58}
59
60struct HostFn {
62 item: syn::ItemFn,
63 name: String,
64 returns: HostFnReturn,
65 cfg: Option<syn::Attribute>,
66}
67
68enum HostFnReturn {
69 Unit,
70 U32,
71 U64,
72 ReturnCode,
73}
74
75impl HostFnReturn {
76 fn map_output(&self) -> TokenStream2 {
77 match self {
78 Self::Unit => quote! { |_| None },
79 _ => quote! { |ret_val| Some(ret_val.into()) },
80 }
81 }
82
83 fn success_type(&self) -> syn::ReturnType {
84 match self {
85 Self::Unit => syn::ReturnType::Default,
86 Self::U32 => parse_quote! { -> u32 },
87 Self::U64 => parse_quote! { -> u64 },
88 Self::ReturnCode => parse_quote! { -> ReturnErrorCode },
89 }
90 }
91
92 fn trace_return_value(&self) -> TokenStream2 {
93 match self {
94 Self::Unit => quote! { None },
95 Self::U32 => quote! { result.as_ref().ok().map(|r| *r as u64) },
96 Self::ReturnCode => quote! { result.as_ref().ok().copied().map(u64::from) },
97 Self::U64 => quote! { result.as_ref().ok().copied() },
98 }
99 }
100}
101
102impl EnvDef {
103 pub fn try_from(item: syn::ItemMod) -> syn::Result<Self> {
104 let span = item.span();
105 let err = |msg| syn::Error::new(span, msg);
106 let items = &item
107 .content
108 .as_ref()
109 .ok_or(err("Invalid environment definition, expected `mod` to be inlined."))?
110 .1;
111
112 let extract_fn = |i: &syn::Item| match i {
113 syn::Item::Fn(i_fn) => Some(i_fn.clone()),
114 _ => None,
115 };
116
117 let host_funcs = items
118 .iter()
119 .filter_map(extract_fn)
120 .map(HostFn::try_from)
121 .collect::<Result<Vec<_>, _>>()?;
122
123 Ok(Self { host_funcs })
124 }
125}
126
127impl HostFn {
128 pub fn try_from(mut item: syn::ItemFn) -> syn::Result<Self> {
129 let err = |span, msg| {
130 let msg = format!("Invalid host function definition.\n{}", msg);
131 syn::Error::new(span, msg)
132 };
133
134 let msg = "Only #[cfg] and #[mutating] attributes are allowed.";
136 let span = item.span();
137 let mut attrs = item.attrs.clone();
138 attrs.retain(|a| !a.path().is_ident("doc"));
139 let mut mutating = false;
140 let mut cfg = None;
141 while let Some(attr) = attrs.pop() {
142 let ident = attr.path().get_ident().ok_or(err(span, msg))?.to_string();
143 match ident.as_str() {
144 "mutating" => {
145 if mutating {
146 return Err(err(span, "#[mutating] can only be specified once"));
147 }
148 mutating = true;
149 },
150 "cfg" => {
151 if cfg.is_some() {
152 return Err(err(span, "#[cfg] can only be specified once"));
153 }
154 cfg = Some(attr);
155 },
156 id => return Err(err(span, &format!("Unsupported attribute \"{id}\". {msg}"))),
157 }
158 }
159
160 if mutating {
161 let stmt = syn::parse_quote! {
162 if self.ext().is_read_only() {
163 return Err(Error::<E::T>::StateChangeDenied.into());
164 }
165 };
166 item.block.stmts.insert(0, stmt);
167 }
168
169 let name = item.sig.ident.to_string();
170
171 let msg = "Every function must start with these two parameters: &mut self, memory: &mut M";
172 let special_args = item
173 .sig
174 .inputs
175 .iter()
176 .take(2)
177 .enumerate()
178 .map(|(i, arg)| is_valid_special_arg(i, arg))
179 .fold(0u32, |acc, valid| if valid { acc + 1 } else { acc });
180
181 if special_args != 2 {
182 return Err(err(span, msg));
183 }
184
185 let msg = r#"Should return one of the following:
187 - Result<(), TrapReason>,
188 - Result<ReturnErrorCode, TrapReason>,
189 - Result<u32, TrapReason>,
190 - Result<u64, TrapReason>"#;
191 let ret_ty = match item.clone().sig.output {
192 syn::ReturnType::Type(_, ty) => Ok(ty.clone()),
193 _ => Err(err(span, &msg)),
194 }?;
195 match *ret_ty {
196 syn::Type::Path(tp) => {
197 let result = &tp.path.segments.last().ok_or(err(span, &msg))?;
198 let (id, span) = (result.ident.to_string(), result.ident.span());
199 id.eq(&"Result".to_string()).then_some(()).ok_or(err(span, &msg))?;
200
201 match &result.arguments {
202 syn::PathArguments::AngleBracketed(group) => {
203 if group.args.len() != 2 {
204 return Err(err(span, &msg));
205 };
206
207 let arg2 = group.args.last().ok_or(err(span, &msg))?;
208
209 let err_ty = match arg2 {
210 syn::GenericArgument::Type(ty) => Ok(ty.clone()),
211 _ => Err(err(arg2.span(), &msg)),
212 }?;
213
214 match err_ty {
215 syn::Type::Path(tp) => Ok(tp
216 .path
217 .segments
218 .first()
219 .ok_or(err(arg2.span(), &msg))?
220 .ident
221 .to_string()),
222 _ => Err(err(tp.span(), &msg)),
223 }?
224 .eq("TrapReason")
225 .then_some(())
226 .ok_or(err(span, &msg))?;
227
228 let arg1 = group.args.first().ok_or(err(span, &msg))?;
229 let ok_ty = match arg1 {
230 syn::GenericArgument::Type(ty) => Ok(ty.clone()),
231 _ => Err(err(arg1.span(), &msg)),
232 }?;
233 let ok_ty_str = match ok_ty {
234 syn::Type::Path(tp) => Ok(tp
235 .path
236 .segments
237 .first()
238 .ok_or(err(arg1.span(), &msg))?
239 .ident
240 .to_string()),
241 syn::Type::Tuple(tt) => {
242 if !tt.elems.is_empty() {
243 return Err(err(arg1.span(), &msg));
244 };
245 Ok("()".to_string())
246 },
247 _ => Err(err(ok_ty.span(), &msg)),
248 }?;
249 let returns = match ok_ty_str.as_str() {
250 "()" => Ok(HostFnReturn::Unit),
251 "u32" => Ok(HostFnReturn::U32),
252 "u64" => Ok(HostFnReturn::U64),
253 "ReturnErrorCode" => Ok(HostFnReturn::ReturnCode),
254 _ => Err(err(arg1.span(), &msg)),
255 }?;
256
257 Ok(Self { item, name, returns, cfg })
258 },
259 _ => Err(err(span, &msg)),
260 }
261 },
262 _ => Err(err(span, &msg)),
263 }
264 }
265}
266
267fn is_valid_special_arg(idx: usize, arg: &FnArg) -> bool {
268 match (idx, arg) {
269 (0, FnArg::Receiver(rec)) => rec.reference.is_some() && rec.mutability.is_some(),
270 (1, FnArg::Typed(pat)) => {
271 let ident = if let syn::Pat::Ident(ref ident) = *pat.pat {
272 &ident.ident
273 } else {
274 return false;
275 };
276 if !(ident == "memory" || ident == "_memory") {
277 return false;
278 }
279 matches!(*pat.ty, syn::Type::Reference(_))
280 },
281 _ => false,
282 }
283}
284
285fn arg_decoder<'a, P, I>(param_names: P, param_types: I) -> TokenStream2
286where
287 P: Iterator<Item = &'a std::boxed::Box<syn::Pat>> + Clone,
288 I: Iterator<Item = &'a std::boxed::Box<syn::Type>> + Clone,
289{
290 const ALLOWED_REGISTERS: usize = 6;
291
292 if param_names.clone().count() > ALLOWED_REGISTERS {
294 panic!("Syscalls take a maximum of {ALLOWED_REGISTERS} arguments");
295 }
296
297 if !param_types.clone().all(|ty| {
300 let syn::Type::Path(path) = &**ty else {
301 panic!("Type needs to be path");
302 };
303 let Some(ident) = path.path.get_ident() else {
304 panic!("Type needs to be ident");
305 };
306 matches!(ident.to_string().as_ref(), "u8" | "u16" | "u32" | "u64")
307 }) {
308 panic!("Only primitive unsigned integers are allowed as arguments to syscalls");
309 }
310
311 let bindings = param_names.zip(param_types).enumerate().map(|(idx, (name, ty))| {
313 let reg = quote::format_ident!("__a{}__", idx);
314 quote! {
315 let #name = #reg as #ty;
316 }
317 });
318 quote! {
319 #( #bindings )*
320 }
321}
322
323fn expand_env(def: &EnvDef) -> TokenStream2 {
328 let impls = expand_functions(def);
329 let bench_impls = expand_bench_functions(def);
330 let docs = expand_func_doc(def);
331 let all_syscalls = expand_func_list(def);
332 let all_trace_ops = expand_trace_op_list(def);
333 let lookup_trace_op = expand_trace_op_lookup(def);
334
335 quote! {
336 pub fn list_syscalls() -> &'static [&'static [u8]] {
338 #all_syscalls
339 }
340
341 pub fn list_trace_ops() -> &'static [&'static [u8]] {
343 #all_trace_ops
344 }
345
346 pub fn lookup_trace_op_index(name: &'static str) -> Option<u8> {
348 #lookup_trace_op
349 }
350
351 impl<'a, E: Ext, M: PolkaVmInstance<E::T>> Runtime<'a, E, M> {
352 fn handle_ecall(
353 &mut self,
354 memory: &mut M,
355 __syscall_symbol__: &[u8],
356 ) -> Result<Option<u64>, TrapReason>
357 {
358 #impls
359 }
360 }
361
362 #[cfg(feature = "runtime-benchmarks")]
363 impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
364 #bench_impls
365 }
366
367 #[cfg(doc)]
377 pub trait SyscallDoc {
378 #docs
379 }
380 }
381}
382
383fn expand_functions(def: &EnvDef) -> TokenStream2 {
384 let impls = def.host_funcs.iter().map(|f| {
385 let params = f.item.sig.inputs.iter().skip(2);
387 let param_names = params.clone().filter_map(|arg| {
388 let FnArg::Typed(arg) = arg else {
389 return None;
390 };
391 Some(&arg.pat)
392 });
393 let param_types = params.clone().filter_map(|arg| {
394 let FnArg::Typed(arg) = arg else {
395 return None;
396 };
397 Some(&arg.ty)
398 });
399 let arg_decoder = arg_decoder(param_names, param_types);
400 let cfg = &f.cfg;
401 let name = &f.name;
402 let syscall_symbol = Literal::byte_string(name.as_bytes());
403 let body = &f.item.block;
404 let map_output = f.returns.map_output();
405 let trace_return = f.returns.trace_return_value();
406 let output = &f.item.sig.output;
407
408 let wrapped_body_with_trace = {
410 let trace_fmt_args = params.clone().filter_map(|arg| match arg {
411 syn::FnArg::Receiver(_) => None,
412 syn::FnArg::Typed(p) => match *p.pat.clone() {
413 syn::Pat::Ident(ref pat_ident) => Some(pat_ident.ident.clone()),
414 _ => None,
415 },
416 });
417
418 let params_fmt_str = trace_fmt_args
419 .clone()
420 .map(|s| format!("{s}: {{:?}}"))
421 .collect::<Vec<_>>()
422 .join(", ");
423 let trace_fmt_str = format!("{}({}) = {{:?}} weight_consumed: {{:?}}", name, params_fmt_str);
424 let trace_args_for_tracer: Vec<_> = trace_fmt_args.clone().collect();
425
426 quote! {
427 crate::tracing::if_tracing(|tracer| {
428 tracer.enter_ecall(#name, &[#( #trace_args_for_tracer as u64 ),*], self)
429 });
430
431 let result = (|| #body)();
433 ::log::trace!(target: "runtime::revive::strace", #trace_fmt_str, #( #trace_fmt_args, )* result, self.ext.frame_meter().weight_consumed());
434
435 crate::tracing::if_tracing(|tracer| tracer.exit_step(self, #trace_return));
436 result
437 }
438 };
439
440 quote! {
441 #cfg
442 #syscall_symbol => {
443 (|| #output {
445 #arg_decoder
446 #wrapped_body_with_trace
447 })().map(#map_output)
448 },
449 }
450 });
451
452 quote! {
453 crate::tracing::if_tracing(|tracer| {
454 tracer.enter_ecall(crate::tracing::PVM_FUEL_NAME, &[], self)
455 });
456
457 let __sync_result__ = self.ext
458 .frame_meter_mut()
459 .sync_from_executor(memory.gas())
460 .map_err(TrapReason::from);
461
462 crate::tracing::if_tracing(|tracer| tracer.exit_step(self, None));
463
464 __sync_result__?;
465
466 self.charge_gas(crate::vm::RuntimeCosts::HostFn).map_err(TrapReason::from)?;
468
469 let (__a0__, __a1__, __a2__, __a3__, __a4__, __a5__) = memory.read_input_regs();
471
472 let result = (|| match __syscall_symbol__ {
474 #( #impls )*
475 _ => Err(TrapReason::SupervisorError(Error::<E::T>::InvalidSyscall.into()))
476 })();
477
478 let gas = self.ext.frame_meter_mut().sync_to_executor();
480 memory.set_gas(gas.into());
481 result
482 }
483}
484
485fn expand_bench_functions(def: &EnvDef) -> TokenStream2 {
486 let impls = def.host_funcs.iter().map(|f| {
487 let params = f.item.sig.inputs.iter().skip(2);
489 let cfg = &f.cfg;
490 let name = &f.name;
491 let body = &f.item.block;
492 let output = &f.item.sig.output;
493
494 let name = Ident::new(&format!("bench_{name}"), Span::call_site());
495 quote! {
496 #cfg
497 pub fn #name(&mut self, memory: &mut M, #(#params),*) #output {
498 #body
499 }
500 }
501 });
502
503 quote! {
504 #( #impls )*
505 }
506}
507
508fn expand_func_doc(def: &EnvDef) -> TokenStream2 {
509 let docs = def.host_funcs.iter().map(|func| {
510 let func_decl = {
512 let mut sig = func.item.sig.clone();
513 sig.inputs = sig
514 .inputs
515 .iter()
516 .skip(2)
517 .map(|p| p.clone())
518 .collect::<Punctuated<FnArg, Comma>>();
519 sig.output = func.returns.success_type();
520 sig.to_token_stream()
521 };
522 let func_doc = {
523 let func_docs = {
524 let docs = func.item.attrs.iter().filter(|a| a.path().is_ident("doc")).map(|d| {
525 let docs = d.to_token_stream();
526 quote! { #docs }
527 });
528 quote! { #( #docs )* }
529 };
530 quote! {
531 #func_docs
532 }
533 };
534 quote! {
535 #func_doc
536 #func_decl;
537 }
538 });
539
540 quote! {
541 #( #docs )*
542 }
543}
544
545fn expand_func_list(def: &EnvDef) -> TokenStream2 {
546 let docs = def.host_funcs.iter().map(|f| {
547 let cfg = &f.cfg;
548 let name = Literal::byte_string(f.name.as_bytes());
549 quote! {
550 #cfg
551 #name.as_slice()
552 }
553 });
554
555 quote! {
557 {
558 static FUNCS: &[&[u8]] = &[#(#docs),*];
559 FUNCS
560 }
561 }
562}
563
564fn expand_trace_op_list(def: &EnvDef) -> TokenStream2 {
565 let syscalls = def.host_funcs.iter().map(|f| {
566 let name = Literal::byte_string(f.name.as_bytes());
567 quote! {
568 #name.as_slice()
569 }
570 });
571 let len = syscalls.clone().count() + 1;
572
573 quote! {
574 {
575 static OPS: [&[u8]; #len] = [
576 #(#syscalls,)*
577 crate::tracing::PVM_FUEL_NAME.as_bytes(),
578 ];
579 OPS.as_slice()
580 }
581 }
582}
583
584fn expand_trace_op_lookup(def: &EnvDef) -> TokenStream2 {
585 let arms = def.host_funcs.iter().enumerate().map(|(idx, f)| {
586 let name_str = &f.name;
587 quote! {
588 #name_str => Some(#idx as u8)
589 }
590 });
591 let pvm_fuel_idx = def.host_funcs.len();
592
593 quote! {
594 match name {
595 #( #arms, )*
596 crate::tracing::PVM_FUEL_NAME => Some(#pvm_fuel_idx as u8),
597 _ => None,
598 }
599 }
600}