Add match blocks capable of matching against enum variants

This commit is contained in:
Olivier 'reivilibre' 2023-08-07 22:44:23 +01:00
parent 062bd8abd7
commit 2918260c5a
10 changed files with 324 additions and 5 deletions

1
Cargo.lock generated
View File

@ -783,6 +783,7 @@ dependencies = [
"itertools", "itertools",
"pollster", "pollster",
"thiserror", "thiserror",
"tracing",
"walkdir", "walkdir",
] ]

View File

@ -13,6 +13,7 @@ pub enum Block {
ComponentElement(ComponentElement), ComponentElement(ComponentElement),
IfBlock(IfBlock), IfBlock(IfBlock),
ForBlock(ForBlock), ForBlock(ForBlock),
MatchBlock(MatchBlock),
Text(StringExpr), Text(StringExpr),
DefineExpandSlot(DefineExpandSlot), DefineExpandSlot(DefineExpandSlot),
DefineFragment(DefineFragment), DefineFragment(DefineFragment),
@ -53,6 +54,25 @@ pub struct ForBlock {
pub loc: Locator, pub loc: Locator,
} }
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct MatchBlock {
pub matchable: Expression,
pub arms: Vec<(MatchBinding, Vec<Block>)>,
pub loc: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum MatchBinding {
/// `Some(x) =>`
TupleVariant { name: IStr, pieces: Vec<Binding> },
/// `None =>`
UnitVariant { name: IStr },
/// `_ =>`
Ignore,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum Binding { pub enum Binding {
Variable(IStr), Variable(IStr),

View File

@ -12,6 +12,7 @@ BlockContent = _{
Text | Text |
DefineExpandSlot | DefineExpandSlot |
ForBlock | ForBlock |
MatchBlock |
DefineFragment DefineFragment
} }
@ -55,6 +56,25 @@ EmptyForBlock = {
PEEK_ALL ~ "empty" ~ lineEnd ~ NewBlock PEEK_ALL ~ "empty" ~ lineEnd ~ NewBlock
} }
MatchBlock = {
"match" ~ ws+ ~ Expr ~ lineEnd ~
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ MatchArm ~
(PEEK_ALL ~ MatchArm)* ~ DROP
}
MatchArm = {
MatchBinding ~ ws* ~ "=>" ~ lineEnd ~ NewBlock
}
MatchBinding = {
MBTupleVariant | IgnoreBinding | MBUnitVariant
}
MBTupleVariant = {
Identifier ~ ws* ~ "(" ~ ws* ~ (Binding ~ ws* ~ ("," ~ ws* ~ Binding ~ ws*)*)? ~ ")"
}
MBUnitVariant = {
Identifier
}
DefineExpandSlot = { DefineExpandSlot = {
SlotOptional? ~ "slot" ~ ws+ ~ ":" ~ Identifier ~ lineEnd SlotOptional? ~ "slot" ~ ws+ ~ ":" ~ Identifier ~ lineEnd
} }

View File

@ -2,7 +2,7 @@
use crate::ast::{ use crate::ast::{
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock, Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock,
HtmlElement, IfBlock, StringExpr, StringPiece, Template, HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, Template,
}; };
use crate::{intern, IStr, Locator}; use crate::{intern, IStr, Locator};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -374,6 +374,50 @@ impl HornbeamParser {
})) }))
} }
fn MatchBlock(input: Node) -> PCResult<Block> {
let loc = nodeloc(&input);
let (expr, arms) = match_nodes!(input.into_children();
[Expr(expr), MatchArm(arms)..] => (expr, arms.collect())
);
Ok(Block::MatchBlock(MatchBlock {
matchable: expr,
arms,
loc,
}))
}
fn MatchArm(input: Node) -> PCResult<(MatchBinding, Vec<Block>)> {
Ok(match_nodes!(input.into_children();
[MatchBinding(binding), blocks..] => (
binding,
HornbeamParser::helper_blocks(blocks)?,
)
))
}
fn MatchBinding(input: Node) -> PCResult<MatchBinding> {
Ok(match_nodes!(input.into_children();
[MBTupleVariant(mb)] => mb,
[MBUnitVariant(mb)] => mb,
[IgnoreBinding(_)] => MatchBinding::Ignore,
))
}
fn MBTupleVariant(input: Node) -> PCResult<MatchBinding> {
Ok(match_nodes!(input.into_children();
[Identifier(name), Binding(bindings)..] => {
MatchBinding::TupleVariant { name, pieces: bindings.collect() }
}
))
}
fn MBUnitVariant(input: Node) -> PCResult<MatchBinding> {
let name = Self::Identifier(input.into_children().single()?)?;
Ok(MatchBinding::UnitVariant { name })
}
fn Binding(input: Node) -> PCResult<Binding> { fn Binding(input: Node) -> PCResult<Binding> {
Ok(match_nodes!(input.into_children(); Ok(match_nodes!(input.into_children();
[VarBinding(b)] => b, [VarBinding(b)] => b,
@ -421,6 +465,7 @@ impl HornbeamParser {
Rule::Text => Some(HornbeamParser::Text(input)?), Rule::Text => Some(HornbeamParser::Text(input)?),
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?), Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?), Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?),
Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?),
Rule::DefineFragment => Some(HornbeamParser::DefineFragment(input)?), Rule::DefineFragment => Some(HornbeamParser::DefineFragment(input)?),
Rule::DefineExpandSlot => Some(HornbeamParser::DefineExpandSlot(input)?), Rule::DefineExpandSlot => Some(HornbeamParser::DefineExpandSlot(input)?),
Rule::EOI => None, Rule::EOI => None,
@ -540,6 +585,22 @@ empty
.unwrap()); .unwrap());
} }
#[test]
fn match_blocks() {
assert_yaml_snapshot!(parse_template(
r#"
if $x
match $y
None =>
"None"
Some($z) =>
"Some(${$z})"
"#,
"inp"
)
.unwrap());
}
#[test] #[test]
fn fragments_and_slots() { fn fragments_and_slots() {
assert_yaml_snapshot!(parse_template( assert_yaml_snapshot!(parse_template(

View File

@ -0,0 +1,53 @@
---
source: hornbeam_grammar/src/parser.rs
expression: "parse_template(r#\"\nif $x\n match $y\n None =>\n \"None\"\n Some($z) =>\n \"Some(${$z})\"\n \"#,\n \"inp\").unwrap()"
---
blocks:
- IfBlock:
condition:
Variable:
name: x
loc:
filename: inp
line: 2
column: 4
blocks:
- MatchBlock:
matchable:
Variable:
name: y
loc:
filename: inp
line: 3
column: 11
arms:
- - UnitVariant:
name: None
- - Text:
pieces:
- Literal: None
- - TupleVariant:
name: Some
pieces:
- Variable: z
- - Text:
pieces:
- Literal: Some(
- Interpolation:
Variable:
name: z
loc:
filename: inp
line: 7
column: 21
- Literal: )
loc:
filename: inp
line: 3
column: 5
else_blocks: []
loc:
filename: inp
line: 2
column: 1

View File

@ -22,6 +22,8 @@ itertools = "0.10.5"
pollster = "0.3.0" pollster = "0.3.0"
tracing = "0.1.37"
[features] [features]
default = ["fluent"] default = ["fluent"]
fluent = ["fluent-templates"] fluent = ["fluent-templates"]

View File

@ -1,9 +1,9 @@
use crate::interface::{LocalisationSystem, OutputSystem}; use crate::interface::{LocalisationSystem, OutputSystem};
use crate::InterpreterError; use crate::InterpreterError;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use bevy_reflect::{FromReflect, Reflect, ReflectRef}; use bevy_reflect::{FromReflect, Reflect, ReflectRef, VariantType};
use fluent_templates::lazy_static::lazy_static; use fluent_templates::lazy_static::lazy_static;
use hornbeam_grammar::ast::Binding; use hornbeam_grammar::ast::{Binding, MatchBinding};
use hornbeam_grammar::Locator; use hornbeam_grammar::Locator;
use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece}; use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece};
use itertools::Itertools; use itertools::Itertools;
@ -13,6 +13,7 @@ use std::collections::BTreeMap;
use std::fmt::Write; use std::fmt::Write;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use tracing::warn;
pub(crate) struct FilledSlot<'a> { pub(crate) struct FilledSlot<'a> {
pub scope_idx: usize, pub scope_idx: usize,
@ -249,7 +250,7 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
} }
} }
} }
other => { _ => {
return Err(InterpreterError::TypeError { return Err(InterpreterError::TypeError {
context: "For".to_string(), context: "For".to_string(),
conflict: format!("not iterable reflective: {reflective:?}."), conflict: format!("not iterable reflective: {reflective:?}."),
@ -268,6 +269,131 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
} }
} }
} }
StepDef::Match { matchable, arms } => {
let matchable_evaled =
self.evaluate_expression(scope_idx, matchable, &step.locator)?;
for (arm_binding, arm_steps) in arms {
// if this arm's binding matches, then bind the variable...
match arm_binding {
MatchBinding::UnitVariant { name } => {
match &matchable_evaled {
Value::Reflective(reflective) => match reflective.reflect_ref() {
ReflectRef::Enum(reflenum) => {
// Check if the variant name matches!
if reflenum.variant_name() != name as &str {
continue;
}
}
_ => {
warn!("trying to `match` weird reflective: {reflective:?} vs {name} at {}", step.locator);
continue;
}
},
_ => {
warn!(
"trying to `match` non-reflective vs {name} at {}",
step.locator
);
continue;
}
}
}
MatchBinding::TupleVariant { name, pieces } => {
match &matchable_evaled {
Value::Reflective(reflective) => match reflective.reflect_ref() {
ReflectRef::Enum(reflenum) => {
// Check if the variant name matches!
if reflenum.variant_name() != name as &str {
continue;
}
// Bind the pieces!
if pieces.len() != reflenum.field_len() {
return Err(InterpreterError::TypeError {
context: "Match".to_string(),
conflict: format!("wrong number of tuple pieces on {name}: wanted {}, found {}", pieces.len(), reflenum.field_len()),
location: step.locator.clone(),
});
}
if reflenum.variant_type() != VariantType::Tuple {
return Err(InterpreterError::TypeError {
context: "Match".to_string(),
conflict: format!(
"not a tuple variant: {name} is a {:?}",
reflenum.variant_type()
),
location: step.locator.clone(),
});
}
for (piece, field) in
pieces.iter().zip(reflenum.iter_fields())
{
// TODO duplicated code. Should probably make some 'Binder' tool that also makes it easy to unbind afterwards!
match piece {
Binding::Variable(var) => {
self.scopes[scope_idx].variables.insert(
String::from(var as &str),
// TODO would be nice to avoid this clone!
Value::from_reflect(
field.value().clone_value(),
),
);
}
Binding::Ignore => {
// nop.
}
}
}
}
_ => {
warn!("trying to `match` weird reflective: {reflective:?} vs {name}(...) at {}", step.locator);
continue;
}
},
_ => {
warn!(
"trying to `match` non-reflective vs {name}(...) at {}",
step.locator
);
continue;
}
}
}
MatchBinding::Ignore => {
// always matches: no variable to bind, no conditions to check!
}
};
// and then execute the instructions
self.run_steps(scope_idx, arm_steps).await?;
// and then unbind the variables.
match arm_binding {
MatchBinding::TupleVariant { pieces, .. } => {
for piece in pieces {
// TODO duplicated code. Should probably make some 'Binder' tool that also makes it easy to unbind afterwards!
match piece {
Binding::Variable(var) => {
self.scopes[scope_idx].variables.remove(var as &str);
}
Binding::Ignore => {
// nop.
}
}
}
}
MatchBinding::UnitVariant { .. } | MatchBinding::Ignore => {
// no variables to unbind
}
};
// (don't fall through to other arms since we matched this one)
break;
}
}
StepDef::Call { name, args, slots } => { StepDef::Call { name, args, slots } => {
let mut evaled_args = BTreeMap::new(); let mut evaled_args = BTreeMap::new();
for (key, expr) in args { for (key, expr) in args {

View File

@ -96,6 +96,13 @@ fn pull_out_entrypoints_from_block<'a>(
pull_out_entrypoints_from_block(child, template_name, target)?; pull_out_entrypoints_from_block(child, template_name, target)?;
} }
} }
Block::MatchBlock(matchb) => {
for (_arm_binding, arm_blocks) in &mut matchb.arms {
for child in arm_blocks {
pull_out_entrypoints_from_block(child, template_name, target)?;
}
}
}
Block::DefineFragment(frag) => { Block::DefineFragment(frag) => {
// First process children // First process children
for child in &mut frag.blocks { for child in &mut frag.blocks {
@ -349,6 +356,26 @@ fn compile_ast_block_to_steps<'a>(
locator: forb.loc.clone(), locator: forb.loc.clone(),
}); });
} }
Block::MatchBlock(matchb) => {
let mut arms_instrs = Vec::new();
for (arm_binding, arm_blocks) in &matchb.arms {
let mut this_arm_instrs = Vec::new();
for b in arm_blocks {
compile_ast_block_to_steps(b, &mut this_arm_instrs)?;
}
arms_instrs.push((arm_binding.clone(), this_arm_instrs));
}
instructions.push(Step {
def: StepDef::Match {
matchable: matchb.matchable.clone(),
arms: arms_instrs,
},
locator: matchb.loc.clone(),
})
}
Block::Text(text) => { Block::Text(text) => {
for piece in &text.pieces { for piece in &text.pieces {
match piece { match piece {

View File

@ -1,4 +1,4 @@
use hornbeam_grammar::ast::Binding; use hornbeam_grammar::ast::{Binding, MatchBinding};
pub use hornbeam_grammar::ast::{Expression, StringPiece}; pub use hornbeam_grammar::ast::{Expression, StringPiece};
use hornbeam_grammar::{IStr, Locator}; use hornbeam_grammar::{IStr, Locator};
use serde::Serialize; use serde::Serialize;
@ -39,6 +39,10 @@ pub enum StepDef {
body_steps: Vec<Step>, body_steps: Vec<Step>,
empty_steps: Vec<Step>, empty_steps: Vec<Step>,
}, },
Match {
matchable: Expression,
arms: Vec<(MatchBinding, Vec<Step>)>,
},
Call { Call {
name: IStr, name: IStr,
args: BTreeMap<IStr, Expression>, args: BTreeMap<IStr, Expression>,

View File

@ -70,6 +70,11 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
apply_peephole_pass(body_steps, pass); apply_peephole_pass(body_steps, pass);
apply_peephole_pass(empty_steps, pass); apply_peephole_pass(empty_steps, pass);
} }
StepDef::Match { arms, .. } => {
for (_, arm_steps) in arms {
apply_peephole_pass(arm_steps, pass);
}
}
StepDef::Call { slots, .. } => { StepDef::Call { slots, .. } => {
for slot_steps in slots.values_mut() { for slot_steps in slots.values_mut() {
apply_peephole_pass(slot_steps, pass); apply_peephole_pass(slot_steps, pass);