#[cfg(feature = "no_std")]
use std::prelude::v1::*;
use crate::ast::{ASTFlags, ASTNode};
#[cfg(not(feature = "no_module"))]
use crate::module_resolvers::StaticModuleResolver;
use crate::{ast::Expr, ast::Stmt, tokenizer::Token, Dynamic, ImmutableString, Module, Shared};
use crate::grain::bytecode::{
site_to_position, sites, AssignOp, Chain, Chunk, Code, Pools, Positions, Strings, Switch,
TableError,
};
use crate::grain::format::{Caps, Sidecar};
pub(crate) type SharedModule = Shared<Module>;
pub type SharedProgram = Shared<Program<'static>>;
#[derive(Debug, Clone)]
pub struct Function {
pub name: u32,
pub params: Vec<u32>,
pub this_type: Option<u32>,
pub chunk: Chunk,
}
pub struct Program<'a> {
caps: Caps,
code: Code<'a>,
main: Chunk,
functions: Vec<Function>,
max_stack: u16,
has_typed_methods: bool,
positions: Positions,
debug_id: u128,
residuals: Vec<Expr>,
consts: Vec<Dynamic>,
names: Strings<'a>,
tokens: Vec<Token>,
assign_ops: Vec<AssignOp>,
chains: Vec<Chain>,
switches: Vec<Switch>,
lib: Option<SharedModule>,
#[cfg(not(feature = "no_module"))]
resolver: Option<Shared<StaticModuleResolver>>,
source: Option<ImmutableString>,
}
impl core::fmt::Debug for Program<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Program")
.field("source", &self.source)
.field("bytes", &self.code.len())
.field("max_stack", &self.main.max_stack())
.field("consts", &self.consts.len())
.field("names", &self.names.len())
.field("residuals", &self.residuals.len())
.field("compiled_fns", &self.functions.len())
.field(
"walked_fns",
&self.lib.as_ref().map_or(0, |lib| lib.count().1),
)
.field("positions", &!self.positions.is_stripped())
.finish()
}
}
fn unsupported_kind(node: &ASTNode) -> Option<&'static str> {
Some(match node {
ASTNode::Stmt(stmt) => match stmt {
Stmt::Switch(..) => "switch",
Stmt::For(..) => "for",
Stmt::TryCatch(..) => "try/catch",
#[cfg(not(feature = "no_module"))]
Stmt::Import(..) => "import",
#[cfg(not(feature = "no_module"))]
Stmt::Export(..) => "export",
#[cfg(not(feature = "no_closure"))]
Stmt::Share(..) => "a closure capture",
Stmt::Return(_, flags, ..) if flags.contains(ASTFlags::BREAK) => "throw",
_ => return None,
},
ASTNode::Expr(expr) => match expr {
Expr::InterpolatedString(..) => "string interpolation",
#[cfg(not(feature = "no_custom_syntax"))]
Expr::Custom(..) => "custom syntax",
Expr::Map(..) => "a non-constant map literal",
_ => return None,
},
})
}
fn node_position(node: &ASTNode) -> rhai::Position {
match node {
ASTNode::Stmt(stmt) => stmt.position(),
ASTNode::Expr(expr) => expr.start_position(),
}
}
pub(crate) struct Parts<'a> {
pub positions: Positions,
pub debug_id: Option<u128>,
pub residuals: Vec<Expr>,
pub consts: Vec<Dynamic>,
pub names: Strings<'a>,
pub tokens: Vec<Token>,
pub assign_ops: Vec<AssignOp>,
pub chains: Vec<Chain>,
pub switches: Vec<Switch>,
pub lib: Option<SharedModule>,
#[cfg(not(feature = "no_module"))]
pub resolver: Option<Shared<StaticModuleResolver>>,
pub source: Option<ImmutableString>,
}
impl<'a> Program<'a> {
pub(crate) fn new(
caps: Caps,
code: Code<'a>,
main: Chunk,
functions: Vec<Function>,
parts: Parts<'a>,
) -> Self {
let has_typed_methods = functions.iter().any(|f| f.this_type.is_some());
let debug_id = parts.debug_id.unwrap_or_else(|| {
crate::grain::format::debug_id(
&parts.positions.to_table(),
&sites::encode(&parts.chains),
)
});
let mut program = Self {
caps,
code,
main,
functions,
max_stack: 0,
has_typed_methods,
positions: parts.positions,
debug_id,
residuals: parts.residuals,
consts: parts.consts,
names: parts.names,
tokens: parts.tokens,
assign_ops: parts.assign_ops,
chains: parts.chains,
switches: parts.switches,
lib: parts.lib,
#[cfg(not(feature = "no_module"))]
resolver: parts.resolver,
source: parts.source,
};
program.recompute_max_stack();
program
}
#[must_use]
pub fn into_owned(self) -> Program<'static> {
Program {
code: Code::Owned(self.code.into_owned()),
caps: self.caps,
main: self.main,
functions: self.functions,
max_stack: self.max_stack,
has_typed_methods: self.has_typed_methods,
positions: self.positions,
debug_id: self.debug_id,
residuals: self.residuals,
consts: self.consts,
names: self.names.into_owned(),
tokens: self.tokens,
assign_ops: self.assign_ops,
chains: self.chains,
switches: self.switches,
lib: self.lib,
#[cfg(not(feature = "no_module"))]
resolver: self.resolver,
source: self.source,
}
}
#[must_use]
pub fn into_shared(self) -> SharedProgram {
Shared::new(self.into_owned())
}
pub fn verify(&self) -> Result<Vec<u16>, crate::grain::bytecode::VerifyError> {
crate::grain::bytecode::verify(
self.caps,
&self.code,
&self.functions(),
&self.chunks(),
&self.pools(),
)
}
fn chunks(&self) -> Vec<Chunk> {
core::iter::once(self.main)
.chain(self.functions.iter().map(|f| f.chunk))
.collect()
}
pub(crate) fn pools(&self) -> Pools<'_> {
Pools {
consts: self.consts.len(),
names: self.names.len(),
tokens: self.tokens.len(),
assign_ops: self.assign_ops.len(),
residuals: self.residuals.len(),
chains: &self.chains,
switches: &self.switches,
}
}
pub(crate) fn tighten_stack(&mut self) {
let Ok(high_water) = self.verify() else {
return;
};
let mut measured = high_water.into_iter();
if let Some(main) = measured.next() {
self.main.set_max_stack(main);
}
for (function, high_water) in self.functions.iter_mut().zip(measured) {
function.chunk.set_max_stack(high_water);
}
self.recompute_max_stack();
}
#[must_use]
pub fn code(&self) -> &[u8] {
&self.code
}
#[must_use]
pub fn caps(&self) -> Caps {
self.caps
}
#[must_use]
pub fn functions(&self) -> &[Function] {
&self.functions
}
pub(crate) fn function(&self, name: u32, argc: usize) -> Option<&Function> {
self.functions
.iter()
.find(|f| f.name == name && f.params.len() == argc && f.this_type.is_none())
}
pub(crate) fn method(&self, name: u32, argc: usize, typed: &str) -> Option<&Function> {
let matching = |f: &&Function| f.name == name && f.params.len() == argc;
if self.has_typed_methods {
let found = self
.functions
.iter()
.find(|f| matching(f) && f.this_type.and_then(|t| self.name(t)) == Some(typed));
if found.is_some() {
return found;
}
}
self.functions
.iter()
.find(|f| matching(f) && f.this_type.is_none())
}
pub(crate) fn function_named(&self, name: &str, argc: usize) -> Option<&Function> {
self.functions.iter().find(|f| {
f.params.len() == argc && f.this_type.is_none() && self.name(f.name) == Some(name)
})
}
#[must_use]
pub fn makes_fn_pointers(&self) -> bool {
self.caps().contains(Caps::FN_PTR)
}
#[must_use]
pub fn max_stack(&self) -> u16 {
self.max_stack
}
fn recompute_max_stack(&mut self) {
self.max_stack = self
.functions
.iter()
.map(|f| f.chunk.max_stack())
.chain(core::iter::once(self.main.max_stack()))
.max()
.unwrap_or(0);
}
pub(crate) fn constant(&self, index: u32) -> Option<&Dynamic> {
self.consts.get(index as usize)
}
pub(crate) fn name(&self, index: u32) -> Option<&str> {
self.names.get(index)
}
pub(crate) fn token(&self, index: u32) -> Option<&Token> {
self.tokens.get(index as usize)
}
pub(crate) fn assign_op(&self, index: u32) -> Option<&AssignOp> {
self.assign_ops.get(index as usize)
}
pub(crate) fn chain(&self, index: u32) -> Option<&Chain> {
self.chains.get(index as usize)
}
pub(crate) fn chains(&self) -> &[Chain] {
&self.chains
}
pub(crate) fn switch(&self, index: u32) -> Option<&Switch> {
self.switches.get(index as usize)
}
#[must_use]
pub fn switches(&self) -> &[Switch] {
&self.switches
}
#[must_use]
pub fn position(&self, pc: usize) -> rhai::Position {
self.positions.get(pc)
}
#[must_use]
pub fn positions(&self) -> &Positions {
&self.positions
}
#[must_use]
pub fn debug_id(&self) -> u128 {
self.debug_id
}
pub fn strip_positions(&mut self) -> Sidecar {
let sidecar = self.sidecar();
self.positions = Positions::Stripped;
for chain in &mut self.chains {
for pos in chain.positions_mut() {
*pos = rhai::Position::NONE;
}
}
sidecar
}
pub fn attach_positions(&mut self, sidecar: &Sidecar) -> Result<(), TableError> {
if sidecar.debug_id != self.debug_id {
return Err(TableError::WrongProgram {
expected: sidecar.debug_id,
found: self.debug_id,
});
}
let positions = Positions::from_table(&sidecar.positions, &self.code)?;
let sites = sites::decode(&sidecar.chains).map_err(TableError::ChainStream)?;
let slots = self.chains.iter().map(Chain::position_slots).sum::<u32>() as usize;
if sites.len() != slots {
return Err(TableError::ChainCount {
sites: sites.len(),
slots,
});
}
let mut sites = sites.into_iter();
for chain in &mut self.chains {
for pos in chain.positions_mut() {
*pos = sites
.next()
.flatten()
.map_or(rhai::Position::NONE, site_to_position);
}
}
self.positions = positions;
Ok(())
}
pub(crate) fn consts(&self) -> &[Dynamic] {
&self.consts
}
pub(crate) fn names(&self) -> &Strings<'a> {
&self.names
}
pub(crate) fn tokens(&self) -> &[Token] {
&self.tokens
}
pub(crate) fn assign_ops(&self) -> &[AssignOp] {
&self.assign_ops
}
#[must_use]
pub fn main(&self) -> &Chunk {
&self.main
}
#[must_use]
pub fn residual_count(&self) -> usize {
self.residuals.len()
}
#[must_use]
pub fn residual_nodes(&self) -> usize {
let mut nodes = 0;
let path = &mut Vec::new();
for residual in &self.residuals {
residual.walk(path, &mut |_| {
nodes += 1;
true
});
}
nodes
}
pub(crate) fn residual(&self, index: u32) -> Option<&Expr> {
self.residuals.get(index as usize)
}
#[must_use]
pub fn first_unsupported(&self) -> Option<(&'static str, rhai::Position)> {
let path = &mut Vec::new();
let mut found: Option<(&'static str, rhai::Position)> = None;
for residual in &self.residuals {
residual.walk(path, &mut |path| {
if found.is_some() {
return false;
}
if let Some(name) = path.last().and_then(unsupported_kind) {
found = Some((name, node_position(path.last().expect("just matched"))));
return false;
}
true
});
if found.is_some() {
break;
}
}
found.or_else(|| {
self.residuals
.first()
.map(|expr| ("an unlowered expression", expr.start_position()))
})
}
pub(crate) fn lib(&self) -> Option<&SharedModule> {
self.lib.as_ref()
}
#[cfg(not(feature = "no_module"))]
pub(crate) fn resolver(&self) -> Option<&Shared<StaticModuleResolver>> {
self.resolver.as_ref()
}
pub(crate) fn source(&self) -> Option<&ImmutableString> {
self.source.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grain::bytecode::{assemble, Op, Positions, Strings};
fn program_of(functions: &[(u32, Option<u32>, usize)]) -> Program<'static> {
let (code, _) = assemble(&[Op::Unit, Op::Return]).expect("must assemble");
let whole = Chunk::new(0, code.len() as u32, 8);
let functions = functions
.iter()
.map(|&(name, this_type, argc)| Function {
name,
params: vec![0; argc],
this_type,
chunk: whole,
})
.collect();
Program::new(
Caps::FUNCTION,
code.into(),
whole,
functions,
Parts {
positions: Positions::default(),
debug_id: None,
residuals: Vec::new(),
consts: Vec::new(),
names: Strings::new(["f", "i64", "string"]),
tokens: Vec::new(),
assign_ops: Vec::new(),
chains: Vec::new(),
switches: Vec::new(),
lib: None,
#[cfg(not(feature = "no_module"))]
resolver: None,
source: None,
},
)
}
#[test]
fn a_typed_method_wins_over_an_untyped_one_of_the_same_arity() {
let program = program_of(&[(0, Some(1), 0), (0, None, 0)]);
assert_eq!(program.method(0, 0, "i64").unwrap().this_type, Some(1));
assert_eq!(program.method(0, 0, "string").unwrap().this_type, None);
}
#[test]
fn a_typed_method_is_unreachable_in_call_style() {
let program = program_of(&[(0, Some(1), 0)]);
assert!(program.function(0, 0).is_none());
assert!(program.function_named("f", 0).is_none());
assert!(program.method(0, 0, "i64").is_some());
}
#[test]
fn arity_is_matched_before_the_receiver_type() {
let program = program_of(&[(0, Some(1), 1), (0, None, 0)]);
assert_eq!(program.method(0, 0, "i64").unwrap().this_type, None);
assert_eq!(program.method(0, 1, "i64").unwrap().this_type, Some(1));
}
#[test]
#[cfg(not(feature = "no_function"))]
fn a_receiver_type_survives_the_round_trip() {
let program = program_of(&[(0, Some(1), 0), (0, None, 0)]);
let bytes = program.write().expect("must be writable");
let reloaded = Program::read(&bytes).expect("must load");
let typed: Vec<_> = reloaded.functions().iter().map(|f| f.this_type).collect();
assert_eq!(typed, vec![Some(1), None]);
assert_eq!(reloaded.method(0, 0, "i64").unwrap().this_type, Some(1));
}
}