#[derive(Debug, Clone, PartialEq)]
pub enum DirectiveKind {
IncludeSymbol,
Witness,
LeanCheck,
}
impl DirectiveKind {
fn label(&self) -> &'static str {
match self {
Self::IncludeSymbol => "include-symbol",
Self::Witness => "witness",
Self::LeanCheck => "lean-check",
}
}
fn from_info(info: &str) -> Option<Self> {
if info.starts_with("include-symbol") {
Some(Self::IncludeSymbol)
} else if info.starts_with("witness") {
Some(Self::Witness)
} else if info.starts_with("lean-check") {
Some(Self::LeanCheck)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Directive {
pub kind: DirectiveKind,
pub attrs: Vec<(String, String)>,
pub byte_range: (usize, usize),
}
impl Directive {
pub fn attr(&self, key: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn require(&self, key: &str) -> Result<&str, crate::CompilerError> {
self.attr(key)
.ok_or_else(|| crate::CompilerError::Attribute {
directive: self.kind.label(),
reason: format!("missing required attribute `{key}`"),
})
}
}
#[derive(Debug)]
pub struct Scanned<'a> {
source: &'a str,
blocks: Vec<Block>,
}
#[derive(Debug)]
pub enum Block {
Text(usize, usize),
Directive(Directive),
}
impl<'a> Scanned<'a> {
pub fn blocks(&self) -> &[Block] {
&self.blocks
}
pub fn source(&self) -> &'a str {
self.source
}
pub fn range(block: &Block) -> (usize, usize) {
match block {
Block::Text(start, end) => (*start, *end),
Block::Directive(d) => d.byte_range,
}
}
pub fn substitute<F>(&self, mut resolve: F) -> Result<String, crate::CompilerError>
where
F: FnMut(&Directive) -> Result<String, crate::CompilerError>,
{
let mut out = String::with_capacity(self.source.len());
let mut cursor = 0usize;
for block in &self.blocks {
let (start, end) = Self::range(block);
out.push_str(self.source.get(cursor..start).unwrap_or_default());
match block {
Block::Text(..) => {
out.push_str(self.source.get(start..end).unwrap_or_default());
}
Block::Directive(d) => {
out.push_str(&resolve(d)?);
out.push_str("\n\n");
}
}
cursor = end;
}
out.push_str(self.source.get(cursor..).unwrap_or_default());
Ok(out)
}
}
pub fn scan(source: &str) -> Scanned<'_> {
let mut blocks: Vec<Block> = Vec::new();
let bytes = source.as_bytes();
let mut cursor = 0usize;
let mut text_start = 0usize;
while cursor < bytes.len() {
let line_end = source[cursor..]
.find('\n')
.map(|p| cursor + p)
.unwrap_or(bytes.len());
let line = source.get(cursor..line_end).unwrap_or_default();
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("```") {
let info = rest.trim();
if let Some(kind) = DirectiveKind::from_info(info).filter(|_| !info.contains('`')) {
if cursor > text_start {
blocks.push(Block::Text(text_start, cursor));
}
let mut close = None;
let mut scan = line_end + 1;
while scan <= bytes.len() {
let e = source[scan..]
.find('\n')
.map(|p| scan + p)
.unwrap_or(bytes.len());
if source.get(scan..e).unwrap_or_default().trim() == "```" {
close = Some(e);
break;
}
scan = e + 1;
}
let end = close.unwrap_or(bytes.len());
blocks.push(Block::Directive(Directive {
kind,
attrs: parse_attrs(info),
byte_range: (cursor, end),
}));
cursor = end;
text_start = end;
continue;
}
}
cursor = line_end + 1;
}
if text_start < bytes.len() {
blocks.push(Block::Text(text_start, bytes.len()));
}
Scanned { source, blocks }
}
fn parse_attrs(info: &str) -> Vec<(String, String)> {
let mut attrs = Vec::new();
let rest = match info.find(char::is_whitespace) {
Some(pos) => &info[pos..],
None => "",
};
let mut tokens = rest.split_whitespace();
while let Some(token) = tokens.next() {
let Some(eq) = token.find('=') else {
continue;
};
let Some(key) = token.get(..eq) else {
continue;
};
let Some(mut value) = token.get(eq + 1..).map(str::to_string) else {
continue;
};
if value.starts_with('"') && !value.ends_with('"') {
for cont in tokens.by_ref() {
value.push(' ');
value.push_str(cont);
if cont.ends_with('"') {
break;
}
}
}
let value = value.trim_matches('"').to_string();
if !attrs.iter().any(|(k, _)| k == key) {
attrs.push((key.to_string(), value));
}
}
attrs
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn finds_directive_fences_and_keeps_other_fences() {
let md = "intro\n\n```include-symbol file=\"a.rs\" symbol=\"x\"\ncode here\n```\n\n\
```rust\nfn plain() {}\n```\n\n```witness wasm=\"m.wasm\" fuel=1000\nbody\n```\n";
let scanned = scan(md);
let directives: Vec<&Directive> = scanned
.blocks()
.iter()
.filter_map(|b| match b {
Block::Directive(d) => Some(d),
_ => None,
})
.collect();
assert_eq!(directives.len(), 2);
assert_eq!(directives[0].kind, DirectiveKind::IncludeSymbol);
assert_eq!(
directives[0].attr("file"),
Some("a.rs"),
"quoted value unquoted"
);
assert_eq!(directives[1].kind, DirectiveKind::Witness);
assert_eq!(directives[1].attr("fuel"), Some("1000"));
}
#[test]
fn quoted_values_with_spaces_stay_one_value() {
let attrs = parse_attrs("witness assert_stdout=\"hello world\" fuel=5");
assert_eq!(
attrs,
vec![
("assert_stdout".to_string(), "hello world".to_string()),
("fuel".to_string(), "5".to_string()),
]
);
}
}