use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use lang_forge::Language;
struct Counting;
static IN_USE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
static MEASURING: Mutex<()> = Mutex::new(());
fn grew(bytes: usize) {
let now = IN_USE.fetch_add(bytes, Ordering::Relaxed) + bytes;
let _ = PEAK.fetch_max(now, Ordering::Relaxed);
}
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let block = unsafe { System.alloc(layout) };
if !block.is_null() {
grew(layout.size());
}
block
}
unsafe fn dealloc(&self, block: *mut u8, layout: Layout) {
unsafe { System.dealloc(block, layout) };
let _ = IN_USE.fetch_sub(layout.size(), Ordering::Relaxed);
}
unsafe fn realloc(&self, block: *mut u8, layout: Layout, size: usize) -> *mut u8 {
let moved = unsafe { System.realloc(block, layout, size) };
if !moved.is_null() {
if size > layout.size() {
grew(size - layout.size());
} else {
let _ = IN_USE.fetch_sub(layout.size() - size, Ordering::Relaxed);
}
}
moved
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
fn peak<T>(work: impl FnOnce() -> T) -> (T, usize) {
let base = IN_USE.load(Ordering::Relaxed);
PEAK.store(base, Ordering::Relaxed);
let out = work();
(out, PEAK.load(Ordering::Relaxed).saturating_sub(base))
}
const MIB: usize = 1 << 20;
#[test]
fn test_memo_memory_does_not_grow_with_nesting_depth() {
let _lock = MEASURING.lock().unwrap_or_else(|e| e.into_inner());
let lang = Language::from_lsf(
"[language]\nname = \"s\"\n[rules]\nfile = \"stmt*\"\nstmt = \"assign | expr ';'\"\n\
assign = \"expr '=' expr ';'\"\nblock = \"'{' stmt* '}'\"\n\
call = \"IDENT '(' (expr (',' expr)*)? ')'\"\n\
[rules.expr]\noperand = \"call | IDENT | NUMBER | block\"\nlevels = [{ left = [\"+\"] }]\n",
)
.unwrap_or_else(|e| panic!("{e}"));
let source = |depth: usize| {
let mut src = String::new();
for _ in 0..depth {
src.push_str("{ ");
for i in 0..12_000 / depth {
src.push_str(&format!("f(x{i}) + 1; "));
}
}
src.push_str("x;");
src.push_str(&" };".repeat(depth));
src
};
let mut peaks = [0; 2];
for (slot, depth) in peaks.iter_mut().zip([1, 60]) {
let src = source(depth);
let (errors, bytes) = peak(|| lang.parse(&src).has_errors());
assert!(!errors);
*slot = bytes;
}
assert!(
peaks[1] < peaks[0] * 3 / 2,
"flat {} MiB, nested {} MiB",
peaks[0] / MIB,
peaks[1] / MIB
);
}
#[test]
fn test_hostile_schematic_is_refused_in_bounded_memory() {
let _lock = MEASURING.lock().unwrap_or_else(|e| e.into_inner());
let mut text = String::from("[language]\nname = \"h\"\n[rules]\n");
for r in 0..100 {
text.push_str(&format!("r{r} = \""));
for g in r * 1000..(r + 1) * 1000 {
let (a, b) = ((2 * g) % 30_000, (2 * g + 1) % 30_000);
text.push_str(&format!("('k{a}' | 'k{b}')? "));
}
text.push_str("\"\n");
}
let (refused, bytes) = peak(|| Language::from_lsf(&text).is_err());
assert!(refused);
assert!(bytes < 128 * MIB, "{} MiB", bytes / MIB);
}
#[test]
fn test_large_keyword_grammar_forges_within_the_table_budget() {
let _lock = MEASURING.lock().unwrap_or_else(|e| e.into_inner());
let alternatives: Vec<String> = (0..30_000).map(|i| format!("'k{i}' 'z'")).collect();
let text = format!(
"[language]\nname = \"k\"\n[rules]\na = \"{}\"\n",
alternatives.join(" | ")
);
let (lang, bytes) = peak(|| Language::from_lsf(&text));
let lang = lang.unwrap_or_else(|e| panic!("{e}"));
assert!(!lang.parse("k12345 z").has_errors());
assert!(bytes < 256 * MIB, "{} MiB", bytes / MIB);
}