use rand::Rng;
use std::collections::{HashMap, HashSet};
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use crate::types::{Pos, Sym};
use crate::type_driven_grammar::LanguageConfig;
use crate::semantic_types::{SemanticType, pos_to_semantic_type};
use crate::scale::ScaleDefinition;
#[derive(Clone, Debug)]
pub struct Production {
pub symbols: Vec<Sym>,
pub refinements: Vec<Option<String>>, pub weight: f64,
}
#[derive(Clone, Debug)]
pub struct GrammarRule {
pub productions: Vec<Production>,
}
#[derive(Clone, Debug)]
pub struct Grammar {
pub(crate) rules: HashMap<String, GrammarRule>,
pub(crate) language_config: Option<LanguageConfig>,
#[allow(dead_code)]
pub(crate) dialect: Option<String>,
pub(crate) payload_separator: String,
pub(crate) dot_is_punctuation: bool,
pub(crate) payload_line_width: Option<usize>,
pub(crate) max_k: Option<usize>,
pub(crate) codec: String,
pub(crate) morphology: Option<String>,
}
#[derive(Clone, Debug)]
pub struct SequenceWithProbability {
pub sequence: Vec<crate::types::Pos>,
pub refinements: Vec<Option<String>>, pub probability: f64,
pub word_slot_pos: HashSet<Pos>,
}
impl SequenceWithProbability {
pub fn new(sequence: Vec<Pos>, refinements: Vec<Option<String>>, probability: f64) -> Self {
let word_slot_pos = sequence.iter()
.filter(|&&pos| !matches!(pos, Pos::Dot | Pos::Prefix | Pos::Aux | Pos::Cop | Pos::To))
.copied()
.collect();
SequenceWithProbability { sequence, refinements, probability, word_slot_pos }
}
}
#[derive(Clone, Debug)]
pub struct DialectConfig {
pub grammar: Grammar,
language: String,
dialect: String,
payload_wl: String,
cover_wl: String,
payload_language: Option<String>,
scale: Option<ScaleDefinition>,
}
impl DialectConfig {
pub fn from_language_dialect(language: &str, dialect: &str) -> Result<Self, Box<dyn std::error::Error>> {
let grammar = Grammar::from_language_dialect(language, dialect)?;
let (mut payload_wl, payload_language, cover_wl) = Self::parse_wordlist_refs(language, dialect);
let scale = Self::parse_scale_ref(language, dialect);
if let Some(ref scale_def) = scale {
if payload_wl == "default" {
payload_wl = dialect.to_string();
}
let chromatic = crate::generator::load_payload_words_for_wordlist(language, "default")
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let derived = crate::scale::filter_payload_by_scale(&chromatic, scale_def)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
crate::generator::inject_scale_payload(language, &payload_wl, derived)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
}
Ok(DialectConfig {
grammar,
language: language.to_string(),
dialect: dialect.to_string(),
payload_wl,
cover_wl,
payload_language,
scale,
})
}
pub fn language(&self) -> &str {
&self.language
}
pub fn dialect(&self) -> &str {
&self.dialect
}
pub fn payload_wordlist(&self) -> &str {
&self.payload_wl
}
pub fn scale(&self) -> Option<&ScaleDefinition> {
self.scale.as_ref()
}
pub fn cover_wordlist(&self) -> &str {
&self.cover_wl
}
pub fn payload_language(&self) -> &str {
self.payload_language.as_deref().unwrap_or(&self.language)
}
pub fn n_refinement_tag(&self) -> Option<String> {
if let Some(ref scale_def) = self.scale {
return Some(crate::scale::derive_refinement_tag(scale_def));
}
None
}
pub fn with_payload_wordlist(mut self, wordlist: &str) -> Self {
self.payload_wl = wordlist.to_string();
let cover_key = format!("{}/cover_{}.yaml", self.language, wordlist);
if crate::generator::data::get_embedded_yaml(&cover_key).is_some() {
self.cover_wl = wordlist.to_string();
}
self
}
pub fn with_cover_wordlist(mut self, wordlist: &str) -> Self {
self.cover_wl = wordlist.to_string();
self
}
pub fn wordlist_filenames(&self) -> (String, String) {
let lang = self.payload_language.as_deref().unwrap_or(&self.language);
crate::generator::data::wordlist_filenames(lang, &self.payload_wl)
}
pub fn available_dialects(language: &str) -> Vec<String> {
let mut dialects = vec!["body".to_string()];
let yaml_content: Option<String> = crate::generator::data::get_embedded_yaml(
&format!("{}/grammar.yaml", language),
).map(|s| s.to_string()).or_else(|| match language {
"latin" => Some(include_str!("../languages/latin/grammar.yaml").to_string()),
"english" => Some(include_str!("../languages/english/grammar.yaml").to_string()),
_ => None,
});
#[cfg(not(target_arch = "wasm32"))]
let yaml_content = yaml_content.or_else(|| {
crate::generator::data::find_language_file(language, "grammar.yaml")
.and_then(|path| std::fs::read_to_string(&path).ok())
});
if let Some(content) = yaml_content {
if let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(&content) {
if let Some(grammar) = doc.get("grammar") {
if let Some(dialect_map) = grammar.get("dialects").and_then(|d| d.as_mapping()) {
for (key, _) in dialect_map {
if let Some(name) = key.as_str() {
if name != "body" {
dialects.push(name.to_string());
}
}
}
}
}
}
}
dialects
}
fn parse_wordlist_refs(language: &str, dialect: &str) -> (String, Option<String>, String) {
if dialect == "body" {
return ("default".to_string(), None, "default".to_string());
}
let yaml_content = crate::generator::data::get_embedded_yaml(
&format!("{}/grammar.yaml", language),
).or_else(|| match language {
"latin" => Some(include_str!("../languages/latin/grammar.yaml")),
"english" => Some(include_str!("../languages/english/grammar.yaml")),
_ => None,
});
fn extract_wordlist_refs(dialects: &serde_yaml::Value, dialect: &str) -> Option<(String, Option<String>, String)> {
let dialect_data = dialects.get(dialect)?;
let parent_refs = dialect_data.get("parent")
.and_then(|p| p.as_str())
.and_then(|parent| extract_wordlist_refs(dialects, parent));
let (default_payload, default_payload_lang, default_cover) = parent_refs
.unwrap_or_else(|| ("default".to_string(), None, "default".to_string()));
let payload = dialect_data.get("payload_wordlist")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or(default_payload);
let payload_lang = dialect_data.get("payload_language")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or(default_payload_lang);
let cover = dialect_data.get("cover_wordlist")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or(default_cover);
Some((payload, payload_lang, cover))
}
if let Some(content) = yaml_content {
if let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(content) {
if let Some(grammar) = doc.get("grammar") {
if let Some(dialects) = grammar.get("dialects") {
if let Some(refs) = extract_wordlist_refs(dialects, dialect) {
return refs;
}
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(content) = crate::generator::data::find_language_file(language, "grammar.yaml")
.and_then(|path| std::fs::read_to_string(&path).ok())
{
if let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(&content) {
if let Some(grammar) = doc.get("grammar") {
if let Some(dialects) = grammar.get("dialects") {
if let Some(refs) = extract_wordlist_refs(dialects, dialect) {
return refs;
}
}
}
}
}
}
("default".to_string(), None, "default".to_string())
}
fn parse_scale_ref(language: &str, dialect: &str) -> Option<ScaleDefinition> {
if dialect == "body" {
return None;
}
fn extract_scale(dialects: &serde_yaml::Value, dialect: &str) -> Option<ScaleDefinition> {
let dialect_data = dialects.get(dialect)?;
if let Some(scale_value) = dialect_data.get("scale") {
if let Ok(scale) = crate::scale::parse_scale_definition(scale_value) {
return Some(scale);
}
}
dialect_data.get("parent")
.and_then(|p| p.as_str())
.and_then(|parent| extract_scale(dialects, parent))
}
let yaml_content = crate::generator::data::get_embedded_yaml(
&format!("{}/grammar.yaml", language),
).or_else(|| match language {
"latin" => Some(include_str!("../languages/latin/grammar.yaml")),
"english" => Some(include_str!("../languages/english/grammar.yaml")),
_ => None,
});
if let Some(content) = yaml_content {
if let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(content) {
if let Some(grammar) = doc.get("grammar") {
if let Some(dialects) = grammar.get("dialects") {
if let Some(scale) = extract_scale(dialects, dialect) {
return Some(scale);
}
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(content) = crate::generator::data::find_language_file(language, "grammar.yaml")
.and_then(|path| std::fs::read_to_string(&path).ok())
{
if let Ok(doc) = serde_yaml::from_str::<serde_yaml::Value>(&content) {
if let Some(grammar) = doc.get("grammar") {
if let Some(dialects) = grammar.get("dialects") {
if let Some(scale) = extract_scale(dialects, dialect) {
return Some(scale);
}
}
}
}
}
}
None
}
}
impl Grammar {
pub fn payload_separator(&self) -> &str {
&self.payload_separator
}
pub fn dot_is_punctuation(&self) -> bool {
self.dot_is_punctuation
}
pub fn payload_line_width(&self) -> Option<usize> {
self.payload_line_width
}
pub fn max_k(&self) -> Option<usize> {
self.max_k
}
pub fn codec(&self) -> &str {
&self.codec
}
pub fn morphology(&self) -> Option<&str> {
self.morphology.as_deref()
}
pub fn default() -> Result<Self, Box<dyn std::error::Error>> {
Self::from_language_dialect("latin", "body")
}
fn build_rules_from_cfg_productions(yaml_content: &str, dialect: &str) -> Result<HashMap<String, GrammarRule>, Box<dyn std::error::Error>> {
use serde_yaml::Value;
let doc: Value = serde_yaml::from_str(yaml_content)?;
let rule_name_map: HashMap<&str, &str> = [
("verb_phrase", "VP"),
("noun_phrase", "NP"),
("prepositional_phrase", "PP"),
("sentence", "S"),
("sentence_content", "SContent"),
("vp_with_pp", "VP_PP_TAIL"),
("vp_base", "VP_BASE"),
].iter().cloned().collect();
let parse_rules_map = |rules_map: &serde_yaml::Mapping| -> Result<HashMap<String, GrammarRule>, Box<dyn std::error::Error>> {
let mut rules = HashMap::new();
for (rule_name, rule_data) in rules_map {
let rule_name_str = rule_name.as_str().ok_or("Invalid rule name")?;
let cfg_name = rule_name_map.get(rule_name_str).copied().unwrap_or(rule_name_str);
if let Some(cfg_prods) = rule_data.get("cfg_productions").and_then(|p| p.as_sequence()) {
let mut productions = Vec::new();
for prod in cfg_prods {
if let Some(prod_map) = prod.as_mapping() {
let production_str = prod_map.get("production")
.and_then(|p| p.as_str())
.ok_or("Missing production string")?;
let weight = prod_map.get("weight")
.and_then(|w| w.as_f64())
.unwrap_or(1.0);
let (symbols, refinements) = Self::parse_cfg_production_string(production_str)?;
productions.push(Production { symbols, refinements, weight });
}
}
if !productions.is_empty() {
let total_weight: f64 = productions.iter().map(|p| p.weight).sum();
if total_weight > 0.0 {
for prod in &mut productions {
prod.weight /= total_weight;
}
} else {
let equal_weight = 1.0 / productions.len() as f64;
for prod in &mut productions {
prod.weight = equal_weight;
}
}
rules.insert(cfg_name.to_string(), GrammarRule { productions });
}
}
}
Ok(rules)
};
let mut rules = HashMap::new();
if let Some(grammar) = doc.get("grammar") {
if let Some(rules_map) = grammar.get("rules").and_then(|r| r.as_mapping()) {
rules = parse_rules_map(rules_map)?;
}
if dialect != "body" {
if let Some(dialects) = grammar.get("dialects") {
if let Some(dialect_data) = dialects.get(dialect) {
if let Some(parent) = dialect_data.get("parent").and_then(|p| p.as_str()) {
if let Some(parent_data) = dialects.get(parent) {
if let Some(parent_rules_map) = parent_data.get("rules").and_then(|r| r.as_mapping()) {
let parent_rules = parse_rules_map(parent_rules_map)?;
for (name, rule) in parent_rules {
rules.insert(name, rule);
}
}
}
}
}
if let Some(dialect_data) = dialects.get(dialect) {
if let Some(dialect_rules_map) = dialect_data.get("rules").and_then(|r| r.as_mapping()) {
let dialect_rules = parse_rules_map(dialect_rules_map)?;
for (name, rule) in dialect_rules {
rules.insert(name, rule);
}
}
}
}
}
if dialect != "body" {
if let Some(dialects) = grammar.get("dialects") {
let type_refs = Self::resolve_type_refinements(dialects, dialect);
if !type_refs.is_empty() {
Self::inject_type_refinements(&mut rules, &type_refs);
}
}
}
}
Ok(rules)
}
fn resolve_type_refinements(
dialects: &serde_yaml::Value,
dialect: &str,
) -> HashMap<String, String> {
let dialect_data = match dialects.get(dialect) {
Some(d) => d,
None => return HashMap::new(),
};
if let Some(tr) = dialect_data.get("type_refinements").and_then(|v| v.as_mapping()) {
let mut refs = HashMap::new();
for (pos_key, ref_val) in tr {
if let (Some(pos_str), Some(ref_str)) = (pos_key.as_str(), ref_val.as_str()) {
refs.insert(pos_str.to_string(), ref_str.to_string());
}
}
if !refs.is_empty() {
return refs;
}
}
if let Some(scale_value) = dialect_data.get("scale") {
if let Ok(scale_def) = crate::scale::parse_scale_definition(scale_value) {
let tag = crate::scale::derive_refinement_tag(&scale_def);
let mut refs = HashMap::new();
refs.insert("N".to_string(), tag);
return refs;
}
}
if let Some(parent) = dialect_data.get("parent").and_then(|p| p.as_str()) {
return Self::resolve_type_refinements(dialects, parent);
}
HashMap::new()
}
fn inject_type_refinements(
rules: &mut HashMap<String, GrammarRule>,
type_refs: &HashMap<String, String>,
) {
for rule in rules.values_mut() {
for prod in &mut rule.productions {
for (i, sym) in prod.symbols.iter().enumerate() {
if let Sym::T(pos) = sym {
let pos_name = pos.as_str();
if let Some(ref_tag) = type_refs.get(pos_name) {
if prod.refinements.get(i).map_or(true, |r| r.is_none()) {
if i < prod.refinements.len() {
prod.refinements[i] = Some(ref_tag.clone());
}
}
}
}
}
}
}
}
fn parse_cfg_production_string(production: &str) -> Result<(Vec<Sym>, Vec<Option<String>>), Box<dyn std::error::Error>> {
let mut symbols = Vec::new();
let mut refinements = Vec::new();
for token in production.split_whitespace() {
let (pos_str, refinement) = if let Some(bracket_start) = token.find('[') {
let pos_part = &token[..bracket_start];
let ref_part = token[bracket_start+1..].trim_end_matches(']');
(pos_part, Some(ref_part.to_string()))
} else {
(token, None)
};
let sym = if let Some(pos) = Pos::from_str(pos_str) {
Sym::T(pos)
} else {
Sym::NT(pos_str.to_string())
};
let ref_out = if matches!(sym, Sym::T(_)) { refinement } else { None };
symbols.push(sym);
refinements.push(ref_out);
}
Ok((symbols, refinements))
}
fn parse_payload_format(yaml_content: &str, dialect: &str) -> (String, bool, Option<usize>, Option<usize>, String, Option<String>) {
let doc: Result<serde_yaml::Value, _> = serde_yaml::from_str(yaml_content);
if let Ok(doc) = doc {
if let Some(grammar) = doc.get("grammar") {
let mut payload_sep = grammar.get("payload_separator")
.and_then(|v| v.as_str())
.unwrap_or(" ")
.to_string();
let dot_is_punct = grammar.get("dot_is_punctuation")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let mut payload_line_width = grammar.get("payload_line_width")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let max_k = grammar.get("max_k")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let mut codec = grammar.get("codec")
.and_then(|v| v.as_str())
.unwrap_or_else(|| {
eprintln!("Warning: grammar.yaml missing 'codec' field, defaulting to 'bitpack'");
"bitpack"
})
.to_string();
let mut morphology = grammar.get("morphology")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if dialect != "body" {
if let Some(dialect_data) = grammar.get("dialects")
.and_then(|d| d.get(dialect))
{
if let Some(sep) = dialect_data.get("payload_separator")
.and_then(|v| v.as_str())
{
payload_sep = sep.to_string();
}
if dialect_data.get("payload_line_width").is_some() {
payload_line_width = dialect_data.get("payload_line_width")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
}
if let Some(c) = dialect_data.get("codec")
.and_then(|v| v.as_str())
{
codec = c.to_string();
}
if let Some(m) = dialect_data.get("morphology")
.and_then(|v| v.as_str())
{
morphology = Some(m.to_string());
}
}
}
return (payload_sep, dot_is_punct, payload_line_width, max_k, codec, morphology);
}
}
(" ".to_string(), true, None, None, "bitpack".to_string(), None)
}
pub fn from_language_dialect(language: &str, dialect: &str) -> Result<Self, Box<dyn std::error::Error>> {
let embedded_yaml = crate::generator::data::get_embedded_yaml(&format!("{}/grammar.yaml", language))
.or_else(|| match language {
"latin" => Some(include_str!("../languages/latin/grammar.yaml")),
"english" => Some(include_str!("../languages/english/grammar.yaml")),
_ => None,
});
if let Some(yaml_content) = embedded_yaml {
match LanguageConfig::from_yaml(yaml_content) {
Ok(language_config) => {
let rules = Self::build_rules_from_cfg_productions(yaml_content, dialect)?;
let (payload_separator, dot_is_punctuation, payload_line_width, max_k, codec, morphology) =
Self::parse_payload_format(yaml_content, dialect);
return Ok(Grammar {
rules,
language_config: Some(language_config),
dialect: Some(dialect.to_string()),
payload_separator,
dot_is_punctuation,
payload_line_width,
max_k,
codec,
morphology,
});
}
Err(e) => {
eprintln!("Warning: Failed to load embedded grammar.yaml: {}", e);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let grammar_yaml_path = crate::generator::data::find_language_file(language, "grammar.yaml");
if let Some(path) = grammar_yaml_path {
let grammar_yaml = std::fs::read_to_string(&path)?;
let language_config = LanguageConfig::from_yaml(&grammar_yaml)?;
let rules = Self::build_rules_from_cfg_productions(&grammar_yaml, dialect)?;
let (payload_separator, dot_is_punctuation, payload_line_width, max_k, codec, morphology) =
Self::parse_payload_format(&grammar_yaml, dialect);
return Ok(Grammar {
rules,
language_config: Some(language_config),
dialect: Some(dialect.to_string()),
payload_separator,
dot_is_punctuation,
payload_line_width,
max_k,
codec,
morphology,
});
}
}
Err(format!("No grammar.yaml found for language '{}'", language).into())
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
pub fn from_file(grammar_path: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error>> {
let grammar_yaml = std::fs::read_to_string(grammar_path)?;
let rules = Self::build_rules_from_cfg_productions(&grammar_yaml, "body")?;
let (payload_separator, dot_is_punctuation, payload_line_width, max_k, codec, morphology) =
Self::parse_payload_format(&grammar_yaml, "body");
Ok(Grammar {
rules,
language_config: None,
dialect: Some("body".to_string()),
payload_separator,
dot_is_punctuation,
payload_line_width,
max_k,
codec,
morphology,
})
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
pub fn precompute_sequences_with_probability_cached_from_file(
grammar_path: impl AsRef<Path>,
start_symbol: &str,
max_k: usize,
) -> Result<Vec<Vec<SequenceWithProbability>>, Box<dyn std::error::Error>> {
let grammar = Self::from_file(grammar_path)?;
Ok(grammar.precompute_sequences_with_probability(start_symbol, max_k))
}
pub fn subject() -> Result<Self, Box<dyn std::error::Error>> {
Self::from_language_dialect("latin", "subject")
}
#[allow(dead_code)]
pub fn expand<R: Rng>(&self, rng: &mut R, non_terminal: &str) -> Option<Vec<Sym>> {
let rule = self.rules.get(non_terminal)?;
let mut rand_val = rng.gen::<f64>();
for prod in &rule.productions {
rand_val -= prod.weight;
if rand_val <= 0.0 {
return Some(prod.symbols.clone());
}
}
rule.productions.first().map(|p| p.symbols.clone())
}
#[allow(dead_code)]
pub fn enumerate_sequences_with_probability(
&self,
start_symbol: &str,
k: usize,
) -> Vec<SequenceWithProbability> {
let mut memo: HashMap<(String, usize), Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)>> = HashMap::new();
self.enumerate_sequences_with_probability_internal(start_symbol, k, &mut memo)
}
pub fn precompute_sequences_with_probability(
&self,
start_symbol: &str,
max_k: usize,
) -> Vec<Vec<SequenceWithProbability>> {
if !self.rules.is_empty() {
} else if let Some(ref config) = self.language_config {
return self.precompute_sequences_type_driven(config, start_symbol, max_k);
}
let mut memo: HashMap<(String, usize), Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)>> = HashMap::new();
let mut by_k: Vec<Vec<SequenceWithProbability>> = vec![Vec::new(); max_k + 1];
for k in 0..=max_k {
by_k[k] = self.enumerate_sequences_with_probability_internal(start_symbol, k, &mut memo);
}
by_k
}
fn precompute_sequences_type_driven(
&self,
config: &LanguageConfig,
start_symbol: &str,
max_k: usize,
) -> Vec<Vec<SequenceWithProbability>> {
let target_type = match start_symbol {
"S" | "SContent" => SemanticType::Truth,
_ => {
if let Some(pos_str) = start_symbol.strip_prefix("S_") {
match pos_str.to_uppercase().as_str() {
"N" => pos_to_semantic_type(&Pos::N),
"V" => pos_to_semantic_type(&Pos::V),
"ADJ" => pos_to_semantic_type(&Pos::Adj),
"ADV" => pos_to_semantic_type(&Pos::Adv),
"PREP" => pos_to_semantic_type(&Pos::Prep),
"DET" => pos_to_semantic_type(&Pos::Det),
_ => SemanticType::Truth,
}
} else {
SemanticType::Truth
}
}
};
let derivations_by_k = config.derive_all(&target_type, max_k);
let mut by_k: Vec<Vec<SequenceWithProbability>> = vec![Vec::new(); max_k + 1];
for k in 0..=max_k {
by_k[k] = derivations_by_k[k]
.iter()
.map(|d| {
SequenceWithProbability::new(
d.sequence.clone(),
d.refinements.clone(),
d.probability,
)
})
.collect();
}
by_k
}
fn enumerate_sequences_with_probability_internal(
&self,
start_symbol: &str,
k: usize,
memo: &mut HashMap<(String, usize), Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)>>,
) -> Vec<SequenceWithProbability> {
fn enumerate_recursive(
grammar: &Grammar,
sym: &Sym,
sym_refinement: Option<&str>,
remaining: usize,
memo: &mut HashMap<(String, usize), Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)>>,
) -> Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)> {
match sym {
Sym::T(pos) => {
if remaining == 1 {
vec![(vec![*pos], vec![sym_refinement.map(|s| s.to_string())], 1.0)]
} else {
Vec::new()
}
}
Sym::Opt(inner) => {
let mut results = Vec::new();
let include_results = enumerate_recursive(grammar, inner, sym_refinement, remaining, memo)
.into_iter()
.map(|(seq, refs, prob)| (seq, refs, prob * 0.5))
.collect::<Vec<_>>();
results.extend(include_results);
if remaining == 0 {
results.push((Vec::new(), Vec::new(), 0.5));
}
results
}
Sym::NT(nt) => {
let key = (nt.clone(), remaining);
if let Some(cached) = memo.get(&key) {
return cached.clone();
}
let rule = match grammar.rules.get(nt) {
Some(r) => r,
None => return Vec::new(),
};
let mut all_results = Vec::new();
for prod in &rule.productions {
let prod_weight = prod.weight;
let mut production_results: Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)> =
vec![(Vec::new(), Vec::new(), 1.0)];
for (sym_idx, symbol) in prod.symbols.iter().enumerate() {
let mut new_results = Vec::new();
let prod_ref = prod.refinements.get(sym_idx).and_then(|r| r.as_deref());
for (partial_seq, partial_refs, partial_prob) in production_results {
let used = partial_seq.len();
let available = remaining.saturating_sub(used);
for symbol_slots in 0..=available {
let symbol_results = enumerate_recursive(
grammar,
symbol,
prod_ref,
symbol_slots,
memo,
);
for (symbol_seq, symbol_refs, symbol_prob) in &symbol_results {
if symbol_seq.len() != symbol_slots {
continue;
}
let mut combined_seq = partial_seq.clone();
combined_seq.extend(symbol_seq.iter().cloned());
let mut combined_refs = partial_refs.clone();
combined_refs.extend(symbol_refs.iter().cloned());
if combined_seq.len() <= remaining {
let combined_prob = partial_prob * symbol_prob;
new_results.push((combined_seq, combined_refs, combined_prob));
}
}
}
}
production_results = new_results;
}
for (seq, refs, symbol_prob) in production_results {
if seq.len() == remaining {
let final_prob = prod_weight * symbol_prob;
all_results.push((seq, refs, final_prob));
}
}
}
let mut prob_map: HashMap<(Vec<crate::types::Pos>, Vec<Option<String>>), f64> = HashMap::new();
for (seq, refs, prob) in all_results {
*prob_map.entry((seq, refs)).or_insert(0.0) += prob;
}
let mut final_results: Vec<(Vec<crate::types::Pos>, Vec<Option<String>>, f64)> =
prob_map.into_iter().map(|((seq, refs), prob)| (seq, refs, prob)).collect();
final_results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
memo.insert(key, final_results.clone());
final_results
}
}
}
let start_sym = Sym::NT(start_symbol.to_string());
let results = enumerate_recursive(self, &start_sym, None, k, memo);
let mut prob_map: HashMap<(Vec<crate::types::Pos>, Vec<Option<String>>), f64> = HashMap::new();
for (seq, refs, prob) in results {
*prob_map.entry((seq, refs)).or_insert(0.0) += prob;
}
let mut sequences: Vec<SequenceWithProbability> = prob_map
.into_iter()
.map(|((sequence, refinements), probability)| SequenceWithProbability::new(sequence, refinements, probability))
.collect();
sequences.sort_by(|a, b| {
b.probability.partial_cmp(&a.probability)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.sequence.cmp(&b.sequence))
.then_with(|| a.refinements.cmp(&b.refinements))
});
sequences
}
pub fn grammar_uses_pos(&self, pos: Pos) -> bool {
self.rules.values().any(|rule|
rule.productions.iter().any(|prod|
prod.symbols.iter().any(|sym| matches!(sym, Sym::T(p) if *p == pos))
)
)
}
pub fn min_sentence_length(&self) -> Option<usize> {
let mut memo = HashMap::new();
for k in 1..=200 {
let sequences = self.enumerate_sequences_with_probability_internal("S", k, &mut memo);
if !sequences.is_empty() {
return Some(k);
}
}
None
}
pub fn format_concise(&self) -> String {
let mut output = String::new();
let mut rules: Vec<_> = self.rules.iter().collect();
rules.sort_by_key(|(name, _)| *name);
if rules.is_empty() {
return "No grammar rules found.\n".to_string();
}
for (non_terminal, rule) in rules {
output.push_str(&format!("{} = ", non_terminal));
let productions_str: Vec<String> = rule.productions.iter().map(|prod| {
let symbols_str: Vec<String> = prod.symbols.iter().enumerate().map(|(idx, sym)| {
let base = match sym {
Sym::T(pos) => pos.as_str().to_string(),
Sym::NT(nt) => nt.clone(),
Sym::Opt(inner) => match &**inner {
Sym::T(pos) => format!("{:?}?", pos),
Sym::NT(nt) => format!("{}?", nt),
Sym::Opt(_) => "Opt?".to_string(),
},
};
if let Some(Some(ref tag)) = prod.refinements.get(idx) {
format!("{}[{}]", base, tag)
} else {
base
}
}).collect();
let prod_str = symbols_str.join(" ");
if (prod.weight - 1.0).abs() > 0.001 {
format!("({:.2}: {})", prod.weight, prod_str)
} else {
prod_str
}
}).collect();
output.push_str(&productions_str.join(" | "));
output.push('\n');
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Pos;
use std::time::{SystemTime, UNIX_EPOCH};
fn format_pos_sequence(seq: &[Pos]) -> String {
seq.iter().map(|pos| pos.as_str()).collect::<Vec<_>>().join(" ")
}
#[test]
fn test_enumerate_sequences_with_probability() {
let grammar = Grammar::default().expect("Failed to load body grammar");
let precomputed = grammar.precompute_sequences_with_probability("S", 20);
let mut total_prob_all_k: f64 = 0.0;
let mut k_values: Vec<usize> = Vec::new();
let mut prob_by_k: Vec<(usize, f64)> = Vec::new();
for k in 3..=20 {
println!("\n=== Testing k = {} ===", k);
let sequences = &precomputed[k];
if sequences.is_empty() {
println!("No valid sequences found for k = {}", k);
continue;
}
println!("Found {} valid sequence(s):", sequences.len());
println!("{:-<80}", "");
let total_prob: f64 = sequences.iter().map(|s| s.probability).sum();
println!("Total probability: {:.6} (probability that grammar generates exactly {} terminals)", total_prob, k);
println!("{:-<80}", "");
total_prob_all_k += total_prob;
k_values.push(k);
prob_by_k.push((k, total_prob));
for (i, seq_prob) in sequences.iter().take(20).enumerate() {
let seq_str = format_pos_sequence(&seq_prob.sequence);
println!("{}. [{:>6.4}%] {}",
i + 1,
seq_prob.probability * 100.0,
seq_str
);
}
if sequences.len() > 20 {
println!("... and {} more sequences", sequences.len() - 20);
}
}
println!("\n{:=<80}", "");
println!("SUMMARY: Probability distribution across all sequence lengths");
println!("{:=<80}", "");
for (k, prob) in &prob_by_k {
println!("k = {:2}: {:.6} ({:.2}%)", k, prob, prob * 100.0);
}
println!("{:-<80}", "");
println!("Sum across all k values: {:.6}", total_prob_all_k);
println!("Expected: close to 1.0 (all possible sequences from grammar)");
println!("{:=<80}", "");
}
#[test]
fn test_enumerate_simple_cases() {
let grammar = Grammar::default().expect("Failed to load body grammar");
println!("Testing VP with k=1:");
let vp_1 = grammar.enumerate_sequences_with_probability("VP", 1);
println!(" Found {} sequences", vp_1.len());
assert!(
vp_1.is_empty(),
"Expected no VP sequences of length 1 after removing bare V, got: {:?}",
vp_1
);
println!("Testing VP with k=2:");
let vp_2 = grammar.enumerate_sequences_with_probability("VP", 2);
println!(" Found {} sequences", vp_2.len());
assert!(!vp_2.is_empty(), "Expected some VP sequences of length 2");
println!("Testing NP with k=1:");
let np_1 = grammar.enumerate_sequences_with_probability("NP", 1);
println!(" Found {} sequences", np_1.len());
for seq in &np_1 {
println!(" {:?} (prob: {})", seq.sequence, seq.probability);
}
println!("Testing S_N with k=3:");
let s_n_3 = grammar.enumerate_sequences_with_probability("S_N", 3);
println!(" Found {} sequences", s_n_3.len());
for seq in &s_n_3 {
println!(" {:?} (prob: {})", seq.sequence, seq.probability);
}
}
#[test]
fn test_enumerate_start_symbols() {
let grammar = Grammar::default().expect("Failed to load body grammar");
let start_symbols = vec!["S_N", "S_V", "S_Adj", "S_Adv", "S_Prep", "S_Det"];
for start in start_symbols {
println!("\n=== Testing start symbol: {} ===", start);
let sequences = grammar.enumerate_sequences_with_probability(start, 5);
if sequences.is_empty() {
println!("No valid sequences found for {} with k=5", start);
continue;
}
println!("Found {} valid sequence(s):", sequences.len());
for (i, seq_prob) in sequences.iter().take(10).enumerate() {
let seq_str = format_pos_sequence(&seq_prob.sequence);
println!(" {}. [{:>6.4}%] {}",
i + 1,
seq_prob.probability * 100.0,
seq_str
);
}
if sequences.len() > 10 {
println!(" ... and {} more", sequences.len() - 10);
}
}
}
#[test]
fn test_parse_refinement_syntax_det_def() {
let (symbols, refinements) = Grammar::parse_cfg_production_string("Det[def] N").unwrap();
assert_eq!(symbols.len(), 2);
assert!(matches!(&symbols[0], Sym::T(Pos::Det)));
assert!(matches!(&symbols[1], Sym::T(Pos::N)));
assert_eq!(refinements[0], Some("def".to_string()));
assert_eq!(refinements[1], None);
}
#[test]
fn test_parse_refinement_syntax_multiple() {
let (symbols, refinements) = Grammar::parse_cfg_production_string("Det[indef] Adj Cop[sg]").unwrap();
assert_eq!(symbols.len(), 3);
assert!(matches!(&symbols[0], Sym::T(Pos::Det)));
assert!(matches!(&symbols[1], Sym::T(Pos::Adj)));
assert!(matches!(&symbols[2], Sym::T(Pos::Cop)));
assert_eq!(refinements[0], Some("indef".to_string()));
assert_eq!(refinements[1], None);
assert_eq!(refinements[2], Some("sg".to_string()));
}
#[test]
fn test_parse_no_refinement_productions() {
let (symbols, refinements) = Grammar::parse_cfg_production_string("N V NP").unwrap();
assert_eq!(symbols.len(), 3);
assert!(matches!(&symbols[0], Sym::T(Pos::N)));
assert!(matches!(&symbols[1], Sym::T(Pos::V)));
assert!(matches!(&symbols[2], Sym::NT(ref s) if s == "NP"));
assert!(refinements.iter().all(|r| r.is_none()),
"All refinements should be None for unrefined production");
}
#[test]
fn test_parse_nonterminal_ignores_refinement() {
let (symbols, refinements) = Grammar::parse_cfg_production_string("NP[foo] V").unwrap();
assert!(matches!(&symbols[0], Sym::NT(ref s) if s == "NP"));
assert_eq!(refinements[0], None, "Non-terminals must not carry refinement");
assert!(matches!(&symbols[1], Sym::T(Pos::V)));
assert_eq!(refinements[1], None);
}
#[test]
fn test_refinement_propagation_through_enumeration() {
let grammar = Grammar::from_language_dialect("english", "body").expect("Failed to load English body grammar");
let np_2 = grammar.enumerate_sequences_with_probability("NP", 2);
assert!(!np_2.is_empty(), "Expected NP sequences of length 2");
let has_refinement = np_2.iter().any(|seq| {
seq.refinements.iter().any(|r| r.is_some())
});
assert!(has_refinement,
"Expected at least one NP(k=2) sequence with a refinement annotation (Det[def] or Det[indef])");
for seq in &np_2 {
if seq.sequence.len() == 2 && seq.sequence[0] == Pos::Det {
let ref_tag = &seq.refinements[0];
if ref_tag.is_some() {
let tag = ref_tag.as_ref().unwrap();
assert!(
tag == "def" || tag == "indef" || tag == "quant",
"Expected Det refinement to be def, indef, or quant, got: {}",
tag
);
}
}
}
}
#[test]
fn test_refinement_cop_in_vp() {
let grammar = Grammar::from_language_dialect("english", "body").expect("Failed to load grammar");
let vp_2 = grammar.enumerate_sequences_with_probability("VP", 2);
let cop_adj_seqs: Vec<&SequenceWithProbability> = vp_2.iter()
.filter(|s| s.sequence == vec![Pos::Cop, Pos::Adj])
.collect();
assert!(!cop_adj_seqs.is_empty(), "Expected [Cop, Adj] sequences in VP(k=2)");
let cop_refs: Vec<Option<String>> = cop_adj_seqs.iter()
.map(|s| s.refinements[0].clone())
.collect();
assert!(cop_refs.contains(&Some("sg".to_string())),
"Expected Cop[sg] refinement in VP Cop Adj sequences, got: {:?}", cop_refs);
assert!(cop_refs.contains(&Some("pl".to_string())),
"Expected Cop[pl] refinement in VP Cop Adj sequences, got: {:?}", cop_refs);
}
#[test]
fn test_grammar_uses_pos_true() {
let grammar = Grammar::from_language_dialect("english", "body").expect("Failed to load grammar");
assert!(grammar.grammar_uses_pos(Pos::Dot), "English body grammar should use Dot");
assert!(grammar.grammar_uses_pos(Pos::Det), "English body grammar should use Det");
assert!(grammar.grammar_uses_pos(Pos::Cop), "English body grammar should use Cop");
assert!(grammar.grammar_uses_pos(Pos::N), "English body grammar should use N");
assert!(grammar.grammar_uses_pos(Pos::V), "English body grammar should use V");
assert!(grammar.grammar_uses_pos(Pos::Adj), "English body grammar should use Adj");
assert!(grammar.grammar_uses_pos(Pos::Adv), "English body grammar should use Adv");
assert!(grammar.grammar_uses_pos(Pos::Modal), "English body grammar should use Modal");
assert!(grammar.grammar_uses_pos(Pos::Prep), "English body grammar should use Prep");
}
#[test]
fn test_grammar_uses_pos_false() {
let grammar = Grammar::from_language_dialect("english", "body").expect("Failed to load grammar");
assert!(!grammar.grammar_uses_pos(Pos::Conj), "English body grammar should not use Conj");
assert!(!grammar.grammar_uses_pos(Pos::Prefix), "English body grammar should not use Prefix");
}
#[test]
fn test_grammar_uses_pos_latin_no_det() {
let grammar = Grammar::from_language_dialect("latin", "body").expect("Failed to load Latin grammar");
assert!(!grammar.grammar_uses_pos(Pos::Det), "Latin body grammar should not use Det");
assert!(grammar.grammar_uses_pos(Pos::N), "Latin body grammar should use N");
assert!(grammar.grammar_uses_pos(Pos::V), "Latin body grammar should use V");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Latin body grammar should use Dot");
}
#[test]
fn test_grammar_uses_pos_subject_no_prefix() {
let grammar = Grammar::from_language_dialect("english", "subject").expect("Failed to load grammar");
assert!(!grammar.grammar_uses_pos(Pos::Prefix), "Base subject grammar should not use Prefix");
}
#[test]
fn test_grammar_uses_pos_subject_re_has_prefix() {
let grammar = Grammar::from_language_dialect("english", "subject_re").expect("Failed to load subject_re grammar");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "subject_re grammar should use Prefix");
}
#[test]
fn test_grammar_uses_pos_subject_fwd_has_prefix() {
let grammar = Grammar::from_language_dialect("english", "subject_fwd").expect("Failed to load subject_fwd grammar");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "subject_fwd grammar should use Prefix");
}
#[test]
fn test_english_prose_dialect_loads() {
let grammar = Grammar::from_language_dialect("english", "prose").expect("Failed to load English prose grammar");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Prose grammar should use Dot");
assert!(grammar.grammar_uses_pos(Pos::V), "Prose grammar should use V");
assert!(grammar.grammar_uses_pos(Pos::Det), "Prose grammar should use Det");
assert!(grammar.grammar_uses_pos(Pos::Conj), "Prose grammar should use Conj");
assert!(grammar.grammar_uses_pos(Pos::Pron), "Prose grammar should use Pron");
let seqs = grammar.precompute_sequences_with_probability("S", 10);
let total: usize = seqs.iter().map(|v| v.len()).sum();
assert!(total > 0, "Prose grammar should produce at least some sequences");
}
#[test]
fn test_latin_spells_dialect_loads() {
let grammar = Grammar::from_language_dialect("latin", "spells").expect("Failed to load Latin spells grammar");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Spells grammar should use Dot");
assert!(grammar.grammar_uses_pos(Pos::V), "Spells grammar should use V");
assert!(grammar.grammar_uses_pos(Pos::N), "Spells grammar should use N");
assert!(grammar.grammar_uses_pos(Pos::Adj), "Spells grammar should use Adj");
assert!(!grammar.grammar_uses_pos(Pos::Det), "Spells grammar should not use Det");
let seqs_k2 = grammar.enumerate_sequences_with_probability("S", 2);
assert!(!seqs_k2.is_empty(), "Spells should produce k=2 sequences (V Dot, N Dot)");
let seqs_k3 = grammar.enumerate_sequences_with_probability("S", 3);
assert!(!seqs_k3.is_empty(), "Spells should produce k=3 sequences (V N Dot, Adj N Dot, etc.)");
}
#[test]
fn test_meta_grammar_loads() {
let grammar = Grammar::from_language_dialect("meta", "body")
.expect("Failed to load meta body grammar");
assert_eq!(grammar.max_k(), Some(12), "Meta grammar should declare max_k=12");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Meta grammar should use Dot");
assert!(grammar.grammar_uses_pos(Pos::N), "Meta grammar should use N");
assert!(grammar.grammar_uses_pos(Pos::V), "Meta grammar should use V");
assert!(grammar.grammar_uses_pos(Pos::Adj), "Meta grammar should use Adj");
assert!(grammar.grammar_uses_pos(Pos::Det), "Meta grammar should use Det");
assert!(grammar.grammar_uses_pos(Pos::Prep), "Meta grammar should use Prep");
assert!(grammar.grammar_uses_pos(Pos::Cop), "Meta grammar should use Cop");
assert!(grammar.grammar_uses_pos(Pos::Conj), "Meta grammar should use Conj");
assert!(grammar.grammar_uses_pos(Pos::Pron), "Meta grammar should use Pron");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Meta grammar should use Modal");
}
#[test]
fn test_meta_grammar_enumerates_sequences() {
let grammar = Grammar::from_language_dialect("meta", "body")
.expect("Failed to load meta body grammar");
let max_k = grammar.max_k().unwrap_or(12);
let precomputed = grammar.precompute_sequences_with_probability("S", max_k);
let mut total_sequences: usize = 0;
let mut total_prob: f64 = 0.0;
for k in 3..=max_k {
let seqs = &precomputed[k];
total_sequences += seqs.len();
total_prob += seqs.iter().map(|s| s.probability).sum::<f64>();
if !seqs.is_empty() {
println!("Meta k={}: {} sequences (prob={:.6})", k, seqs.len(),
seqs.iter().map(|s| s.probability).sum::<f64>());
}
}
println!("Meta grammar total: {} sequences, prob sum={:.6}", total_sequences, total_prob);
assert!(total_sequences > 0, "Meta grammar should produce at least some sequences");
assert!(total_prob > 0.5, "Meta grammar should cover >50% probability mass within k=12");
}
#[test]
fn test_meta_grammar_minimum_sentence() {
let grammar = Grammar::from_language_dialect("meta", "body")
.expect("Failed to load meta body grammar");
let seqs_k4 = grammar.enumerate_sequences_with_probability("S", 4);
assert!(!seqs_k4.is_empty(),
"Meta grammar should produce sentences at k=4");
for seq in seqs_k4.iter().take(5) {
let seq_str = format_pos_sequence(&seq.sequence);
println!("Meta k=4: {} (prob={:.4})", seq_str, seq.probability);
assert_eq!(*seq.sequence.last().unwrap(), Pos::Dot,
"Meta sentence should end with Dot: {}", seq_str);
}
}
#[test]
fn test_cs_grammar_payload_format() {
let grammar = Grammar::from_language_dialect("cs", "body").expect("Failed to load CS grammar");
assert_eq!(grammar.payload_separator(), "", "CS grammar should concatenate payload words");
assert!(!grammar.dot_is_punctuation(), "CS grammar Dot should produce cover words, not periods");
assert_eq!(grammar.payload_line_width(), Some(76), "CS grammar should wrap payload at 76 chars");
assert!(grammar.grammar_uses_pos(Pos::N), "CS grammar should use N");
assert!(grammar.grammar_uses_pos(Pos::Dot), "CS grammar should use Dot");
assert!(grammar.grammar_uses_pos(Pos::Aux), "CS grammar should use Aux");
}
#[test]
fn test_payload_separator_dialect_override() {
let base = Grammar::from_language_dialect("cs", "body")
.expect("Failed to load CS body grammar");
assert_eq!(base.payload_separator(), "",
"CS body should use grammar-level default (concatenated)");
let seal = Grammar::from_language_dialect("cs", "seal_nostr")
.expect("Failed to load CS seal_nostr grammar");
assert_eq!(seal.payload_separator(), "",
"seal_nostr should inherit concatenated separator from CS grammar");
}
#[test]
fn test_english_grammar_default_payload_format() {
let grammar = Grammar::from_language_dialect("english", "body").expect("Failed to load grammar");
assert_eq!(grammar.payload_separator(), " ", "English grammar should space-separate payload words");
assert!(grammar.dot_is_punctuation(), "English grammar Dot should append periods");
assert_eq!(grammar.payload_line_width(), None, "English grammar should not wrap payload lines");
}
#[test]
fn test_sequence_cache_roundtrip_tempdir() {
let grammar_str = include_str!("../languages/english/grammar.yaml");
let tmp = std::env::temp_dir();
let uniq = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let grammar_path = tmp.join(format!("glossia_grammar_{uniq}.yaml"));
std::fs::write(&grammar_path, grammar_str).unwrap();
let by_k_1 = Grammar::precompute_sequences_with_probability_cached_from_file(&grammar_path, "S", 20)
.expect("cache compute 1");
let by_k_2 = Grammar::precompute_sequences_with_probability_cached_from_file(&grammar_path, "S", 20)
.expect("cache compute 2");
assert_eq!(by_k_1.len(), by_k_2.len());
let has_sequences = (3..=20).any(|k| !by_k_1[k].is_empty());
assert!(has_sequences, "Should have at least some non-empty sequence lengths");
for k in 3..=20 {
assert_eq!(by_k_1[k].len(), by_k_2[k].len(), "Mismatch at k={}", k);
}
}
#[test]
fn test_dialect_config_latin_body_defaults() {
let config = DialectConfig::from_language_dialect("latin", "body")
.expect("Failed to load Latin body dialect config");
assert_eq!(config.language(), "latin");
assert_eq!(config.dialect(), "body");
assert_eq!(config.payload_wordlist(), "default");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_latin_spells_hp_wordlist() {
let config = DialectConfig::from_language_dialect("latin", "spells")
.expect("Failed to load Latin spells dialect config");
assert_eq!(config.language(), "latin");
assert_eq!(config.dialect(), "spells");
assert_eq!(config.payload_wordlist(), "hp",
"Latin spells dialect should use payload_hp.yaml");
assert_eq!(config.cover_wordlist(), "default",
"Latin spells dialect should use default cover.yaml");
}
#[test]
fn test_dialect_config_english_subject_defaults() {
let config = DialectConfig::from_language_dialect("english", "subject")
.expect("Failed to load English subject dialect config");
assert_eq!(config.payload_wordlist(), "default");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_english_prose_defaults() {
let config = DialectConfig::from_language_dialect("english", "prose")
.expect("Failed to load English prose dialect config");
assert_eq!(config.payload_wordlist(), "default");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_cs_nip04_base64() {
let config = DialectConfig::from_language_dialect("cs", "nip04")
.expect("Failed to load CS nip04 dialect config");
assert_eq!(config.payload_wordlist(), "base64",
"CS nip04 should use payload_base64.yaml (base64 is the NIP-04 wire format)");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_with_payload_override() {
let config = DialectConfig::from_language_dialect("latin", "body")
.expect("Failed to load Latin body dialect config");
assert_eq!(config.payload_wordlist(), "default");
let overridden = config.with_payload_wordlist("hp");
assert_eq!(overridden.payload_wordlist(), "hp",
"with_payload_wordlist should update payload_wl");
assert_eq!(overridden.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_with_cover_override() {
let config = DialectConfig::from_language_dialect("latin", "body")
.expect("Failed to load Latin body dialect config");
let overridden = config.with_cover_wordlist("custom");
assert_eq!(overridden.cover_wordlist(), "custom");
assert_eq!(overridden.payload_wordlist(), "default");
}
#[test]
fn test_dialect_config_wordlist_filenames() {
let config = DialectConfig::from_language_dialect("latin", "spells")
.expect("Failed to load Latin spells dialect config");
let (payload_file, cover_file) = config.wordlist_filenames();
assert_eq!(payload_file, "payload_hp.yaml",
"Spells dialect should resolve to payload_hp.yaml");
assert_eq!(cover_file, "cover.yaml",
"Spells dialect cover should resolve to cover.yaml (fallback)");
}
#[test]
fn test_dialect_config_grammar_is_valid() {
let config = DialectConfig::from_language_dialect("latin", "spells")
.expect("Failed to load Latin spells dialect config");
let seqs = config.grammar.enumerate_sequences_with_probability("S", 2);
assert!(!seqs.is_empty(), "Spells grammar should produce k=2 sequences");
}
#[test]
fn test_dialect_config_available_dialects_latin() {
let dialects = DialectConfig::available_dialects("latin");
assert!(dialects.contains(&"body".to_string()), "Should include body");
assert!(dialects.contains(&"subject".to_string()), "Should include subject");
assert!(dialects.contains(&"spells".to_string()), "Should include spells");
assert!(dialects.contains(&"payload_only".to_string()), "Should include payload_only");
}
#[test]
fn test_dialect_config_available_dialects_english() {
let dialects = DialectConfig::available_dialects("english");
assert!(dialects.contains(&"body".to_string()), "Should include body");
assert!(dialects.contains(&"subject".to_string()), "Should include subject");
assert!(dialects.contains(&"prose".to_string()), "Should include prose");
assert!(dialects.contains(&"payload_only".to_string()), "Should include payload_only");
}
#[test]
fn test_dialect_config_unknown_dialect_defaults() {
let config = DialectConfig::from_language_dialect("latin", "nonexistent")
.expect("Unknown dialect should still load (uses base grammar)");
assert_eq!(config.payload_wordlist(), "default");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_cs_sig_grammar_loads() {
let grammar = Grammar::from_language_dialect("cs", "sig")
.expect("Failed to load CS sig grammar");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Sig grammar should use Dot (-----)");
assert!(grammar.grammar_uses_pos(Pos::Aux), "Sig grammar should use Aux (BEGIN/END)");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Sig grammar should use Modal (SIGNATURE)");
assert!(grammar.grammar_uses_pos(Pos::N), "Sig grammar should use N (payload)");
assert!(!grammar.grammar_uses_pos(Pos::Prefix), "Plain sig should not use Prefix");
assert!(!grammar.grammar_uses_pos(Pos::Cop), "Sig grammar should not use Cop (ENCRYPTED)");
}
#[test]
fn test_cs_sig_pgp_grammar_loads() {
let grammar = Grammar::from_language_dialect("cs", "sig_pgp")
.expect("Failed to load CS sig_pgp grammar");
assert!(grammar.grammar_uses_pos(Pos::Dot), "Sig PGP should use Dot");
assert!(grammar.grammar_uses_pos(Pos::Aux), "Sig PGP should use Aux");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Sig PGP should use Modal (SIGNATURE)");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "Sig PGP should use Prefix (PGP)");
assert!(grammar.grammar_uses_pos(Pos::N), "Sig PGP should use N (payload)");
assert!(!grammar.grammar_uses_pos(Pos::Cop), "Sig PGP should not use Cop");
assert!(!grammar.grammar_uses_pos(Pos::To), "Sig PGP should not use To");
}
#[test]
fn test_cs_sig_produces_sequences() {
let grammar = Grammar::from_language_dialect("cs", "sig")
.expect("Failed to load CS sig grammar");
let seqs_11 = grammar.enumerate_sequences_with_probability("S", 11);
assert!(!seqs_11.is_empty(),
"Plain sig should produce k=11 sequences (5 header + 1 body + 5 footer)");
}
#[test]
fn test_cs_sig_pgp_produces_sequences() {
let grammar = Grammar::from_language_dialect("cs", "sig_pgp")
.expect("Failed to load CS sig_pgp grammar");
let seqs_13 = grammar.enumerate_sequences_with_probability("S", 13);
assert!(!seqs_13.is_empty(),
"PGP sig should produce k=13 sequences (6 header + 1 body + 6 footer)");
}
#[test]
fn test_cs_sig_shorter_than_message() {
let sig_grammar = Grammar::from_language_dialect("cs", "sig_pgp")
.expect("Failed to load sig_pgp grammar");
let msg_grammar = Grammar::from_language_dialect("cs", "pgp")
.expect("Failed to load pgp grammar");
let sig_min = sig_grammar.min_sentence_length()
.expect("sig_pgp should have a minimum length");
let msg_min = msg_grammar.min_sentence_length()
.expect("pgp should have a minimum length");
assert_eq!(msg_min - sig_min, 2,
"PGP sig should be 2 tokens shorter than PGP message (1 less in header + 1 less in footer)");
}
#[test]
fn test_dialect_config_cs_sig_base58() {
let config = DialectConfig::from_language_dialect("cs", "sig")
.expect("Failed to load CS sig dialect config");
assert_eq!(config.payload_wordlist(), "base58",
"CS sig should use payload_base58.yaml");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_cs_sig_pgp_base58() {
let config = DialectConfig::from_language_dialect("cs", "sig_pgp")
.expect("Failed to load CS sig_pgp dialect config");
assert_eq!(config.payload_wordlist(), "base58",
"CS sig_pgp should use payload_base58.yaml");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_cs_sig_nostr_grammar_loads() {
let grammar = Grammar::from_language_dialect("cs", "sig_nostr")
.expect("Failed to load CS sig_nostr grammar");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "Sig Nostr should use Prefix (NOSTR)");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Sig Nostr should use Modal (SIGNATURE)");
assert!(!grammar.grammar_uses_pos(Pos::Cop), "Sig Nostr should not use Cop (ENCRYPTED)");
assert!(!grammar.grammar_uses_pos(Pos::To), "Sig Nostr should not use To (MESSAGE)");
}
#[test]
fn test_cs_sig_nostr_produces_sequences() {
let grammar = Grammar::from_language_dialect("cs", "sig_nostr")
.expect("Failed to load CS sig_nostr grammar");
let seqs_13 = grammar.enumerate_sequences_with_probability("S", 13);
assert!(!seqs_13.is_empty(),
"Nostr sig should produce k=13 sequences (6 header + 1 body + 6 footer)");
}
#[test]
fn test_cs_sig_nostr_same_framing_as_sig_pgp() {
let nostr = Grammar::from_language_dialect("cs", "sig_nostr")
.expect("Failed to load sig_nostr");
let pgp = Grammar::from_language_dialect("cs", "sig_pgp")
.expect("Failed to load sig_pgp");
assert_eq!(
nostr.min_sentence_length(),
pgp.min_sentence_length(),
"Nostr and PGP sig should have same framing overhead"
);
}
#[test]
fn test_dialect_config_cs_sig_nostr_base16() {
let config = DialectConfig::from_language_dialect("cs", "sig_nostr")
.expect("Failed to load CS sig_nostr dialect config");
assert_eq!(config.payload_wordlist(), "base16",
"CS sig_nostr should use payload_base16.yaml (hex for Schnorr sigs)");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_cs_sig_latin_grammar_loads() {
let grammar = Grammar::from_language_dialect("cs", "sig_latin")
.expect("Failed to load CS sig_latin grammar");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "Sig Latin should use Prefix (NOSTR)");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Sig Latin should use Modal (SIGNATURE)");
assert!(!grammar.grammar_uses_pos(Pos::Cop), "Sig Latin should not use Cop (ENCRYPTED)");
assert!(!grammar.grammar_uses_pos(Pos::To), "Sig Latin should not use To (MESSAGE)");
}
#[test]
fn test_cs_sig_latin_produces_sequences() {
let grammar = Grammar::from_language_dialect("cs", "sig_latin")
.expect("Failed to load CS sig_latin grammar");
let seqs_13 = grammar.enumerate_sequences_with_probability("S", 13);
assert!(!seqs_13.is_empty(),
"Sig Latin should produce k=13 sequences (6 header + 1 body + 6 footer)");
}
#[test]
fn test_cs_sig_latin_same_framing_as_sig_nostr() {
let latin = Grammar::from_language_dialect("cs", "sig_latin")
.expect("Failed to load sig_latin");
let nostr = Grammar::from_language_dialect("cs", "sig_nostr")
.expect("Failed to load sig_nostr");
assert_eq!(
latin.min_sentence_length(),
nostr.min_sentence_length(),
"sig_latin and sig_nostr should have same framing overhead"
);
}
#[test]
fn test_dialect_config_cs_sig_latin() {
let config = DialectConfig::from_language_dialect("cs", "sig_latin")
.expect("Failed to load CS sig_latin dialect config");
assert_eq!(config.payload_wordlist(), "default",
"CS sig_latin should use default payload wordlist (Latin words)");
assert_eq!(config.payload_language(), "latin",
"CS sig_latin should resolve payload from Latin language");
}
#[test]
fn test_cs_seal_nostr_grammar_loads() {
let grammar = Grammar::from_language_dialect("cs", "seal_nostr")
.expect("Failed to load CS seal_nostr grammar");
assert!(grammar.grammar_uses_pos(Pos::Prefix), "Seal Nostr should use Prefix (NOSTR)");
assert!(grammar.grammar_uses_pos(Pos::Modal), "Seal Nostr should use Modal (SEAL)");
assert!(!grammar.grammar_uses_pos(Pos::Cop), "Seal Nostr should not use Cop (ENCRYPTED)");
assert!(!grammar.grammar_uses_pos(Pos::To), "Seal Nostr should not use To (MESSAGE)");
}
#[test]
fn test_cs_seal_nostr_produces_sequences() {
let grammar = Grammar::from_language_dialect("cs", "seal_nostr")
.expect("Failed to load CS seal_nostr grammar");
let seqs_14 = grammar.enumerate_sequences_with_probability("S", 14);
assert!(!seqs_14.is_empty(),
"Nostr seal should produce k=14 sequences (6 header + 1 prefix + 1 body + 6 footer)");
}
#[test]
fn test_dialect_config_cs_seal_nostr_bech32() {
let config = DialectConfig::from_language_dialect("cs", "seal_nostr")
.expect("Failed to load CS seal_nostr dialect config");
assert_eq!(config.payload_wordlist(), "bech32",
"CS seal_nostr should use payload_bech32.yaml");
assert_eq!(config.cover_wordlist(), "default");
}
#[test]
fn test_dialect_config_available_dialects_cs_includes_sig() {
let dialects = DialectConfig::available_dialects("cs");
assert!(dialects.contains(&"sig".to_string()),
"CS should include sig dialect");
assert!(dialects.contains(&"sig_pgp".to_string()),
"CS should include sig_pgp dialect");
assert!(dialects.contains(&"sig_nostr".to_string()),
"CS should include sig_nostr dialect");
assert!(dialects.contains(&"seal_nostr".to_string()),
"CS should include seal_nostr dialect");
}
#[test]
fn test_music_pentatonic_scale_definition() {
let config = DialectConfig::from_language_dialect("music", "pentatonic")
.expect("Failed to load music pentatonic dialect config");
let scale = config.scale().expect("Pentatonic dialect should have a scale definition");
assert_eq!(scale.intervals, vec![2, 2, 3, 2, 3], "Major pentatonic intervals");
assert_eq!(scale.root, "C");
assert_eq!(config.payload_wordlist(), "pentatonic");
let words = crate::generator::load_payload_words_for_wordlist("music", "pentatonic")
.expect("Should load scale-derived payload");
assert!(words.len() > 40, "Should have at least 40 pentatonic notes, got {}", words.len());
assert!(words.len() < 60, "Should have fewer than 60 notes, got {}", words.len());
for word in &words {
let pc_name = crate::scale::pitch_class_of_note(word)
.expect(&format!("Should extract pitch class from '{}'", word));
let pc = crate::scale::pitch_class_from_name(pc_name)
.expect(&format!("Should parse pitch class '{}'", pc_name));
assert!(
[0, 2, 4, 7, 9].contains(&pc),
"Note '{}' has pitch class {} ({}) which is not in C major pentatonic",
word, pc, pc_name
);
}
}
#[test]
fn test_music_blues_scale_definition() {
let config = DialectConfig::from_language_dialect("music", "blues")
.expect("Failed to load music blues dialect config");
let scale = config.scale().expect("Blues dialect should have a scale definition");
assert_eq!(scale.intervals, vec![3, 2, 1, 1, 3, 2], "Blues scale intervals");
assert_eq!(scale.root, "A");
let words = crate::generator::load_payload_words_for_wordlist("music", "blues")
.expect("Should load blues scale payload");
assert!(words.len() > 50, "Blues should have more notes than pentatonic, got {}", words.len());
let valid_pcs = [0, 2, 3, 4, 7, 9]; for word in &words {
let pc = crate::scale::pitch_class_of_note(word)
.and_then(crate::scale::pitch_class_from_name)
.expect(&format!("Should get pitch class for '{}'", word));
assert!(
valid_pcs.contains(&pc),
"Note '{}' (pc={}) not in A blues scale",
word, pc
);
}
}
#[test]
fn test_music_scored_dialect_no_scale() {
let config = DialectConfig::from_language_dialect("music", "scored")
.expect("Failed to load music scored dialect config");
assert!(config.scale().is_none(), "Scored dialect should not have a scale");
assert_eq!(config.payload_wordlist(), "default");
}
#[test]
fn test_music_pentatonic_scored_inherits_scale() {
let config = DialectConfig::from_language_dialect("music", "pentatonic-scored")
.expect("Failed to load music pentatonic-scored dialect config");
let scale = config.scale().expect("pentatonic-scored should have a scale");
assert_eq!(scale.intervals, vec![2, 2, 3, 2, 3]);
assert_eq!(config.payload_wordlist(), "pentatonic-scored");
}
#[test]
fn test_music_pentatonic_n_slots_carry_refinement() {
let config = DialectConfig::from_language_dialect("music", "pentatonic")
.expect("Failed to load music pentatonic dialect config");
let sequences = config.grammar.precompute_sequences_with_probability("S", 4);
let mut found_n_with_refinement = false;
for k_seqs in &sequences {
for seq in k_seqs {
for (i, &pos) in seq.sequence.iter().enumerate() {
if pos == Pos::N {
let ref_tag = seq.refinements.get(i).and_then(|r| r.as_deref());
assert_eq!(
ref_tag,
Some("pentatonic/C"),
"N slot at position {} should have refinement 'pentatonic/C', got {:?}",
i, ref_tag
);
found_n_with_refinement = true;
}
}
}
}
assert!(found_n_with_refinement, "Should have found at least one N slot in pentatonic sequences");
}
#[test]
fn test_music_blues_n_slots_carry_refinement() {
let config = DialectConfig::from_language_dialect("music", "blues")
.expect("Failed to load music blues dialect config");
let sequences = config.grammar.precompute_sequences_with_probability("S", 4);
let mut found_n = false;
for k_seqs in &sequences {
for seq in k_seqs {
for (i, &pos) in seq.sequence.iter().enumerate() {
if pos == Pos::N {
let ref_tag = seq.refinements.get(i).and_then(|r| r.as_deref());
assert_eq!(
ref_tag,
Some("blues/A"),
"N slot at position {} should have refinement 'blues/A', got {:?}",
i, ref_tag
);
found_n = true;
}
}
}
}
assert!(found_n, "Should have found at least one N slot in blues sequences");
}
#[test]
fn test_music_raw_chromatic_no_n_refinement() {
let config = DialectConfig::from_language_dialect("music", "raw")
.expect("Failed to load music raw dialect config");
let sequences = config.grammar.precompute_sequences_with_probability("S", 3);
for k_seqs in &sequences {
for seq in k_seqs {
for (i, &pos) in seq.sequence.iter().enumerate() {
if pos == Pos::N {
let ref_tag = seq.refinements.get(i).and_then(|r| r.as_deref());
assert_eq!(
ref_tag, None,
"Raw chromatic N slot should have no refinement, got {:?}",
ref_tag
);
}
}
}
}
}
}