Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
362 lines
10 KiB
Rust
362 lines
10 KiB
Rust
use std::borrow::Cow;
|
|
use std::collections::{BTreeMap, HashSet};
|
|
use std::fmt;
|
|
|
|
#[cfg(feature = "macros")]
|
|
use std::sync::Arc;
|
|
|
|
use crate::environment::Environment;
|
|
use crate::error::{Error, ErrorKind};
|
|
use crate::value::Value;
|
|
use crate::vm::loop_object::LoopState;
|
|
|
|
#[cfg(feature = "macros")]
|
|
use crate::vm::closure_object::Closure;
|
|
|
|
type Locals<'env> = BTreeMap<&'env str, Value>;
|
|
|
|
pub(crate) struct Frame<'env> {
|
|
pub(crate) locals: Locals<'env>,
|
|
pub(crate) ctx: Value,
|
|
pub(crate) current_loop: Option<LoopState>,
|
|
|
|
// normally a frame does not carry a closure, but it can when a macro is
|
|
// declared. Once that happens, all writes to the frames locals are also
|
|
// duplicated into the closure. Macros declared on that level, then share
|
|
// the closure object to enclose the parent values. This emulates the
|
|
// behavior of closures in Jinja2.
|
|
#[cfg(feature = "macros")]
|
|
pub(crate) closure: Option<Arc<Closure>>,
|
|
}
|
|
|
|
impl<'env> Default for Frame<'env> {
|
|
fn default() -> Frame<'env> {
|
|
Frame::new(Value::UNDEFINED)
|
|
}
|
|
}
|
|
|
|
impl<'env> Frame<'env> {
|
|
/// Creates a new frame with the given context and no validation
|
|
pub fn new(ctx: Value) -> Frame<'env> {
|
|
Frame {
|
|
locals: Locals::new(),
|
|
ctx,
|
|
current_loop: None,
|
|
#[cfg(feature = "macros")]
|
|
closure: None,
|
|
}
|
|
}
|
|
|
|
/// Creates a new frame with the given context and validates the value is not invalid
|
|
pub fn new_checked(root: Value) -> Result<Frame<'env>, Error> {
|
|
Ok(Frame::new(ok!(root.validate())))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "internal_debug")]
|
|
impl fmt::Debug for Frame<'_> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut m = f.debug_map();
|
|
m.entry(&"locals", &self.locals);
|
|
if let Some(LoopState {
|
|
object: ref controller,
|
|
..
|
|
}) = self.current_loop
|
|
{
|
|
m.entry(&"loop", controller);
|
|
}
|
|
if !self.ctx.is_undefined() {
|
|
m.entry(&"ctx", &self.ctx);
|
|
}
|
|
m.finish()
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(feature = "internal_debug", derive(Debug))]
|
|
pub(crate) struct Stack {
|
|
values: Vec<Value>,
|
|
}
|
|
|
|
impl Default for Stack {
|
|
fn default() -> Stack {
|
|
Stack {
|
|
values: Vec::with_capacity(24),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Stack {
|
|
pub fn push(&mut self, arg: Value) {
|
|
self.values.push(arg);
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn pop(&mut self) -> Value {
|
|
self.values.pop().unwrap()
|
|
}
|
|
|
|
pub fn reverse_top(&mut self, n: usize) {
|
|
let start = self.values.len() - n;
|
|
self.values[start..].reverse();
|
|
}
|
|
|
|
pub fn get_call_args(&mut self, n: Option<u16>) -> &[Value] {
|
|
let n = match n {
|
|
Some(n) => n as usize,
|
|
None => self.pop().as_usize().unwrap(),
|
|
};
|
|
&self.values[self.values.len() - n..]
|
|
}
|
|
|
|
pub fn drop_top(&mut self, n: usize) {
|
|
self.values.truncate(self.values.len() - n);
|
|
}
|
|
|
|
pub fn try_pop(&mut self) -> Option<Value> {
|
|
self.values.pop()
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn peek(&self) -> &Value {
|
|
self.values.last().unwrap()
|
|
}
|
|
}
|
|
|
|
impl From<Vec<Value>> for Stack {
|
|
fn from(values: Vec<Value>) -> Stack {
|
|
Stack { values }
|
|
}
|
|
}
|
|
|
|
pub(crate) struct Context<'env> {
|
|
env: &'env Environment<'env>,
|
|
stack: Vec<Frame<'env>>,
|
|
outer_stack_depth: usize,
|
|
recursion_limit: usize,
|
|
}
|
|
|
|
impl fmt::Debug for Context<'_> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut vars = Vec::from_iter(self.known_variables(false));
|
|
vars.sort();
|
|
f.debug_map()
|
|
.entries(vars.into_iter().map(|key| {
|
|
let value = self.load(&key).unwrap_or_default();
|
|
(key, value)
|
|
}))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl<'env> Context<'env> {
|
|
/// Creates an empty context.
|
|
pub fn new(env: &'env Environment<'env>) -> Context<'env> {
|
|
Context {
|
|
env,
|
|
stack: Vec::with_capacity(40),
|
|
outer_stack_depth: 0,
|
|
recursion_limit: env.recursion_limit(),
|
|
}
|
|
}
|
|
|
|
/// Creates a context
|
|
pub fn new_with_frame(env: &'env Environment<'env>, frame: Frame<'env>) -> Context<'env> {
|
|
let mut rv = Context::new(env);
|
|
rv.stack.push(frame);
|
|
rv
|
|
}
|
|
|
|
/// The env
|
|
#[inline(always)]
|
|
pub fn env(&self) -> &'env Environment<'env> {
|
|
self.env
|
|
}
|
|
|
|
/// Stores a variable in the context.
|
|
pub fn store(&mut self, key: &'env str, value: Value) {
|
|
let top = self.stack.last_mut().unwrap();
|
|
#[cfg(feature = "macros")]
|
|
{
|
|
if let Some(ref closure) = top.closure {
|
|
closure.store(key, value.clone());
|
|
}
|
|
}
|
|
top.locals.insert(key, value);
|
|
}
|
|
|
|
/// Adds a value to a closure if missing.
|
|
///
|
|
/// All macros declare on a certain level reuse the same closure. This is done
|
|
/// to emulate the behavior of how scopes work in Jinja2 in Python. The
|
|
/// unfortunate downside is that this has to be done with a `Mutex`.
|
|
#[cfg(feature = "macros")]
|
|
pub fn enclose(&mut self, key: &str) {
|
|
self.stack
|
|
.last_mut()
|
|
.unwrap()
|
|
.closure
|
|
.as_mut()
|
|
.unwrap()
|
|
.clone()
|
|
.store_if_missing(key, || self.load(key).unwrap_or(Value::UNDEFINED));
|
|
}
|
|
|
|
/// Loads the closure and returns it.
|
|
#[cfg(feature = "macros")]
|
|
pub fn closure(&mut self) -> Option<&Arc<Closure>> {
|
|
self.stack.last_mut().unwrap().closure.as_ref()
|
|
}
|
|
|
|
/// Temporarily takes the closure.
|
|
///
|
|
/// This is done because includes are in the same scope as the module that
|
|
/// triggers the import, but we do not want to allow closures to be modified
|
|
/// from another file as this would be very confusing.
|
|
///
|
|
/// This means that if you override a variable referenced by a macro after
|
|
/// including in the parent template, it will not override the value seen by
|
|
/// the macro.
|
|
#[cfg(all(feature = "multi_template", feature = "macros"))]
|
|
pub fn take_closure(&mut self) -> Option<Arc<Closure>> {
|
|
self.stack.last_mut().unwrap().closure.take()
|
|
}
|
|
|
|
/// Puts the closure back.
|
|
#[cfg(feature = "macros")]
|
|
pub fn reset_closure(&mut self, closure: Option<Arc<Closure>>) {
|
|
self.stack.last_mut().unwrap().closure = closure;
|
|
}
|
|
|
|
/// Return the base context value
|
|
#[cfg(feature = "macros")]
|
|
pub fn clone_base(&self) -> Value {
|
|
self.stack
|
|
.first()
|
|
.map(|x| x.ctx.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Looks up a variable in the context.
|
|
pub fn load(&self, key: &str) -> Option<Value> {
|
|
for frame in self.stack.iter().rev() {
|
|
// look at locals first
|
|
if let Some(value) = frame.locals.get(key) {
|
|
return Some(value.clone());
|
|
}
|
|
|
|
// if we are a loop, check if we are looking up the special loop var.
|
|
if let Some(ref l) = frame.current_loop {
|
|
if l.with_loop_var && key == "loop" {
|
|
return Some(Value::from_dyn_object(l.object.clone()));
|
|
}
|
|
}
|
|
|
|
// perform a fast lookup. This one will not produce errors if the
|
|
// context is undefined or of the wrong type.
|
|
if let Some(rv) = frame.ctx.get_attr_fast(key) {
|
|
return Some(rv);
|
|
}
|
|
}
|
|
|
|
self.env.get_global(key)
|
|
}
|
|
|
|
/// Returns an iterable of all declared variables.
|
|
pub fn known_variables(&self, with_globals: bool) -> HashSet<Cow<'_, str>> {
|
|
let mut seen = HashSet::<Cow<'_, str>>::new();
|
|
for frame in self.stack.iter().rev() {
|
|
for key in frame.locals.keys() {
|
|
if !seen.contains(&Cow::Borrowed(*key)) {
|
|
seen.insert(Cow::Borrowed(key));
|
|
}
|
|
}
|
|
|
|
if let Some(ref l) = frame.current_loop {
|
|
if l.with_loop_var && !seen.contains("loop") {
|
|
seen.insert(Cow::Borrowed("loop"));
|
|
}
|
|
}
|
|
|
|
if let Ok(iter) = frame.ctx.try_iter() {
|
|
for key in iter {
|
|
if let Some(str_key) = key.as_str() {
|
|
if !seen.contains(&Cow::Borrowed(str_key))
|
|
&& frame.ctx.get_item(&key).is_ok()
|
|
{
|
|
seen.insert(Cow::Owned(str_key.to_owned()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if with_globals {
|
|
seen.extend(self.env.globals().map(|x| Cow::Borrowed(x.0)));
|
|
}
|
|
seen
|
|
}
|
|
|
|
/// Pushes a new layer.
|
|
pub fn push_frame(&mut self, layer: Frame<'env>) -> Result<(), Error> {
|
|
ok!(self.check_depth());
|
|
self.stack.push(layer);
|
|
Ok(())
|
|
}
|
|
|
|
/// Pops the topmost layer.
|
|
#[track_caller]
|
|
pub fn pop_frame(&mut self) -> Frame<'env> {
|
|
self.stack.pop().unwrap()
|
|
}
|
|
|
|
/// Returns the root locals (exports)
|
|
#[track_caller]
|
|
pub fn exports(&self) -> &Locals<'env> {
|
|
&self.stack.first().unwrap().locals
|
|
}
|
|
|
|
/// Returns the current locals mutably.
|
|
#[track_caller]
|
|
#[cfg(feature = "multi_template")]
|
|
pub fn current_locals_mut(&mut self) -> &mut Locals<'env> {
|
|
&mut self.stack.last_mut().unwrap().locals
|
|
}
|
|
|
|
/// Returns the current innermost loop state.
|
|
pub fn current_loop(&mut self) -> Option<&mut LoopState> {
|
|
self.stack
|
|
.iter_mut()
|
|
.rev()
|
|
.filter_map(|x| x.current_loop.as_mut())
|
|
.next()
|
|
}
|
|
|
|
/// The real depth of the context.
|
|
pub fn depth(&self) -> usize {
|
|
self.outer_stack_depth + self.stack.len()
|
|
}
|
|
|
|
/// Increase the stack depth.
|
|
#[allow(unused)]
|
|
pub fn incr_depth(&mut self, delta: usize) -> Result<(), Error> {
|
|
self.outer_stack_depth += delta;
|
|
ok!(self.check_depth());
|
|
Ok(())
|
|
}
|
|
|
|
/// Decrease the stack depth.
|
|
#[allow(unused)]
|
|
pub fn decr_depth(&mut self, delta: usize) {
|
|
self.outer_stack_depth -= delta;
|
|
}
|
|
|
|
fn check_depth(&self) -> Result<(), Error> {
|
|
if self.depth() > self.recursion_limit {
|
|
return Err(Error::new(
|
|
ErrorKind::InvalidOperation,
|
|
"recursion limit exceeded",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|