use super::context::Frame;
use super::utils::{
check_duplicate_key, insert_into_current, parse_quoted_string, validate_indent_for_child,
validate_nested_list_indent,
};
use crate::document::{Item, Node};
use crate::error::{HedlError, HedlResult};
use crate::inference::{infer_quoted_value, infer_value, InferenceContext};
use crate::lex::row::parse_csv_row;
use crate::lex::{is_valid_key_token, is_valid_type_name, strip_comment};
use crate::limits::Limits;
use crate::reference::{register_node, TypeRegistry};
use crate::value::Value;
use std::collections::BTreeMap;
pub(super) struct MatrixParseParams<'a> {
pub content: &'a str,
pub indent: usize,
pub line_num: usize,
pub header: &'a crate::header::Header,
pub limits: &'a Limits,
}
pub(super) fn parse_non_matrix_line(
stack: &mut Vec<Frame>,
content: &str,
indent: usize,
line_num: usize,
header: &crate::header::Header,
limits: &Limits,
total_keys: &mut usize,
) -> HedlResult<()> {
let content = strip_comment(content);
let colon_pos = content
.find(':')
.ok_or_else(|| HedlError::syntax("expected ':' in line", line_num))?;
let key_with_hint = content[..colon_pos].trim();
let after_colon = &content[colon_pos + 1..];
let (key, count_hint) = parse_key_with_count_hint(key_with_hint, line_num)?;
if !is_valid_key_token(&key) {
return Err(HedlError::syntax(format!("invalid key: {}", key), line_num));
}
check_duplicate_key(stack, &key, line_num, limits, total_keys)?;
let after_colon_trimmed = after_colon.trim();
if after_colon_trimmed.is_empty() {
if count_hint.is_some() {
return Err(HedlError::syntax(
"count hint not allowed on object declarations",
line_num,
));
}
validate_indent_for_child(stack, indent, line_num)?;
stack.push(Frame::Object {
indent,
key: key.to_string(),
object: BTreeMap::new(),
});
} else if after_colon_trimmed.starts_with('@') && is_list_start(after_colon_trimmed) {
let parent_list_idx = validate_nested_list_indent(stack, indent, line_num)?;
let (type_name, schema) = parse_list_start(after_colon_trimmed, line_num, header, limits)?;
if let Some(_parent_idx) = parent_list_idx {
let capacity = count_hint.unwrap_or(0).min(limits.max_nodes);
stack.push(Frame::List {
row_indent: indent + 1,
type_name,
schema,
last_row_values: None,
list: Vec::with_capacity(capacity),
key: key.to_string(),
count_hint,
});
} else {
let capacity = count_hint.unwrap_or(0).min(limits.max_nodes);
stack.push(Frame::List {
row_indent: indent + 1,
type_name,
schema,
last_row_values: None,
list: Vec::with_capacity(capacity),
key: key.to_string(),
count_hint,
});
}
} else {
if count_hint.is_some() {
return Err(HedlError::syntax(
"count hint not allowed on scalar values",
line_num,
));
}
if !after_colon.starts_with(' ') {
return Err(HedlError::syntax(
"space required after ':' in key-value",
line_num,
));
}
validate_indent_for_child(stack, indent, line_num)?;
let value_str = after_colon.trim();
let ctx = InferenceContext::for_key_value(&header.aliases)
.with_version(header.version)
.with_null_char(header.null_char);
let value = if value_str.starts_with('"') {
let inner = parse_quoted_string(value_str, line_num)?;
infer_quoted_value(&inner)
} else {
infer_value(value_str, &ctx, line_num)?
};
insert_into_current(stack, key.to_string(), Item::Scalar(value));
}
Ok(())
}
pub(super) fn parse_key_with_count_hint(
key: &str,
line_num: usize,
) -> HedlResult<(String, Option<usize>)> {
if let Some(paren_pos) = key.find('(') {
let key_part = &key[..paren_pos];
if !key.ends_with(')') {
return Err(HedlError::syntax(
"unclosed count hint parenthesis",
line_num,
));
}
let count_str = &key[paren_pos + 1..key.len() - 1];
let count = count_str.parse::<usize>().map_err(|_| {
HedlError::syntax(format!("invalid count hint: '{}'", count_str), line_num)
})?;
if count == 0 {
return Err(HedlError::syntax(
"count hint must be greater than zero",
line_num,
));
}
Ok((key_part.to_string(), Some(count)))
} else {
Ok((key.to_string(), None))
}
}
pub(super) fn is_list_start(s: &str) -> bool {
let s = s.trim();
if !s.starts_with('@') {
return false;
}
let rest = &s[1..];
let type_end = rest
.find(|c: char| c == '[' || c.is_whitespace())
.unwrap_or(rest.len());
let type_name = &rest[..type_end];
is_valid_type_name(type_name)
}
pub(super) fn parse_list_start(
s: &str,
line_num: usize,
header: &crate::header::Header,
limits: &Limits,
) -> HedlResult<(String, Vec<String>)> {
let s = s.trim();
let rest = &s[1..];
if let Some(bracket_pos) = rest.find('[') {
let type_name = &rest[..bracket_pos];
if !is_valid_type_name(type_name) {
return Err(HedlError::syntax(
format!("invalid type name: {}", type_name),
line_num,
));
}
let schema_str = &rest[bracket_pos..];
let schema = parse_inline_schema(schema_str, line_num, limits)?;
if let Some(declared) = header.structs.get(type_name) {
if declared != &schema {
return Err(HedlError::schema(
format!(
"inline schema for '{}' doesn't match declared schema",
type_name
),
line_num,
));
}
}
Ok((type_name.to_string(), schema))
} else {
let type_name = rest.trim();
if !is_valid_type_name(type_name) {
return Err(HedlError::syntax(
format!("invalid type name: {}", type_name),
line_num,
));
}
let schema = header
.structs
.get(type_name)
.ok_or_else(|| HedlError::schema(format!("undefined type: {}", type_name), line_num))?;
Ok((type_name.to_string(), schema.clone()))
}
}
pub(super) fn parse_inline_schema(
s: &str,
line_num: usize,
limits: &Limits,
) -> HedlResult<Vec<String>> {
if !s.starts_with('[') || !s.ends_with(']') {
return Err(HedlError::syntax("invalid inline schema format", line_num));
}
let inner = &s[1..s.len() - 1];
let estimated_count = inner.matches(',').count() + 1;
let mut columns = Vec::with_capacity(estimated_count.min(limits.max_columns));
for part in inner.split(',') {
let col = part.trim();
if col.is_empty() {
continue;
}
if !is_valid_key_token(col) {
return Err(HedlError::syntax(
format!("invalid column name: {}", col),
line_num,
));
}
columns.push(col.to_string());
}
if columns.is_empty() {
return Err(HedlError::syntax("empty inline schema", line_num));
}
if columns.len() > limits.max_columns {
return Err(HedlError::security(
format!("too many columns: {}", columns.len()),
line_num,
));
}
Ok(columns)
}
pub(super) fn parse_row_prefix(
content: &str,
line_num: usize,
version: (u32, u32),
) -> HedlResult<(Option<usize>, &str)> {
if !content.starts_with('|') {
return Err(HedlError::syntax(
"matrix row must start with '|'",
line_num,
));
}
let rest = &content[1..];
if version >= (2, 0) && rest.starts_with(' ') {
return Err(HedlError::syntax(
"v2.0 does not allow space after '|' in matrix rows",
line_num,
));
}
if rest.starts_with('[') {
if let Some(bracket_end) = rest.find(']') {
let count_str = &rest[1..bracket_end];
if let Ok(count) = count_str.parse::<usize>() {
if version >= (2, 0) {
return Err(HedlError::syntax(
"v2.0 does not allow inline count hints |[N], use %C: header directives instead",
line_num,
));
}
let data = rest[bracket_end + 1..].trim_start();
return Ok((Some(count), data));
}
}
}
Ok((None, rest))
}
pub(super) fn parse_matrix_row(
stack: &mut Vec<Frame>,
params: &MatrixParseParams<'_>,
type_registries: &mut TypeRegistry,
node_count: &mut usize,
) -> HedlResult<()> {
let MatrixParseParams {
content,
indent,
line_num,
header,
limits,
} = params;
let (indent, line_num) = (*indent, *line_num);
let (child_count, csv_content) = parse_row_prefix(content, line_num, header.version)?;
let csv_content = strip_comment(csv_content).trim();
let fields =
parse_csv_row(csv_content).map_err(|e| HedlError::syntax(e.to_string(), line_num))?;
let list_frame_idx =
find_list_frame(stack, indent, line_num, header, limits, Some(fields.len()))?;
let (schema_len, type_name_owned, prev_row_clone) = {
let frame = &stack[list_frame_idx];
match frame {
Frame::List {
type_name,
schema,
last_row_values,
..
} => (schema.len(), type_name.clone(), last_row_values.clone()),
_ => unreachable!(),
}
};
if fields.len() != schema_len {
return Err(HedlError::shape(
format!("expected {} columns, got {}", schema_len, fields.len()),
line_num,
));
}
let mut values = Vec::with_capacity(fields.len());
for (col_idx, field) in fields.iter().enumerate() {
let ctx = InferenceContext::for_matrix_cell(
&header.aliases,
col_idx,
prev_row_clone.as_deref(),
&type_name_owned,
)
.with_version(header.version)
.with_null_char(header.null_char);
let value = if field.is_quoted {
infer_quoted_value(&field.value)
} else {
infer_value(&field.value, &ctx, line_num)?
};
values.push(value);
}
let id = match &values[0] {
Value::String(s) => s.clone(),
_ => {
return Err(HedlError::semantic("ID column must be a string", line_num));
}
};
register_node(type_registries, &type_name_owned, &id, line_num, limits)?;
*node_count = node_count
.checked_add(1)
.ok_or_else(|| HedlError::security("node count overflow", line_num))?;
if *node_count > limits.max_nodes {
return Err(HedlError::security(
format!("too many nodes: exceeds limit of {}", limits.max_nodes),
line_num,
));
}
if let Frame::List {
last_row_values,
list,
..
} = &mut stack[list_frame_idx]
{
*last_row_values = Some(values.clone());
let mut node = Node::new(&type_name_owned, &*id, values);
if let Some(count) = child_count {
node.set_child_count(count);
}
list.push(node);
}
Ok(())
}
pub(super) fn is_inline_child_list(content: &str) -> bool {
if !content.starts_with('@') {
return false;
}
if let Some(hash_pos) = content.find('#') {
if let Some(colon_pos) = content[hash_pos..].find(":|") {
let count_str = &content[hash_pos + 1..hash_pos + colon_pos];
return count_str.chars().all(|c| c.is_ascii_digit()) && !count_str.is_empty();
}
}
false
}
pub(super) fn is_expanded_child_list(content: &str) -> bool {
if !content.starts_with('@') {
return false;
}
let trimmed = content.trim();
if !trimmed.ends_with(':') || trimmed.ends_with(":|") {
return false;
}
if let Some(hash_pos) = trimmed.find('#') {
let colon_pos = trimmed.len() - 1;
if hash_pos < colon_pos {
let count_str = &trimmed[hash_pos + 1..colon_pos];
return count_str.chars().all(|c| c.is_ascii_digit()) && !count_str.is_empty();
}
}
false
}
pub(super) fn parse_expanded_child_list(
stack: &mut Vec<Frame>,
content: &str,
indent: usize,
line_num: usize,
header: &crate::header::Header,
limits: &Limits,
) -> HedlResult<()> {
let trimmed = content.trim();
let hash_pos = trimmed
.find('#')
.ok_or_else(|| HedlError::syntax("expected '#' in expanded child list", line_num))?;
let type_name = &trimmed[1..hash_pos];
if !is_valid_type_name(type_name) {
return Err(HedlError::syntax(
format!("invalid type name in expanded child list: {}", type_name),
line_num,
));
}
let colon_pos = trimmed.len() - 1;
let count_str = &trimmed[hash_pos + 1..colon_pos];
let declared_count: usize = count_str.parse().map_err(|_| {
HedlError::syntax(
format!("invalid count in expanded child list: {}", count_str),
line_num,
)
})?;
let child_schema = header.structs.get(type_name).ok_or_else(|| {
HedlError::schema(
format!("type '{}' not defined in expanded child list", type_name),
line_num,
)
})?;
let parent_list_idx = find_parent_list_for_inline_children(stack, indent, line_num)?;
let parent_type_name = {
let frame = &stack[parent_list_idx];
match frame {
Frame::List { type_name, .. } => type_name.clone(),
_ => unreachable!(),
}
};
let child_types = header.nests.get(&parent_type_name).ok_or_else(|| {
HedlError::schema(
format!(
"no NEST rule for parent type '{}' to child type '{}'",
parent_type_name, type_name
),
line_num,
)
})?;
if !child_types.iter().any(|s| s == type_name) {
return Err(HedlError::schema(
format!(
"type '{}' is not a declared child of '{}' (allowed: {:?})",
type_name, parent_type_name, child_types
),
line_num,
));
}
let current_depth = stack
.iter()
.filter(|f| matches!(f, Frame::List { .. }))
.count();
if current_depth >= limits.max_nest_depth {
return Err(HedlError::security(
format!(
"NEST hierarchy depth {} exceeds maximum allowed depth {}",
current_depth + 1,
limits.max_nest_depth
),
line_num,
));
}
let capacity = declared_count.min(limits.max_nodes);
stack.push(Frame::List {
row_indent: indent + 1,
type_name: type_name.to_string(),
schema: child_schema.clone(),
last_row_values: None,
list: Vec::with_capacity(capacity),
key: type_name.to_string(),
count_hint: Some(declared_count),
});
Ok(())
}
pub(super) fn parse_inline_child_list(
stack: &mut [Frame],
params: &MatrixParseParams<'_>,
type_registries: &mut TypeRegistry,
node_count: &mut usize,
) -> HedlResult<()> {
let MatrixParseParams {
content,
indent,
line_num,
header,
limits,
} = params;
let (indent, line_num) = (*indent, *line_num);
let hash_pos = content
.find('#')
.ok_or_else(|| HedlError::syntax("expected '#' in inline child list", line_num))?;
let type_name = &content[1..hash_pos];
if !is_valid_type_name(type_name) {
return Err(HedlError::syntax(
format!("invalid type name in inline child list: {}", type_name),
line_num,
));
}
let colon_pipe_pos = content
.find(":|")
.ok_or_else(|| HedlError::syntax("expected ':|' in inline child list", line_num))?;
let count_str = &content[hash_pos + 1..colon_pipe_pos];
let declared_count: usize = count_str.parse().map_err(|_| {
HedlError::syntax(
format!("invalid count in inline child list: {}", count_str),
line_num,
)
})?;
let children_data = strip_comment(&content[colon_pipe_pos + 2..]);
let child_rows: Vec<&str> = if children_data.is_empty() {
Vec::new()
} else {
crate::lex::csv::split_inline_children(children_data, header.quote_char)
.map_err(|e| HedlError::syntax(e.to_string(), line_num))?
};
if child_rows.len() != declared_count {
return Err(HedlError::syntax(
format!(
"inline child count mismatch: declared {} but found {}",
declared_count,
child_rows.len()
),
line_num,
));
}
let child_schema = header.structs.get(type_name).ok_or_else(|| {
HedlError::schema(
format!("type '{}' not defined in inline child list", type_name),
line_num,
)
})?;
let parent_list_idx = find_parent_list_for_inline_children(stack, indent, line_num)?;
let parent_type_name = {
let frame = &stack[parent_list_idx];
match frame {
Frame::List { type_name, .. } => type_name.clone(),
_ => unreachable!(),
}
};
let child_types = header.nests.get(&parent_type_name).ok_or_else(|| {
HedlError::schema(
format!(
"no NEST rule for parent type '{}' to child type '{}'",
parent_type_name, type_name
),
line_num,
)
})?;
if !child_types.iter().any(|s| s == type_name) {
return Err(HedlError::schema(
format!(
"type '{}' is not a declared child of '{}' (allowed: {:?})",
type_name, parent_type_name, child_types
),
line_num,
));
}
let current_depth = stack
.iter()
.filter(|f| matches!(f, Frame::List { .. }))
.count();
if current_depth >= limits.max_nest_depth {
return Err(HedlError::security(
format!(
"NEST hierarchy depth {} exceeds maximum allowed depth {}",
current_depth + 1,
limits.max_nest_depth
),
line_num,
));
}
let mut child_nodes = Vec::with_capacity(declared_count);
let mut last_row_values: Option<Vec<Value>> = None;
for child_csv in &child_rows {
let child_csv = child_csv.trim();
if child_csv.is_empty() {
continue;
}
let fields =
parse_csv_row(child_csv).map_err(|e| HedlError::syntax(e.to_string(), line_num))?;
if fields.len() != child_schema.len() {
return Err(HedlError::shape(
format!(
"expected {} columns for type '{}', got {}",
child_schema.len(),
type_name,
fields.len()
),
line_num,
));
}
let mut values = Vec::with_capacity(fields.len());
for (col_idx, field) in fields.iter().enumerate() {
let ctx = InferenceContext::for_matrix_cell(
&header.aliases,
col_idx,
last_row_values.as_deref(),
type_name,
)
.with_version(header.version)
.with_null_char(header.null_char);
let value = if field.is_quoted {
infer_quoted_value(&field.value)
} else {
infer_value(&field.value, &ctx, line_num)?
};
values.push(value);
}
let id = match &values[0] {
Value::String(s) => s.clone(),
_ => {
return Err(HedlError::semantic(
"ID column must be a string in inline child",
line_num,
));
}
};
register_node(type_registries, type_name, &id, line_num, limits)?;
*node_count = node_count
.checked_add(1)
.ok_or_else(|| HedlError::security("node count overflow", line_num))?;
if *node_count > limits.max_nodes {
return Err(HedlError::security(
format!("too many nodes: exceeds limit of {}", limits.max_nodes),
line_num,
));
}
let node = Node::new(type_name, &*id, values.clone());
child_nodes.push(node);
last_row_values = Some(values);
}
if let Frame::List { list, .. } = &mut stack[parent_list_idx] {
if let Some(parent_node) = list.last_mut() {
let children = parent_node
.children
.get_or_insert_with(|| Box::new(BTreeMap::new()));
children
.entry(type_name.to_string())
.or_default()
.extend(child_nodes);
} else {
return Err(HedlError::orphan_row(
"inline child list has no parent row to attach to",
line_num,
));
}
}
Ok(())
}
pub(super) fn find_parent_list_for_inline_children(
stack: &[Frame],
indent: usize,
line_num: usize,
) -> HedlResult<usize> {
for (idx, frame) in stack.iter().enumerate().rev() {
if let Frame::List {
row_indent, list, ..
} = frame
{
if indent == *row_indent + 1 {
if list.is_empty() {
return Err(HedlError::orphan_row(
"inline child list has no parent row",
line_num,
));
}
return Ok(idx);
}
}
}
Err(HedlError::syntax(
"inline child list outside of list context",
line_num,
))
}
pub(super) fn find_list_frame(
stack: &mut Vec<Frame>,
indent: usize,
line_num: usize,
header: &crate::header::Header,
limits: &Limits,
field_count: Option<usize>,
) -> HedlResult<usize> {
for (idx, frame) in stack.iter().enumerate().rev() {
if let Frame::List {
row_indent,
type_name,
list,
..
} = frame
{
if indent == *row_indent {
if let Some(fc) = field_count {
let schema = header.structs.get(type_name);
if schema.is_some_and(|s| s.len() == fc) {
return Ok(idx);
}
let parent_row_indent = indent - 1;
let parent_frame = stack.iter().rev().find(|f| {
matches!(f, Frame::List { row_indent: ri, .. } if *ri == parent_row_indent)
});
if let Some(Frame::List {
type_name: parent_type,
..
}) = parent_frame
{
if let Some(child_types) = header.nests.get(parent_type) {
let matching_type = child_types
.iter()
.find(|ct| header.structs.get(*ct).is_some_and(|s| s.len() == fc));
if let Some(new_type) = matching_type {
let new_schema = header
.structs
.get(new_type)
.expect("matching_type exists in header.structs")
.clone();
stack.push(Frame::List {
row_indent: indent,
type_name: new_type.clone(),
schema: new_schema,
last_row_values: None,
list: Vec::new(),
key: new_type.clone(),
count_hint: None,
});
return Ok(stack.len() - 1);
}
}
}
}
return Ok(idx);
} else if indent == *row_indent + 1 {
if list.is_empty() {
return Err(HedlError::orphan_row(
"child row has no parent row",
line_num,
));
}
let child_types = header.nests.get(type_name).ok_or_else(|| {
HedlError::orphan_row(
format!("no NEST rule for parent type '{}'", type_name),
line_num,
)
})?;
let child_type = if child_types.len() == 1 {
&child_types[0]
} else if let Some(fc) = field_count {
let matching_type = child_types.iter().find(|ct| {
header
.structs
.get(*ct)
.is_some_and(|schema| schema.len() == fc)
});
matching_type.unwrap_or(&child_types[0])
} else {
&child_types[0]
};
let child_schema = header.structs.get(child_type).ok_or_else(|| {
HedlError::schema(format!("child type '{}' not defined", child_type), line_num)
})?;
let current_depth = stack
.iter()
.filter(|f| matches!(f, Frame::List { .. }))
.count();
if current_depth >= limits.max_nest_depth {
return Err(HedlError::security(
format!(
"NEST hierarchy depth {} exceeds maximum allowed depth {}",
current_depth + 1,
limits.max_nest_depth
),
line_num,
));
}
stack.push(Frame::List {
row_indent: indent,
type_name: child_type.clone(),
schema: child_schema.clone(),
last_row_values: None,
list: Vec::new(),
key: child_type.clone(),
count_hint: None, });
return Ok(stack.len() - 1);
}
}
}
Err(HedlError::syntax(
"matrix row outside of list context",
line_num,
))
}