use std::alloc::{GlobalAlloc, Layout, System};
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::io::Write as _;
use std::panic::{self, AssertUnwindSafe};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use vimlrs::fusevm_bridge::{observe_errors_begin, observe_errors_take};
use vimlrs::ported::eval::encode::encode_tv2string;
use vimlrs::ported::eval::funcs_argc::BUILTIN_ARGC;
use vimlrs::ported::message::did_emsg;
const MEM_LIMIT: usize = 1 << 30;
const CHUNK_TIMEOUT: Duration = Duration::from_secs(30);
const CHUNK: usize = 250;
struct Budget;
static USED: AtomicUsize = AtomicUsize::new(0);
static LIMIT: AtomicUsize = AtomicUsize::new(usize::MAX);
unsafe impl GlobalAlloc for Budget {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
if USED.fetch_add(l.size(), Ordering::Relaxed) + l.size() > LIMIT.load(Ordering::Relaxed) {
USED.fetch_sub(l.size(), Ordering::Relaxed);
return std::ptr::null_mut();
}
let p = unsafe { System.alloc(l) };
if p.is_null() {
USED.fetch_sub(l.size(), Ordering::Relaxed);
}
p
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
USED.fetch_sub(l.size(), Ordering::Relaxed);
unsafe { System.dealloc(p, l) }
}
}
#[global_allocator]
static ALLOC: Budget = Budget;
fn wait_bounded(child: &mut Child, deadline: Duration) -> bool {
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => return true,
Ok(None) if start.elapsed() < deadline => std::thread::sleep(Duration::from_millis(20)),
_ => {
let _ = child.kill();
let _ = child.wait();
return false;
}
}
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[self.below(xs.len())]
}
fn chance(&mut self, n: u64, d: u64) -> bool {
self.next() % d < n
}
}
#[derive(Clone, Copy, PartialEq)]
enum Shape {
Str,
Num,
Float,
Bool,
Pat,
Fmt,
List,
StrList,
NumList,
Dict,
Blob,
Any,
Lambda,
Repl,
SortFlag,
Func,
Count,
}
use Shape::*;
const FUNCS: &[(&str, &[Shape])] = &[
("strlen", &[Str]),
("strchars", &[Str, Bool]),
("strwidth", &[Str]),
("strdisplaywidth", &[Str, Num]),
("strcharlen", &[Str]),
("strpart", &[Str, Num, Num, Bool]),
("strcharpart", &[Str, Num, Num]),
("strgetchar", &[Str, Num]),
("strcharatpos", &[Str, Num]),
("stridx", &[Str, Str, Num]),
("strridx", &[Str, Str, Num]),
("strtrans", &[Str]),
("tolower", &[Str]),
("toupper", &[Str]),
("trim", &[Str, Str, Num]),
("repeat", &[Any, Count]),
("reverse", &[Any]),
("split", &[Str, Pat, Bool]),
("join", &[List, Str]),
("substitute", &[Str, Pat, Repl, Str]),
("escape", &[Str, Str]),
("shellescape", &[Str, Bool]),
("fnameescape", &[Str]),
("printf", &[Fmt, Any, Any, Any]),
("nr2char", &[Num, Bool]),
("char2nr", &[Str, Bool]),
("str2nr", &[Str, Num, Bool]),
("str2float", &[Str, Bool]),
("str2list", &[Str, Bool]),
("list2str", &[NumList, Bool]),
("byteidx", &[Str, Num, Bool]),
("byteidxcomp", &[Str, Num, Bool]),
("charidx", &[Str, Num, Bool]),
("strspn", &[Str, Str]),
("strcspn", &[Str, Str]),
("eval", &[Str]),
("tr", &[Str, Str, Str]),
("slice", &[Any, Num, Num]),
("sha256", &[Str]),
("charclass", &[Str]),
("strutf16len", &[Str, Bool]),
("utf16idx", &[Str, Num, Bool, Bool]),
("byteidxcomp", &[Str, Num]),
("string", &[Any]),
("iconv", &[Str, Str, Str]),
("keytrans", &[Str]),
("match", &[Str, Pat, Num, Num]),
("matchend", &[Str, Pat, Num, Num]),
("matchstr", &[Str, Pat, Num, Num]),
("matchstrpos", &[Str, Pat, Num, Num]),
("matchlist", &[Str, Pat, Num, Num]),
("matchbufline", &[Num, Pat, Num, Num]),
("matchfuzzy", &[StrList, Str]),
("abs", &[Float]),
("ceil", &[Float]),
("floor", &[Float]),
("round", &[Float]),
("trunc", &[Float]),
("float2nr", &[Float]),
("fmod", &[Float, Float]),
("pow", &[Float, Float]),
("sqrt", &[Float]),
("exp", &[Float]),
("log", &[Float]),
("log10", &[Float]),
("sin", &[Float]),
("cos", &[Float]),
("tan", &[Float]),
("asin", &[Float]),
("acos", &[Float]),
("atan", &[Float]),
("atan2", &[Float, Float]),
("sinh", &[Float]),
("cosh", &[Float]),
("tanh", &[Float]),
("and", &[Num, Num]),
("or", &[Num, Num]),
("xor", &[Num, Num]),
("invert", &[Num]),
("max", &[NumList]),
("min", &[NumList]),
("isinf", &[Float]),
("isnan", &[Float]),
("len", &[Any]),
("empty", &[Any]),
("get", &[Any, Any, Any]),
("add", &[List, Any]),
("insert", &[List, Any, Num]),
("remove", &[Any, Any, Any]),
("extend", &[List, List, Num]),
("copy", &[Any]),
("deepcopy", &[Any, Bool]),
("count", &[Any, Any, Bool, Num]),
("index", &[List, Any, Num, Bool]),
("indexof", &[List, Lambda]),
("map", &[Any, Lambda]),
("filter", &[Any, Lambda]),
("sort", &[List, SortFlag]),
("uniq", &[List, SortFlag]),
("reduce", &[List, Lambda, Any]),
("flatten", &[List, Count]),
("flattennew", &[List, Count]),
("range", &[Count, Count, Count]),
("keys", &[Dict]),
("values", &[Dict]),
("items", &[Dict]),
("has_key", &[Dict, Str]),
("zip", &[List, List]),
("extendnew", &[List, List, Num]),
("mapnew", &[Any, Lambda]),
("call", &[Func, List]),
("funcref", &[Str, List]),
("matchfuzzypos", &[StrList, Str]),
("blob2list", &[Blob]),
("list2blob", &[NumList]),
("blob2str", &[Blob]),
("str2blob", &[StrList]),
("type", &[Any]),
("typename", &[Any]),
("json_encode", &[Any]),
("json_decode", &[Str]),
("js_encode", &[Any]),
("js_decode", &[Str]),
("msgpackdump", &[List]),
("id", &[Any]),
];
const STRINGS: &[&str] = &[
"''",
"'a'",
"'abc'",
"'ABC'",
"'hello world'",
"' padded '",
"'a,b,,c'",
"'foo.bar'",
"'1234'",
"'-7'",
"'3.5e2'",
"'0x1f'",
"'tab\\there'",
"'ünïcø∂é'",
"'日本語'",
"'e\u{0301}combining'",
"'a''quote'",
"\"dq\\tstr\"",
"\"nl\\nhere\"",
"'*.[]^$\\'",
"'\\d\\+'",
"\"\\<Esc>\"",
"\"\\<C-A>\"",
"\"\\<Space>x\"",
"\"\\x41\\x42\"",
"\"\\101\\102\"",
"\"\\u00e9\"",
"\"a\\\\b\"",
];
const NUMS: &[&str] = &[
"0",
"1",
"2",
"3",
"-1",
"-2",
"7",
"10",
"-10",
"255",
"256",
"-255",
"2147483647",
"-2147483648",
"9223372036854775807",
"-9223372036854775808",
"0x1f",
"0b1011",
"017",
"100000",
];
const FUNCS_REF: &[&str] = &[
"function('strlen')",
"function('toupper')",
"function('abs')",
"function('add')",
"{x -> x}",
"{x -> type(x)}",
"function('printf', ['%d'])",
];
const COUNTS: &[&str] = &["0", "1", "2", "3", "5", "10", "64", "1000", "-1", "-3"];
const FLOATS: &[&str] = &[
"0.0",
"1.0",
"-1.0",
"0.5",
"-0.5",
"1.5",
"2.75",
"-3.25",
"3.141592653589793",
"1.0e10",
"1.0e-10",
"-1.0e300",
"1.0e308",
"123456789.123456789",
"0.1",
];
const PATS: &[&str] = &[
"'a'",
"'.'",
"'.*'",
"'^a'",
"'c$'",
"'\\.'",
"'a\\+'",
"'a\\='",
"'a\\?'",
"'a\\{2}'",
"'a\\{1,3}'",
"'a\\{-}'",
"'a\\{-1,}'",
"'\\d'",
"'\\d\\+'",
"'\\D'",
"'\\w\\+'",
"'\\W'",
"'\\s'",
"'\\S\\+'",
"'\\a'",
"'\\l'",
"'\\u'",
"'\\x'",
"'\\o'",
"'\\h'",
"'\\k'",
"'\\p'",
"'[abc]'",
"'[^abc]'",
"'[a-z]\\+'",
"'[[:digit:]]\\+'",
"'[[:alpha:]]'",
"'[[:space:]]'",
"'[[:punct:]]'",
"'\\(a\\)\\(b\\)'",
"'\\(ab\\)\\1'",
"'a\\|b'",
"'\\%(ab\\)\\+'",
"'\\zsa'",
"'a\\zs.'",
"'.\\zea'",
"'\\<a'",
"'a\\>'",
"'\\ca'",
"'\\Ca'",
"'\\vA+'",
"'\\v(a|b)+'",
"'\\V.'",
"'\\Ma.'",
"'a\\@='",
"'a\\@!'",
"'\\(a\\)\\@<=b'",
"'\\(a\\)\\@<!b'",
"'\\%[abc]'",
"'\\%d97'",
"'\\%x61'",
"'\\_.'",
"'\\n'",
"'\\t'",
"''",
];
const FMTS: &[&str] = &[
"'%d'",
"'%5d'",
"'%-5d|'",
"'%05d'",
"'%+d'",
"'% d'",
"'%x'",
"'%X'",
"'%#x'",
"'%o'",
"'%b'",
"'%c'",
"'%s'",
"'%10s|'",
"'%-10s|'",
"'%.3s'",
"'%S'",
"'%f'",
"'%.2f'",
"'%e'",
"'%E'",
"'%g'",
"'%G'",
"'%%'",
"'%*d'",
"'%.*f'",
"'%1$s %1$s'",
"'a%db%sc'",
"'%s=%d'",
];
const LISTS: &[&str] = &[
"[]",
"[1]",
"[1,2,3]",
"[3,1,2]",
"[1,1,2,2,3]",
"['a','b','c']",
"['b','a','C','A']",
"[1,'a',2.5]",
"[[1,2],[3,4]]",
"[[1,[2,[3]]]]",
"[{'a':1},{'a':2}]",
"[0,-1,255]",
"[v:true,v:false,v:null]",
"['10','9','2']",
];
const STRLISTS: &[&str] = &[
"[]",
"['a']",
"['a','b','c']",
"['foo','bar','baz']",
"['B','a','C']",
"['','x','']",
];
const NUMLISTS: &[&str] = &[
"[]",
"[0]",
"[1,2,3]",
"[3,1,2]",
"[-1,0,1]",
"[65,66,67]",
"[255,256,0]",
"[97,0x1f,10]",
];
const DICTS: &[&str] = &[
"{}",
"{'a':1}",
"{'a':1,'b':2}",
"{'b':2,'a':1}",
"{'a':[1,2],'b':{'c':3}}",
"{'1':'x','2':'y'}",
"#{a:1,b:2}",
];
const BLOBS: &[&str] = &["0z", "0z00", "0zFF", "0z0011DEADBEEF", "0z61.62.63"];
const SPECIALS: &[&str] = &["v:true", "v:false", "v:null", "v:none"];
const REPLS: &[&str] = &[
"'X'",
"''",
"'[&]'",
"'\\0\\0'",
"'\\1'",
"'<\\1>'",
"'\\u&'",
"'\\U&'",
"'\\l&'",
"'\\e'",
"'\\=submatch(0)'",
"'\\=toupper(submatch(0))'",
"'\\=submatch(0).submatch(0)'",
"'\\=len(submatch(0))'",
"'\\n'",
"'\\r'",
"'~'",
];
const LAMBDAS: &[&str] = &[
"{i,v -> v}",
"{i,v -> i}",
"{_,v -> v * 2}",
"{_,v -> type(v)}",
"{_,v -> string(v)}",
"{_,v -> v is v}",
"{i,v -> i % 2}",
"{_,v -> empty(v)}",
];
const SORT_FLAGS: &[&str] = &[
"'i'",
"'n'",
"'N'",
"'l'",
"1",
"0",
"{a,b -> a > b ? -1 : 1}",
];
const VARS: &[&str] = &["g:n", "g:f", "g:s", "g:l", "g:d", "g:b", "g:e"];
const PRELUDE: &[&str] = &[
"let g:n = 42",
"let g:f = 2.5",
"let g:s = 'hello'",
"let g:l = [1, 2, 3]",
"let g:d = {'a': 1, 'b': 2}",
"let g:b = 0z0011",
"let g:e = []",
];
const BINOPS: &[&str] = &[
"+", "-", "*", "/", "%", ".", "..", "&&", "||", "==", "!=", ">", ">=", "<", "<=", "=~", "!~",
"==#", "!=#", ">#", "<#", "=~#", "!~#", "==?", "!=?", ">?", "<?", "=~?", "!~?", "is", "isnot",
];
fn shaped(rng: &mut Rng, s: Shape) -> String {
match s {
Str => rng.pick(STRINGS).to_string(),
Num => rng.pick(NUMS).to_string(),
Float => rng.pick(FLOATS).to_string(),
Bool => ["0", "1"][rng.below(2)].to_string(),
Pat => rng.pick(PATS).to_string(),
Fmt => rng.pick(FMTS).to_string(),
List => rng.pick(LISTS).to_string(),
StrList => rng.pick(STRLISTS).to_string(),
NumList => rng.pick(NUMLISTS).to_string(),
Dict => rng.pick(DICTS).to_string(),
Blob => rng.pick(BLOBS).to_string(),
Lambda => rng.pick(LAMBDAS).to_string(),
Repl => rng.pick(REPLS).to_string(),
SortFlag => rng.pick(SORT_FLAGS).to_string(),
Count => rng.pick(COUNTS).to_string(),
Func => rng.pick(FUNCS_REF).to_string(),
Any => any_value(rng),
}
}
fn any_value(rng: &mut Rng) -> String {
match rng.below(8) {
0 => rng.pick(NUMS).to_string(),
1 => rng.pick(STRINGS).to_string(),
2 => rng.pick(FLOATS).to_string(),
3 => rng.pick(LISTS).to_string(),
4 => rng.pick(DICTS).to_string(),
5 => rng.pick(BLOBS).to_string(),
6 => rng.pick(SPECIALS).to_string(),
_ => rng.pick(VARS).to_string(),
}
}
fn argc_range(name: &str) -> (u8, u8) {
BUILTIN_ARGC
.binary_search_by_key(&name, |(n, _, _)| n)
.map(|i| (BUILTIN_ARGC[i].1, BUILTIN_ARGC[i].2))
.unwrap_or((0, 0))
}
fn gen_call(rng: &mut Rng, funcs: &[(&str, &[Shape])]) -> String {
let (name, shapes) = *rng.pick(funcs);
let (min, max) = argc_range(name);
let hi = (max as usize).min(shapes.len().max(min as usize));
let n = if hi > min as usize {
min as usize + rng.below(hi - min as usize + 1)
} else {
min as usize
};
let args: Vec<String> = (0..n)
.map(|i| shaped(rng, shapes[i.min(shapes.len() - 1)]))
.collect();
format!("{name}({})", args.join(","))
}
const RX_SUBJECTS: &[&str] = &[
"''",
"'a'",
"'abc'",
"'aaa'",
"'abcabc'",
"'a1b2c3'",
"'Hello World'",
"' spaced '",
"'a.b*c'",
"'foo_bar-baz'",
"'ünïcø∂é'",
"'日本語abc'",
"'e\u{0301}combining'",
"'tab\there'",
"'AbCdEf'",
];
fn rx_atom(rng: &mut Rng) -> String {
match rng.below(14) {
0 => rng
.pick(&["a", "b", "c", "x", "1", "_", "-", ".", "é"])
.to_string(),
1 => ".".into(),
2 => rng
.pick(&[
"\\d", "\\D", "\\w", "\\W", "\\s", "\\S", "\\a", "\\l", "\\u", "\\x", "\\o", "\\h",
"\\k", "\\p", "\\i",
])
.to_string(),
3 => rng
.pick(&[
"[abc]", "[^abc]", "[a-z]", "[A-Z0-9]", "[^0-9]", "[.*]", "[]a]",
])
.to_string(),
4 => rng
.pick(&[
"[[:alpha:]]",
"[[:digit:]]",
"[[:space:]]",
"[[:punct:]]",
"[[:upper:]]",
"[[:alnum:]]",
])
.to_string(),
5 => format!("\\({}\\)", rx_branch(rng, 0)),
6 => format!("\\%({}\\)", rx_branch(rng, 0)),
7 => rng.pick(&["\\<", "\\>", "^", "$"]).to_string(),
8 => rng.pick(&["\\zs", "\\ze"]).to_string(),
9 => rng
.pick(&["\\%d97", "\\%x62", "\\%o143", "\\%u0061"])
.to_string(),
10 => rng.pick(&["\\_.", "\\_s", "\\_a", "\\_[a-z]"]).to_string(),
11 => format!("\\%[{}]", rng.pick(&["abc", "ab", "xyz"])),
12 => rng.pick(&["\\1", "\\2"]).to_string(),
_ => rng.pick(&["\\.", "\\*", "\\[", "\\\\"]).to_string(),
}
}
fn rx_piece(rng: &mut Rng, depth: u32) -> String {
let atom = if depth > 0 && rng.chance(1, 5) {
format!("\\({}\\)", rx_branch(rng, depth - 1))
} else {
rx_atom(rng)
};
match rng.below(9) {
0 => format!("{atom}*"),
1 => format!("{atom}\\+"),
2 => format!("{atom}\\?"),
3 => format!("{atom}\\="),
4 => format!("{atom}\\{{2}}"),
5 => format!("{atom}\\{{1,3}}"),
6 => format!("{atom}\\{{-}}"),
7 => format!("{atom}\\{{-1,}}"),
_ => atom,
}
}
fn rx_branch(rng: &mut Rng, depth: u32) -> String {
let n = 1 + rng.below(3);
let mut out = String::new();
for _ in 0..n {
out.push_str(&rx_piece(rng, depth));
}
if rng.chance(1, 4) {
out.push_str("\\|");
out.push_str(&rx_piece(rng, depth));
}
out
}
fn rx_pattern(rng: &mut Rng) -> String {
let body = rx_branch(rng, 1);
let prefix = match rng.below(8) {
0 => "\\c",
1 => "\\C",
2 => "\\v",
3 => "\\V",
4 => "\\M",
_ => "",
};
format!("'{prefix}{body}'")
}
fn gen_regex(rng: &mut Rng) -> String {
let pat = rx_pattern(rng);
let subj = rng.pick(RX_SUBJECTS);
match rng.below(8) {
0 => format!("match({subj}, {pat})"),
1 => format!("matchstr({subj}, {pat})"),
2 => format!("matchend({subj}, {pat})"),
3 => format!("matchlist({subj}, {pat})"),
4 => format!("substitute({subj}, {pat}, 'X', '')"),
5 => format!("substitute({subj}, {pat}, 'X', 'g')"),
6 => format!("substitute({subj}, {pat}, '[&]', 'g')"),
_ => format!("split({subj}, {pat})"),
}
}
const STMT_SHAPES: &[&str] = &[
"function! F(a) \n return a:a \n endfunction \n echo F(%E)",
"function! F(a, b = 2) \n return a:b \n endfunction \n echo F(1)",
"function! F(...) \n return a:0 \n endfunction \n echo F(%E, 2)",
"function! F(...) \n return a:000 \n endfunction \n echo F(%E)",
"function! F(a) \n if a:a > 0 \n return 'p' \n endif \n return 'n' \n endfunction \n echo F(1)",
"function! F() closure \n return 1 \n endfunction \n echo F()",
"let l:x = %E | let Fn = {-> l:x} | echo Fn()",
"let x = 1 | let x += %E | echo x",
"let s = 'a' | let s .= 'b' | echo s",
"let n = 10 | let n -= 3 | echo n",
"let [a, b] = [1, 2] | echo a + b",
"let [a; rest] = [1, 2, 3] | echo rest",
"let l = [1,2,3] | let l[0] = %E | echo l",
"let d = {'a': 1} | let d.a = %E | echo d",
"let d = {'a': 1} | let d['b'] = 2 | echo d",
"let l = [1,2,3] | let l[0:1] = [9,9] | echo l",
"let g:tmp = 1 | unlet g:tmp | echo exists('g:tmp')",
"let d = {'a':1,'b':2} | unlet d.a | echo d",
"for [k, v] in items({'a':1}) | echo k . v | endfor",
"for c in split('ab', '\\zs') | echo c | endfor",
"for b in 0z0102 | echo b | endfor",
"for i in range(3) | if i == 1 | continue | endif | echo i | endfor",
"for i in range(5) | if i == 2 | break | endif | echo i | endfor",
"let x = %E | echo $'v={x}'",
"let i = 0 | while i < 3 | let i += 1 | endwhile | echo i",
];
fn gen_cmd(rng: &mut Rng, funcs: &[(&str, &[Shape])], depth: u32) -> String {
let e = |rng: &mut Rng| gen_expr(rng, funcs, depth);
match rng.below(12) {
0 => format!("echo {}", e(rng)),
1 => format!("let x = {}", e(rng)),
2 => format!("let g:l = {}", e(rng)),
3 => format!("echo {} | echo {}", e(rng), e(rng)),
4 => format!("echo {} | echo 'tail'", e(rng)),
5 => format!("if {} | echo 'y' | else | echo 'n' | endif", e(rng)),
6 => format!("for i in {} | echo i | endfor", rng.pick(LISTS)),
7 => format!("try | echo {} | catch | echo 'caught' | endtry", e(rng)),
8 => format!("try\necho {}\ncatch\necho 'caught'\nendtry", e(rng)),
9 => format!("silent! echo {}", e(rng)),
10 => format!("let d = {} | echo d", e(rng)),
_ => format!("call add(g:l, {}) | echo g:l", e(rng)),
}
}
fn gen_stmt(rng: &mut Rng, funcs: &[(&str, &[Shape])], depth: u32) -> String {
let cmd = if rng.chance(1, 2) {
rng.pick(STMT_SHAPES)
.replace("%E", &gen_expr(rng, funcs, depth.min(1)))
} else {
gen_cmd(rng, funcs, depth)
};
format!("execute({})", vim_quote(&cmd))
}
fn gen_expr(rng: &mut Rng, funcs: &[(&str, &[Shape])], depth: u32) -> String {
if depth == 0 {
return match rng.below(4) {
0 => gen_call(rng, funcs),
_ => any_value(rng),
};
}
match rng.below(10) {
0..=4 => {
let call = gen_call(rng, funcs);
if rng.chance(1, 5) {
let i = rng.pick(&["0", "1", "-1", "2", "5", "-5"]);
if rng.chance(1, 2) {
format!("{call}[{i}]")
} else {
let j = rng.pick(&["", "0", "1", "-1", "3"]);
format!("{call}[{i}:{j}]")
}
} else {
call
}
}
5..=7 => {
let a = gen_expr(rng, funcs, depth - 1);
let b = gen_expr(rng, funcs, depth - 1);
let op = rng.pick(BINOPS);
format!("({a} {op} {b})")
}
8 => {
let a = gen_expr(rng, funcs, depth - 1);
let op = rng.pick(&["!", "-", "+"]);
format!("{op}({a})")
}
_ => {
let c = gen_expr(rng, funcs, depth - 1);
let a = gen_expr(rng, funcs, depth - 1);
let b = gen_expr(rng, funcs, depth - 1);
format!("({c} ? {a} : {b})")
}
}
}
#[derive(Clone, PartialEq, Eq)]
enum Outcome {
Val(String),
Err(String),
Panic(String),
Missing,
}
impl Outcome {
fn show(&self) -> String {
match self {
Outcome::Val(v) => v.clone(),
Outcome::Err(e) => format!("<{e}>"),
Outcome::Panic(p) => format!("<PANIC: {p}>"),
Outcome::Missing => "<none>".into(),
}
}
}
fn enumber(msg: &str) -> String {
let b = msg.as_bytes();
for i in 0..b.len() {
if b[i] == b'E' && b.get(i + 1).is_some_and(u8::is_ascii_digit) {
let d: String = msg[i + 1..]
.chars()
.take_while(char::is_ascii_digit)
.collect();
if msg[i + 1 + d.len()..].starts_with(':') {
return format!("E{d}");
}
}
}
"E?".into()
}
fn eval_one(expr: &str) -> Outcome {
let r = panic::catch_unwind(AssertUnwindSafe(|| {
observe_errors_begin();
for line in PRELUDE {
let _ = vimlrs::eval_source(line);
}
let _ = observe_errors_take();
did_emsg.with(|d| d.set(0));
observe_errors_begin();
let out = vimlrs::eval_expr(expr);
let errs = observe_errors_take();
let unhandled = did_emsg.with(|d| d.get()) != 0;
match out {
Ok(v) if !unhandled => Outcome::Val(encode_tv2string(&v)),
Ok(_) => Outcome::Err(enumber(errs.first().map_or("E?", |s| s.as_str()))),
Err(e) => Outcome::Err(enumber(&e.to_string())),
}
}));
r.unwrap_or_else(|e| {
let msg = e
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "?".into());
Outcome::Panic(format!("panic: {}", msg.lines().next().unwrap_or("?")))
})
}
fn child_main(corpus: &Path, start: usize, out: &Path) -> ! {
LIMIT.store(MEM_LIMIT, Ordering::Relaxed);
panic::set_hook(Box::new(|_| {}));
let text = std::fs::read_to_string(corpus).expect("corpus");
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(out)
.expect("outcomes");
for (i, expr) in text.lines().enumerate().skip(start) {
let (tag, payload) = match eval_one(expr) {
Outcome::Val(v) => ("V", v),
Outcome::Err(e) => ("E", e),
Outcome::Panic(p) => ("P", p),
Outcome::Missing => ("P", "vanished".into()),
};
let _ = writeln!(f, "{i}\t{tag}\t{}", payload.replace('\n', "\0"));
let _ = f.flush();
}
std::process::exit(0);
}
fn slurp(path: &Path) -> String {
std::fs::read(path)
.map(|b| String::from_utf8_lossy(&b).into_owned())
.unwrap_or_default()
}
fn read_outcomes(path: &Path, res: &mut [Outcome]) {
let text = slurp(path);
for line in text.lines() {
let mut it = line.splitn(3, '\t');
let (Some(i), Some(tag), Some(payload)) = (it.next(), it.next(), it.next()) else {
continue;
};
let Ok(i) = i.parse::<usize>() else { continue };
if i >= res.len() {
continue;
}
res[i] = match tag {
"V" => Outcome::Val(payload.to_string()),
"E" => Outcome::Err(payload.to_string()),
_ => Outcome::Panic(payload.to_string()),
};
}
}
fn run_vimlrs(exprs: &[String], tmp: &Path) -> Vec<Outcome> {
let corpus = tmp.join("corpus.txt");
let out = tmp.join("outcomes.txt");
std::fs::write(&corpus, format!("{}\n", exprs.join("\n"))).expect("write corpus");
let _ = std::fs::remove_file(&out);
let me = std::env::current_exe().expect("current exe");
let mut res = vec![Outcome::Missing; exprs.len()];
let mut next = 0usize;
while next < exprs.len() {
let mut child = Command::new(&me)
.arg("--child")
.arg(&corpus)
.arg(next.to_string())
.arg(&out)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn child");
let finished = wait_bounded(&mut child, CHUNK_TIMEOUT);
read_outcomes(&out, &mut res);
let Some(stuck) = (next..exprs.len()).find(|&i| res[i] == Outcome::Missing) else {
break;
};
if finished && stuck == exprs.len() {
break;
}
res[stuck] = Outcome::Panic(if finished {
"crashed (abort / OOM / signal)".into()
} else {
format!("hung (>{}s)", CHUNK_TIMEOUT.as_secs())
});
next = stuck + 1;
}
res
}
fn vim_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "''"))
}
fn driver(exprs: &[String], lo: usize, hi: usize, out_path: &Path) -> String {
let mut s = String::new();
s.push_str("set cpo&vim\n");
let _ = writeln!(
s,
"let s:exprs = [{}]",
exprs[lo..hi]
.iter()
.map(|e| vim_quote(e))
.collect::<Vec<_>>()
.join(",")
);
let _ = writeln!(
s,
"let s:out = {}",
vim_quote(&out_path.display().to_string())
);
let _ = writeln!(s, "for s:i in range({lo}, {})", hi - 1);
for line in PRELUDE {
let _ = writeln!(s, " {line}");
}
s.push_str(" try\n");
let _ = writeln!(
s,
" let s:r = s:i . \"\\tV\\t\" . string(eval(s:exprs[s:i - {lo}]))"
);
s.push_str(" catch\n");
s.push_str(" let s:r = s:i . \"\\tE\\t\" . v:exception\n");
s.push_str(" endtry\n");
s.push_str(" call writefile([s:r], s:out, 'a')\n");
s.push_str("endfor\n");
s.push_str("qa!\n");
s
}
fn read_oracle(path: &Path, res: &mut [Outcome]) {
let text = slurp(path);
for line in text.lines() {
let mut it = line.splitn(3, '\t');
let (Some(i), Some(tag), Some(payload)) = (it.next(), it.next(), it.next()) else {
continue;
};
let Ok(i) = i.parse::<usize>() else { continue };
if i >= res.len() {
continue;
}
res[i] = match tag {
"V" => Outcome::Val(payload.to_string()),
"E" => Outcome::Err(enumber(payload)),
_ => continue,
};
}
}
fn run_oracle(engine: &str, exprs: &[String], tmp: &Path) -> Vec<Outcome> {
let script = tmp.join(format!("drv_{engine}.vim"));
let out = tmp.join(format!("out_{engine}.txt"));
let _ = std::fs::remove_file(&out);
let mut res = vec![Outcome::Missing; exprs.len()];
let mut next = 0usize;
while next < exprs.len() {
let hi = (next + CHUNK).min(exprs.len());
std::fs::write(&script, driver(exprs, next, hi, &out)).expect("write driver");
let mut cmd = Command::new(engine);
if engine == "nvim" {
cmd.args(["--headless", "--clean", "-S"]).arg(&script);
} else {
cmd.args(["-es", "-u", "NONE", "-i", "NONE", "-S"])
.arg(&script);
}
let spawned = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
let Ok(mut child) = spawned else {
return res; };
wait_bounded(&mut child, CHUNK_TIMEOUT);
read_oracle(&out, &mut res);
next = match (next..hi).find(|&i| res[i] == Outcome::Missing) {
Some(stuck) => stuck + 1,
None => hi,
};
}
res
}
#[derive(Clone, Copy, PartialEq)]
enum Class {
Ok,
Gap,
Panic,
Divergent,
OracleFail,
}
fn classify(v: &Outcome, nv: &Outcome, vi: &Outcome) -> Class {
if matches!(v, Outcome::Panic(_)) {
return Class::Panic;
}
match (nv, vi) {
(Outcome::Missing, _) | (_, Outcome::Missing) => Class::OracleFail,
(a, b) if a == b => {
if v == a {
Class::Ok
} else {
Class::Gap
}
}
_ => {
if v == nv {
Class::Ok
} else {
Class::Divergent
}
}
}
}
fn signature(expr: &str, v: &Outcome, o: &Outcome) -> String {
let head: String = expr
.trim_start_matches(['(', '!', '-', '+'])
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
let name = if head.is_empty() {
"<expr>".to_string()
} else {
head
};
let kind = |o: &Outcome| match o {
Outcome::Val(_) => "val".to_string(),
Outcome::Err(e) => e.clone(),
Outcome::Panic(_) => "panic".into(),
Outcome::Missing => "none".into(),
};
format!("{name} [{} vs {}]", kind(v), kind(o))
}
struct Args {
count: usize,
seed: u64,
depth: u32,
only: Vec<String>,
corpus: Option<String>,
verbose: bool,
stmts: bool,
regex: bool,
}
fn parse_args() -> Args {
let mut a = Args {
count: 1000,
seed: 1,
depth: 2,
only: Vec::new(),
corpus: None,
verbose: false,
stmts: false,
regex: false,
};
let argv: Vec<String> = std::env::args().skip(1).collect();
let mut i = 0;
while i < argv.len() {
let next = |i: usize| argv.get(i + 1).cloned().unwrap_or_default();
match argv[i].as_str() {
"--count" | "-n" => {
a.count = next(i).parse().unwrap_or(a.count);
i += 1;
}
"--seed" | "-s" => {
a.seed = next(i).parse().unwrap_or(a.seed);
i += 1;
}
"--depth" | "-d" => {
a.depth = next(i).parse().unwrap_or(a.depth);
i += 1;
}
"--only" => {
a.only = next(i).split(',').map(str::to_string).collect();
i += 1;
}
"--corpus" => {
a.corpus = Some(next(i));
i += 1;
}
"--stmts" => a.stmts = true,
"--regex" => a.regex = true,
"--verbose" | "-v" => a.verbose = true,
"--help" | "-h" => {
println!(
"fuzz-parity — differential fuzzer: vimlrs vs nvim + vim\n\n\
USAGE: fuzz-parity [--count N] [--seed S] [--depth D]\n\
\x20 [--only fn1,fn2] [--corpus FILE] [--verbose]\n\n\
--count N expressions to generate (default 1000)\n\
--seed S PRNG seed; same seed → same corpus (default 1)\n\
--depth D max expression nesting depth (default 2)\n\
--only LIST restrict generation to these builtins\n\
--stmts fuzz STATEMENTS (:let/:if/:for/:try/`|` lines) via execute()\n\
--regex fuzz REGEX: grammar-built patterns x subjects x APIs\n\
--corpus FILE append confirmed gaps as `expr<TAB>expected` lines\n\
--verbose list every case, not just divergences"
);
std::process::exit(0);
}
_ => {}
}
i += 1;
}
a
}
fn main() {
let argv: Vec<String> = std::env::args().collect();
if argv.get(1).map(String::as_str) == Some("--child") {
let corpus = PathBuf::from(&argv[2]);
let start: usize = argv[3].parse().expect("child start index");
let out = PathBuf::from(&argv[4]);
child_main(&corpus, start, &out);
}
let args = parse_args();
let funcs: Vec<(&str, &[Shape])> = if args.only.is_empty() {
FUNCS.to_vec()
} else {
FUNCS
.iter()
.filter(|(n, _)| args.only.iter().any(|o| o == n))
.copied()
.collect()
};
if funcs.is_empty() {
eprintln!("fuzz-parity: --only matched no allow-listed builtin");
std::process::exit(2);
}
let mut rng = Rng(args.seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1);
let mut exprs: Vec<String> = Vec::with_capacity(args.count);
while exprs.len() < args.count {
let e = if args.regex {
gen_regex(&mut rng)
} else if args.stmts {
gen_stmt(&mut rng, &funcs, args.depth)
} else {
gen_expr(&mut rng, &funcs, args.depth)
};
if !e.contains('\n') && !e.contains('\0') {
exprs.push(e);
}
}
eprintln!(
"fuzz-parity: {} exprs, seed {}, depth {}",
exprs.len(),
args.seed,
args.depth
);
let tmp = std::env::temp_dir().join("vimlrs-fuzz");
std::fs::create_dir_all(&tmp).expect("tmp dir");
let mine = run_vimlrs(&exprs, &tmp);
let nv = run_oracle("nvim", &exprs, &tmp);
let vi = run_oracle("vim", &exprs, &tmp);
if nv.iter().all(|o| *o == Outcome::Missing) {
eprintln!("fuzz-parity: nvim produced no results — is it installed?");
}
if vi.iter().all(|o| *o == Outcome::Missing) {
eprintln!("fuzz-parity: vim produced no results — is it installed?");
}
let mut buckets: BTreeMap<(u8, String), (usize, String, String, String)> = BTreeMap::new();
let mut counts = [0usize; 5];
let mut corpus_lines: Vec<String> = Vec::new();
for (i, e) in exprs.iter().enumerate() {
let class = classify(&mine[i], &nv[i], &vi[i]);
let slot = match class {
Class::Ok => 0,
Class::Gap => 1,
Class::Panic => 2,
Class::Divergent => 3,
Class::OracleFail => 4,
};
counts[slot] += 1;
if args.verbose {
println!(
"[{}] {e}\n viml={} nvim={} vim={}",
match class {
Class::Ok => "ok",
Class::Gap => "GAP",
Class::Panic => "PANIC",
Class::Divergent => "div",
Class::OracleFail => "oracle-fail",
},
mine[i].show(),
nv[i].show(),
vi[i].show()
);
}
if matches!(class, Class::Ok | Class::OracleFail) {
continue;
}
if class == Class::Gap {
if let Outcome::Val(want) = &nv[i] {
corpus_lines.push(format!("{e}\t{want}"));
} else if let Outcome::Err(want) = &nv[i] {
corpus_lines.push(format!("{e}\t!{want}"));
}
}
let sig = signature(e, &mine[i], &nv[i]);
let entry = buckets.entry((slot as u8, sig)).or_insert_with(|| {
(
0,
e.clone(),
mine[i].show(),
format!("nvim={} vim={}", nv[i].show(), vi[i].show()),
)
});
entry.0 += 1;
}
let heading = |slot: u8| match slot {
1 => "CONFIRMED GAPS (nvim == vim, vimlrs differs)",
2 => "PANICS (vimlrs crashed)",
3 => "VIM/NEOVIM DIVERGENCE (advisory — no single spec)",
_ => "OTHER",
};
let mut last = 0u8;
for ((slot, sig), (n, expr, got, want)) in &buckets {
if *slot != last {
println!("\n══ {} ══", heading(*slot));
last = *slot;
}
println!("\n {sig} ×{n}");
println!(" expr: {expr}");
println!(" vimlrs: {got}");
println!(" oracle: {want}");
}
println!(
"\n── summary ──\n ok: {}\n GAPS: {} ({} distinct)\n PANICS: {}\n divergent: {}\n oracle-fail: {}",
counts[0],
counts[1],
buckets.keys().filter(|(s, _)| *s == 1).count(),
counts[2],
counts[3],
counts[4]
);
if let Some(path) = &args.corpus {
if !corpus_lines.is_empty() {
corpus_lines.sort();
corpus_lines.dedup();
let prev = std::fs::read_to_string(path).unwrap_or_default();
let mut all: Vec<&str> = prev
.lines()
.chain(corpus_lines.iter().map(String::as_str))
.collect();
all.sort_unstable();
all.dedup();
std::fs::write(path, format!("{}\n", all.join("\n"))).expect("write corpus");
println!(" corpus: {} cases → {path}", all.len());
}
}
if counts[1] > 0 || counts[2] > 0 {
std::process::exit(1);
}
}