use rowan::TextRange;
use crate::hir::{
Block, BlockStmt, Choice, ChoiceSet, CondKind, Conditional, Content, ContentPart, DivertPath,
DivertTarget, ElseBranch, ForStmt, HirFile, IfStmt, Knot, LambdaBody, LambdaExpr, LogicBlock,
Param, Path, Return, ReturnKind, Sequence, Stmt, StringPart, Tag, WhileStmt,
};
use crate::host_manifest::DocBlock;
use crate::{Expr, ParamInfo, Scope, SymbolKind, VisibilityMark};
use super::{DeclaredSymbol, LocalSymbol, RefKind, SymbolManifest, UnresolvedRef};
#[must_use]
pub fn project_manifest(hir: &HirFile) -> SymbolManifest {
let mut p = Projector::default();
p.walk_block(&hir.root_content, None, None);
for v in &hir.variables {
p.declare(
SymbolKind::Variable,
v.name.text.clone(),
v.name.range,
Vec::new(),
None,
v.visibility,
v.was.clone(),
v.doc.clone(),
);
if let Some(ann) = &v.annotation {
p.walk_type_annotation(ann, None, None);
}
p.walk_expr(&v.value, None, None);
}
for c in &hir.constants {
p.declare(
SymbolKind::Constant,
c.name.text.clone(),
c.name.range,
Vec::new(),
None,
c.visibility,
c.was.clone(),
c.doc.clone(),
);
if let Some(ann) = &c.annotation {
p.walk_type_annotation(ann, None, None);
}
p.walk_expr(&c.value, None, None);
}
for l in &hir.lists {
p.declare(
SymbolKind::List,
l.name.text.clone(),
l.name.range,
Vec::new(),
None,
l.visibility,
l.was.clone(),
l.doc.clone(),
);
for m in &l.members {
let qualified = format!("{}.{}", l.name.text, m.name.text);
p.manifest.list_items.push(DeclaredSymbol {
name: qualified,
range: m.name.range,
params: Vec::new(),
detail: None,
visibility: None,
was: None,
});
}
}
for s in &hir.structs {
p.declare(
SymbolKind::Struct,
s.name.text.clone(),
s.name.range,
Vec::new(),
None,
s.visibility,
None,
s.doc.clone(),
);
for f in &s.fields {
p.walk_type_annotation(&f.ty, None, None);
}
}
for e in &hir.externals {
p.declare(
SymbolKind::External,
e.name.text.clone(),
e.name.range,
e.params.clone(),
None,
e.visibility,
e.was.clone(),
e.doc.clone(),
);
}
for knot in &hir.knots {
p.project_knot(knot);
}
p.manifest
}
#[derive(Default)]
struct Projector {
manifest: SymbolManifest,
}
impl Projector {
fn scope_of(knot: Option<&str>, stitch: Option<&str>) -> Scope {
Scope {
knot: knot.map(str::to_string),
stitch: stitch.map(str::to_string),
}
}
fn qualify_label(knot: Option<&str>, stitch: Option<&str>, label: &str) -> String {
match (knot, stitch) {
(Some(k), Some(s)) => format!("{k}.{s}.{label}"),
(Some(k), None) => format!("{k}.{label}"),
_ => label.to_string(),
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors EffectSink::declare_full's shape"
)]
fn declare(
&mut self,
kind: SymbolKind,
name: String,
range: TextRange,
params: Vec<ParamInfo>,
detail: Option<String>,
visibility: Option<VisibilityMark>,
was: Option<(String, TextRange)>,
doc: Option<DocBlock>,
) {
if let Some(doc) = doc {
self.manifest.docs.insert((kind, name.clone()), doc);
}
let sym = DeclaredSymbol {
name,
range,
params,
detail,
visibility,
was,
};
match kind {
SymbolKind::Knot => self.manifest.knots.push(sym),
SymbolKind::Stitch => self.manifest.stitches.push(sym),
SymbolKind::Variable => self.manifest.variables.push(sym),
SymbolKind::Constant => self.manifest.constants.push(sym),
SymbolKind::List => self.manifest.lists.push(sym),
SymbolKind::Struct => self.manifest.structs.push(sym),
SymbolKind::External => self.manifest.externals.push(sym),
SymbolKind::Label => self.manifest.labels.push(sym),
SymbolKind::ListItem => self.manifest.list_items.push(sym),
SymbolKind::Param | SymbolKind::Temp => {}
}
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors declare's shape (issue #2287: module_qualified is a plain \
positional passthrough, not a new structural concern)"
)]
fn push_ref(
&mut self,
path: String,
range: TextRange,
kind: RefKind,
knot: Option<&str>,
stitch: Option<&str>,
arg_count: Option<usize>,
module_qualified: bool,
) {
if path.is_empty() {
return;
}
self.manifest.unresolved.push(UnresolvedRef {
path,
range,
kind,
scope: Self::scope_of(knot, stitch),
arg_count,
module_qualified,
});
}
fn push_path_ref(
&mut self,
path: &Path,
kind: RefKind,
knot: Option<&str>,
stitch: Option<&str>,
arg_count: Option<usize>,
) {
self.push_ref(
path_text(path),
path.range,
kind,
knot,
stitch,
arg_count,
false,
);
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors declare's shape (issue #530: annotation is a plain \
positional passthrough, not a new structural concern)"
)]
fn push_local(
&mut self,
name: String,
range: TextRange,
kind: SymbolKind,
knot: Option<&str>,
stitch: Option<&str>,
param_detail: Option<ParamInfo>,
annotation: Option<crate::TypeExpr>,
) {
self.manifest.locals.push(LocalSymbol {
name,
range,
scope: Self::scope_of(knot, stitch),
kind,
param_detail,
annotation,
});
}
fn walk_type_annotation(
&mut self,
ty: &crate::TypeExpr,
knot: Option<&str>,
stitch: Option<&str>,
) {
if let crate::TypeExpr::Named { name, range } = ty {
self.push_ref(
name.clone(),
*range,
RefKind::Type,
knot,
stitch,
None,
false,
);
}
}
fn push_label(&mut self, name: String, range: TextRange) {
self.manifest.labels.push(DeclaredSymbol {
name,
range,
params: Vec::new(),
detail: None,
visibility: None,
was: None,
});
}
fn project_knot(&mut self, knot: &Knot) {
let bucket = knot.symbol_kind();
let detail = if knot.is_function {
Some("function".to_owned())
} else {
None
};
self.declare(
bucket,
knot.name.text.clone(),
knot.name.range,
param_infos(&knot.params),
detail,
knot.visibility,
knot.was.clone(),
knot.doc.clone(),
);
for param in &knot.params {
self.push_local(
param.name.text.clone(),
param.name.range,
SymbolKind::Param,
Some(knot.name.text.as_str()),
None,
Some(param_info(param)),
param.annotation.clone(),
);
if let Some(ann) = ¶m.annotation {
self.walk_type_annotation(ann, Some(&knot.name.text), None);
}
}
if let Some(rt) = &knot.return_type {
self.walk_type_annotation(rt, Some(&knot.name.text), None);
}
self.walk_block(&knot.body, Some(&knot.name.text), None);
for st in &knot.stitches {
let qualified = format!("{}.{}", knot.name.text, st.name.text);
self.declare(
SymbolKind::Stitch,
qualified,
st.name.range,
param_infos(&st.params),
None,
st.visibility,
st.was.clone(),
st.doc.clone(),
);
for param in &st.params {
self.push_local(
param.name.text.clone(),
param.name.range,
SymbolKind::Param,
Some(knot.name.text.as_str()),
Some(st.name.text.as_str()),
Some(param_info(param)),
param.annotation.clone(),
);
if let Some(ann) = ¶m.annotation {
self.walk_type_annotation(ann, Some(&knot.name.text), Some(&st.name.text));
}
}
if let Some(rt) = &st.return_type {
self.walk_type_annotation(rt, Some(&knot.name.text), Some(&st.name.text));
}
self.walk_block(&st.body, Some(&knot.name.text), Some(&st.name.text));
}
}
fn walk_block(&mut self, block: &Block, knot: Option<&str>, stitch: Option<&str>) {
if let Some(name) = &block.label {
let qualified = Self::qualify_label(knot, stitch, &name.text);
self.push_label(qualified, name.range);
}
for stmt in &block.stmts {
self.walk_stmt(stmt, knot, stitch);
}
}
fn walk_stmt(&mut self, stmt: &Stmt, knot: Option<&str>, stitch: Option<&str>) {
match stmt {
Stmt::Content(c) => self.walk_content(c, knot, stitch),
Stmt::Divert(d) => self.walk_divert_target(&d.target, knot, stitch),
Stmt::TunnelCall(t) => {
for target in &t.targets {
self.walk_divert_target(target, knot, stitch);
}
}
Stmt::ThreadStart(t) => self.walk_divert_target(&t.target, knot, stitch),
Stmt::TempDecl(t) => {
if let Some(e) = &t.value {
self.walk_expr(e, knot, stitch);
}
if let Some(ann) = &t.annotation {
self.walk_type_annotation(ann, knot, stitch);
}
self.push_local(
t.name.text.clone(),
t.name.range,
SymbolKind::Temp,
knot,
stitch,
None,
t.annotation.clone(),
);
}
Stmt::Assignment(a) => {
self.walk_expr(&a.target, knot, stitch);
self.walk_expr(&a.value, knot, stitch);
}
Stmt::Return(r) => self.walk_return(r, knot, stitch),
Stmt::ChoiceSet(cs) => self.walk_choice_set(cs, knot, stitch),
Stmt::LabeledBlock(b) => self.walk_block(b, knot, stitch),
Stmt::Conditional(c) => self.walk_conditional(c, knot, stitch),
Stmt::Sequence(s) => self.walk_sequence(s, knot, stitch),
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => self.walk_expr(e, knot, stitch),
Stmt::EndOfLine | Stmt::EndElementRun => {}
Stmt::LogicBlock(lb) => self.walk_logic_block(lb, knot, stitch),
Stmt::Await(a) => {
if let Some(e) = &a.condition {
self.walk_expr(e, knot, stitch);
}
}
}
}
fn walk_return(&mut self, r: &Return, knot: Option<&str>, stitch: Option<&str>) {
match (&r.value, r.kind) {
(Some(Expr::DivertTarget(p)), ReturnKind::TunnelRedirect) => {
let (path, module_qualified) = divert_path_text(p);
self.push_ref(
path,
p.range,
RefKind::Divert,
knot,
stitch,
Some(r.onwards_args.len()),
module_qualified,
);
}
(Some(e), _) => self.walk_expr(e, knot, stitch),
(None, _) => {}
}
for e in &r.onwards_args {
self.walk_expr(e, knot, stitch);
}
}
fn walk_divert_target(
&mut self,
target: &DivertTarget,
knot: Option<&str>,
stitch: Option<&str>,
) {
if let DivertPath::Path(p) = &target.path {
let (path, module_qualified) = divert_path_text(p);
self.push_ref(
path,
p.range,
RefKind::Divert,
knot,
stitch,
Some(target.args.len()),
module_qualified,
);
}
for e in &target.args {
self.walk_expr(e, knot, stitch);
}
}
fn walk_content(&mut self, content: &Content, knot: Option<&str>, stitch: Option<&str>) {
for part in &content.parts {
self.walk_content_part(part, knot, stitch);
}
for tag in &content.tags {
self.walk_tag(tag, knot, stitch);
}
}
fn walk_content_part(&mut self, part: &ContentPart, knot: Option<&str>, stitch: Option<&str>) {
match part {
ContentPart::Interpolation(e) => self.walk_expr(e, knot, stitch),
ContentPart::InlineConditional(c) => self.walk_conditional(c, knot, stitch),
ContentPart::InlineSequence(s) => self.walk_sequence(s, knot, stitch),
ContentPart::Span(span) => {
for child in &span.children {
self.walk_content_part(child, knot, stitch);
}
}
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn walk_tag(&mut self, tag: &Tag, knot: Option<&str>, stitch: Option<&str>) {
for part in &tag.parts {
self.walk_content_part(part, knot, stitch);
}
}
fn walk_choice_set(&mut self, cs: &ChoiceSet, knot: Option<&str>, stitch: Option<&str>) {
for choice in &cs.choices {
self.walk_choice(choice, knot, stitch);
}
self.walk_block(&cs.continuation, knot, stitch);
}
fn walk_choice(&mut self, choice: &Choice, knot: Option<&str>, stitch: Option<&str>) {
if let Some(name) = &choice.label {
let qualified = Self::qualify_label(knot, stitch, &name.text);
self.push_label(qualified, name.range);
}
if let Some(e) = &choice.condition {
self.walk_expr(e, knot, stitch);
}
if let Some(c) = &choice.start_content {
self.walk_content(c, knot, stitch);
}
if let Some(c) = &choice.bracket_content {
self.walk_content(c, knot, stitch);
}
if let Some(c) = &choice.inner_content {
self.walk_content(c, knot, stitch);
}
for tag in &choice.tags {
self.walk_tag(tag, knot, stitch);
}
self.push_as_binding(choice.binding.as_ref(), knot, stitch);
self.walk_block(&choice.body, knot, stitch);
}
fn walk_conditional(&mut self, cond: &Conditional, knot: Option<&str>, stitch: Option<&str>) {
if let CondKind::Switch(e) = &cond.kind {
self.walk_expr(e, knot, stitch);
}
for branch in &cond.branches {
if let Some(e) = &branch.condition {
self.walk_expr(e, knot, stitch);
}
self.push_as_binding(branch.binding.as_ref(), knot, stitch);
self.walk_block(&branch.body, knot, stitch);
}
}
fn push_as_binding(
&mut self,
binding: Option<&crate::Name>,
knot: Option<&str>,
stitch: Option<&str>,
) {
if let Some(name) = binding {
self.push_local(
name.text.clone(),
name.range,
SymbolKind::Temp,
knot,
stitch,
None,
None,
);
}
}
fn walk_sequence(&mut self, seq: &Sequence, knot: Option<&str>, stitch: Option<&str>) {
for branch in &seq.branches {
self.walk_block(&branch.body, knot, stitch);
}
}
fn walk_logic_block(&mut self, lb: &LogicBlock, knot: Option<&str>, stitch: Option<&str>) {
for bs in &lb.stmts {
self.walk_block_stmt(bs, knot, stitch);
}
}
fn walk_block_stmt(&mut self, bs: &BlockStmt, knot: Option<&str>, stitch: Option<&str>) {
match bs {
BlockStmt::TempDecl(t) => {
if let Some(e) = &t.value {
self.walk_expr(e, knot, stitch);
}
if let Some(ann) = &t.annotation {
self.walk_type_annotation(ann, knot, stitch);
}
self.push_local(
t.name.text.clone(),
t.name.range,
SymbolKind::Temp,
knot,
stitch,
None,
t.annotation.clone(),
);
}
BlockStmt::Assignment(a) => {
self.walk_expr(&a.target, knot, stitch);
self.walk_expr(&a.value, knot, stitch);
}
BlockStmt::Return(r) => self.walk_return(r, knot, stitch),
BlockStmt::If(i) => self.walk_if_stmt(i, knot, stitch),
BlockStmt::While(w) => self.walk_while_stmt(w, knot, stitch),
BlockStmt::For(f) => self.walk_for_stmt(f, knot, stitch),
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
BlockStmt::ExprStmt(e) => self.walk_expr(e, knot, stitch),
BlockStmt::Await(a) => {
if let Some(e) = &a.condition {
self.walk_expr(e, knot, stitch);
}
}
}
}
fn walk_if_stmt(&mut self, i: &IfStmt, knot: Option<&str>, stitch: Option<&str>) {
self.walk_expr(&i.condition, knot, stitch);
self.push_as_binding(i.binding.as_ref(), knot, stitch);
for s in &i.body {
self.walk_block_stmt(s, knot, stitch);
}
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => self.walk_if_stmt(inner, knot, stitch),
Some(ElseBranch::Else(stmts)) => {
for s in stmts {
self.walk_block_stmt(s, knot, stitch);
}
}
None => {}
}
}
fn walk_while_stmt(&mut self, w: &WhileStmt, knot: Option<&str>, stitch: Option<&str>) {
self.walk_expr(&w.condition, knot, stitch);
self.push_as_binding(w.binding.as_ref(), knot, stitch);
for s in &w.body {
self.walk_block_stmt(s, knot, stitch);
}
}
fn walk_for_stmt(&mut self, f: &ForStmt, knot: Option<&str>, stitch: Option<&str>) {
self.walk_expr(&f.iterable, knot, stitch);
self.push_local(
f.var_name.text.clone(),
f.var_name.range,
SymbolKind::Temp,
knot,
stitch,
None,
None,
);
if let Some(val_name) = &f.val_name {
self.push_local(
val_name.text.clone(),
val_name.range,
SymbolKind::Temp,
knot,
stitch,
None,
None,
);
}
for s in &f.body {
self.walk_block_stmt(s, knot, stitch);
}
}
fn walk_expr(&mut self, expr: &Expr, knot: Option<&str>, stitch: Option<&str>) {
match expr {
Expr::Int(_) | Expr::Float(_) | Expr::Bool(_) | Expr::Null => {}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(e) = part {
self.walk_expr(e, knot, stitch);
}
}
}
Expr::Path(p) => {
self.push_path_ref(p, RefKind::Variable, knot, stitch, None);
}
Expr::DivertTarget(p) => {
let (path, module_qualified) = divert_path_text(p);
self.push_ref(
path,
p.range,
RefKind::Divert,
knot,
stitch,
None,
module_qualified,
);
}
Expr::ListLiteral(items) => {
for p in items {
self.push_path_ref(p, RefKind::List, knot, stitch, None);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => {
self.walk_expr(inner, knot, stitch);
}
Expr::Infix(ie) => {
self.walk_expr(&ie.lhs, knot, stitch);
self.walk_expr(&ie.rhs, knot, stitch);
}
Expr::Call(path, args) => {
self.push_path_ref(path, RefKind::Function, knot, stitch, Some(args.len()));
for a in args {
self.walk_expr(a, knot, stitch);
}
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
self.walk_expr(e, knot, stitch);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
self.walk_expr(k, knot, stitch);
self.walk_expr(v, knot, stitch);
}
}
Expr::Index(idx) => {
self.walk_expr(&idx.base, knot, stitch);
self.walk_expr(&idx.index, knot, stitch);
}
Expr::Range(r) => {
self.walk_expr(&r.start, knot, stitch);
self.walk_expr(&r.end, knot, stitch);
}
Expr::StructLiteral(sl) => {
self.push_ref(
sl.shape.text.clone(),
sl.shape.range,
RefKind::Struct,
knot,
stitch,
None,
false,
);
for (_, v) in &sl.fields {
self.walk_expr(v, knot, stitch);
}
}
Expr::FieldAccess(fa) => self.walk_expr(&fa.base, knot, stitch),
Expr::FnLiteral(fl) => {
self.push_path_ref(&fl.target, RefKind::Function, knot, stitch, None);
for a in &fl.args {
self.walk_expr(a, knot, stitch);
}
}
Expr::RefArg(ra) => self.walk_expr(&ra.operand, knot, stitch),
Expr::Lambda(l) => self.walk_lambda(l, knot, stitch),
Expr::Fragment(stmts) => {
for s in stmts {
self.walk_stmt(s, knot, stitch);
}
}
}
}
fn walk_lambda(&mut self, l: &LambdaExpr, knot: Option<&str>, stitch: Option<&str>) {
for p in &l.params {
self.push_local(
p.name.text.clone(),
p.name.range,
SymbolKind::Temp,
knot,
stitch,
None,
None,
);
if let Some(ann) = &p.annotation {
self.walk_type_annotation(ann, knot, stitch);
}
}
if let Some(rt) = &l.return_type {
self.walk_type_annotation(rt, knot, stitch);
}
match &l.body {
LambdaBody::Expr(e) => self.walk_expr(e, knot, stitch),
LambdaBody::Block { stmts, tail } => {
for s in stmts {
self.walk_block_stmt(s, knot, stitch);
}
if let Some(t) = tail {
self.walk_expr(t, knot, stitch);
}
}
}
}
}
fn param_info(p: &Param) -> ParamInfo {
ParamInfo {
name: p.name.text.clone(),
is_ref: p.is_ref,
is_divert: p.is_divert,
}
}
fn param_infos(params: &[Param]) -> Vec<ParamInfo> {
params.iter().map(param_info).collect()
}
fn path_text(path: &Path) -> String {
path.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join(".")
}
fn divert_path_text(path: &Path) -> (String, bool) {
if path.crosses_module_wall {
let text = path
.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join("::");
(text, true)
} else {
(path_text(path), false)
}
}
#[cfg(test)]
#[expect(
clippy::panic,
reason = "test-only unwrap_or_else(|| panic!(...)) assertion helpers"
)]
mod tests {
use brink_syntax::parse;
use super::*;
use crate::FileId;
fn lower(source: &str) -> HirFile {
let parsed = parse(source);
let tree = parsed.tree();
let (hir, _legacy_manifest, diags) = crate::hir::lower(FileId(0), &tree);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
hir
}
#[test]
fn docs_project_for_every_declaration_kind() {
let hir = lower(
"\
/// An external.
EXTERNAL ping(x)
/// A variable.
VAR health = 100
/// A constant.
CONST SPEED = 0.5
/// A list.
LIST mood = happy, sad
/// A knot.
== hub ==
intro
/// A nested stitch.
= market
stalls
/// A function knot.
== function damage(weapon) ==
~ return 1
",
);
let manifest = project_manifest(&hir);
let doc_text = |kind: SymbolKind, name: &str| {
manifest
.docs
.get(&(kind, name.to_string()))
.unwrap_or_else(|| panic!("doc for {kind:?} {name}"))
.doc
.clone()
};
assert_eq!(
doc_text(SymbolKind::External, "ping").as_deref(),
Some("An external.")
);
assert_eq!(
doc_text(SymbolKind::Variable, "health").as_deref(),
Some("A variable.")
);
assert_eq!(
doc_text(SymbolKind::Constant, "SPEED").as_deref(),
Some("A constant.")
);
assert_eq!(
doc_text(SymbolKind::List, "mood").as_deref(),
Some("A list.")
);
assert_eq!(
doc_text(SymbolKind::Knot, "hub").as_deref(),
Some("A knot.")
);
assert_eq!(
doc_text(SymbolKind::Stitch, "hub.market").as_deref(),
Some("A nested stitch."),
"nested stitch docs are keyed by qualified name"
);
assert_eq!(
doc_text(SymbolKind::Knot, "damage").as_deref(),
Some("A function knot.")
);
}
#[test]
fn visibility_and_was_project_onto_declared_symbols() {
let hir = lower(
"\
== hub ==
#@was(old_hub)
#@private
Hello
-> END
#@was(old_health)
VAR health = 100
",
);
let manifest = project_manifest(&hir);
assert_eq!(
manifest.variables[0].was.as_ref().map(|(n, _)| n.as_str()),
Some("old_health")
);
assert_eq!(manifest.knots[0].visibility, Some(VisibilityMark::Private));
assert_eq!(
manifest.knots[0].was.as_ref().map(|(n, _)| n.as_str()),
Some("old_hub")
);
}
#[test]
fn external_params_keep_their_names_not_just_a_count() {
let hir = lower("EXTERNAL greet(name, times)\n");
let manifest = project_manifest(&hir);
let ext = &manifest.externals[0];
assert_eq!(
ext.params
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["name", "times"]
);
}
#[test]
fn list_items_project_with_qualified_names() {
let hir = lower("LIST mood = happy, (sad), angry\n");
let manifest = project_manifest(&hir);
let names: Vec<_> = manifest
.list_items
.iter()
.map(|s| s.name.as_str())
.collect();
assert_eq!(names, vec!["mood.happy", "mood.sad", "mood.angry"]);
}
#[test]
fn promoted_top_level_stitch_declares_bare_stitch_not_knot() {
let hir = lower("= market\nstalls\n-> END\n");
let manifest = project_manifest(&hir);
assert!(manifest.knots.is_empty(), "no real knot in this file");
assert_eq!(manifest.stitches.len(), 1);
assert_eq!(manifest.stitches[0].name, "market");
}
#[test]
fn params_project_as_locals_scoped_to_their_container() {
let hir = lower(
"\
== hub(gold) ==
= market(item)
buy {item} for {gold}
-> END
",
);
let manifest = project_manifest(&hir);
let hub_gold = manifest
.locals
.iter()
.find(|l| l.name == "gold")
.expect("knot param `gold` is a local");
assert_eq!(hub_gold.kind, SymbolKind::Param);
assert_eq!(hub_gold.scope.knot.as_deref(), Some("hub"));
assert_eq!(hub_gold.scope.stitch, None);
let market_item = manifest
.locals
.iter()
.find(|l| l.name == "item")
.expect("stitch param `item` is a local");
assert_eq!(market_item.scope.knot.as_deref(), Some("hub"));
assert_eq!(market_item.scope.stitch.as_deref(), Some("market"));
}
#[test]
fn temp_decl_and_for_loop_binding_project_as_temp_locals() {
let hir = lower(
"\
== hub ==
~ temp x = 1
~ { for y in #[1, 2, 3] { } }
-> END
",
);
let manifest = project_manifest(&hir);
let temp_names: Vec<_> = manifest
.locals
.iter()
.filter(|l| l.kind == SymbolKind::Temp)
.map(|l| l.name.as_str())
.collect();
assert!(temp_names.contains(&"x"), "{temp_names:?}");
assert!(temp_names.contains(&"y"), "{temp_names:?}");
}
#[test]
fn every_ref_kind_projects_with_the_right_scope_and_arg_count() {
let hir = lower(
"\
VAR g = 0
LIST L = a, b
STRUCT Point = #{ x: int }
EXTERNAL beep(n)
== hub ==
{g}
~ beep(1, 2)
~ temp chosen = (a, b)
~ temp p = Point#{ x: 1 }
-> away
=== away ===
-> END
",
);
let manifest = project_manifest(&hir);
let find = |kind: RefKind, path: &str| {
manifest
.unresolved
.iter()
.find(|r| r.kind == kind && r.path == path)
.unwrap_or_else(|| {
panic!(
"expected a {kind:?} ref to `{path}`: {:?}",
manifest.unresolved
)
})
};
let variable = find(RefKind::Variable, "g");
assert_eq!(variable.scope.knot.as_deref(), Some("hub"));
assert_eq!(variable.arg_count, None);
let func = find(RefKind::Function, "beep");
assert_eq!(func.arg_count, Some(2));
let list = find(RefKind::List, "a");
assert_eq!(list.arg_count, None);
let strukt = find(RefKind::Struct, "Point");
assert_eq!(strukt.arg_count, None);
let divert = find(RefKind::Divert, "away");
assert_eq!(divert.arg_count, Some(0));
}
#[test]
fn choice_and_gather_labels_project_with_qualified_names() {
let hir = lower(
"\
== hub ==
* (opener) [Go] Onward.
- (settle) Settled.
-> END
",
);
let manifest = project_manifest(&hir);
let names: Vec<_> = manifest.labels.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"hub.opener"), "{names:?}");
assert!(names.contains(&"hub.settle"), "{names:?}");
}
}