use std::sync::Arc;
use sim_kernel::{Cx, Expr, NumberLiteral, ShapeDoc, Symbol};
use sim_shape::{AnyShape, ExprKind, ExprKindShape, ListShape, OrShape, Shape};
use crate::seed::r7rs_seed_corpus;
pub struct ExprSpace {
grammar: Arc<dyn Shape>,
seed: Vec<Expr>,
atoms: Vec<Expr>,
max_depth: usize,
}
impl ExprSpace {
pub fn r7rs_core_space(max_depth: usize) -> Self {
Self {
grammar: r7rs_core_grammar(),
seed: r7rs_seed_corpus(),
atoms: r7rs_core_atoms(),
max_depth,
}
}
pub fn core_round_trip_space(max_depth: usize) -> Self {
Self {
grammar: r7rs_core_grammar(),
seed: core_round_trip_seed_corpus(),
atoms: r7rs_core_atoms(),
max_depth,
}
}
pub fn grammar(&self) -> Arc<dyn Shape> {
Arc::clone(&self.grammar)
}
pub fn seed_corpus(&self) -> Vec<Expr> {
self.seed.clone()
}
pub fn describe_grammar(&self, cx: &mut Cx) -> sim_kernel::Result<ShapeDoc> {
self.grammar.describe(cx)
}
pub fn max_depth(&self) -> usize {
self.max_depth
}
pub fn contains(&self, cx: &mut Cx, expr: &Expr) -> bool {
matches!(self.grammar.check_expr(cx, expr), Ok(matched) if matched.accepted)
}
pub fn enumerate(&self, budget: usize) -> Vec<Expr> {
let mut out = Vec::new();
for seed in &self.seed {
push_unique(&mut out, seed.clone(), budget);
}
for atom in &self.atoms {
push_unique(&mut out, atom.clone(), budget);
}
let mut depth = 1;
while depth < self.max_depth && out.len() < budget {
let frontier = out.clone();
for head in &frontier {
for tail in &self.atoms {
if out.len() >= budget {
break;
}
push_unique(
&mut out,
Expr::List(vec![head.clone(), tail.clone()]),
budget,
);
}
}
depth += 1;
}
out.truncate(budget);
out
}
}
pub fn r7rs_core_grammar() -> Arc<dyn Shape> {
Arc::new(OrShape::new(vec![
Arc::new(ExprKindShape::new(ExprKind::Bool)),
Arc::new(ExprKindShape::new(ExprKind::Symbol)),
Arc::new(ExprKindShape::new(ExprKind::String)),
Arc::new(ExprKindShape::new(ExprKind::Number)),
Arc::new(ListShape::with_rest(Vec::new(), Arc::new(AnyShape))),
]))
}
fn push_unique(out: &mut Vec<Expr>, expr: Expr, budget: usize) {
if out.len() < budget && !out.iter().any(|existing| existing.canonical_eq(&expr)) {
out.push(expr);
}
}
fn r7rs_core_atoms() -> Vec<Expr> {
vec![
Expr::Bool(true),
Expr::Bool(false),
Expr::Symbol(Symbol::new("answer")),
Expr::String("sim".to_owned()),
Expr::Number(NumberLiteral {
domain: Symbol::qualified("numbers", "i64"),
canonical: "1".to_owned(),
}),
]
}
fn core_round_trip_seed_corpus() -> Vec<Expr> {
vec![
Expr::Bool(true),
Expr::Bool(false),
Expr::Symbol(Symbol::new("answer")),
Expr::String("sim".to_owned()),
]
}
#[cfg(test)]
mod tests {
use sim_kernel::testing::bare_cx as cx;
use super::*;
#[test]
fn r7rs_core_enumeration_is_stable_and_in_space() {
let mut cx = cx();
let space = ExprSpace::r7rs_core_space(3);
let first = space.enumerate(64);
let second = space.enumerate(64);
assert_eq!(first, second);
assert!(first.len() > 5);
for expr in first {
assert!(space.contains(&mut cx, &expr), "out-of-space: {expr:?}");
}
}
#[test]
fn grammar_description_is_browsable() {
let mut cx = cx();
let space = ExprSpace::r7rs_core_space(3);
let doc = space.describe_grammar(&mut cx).unwrap();
assert_eq!(doc.name, "or shape");
assert!(doc.details.iter().any(|detail| detail == "expr-kind bool"));
assert!(doc.details.iter().any(|detail| detail == "list shape"));
}
#[test]
fn grammar_tests_enumerated_exprs_are_all_in_space() {
let mut cx = cx();
let space = ExprSpace::r7rs_core_space(3);
for expr in space.enumerate(128) {
assert!(space.contains(&mut cx, &expr), "out-of-space: {expr:?}");
}
for seed in space.seed_corpus() {
assert!(
space.contains(&mut cx, &seed),
"seed not in space: {seed:?}"
);
}
}
#[test]
fn round_trip_space_omits_lowering_sensitive_quote_seed() {
let space = ExprSpace::core_round_trip_space(3);
let seed = space.seed_corpus();
assert!(seed.iter().any(|expr| matches!(expr, Expr::Bool(true))));
assert!(!seed.iter().any(|expr| matches!(
expr,
Expr::List(items)
if items.first() == Some(&Expr::Symbol(Symbol::new("quote")))
)));
}
}