diff --git a/crates/arc-attractor/src/parser/lexer.rs b/crates/arc-attractor/src/parser/lexer.rs index 3bbe574fb..44c86828c 100644 --- a/crates/arc-attractor/src/parser/lexer.rs +++ b/crates/arc-attractor/src/parser/lexer.rs @@ -238,7 +238,27 @@ pub mod combinators { Ok((rest, AstValue::Str(format!("{num}{unit}")))) } - /// Parse an AST value: duration, float, integer, boolean, quoted string, or bare identifier. + /// Parse a bare string value containing hyphens and dots (e.g., `gpt-5.2-codex`). + /// + /// Must start with an alpha/underscore character, then may continue with + /// alphanumeric, underscore, hyphen, or dot characters. Must contain at + /// least one hyphen or dot (otherwise `identifier` handles it). + pub fn bare_string(input: &str) -> IResult<&str, String> { + let (rest, raw) = recognize(pair( + take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), + take_while(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'), + ))(input)?; + if !raw.contains('-') && !raw.contains('.') { + return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Verify, + ))); + } + Ok((rest, raw.to_string())) + } + + /// Parse an AST value: duration, float, integer, boolean, quoted string, bare identifier, + /// or bare string (e.g., `gpt-5.2-codex`). pub fn value(input: &str) -> IResult<&str, AstValue> { let input = input.trim_start(); alt(( @@ -247,6 +267,7 @@ pub mod combinators { map(float_value, AstValue::Float), map(integer_value, AstValue::Int), map(boolean, AstValue::Bool), + map(bare_string, AstValue::Str), map(identifier, |s: &str| AstValue::Ident(s.to_string())), ))(input) } @@ -403,4 +424,39 @@ mod tests { assert_eq!(value("true"), Ok(("", AstValue::Bool(true)))); assert_eq!(value("LR"), Ok(("", AstValue::Ident("LR".into())))); } + + #[test] + fn parse_bare_string_with_hyphens_and_dots() { + assert_eq!( + bare_string("gpt-5.2 rest"), + Ok((" rest", "gpt-5.2".into())) + ); + assert_eq!( + bare_string("gpt-5.2-codex"), + Ok(("", "gpt-5.2-codex".into())) + ); + assert_eq!( + bare_string("gpt-5.3-codex-spark"), + Ok(("", "gpt-5.3-codex-spark".into())) + ); + assert_eq!( + bare_string("gemini-3-flash-preview"), + Ok(("", "gemini-3-flash-preview".into())) + ); + // Plain identifier without hyphens/dots should fail (identifier handles it) + assert!(bare_string("LR").is_err()); + assert!(bare_string("openai").is_err()); + } + + #[test] + fn parse_value_bare_string() { + assert_eq!( + value("gpt-5.2"), + Ok(("", AstValue::Str("gpt-5.2".into()))) + ); + assert_eq!( + value("gpt-5.2-codex"), + Ok(("", AstValue::Str("gpt-5.2-codex".into()))) + ); + } } diff --git a/crates/arc-attractor/tests/kilroy_compat.rs b/crates/arc-attractor/tests/kilroy_compat.rs index f4413bdbc..02d049560 100644 --- a/crates/arc-attractor/tests/kilroy_compat.rs +++ b/crates/arc-attractor/tests/kilroy_compat.rs @@ -26,34 +26,27 @@ fn parse_kilroy_simple_example() { assert!(graph.find_exit_node().is_some()); } -// The batch_*.dot files use unquoted values with hyphens/dots (e.g., `llm_model=gpt-5.2`) -// which is valid in kilroy's more lenient Go parser but not in arc's strict DOT parser. -// These tests document the parser gap: arc requires quoting such values. #[test] -fn parse_kilroy_batch_clean_requires_quoted_model_values() { - let err = parse_kilroy_dot("batch_clean.dot").unwrap_err(); - assert!( - err.contains("grammar error"), - "expected grammar error for unquoted `gpt-5.2`, got: {err}" - ); +fn parse_kilroy_batch_clean() { + let graph = parse_kilroy_dot("batch_clean.dot").unwrap(); + assert_eq!(graph.name, "G"); + assert_eq!(graph.nodes.len(), 3); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); } #[test] -fn parse_kilroy_batch_has_errors_requires_quoted_model_values() { - let err = parse_kilroy_dot("batch_has_errors.dot").unwrap_err(); - assert!( - err.contains("grammar error"), - "expected grammar error for unquoted `gpt-5.2`, got: {err}" - ); +fn parse_kilroy_batch_has_errors() { + // This file is intentionally missing llm_provider on the work node. + // It should still parse successfully — validation is separate from parsing. + let graph = parse_kilroy_dot("batch_has_errors.dot").unwrap(); + assert_eq!(graph.nodes.len(), 3); } #[test] -fn parse_kilroy_batch_warnings_only_requires_quoted_model_values() { - let err = parse_kilroy_dot("batch_warnings_only.dot").unwrap_err(); - assert!( - err.contains("grammar error"), - "expected grammar error for unquoted `gpt-5.2`, got: {err}" - ); +fn parse_kilroy_batch_warnings_only() { + let graph = parse_kilroy_dot("batch_warnings_only.dot").unwrap(); + assert_eq!(graph.nodes.len(), 3); } #[test]