Skip to main content

jia_parse/parser/
mod.rs

1//! Recursive-descent parser for PDDL domain and problem files.
2//!
3//! Consumes a token stream from the [`crate::lexer`] and produces the AST defined in
4//! [`crate::ast`]. The parser handles all PDDL features encountered in IPC benchmarks
5//! (1998--2014), including:
6//!
7//! - Typing and type hierarchies (`:types`, `either`)
8//! - Durative actions with temporal conditions/effects
9//! - Numeric fluents, action costs, and metrics
10//! - ADL quantifiers (`forall`, `exists`) and conditional effects (`when`)
11//! - PDDL3 trajectory constraints and preferences
12//! - Legacy PDDL 1.2 `:vars` in actions
13//!
14//! ## Entry points
15//!
16//! | Function | Input | Output |
17//! |---|---|---|
18//! | [`parse_domain_str`] | raw PDDL string | [`Domain`] |
19//! | [`parse_problem_str`] | raw PDDL string | [`Problem`] |
20//! | [`parse_domain`] | pre-tokenized `&[Token]` | [`Domain`] |
21//! | [`parse_problem`] | pre-tokenized `&[Token]` | [`Problem`] |
22
23mod condition;
24mod cursor;
25mod domain;
26mod effect;
27mod expr;
28mod problem;
29mod terms;
30mod utils;
31
32use crate::ast::{Domain, Problem};
33use crate::error::ParseError;
34use crate::lexer::Token;
35
36// ---------------------------------------------------------------------------
37// Public API
38// ---------------------------------------------------------------------------
39
40/// Parse a pre-tokenized PDDL domain.
41///
42/// Prefer [`parse_domain_str`] unless you need to inspect or reuse the token stream.
43///
44/// # Arguments
45///
46/// * `tokens` - Token stream produced by [`crate::lexer::tokenize`]
47///
48/// # Returns
49///
50/// A [`Domain`] AST on success.
51///
52/// # Errors
53///
54/// Returns [`ParseError`] if the token stream does not represent a valid PDDL domain.
55pub fn parse_domain(tokens: &[Token]) -> Result<Domain, ParseError> {
56    let mut p = cursor::Parser::new(tokens);
57    let mut domain = domain::parse_domain_def(&mut p)?;
58    domain.sort_alphabetically();
59    Ok(domain)
60}
61
62/// Parse a pre-tokenized PDDL problem.
63///
64/// Prefer [`parse_problem_str`] unless you need to inspect or reuse the token stream.
65///
66/// # Arguments
67///
68/// * `tokens` - Token stream produced by [`crate::lexer::tokenize`]
69///
70/// # Returns
71///
72/// A [`Problem`] AST on success.
73///
74/// # Errors
75///
76/// Returns [`ParseError`] if the token stream does not represent a valid PDDL problem.
77pub fn parse_problem(tokens: &[Token]) -> Result<Problem, ParseError> {
78    let mut p = cursor::Parser::new(tokens);
79    let mut problem = problem::parse_problem_def(&mut p)?;
80    problem.sort_alphabetically();
81    Ok(problem)
82}
83
84/// Tokenize and parse a PDDL domain from a raw source string.
85///
86/// This is the primary convenience entry point for domain parsing. It chains
87/// [`crate::lexer::tokenize`] and [`parse_domain`] in one call.
88///
89/// # Arguments
90///
91/// * `input` - The raw PDDL domain source text
92///
93/// # Returns
94///
95/// A [`Domain`] AST on success.
96///
97/// # Errors
98///
99/// Returns [`ParseError`] on tokenization failures (e.g. unexpected characters)
100/// or parse failures (e.g. malformed S-expressions, unknown sections).
101pub fn parse_domain_str(input: &str) -> Result<Domain, ParseError> {
102    let tokens = crate::lexer::tokenize(input)?;
103    parse_domain(&tokens)
104}
105
106/// Tokenize and parse a PDDL problem from a raw source string.
107///
108/// This is the primary convenience entry point for problem parsing. It chains
109/// [`crate::lexer::tokenize`] and [`parse_problem`] in one call.
110///
111/// # Arguments
112///
113/// * `input` - The raw PDDL problem source text
114///
115/// # Returns
116///
117/// A [`Problem`] AST on success.
118///
119/// # Errors
120///
121/// Returns [`ParseError`] on tokenization failures or parse failures.
122pub fn parse_problem_str(input: &str) -> Result<Problem, ParseError> {
123    let tokens = crate::lexer::tokenize(input)?;
124    parse_problem(&tokens)
125}
126
127// ---------------------------------------------------------------------------
128// Tests
129// ---------------------------------------------------------------------------
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::ast::{AssignOp, Condition, DurationConstraint, Effect, Optimization};
135
136    #[test]
137    fn test_parse_minimal_domain() {
138        let input = r#"
139(define (domain test)
140  (:requirements :strips :typing)
141  (:types block - object)
142  (:predicates (on ?x - block ?y - block) (clear ?x - block))
143)
144"#;
145        let domain = parse_domain_str(input).unwrap();
146        assert_eq!(domain.name, "test");
147        assert_eq!(domain.requirements.len(), 2);
148        assert_eq!(domain.predicates.len(), 2);
149        assert_eq!(domain.predicates[0].name, "clear");
150        assert_eq!(domain.predicates[1].name, "on");
151    }
152
153    #[test]
154    fn test_parse_durative_action() {
155        let input = r#"
156(define (domain test)
157  (:durative-action move
158    :parameters (?x - obj)
159    :duration (= ?duration 5)
160    :condition (and
161      (at start (clear ?x))
162      (over all (safe ?x))
163    )
164    :effect (and
165      (at start (not (clear ?x)))
166      (at end (moved ?x))
167    )
168  )
169)
170"#;
171        let domain = parse_domain_str(input).unwrap();
172        assert_eq!(domain.durative_actions.len(), 1);
173        let da = &domain.durative_actions[0];
174        assert_eq!(da.name, "move");
175    }
176
177    #[test]
178    fn test_parse_minimal_problem() {
179        let input = r#"
180(define (problem test-prob)
181  (:domain test)
182  (:objects a b - block)
183  (:init (on a b) (clear a) (= (cost) 0))
184  (:goal (and (on b a)))
185)
186"#;
187        let problem = parse_problem_str(input).unwrap();
188        assert_eq!(problem.name, "test-prob");
189        assert_eq!(problem.domain_name, "test");
190        assert_eq!(problem.init.len(), 3);
191    }
192
193    #[test]
194    fn test_parse_metric() {
195        let input = r#"
196(define (problem test)
197  (:domain d)
198  (:init)
199  (:goal (and))
200  (:metric minimize (total-time))
201)
202"#;
203        let problem = parse_problem_str(input).unwrap();
204        assert!(problem.metric.is_some());
205        let m = problem.metric.unwrap();
206        assert_eq!(m.optimization, Optimization::Minimize);
207    }
208
209    #[test]
210    fn test_parse_functions_derived_and_duration_inequalities() {
211        let input = r#"
212(define (domain test)
213  (:requirements :typing :numeric-fluents :durative-actions :derived-predicates)
214  (:predicates (ready ?x - obj))
215  (:functions (fuel ?x - obj) (total-cost) - number)
216  (:derived (available ?x - obj) (ready ?x))
217  (:durative-action wait
218    :parameters (?x - obj)
219    :duration (and (>= ?duration 1) (<= ?duration 5))
220    :condition (and (at start (ready ?x)))
221    :effect (and))
222)
223"#;
224
225        let domain = parse_domain_str(input).unwrap();
226
227        assert_eq!(domain.functions.len(), 2);
228        assert_eq!(domain.derived_predicates.len(), 1);
229        assert!(matches!(
230            domain.durative_actions[0].duration,
231            DurationConstraint::And(_)
232        ));
233    }
234
235    #[test]
236    fn test_parse_numeric_precondition() {
237        let input = r#"
238(define (domain test)
239  (:durative-action act
240    :parameters (?d - driver)
241    :duration (= ?duration 10)
242    :condition (and
243      (at start (>= (time_available ?d) 10))
244    )
245    :effect (and
246      (at start (decrease (time_available ?d) 10))
247    )
248  )
249)
250"#;
251        let domain = parse_domain_str(input).unwrap();
252        let da = &domain.durative_actions[0];
253        assert!(da.condition.is_some());
254        assert!(da.effect.is_some());
255    }
256
257    #[test]
258    fn test_parse_broad_condition_variants() {
259        let input = r#"
260(define (problem test)
261  (:domain d)
262  (:init)
263  (:goal
264    (and
265      (or (ready a) (ready b))
266      (not (blocked a))
267      (imply (ready a) (ready b))
268      (forall (?x - obj) (ready ?x))
269      (exists (?x - obj) (ready ?x))
270      (preference (ready a))
271      (at a loc)
272      (over a b)
273      (= a b)
274      (= (cost) 0)
275      (< (cost) 10)
276      (> (cost) 0)
277      (always (ready a))
278      (sometime (ready b))
279      (at-most-once (ready a))
280      (within 5 (ready a))
281      (sometime-before (ready a) (ready b))
282      (sometime-after (ready a) (ready b))
283      (always-within 3 (ready a) (ready b))
284      (hold-during 1 2 (ready a))
285      (hold-after 4 (ready a))
286    ))
287)
288"#;
289
290        let problem = parse_problem_str(input).unwrap();
291        let conditions = match &problem.goal {
292            Condition::And(conditions) => Some(conditions),
293            _ => None,
294        }
295        .unwrap();
296
297        assert!(conditions.iter().any(|c| matches!(c, Condition::Or(_))));
298        assert!(conditions.iter().any(|c| matches!(c, Condition::Not(_))));
299        assert!(conditions
300            .iter()
301            .any(|c| matches!(c, Condition::Imply(_, _))));
302        assert!(conditions
303            .iter()
304            .any(|c| matches!(c, Condition::Forall { .. })));
305        assert!(conditions
306            .iter()
307            .any(|c| matches!(c, Condition::Exists { .. })));
308        assert!(conditions
309            .iter()
310            .any(|c| matches!(c, Condition::Preference { name: None, .. })));
311        assert!(conditions.iter().any(|c| matches!(
312            c,
313            Condition::Predicate(predicate) if predicate.name == "at"
314        )));
315        assert!(conditions.iter().any(|c| matches!(
316            c,
317            Condition::Predicate(predicate) if predicate.name == "over"
318        )));
319        assert!(conditions
320            .iter()
321            .any(|c| matches!(c, Condition::Equals(_, _))));
322        assert!(conditions
323            .iter()
324            .any(|c| matches!(c, Condition::NumericComparison { .. })));
325        assert!(conditions.iter().any(|c| matches!(c, Condition::Always(_))));
326        assert!(conditions
327            .iter()
328            .any(|c| matches!(c, Condition::Sometime(_))));
329        assert!(conditions
330            .iter()
331            .any(|c| matches!(c, Condition::AtMostOnce(_))));
332        assert!(conditions
333            .iter()
334            .any(|c| matches!(c, Condition::Within(_, _))));
335        assert!(conditions
336            .iter()
337            .any(|c| matches!(c, Condition::SometimeBefore(_, _))));
338        assert!(conditions
339            .iter()
340            .any(|c| matches!(c, Condition::SometimeAfter(_, _))));
341        assert!(conditions
342            .iter()
343            .any(|c| matches!(c, Condition::AlwaysWithin(_, _, _))));
344        assert!(conditions
345            .iter()
346            .any(|c| matches!(c, Condition::HoldDuring(_, _, _))));
347        assert!(conditions
348            .iter()
349            .any(|c| matches!(c, Condition::HoldAfter(_, _))));
350    }
351
352    #[test]
353    fn test_parse_broad_effect_variants() {
354        let input = r#"
355(define (domain test)
356  (:action a
357    :parameters (?x - obj)
358    :precondition (and)
359    :effect
360      (and
361        (at ?x loc)
362        (assign (cost) 1)
363        (increase (cost) 2)
364        (scale-up (cost) 3)
365        (when (ready ?x) (decrease (cost) 1))))
366  (:durative-action d
367    :parameters (?x - obj)
368    :duration (= ?duration 1)
369    :condition (and)
370    :effect
371      (and
372        (at start (at ?x loc))
373        (at end (scale-down (cost) 2))))
374)
375"#;
376
377        let domain = parse_domain_str(input).unwrap();
378        let effects = domain.actions[0]
379            .effect
380            .as_ref()
381            .and_then(|effect| match effect {
382                Effect::And(effects) => Some(effects),
383                _ => None,
384            })
385            .unwrap();
386
387        assert!(effects.iter().any(|e| matches!(
388            e,
389            Effect::Predicate(predicate) if predicate.name == "at"
390        )));
391        assert!(effects.iter().any(|e| matches!(
392            e,
393            Effect::NumericAssign {
394                op: AssignOp::Assign,
395                ..
396            }
397        )));
398        assert!(effects.iter().any(|e| matches!(
399            e,
400            Effect::NumericAssign {
401                op: AssignOp::Increase,
402                ..
403            }
404        )));
405        assert!(effects.iter().any(|e| matches!(
406            e,
407            Effect::NumericAssign {
408                op: AssignOp::ScaleUp,
409                ..
410            }
411        )));
412        assert!(effects.iter().any(|e| matches!(e, Effect::When { .. })));
413
414        let durative_effects = domain.durative_actions[0]
415            .effect
416            .as_ref()
417            .and_then(|effect| match effect {
418                Effect::And(effects) => Some(effects),
419                _ => None,
420            })
421            .unwrap();
422        assert!(durative_effects
423            .iter()
424            .any(|e| matches!(e, Effect::AtStart(_))));
425        assert!(durative_effects
426            .iter()
427            .any(|e| matches!(e, Effect::AtEnd(_))));
428    }
429
430    #[test]
431    fn parser_skips_unknown_domain_and_problem_sections() {
432        let domain = parse_domain_str(
433            r#"
434(define (domain test)
435  (:requirements :strips)
436  (:unknown-section (nested value))
437  (legacy-section (ignored value))
438  (:action a
439    :parameters ()
440    :precondition (and)
441    :unknown-action-field (ignored value)
442    :effect (and))
443  (:durative-action d
444    :parameters ()
445    :duration (= ?duration 1)
446    :unknown-durative-field (ignored value)
447    :condition (and)
448    :effect (and))
449)
450"#,
451        )
452        .unwrap();
453        assert_eq!(domain.actions.len(), 1);
454        assert_eq!(domain.durative_actions.len(), 1);
455
456        let problem = parse_problem_str(
457            r#"
458(define (problem p)
459  (:domain test)
460  (:unknown-section (nested value))
461  (legacy-section (ignored value))
462  (:init)
463  (:goal (and))
464)
465"#,
466        )
467        .unwrap();
468        assert_eq!(problem.domain_name, "test");
469    }
470
471    #[test]
472    fn parser_skips_unknown_action_fields_with_sexp_values() {
473        let domain = parse_domain_str(
474            r#"
475(define (domain test)
476  (:action a
477    :parameters ()
478    :precondition (and)
479    :unknown-action-field (ignored value)
480    :effect (and))
481  (:durative-action d
482    :parameters ()
483    :duration (= ?duration 1)
484    :condition (and)
485    :unknown-durative-field (ignored value)
486    :effect (and))
487)
488"#,
489        )
490        .unwrap();
491
492        assert_eq!(domain.actions.len(), 1);
493        assert_eq!(domain.durative_actions.len(), 1);
494    }
495
496    #[test]
497    fn parser_reports_malformed_conditions_effects_and_metrics() {
498        for input in [
499            "(define (problem p) (:domain d) (:goal 42))",
500            "(define (problem p) (:domain d) (:goal (42)))",
501            "(define (problem p) (:domain d) (:goal (> ?x 1)))",
502            "(define (problem p) (:domain d) (:goal (at",
503            "(define (problem p) (:domain d) (:goal (over",
504            "(define (problem p) (:domain d) (:goal (and)) (:metric fastest 1))",
505            "(define (problem p) (:domain d) (:goal (and)) (:metric minimize ?x))",
506            "(define (problem p) (:domain d) (:goal (and)) (:metric minimize (42)))",
507        ] {
508            assert!(parse_problem_str(input).is_err(), "{input}");
509        }
510
511        for input in [
512            "(define (domain d) (:action a :parameters () :precondition (and) :effect (42)))",
513            "(define (domain d) (:action a :parameters () :precondition (and) :effect (at",
514            "(define (domain d) (:action a :parameters () :precondition (and) :effect (increase (cost) ?x)))",
515        ] {
516            assert!(parse_domain_str(input).is_err(), "{input}");
517        }
518    }
519}