use rowan::TextRange;
use crate::hir;
use crate::symbols::SymbolKind;
use crate::{Diagnostic, DiagnosticCode};
use super::content::lower_content;
use super::context::LowerCtx;
use super::expr::{lower_expr, path_to_string};
use super::lir;
pub(super) fn stmt_provenance(stmt: &hir::Stmt, ctx: &LowerCtx<'_>) -> crate::Provenance {
match stmt {
hir::Stmt::Content(c) => c.ptr.unwrap_or(ctx.current_stmt_provenance),
hir::Stmt::Divert(d) => d.ptr.unwrap_or(ctx.current_stmt_provenance),
hir::Stmt::TunnelCall(t) => t.ptr,
hir::Stmt::ThreadStart(t) => t.ptr,
hir::Stmt::TempDecl(t) => t.ptr,
hir::Stmt::Assignment(a) => a.ptr,
hir::Stmt::Return(r) => r.ptr.unwrap_or(ctx.current_stmt_provenance),
hir::Stmt::ChoiceSet(cs) => {
ctx.provenance_at(choice_set_anchor_range(cs), crate::NodeClass::Stmt)
}
hir::Stmt::LabeledBlock(b) => {
ctx.provenance_at(labeled_block_anchor_range(b), crate::NodeClass::Stmt)
}
hir::Stmt::Conditional(c) => c.ptr,
hir::Stmt::Sequence(s) => s.ptr,
hir::Stmt::LogicBlock(lb) => lb.ptr,
hir::Stmt::Await(a) => a.ptr,
hir::Stmt::ExprStmt(e) | hir::Stmt::AttachElement(e) => crate::hir::expr_span(e)
.map_or(ctx.current_stmt_provenance, |r| {
ctx.provenance_at(r, crate::NodeClass::Expr)
}),
hir::Stmt::EndOfLine | hir::Stmt::EndElementRun => ctx.current_stmt_provenance,
}
}
pub(super) fn lower_stmt(stmt: &hir::Stmt, ctx: &mut LowerCtx<'_>) -> Option<lir::Stmt> {
let provenance = ctx.enter_stmt(stmt_provenance(stmt, ctx));
let kind = match stmt {
hir::Stmt::Divert(divert) => Some(lir::StmtKind::Divert(lower_divert_target(
&divert.target,
ctx,
))),
hir::Stmt::TunnelCall(tunnel) => {
let targets = tunnel
.targets
.iter()
.map(|t| {
let d = lower_divert_target(t, ctx);
lir::TunnelTarget {
target: d.target,
args: d.args,
}
})
.collect();
Some(lir::StmtKind::TunnelCall(lir::TunnelCall { targets }))
}
hir::Stmt::ThreadStart(thread) => {
let d = lower_divert_target(&thread.target, ctx);
Some(lir::StmtKind::ThreadStart(lir::ThreadStart {
target: d.target,
args: d.args,
}))
}
hir::Stmt::TempDecl(decl) => lower_temp_decl(decl, ctx),
hir::Stmt::Assignment(assign) => {
let target = lower_assign_target(&assign.target, ctx)?;
let value = lower_expr(&assign.value, ctx);
Some(lir::StmtKind::Assign {
target,
op: assign.op,
value,
})
}
hir::Stmt::Return(ret) => {
let value = ret.value.as_ref().map(|e| lower_expr(e, ctx));
let is_tunnel = ret.kind == hir::ReturnKind::TunnelRedirect;
let args = ret
.onwards_args
.iter()
.map(|a| lir::CallArg::Value(lower_expr(a, ctx)))
.collect();
Some(lir::StmtKind::Return {
value,
is_tunnel,
args,
})
}
hir::Stmt::ExprStmt(expr) => {
let mut postfix_out = Vec::new();
if super::blocks::try_lower_postfix_stmt(expr, ctx, &mut postfix_out) {
return postfix_out.into_iter().next();
}
Some(lir::StmtKind::ExprStmt(lower_expr(expr, ctx)))
}
hir::Stmt::Content(content) => {
if let Some(emission) = super::recognize::try_recognize(content, ctx) {
Some(lir::StmtKind::EmitLine(emission))
} else {
Some(lir::StmtKind::EmitContent(lower_content(content, ctx)))
}
}
hir::Stmt::ChoiceSet(_)
| hir::Stmt::LabeledBlock(_)
| hir::Stmt::Conditional(_)
| hir::Stmt::Sequence(_) => {
reject_unsupported_inline_construct(stmt, ctx);
None
}
hir::Stmt::EndOfLine => Some(lir::StmtKind::EndOfLine),
hir::Stmt::LogicBlock(_) => {
debug_assert!(
false,
"T1b LogicBlock should be dispatched by lower_block_with_children, \
not reach lower_stmt"
);
None
}
hir::Stmt::Await(a) => {
emit_await_lowering_fence(ctx, a.ptr.text_range());
None
}
hir::Stmt::AttachElement(expr) => Some(lir::StmtKind::AttachElement(lower_expr(expr, ctx))),
hir::Stmt::EndElementRun => Some(lir::StmtKind::EndElementRun),
};
kind.map(|k| lir::Stmt::new(k, provenance))
}
fn lower_temp_decl(decl: &hir::TempDecl, ctx: &mut LowerCtx<'_>) -> Option<lir::StmtKind> {
let slot = ctx.temp_slot_raw(&decl.name.text)?;
let name = ctx.names.intern(&decl.name.text);
let value = decl.value.as_ref().map(|e| lower_expr(e, ctx));
ctx.visible_temps.insert(decl.name.text.clone());
ctx.record_temp_annotation(slot, decl.annotation.as_ref());
Some(lir::StmtKind::DeclareTemp {
slot,
name,
value,
synthetic: decl.synthetic,
})
}
pub(super) fn emit_await_lowering_fence(ctx: &mut LowerCtx<'_>, range: TextRange) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: `await` (FlowFrame suspension) parses and purity-checks, but its runtime \
spill/restore semantics are not implemented yet (docs/flow-suspension-spec.md \
§3, FS-3)",
DiagnosticCode::E052.title(),
),
code: DiagnosticCode::E052,
});
}
fn reject_unsupported_inline_construct(stmt: &hir::Stmt, ctx: &mut LowerCtx<'_>) {
let (range, construct) = match stmt {
hir::Stmt::ChoiceSet(cs) => (choice_set_anchor_range(cs), "a nested choice"),
hir::Stmt::LabeledBlock(inner) => (
labeled_block_anchor_range(inner),
"a nested labeled gather block (`- (label)`)",
),
hir::Stmt::Conditional(cond) => (cond.ptr.text_range(), "a nested multi-line conditional"),
hir::Stmt::Sequence(seq) => (
seq.ptr.text_range(),
"a nested sequence (stopping/cycle/once/shuffle)",
),
_ => unreachable!(
"reject_unsupported_inline_construct is only called for ChoiceSet/LabeledBlock/\
Conditional/Sequence"
),
};
emit_unsupported_nested_construct(ctx, range, construct);
}
fn emit_unsupported_nested_construct(ctx: &mut LowerCtx<'_>, range: TextRange, construct: &str) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: {construct} is not supported embedded in inline content (e.g. a choice's own \
display/bracket/inner text) — it needs a child container, which an inline-content \
position cannot hold",
DiagnosticCode::E059.title(),
),
code: DiagnosticCode::E059,
});
}
fn choice_set_anchor_range(cs: &hir::ChoiceSet) -> TextRange {
cs.choices.first().map_or_else(
|| TextRange::new(0.into(), 0.into()),
|c| c.ptr.text_range(),
)
}
fn labeled_block_anchor_range(block: &hir::Block) -> TextRange {
block
.label
.as_ref()
.map_or_else(|| TextRange::new(0.into(), 0.into()), |label| label.range)
}
fn lower_divert_target(target: &hir::DivertTarget, ctx: &mut LowerCtx<'_>) -> lir::Divert {
let lir_target = match &target.path {
hir::DivertPath::Done => lir::DivertTarget::Done,
hir::DivertPath::End => lir::DivertTarget::End,
hir::DivertPath::Path(path) => {
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
let name_id = ctx.names.intern(&name);
lir::DivertTarget::VariableTemp(slot, name_id)
} else if let Some(info) = ctx.resolve_path(path.range) {
match info.kind {
SymbolKind::Variable | SymbolKind::Constant => {
lir::DivertTarget::Variable(info.id)
}
_ => lir::DivertTarget::Address(info.id),
}
} else {
lir::DivertTarget::Done
}
}
};
let target_params = match &target.path {
hir::DivertPath::Path(path) => ctx
.resolve_path(path.range)
.map(|info| info.params.clone())
.unwrap_or_default(),
_ => Vec::new(),
};
let args = super::expr::lower_call_args(&target.args, &target_params, ctx);
lir::Divert {
target: lir_target,
args,
}
}
pub(super) fn lower_assign_target(
expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
) -> Option<lir::AssignTarget> {
match expr {
hir::Expr::Path(path) => {
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
if reject_as_binding_write(slot, &name, path.range, ctx) {
return None;
}
let name_id = ctx.names.intern(&name);
return Some(lir::AssignTarget::Temp(slot, name_id));
}
if let Some(info) = ctx.resolve_path(path.range) {
if reject_const_write(info, path.range, ctx) {
return None;
}
if info.kind == SymbolKind::Temp
&& let Some(slot) = ctx.temp_slot_raw(&name)
{
if reject_as_binding_write(slot, &name, path.range, ctx) {
return None;
}
let name_id = ctx.names.intern(&name);
return Some(lir::AssignTarget::Temp(slot, name_id));
}
let id = if info.kind == SymbolKind::List {
super::decls::list_def_to_global_var(info.id)
} else {
info.id
};
return Some(lir::AssignTarget::Global(id));
}
None
}
_ => None,
}
}
pub(super) fn reject_as_binding_write(
slot: u16,
name: &str,
range: TextRange,
ctx: &mut LowerCtx<'_>,
) -> bool {
if ctx.as_binding_slots.contains(&slot) {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: `{name}` is an `as` binding — it is immutable and cannot be \
assigned to or mutated in place",
crate::DiagnosticCode::E148.title(),
),
code: crate::DiagnosticCode::E148,
});
return true;
}
false
}
pub(super) fn reject_const_write(
info: &crate::symbols::SymbolInfo,
range: TextRange,
ctx: &mut LowerCtx<'_>,
) -> bool {
if info.kind == SymbolKind::Constant {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: `{}` is declared CONST — it cannot be reassigned, mutated, or passed by \
`ref`",
crate::DiagnosticCode::E187.title(),
info.name,
),
code: crate::DiagnosticCode::E187,
});
return true;
}
false
}