diff --git a/crates/oa-core/src/language/analysis_source/syntax.rs b/crates/oa-core/src/language/analysis_source/syntax.rs index 63166d01..238d4d85 100644 --- a/crates/oa-core/src/language/analysis_source/syntax.rs +++ b/crates/oa-core/src/language/analysis_source/syntax.rs @@ -40,266 +40,282 @@ pub(super) fn binding(p: &mut Cursor<'_, '_>) -> Result { while let Some(task) = tasks.pop() { p.budget.active()?; match task { - Task::Expression(minimum) => { - let start = p.span().start; - if p.text() != "(" { - p.budget.entry(p.span())?; - } - tasks.push(Task::Continue(minimum)); - match p.text() { - "true" | "false" => { - let value = p.text() == "true"; - let span = p.span(); - p.cursor += 1; - values.push(p.node(ExpressionKind::Boolean(value), span)?); - } - "-" => { - p.open()?; - p.cursor += 1; - tasks.push(Task::Unary(start, UnaryOp::Negate)); - tasks.push(Task::Expression(7)); - } - "(" => { - p.open()?; - p.cursor += 1; - tasks.push(Task::Group(start)); - tasks.push(Task::Expression(0)); - } - "[" => { - p.open()?; - p.cursor += 1; - tasks.push(Task::Next(Sequence::new(SequenceKind::List, start))); - } - "@" => { - p.cursor += 1; - let reference = p.reference()?; - if p.text() == "(" { - p.open()?; - p.cursor += 1; - tasks.push(Task::Next(Sequence::new( - SequenceKind::Call(reference), - start, - ))); - } else { - let end = reference.span.end; - values.push( - p.node(ExpressionKind::Property(reference), Span { start, end })?, - ); - } - } - "$" => { - p.cursor += 1; - let reference = p.reference()?; - let end = reference.span.end; - values.push( - p.node(ExpressionKind::Parameter(reference), Span { start, end })?, - ); - } - "ratio" => { - let (value, literal) = p.exact_literal()?; - values.push(p.number(value, literal)?); - } - "abs" | "not" | "if" | "sum" => { - let kind = match p.text() { - "abs" => SequenceKind::Unary(UnaryOp::Absolute), - "not" => SequenceKind::Unary(UnaryOp::Not), - "if" => SequenceKind::If, - _ => SequenceKind::Sum, - }; - p.cursor += 1; - p.open()?; - p.take("(")?; - tasks.push(Task::Next(Sequence::new(kind, start))); - } - "power" => { - p.cursor += 1; - p.open()?; - p.take("(")?; - tasks.push(Task::Power(start)); - tasks.push(Task::Expression(0)); - } - "as_kind" => { - p.cursor += 1; - p.open()?; - p.take("(")?; - p.take("@")?; - let kind = p.reference()?; - p.take(",")?; - tasks.push(Task::AsKind(start, kind)); - tasks.push(Task::Expression(0)); - } - _ if p - .tokens - .get(p.cursor) - .is_some_and(|token| token.class == Class::Number) => - { - let (value, literal) = p.exact_literal()?; - values.push(p.number(value, literal)?); - } - _ => return Err(error(p.span(), "Expected an analysis expression.")), - } - } - Task::Continue(minimum) => { - if let Some((op, precedence)) = - operator(p.text()).filter(|(_, prec)| *prec >= minimum) - { - let left = values.pop().expect("completed Pratt operand"); - if comparison(op) && ungrouped_comparison(&left) { - return Err(error(p.span(), "Comparison children require parentheses.")); - } - p.budget.entry(p.span())?; - p.cursor += 1; - tasks.push(Task::Continue(minimum)); - tasks.push(Task::Binary(left, op)); - tasks.push(Task::Expression(precedence + 1)); - } - } - Task::Binary(left, op) => { - let right = values.pop().expect("completed right operand"); - if comparison(op) && ungrouped_comparison(&right) { - return Err(error( - right.span, - "Comparison children require parentheses.", - )); - } - let span = Span { - start: left.span.start, - end: right.span.end, - }; - values.push(p.node( - ExpressionKind::Binary { - op, - left: Box::new(left), - right: Box::new(right), - }, - span, - )?); - } - Task::Unary(start, op) => { - let operand = values.pop().expect("completed unary operand"); - let end = operand.span.end; - p.close(); - values.push(p.node( - ExpressionKind::Unary { - op, - operand: Box::new(operand), - }, - Span { start, end }, - )?); - } - Task::Group(start) => { - let end = p.take(")")?.end; - p.close(); - let value = values.last_mut().expect("completed grouped expression"); - value.grouped = true; - value.span = Span { start, end }; - } - Task::Next(mut sequence) => { - let closing = if matches!(sequence.kind, SequenceKind::List) { - "]" - } else { - ")" - }; - if p.text() == closing { - let end = p.take(closing)?.end; - p.close(); - values.push(p.sequence(sequence, end)?); - } else { - let count = sequence.values.len(); - let maximum = match sequence.kind { - SequenceKind::List => crate::analysis::MAX_LIST_LENGTH, - SequenceKind::Unary(_) => 1, - SequenceKind::If => 3, - SequenceKind::Sum => 2, - SequenceKind::Call(_) => usize::MAX, - }; - if count >= maximum { - return Err(error( - p.span(), - "Too many list members or constructor arguments.", - )); - } - let key = if matches!(sequence.kind, SequenceKind::Call(_)) { - p.budget.entry(p.span())?; - let key = p.reference()?; - if !sequence.keys.insert(key.name.clone()) { - return Err(error(key.span, "Duplicate call argument name.")); - } - p.take(":")?; - Some(key) - } else { - None - }; - tasks.push(Task::Collected(sequence, key)); - tasks.push(Task::Expression(0)); - } - } - Task::Collected(mut sequence, key) => { - sequence - .values - .push((key, values.pop().expect("completed sequence value"))); - let closing = if matches!(sequence.kind, SequenceKind::List) { - "]" - } else { - ")" - }; - if p.text() != closing { - p.take(",")?; - if p.text() == closing { - return Err(error(p.span(), "Trailing comma is not an expression.")); - } - } - tasks.push(Task::Next(sequence)); - } - Task::Power(start) => { - let base = values.pop().expect("completed power base"); - p.take(",")?; - let negative = if p.text() == "-" { - p.cursor += 1; - true - } else { - false - }; - let span = p.integer()?; - let magnitude = p.source[span.start..span.end] - .parse::() - .map_err(|_| error(span, "Power exponent is outside -16..16."))?; - if magnitude > 16 { - return Err(error(span, "Power exponent is outside -16..16.")); - } - let exponent = if negative { - -(magnitude as i8) - } else { - magnitude as i8 - }; - let end = p.take(")")?.end; - p.close(); - values.push(p.node( - ExpressionKind::Power { - base: Box::new(base), - exponent, - }, - Span { start, end }, - )?); - } - Task::AsKind(start, kind) => { - let value = values.pop().expect("completed kind assertion value"); - let end = p.take(")")?.end; - p.close(); - values.push(p.node( - ExpressionKind::AsKind { - kind, - value: Box::new(value), - }, - Span { start, end }, - )?); - } + Task::Expression(minimum) => prefix(p, minimum, &mut tasks, &mut values)?, + task => continue_task(p, task, &mut tasks, &mut values)?, } } Ok(Binding::Expression( values.pop().expect("one complete expression"), )) } +// Keep mutually exclusive construction temporaries out of the numeric prefix +// call chain. Both steps borrow the driver's original iterative stacks. +fn prefix( + p: &mut Cursor<'_, '_>, + minimum: u8, + tasks: &mut Vec, + values: &mut Vec, +) -> Result<(), Error> { + let start = p.span().start; + if p.text() != "(" { + p.budget.entry(p.span())?; + } + tasks.push(Task::Continue(minimum)); + match p.text() { + "true" | "false" => { + let value = p.text() == "true"; + let span = p.span(); + p.cursor += 1; + values.push(p.node(ExpressionKind::Boolean(value), span)?); + } + "-" => { + p.open()?; + p.cursor += 1; + tasks.push(Task::Unary(start, UnaryOp::Negate)); + tasks.push(Task::Expression(7)); + } + "(" => { + p.open()?; + p.cursor += 1; + tasks.push(Task::Group(start)); + tasks.push(Task::Expression(0)); + } + "[" => { + p.open()?; + p.cursor += 1; + tasks.push(Task::Next(Sequence::new(SequenceKind::List, start))); + } + "@" => { + p.cursor += 1; + let reference = p.reference()?; + if p.text() == "(" { + p.open()?; + p.cursor += 1; + tasks.push(Task::Next(Sequence::new( + SequenceKind::Call(reference), + start, + ))); + } else { + let end = reference.span.end; + values.push(p.node(ExpressionKind::Property(reference), Span { start, end })?); + } + } + "$" => { + p.cursor += 1; + let reference = p.reference()?; + let end = reference.span.end; + values.push(p.node(ExpressionKind::Parameter(reference), Span { start, end })?); + } + "ratio" => { + let (value, literal) = p.exact_literal()?; + values.push(p.number(value, literal)?); + } + "abs" | "not" | "if" | "sum" => { + let kind = match p.text() { + "abs" => SequenceKind::Unary(UnaryOp::Absolute), + "not" => SequenceKind::Unary(UnaryOp::Not), + "if" => SequenceKind::If, + _ => SequenceKind::Sum, + }; + p.cursor += 1; + p.open()?; + p.take("(")?; + tasks.push(Task::Next(Sequence::new(kind, start))); + } + "power" => { + p.cursor += 1; + p.open()?; + p.take("(")?; + tasks.push(Task::Power(start)); + tasks.push(Task::Expression(0)); + } + "as_kind" => { + p.cursor += 1; + p.open()?; + p.take("(")?; + p.take("@")?; + let kind = p.reference()?; + p.take(",")?; + tasks.push(Task::AsKind(start, kind)); + tasks.push(Task::Expression(0)); + } + _ if p + .tokens + .get(p.cursor) + .is_some_and(|token| token.class == Class::Number) => + { + let (value, literal) = p.exact_literal()?; + values.push(p.number(value, literal)?); + } + _ => return Err(error(p.span(), "Expected an analysis expression.")), + } + Ok(()) +} +fn continue_task( + p: &mut Cursor<'_, '_>, + task: Task, + tasks: &mut Vec, + values: &mut Vec, +) -> Result<(), Error> { + match task { + Task::Expression(_) => unreachable!("prefix tasks are dispatched by the driver"), + Task::Continue(minimum) => { + if let Some((op, precedence)) = operator(p.text()).filter(|(_, prec)| *prec >= minimum) + { + let left = values.pop().expect("completed Pratt operand"); + if comparison(op) && ungrouped_comparison(&left) { + return Err(error(p.span(), "Comparison children require parentheses.")); + } + p.budget.entry(p.span())?; + p.cursor += 1; + tasks.push(Task::Continue(minimum)); + tasks.push(Task::Binary(left, op)); + tasks.push(Task::Expression(precedence + 1)); + } + } + Task::Binary(left, op) => { + let right = values.pop().expect("completed right operand"); + if comparison(op) && ungrouped_comparison(&right) { + return Err(error( + right.span, + "Comparison children require parentheses.", + )); + } + let span = Span { + start: left.span.start, + end: right.span.end, + }; + values.push(p.node( + ExpressionKind::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + span, + )?); + } + Task::Unary(start, op) => { + let operand = values.pop().expect("completed unary operand"); + let end = operand.span.end; + p.close(); + values.push(p.node( + ExpressionKind::Unary { + op, + operand: Box::new(operand), + }, + Span { start, end }, + )?); + } + Task::Group(start) => { + let end = p.take(")")?.end; + p.close(); + let value = values.last_mut().expect("completed grouped expression"); + value.grouped = true; + value.span = Span { start, end }; + } + Task::Next(mut sequence) => { + let closing = if matches!(sequence.kind, SequenceKind::List) { + "]" + } else { + ")" + }; + if p.text() == closing { + let end = p.take(closing)?.end; + p.close(); + values.push(p.sequence(sequence, end)?); + } else { + let count = sequence.values.len(); + let maximum = match sequence.kind { + SequenceKind::List => crate::analysis::MAX_LIST_LENGTH, + SequenceKind::Unary(_) => 1, + SequenceKind::If => 3, + SequenceKind::Sum => 2, + SequenceKind::Call(_) => usize::MAX, + }; + if count >= maximum { + return Err(error( + p.span(), + "Too many list members or constructor arguments.", + )); + } + let key = if matches!(sequence.kind, SequenceKind::Call(_)) { + p.budget.entry(p.span())?; + let key = p.reference()?; + if !sequence.keys.insert(key.name.clone()) { + return Err(error(key.span, "Duplicate call argument name.")); + } + p.take(":")?; + Some(key) + } else { + None + }; + tasks.push(Task::Collected(sequence, key)); + tasks.push(Task::Expression(0)); + } + } + Task::Collected(mut sequence, key) => { + sequence + .values + .push((key, values.pop().expect("completed sequence value"))); + let closing = if matches!(sequence.kind, SequenceKind::List) { + "]" + } else { + ")" + }; + if p.text() != closing { + p.take(",")?; + if p.text() == closing { + return Err(error(p.span(), "Trailing comma is not an expression.")); + } + } + tasks.push(Task::Next(sequence)); + } + Task::Power(start) => { + let base = values.pop().expect("completed power base"); + p.take(",")?; + let negative = if p.text() == "-" { + p.cursor += 1; + true + } else { + false + }; + let span = p.integer()?; + let magnitude = p.source[span.start..span.end] + .parse::() + .map_err(|_| error(span, "Power exponent is outside -16..16."))?; + if magnitude > 16 { + return Err(error(span, "Power exponent is outside -16..16.")); + } + let exponent = if negative { + -(magnitude as i8) + } else { + magnitude as i8 + }; + let end = p.take(")")?.end; + p.close(); + values.push(p.node( + ExpressionKind::Power { + base: Box::new(base), + exponent, + }, + Span { start, end }, + )?); + } + Task::AsKind(start, kind) => { + let value = values.pop().expect("completed kind assertion value"); + let end = p.take(")")?.end; + p.close(); + values.push(p.node( + ExpressionKind::AsKind { + kind, + value: Box::new(value), + }, + Span { start, end }, + )?); + } + } + Ok(()) +} impl Sequence { fn new(kind: SequenceKind, start: usize) -> Self { Self { diff --git a/harness/analysis-mixed-parser-stack-repair-20260925.md b/harness/analysis-mixed-parser-stack-repair-20260925.md new file mode 100644 index 00000000..d0a8c06a --- /dev/null +++ b/harness/analysis-mixed-parser-stack-repair-20260925.md @@ -0,0 +1,13 @@ +# Private Pratt parser stack-frame repair candidate + +2026-09-25. Separate correction after emission source `19a4082117b88caf332198e4c685bde91005cb52`. Parent recorded eleven focused emission tests passing, then a full-suite STATUS_STACK_OVERFLOW (0xc00000fd) in the unchanged ordinary-index 128 KiB control. The prior `fa7a9804` full run completed. This note preserves the failed candidate rather than treating focused success as acceptance. + +Parent's exact 19a4082 artifact is 42,402,816 bytes, SHA256 `777457cfefb8a3046e3f0809cabd936cfb742abe792befad2c50e05dfac1bf6b`. Its LLDB trace reproduces overflow in Windows allocation beneath formatting in Exact::parse_decimal, then exact_literal, syntax::binding, declarations::parse, State::parse_typed, Parser::unit and parse_impl. No emitter call is active. A debugger process killed with exit 0 is not a test pass. Raw parent records live in `.local/analysis-mixed-emission-gates-20260925/emission-full-19a4082-1.*` and `emission-debugger-19a4082-1.*`; integration owns publication. + +Parent measured fixed active prologues: binding 27,272 B, declarations::parse 22,664 B, Parser::unit 11,864 B, State::parse_typed 6,952 B. The previously measured outer frames (parse_impl 11,048 B, profile wrapper 200 B, parse 280 B, accepted 1,432 B, indexed 1,496 B, worker 6,920 B) match fa7. These partial native reservations are not total stack use. No inherited owner field or parser source changed in emission, so duplicated inline policy is not the cause established here. Environment-sensitive headroom remains a limitation; the failed full run still blocks acceptance. + +## Narrow correction + +`syntax.rs` keeps the same iterative binding driver, task/value Vec allocations, initial active check/unbound branch, per-popped-task active check, and final value extraction. The current Task::Expression body moves mechanically to a private prefix step borrowing those Vecs and Cursor. Remaining task arms move mechanically to a continuation step, also borrowing the same containers. No recursive parser call, new collection, clone, Box/Arc, numeric parsing change, work debit, depth limit, logical reservation or producer-timing claim is introduced. All cursor mutations, pushes/pops, opening/closing, validation, spans and error order remain at the same points. Steps return unit or the same Error; the driver retains ownership and cleanup of its original stacks. + +The intended native-stack improvement is that numeric prefix work no longer runs below all continuation construction temporaries in one large function. This is a source-level rationale, not an accepted measurement. Parent must measure the new active driver+prefix prologue sum against 27,272 B (and driver+continuation where relevant), then run unchanged stack controls and required focused/full gates. No test, fixture, allowance, RUST_MIN_STACK, Exact implementation or forwarding wrapper was changed. Independent source review checks all moved arms; targeted rustfmt and diff checks are the only implementer tools, with no compiler/runtime execution.