Skip to content

Commit 56449a1

Browse files
Franklin-QiAlonely0
authored andcommitted
parser: add proptest round-trip fuzzing for AST pretty-printing
Add a proptest-based alternative to libFuzzer for aarch64-friendly round-trip testing: generate random programs, materialize them into ASTs, pretty-print via Display, re-parse, and compare s-expr signatures. Includes a hand-written AST generator, smoke tests, and proptest regression seeds. Closes #41
1 parent 4f591cc commit 56449a1

6 files changed

Lines changed: 640 additions & 5 deletions

File tree

Cargo.lock

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

parser/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ ahash.workspace = true
1818
derive_more.workspace = true
1919
ariadne = "0.6.0"
2020

21+
[dev-dependencies]
22+
proptest = "1.6.0"
23+
2124
[lints]
2225
workspace = true

parser/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ mod diagnostics;
88
mod idempotency;
99
mod lex;
1010
mod pratt;
11+
#[cfg(test)]
12+
mod prop_tests;
1113
mod sexpr;
1214
#[cfg(test)]
15+
mod testing;
16+
#[cfg(test)]
1317
mod tests;
1418

1519
use std::mem::replace;
@@ -750,7 +754,7 @@ impl<'a> Parser<'a> {
750754
}
751755

752756
impl<'a> Ast<'a> {
753-
fn new(arena: &'a Bump) -> Self {
757+
pub(crate) fn new(arena: &'a Bump) -> Self {
754758
Self {
755759
loads: Vec::new_in(arena),
756760
begin: Vec::new_in(arena),

parser/src/prop_tests.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// This file is part of the uutils awk package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// files that was distributed with this source code.
5+
6+
use std::fmt::Write;
7+
8+
use bumpalo::Bump;
9+
use proptest::prelude::*;
10+
11+
use crate::testing::{
12+
ast_gen::{self, GenAtom, GenBody, GenExpr, GenPattern, GenProgram, GenRule, GenStatement},
13+
roundtrip_ast, roundtrip_source,
14+
};
15+
16+
proptest! {
17+
#![proptest_config(ProptestConfig {
18+
cases: 128,
19+
.. ProptestConfig::default()
20+
})]
21+
22+
#[test]
23+
fn roundtrip_generated_program(program in ast_gen::gen_program()) {
24+
let arena = Bump::new();
25+
let ast = ast_gen::materialize(&program, &arena);
26+
roundtrip_ast(&ast).map_err(|e| TestCaseError::fail(e))?;
27+
}
28+
29+
#[test]
30+
fn roundtrip_generated_source(program in ast_gen::gen_program()) {
31+
let arena = Bump::new();
32+
let ast = ast_gen::materialize(&program, &arena);
33+
let mut source = String::new();
34+
write!(source, "{ast}").map_err(|e| TestCaseError::fail(format!("display failed: {e}")))?;
35+
roundtrip_source(&source).map_err(|e| TestCaseError::fail(e))?;
36+
}
37+
}
38+
39+
#[test]
40+
fn roundtrip_smoke_cases() {
41+
let cases = [
42+
"{ print 1 + a }",
43+
"BEGIN { print 1 }",
44+
"{ if (a) print; else print 0 }",
45+
"{ while (a < 10) a++ }",
46+
"{ a = 1; b = a + 2 }",
47+
"/pat/ { print $1, $2 }",
48+
"BEGIN { print \"hi\" }",
49+
];
50+
for source in cases {
51+
roundtrip_source(source)
52+
.unwrap_or_else(|e| panic!("round-trip failed for {source:?}: {e}"));
53+
}
54+
}
55+
56+
#[test]
57+
fn roundtrip_handwritten_ast() {
58+
let program = GenProgram {
59+
rules: vec![
60+
GenRule {
61+
pattern: Some(GenPattern::Regex(0)),
62+
body: GenBody {
63+
statements: vec![GenStatement::Print(vec![GenExpr::Atom(GenAtom::Var(0))])],
64+
},
65+
},
66+
GenRule {
67+
pattern: None,
68+
body: GenBody {
69+
statements: vec![
70+
GenStatement::If {
71+
condition: GenExpr::Binary(
72+
crate::ast::BinaryOperator::Lt,
73+
Box::new(GenExpr::Atom(GenAtom::Var(0))),
74+
Box::new(GenExpr::Atom(GenAtom::SmallInt(10))),
75+
),
76+
then_body: GenBody {
77+
statements: vec![GenStatement::Print(vec![GenExpr::Atom(
78+
GenAtom::SmallInt(1),
79+
)])],
80+
},
81+
else_body: None,
82+
},
83+
GenStatement::Print(vec![GenExpr::Binary(
84+
crate::ast::BinaryOperator::Add,
85+
Box::new(GenExpr::Atom(GenAtom::SmallInt(1))),
86+
Box::new(GenExpr::Atom(GenAtom::Var(0))),
87+
)]),
88+
GenStatement::Expr(GenExpr::Binary(
89+
crate::ast::BinaryOperator::Eq,
90+
Box::new(GenExpr::Atom(GenAtom::SmallInt(0))),
91+
Box::new(GenExpr::Atom(GenAtom::SmallInt(1))),
92+
)),
93+
],
94+
},
95+
},
96+
],
97+
begin: Some(GenBody {
98+
statements: vec![GenStatement::Print(vec![GenExpr::Atom(GenAtom::SmallInt(
99+
0,
100+
))])],
101+
}),
102+
};
103+
104+
let arena = Bump::new();
105+
let ast = ast_gen::materialize(&program, &arena);
106+
roundtrip_ast(&ast).expect("hand-written AST should round-trip");
107+
}

0 commit comments

Comments
 (0)