Skip to content

Commit 7248563

Browse files
Feat and Fix: Template memoization by value, scope chain for calls, version table precompute, OWASP limits, unclosed paren detection.
1 parent 0b3c5e8 commit 7248563

3 files changed

Lines changed: 92 additions & 33 deletions

File tree

compiler/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ hashbrown = { version = "0.15", default-features = false, features = ["default-h
2727
log = "0.4"
2828
stderrlog = { version = "0.6", default-features = false }
2929
lol_alloc = { version = "0.4", optional = true }
30+
itoa = "1"
31+
ryu = "1"
3032

3133
[dev-dependencies]
3234
serde = { version = "1", features = ["derive"] }

compiler/src/modules/parser.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,7 +1857,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
18571857
}
18581858

18591859
fn parse_args(&mut self) -> u16 {
1860-
self.advance();
1860+
self.advance(); // consume '('
18611861
let mut argc = 0;
18621862
while !matches!(self.peek(), Some(TokenType::Rpar) | None) {
18631863
if self.eat_if(TokenType::Star) {
@@ -1870,9 +1870,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
18701870
let t = self.advance();
18711871
if matches!(self.peek(), Some(TokenType::Equal)) {
18721872
self.advance();
1873-
let i = self
1874-
.chunk
1875-
.push_const(Value::Str(self.lexeme(&t).to_string()));
1873+
let i = self.chunk.push_const(Value::Str(self.lexeme(&t).to_string()));
18761874
self.chunk.emit(OpCode::LoadConst, i);
18771875
self.expr();
18781876
} else {
@@ -1887,7 +1885,7 @@ impl<'src, I: Iterator<Item = Token>> Parser<'src, I> {
18871885
self.advance();
18881886
}
18891887
}
1890-
self.advance();
1888+
self.eat(TokenType::Rpar);
18911889
argc
18921890
}
18931891

compiler/src/modules/vm.rs

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use core::fmt;
1212
// ── A04:2021 — runtime limits ──
1313

1414
const MAX_STACK: usize = 1_024;
15-
const MAX_CALLS: usize = 256;
15+
const MAX_CALLS: usize = 512;
1616
const MAX_HEAP: usize = 100_000;
17-
const MAX_OPS: usize = 10_000_000;
17+
const MAX_OPS: usize = 100_000_000;
1818
const CACHE_THRESHOLD: u8 = 8;
1919
const HOTSPOT_THRESHOLD: u32 = 1_000;
2020
const TEMPLATE_THRESHOLD: u32 = 4;
@@ -75,9 +75,22 @@ impl Obj {
7575

7676
pub fn display(&self) -> String {
7777
match self {
78-
Self::Int(i) => i.to_string(),
79-
Self::Float(f) if *f == (*f as i64) as f64 && f.is_finite() => format!("{:.1}", f),
80-
Self::Float(f) => f.to_string(),
78+
Self::Int(i) => {
79+
let mut buf = itoa::Buffer::new();
80+
buf.format(*i).into()
81+
}
82+
Self::Float(f) if *f == (*f as i64) as f64 && f.is_finite() => {
83+
let mut s = itoa::Buffer::new();
84+
let i = *f as i64;
85+
let mut out = String::with_capacity(20);
86+
out.push_str(s.format(i));
87+
out.push_str(".0");
88+
out
89+
}
90+
Self::Float(f) => {
91+
let mut buf = ryu::Buffer::new();
92+
buf.format(*f).into()
93+
}
8194
Self::Str(s) => s.clone(),
8295
Self::Bool(b) => if *b { "True" } else { "False" }.into(),
8396
Self::None => "None".into(),
@@ -175,26 +188,44 @@ impl InlineCache {
175188

176189
// ── Template table — memoized function results ──
177190

178-
struct TemplateEntry { result: Obj, hits: u32 }
191+
struct TemplateEntry { args: Vec<Obj>, result: Obj, hits: u32 }
179192

180-
struct TemplateTable { entries: HashMap<(usize, Vec<u8>), TemplateEntry> }
193+
struct TemplateTable { entries: HashMap<usize, Vec<TemplateEntry>> }
181194

182195
impl TemplateTable {
183196
fn new() -> Self { Self { entries: HashMap::new() } }
184197

185198
fn lookup(&self, fi: usize, args: &[Obj]) -> Option<&Obj> {
186-
let e = self.entries.get(&(fi, args.iter().map(type_tag).collect()))?;
187-
if e.hits >= TEMPLATE_THRESHOLD { Some(&e.result) } else { None }
199+
let entries = self.entries.get(&fi)?;
200+
entries.iter()
201+
.find(|e| e.hits >= TEMPLATE_THRESHOLD && Self::args_eq(&e.args, args))
202+
.map(|e| &e.result)
188203
}
189204

190205
fn record(&mut self, fi: usize, args: &[Obj], result: &Obj) {
191-
let key = (fi, args.iter().map(type_tag).collect());
192-
let e = self.entries.entry(key).or_insert(TemplateEntry { result: result.clone(), hits: 0 });
193-
e.hits += 1;
194-
e.result = result.clone();
206+
let entries = self.entries.entry(fi).or_insert_with(Vec::new);
207+
if let Some(e) = entries.iter_mut().find(|e| Self::args_eq(&e.args, args)) {
208+
e.hits += 1;
209+
e.result = result.clone();
210+
} else if entries.len() < 256 {
211+
entries.push(TemplateEntry { args: args.to_vec(), result: result.clone(), hits: 1 });
212+
}
213+
}
214+
215+
fn args_eq(a: &[Obj], b: &[Obj]) -> bool {
216+
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| match (x, y) {
217+
(Obj::Int(a), Obj::Int(b)) => a == b,
218+
(Obj::Float(a), Obj::Float(b)) => a == b,
219+
(Obj::Str(a), Obj::Str(b)) => a == b,
220+
(Obj::Bool(a), Obj::Bool(b)) => a == b,
221+
(Obj::None, Obj::None) => true,
222+
_ => false,
223+
})
195224
}
196225

197-
fn cached_count(&self) -> usize { self.entries.values().filter(|e| e.hits >= TEMPLATE_THRESHOLD).count() }
226+
fn cached_count(&self) -> usize {
227+
self.entries.values().map(|v| v.iter().filter(|e| e.hits >= TEMPLATE_THRESHOLD).count()).sum()
228+
}
198229
}
199230

200231
// ── Adaptive engine — hotspot detection + bytecode overlay ──
@@ -240,15 +271,26 @@ pub struct VM<'a> {
240271
budget: usize,
241272
depth: usize,
242273
pub output: Vec<String>,
274+
versions: HashMap<String, String>, // "x_2" -> "x_1", precomputed prev version map
243275
}
244276

245277
impl<'a> VM<'a> {
246278
pub fn new(chunk: &'a SSAChunk) -> Self {
247279
let n = chunk.instructions.len();
280+
let mut versions = HashMap::new();
281+
for name in &chunk.names {
282+
if let Some(pos) = name.rfind('_') {
283+
if let Ok(ver) = name[pos + 1..].parse::<u32>() {
284+
if ver > 0 {
285+
versions.insert(name.clone(), format!("{}_{}", &name[..pos], ver - 1));
286+
}
287+
}
288+
}
289+
}
248290
Self {
249291
stack: Vec::with_capacity(256), chunk, pool: Pool::new(),
250292
cache: InlineCache::new(n), templates: TemplateTable::new(), adaptive: AdaptiveEngine::new(n),
251-
budget: MAX_OPS, depth: 0, output: Vec::new(),
293+
budget: MAX_OPS, depth: 0, output: Vec::new(), versions,
252294
}
253295
}
254296

@@ -333,14 +375,12 @@ impl<'a> VM<'a> {
333375

334376
OpCode::LoadConst => self.push(self.to_obj(&chunk.constants[op as usize]))?,
335377
OpCode::LoadName => { let n = &chunk.names[op as usize]; self.push(names.get(n).cloned().ok_or_else(|| VmErr::Name(n.clone()))?)?; }
336-
OpCode::StoreName => {
378+
OpCode::StoreName => {
337379
let v = self.pop()?;
338380
let full = &chunk.names[op as usize];
339381
names.insert(full.clone(), v.clone());
340-
if let Some(pos) = full.rfind('_') {
341-
if let Ok(ver) = full[pos + 1..].parse::<u32>() {
342-
if ver > 0 { names.insert(format!("{}_{}", &full[..pos], ver - 1), v); }
343-
}
382+
if let Some(prev) = self.versions.get(full) {
383+
names.insert(prev.clone(), v);
344384
}
345385
}
346386
OpCode::LoadTrue => self.push(Obj::Bool(true))?,
@@ -433,24 +473,43 @@ impl<'a> VM<'a> {
433473
OpCode::MakeFunction | OpCode::MakeCoroutine => self.push(Obj::Func(op as usize))?,
434474
OpCode::Call => {
435475
let argc = op as usize;
436-
let args = self.pop_n(argc)?;
437-
let func = self.pop()?;
438-
match func {
476+
if self.depth >= MAX_CALLS { return Err(VmErr::CallDepth); }
477+
478+
// Extraer argumentos en orden de llamada (sin reverse)
479+
let mut args = Vec::with_capacity(argc);
480+
for _ in 0..argc {
481+
args.push(self.pop()?);
482+
}
483+
args.reverse();
484+
let callable = self.pop()?;
485+
486+
match callable {
439487
Obj::Func(fi) => {
440-
if self.depth >= MAX_CALLS { return Err(VmErr::CallDepth); }
441-
if let Some(cached) = self.templates.lookup(fi, &args) { self.push(cached.clone())?; continue; }
488+
if let Some(cached) = self.templates.lookup(fi, &args) {
489+
self.push(cached.clone())?;
490+
continue;
491+
}
442492
self.depth += 1;
443493
let (params, body, _) = &self.chunk.functions[fi];
444-
let mut fn_ns = names.clone();
494+
// Scope chain: only bind params, pass outer scope by ref
495+
let mut fn_ns: HashMap<String, Obj> = HashMap::with_capacity(params.len() + 4);
445496
for (i, p) in params.iter().enumerate() {
446-
if i < args.len() { fn_ns.insert(format!("{}_0", p.trim_start_matches('*')), args[i].clone()); }
497+
if i < args.len() {
498+
fn_ns.insert(format!("{}_0", p.trim_start_matches('*')), args[i].clone());
499+
}
500+
}
501+
// Copy only function definitions from outer scope (for recursion)
502+
for (k, v) in names.iter() {
503+
if matches!(v, Obj::Func(_)) {
504+
fn_ns.insert(k.clone(), v.clone());
505+
}
447506
}
448507
let result = self.exec(body, &mut fn_ns)?;
449508
self.depth -= 1;
450509
self.templates.record(fi, &args, &result);
451510
self.push(result)?;
452511
}
453-
_ => return Err(VmErr::Type(format!("'{}' not callable", func.ty()))),
512+
_ => return Err(VmErr::Type("call non‑function".into())),
454513
}
455514
}
456515

0 commit comments

Comments
 (0)