use std::collections::{BTreeMap, BTreeSet};
use rowan::TextRange;
use crate::provenance::Provenance;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Name {
pub text: String,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Path {
pub segments: Vec<Name>,
pub range: TextRange,
pub crosses_module_wall: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
pub parts: Vec<ContentPart>,
pub ptr: Provenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HirFile {
pub root_content: Block,
pub knots: Vec<Knot>,
pub variables: Vec<VarDecl>,
pub constants: Vec<ConstDecl>,
pub lists: Vec<ListDecl>,
pub structs: Vec<StructDecl>,
pub externals: Vec<ExternalDecl>,
pub includes: Vec<IncludeSite>,
pub module: Option<ModuleDecl>,
pub imports: Vec<Import>,
pub visibility: Vec<VisibilityDirective>,
pub was_directives: Vec<TextRange>,
pub allow_scopes: Vec<crate::suppressions::AllowScope>,
pub element_matches: Vec<ElementMatch>,
pub cue_names: Vec<CueSite>,
pub native: bool,
pub claim_handlers: Vec<ClaimHandlerDecl>,
pub dispatch_handlers: Vec<DispatchHandlerDecl>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleDecl {
pub name: String,
pub range: TextRange,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Import {
pub module: String,
pub module_range: TextRange,
pub items: Vec<ImportItem>,
pub bare: bool,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportItem {
pub name: String,
pub alias: Option<String>,
pub range: TextRange,
}
impl ImportItem {
#[must_use]
pub fn local_name(&self) -> &str {
self.alias.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisibilityDirective {
pub mark: crate::VisibilityMark,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectsAssertion {
pub pure: bool,
pub silent: bool,
pub total: bool,
pub reads: Vec<String>,
pub writes: Vec<String>,
pub calls: Vec<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementAnnotation {
pub pattern: String,
pub captures: Vec<String>,
pub alias: Option<String>,
pub block: bool,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConventionAnnotation {
pub pattern: String,
pub order: i64,
pub captures: Vec<String>,
pub block: bool,
pub attach: Option<Name>,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementKind {
ContentLine,
SceneHeading,
BangDispatch,
Cue,
Parenthetical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementCapture {
pub name: String,
pub text: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementDisposition {
Call,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementMatch {
pub line: TextRange,
pub kind: ElementKind,
pub handler: Name,
pub annotation: TextRange,
pub captures: Vec<ElementCapture>,
pub disposition: ElementDisposition,
pub content: Option<TextRange>,
pub injected: bool,
pub slug: Option<ElementCapture>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClaimHandlerDecl {
pub name: Name,
pub annotation: TextRange,
pub params: Vec<String>,
pub pattern: String,
pub block: bool,
pub order: i64,
pub attach: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DispatchHandlerDecl {
pub dispatch_name: String,
pub name: Name,
pub annotation: TextRange,
pub params: Vec<String>,
pub pattern: String,
pub block: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConventionMode {
Attach,
Wrap,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaTypeShape {
Named(String),
Generic {
name: String,
args: Vec<SchemaTypeShape>,
},
Fn {
params: Vec<SchemaTypeShape>,
ret: Box<SchemaTypeShape>,
},
}
impl From<&TypeExpr> for SchemaTypeShape {
fn from(ty: &TypeExpr) -> Self {
match ty {
TypeExpr::Named { name, .. } => Self::Named(name.clone()),
TypeExpr::Generic { name, args, .. } => Self::Generic {
name: name.clone(),
args: args.iter().map(Self::from).collect(),
},
TypeExpr::Fn { params, ret, .. } => Self::Fn {
params: params.iter().map(Self::from).collect(),
ret: Box::new(Self::from(&**ret)),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConventionAttachField {
pub name: String,
pub ty: SchemaTypeShape,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConventionAttachSchema {
Resolved {
name: String,
fields: Vec<ConventionAttachField>,
},
Unresolved(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConventionProjectionEntry {
pub name: Name,
pub dispatch_name: Option<String>,
pub pattern: String,
pub order: i64,
pub mode: ConventionMode,
pub disposition: ElementDisposition,
pub attach: Option<ConventionAttachSchema>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ConventionsProjection {
pub entries: Vec<ConventionProjectionEntry>,
pub dispatch: Vec<ConventionProjectionEntry>,
pub transitions: Vec<crate::dialect::TransitionRow>,
pub templates: crate::dialect::Templates,
}
impl ConventionsProjection {
#[must_use]
pub fn from_decls(
decls: &[ClaimHandlerDecl],
dispatch_decls: &[DispatchHandlerDecl],
structs: &BTreeMap<String, Vec<ConventionAttachField>>,
) -> Self {
Self {
entries: decls
.iter()
.map(|decl| ConventionProjectionEntry {
name: decl.name.clone(),
dispatch_name: None,
pattern: decl.pattern.clone(),
order: decl.order,
mode: if decl.block {
ConventionMode::Wrap
} else {
ConventionMode::Attach
},
disposition: ElementDisposition::Call,
attach: decl.attach.as_ref().map(|name| match structs.get(name) {
Some(fields) => ConventionAttachSchema::Resolved {
name: name.clone(),
fields: fields.clone(),
},
None => ConventionAttachSchema::Unresolved(name.clone()),
}),
})
.collect(),
dispatch: dispatch_decls
.iter()
.enumerate()
.map(|(index, decl)| ConventionProjectionEntry {
name: decl.name.clone(),
dispatch_name: Some(decl.dispatch_name.clone()),
pattern: decl.pattern.clone(),
order: i64::try_from(index).unwrap_or(i64::MAX),
mode: if decl.block {
ConventionMode::Wrap
} else {
ConventionMode::Attach
},
disposition: ElementDisposition::Call,
attach: None,
})
.collect(),
transitions: Vec::new(),
templates: crate::dialect::Templates::default(),
}
}
pub fn with_succession(
mut self,
transitions: Vec<crate::dialect::TransitionRow>,
templates: crate::dialect::Templates,
) -> Result<Self, Vec<crate::dialect::DialectError>> {
let known: BTreeSet<&str> = self
.entries
.iter()
.map(|entry| entry.name.text.as_str())
.chain(crate::dialect::reserved_structural_kinds().iter().copied())
.collect();
let errors =
crate::dialect::validate_succession(&transitions, &templates, |k| known.contains(k));
if errors.is_empty() {
self.transitions = transitions;
self.templates = templates;
Ok(self)
} else {
Err(errors)
}
}
#[must_use]
pub fn to_wire(&self) -> brink_format::ConventionsProjectionDef {
brink_format::ConventionsProjectionDef {
entries: self
.entries
.iter()
.map(ConventionProjectionEntry::to_wire)
.collect(),
}
}
}
impl ConventionProjectionEntry {
#[must_use]
pub fn to_wire(&self) -> brink_format::ConventionEntryDef {
brink_format::ConventionEntryDef {
name: self.name.text.clone(),
pattern: self.pattern.clone(),
order: self.order,
mode: match self.mode {
ConventionMode::Attach => brink_format::ConventionModeDef::Attach,
ConventionMode::Wrap => brink_format::ConventionModeDef::Wrap,
},
attach: self.attach.as_ref().map(ConventionAttachSchema::to_wire),
}
}
}
impl ConventionAttachSchema {
#[must_use]
pub fn to_wire(&self) -> brink_format::ConventionAttachDef {
match self {
Self::Resolved { name, fields } => brink_format::ConventionAttachDef::Resolved {
name: name.clone(),
fields: fields.iter().map(ConventionAttachField::to_wire).collect(),
},
Self::Unresolved(name) => brink_format::ConventionAttachDef::Unresolved(name.clone()),
}
}
}
impl ConventionAttachField {
#[must_use]
pub fn to_wire(&self) -> brink_format::ConventionAttachFieldDef {
brink_format::ConventionAttachFieldDef {
name: self.name.clone(),
ty: self.ty.to_wire(),
}
}
}
impl SchemaTypeShape {
#[must_use]
pub fn to_wire(&self) -> brink_format::SchemaTypeDef {
match self {
Self::Named(name) => brink_format::SchemaTypeDef::Named(name.clone()),
Self::Generic { name, args } => brink_format::SchemaTypeDef::Generic {
name: name.clone(),
args: args.iter().map(SchemaTypeShape::to_wire).collect(),
},
Self::Fn { params, ret } => brink_format::SchemaTypeDef::Fn {
params: params.iter().map(SchemaTypeShape::to_wire).collect(),
ret: Box::new(ret.to_wire()),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CueSite {
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleToken {
AlignLeft,
AlignCenter,
AlignRight,
Bold,
Italic,
Dim,
Mono,
Uppercase,
Conceal,
Color(String),
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyleEntry {
pub key: String,
pub value: StyleToken,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyleAnnotation {
pub entries: Vec<StyleEntry>,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Knot {
pub ptr: Provenance,
pub name: Name,
pub is_function: bool,
pub params: Vec<Param>,
pub body: Block,
pub stitches: Vec<Stitch>,
pub is_local: bool,
pub effects_assertion: Option<EffectsAssertion>,
pub element_annotation: Option<ElementAnnotation>,
pub convention_annotation: Option<ConventionAnnotation>,
pub style_annotation: Option<StyleAnnotation>,
pub return_type: Option<TypeExpr>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
impl Knot {
#[must_use]
pub fn symbol_kind(&self) -> crate::SymbolKind {
if self.ptr.class() == crate::provenance::NodeClass::Stitch {
crate::SymbolKind::Stitch
} else {
crate::SymbolKind::Knot
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stitch {
pub ptr: Provenance,
pub name: Name,
pub params: Vec<Param>,
pub body: Block,
pub is_local: bool,
pub effects_assertion: Option<EffectsAssertion>,
pub element_annotation: Option<ElementAnnotation>,
pub convention_annotation: Option<ConventionAnnotation>,
pub style_annotation: Option<StyleAnnotation>,
pub return_type: Option<TypeExpr>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Name,
pub is_ref: bool,
pub is_divert: bool,
pub annotation: Option<TypeExpr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeExpr {
Named { name: String, range: TextRange },
Generic {
name: String,
args: Vec<TypeExpr>,
range: TextRange,
},
Fn {
params: Vec<TypeExpr>,
ret: Box<TypeExpr>,
range: TextRange,
},
}
impl TypeExpr {
#[must_use]
pub fn range(&self) -> TextRange {
match self {
Self::Named { range, .. } | Self::Generic { range, .. } | Self::Fn { range, .. } => {
*range
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Block {
pub label: Option<Name>,
pub stmts: Vec<Stmt>,
pub container_id: Option<brink_format::DefinitionId>,
pub tail: Tail,
}
impl Block {
#[must_use]
pub fn from_stmts(stmts: Vec<Stmt>) -> Self {
let tail = tail_from_stmts(&stmts);
Self {
label: None,
stmts,
container_id: None,
tail,
}
}
#[must_use]
pub fn tail(&self) -> &Tail {
&self.tail
}
pub fn recompute_tail(&mut self) {
self.tail = tail_from_stmts(&self.stmts);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Tail {
Value(Expr),
Diverge(Terminator),
#[default]
Unit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Terminator {
Divert(Divert),
Return(Return),
}
#[must_use]
pub fn tail_from_stmts(stmts: &[Stmt]) -> Tail {
match stmts.last() {
Some(Stmt::Divert(d)) => Tail::Diverge(Terminator::Divert(d.clone())),
Some(Stmt::Return(r)) => Tail::Diverge(Terminator::Return(r.clone())),
_ => Tail::Unit,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Content(Content),
Divert(Divert),
TunnelCall(TunnelCall),
ThreadStart(ThreadStart),
TempDecl(TempDecl),
Assignment(Assignment),
Return(Return),
ChoiceSet(Box<ChoiceSet>),
LabeledBlock(Box<Block>),
Conditional(Conditional),
Sequence(Sequence),
ExprStmt(Expr),
EndOfLine,
LogicBlock(LogicBlock),
Await(AwaitStmt),
AttachElement(Expr),
EndElementRun,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogicBlock {
pub ptr: Provenance,
pub stmts: Vec<BlockStmt>,
pub scope: LogicBlockScope,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogicBlockScope {
#[default]
Standalone,
Opens,
Continues,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockStmt {
TempDecl(TempDecl),
Assignment(Assignment),
Return(Return),
If(IfStmt),
While(WhileStmt),
For(ForStmt),
Break(Provenance),
Continue(Provenance),
ExprStmt(Expr),
Await(AwaitStmt),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IfStmt {
pub ptr: Provenance,
pub condition: Expr,
pub binding: Option<Name>,
pub body: Vec<BlockStmt>,
pub else_branch: Option<ElseBranch>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ElseBranch {
ElseIf(Box<IfStmt>),
Else(Vec<BlockStmt>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WhileStmt {
pub ptr: Provenance,
pub condition: Expr,
pub binding: Option<Name>,
pub body: Vec<BlockStmt>,
pub is_await: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AwaitStmt {
pub ptr: Provenance,
pub condition: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForStmt {
pub ptr: Provenance,
pub var_name: Name,
pub val_name: Option<Name>,
pub iterable: Expr,
pub body: Vec<BlockStmt>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChoiceSetContext {
Weave,
Inline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChoiceSet {
pub choices: Vec<Choice>,
pub continuation: Block,
pub context: ChoiceSetContext,
pub depth: u32,
pub gather_id: Option<brink_format::DefinitionId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Choice {
pub ptr: Provenance,
pub is_sticky: bool,
pub is_fallback: bool,
pub label: Option<Name>,
pub condition: Option<Expr>,
pub binding: Option<Name>,
pub start_content: Option<Content>,
pub bracket_content: Option<Content>,
pub inner_content: Option<Content>,
pub tags: Vec<Tag>,
pub body: Block,
pub container_id: Option<brink_format::DefinitionId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Content {
pub ptr: Option<Provenance>,
pub parts: Vec<ContentPart>,
pub tags: Vec<Tag>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentPart {
Text(String),
Glue,
Spring,
Interpolation(Expr),
InlineConditional(Conditional),
InlineSequence(Sequence),
Span(SpanPart),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpanPart {
pub ptr: Provenance,
pub name: String,
pub attrs: Vec<SpanAttr>,
pub children: Vec<ContentPart>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpanAttr {
pub ptr: Provenance,
pub name: String,
pub value: String,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SequenceType: u8 {
const STOPPING = 0x01;
const CYCLE = 0x02;
const ONCE = 0x04;
const SHUFFLE = 0x08;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CondKind {
InitialCondition,
IfElse,
Switch(Expr),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conditional {
pub ptr: Provenance,
pub kind: CondKind,
pub branches: Vec<CondBranch>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CondBranch {
pub ptr: Provenance,
pub condition: Option<Expr>,
pub binding: Option<Name>,
pub body: Block,
pub container_id: Option<brink_format::DefinitionId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sequence {
pub ptr: Provenance,
pub kind: SequenceType,
pub branches: Vec<SequenceBranch>,
pub container_id: Option<brink_format::DefinitionId>,
pub counter_id: Option<brink_format::DefinitionId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SequenceBranch {
pub ptr: Provenance,
pub body: Block,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Divert {
pub ptr: Option<Provenance>,
pub target: DivertTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TunnelCall {
pub ptr: Provenance,
pub targets: Vec<DivertTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadStart {
pub ptr: Provenance,
pub target: DivertTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DivertTarget {
pub path: DivertPath,
pub args: Vec<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DivertPath {
Path(Path),
Done,
End,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Return {
pub ptr: Option<Provenance>,
pub kind: ReturnKind,
pub value: Option<Expr>,
pub onwards_args: Vec<Expr>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReturnKind {
Explicit,
TunnelRedirect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Int(i32),
Float(FloatBits),
Bool(bool),
String(StringExpr),
Null,
Path(Path),
DivertTarget(Path),
ListLiteral(Vec<Path>),
Prefix(PrefixOp, Box<Expr>),
Infix(InfixExpr),
Postfix(Box<Expr>, PostfixOp),
Call(Path, Vec<Expr>),
ArrayLiteral(ArrayLiteral),
MapLiteral(MapLiteral),
Index(IndexExpr),
Range(RangeExpr),
StructLiteral(StructLiteral),
FieldAccess(FieldAccessExpr),
FnLiteral(FnLiteral),
Lambda(Box<LambdaExpr>),
RefArg(RefArgExpr),
Fragment(Vec<Stmt>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FnLiteral {
pub ptr: Provenance,
pub target: Path,
pub args: Vec<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LambdaExpr {
pub ptr: Provenance,
pub params: Vec<Param>,
pub return_type: Option<TypeExpr>,
pub body: LambdaBody,
pub container_id: Option<brink_format::DefinitionId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LambdaBody {
Expr(Box<Expr>),
Block {
stmts: Vec<BlockStmt>,
tail: Option<Box<Expr>>,
},
}
impl LambdaBody {
#[must_use]
pub fn value_exprs(&self) -> Vec<&Expr> {
match self {
Self::Expr(e) => vec![e],
Self::Block { tail, .. } => tail.as_deref().into_iter().collect(),
}
}
#[must_use]
pub fn all_exprs(&self) -> Vec<&Expr> {
match self {
Self::Expr(e) => vec![e],
Self::Block { stmts, tail } => {
let mut out = Vec::new();
for s in stmts {
push_block_stmt_exprs(s, &mut out);
}
out.extend(tail.as_deref());
out
}
}
}
}
fn push_block_stmt_exprs<'a>(bs: &'a BlockStmt, out: &mut Vec<&'a Expr>) {
match bs {
BlockStmt::TempDecl(t) => out.extend(t.value.as_ref()),
BlockStmt::Assignment(a) => {
out.push(&a.target);
out.push(&a.value);
}
BlockStmt::Return(r) => {
out.extend(r.value.as_ref());
out.extend(r.onwards_args.iter());
}
BlockStmt::If(i) => push_if_stmt_exprs(i, out),
BlockStmt::While(w) => {
out.push(&w.condition);
for s in &w.body {
push_block_stmt_exprs(s, out);
}
}
BlockStmt::For(f) => {
out.push(&f.iterable);
for s in &f.body {
push_block_stmt_exprs(s, out);
}
}
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
BlockStmt::ExprStmt(e) => out.push(e),
BlockStmt::Await(a) => out.extend(a.condition.as_ref()),
}
}
fn push_if_stmt_exprs<'a>(i: &'a IfStmt, out: &mut Vec<&'a Expr>) {
out.push(&i.condition);
for s in &i.body {
push_block_stmt_exprs(s, out);
}
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => push_if_stmt_exprs(inner, out),
Some(ElseBranch::Else(stmts)) => {
for s in stmts {
push_block_stmt_exprs(s, out);
}
}
None => {}
}
}
#[must_use]
pub fn fragment_stmt_exprs(stmts: &[Stmt]) -> Vec<&Expr> {
let mut out = Vec::new();
for s in stmts {
push_stmt_exprs(s, &mut out);
}
out
}
fn push_stmt_exprs<'a>(stmt: &'a Stmt, out: &mut Vec<&'a Expr>) {
match stmt {
Stmt::Content(c) => {
for part in &c.parts {
if let ContentPart::Interpolation(e) = part {
out.push(e);
}
}
}
Stmt::Divert(d) => out.extend(d.target.args.iter()),
Stmt::TunnelCall(t) => {
for target in &t.targets {
out.extend(target.args.iter());
}
}
Stmt::ThreadStart(t) => out.extend(t.target.args.iter()),
Stmt::TempDecl(t) => out.extend(t.value.as_ref()),
Stmt::Assignment(a) => {
out.push(&a.target);
out.push(&a.value);
}
Stmt::Return(r) => {
out.extend(r.value.as_ref());
out.extend(r.onwards_args.iter());
}
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
out.extend(choice.condition.as_ref());
for c in [
&choice.start_content,
&choice.bracket_content,
&choice.inner_content,
]
.into_iter()
.flatten()
{
for part in &c.parts {
if let ContentPart::Interpolation(e) = part {
out.push(e);
}
}
}
for s in &choice.body.stmts {
push_stmt_exprs(s, out);
}
}
for s in &cs.continuation.stmts {
push_stmt_exprs(s, out);
}
}
Stmt::LabeledBlock(b) => {
for s in &b.stmts {
push_stmt_exprs(s, out);
}
}
Stmt::Conditional(cond) => {
if let CondKind::Switch(e) = &cond.kind {
out.push(e);
}
for branch in &cond.branches {
out.extend(branch.condition.as_ref());
for s in &branch.body.stmts {
push_stmt_exprs(s, out);
}
}
}
Stmt::Sequence(seq) => {
for branch in &seq.branches {
for s in &branch.body.stmts {
push_stmt_exprs(s, out);
}
}
}
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => out.push(e),
Stmt::EndOfLine | Stmt::EndElementRun => {}
Stmt::LogicBlock(lb) => {
for s in &lb.stmts {
push_block_stmt_exprs(s, out);
}
}
Stmt::Await(a) => out.extend(a.condition.as_ref()),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefArgExpr {
pub ptr: Provenance,
pub operand: Box<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructLiteral {
pub ptr: Provenance,
pub shape: Name,
pub fields: Vec<(Name, Expr)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldAccessExpr {
pub ptr: Provenance,
pub base: Box<Expr>,
pub field: Name,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArrayLiteral {
pub ptr: Provenance,
pub elements: Vec<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MapLiteral {
pub ptr: Provenance,
pub entries: Vec<(Expr, Expr)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InfixExpr {
pub ptr: Provenance,
pub lhs: Box<Expr>,
pub op: InfixOp,
pub rhs: Box<Expr>,
}
impl InfixExpr {
#[must_use]
pub fn new(ptr: Provenance, lhs: Expr, op: InfixOp, rhs: Expr) -> Self {
Self {
ptr,
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexExpr {
pub ptr: Provenance,
pub base: Box<Expr>,
pub index: Box<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeExpr {
pub ptr: Provenance,
pub start: Box<Expr>,
pub end: Box<Expr>,
pub inclusive: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FloatBits(pub u64);
impl FloatBits {
pub fn from_f64(f: f64) -> Self {
Self(f.to_bits())
}
pub fn to_f64(self) -> f64 {
f64::from_bits(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StringExpr {
pub parts: Vec<StringPart>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StringPart {
Literal(String),
Interpolation(Box<Expr>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PrefixOp {
Negate,
Not,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PostfixOp {
Increment,
Decrement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InfixOp {
Add,
Sub,
Mul,
Div,
Mod,
Intersect,
Eq,
NotEq,
Lt,
Gt,
LtEq,
GtEq,
And,
Or,
Has,
HasNot,
Coalesce,
}
#[must_use]
pub fn display_expr(expr: &Expr) -> String {
match expr {
Expr::Path(p) => {
let mut out = String::new();
for (i, seg) in p.segments.iter().enumerate() {
if i > 0 {
out.push('.');
}
out.push_str(&seg.text);
}
out
}
Expr::Int(n) => n.to_string(),
Expr::Float(f) => f.to_f64().to_string(),
Expr::Bool(b) => b.to_string(),
Expr::String(_) => "\"...\"".to_string(),
Expr::Null => "null".to_string(),
Expr::DivertTarget(p) => {
let mut out = "-> ".to_string();
for (i, seg) in p.segments.iter().enumerate() {
if i > 0 {
out.push('.');
}
out.push_str(&seg.text);
}
out
}
Expr::ListLiteral(_) => "(...)".to_string(),
Expr::Prefix(op, inner) => {
format!("{}{}", op.as_str(), display_expr(inner))
}
Expr::Infix(ie) => {
format!(
"{} {} {}",
display_expr(&ie.lhs),
ie.op.as_str(),
display_expr(&ie.rhs)
)
}
Expr::Postfix(inner, op) => {
format!("{}{}", display_expr(inner), op.as_str())
}
Expr::Call(path, _) => {
let mut name = String::new();
for (i, seg) in path.segments.iter().enumerate() {
if i > 0 {
name.push('.');
}
name.push_str(&seg.text);
}
format!("{name}(...)")
}
Expr::ArrayLiteral(_) => "#[...]".to_string(),
Expr::MapLiteral(_) => "#{...}".to_string(),
Expr::Index(idx) => format!("{}[{}]", display_expr(&idx.base), display_expr(&idx.index)),
Expr::StructLiteral(sl) => format!("{}#{{...}}", sl.shape.text),
Expr::FieldAccess(fa) => format!("{}.{}", display_expr(&fa.base), fa.field.text),
Expr::FnLiteral(fl) => {
let mut name = String::new();
for (i, seg) in fl.target.segments.iter().enumerate() {
if i > 0 {
name.push('.');
}
name.push_str(&seg.text);
}
if fl.args.is_empty() {
format!("#fn({name})")
} else {
format!("#fn({name}, ...)")
}
}
Expr::RefArg(ra) => format!("ref {}", display_expr(&ra.operand)),
Expr::Lambda(l) => {
let params = l
.params
.iter()
.map(|p| p.name.text.as_str())
.collect::<Vec<_>>()
.join(", ");
match &l.body {
LambdaBody::Expr(e) => format!("|{params}| {}", display_expr(e)),
LambdaBody::Block { .. } => format!("|{params}| {{ ... }}"),
}
}
Expr::Range(r) => {
let op = if r.inclusive { "..=" } else { ".." };
format!("{}{op}{}", display_expr(&r.start), display_expr(&r.end))
}
Expr::Fragment(_) => "{...}".to_string(),
}
}
impl PrefixOp {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Negate => "-",
Self::Not => "not ",
}
}
}
impl PostfixOp {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Increment => "++",
Self::Decrement => "--",
}
}
}
impl InfixOp {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Add => "+",
Self::Sub => "-",
Self::Mul => "*",
Self::Div => "/",
Self::Mod => "%",
Self::Intersect => "^",
Self::Eq => "==",
Self::NotEq => "!=",
Self::Lt => "<",
Self::Gt => ">",
Self::LtEq => "<=",
Self::GtEq => ">=",
Self::And => "&&",
Self::Or => "||",
Self::Has => "?",
Self::HasNot => "!?",
Self::Coalesce => "or",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VarDecl {
pub ptr: Provenance,
pub name: Name,
pub value: Expr,
pub is_local: bool,
pub annotation: Option<TypeExpr>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstDecl {
pub ptr: Provenance,
pub name: Name,
pub value: Expr,
pub annotation: Option<TypeExpr>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TempDecl {
pub ptr: Provenance,
pub name: Name,
pub value: Option<Expr>,
pub annotation: Option<TypeExpr>,
pub synthetic: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assignment {
pub ptr: Provenance,
pub target: Expr,
pub op: AssignOp,
pub value: Expr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AssignOp {
Set,
Add,
Sub,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListDecl {
pub ptr: Provenance,
pub name: Name,
pub members: Vec<ListMember>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListMember {
pub name: Name,
pub value: Option<i32>,
pub is_active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructDecl {
pub ptr: Provenance,
pub name: Name,
pub fields: Vec<StructFieldDecl>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructFieldDecl {
pub name: Name,
pub ty: TypeExpr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalDecl {
pub ptr: Provenance,
pub name: Name,
pub param_count: u8,
pub params: Vec<crate::ParamInfo>,
pub doc: Option<crate::host_manifest::DocBlock>,
pub visibility: Option<crate::VisibilityMark>,
pub was: Option<(String, TextRange)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncludeSite {
pub file_path: String,
pub ptr: Provenance,
}
#[cfg(test)]
mod lambda_body_tests {
use super::*;
use crate::FileId;
fn lambda_body_of(src: &str) -> LambdaBody {
let parsed = brink_syntax_native::parse(src);
assert!(parsed.errors().is_empty(), "{:?}", parsed.errors());
let tree = parsed.tree();
let (hir, _manifest, _diag) = crate::hir::lower_native::lower(FileId(0), &tree);
let Expr::Lambda(l) = &hir.variables[0].value else {
unreachable!("fixture's initializer is a lambda: {:?}", hir.variables[0]);
};
l.body.clone()
}
#[test]
fn a_single_expression_body_yields_the_same_one_expression_either_way() {
let body = lambda_body_of("var f = |x| x + 1\n");
assert_eq!(body.value_exprs().len(), 1);
assert_eq!(body.all_exprs().len(), 1);
}
#[test]
fn a_braced_body_hides_its_statements_from_value_exprs_but_not_all_exprs() {
let body = lambda_body_of("var f = ||: int {\n let a = 1;\n let b = 2;\n 3\n};\n");
assert_eq!(body.value_exprs().len(), 1, "just the `3` tail");
let all = body.all_exprs();
assert_eq!(all.len(), 3, "{all:?}");
assert_eq!(all[2], &Expr::Int(3), "the tail comes last: {all:?}");
}
#[test]
fn a_statement_terminated_body_yields_nothing_from_value_exprs() {
let body = lambda_body_of("var f = ||: int {\n return 7;\n};\n");
assert!(body.value_exprs().is_empty());
assert_eq!(body.all_exprs().len(), 1, "the `return`'s operand");
}
#[test]
fn nested_statement_bodies_are_flattened() {
let body = lambda_body_of(
"var f = ||: int {\n if 1 == 1 {\n let a = 2;\n } else {\n let b = 3;\n }\n 4\n};\n",
);
let all = body.all_exprs();
assert_eq!(all.len(), 4, "{all:?}");
}
}
#[cfg(test)]
mod conventions_projection_tests {
use super::*;
fn name(text: &str) -> Name {
Name {
text: text.to_string(),
range: TextRange::default(),
}
}
fn decl(name_text: &str, order: i64, block: bool, attach: Option<&str>) -> ClaimHandlerDecl {
ClaimHandlerDecl {
name: name(name_text),
annotation: TextRange::default(),
params: Vec::new(),
pattern: format!("^{name_text}$"),
block,
order,
attach: attach.map(str::to_string),
}
}
fn no_structs() -> BTreeMap<String, Vec<ConventionAttachField>> {
BTreeMap::new()
}
fn dispatch_decl(name_text: &str, block: bool) -> DispatchHandlerDecl {
dispatch_decl_aliased(name_text, name_text, block)
}
fn dispatch_decl_aliased(
dispatch_name_text: &str,
name_text: &str,
block: bool,
) -> DispatchHandlerDecl {
DispatchHandlerDecl {
dispatch_name: dispatch_name_text.to_string(),
name: name(name_text),
annotation: TextRange::default(),
params: Vec::new(),
pattern: format!("^{name_text}$"),
block,
}
}
fn field(name: &str, ty: SchemaTypeShape) -> ConventionAttachField {
ConventionAttachField {
name: name.to_string(),
ty,
}
}
#[test]
fn from_decls_preserves_input_order() {
let decls = vec![
decl("exterior", 20, false, None),
decl("interior", 10, false, None),
];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
let names: Vec<&str> = projection
.entries
.iter()
.map(|e| e.name.text.as_str())
.collect();
assert_eq!(names, vec!["exterior", "interior"]);
}
#[test]
fn from_decls_preserves_dispatch_input_order_too() {
let dispatch_decls = vec![dispatch_decl("walkie", false), dispatch_decl("radio", true)];
let projection = ConventionsProjection::from_decls(&[], &dispatch_decls, &no_structs());
let names: Vec<&str> = projection
.dispatch
.iter()
.map(|e| e.name.text.as_str())
.collect();
assert_eq!(
names,
vec!["walkie", "radio"],
"dispatch rows must not be re-sorted behind the caller's back either"
);
}
#[test]
fn dispatch_row_carries_the_alias_as_dispatch_name() {
let dispatch_decls = vec![dispatch_decl_aliased("walkie", "tally", false)];
let projection = ConventionsProjection::from_decls(&[], &dispatch_decls, &no_structs());
assert_eq!(projection.dispatch[0].name.text, "tally");
assert_eq!(
projection.dispatch[0].dispatch_name.as_deref(),
Some("walkie")
);
}
#[test]
fn a_bang_dispatch_only_project_still_projects_a_row() {
let dispatch_decls = vec![dispatch_decl("radio", false)];
let projection = ConventionsProjection::from_decls(&[], &dispatch_decls, &no_structs());
assert!(
projection.entries.is_empty(),
"no @[convention] handler was declared"
);
assert_eq!(
projection.dispatch.len(),
1,
"the one @[element] handler must get a row"
);
assert_eq!(projection.dispatch[0].name.text, "radio");
}
#[test]
fn dispatch_rows_carry_no_attach_and_a_real_mode() {
let dispatch_decls = vec![dispatch_decl("radio", false), dispatch_decl("cue", true)];
let projection = ConventionsProjection::from_decls(&[], &dispatch_decls, &no_structs());
assert_eq!(projection.dispatch[0].attach, None);
assert_eq!(projection.dispatch[0].mode, ConventionMode::Attach);
assert_eq!(projection.dispatch[1].attach, None);
assert_eq!(projection.dispatch[1].mode, ConventionMode::Wrap);
}
#[test]
fn dispatch_rows_carry_call_disposition() {
let projection =
ConventionsProjection::from_decls(&[], &[dispatch_decl("radio", false)], &no_structs());
assert_eq!(projection.dispatch[0].disposition, ElementDisposition::Call);
}
#[test]
fn block_flag_becomes_wrap_mode_and_its_absence_becomes_attach_mode() {
let decls = vec![
decl("cue", 10, true, None),
decl("interior", 20, false, None),
];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
assert_eq!(projection.entries[0].mode, ConventionMode::Wrap);
assert_eq!(projection.entries[1].mode, ConventionMode::Attach);
}
#[test]
fn attach_schema_resolves_to_the_declared_fields_and_types() {
let decls = vec![decl("cue", 10, false, Some("Cue"))];
let mut structs = no_structs();
structs.insert(
"Cue".to_string(),
vec![
field("speaker", SchemaTypeShape::Named("string".to_string())),
field("voiceover", SchemaTypeShape::Named("bool".to_string())),
field("offscreen", SchemaTypeShape::Named("bool".to_string())),
],
);
let projection = ConventionsProjection::from_decls(&decls, &[], &structs);
assert_eq!(
projection.entries[0].attach,
Some(ConventionAttachSchema::Resolved {
name: "Cue".to_string(),
fields: vec![
field("speaker", SchemaTypeShape::Named("string".to_string())),
field("voiceover", SchemaTypeShape::Named("bool".to_string())),
field("offscreen", SchemaTypeShape::Named("bool".to_string())),
],
})
);
}
#[test]
fn attach_schema_field_order_is_preserved_not_resorted() {
let decls = vec![decl("cue", 10, false, Some("Cue"))];
let mut structs = no_structs();
structs.insert(
"Cue".to_string(),
vec![
field("b_field", SchemaTypeShape::Named("bool".to_string())),
field("a_field", SchemaTypeShape::Named("string".to_string())),
],
);
let projection = ConventionsProjection::from_decls(&decls, &[], &structs);
let Some(ConventionAttachSchema::Resolved { fields, .. }) = &projection.entries[0].attach
else {
unreachable!(
"expected a resolved schema: {:?}",
projection.entries[0].attach
);
};
let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
assert_eq!(
names,
vec!["b_field", "a_field"],
"field order must not be re-sorted"
);
}
#[test]
fn attach_schema_unresolved_struct_name_is_flagged_not_dropped() {
let decls = vec![decl("cue", 10, false, Some("Ghost"))];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
assert_eq!(
projection.entries[0].attach,
Some(ConventionAttachSchema::Unresolved("Ghost".to_string()))
);
}
#[test]
fn no_attach_clause_projects_to_none() {
let decls = vec![decl("interior", 10, false, None)];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
assert_eq!(projection.entries[0].attach, None);
}
#[test]
fn every_entry_carries_the_call_disposition() {
let decls = vec![decl("interior", 10, false, None)];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
assert_eq!(projection.entries[0].disposition, ElementDisposition::Call);
}
#[test]
fn an_empty_decl_set_projects_to_an_empty_projection() {
let projection = ConventionsProjection::from_decls(&[], &[], &no_structs());
assert!(projection.entries.is_empty());
assert_eq!(projection, ConventionsProjection::default());
}
#[test]
fn order_and_pattern_are_carried_through_verbatim() {
let decls = vec![decl("interior", 42, false, None)];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
assert_eq!(projection.entries[0].order, 42);
assert_eq!(projection.entries[0].pattern, "^interior$");
}
#[test]
fn to_wire_carries_the_resolved_schema_losslessly() {
let decls = vec![decl("cue", 5, true, Some("Cue"))];
let mut structs = no_structs();
structs.insert(
"Cue".to_string(),
vec![field(
"speaker",
SchemaTypeShape::Named("string".to_string()),
)],
);
let projection = ConventionsProjection::from_decls(&decls, &[], &structs);
let wire = projection.to_wire();
assert_eq!(wire.entries.len(), 1);
assert_eq!(wire.entries[0].name, "cue");
assert_eq!(wire.entries[0].mode, brink_format::ConventionModeDef::Wrap);
assert_eq!(
wire.entries[0].attach,
Some(brink_format::ConventionAttachDef::Resolved {
name: "Cue".to_string(),
fields: vec![brink_format::ConventionAttachFieldDef {
name: "speaker".to_string(),
ty: brink_format::SchemaTypeDef::Named("string".to_string()),
}],
})
);
}
#[test]
fn to_wire_carries_unresolved_attach_as_unresolved_not_none() {
let decls = vec![decl("cue", 5, false, Some("Ghost"))];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
let wire = projection.to_wire();
assert_eq!(
wire.entries[0].attach,
Some(brink_format::ConventionAttachDef::Unresolved(
"Ghost".to_string()
))
);
}
#[test]
fn to_wire_round_trips_through_the_inkb_codec() {
let decls = vec![
decl("cue", 5, true, Some("Cue")),
decl("interior", 10, false, None),
];
let mut structs = no_structs();
structs.insert(
"Cue".to_string(),
vec![field(
"speaker",
SchemaTypeShape::Named("string".to_string()),
)],
);
let wire = ConventionsProjection::from_decls(&decls, &[], &structs).to_wire();
let mut buf = Vec::new();
brink_format::write_conventions_projection(&wire, &mut buf);
let mut offset = 0;
let decoded = brink_format::read_conventions_projection(&buf, &mut offset)
.expect("decode the wire form this crate just encoded");
assert_eq!(decoded, wire);
}
use crate::dialect::{TemplateEntry, Templates, TransitionAction, TransitionRow};
fn character_row() -> TransitionRow {
TransitionRow {
on: "character".to_string(),
key: "Tab".to_string(),
has_content: Some(true),
action: TransitionAction::Convert {
kind: "dialogue".to_string(),
},
hint: None,
}
}
#[test]
fn with_succession_accepts_rows_keyed_to_a_declared_convention_kind() {
let decls = vec![
decl("character", 1, false, None),
decl("dialogue", 2, false, None),
];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs());
let templates = Templates {
entries: vec![TemplateEntry {
kind: "character".to_string(),
label: "Character cue".to_string(),
picker_key: Some("@".to_string()),
blank_tab: true,
}],
};
let attached = projection
.with_succession(vec![character_row()], templates.clone())
.expect("both rows key off declared convention kinds");
assert_eq!(attached.transitions, vec![character_row()]);
assert_eq!(attached.templates, templates);
}
#[test]
fn with_succession_accepts_reserved_structural_kinds() {
let projection = ConventionsProjection::from_decls(&[], &[], &no_structs());
let row = TransitionRow {
on: "narrative".to_string(),
key: "Enter".to_string(),
has_content: None,
action: TransitionAction::Newline,
hint: None,
};
let attached = projection
.with_succession(vec![row], Templates::default())
.expect("`narrative` is a reserved structural kind");
assert_eq!(attached.transitions.len(), 1);
}
#[test]
fn with_succession_rejects_a_kind_this_projection_never_declared() {
let projection = ConventionsProjection::from_decls(
&[decl("character", 1, false, None)],
&[],
&no_structs(),
);
let row = TransitionRow {
on: "parenthetical".to_string(),
key: "Tab".to_string(),
has_content: None,
action: TransitionAction::Strip,
hint: None,
};
let errors = projection
.with_succession(vec![row], Templates::default())
.expect_err(
"`parenthetical` is declared by neither this projection nor reserved-structural",
);
assert_eq!(
errors,
vec![crate::dialect::DialectError::TransitionUndeclaredKind(
"parenthetical".to_string()
)]
);
}
#[test]
fn with_succession_rejects_an_undeclared_convert_target() {
let projection = ConventionsProjection::from_decls(
&[decl("character", 1, false, None)],
&[],
&no_structs(),
);
let row = TransitionRow {
on: "character".to_string(),
key: "Tab".to_string(),
has_content: None,
action: TransitionAction::Convert {
kind: "dialogue".to_string(),
},
hint: None,
};
let errors = projection
.with_succession(vec![row], Templates::default())
.expect_err("`dialogue` is never declared");
assert_eq!(
errors,
vec![crate::dialect::DialectError::TransitionUndeclaredKind(
"dialogue".to_string()
)]
);
}
#[test]
fn with_succession_rejects_a_template_entry_for_an_undeclared_kind() {
let projection = ConventionsProjection::from_decls(&[], &[], &no_structs());
let templates = Templates {
entries: vec![TemplateEntry {
kind: "character".to_string(),
label: "Character cue".to_string(),
picker_key: None,
blank_tab: false,
}],
};
let errors = projection
.with_succession(Vec::new(), templates)
.expect_err("`character` is never declared");
assert_eq!(
errors,
vec![crate::dialect::DialectError::TemplateUndeclaredKind(
"character".to_string()
)]
);
}
#[test]
fn to_wire_does_not_carry_succession_rows() {
let decls = vec![
decl("character", 1, false, None),
decl("dialogue", 2, false, None),
];
let projection = ConventionsProjection::from_decls(&decls, &[], &no_structs())
.with_succession(
vec![character_row()],
Templates {
entries: vec![TemplateEntry {
kind: "character".to_string(),
label: "Character cue".to_string(),
picker_key: Some("@".to_string()),
blank_tab: true,
}],
},
)
.expect("keyed to a declared convention kind");
assert_eq!(projection.transitions.len(), 1, "attached in-process");
assert_eq!(projection.templates.entries.len(), 1, "attached in-process");
let wire = projection.to_wire();
assert_eq!(wire.entries.len(), 2);
let mut buf = Vec::new();
brink_format::write_conventions_projection(&wire, &mut buf);
let mut offset = 0;
let decoded = brink_format::read_conventions_projection(&buf, &mut offset)
.expect("decode the wire form this crate just encoded");
assert_eq!(decoded, wire);
}
}