Skip to main content

md_tmpl_core/frontmatter/
params.rs

1//! Parameter declaration parsing for frontmatter `params:` blocks.
2//!
3//! Handles both inline (`[name = str, count = int]`) and block
4//! (`- name = str`) formats, including default values and nested types.
5
6use alloc::{
7    boxed::Box,
8    string::{String, ToString},
9    sync::Arc,
10    vec::Vec,
11};
12
13use super::ImportedNamespace;
14use crate::{
15    compat::HashMap,
16    error::TemplateError,
17    types::{VarDecl, VarType},
18    value::Value,
19};
20
21/// Join YAML continuation lines into one logical line per top-level entry.
22///
23/// Any line starting with whitespace is appended to the preceding logical line.
24///
25/// Blank lines and full-line `#` comments are layout/documentation only: they
26/// are skipped entirely and, crucially, do **not** terminate an in-progress
27/// block list. This lets block entries be separated by blank lines or
28/// interleaved with comments for readability (e.g. a documented `consts:`
29/// block) while still joining every entry onto its section's logical line
30/// instead of orphaning entries after the first blank onto a stray line that
31/// no section prefix matches.
32pub(crate) fn join_continuation_lines(block: &str) -> Vec<String> {
33    let mut logical: Vec<String> = Vec::new();
34    for raw in block.lines() {
35        let trimmed = raw.trim();
36        // Skip blanks and full-line comments without breaking continuation.
37        if trimmed.is_empty() || trimmed.starts_with(crate::consts::FM_COMMENT_PREFIX) {
38            continue;
39        }
40        if raw.starts_with(' ') || raw.starts_with('\t') {
41            // Continuation of previous logical line.
42            if let Some(prev) = logical.last_mut() {
43                prev.push(' ');
44                prev.push_str(trimmed);
45            } else {
46                logical.push(raw.to_string());
47            }
48        } else {
49            logical.push(raw.to_string());
50        }
51    }
52    logical
53}
54
55/// Parse the value part after `params:` or `consts:`.
56///
57/// Supports both inline and block list formats:
58/// - Inline: `[name = str, count = int]`
59pub(crate) fn parse_declarations(
60    rest: &str,
61    type_aliases: &HashMap<String, VarType>,
62    resolved_imports: &HashMap<String, ImportedNamespace>,
63    is_constant: bool,
64    available_consts: &HashMap<String, Value>,
65) -> Result<Vec<VarDecl>, TemplateError> {
66    let rest = rest.trim();
67    if rest.is_empty() {
68        // `params:` with no value and no continuation lines → empty params.
69        return Ok(vec![]);
70    }
71
72    // Strip only the outermost `[` and `]` (inline YAML flow sequence).
73    let inner = rest
74        .strip_prefix(crate::consts::BRACKET_OPEN)
75        .and_then(|s| s.strip_suffix(crate::consts::BRACKET_CLOSE))
76        .unwrap_or(rest);
77
78    // Handle block list format: entries are `- name = type` joined by spaces
79    // (after continuation line joining, the `- ` markers are preserved).
80    let entries = if inner.contains("- ") {
81        // Split on ` - ` to separate entries, then strip leading `- ` from
82        // the first entry if present.
83        let mut result = Vec::new();
84        for part in inner.split(" - ") {
85            let part = part.trim().strip_prefix('-').unwrap_or(part).trim();
86            if !part.is_empty() {
87                result.push(part.to_string());
88            }
89        }
90        result
91    } else {
92        // Inline format: split on commas at bracket-depth 0.
93        split_at_depth_zero(inner)
94            .into_iter()
95            .map(ToString::to_string)
96            .collect()
97    };
98
99    let mut decls = Vec::new();
100    let mut seen_names = crate::compat::HashSet::new();
101    let mut current_consts = available_consts.clone();
102    for entry in &entries {
103        let e = entry.trim();
104        let trimmed = crate::consts::strip_string_literal(e).unwrap_or(e).trim();
105        if let Some(decl) = parse_single_declaration(
106            trimmed,
107            type_aliases,
108            resolved_imports,
109            is_constant,
110            &mut current_consts,
111            &mut seen_names,
112        )? {
113            decls.push(decl);
114        }
115    }
116
117    Ok(decls)
118}
119
120/// Parse a single declaration entry (e.g. `name = str := "default"`) into a [`VarDecl`].
121fn parse_single_declaration(
122    trimmed: &str,
123    type_aliases: &HashMap<String, VarType>,
124    resolved_imports: &HashMap<String, ImportedNamespace>,
125    is_constant: bool,
126    current_consts: &mut HashMap<String, Value>,
127    seen_names: &mut crate::compat::HashSet<String>,
128) -> Result<Option<VarDecl>, TemplateError> {
129    if trimmed.is_empty() {
130        return Ok(None);
131    }
132
133    // Find `=` at depth 0 to split name from type+default.
134    let Some(eq_pos) = find_char_at_depth_zero(trimmed, crate::consts::EQUALS) else {
135        let label = if is_constant { "constant" } else { "param" };
136        return Err(TemplateError::syntax(format!(
137            "{label} '{trimmed}' is missing a type annotation (expected 'name = type')"
138        )));
139    };
140
141    let name = trimmed[..eq_pos].trim().to_string();
142    let type_and_default = trimmed[eq_pos + 1..].trim();
143
144    // Check duplicate names.
145    if !seen_names.insert(name.clone()) {
146        let err = if is_constant {
147            crate::consts::ERR_DUPLICATE_CONST
148        } else {
149            crate::consts::ERR_DUPLICATE_PARAM
150        };
151        return Err(TemplateError::syntax(format!("{err}: '{name}'")));
152    }
153
154    // Check reserved keywords.
155    if crate::consts::RESERVED_NAMES.contains(&name.as_str()) {
156        return Err(TemplateError::syntax(format!(
157            "{}: '{name}'",
158            crate::consts::ERR_RESERVED_KEYWORD
159        )));
160    }
161
162    // Find `:=` at depth 0 to split type from default value.
163    let (type_str, default_part) =
164        if let Some(assign_pos) = find_assign_default_at_depth_zero(type_and_default) {
165            (
166                type_and_default[..assign_pos].trim(),
167                Some(type_and_default[assign_pos + 2..].trim()),
168            )
169        } else {
170            (type_and_default, None)
171        };
172
173    let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)
174        .map_err(|e| TemplateError::syntax(format!("declaration '{name}': {e}")))?;
175
176    let default_value = if let Some(dp) = default_part {
177        let default = parse_default_value_full(
178            dp,
179            &var_type,
180            current_consts,
181            type_aliases,
182            resolved_imports,
183        )
184        .or_else(|| resolve_const_default(dp, current_consts))
185        .or_else(|| resolve_kinds_default(dp, type_aliases, resolved_imports))
186        .ok_or_else(|| {
187            TemplateError::syntax(format!(
188                "invalid default value '{dp}' for declaration '{name}' (strings must be quoted)"
189            ))
190        })?;
191        current_consts.insert(name.clone(), default.clone());
192        Some(default)
193    } else {
194        None
195    };
196
197    // For constants, the default value is mandatory.
198    if is_constant && default_value.is_none() {
199        return Err(TemplateError::syntax(format!(
200            "constant '{name}' is missing a value (expected 'name = type := value')"
201        )));
202    }
203
204    // Validate that the default value matches the declared type.
205    if let Some(ref default) = default_value
206        && !var_type.matches(default)
207    {
208        let label = if is_constant { "constant" } else { "param" };
209        return Err(TemplateError::syntax(format!(
210            "{label} '{name}': value has type '{}' but declared type is '{var_type}'",
211            default.type_name()
212        )));
213    }
214
215    Ok(Some(VarDecl {
216        name,
217        var_type,
218        default_value,
219    }))
220}
221
222// Compatibility wrapper for `params:` removed as it is now unused.
223
224/// Strip enclosing compound type delimiter pair `(...)`.
225pub(crate) fn strip_type_brackets(s: &str) -> Option<&str> {
226    if let (Some(inner), true) = (
227        s.strip_prefix(crate::consts::PAREN_OPEN),
228        s.ends_with(crate::consts::PAREN_CLOSE),
229    ) {
230        Some(&inner[..inner.len() - 1])
231    } else {
232        None
233    }
234}
235
236/// Split a string on commas at bracket-depth 0, ignoring commas inside quoted
237/// string literals.
238///
239/// Delimiters (brackets, braces, parens, angle brackets, and the separating
240/// comma) that appear inside a `"..."` or `'...'` string literal are treated as
241/// literal characters. This lets struct/list default values contain quoted
242/// strings with embedded commas or brackets (e.g. `{msg = "a, b", n = 1}`)
243/// without the field separator being misdetected.
244pub(crate) fn split_at_depth_zero(input: &str) -> Vec<&str> {
245    use crate::consts::{
246        ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, COMMA,
247        PAREN_CLOSE, PAREN_OPEN, QUOTE_DOUBLE, QUOTE_SINGLE,
248    };
249    let mut entries = Vec::new();
250    let mut depth: u32 = 0;
251    let mut start = 0;
252    // When inside a string literal, holds the opening quote char; delimiters are
253    // ignored until the matching closing quote is seen.
254    let mut in_quote: Option<char> = None;
255    for (i, ch) in input.char_indices() {
256        if let Some(q) = in_quote {
257            if ch == q {
258                in_quote = None;
259            }
260            continue;
261        }
262        match ch {
263            QUOTE_DOUBLE | QUOTE_SINGLE => in_quote = Some(ch),
264            ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
265            ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
266                depth = depth.saturating_sub(1);
267            }
268            COMMA if depth == 0 => {
269                entries.push(&input[start..i]);
270                start = i + 1;
271            }
272            _ => {}
273        }
274    }
275    entries.push(&input[start..]);
276    entries
277}
278
279/// Find the first occurrence of `target` at bracket-depth 0.
280pub(crate) fn find_char_at_depth_zero(input: &str, target: char) -> Option<usize> {
281    use crate::consts::{
282        ANGLE_CLOSE, ANGLE_OPEN, BRACE_CLOSE, BRACE_OPEN, BRACKET_CLOSE, BRACKET_OPEN, PAREN_CLOSE,
283        PAREN_OPEN,
284    };
285    let mut depth: u32 = 0;
286    for (i, ch) in input.char_indices() {
287        match ch {
288            ANGLE_OPEN | BRACKET_OPEN | PAREN_OPEN | BRACE_OPEN => depth += 1,
289            ANGLE_CLOSE | BRACKET_CLOSE | PAREN_CLOSE | BRACE_CLOSE => {
290                depth = depth.saturating_sub(1);
291            }
292            c if c == target && depth == 0 => return Some(i),
293            _ => {}
294        }
295    }
296    None
297}
298
299/// Find the position of `:=` at bracket-depth zero.
300fn find_assign_default_at_depth_zero(input: &str) -> Option<usize> {
301    use crate::consts::{
302        ANGLE_CLOSE_BYTE, ANGLE_OPEN_BYTE, BRACE_CLOSE_BYTE, BRACE_OPEN_BYTE, BRACKET_CLOSE_BYTE,
303        BRACKET_OPEN_BYTE, COLON_BYTE, EQUALS_BYTE, PAREN_CLOSE_BYTE, PAREN_OPEN_BYTE,
304    };
305    let mut depth: u32 = 0;
306    let bytes = input.as_bytes();
307    for (i, &b) in bytes.iter().enumerate() {
308        match b {
309            ANGLE_OPEN_BYTE | BRACKET_OPEN_BYTE | PAREN_OPEN_BYTE | BRACE_OPEN_BYTE => depth += 1,
310            ANGLE_CLOSE_BYTE | BRACKET_CLOSE_BYTE | PAREN_CLOSE_BYTE | BRACE_CLOSE_BYTE => {
311                depth = depth.saturating_sub(1);
312            }
313            COLON_BYTE if depth == 0 && bytes.get(i + 1) == Some(&EQUALS_BYTE) => return Some(i),
314            _ => {}
315        }
316    }
317    None
318}
319
320/// Parse a type annotation string into a [`VarType`].
321///
322/// Supported forms:
323/// - `str` → [`VarType::Str`]
324/// - `bool` → [`VarType::Bool`]
325/// - `int` → [`VarType::Int`]
326/// - `float` → [`VarType::Float`]
327/// - `list(name = str, count = int)` → [`VarType::List`] with field declarations
328/// - `struct(key = str)` → [`VarType::Struct`] with field declarations
329/// - `enum(A, B(field = type))` → [`VarType::Enum`] with variant declarations
330///
331/// # Errors
332///
333/// Returns an error string if the type annotation is malformed or
334/// references an unknown type name.
335fn starts_with_compound_type(s: &str, keyword: &str) -> bool {
336    if let Some(rest) = s.strip_prefix(keyword) {
337        let rest = rest.trim_start();
338        rest.starts_with(crate::consts::PAREN_OPEN)
339    } else {
340        false
341    }
342}
343
344/// Parses a type annotation string into a `VarType`.
345///
346/// # Errors
347/// Returns an error string if the type annotation syntax is invalid or references an unknown type alias.
348pub fn parse_type_annotation(
349    s: &str,
350    type_aliases: &HashMap<String, VarType>,
351    resolved_imports: &HashMap<String, ImportedNamespace>,
352) -> Result<VarType, String> {
353    use crate::consts::{
354        ANGLE_OPEN, BRACKET_OPEN, ERR_COMPOUND_BRACKETS_PROHIBITED, TYPE_BOOL, TYPE_ENUM,
355        TYPE_FLOAT, TYPE_INT, TYPE_LIST, TYPE_OPTION, TYPE_STR, TYPE_STRUCT, TYPE_TMPL,
356    };
357
358    let s = crate::consts::strip_string_literal(s.trim())
359        .unwrap_or(s.trim())
360        .trim();
361
362    for kw in &[TYPE_LIST, TYPE_STRUCT, TYPE_ENUM, TYPE_TMPL, TYPE_OPTION] {
363        if let Some(rest) = s.strip_prefix(kw) {
364            let rest_trimmed = rest.trim_start();
365            if rest_trimmed.starts_with(ANGLE_OPEN) || rest_trimmed.starts_with(BRACKET_OPEN) {
366                return Err(format!(
367                    "compound type '{kw}': {ERR_COMPOUND_BRACKETS_PROHIBITED}"
368                ));
369            }
370        }
371    }
372
373    // Check type aliases first (own or inherited).
374    if let Some(ty) = type_aliases.get(s) {
375        return Ok(ty.clone());
376    }
377
378    // Check dotted import paths: `stem.TypeName`.
379    if let Some(dot_pos) = s.find(crate::consts::PATH_SEP) {
380        let stem = &s[..dot_pos];
381        let type_name = &s[dot_pos + 1..];
382        if let Some(ns) = resolved_imports.get(stem) {
383            if let Some(ty) = ns.type_aliases.get(type_name) {
384                return Ok(ty.clone());
385            }
386            if let Some(ty) = ns.param_types.get(type_name) {
387                return Ok(ty.clone());
388            }
389            return Err(format!("import '{stem}' has no type '{type_name}'"));
390        }
391    }
392
393    if s == TYPE_STR {
394        Ok(VarType::Str)
395    } else if s == TYPE_BOOL {
396        Ok(VarType::Bool)
397    } else if s == TYPE_INT {
398        Ok(VarType::Int)
399    } else if s == TYPE_FLOAT {
400        Ok(VarType::Float)
401    } else if starts_with_compound_type(s, TYPE_LIST) {
402        parse_compound_type_list(s, type_aliases, resolved_imports)
403    } else if starts_with_compound_type(s, TYPE_STRUCT) {
404        parse_compound_type_struct(s, type_aliases, resolved_imports)
405    } else if starts_with_compound_type(s, TYPE_ENUM) {
406        parse_enum_type(s, type_aliases, resolved_imports)
407    } else if starts_with_compound_type(s, TYPE_TMPL) {
408        parse_tmpl_type(s, type_aliases, resolved_imports)
409    } else if starts_with_compound_type(s, TYPE_OPTION) {
410        parse_option_type(s, type_aliases, resolved_imports)
411    } else {
412        Err(format!("unknown type '{s}'"))
413    }
414}
415
416/// Parse an enum type like `enum(Confirmed(evidence = list(text = str)), Inconclusive)`.
417fn parse_enum_type(
418    s: &str,
419    type_aliases: &HashMap<String, VarType>,
420    resolved_imports: &HashMap<String, ImportedNamespace>,
421) -> Result<VarType, String> {
422    use crate::{consts::TYPE_ENUM, types::VariantDecl};
423
424    let rest = s.strip_prefix(TYPE_ENUM).unwrap_or("").trim();
425    let Some(inner) = strip_type_brackets(rest) else {
426        return Err(format!("malformed enum type: '{s}'"));
427    };
428    let entries = split_at_depth_zero(inner);
429    let mut variants = Vec::new();
430    for entry in entries {
431        let entry = entry.trim();
432        if entry.is_empty() {
433            continue;
434        }
435        if let (Some(open_idx), Some(close_idx)) = (
436            entry.find(crate::consts::PAREN_OPEN),
437            entry.rfind(crate::consts::PAREN_CLOSE),
438        ) {
439            let name = entry[..open_idx].trim().to_string();
440            let fields_str = &entry[open_idx + 1..close_idx];
441            let fields = parse_field_declarations(fields_str, type_aliases, resolved_imports)?;
442            if fields.iter().any(|f| f.name.is_empty()) {
443                return Err(
444                    "enum struct variant must use named fields (e.g. Variant(name = str))"
445                        .to_string(),
446                );
447            }
448            variants.push(VariantDecl { name, fields });
449            continue;
450        }
451        variants.push(VariantDecl {
452            name: entry.to_string(),
453            fields: vec![],
454        });
455    }
456    if variants.is_empty() {
457        return Err("enum must have at least one variant".to_string());
458    }
459    // Reject variant names that shadow builtin type keywords.
460    for v in &variants {
461        if crate::consts::RESERVED_NAMES.contains(&v.name.as_str()) {
462            return Err(format!(
463                "enum variant name '{}' shadows a builtin type keyword",
464                v.name
465            ));
466        }
467    }
468    Ok(VarType::Enum(variants))
469}
470
471/// Parse a compound type like `list(name = str, count = int)`.
472fn parse_compound_type_list(
473    s: &str,
474    type_aliases: &HashMap<String, VarType>,
475    resolved_imports: &HashMap<String, ImportedNamespace>,
476) -> Result<VarType, String> {
477    use crate::consts::TYPE_LIST;
478
479    let rest = s.strip_prefix(TYPE_LIST).unwrap_or("").trim();
480    let Some(inner) = strip_type_brackets(rest) else {
481        return Err(format!("malformed list type: '{s}'"));
482    };
483    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
484    if fields.is_empty() {
485        return Err("untyped list() is not allowed; must specify element type or fields (e.g., list(str) or list(name = str))".to_string());
486    }
487    if fields.len() > 1 && fields.iter().any(|f| f.name.is_empty()) {
488        return Err(
489            "list with multiple fields must use named fields (e.g. list(name = str, count = int))"
490                .to_string(),
491        );
492    }
493    // Reject literal raw struct declarations inside list definitions (e.g. list(struct(name = str, count = int))).
494    // Users should write named fields directly (e.g. list(name = str, count = int)) or reference a strong Type alias.
495    let inner_trimmed = inner.trim();
496    if inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_ANGLE_PREFIX)
497        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_PREFIX)
498        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_BRACKET_PREFIX)
499        || inner_trimmed.starts_with(crate::consts::TYPE_STRUCT_SPACE_PREFIX)
500    {
501        return Err(
502            "list(struct(..)) is redundant; use named fields directly: list(name = str, count = int)"
503                .to_string(),
504        );
505    }
506    // If the inner type resolved to a strong struct alias (e.g. list(MyStruct)),
507    // unwrap the struct fields directly into the list fields.
508    if fields.len() == 1 && fields[0].name.is_empty() {
509        if let VarType::Struct(ref struct_fields) = fields[0].var_type {
510            return Ok(VarType::List(struct_fields.clone()));
511        }
512    }
513    Ok(VarType::List(fields))
514}
515
516/// Parse a compound type like `struct(key = str, value = int)`.
517fn parse_compound_type_struct(
518    s: &str,
519    type_aliases: &HashMap<String, VarType>,
520    resolved_imports: &HashMap<String, ImportedNamespace>,
521) -> Result<VarType, String> {
522    use crate::consts::TYPE_STRUCT;
523
524    let rest = s.strip_prefix(TYPE_STRUCT).unwrap_or("").trim();
525    let Some(inner) = strip_type_brackets(rest) else {
526        return Err(format!("malformed struct type: '{s}'"));
527    };
528    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
529    if fields.is_empty() {
530        return Err(
531            "untyped struct() is not allowed; must specify fields (e.g., struct(name = str))"
532                .to_string(),
533        );
534    }
535    if fields.iter().any(|f| f.name.is_empty()) {
536        return Err(
537            "struct must use named fields (e.g. struct(name = str, count = int))".to_string(),
538        );
539    }
540    Ok(VarType::Struct(fields))
541}
542
543/// Parse a tmpl type like `tmpl(name = str, count = int)`.
544fn parse_tmpl_type(
545    s: &str,
546    type_aliases: &HashMap<String, VarType>,
547    resolved_imports: &HashMap<String, ImportedNamespace>,
548) -> Result<VarType, String> {
549    use crate::consts::TYPE_TMPL;
550
551    let rest = s.strip_prefix(TYPE_TMPL).unwrap_or("").trim();
552    let Some(inner) = strip_type_brackets(rest) else {
553        return Err(format!("malformed tmpl type: '{s}'"));
554    };
555    let fields = parse_field_declarations(inner, type_aliases, resolved_imports)?;
556    if fields.iter().any(|f| f.name.is_empty()) {
557        return Err("tmpl must use named fields (e.g. tmpl(name = str, count = int))".to_string());
558    }
559    Ok(VarType::Tmpl(fields))
560}
561
562/// Parse `option(T)` into [`VarType::Option`].
563fn parse_option_type(
564    s: &str,
565    type_aliases: &HashMap<String, VarType>,
566    resolved_imports: &HashMap<String, ImportedNamespace>,
567) -> Result<VarType, String> {
568    use crate::consts::TYPE_OPTION;
569
570    let rest = s.strip_prefix(TYPE_OPTION).unwrap_or("").trim();
571    let Some(inner) = strip_type_brackets(rest) else {
572        return Err(format!("malformed option type: '{s}'"));
573    };
574    let inner = inner.trim();
575    if inner.is_empty() {
576        return Err("option() requires an inner type (e.g. option(str))".to_string());
577    }
578    let inner_type = parse_type_annotation(inner, type_aliases, resolved_imports)?;
579    Ok(VarType::Option(Box::new(inner_type)))
580}
581
582/// Parse field declarations like `name = str, count = int` into [`VarDecl`]s.
583fn parse_field_declarations(
584    inner: &str,
585    type_aliases: &HashMap<String, VarType>,
586    resolved_imports: &HashMap<String, ImportedNamespace>,
587) -> Result<Vec<VarDecl>, String> {
588    let entries = split_at_depth_zero(inner);
589    let mut decls = Vec::new();
590    for f in &entries {
591        let f = f.trim();
592        if f.is_empty() {
593            continue;
594        }
595        let (name, type_str) =
596            if let Some(eq_pos) = find_char_at_depth_zero(f, crate::consts::EQUALS) {
597                (f[..eq_pos].trim().to_string(), f[eq_pos + 1..].trim())
598            } else {
599                (String::new(), f)
600            };
601        let var_type = parse_type_annotation(type_str, type_aliases, resolved_imports)?;
602        decls.push(VarDecl {
603            name,
604            var_type,
605            default_value: None,
606        });
607    }
608    Ok(decls)
609}
610
611/// Parse the *inner* content of a `{key = value, ...}` struct default into
612/// a [`Value::Struct`].
613///
614/// Uses `=` as the key-value separator (not `:`) and curly braces for
615/// delimiters.
616fn parse_struct_default(
617    inner: &str,
618    fields: &[VarDecl],
619    available_consts: &HashMap<String, Value>,
620    type_aliases: &HashMap<String, VarType>,
621    resolved_imports: &HashMap<String, ImportedNamespace>,
622) -> Value {
623    let entries = split_at_depth_zero(inner);
624    let mut map = HashMap::new();
625    for e in entries {
626        let e = e.trim();
627        if e.is_empty() {
628            continue;
629        }
630        if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
631            let key = e[..eq_pos].trim();
632            let val_str = e[eq_pos + 1..].trim();
633            let field_type = fields
634                .iter()
635                .find(|d| d.name == key)
636                .map_or(&VarType::Str, |d| &d.var_type);
637            if let Some(v) = parse_default_value_full(
638                val_str,
639                field_type,
640                available_consts,
641                type_aliases,
642                resolved_imports,
643            ) {
644                map.insert(key.to_string(), v);
645            }
646        }
647    }
648    Value::Struct(Arc::new(map))
649}
650
651/// Resolve a const name used as a default value.
652///
653/// Looks up `name` in the available constants map, supporting both local
654/// const names (e.g. `MAX`) and imported const names (e.g. `lib.LIMIT`).
655/// Returns a clone of the const value if found.
656fn resolve_const_default(name: &str, available_consts: &HashMap<String, Value>) -> Option<Value> {
657    let name = name.trim();
658    if name.is_empty() {
659        return None;
660    }
661    available_consts.get(name).cloned()
662}
663
664/// Resolve a function expression default like `kinds(EnumType)` into a [`Value::List`].
665fn resolve_kinds_default(
666    expr: &str,
667    type_aliases: &HashMap<String, VarType>,
668    resolved_imports: &HashMap<String, ImportedNamespace>,
669) -> Option<Value> {
670    let s = expr.trim();
671    let inner = s
672        .strip_prefix(crate::consts::FN_KINDS)?
673        .strip_prefix(crate::consts::PAREN_OPEN)?
674        .strip_suffix(crate::consts::PAREN_CLOSE)?
675        .trim();
676    if inner.is_empty() {
677        return None;
678    }
679    let var_type = if let Some(dot_pos) = inner.find(crate::consts::PATH_SEP) {
680        let ns_name = &inner[..dot_pos];
681        let type_name = &inner[dot_pos + 1..];
682        resolved_imports.get(ns_name)?.type_aliases.get(type_name)
683    } else {
684        type_aliases.get(inner)
685    };
686    if let Some(VarType::Enum(variants)) = var_type {
687        let list: Vec<Value> = variants
688            .iter()
689            .map(|v| Value::Str(v.name.clone()))
690            .collect();
691        Some(Value::List(Arc::new(list)))
692    } else {
693        None
694    }
695}
696
697/// Parse a default value string into a [`Value`].
698///
699/// Supports:
700/// - Inline lists: `[1, 2, 3]` or `['a', 'b']`
701/// - Inline structs: `{key = value, key2 = value2}`
702/// - List of structs: `[{k = v1}, {k = v2}]`
703/// - Quoted strings: `"hello"` or `'hello'`
704/// - Integers, floats, booleans
705///
706/// Lists use `[]` and structs use `{}` with `=` as the key-value separator.
707#[cfg(test)]
708pub(crate) fn parse_default_value_with_type(
709    s: &str,
710    var_type: &VarType,
711    available_consts: &HashMap<String, Value>,
712) -> Option<Value> {
713    let empty_aliases = HashMap::new();
714    let empty_imports = HashMap::new();
715    parse_default_value_full(
716        s,
717        var_type,
718        available_consts,
719        &empty_aliases,
720        &empty_imports,
721    )
722}
723
724pub(crate) fn parse_default_value_full(
725    s: &str,
726    var_type: &VarType,
727    available_consts: &HashMap<String, Value>,
728    type_aliases: &HashMap<String, VarType>,
729    resolved_imports: &HashMap<String, ImportedNamespace>,
730) -> Option<Value> {
731    let s = s.trim();
732    if s.is_empty() {
733        return None;
734    }
735
736    // Handle list defaults: [a, b, c]
737    if s.starts_with(crate::consts::BRACKET_OPEN) && s.ends_with(crate::consts::BRACKET_CLOSE) {
738        let inner = &s[1..s.len() - 1];
739        if inner.trim().is_empty() {
740            return Some(Value::List(Arc::new(Vec::new())));
741        }
742        let entries = split_at_depth_zero(inner);
743        let mut list = Vec::new();
744        let elem_type = match var_type {
745            VarType::List(fields) => {
746                if fields.len() == 1 && fields[0].name.is_empty() {
747                    &fields[0].var_type
748                } else {
749                    var_type
750                }
751            }
752            _ => var_type,
753        };
754        for e in entries {
755            if let Some(v) = parse_default_value_full(
756                e,
757                elem_type,
758                available_consts,
759                type_aliases,
760                resolved_imports,
761            ) {
762                list.push(v);
763            }
764        }
765        return Some(Value::List(Arc::new(list)));
766    }
767
768    // Handle struct defaults: {key = value, ...}
769    if s.starts_with('{') && s.ends_with('}') {
770        let inner = &s[1..s.len() - 1].trim();
771        if inner.is_empty() {
772            return match var_type {
773                VarType::Struct(_) => Some(Value::Struct(Arc::new(HashMap::new()))),
774                _ => None,
775            };
776        }
777
778        let fields = match var_type {
779            VarType::Struct(f) | VarType::List(f) => f.as_slice(),
780            _ => &[],
781        };
782        return Some(parse_struct_default(
783            inner,
784            fields,
785            available_consts,
786            type_aliases,
787            resolved_imports,
788        ));
789    }
790
791    // Quoted string
792    if let Some(inner) = crate::consts::strip_string_literal(s) {
793        return Some(Value::Str(inner.to_string()));
794    }
795
796    // Boolean
797    if s == crate::consts::LIT_TRUE {
798        return Some(Value::Bool(true));
799    }
800    if s == crate::consts::LIT_FALSE {
801        return Some(Value::Bool(false));
802    }
803
804    // Integer
805    if let Ok(n) = s.parse::<i64>() {
806        return Some(Value::Int(n));
807    }
808
809    // Float
810    if let Ok(n) = s.parse::<f64>() {
811        return Some(Value::Float(n));
812    }
813
814    // Handle option(T) defaults: None maps to Value::None, otherwise delegate
815    // to the inner type.
816    if let VarType::Option(inner) = var_type {
817        if s == crate::consts::OPTION_NONE {
818            return Some(Value::None);
819        }
820        return parse_default_value_full(
821            s,
822            inner,
823            available_consts,
824            type_aliases,
825            resolved_imports,
826        );
827    }
828
829    // If the expected type is an Enum, handle variant identifiers.
830    if let VarType::Enum(variants) = var_type {
831        return parse_enum_default_value(
832            s,
833            variants,
834            available_consts,
835            type_aliases,
836            resolved_imports,
837        );
838    }
839
840    if let Some(val) = resolve_const_default(s, available_consts) {
841        return Some(val);
842    }
843    if let Some(val) = resolve_kinds_default(s, type_aliases, resolved_imports) {
844        return Some(val);
845    }
846
847    // Intentional removal of fallback: unquoted strings are no longer allowed
848    // as default values. All string defaults must be explicitly quoted.
849    None
850}
851
852/// Parse a default value for an enum variant — either a unit variant name
853/// (e.g. `Active`) or a struct variant with fields (e.g. `Error(msg = "oops")`).
854fn parse_enum_default_value(
855    s: &str,
856    variants: &[crate::types::VariantDecl],
857    available_consts: &HashMap<String, Value>,
858    type_aliases: &HashMap<String, VarType>,
859    resolved_imports: &HashMap<String, ImportedNamespace>,
860) -> Option<Value> {
861    // Check for struct variant default: VariantName(field = value, ...)
862    // Uses () to match the type declaration syntax and avoid ambiguity
863    // with <> which is used for struct/list defaults.
864    if let Some(open_pos) = s.find(crate::consts::PAREN_OPEN) {
865        if s.ends_with(crate::consts::PAREN_CLOSE) {
866            let variant_name = s[..open_pos].trim();
867            let inner = &s[open_pos + 1..s.len() - 1];
868            // Find the variant declaration.
869            let variant = variants.iter().find(|v| v.name == variant_name);
870            match variant {
871                Some(v) if v.fields.is_empty() => {
872                    return None; // Unit variant can't have fields
873                }
874                Some(v) => {
875                    // Parse field values and build a tagged dict.
876                    let entries = split_at_depth_zero(inner);
877                    let mut map = HashMap::new();
878                    map.insert(
879                        crate::consts::ENUM_TAG_KEY.to_string(),
880                        Value::Str(variant_name.to_string()),
881                    );
882                    for e in entries {
883                        let e = e.trim();
884                        if e.is_empty() {
885                            continue;
886                        }
887                        if let Some(eq_pos) = find_char_at_depth_zero(e, crate::consts::EQUALS) {
888                            let key = e[..eq_pos].trim();
889                            let val_str = e[eq_pos + 1..].trim();
890                            let field_type = v
891                                .fields
892                                .iter()
893                                .find(|f| f.name == key)
894                                .map_or(&VarType::Str, |f| &f.var_type);
895                            if let Some(val) = parse_default_value_full(
896                                val_str,
897                                field_type,
898                                available_consts,
899                                type_aliases,
900                                resolved_imports,
901                            ) {
902                                map.insert(key.to_string(), val);
903                            }
904                        }
905                    }
906                    return Some(Value::Struct(Arc::new(map)));
907                }
908                None => return None, // Unknown variant
909            }
910        }
911    }
912
913    // Bare identifier — must be a known unit variant.
914    let variant = variants.iter().find(|v| v.name == s);
915    match variant {
916        Some(v) if !v.fields.is_empty() => {
917            // Struct variant without fields — reject.
918            None
919        }
920        Some(_) => Some(Value::Str(s.to_string())),
921        None => None, // Unknown variant name
922    }
923}
924
925#[cfg(test)]
926pub(crate) fn parse_default_value(s: &str) -> Option<Value> {
927    parse_default_value_with_type(s, &VarType::Str, &HashMap::new())
928}
929
930#[cfg(test)]
931#[path = "params_tests.rs"]
932mod tests;