use std::collections::BTreeSet;
use super::types::{
AssignOp, Assignment, Block, BlockStmt, ChoiceSet, CondKind, Conditional, Content, ContentPart,
ElseBranch, Expr, ForStmt, HirFile, IfStmt, Sequence, Stmt, StringPart, TempDecl, WhileStmt,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ContinuationSite {
pub def_path: String,
pub site_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AwaitFrameShape {
pub site: ContinuationSite,
pub crossing_locals: Vec<String>,
}
#[must_use]
pub fn compute_frame_shapes(hir: &HirFile) -> Vec<AwaitFrameShape> {
let mut out = Vec::new();
analyze_def(&hir.root_content, "", &mut out);
for knot in &hir.knots {
analyze_def(&knot.body, &knot.name.text, &mut out);
for stitch in &knot.stitches {
analyze_def(
&stitch.body,
&format!("{}.{}", knot.name.text, stitch.name.text),
&mut out,
);
}
}
out
}
fn analyze_def(body: &Block, def_path: &str, out: &mut Vec<AwaitFrameShape>) {
let mut a = Analyzer::default();
a.walk_block(body);
if a.awaits.is_empty() {
return;
}
let decl_order: Vec<String> = a.decls.iter().map(|(name, _)| name.clone()).collect();
let local_names: BTreeSet<&String> = a.decls.iter().map(|(name, _)| name).collect();
for site in &a.awaits {
let mut crossing_set: BTreeSet<&String> = BTreeSet::new();
for name in &local_names {
let declared_before = a.decls.iter().any(|(n, pos)| n == *name && *pos < site.pos);
if !declared_before {
continue;
}
let read_after = a.reads.iter().any(|(pos, n)| n == *name && *pos > site.pos);
let read_in_loop = site.loop_ids.iter().any(|id| {
let (start, end) = a.loops[*id];
a.reads
.iter()
.any(|(pos, n)| n == *name && *pos >= start && *pos < end)
});
let in_condition = site.cond_reads.iter().any(|n| n == *name);
if read_after || read_in_loop || in_condition {
crossing_set.insert(*name);
}
}
let crossing_locals: Vec<String> = decl_order
.iter()
.filter(|name| crossing_set.contains(name))
.cloned()
.collect::<Vec<_>>()
.into_iter()
.fold(Vec::new(), |mut acc, name| {
if !acc.contains(&name) {
acc.push(name);
}
acc
});
out.push(AwaitFrameShape {
site: ContinuationSite {
def_path: def_path.to_owned(),
site_index: site.site_index,
},
crossing_locals,
});
}
}
struct AwaitRec {
pos: usize,
site_index: usize,
cond_reads: Vec<String>,
loop_ids: Vec<usize>,
}
#[derive(Default)]
struct Analyzer {
pos: usize,
reads: Vec<(usize, String)>,
decls: Vec<(String, usize)>,
awaits: Vec<AwaitRec>,
loops: Vec<(usize, usize)>,
loop_stack: Vec<usize>,
site_counter: usize,
}
impl Analyzer {
fn next_pos(&mut self) -> usize {
let p = self.pos;
self.pos += 1;
p
}
fn record_reads(&mut self, expr: &Expr, pos: usize) {
collect_reads(expr, &mut |name| self.reads.push((pos, name.clone())));
}
fn walk_block(&mut self, block: &Block) {
for stmt in &block.stmts {
self.walk_stmt(stmt);
}
}
fn walk_stmt(&mut self, stmt: &Stmt) {
let pos = self.next_pos();
match stmt {
Stmt::Content(c) => self.walk_content(c, pos),
Stmt::Divert(d) => {
for e in &d.target.args {
self.record_reads(e, pos);
}
}
Stmt::TunnelCall(t) => {
for target in &t.targets {
for e in &target.args {
self.record_reads(e, pos);
}
}
}
Stmt::ThreadStart(t) => {
for e in &t.target.args {
self.record_reads(e, pos);
}
}
Stmt::TempDecl(decl) => self.walk_temp_decl(decl, pos),
Stmt::Assignment(a) => self.walk_assignment(a, pos),
Stmt::Return(r) => {
if let Some(e) = &r.value {
self.record_reads(e, pos);
}
for e in &r.onwards_args {
self.record_reads(e, pos);
}
}
Stmt::ChoiceSet(cs) => self.walk_choice_set(cs),
Stmt::LabeledBlock(b) => self.walk_block(b),
Stmt::Conditional(c) => self.walk_conditional(c),
Stmt::Sequence(s) => self.walk_sequence(s),
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => self.record_reads(e, pos),
Stmt::EndOfLine | Stmt::EndElementRun => {}
Stmt::LogicBlock(lb) => {
for bs in &lb.stmts {
self.walk_block_stmt(bs);
}
}
Stmt::Await(a) => {
let cond_reads = a
.condition
.as_ref()
.map(collect_read_names)
.unwrap_or_default();
if let Some(e) = &a.condition {
self.record_reads(e, pos);
}
self.record_await(pos, cond_reads);
}
}
}
fn walk_block_stmt(&mut self, bs: &BlockStmt) {
let pos = self.next_pos();
match bs {
BlockStmt::TempDecl(decl) => self.walk_temp_decl(decl, pos),
BlockStmt::Assignment(a) => self.walk_assignment(a, pos),
BlockStmt::Return(r) => {
if let Some(e) = &r.value {
self.record_reads(e, pos);
}
for e in &r.onwards_args {
self.record_reads(e, pos);
}
}
BlockStmt::If(i) => self.walk_if_stmt(i, pos),
BlockStmt::While(w) => self.walk_while_stmt(w, pos),
BlockStmt::For(f) => self.walk_for_stmt(f, pos),
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
BlockStmt::ExprStmt(e) => self.record_reads(e, pos),
BlockStmt::Await(a) => {
let cond_reads = a
.condition
.as_ref()
.map(collect_read_names)
.unwrap_or_default();
if let Some(e) = &a.condition {
self.record_reads(e, pos);
}
self.record_await(pos, cond_reads);
}
}
}
fn walk_temp_decl(&mut self, decl: &TempDecl, pos: usize) {
self.decls.push((decl.name.text.clone(), pos));
if let Some(e) = &decl.value {
self.record_reads(e, pos);
}
}
fn walk_assignment(&mut self, a: &Assignment, pos: usize) {
let is_plain_binding =
a.op == AssignOp::Set && matches!(&a.target, Expr::Path(p) if p.segments.len() == 1);
if !is_plain_binding {
self.record_reads(&a.target, pos);
}
self.record_reads(&a.value, pos);
}
fn walk_if_stmt(&mut self, i: &IfStmt, pos: usize) {
self.record_reads(&i.condition, pos);
if let Some(binding) = &i.binding {
self.decls.push((binding.text.clone(), pos));
}
for s in &i.body {
self.walk_block_stmt(s);
}
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => {
let else_pos = self.next_pos();
self.walk_if_stmt(inner, else_pos);
}
Some(ElseBranch::Else(stmts)) => {
for s in stmts {
self.walk_block_stmt(s);
}
}
None => {}
}
}
fn walk_while_stmt(&mut self, w: &WhileStmt, pos: usize) {
let loop_id = self.loops.len();
self.loops.push((pos, pos)); self.loop_stack.push(loop_id);
if w.is_await {
let cond_reads = collect_read_names(&w.condition);
self.record_await(pos, cond_reads);
}
self.record_reads(&w.condition, pos);
if let Some(binding) = &w.binding {
self.decls.push((binding.text.clone(), pos));
}
for s in &w.body {
self.walk_block_stmt(s);
}
self.loop_stack.pop();
self.loops[loop_id].1 = self.pos;
}
fn walk_for_stmt(&mut self, f: &ForStmt, pos: usize) {
self.decls.push((f.var_name.text.clone(), pos));
if let Some(val_name) = &f.val_name {
self.decls.push((val_name.text.clone(), pos));
}
self.record_reads(&f.iterable, pos);
let loop_id = self.loops.len();
self.loops.push((pos, pos));
self.loop_stack.push(loop_id);
for s in &f.body {
self.walk_block_stmt(s);
}
self.loop_stack.pop();
self.loops[loop_id].1 = self.pos;
}
fn record_await(&mut self, pos: usize, cond_reads: Vec<String>) {
let site_index = self.site_counter;
self.site_counter += 1;
self.awaits.push(AwaitRec {
pos,
site_index,
cond_reads,
loop_ids: self.loop_stack.clone(),
});
}
fn walk_content(&mut self, content: &Content, pos: usize) {
for part in &content.parts {
self.walk_content_part(part, pos);
}
}
fn walk_content_part(&mut self, part: &ContentPart, pos: usize) {
match part {
ContentPart::Interpolation(e) => self.record_reads(e, pos),
ContentPart::InlineConditional(c) => self.walk_conditional_at(c, pos),
ContentPart::InlineSequence(s) => self.walk_sequence(s),
ContentPart::Span(span) => {
for child in &span.children {
self.walk_content_part(child, pos);
}
}
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn walk_choice_set(&mut self, cs: &ChoiceSet) {
for choice in &cs.choices {
let pos = self.next_pos();
if let Some(e) = &choice.condition {
self.record_reads(e, pos);
}
for c in [
&choice.start_content,
&choice.bracket_content,
&choice.inner_content,
]
.into_iter()
.flatten()
{
self.walk_content(c, pos);
}
self.walk_block(&choice.body);
}
self.walk_block(&cs.continuation);
}
fn walk_conditional(&mut self, cond: &Conditional) {
let pos = self.next_pos();
self.walk_conditional_at(cond, pos);
}
fn walk_conditional_at(&mut self, cond: &Conditional, pos: usize) {
if let CondKind::Switch(e) = &cond.kind {
self.record_reads(e, pos);
}
for branch in &cond.branches {
if let Some(e) = &branch.condition {
self.record_reads(e, pos);
}
if let Some(binding) = &branch.binding {
self.decls.push((binding.text.clone(), pos));
}
self.walk_block(&branch.body);
}
}
fn walk_sequence(&mut self, seq: &Sequence) {
for branch in &seq.branches {
self.walk_block(&branch.body);
}
}
}
fn collect_reads(expr: &Expr, sink: &mut impl FnMut(&String)) {
match expr {
Expr::Path(p) => {
if p.segments.len() == 1 {
sink(&p.segments[0].text);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => collect_reads(inner, sink),
Expr::Infix(ie) => {
collect_reads(&ie.lhs, sink);
collect_reads(&ie.rhs, sink);
}
Expr::Call(_path, args) => {
for arg in args {
collect_reads(arg, sink);
}
}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(e) = part {
collect_reads(e, sink);
}
}
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
collect_reads(e, sink);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
collect_reads(k, sink);
collect_reads(v, sink);
}
}
Expr::Index(idx) => {
collect_reads(&idx.base, sink);
collect_reads(&idx.index, sink);
}
Expr::StructLiteral(sl) => {
for (_name, v) in &sl.fields {
collect_reads(v, sink);
}
}
Expr::FieldAccess(fa) => collect_reads(&fa.base, sink),
Expr::FnLiteral(fl) => {
for arg in &fl.args {
collect_reads(arg, sink);
}
}
Expr::RefArg(ra) => collect_reads(&ra.operand, sink),
Expr::Lambda(l) => match &l.body {
crate::LambdaBody::Expr(e) => collect_reads(e, sink),
crate::LambdaBody::Block { stmts, tail } => {
for s in stmts {
collect_stmt_reads(s, sink);
}
if let Some(t) = tail {
collect_reads(t, sink);
}
}
},
Expr::Range(r) => {
collect_reads(&r.start, sink);
collect_reads(&r.end, sink);
}
Expr::Fragment(stmts) => collect_fragment_reads(stmts, sink),
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::Null
| Expr::DivertTarget(_)
| Expr::ListLiteral(_) => {}
}
}
fn collect_fragment_reads(stmts: &[Stmt], sink: &mut impl FnMut(&String)) {
for e in super::types::fragment_stmt_exprs(stmts) {
collect_reads(e, sink);
}
}
fn collect_stmt_reads(stmt: &super::types::BlockStmt, sink: &mut impl FnMut(&String)) {
use super::types::BlockStmt as B;
match stmt {
B::TempDecl(t) => {
if let Some(e) = &t.value {
collect_reads(e, sink);
}
}
B::Assignment(a) => {
collect_reads(&a.target, sink);
collect_reads(&a.value, sink);
}
B::Return(r) => {
if let Some(e) = &r.value {
collect_reads(e, sink);
}
for a in &r.onwards_args {
collect_reads(a, sink);
}
}
B::If(i) => {
collect_reads(&i.condition, sink);
for s in &i.body {
collect_stmt_reads(s, sink);
}
match &i.else_branch {
Some(super::types::ElseBranch::ElseIf(nested)) => {
collect_stmt_reads(&B::If((**nested).clone()), sink);
}
Some(super::types::ElseBranch::Else(body)) => {
for s in body {
collect_stmt_reads(s, sink);
}
}
None => {}
}
}
B::While(w) => {
collect_reads(&w.condition, sink);
for s in &w.body {
collect_stmt_reads(s, sink);
}
}
B::For(f) => {
collect_reads(&f.iterable, sink);
for s in &f.body {
collect_stmt_reads(s, sink);
}
}
B::ExprStmt(e) => collect_reads(e, sink),
B::Await(a) => {
if let Some(e) = &a.condition {
collect_reads(e, sink);
}
}
B::Break(_) | B::Continue(_) => {}
}
}
fn collect_read_names(expr: &Expr) -> Vec<String> {
let mut names = Vec::new();
collect_reads(expr, &mut |name| names.push(name.clone()));
names
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileId;
use brink_syntax::parse;
fn shapes(src: &str) -> Vec<AwaitFrameShape> {
let parsed = parse(src);
let tree = parsed.tree();
let (hir, _, _) = crate::hir::lower::lower(FileId(0), &tree);
compute_frame_shapes(&hir)
}
#[test]
fn no_await_no_shapes() {
assert!(shapes("Hello.\n=== knot ===\nHi {name}\n-> END\n").is_empty());
}
#[test]
fn local_read_after_park_crosses() {
let s =
shapes("=== patrol ===\n~ temp x = 5\n~ await x > 3\nGuard has {x} left.\n-> END\n");
assert_eq!(s.len(), 1, "one await site");
assert_eq!(s[0].site.def_path, "patrol");
assert_eq!(s[0].site.site_index, 0);
assert_eq!(s[0].crossing_locals, vec!["x".to_owned()]);
}
#[test]
fn local_dead_after_park_does_not_cross() {
let s = shapes(
"=== patrol ===\n~ temp y = 1\n~ temp x = y + 1\n~ await x > 0\nDone.\n-> END\n",
);
assert_eq!(s.len(), 1);
assert_eq!(s[0].crossing_locals, vec!["x".to_owned()]);
}
#[test]
fn condition_reads_cross_even_without_later_use() {
let s = shapes("=== gate ===\n~ temp g = 10\n~ await g > 100\n-> END\n");
assert_eq!(s.len(), 1);
assert_eq!(s[0].crossing_locals, vec!["g".to_owned()]);
}
#[test]
fn loop_iterator_crosses_park_inside_loop() {
let s = shapes(
"=== sweep ===\n~ {\n for room in rooms {\n await ready\n visit(room)\n }\n}\n-> END\n",
);
assert_eq!(s.len(), 1, "one await inside the loop");
assert!(
s[0].crossing_locals.contains(&"room".to_owned()),
"the for-iterator crosses: {:?}",
s[0].crossing_locals
);
}
#[test]
fn while_await_records_a_site() {
let s = shapes(
"=== ambient ===\n~ {\n temp n = 0\n while await alarm {\n n = n + 1\n }\n}\n-> END\n",
);
assert_eq!(s.len(), 1);
assert_eq!(s[0].site.def_path, "ambient");
assert!(
s[0].crossing_locals.contains(&"n".to_owned()),
"loop-carried local crosses: {:?}",
s[0].crossing_locals
);
}
#[test]
fn multiple_sites_numbered_in_order() {
let s = shapes(
"=== twostep ===\n~ temp a = 1\n~ await a > 0\n~ temp b = 2\n~ await b > 0\nEnd {a} {b}\n-> END\n",
);
assert_eq!(s.len(), 2);
assert_eq!(s[0].site.site_index, 0);
assert_eq!(s[1].site.site_index, 1);
assert!(!s[0].crossing_locals.contains(&"b".to_owned()));
assert!(s[1].crossing_locals.contains(&"a".to_owned()));
assert!(s[1].crossing_locals.contains(&"b".to_owned()));
}
#[test]
fn stitch_def_path_is_qualified() {
let s = shapes("=== knot ===\n= inner\n~ temp x = 1\n~ await x > 0\nGot {x}\n-> END\n");
assert_eq!(s.len(), 1);
assert_eq!(s[0].site.def_path, "knot.inner");
}
#[test]
fn index_target_base_and_index_cross_after_park() {
let s = shapes(
"=== task ===\n~ temp arr = #[1, 2, 3]\n~ temp i = 0\n~ await ready\n~ arr[i] = 99\n-> END\n",
);
assert_eq!(s.len(), 1);
assert!(
s[0].crossing_locals.contains(&"arr".to_owned()),
"index base must cross: {:?}",
s[0].crossing_locals
);
assert!(
s[0].crossing_locals.contains(&"i".to_owned()),
"index expression must cross: {:?}",
s[0].crossing_locals
);
}
#[test]
fn field_access_target_index_base_crosses_after_park() {
let s = shapes(
"=== task ===\n~ temp arr = #[1, 2, 3]\n~ temp i = 0\n~ await ready\n~ arr[i].field = 99\n-> END\n",
);
assert_eq!(s.len(), 1);
assert!(
s[0].crossing_locals.contains(&"arr".to_owned()),
"field-access's index base must cross: {:?}",
s[0].crossing_locals
);
assert!(
s[0].crossing_locals.contains(&"i".to_owned()),
"field-access's index expression must cross: {:?}",
s[0].crossing_locals
);
}
}