1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! The compiled-chunk cache, keyed by source text.
//!
//! A Tcl script can build a script at run time and evaluate it, and `eval` in a
//! loop evaluates the same text on every pass. Parsing and lowering that text
//! once is the whole reason tclrs compiles rather than interprets, so the text
//! is the cache key: identical source is identical bytecode, whatever produced
//! it.
//!
//! The key is the source text and nothing else. Nothing outside the text can
//! change what it lowers to — the compiler reads no interpreter state, and the
//! variables a chunk touches are bound to slots by name, not by value — so
//! there is no context to fold into the key and no way for an entry to go
//! stale within a process.
//!
//! Entries are held as `Arc<Chunk>` and cloned into the VM per run, because
//! `fusevm::VM::new` takes the chunk by value. That clone copies the op vector,
//! the constant pool and the name table; it does not parse, resolve a variable
//! name, or lay out a jump.
use std::collections::HashMap;
use std::sync::Arc;
use fusevm::Chunk;
use crate::runtime::TclError;
/// How many compiled scripts one interpreter keeps.
pub const DEFAULT_CAPACITY: usize = 1024;
/// Compiled scripts, keyed by their source text.
#[derive(Debug)]
pub struct ChunkCache {
/// Keyed by the source text *and* by whether it was lowered for a frame
/// projection, because the same text lowers differently in the two cases —
/// see [`crate::compiler::compile_projected`]. Almost every script is
/// compiled one way only, so the second key costs a `bool` per entry and
/// nothing else.
entries: HashMap<(bool, String), Arc<Chunk>>,
capacity: usize,
hits: u64,
misses: u64,
}
impl ChunkCache {
/// A cache holding up to [`DEFAULT_CAPACITY`] scripts.
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: HashMap::new(),
capacity: capacity.max(1),
hits: 0,
misses: 0,
}
}
/// The chunk for `src`, compiling it on the first request.
///
/// A source that fails to compile is not stored: the failure is reported on
/// every attempt, and a diagnostic is not worth a cache slot.
pub fn compile(&mut self, src: &str) -> Result<Arc<Chunk>, TclError> {
self.compile_in(src, false)
}
/// [`ChunkCache::compile`], for a script that will run inside a frame
/// projection. Cached apart from the same text compiled outside one.
pub fn compile_in(&mut self, src: &str, projected: bool) -> Result<Arc<Chunk>, TclError> {
let key = (projected, src.to_string());
if let Some(chunk) = self.entries.get(&key) {
self.hits += 1;
return Ok(Arc::clone(chunk));
}
self.misses += 1;
// An inline `rust { ... }` block is not Tcl and never reaches the
// parser: it is rewritten into a command first. The cache is keyed by
// the source as written, so the rewrite is paid once per distinct
// script, like the parse and the lowering it is part of.
let rewritten = crate::rust_ffi::desugar(src);
let script = crate::parser::parse(&rewritten).map_err(|e| TclError {
msg: e.msg,
line: Some(e.line),
code: crate::runtime::TCL_ERROR,
level: 0,
})?;
let lowered = if projected {
crate::compiler::compile_projected(&script)
} else {
crate::compiler::compile(&script)
};
let chunk = Arc::new(lowered.map_err(|e| TclError {
msg: e.msg,
line: Some(e.line),
code: crate::runtime::TCL_ERROR,
level: 0,
})?);
// At capacity the whole cache is dropped rather than one entry chosen.
// An `eval` loop reuses a handful of sources and never reaches the
// limit; a script that reaches it is generating fresh text every pass,
// where no eviction order would have kept the useful entry either.
if self.entries.len() >= self.capacity {
self.entries.clear();
}
self.entries.insert(key, Arc::clone(&chunk));
Ok(chunk)
}
/// How many compiled scripts are held.
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// `(hits, misses)` — one miss per compilation performed.
pub fn stats(&self) -> (u64, u64) {
(self.hits, self.misses)
}
/// Drop every entry, keeping the counters.
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl Default for ChunkCache {
fn default() -> Self {
Self::new()
}
}