在 Rust 中使用过程宏模拟 Python 缩进语法
预期形式
python! {
fn f(a: u32, b: u32) -> u32:
return a + b
}
python! {
fn main():
for ith in 0..7:
for jth in 0..ith:
let ans = f(ith, jth)
println!("{}", ans)
}
fn f(a: u32, b: u32) -> u32 {
return a + b
}
fn main() {
for ith in 0..7 {
for jth in 0..ith {
let ans = f(ith, jth)
println!("{}", ans)
}
}
}
原理
- Procedural Macros
- Function-like macros:
custom!(...)
- Derive macros:
#[derive(CustomDerive)]
- Attribute macros:
#[CustomAttribute]
- Function-like macros:
使用 Rust 中的过程宏,对源代码进行预处理。首先将行末的冒号替换为左大括号,其次通过将缩进数量压栈的方式,计算某一行代码是否属于当前的逻辑范围,进而补全右大括号,从而模拟 Python 的缩进语法,代码如下:
lib.rs
#![feature(proc_macro_span)]
extern crate proc_macro;
use proc_macro::TokenStream;
use std::cmp::Ordering::{Greater, Less, Equal};
#[proc_macro]
pub fn python(tokens: TokenStream) -> TokenStream {
let mut lines: Vec<String> = source_code(tokens)
.lines()
.map(|line| String::from(line.trim_end()))
.filter(|line| !line.is_empty())
.collect();
let mut stack = vec![indentation_size(&lines[0])];
for ith in 0..lines.len() {
// :
if lines[ith].ends_with(':') {
lines[ith].pop();
(lines[ith].push(' '), lines[ith].push('{'));
} else {
lines[ith].push(';');
}
// }
let indent = indentation_size(&lines[ith]);
loop {
match stack.iter().last() {
Some(previous) => {
match indent.cmp(previous) {
Greater => stack.push(indent),
Less => {
lines[ith - 1].push('}');
stack.pop();
},
Equal => break,
};
}
None => unreachable!(),
};
}
}
let suffix: String = (0..stack.len() - 1).map(|_| "}").collect();
return format!("{}{}", lines.join("\n"), suffix).parse().unwrap();
}
fn source_code(tokens: TokenStream) -> String {
let mut ans = String::new();
let (mut line, mut column) = (0, 0);
for token in tokens {
let location = token.span().start();
let string = token.to_string();
// vertical
if line < location.line {
ans.push('\n');
line = location.line;
column = 0;
}
// horizontal
while column < location.column {
ans.push(' ');
column += 1;
}
// token
ans.push_str(&string);
column += string.len();
}
return ans;
}
fn indentation_size(string: &String) -> usize {
return string.len() - string.trim_start().len();
}