Skip to main content

argumentation_values/
apx.rs

1//! APX text format I/O for VAFs (ASPARTIX-compatible).
2//!
3//! The APX format uses Prolog-style facts:
4//!
5//! ```text
6//! arg(h1).
7//! arg(c1).
8//! att(h1, c1).
9//! att(c1, h1).
10//! val(h1, life).
11//! val(c1, property).
12//! valpref(life, property).
13//! ```
14//!
15//! Comments start with `%` and run to end of line. Whitespace is ignored.
16//!
17//! `valpref(a, b)` means value `a` is strictly preferred over value `b`.
18//! Multiple `valpref` facts together encode an audience.
19
20use crate::error::Error;
21use crate::framework::ValueBasedFramework;
22use crate::types::{Audience, Value, ValueAssignment};
23use argumentation::ArgumentationFramework;
24
25/// Parse an APX document into (framework, audience) pair.
26///
27/// The resulting framework owns string-typed argument labels (matching
28/// the `arg(name)` identifier in the input). The audience is derived
29/// from the `valpref` facts; if no `valpref` facts are present, the
30/// audience is empty.
31pub fn from_apx(input: &str) -> Result<(ValueBasedFramework<String>, Audience), Error> {
32    let mut base = ArgumentationFramework::new();
33    let mut values = ValueAssignment::new();
34    let mut prefs: Vec<(String, String)> = Vec::new();
35
36    for (line_idx, raw_line) in input.lines().enumerate() {
37        let line_no = line_idx + 1;
38        // Strip comments
39        let line = raw_line.split('%').next().unwrap_or("").trim();
40        if line.is_empty() {
41            continue;
42        }
43        // Each fact ends with `.`
44        let fact = line.strip_suffix('.').ok_or_else(|| Error::ApxParse {
45            line: line_no,
46            reason: format!("expected fact ending with '.', got: {line}"),
47        })?;
48        // Predicate(args) form
49        let (pred, args) = parse_pred(fact, line_no)?;
50        match pred {
51            "arg" => {
52                let [name] = expect_n_args::<1>(&args, line_no, "arg")?;
53                base.add_argument(name.to_string());
54            }
55            "att" => {
56                let [attacker, target] = expect_n_args::<2>(&args, line_no, "att")?;
57                let attacker_s = attacker.to_string();
58                let target_s = target.to_string();
59                base.add_attack(&attacker_s, &target_s)
60                    .map_err(Error::from)?;
61            }
62            "val" => {
63                let [arg, value] = expect_n_args::<2>(&args, line_no, "val")?;
64                values.promote(arg.to_string(), Value::new(value.to_string()));
65            }
66            "valpref" => {
67                let [a, b] = expect_n_args::<2>(&args, line_no, "valpref")?;
68                prefs.push((a.to_string(), b.to_string()));
69            }
70            other => {
71                return Err(Error::ApxParse {
72                    line: line_no,
73                    reason: format!("unknown predicate: {other}"),
74                });
75            }
76        }
77    }
78
79    let audience = audience_from_prefs(&prefs);
80    Ok((ValueBasedFramework::new(base, values), audience))
81}
82
83/// Serialise a VAF + audience to APX format.
84pub fn to_apx(vaf: &ValueBasedFramework<String>, audience: &Audience) -> String {
85    let mut out = String::new();
86    let mut args: Vec<&String> = vaf.base().arguments().collect();
87    args.sort();
88    for arg in &args {
89        out.push_str(&format!("arg({arg}).\n"));
90    }
91    for target in &args {
92        let mut attackers: Vec<&String> = vaf.base().attackers(*target).into_iter().collect();
93        attackers.sort();
94        for atk in attackers {
95            out.push_str(&format!("att({atk}, {target}).\n"));
96        }
97    }
98    let mut entries: Vec<(&String, &[Value])> = vaf
99        .value_assignment()
100        .entries()
101        .collect();
102    entries.sort_by(|a, b| a.0.cmp(b.0));
103    for (arg, vals) in entries {
104        for v in vals {
105            out.push_str(&format!("val({arg}, {v}).\n"));
106        }
107    }
108    // valpref: emit one fact per strict preference. The pairwise scan is
109    // O(n²) on |values| but n is bounded by ENUMERATION_LIMIT * a small
110    // constant in practice. ASPARTIX accepts redundant valpref facts
111    // (it computes the transitive closure on import), so emitting all
112    // strict pairs (not just the transitive reduction) is fine.
113    let all_values: Vec<&Value> = audience.values().collect();
114    let mut emitted = std::collections::BTreeSet::new();
115    for a in &all_values {
116        for b in &all_values {
117            if audience.prefers(a, b) && emitted.insert((a.as_str(), b.as_str())) {
118                out.push_str(&format!("valpref({a}, {b}).\n"));
119            }
120        }
121    }
122    out
123}
124
125fn parse_pred(fact: &str, line: usize) -> Result<(&str, Vec<&str>), Error> {
126    let open = fact.find('(').ok_or_else(|| Error::ApxParse {
127        line,
128        reason: format!("expected '(' in fact: {fact}"),
129    })?;
130    let close = fact.rfind(')').ok_or_else(|| Error::ApxParse {
131        line,
132        reason: format!("expected ')' in fact: {fact}"),
133    })?;
134    if close <= open {
135        return Err(Error::ApxParse {
136            line,
137            reason: format!("malformed fact: {fact}"),
138        });
139    }
140    let pred = &fact[..open];
141    let args_str = &fact[open + 1..close];
142    let args: Vec<&str> = args_str.split(',').map(|s| s.trim()).collect();
143    Ok((pred, args))
144}
145
146fn expect_n_args<const N: usize>(
147    args: &[&str],
148    line: usize,
149    pred: &str,
150) -> Result<[String; N], Error> {
151    if args.len() != N {
152        return Err(Error::ApxParse {
153            line,
154            reason: format!(
155                "{pred} expects {N} arg(s), got {got}",
156                got = args.len()
157            ),
158        });
159    }
160    let mut out: [String; N] = std::array::from_fn(|_| String::new());
161    for (i, a) in args.iter().enumerate() {
162        out[i] = (*a).to_string();
163    }
164    Ok(out)
165}
166
167/// Build an Audience from a set of pairwise (strict) preferences.
168///
169/// Strategy: topologically sort the directed preference graph (a → b if
170/// `valpref(a, b)`). Each topological level becomes one tier.
171fn audience_from_prefs(prefs: &[(String, String)]) -> Audience {
172    use std::collections::{BTreeSet, HashMap, HashSet};
173
174    if prefs.is_empty() {
175        return Audience::new();
176    }
177
178    // Collect all distinct values.
179    let mut all: BTreeSet<String> = BTreeSet::new();
180    for (a, b) in prefs {
181        all.insert(a.clone());
182        all.insert(b.clone());
183    }
184
185    // Build successor map (a → set of b's that a outranks).
186    let mut outranks: HashMap<String, HashSet<String>> = HashMap::new();
187    let mut indegree: HashMap<String, usize> = HashMap::new();
188    for v in &all {
189        outranks.entry(v.clone()).or_default();
190        indegree.entry(v.clone()).or_insert(0);
191    }
192    for (a, b) in prefs {
193        if outranks.get_mut(a).unwrap().insert(b.clone()) {
194            *indegree.get_mut(b).unwrap() += 1;
195        }
196    }
197
198    let mut tiers: Vec<Vec<Value>> = Vec::new();
199    let mut remaining: HashSet<String> = all.into_iter().collect();
200    while !remaining.is_empty() {
201        let mut current_tier: Vec<String> = remaining
202            .iter()
203            .filter(|v| *indegree.get(*v).unwrap() == 0)
204            .cloned()
205            .collect();
206        if current_tier.is_empty() {
207            // Cycle — emit remaining as one indistinguishable tier.
208            current_tier = remaining.iter().cloned().collect();
209        }
210        current_tier.sort();
211        for v in &current_tier {
212            remaining.remove(v);
213            // Decrement indegrees for successors.
214            if let Some(succs) = outranks.get(v) {
215                for s in succs {
216                    if let Some(d) = indegree.get_mut(s)
217                        && *d > 0
218                    {
219                        *d -= 1;
220                    }
221                }
222            }
223        }
224        tiers.push(current_tier.into_iter().map(Value::new).collect());
225    }
226
227    Audience::from_tiers(tiers)
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn round_trip_small_vaf() {
236        let input = r#"
237% Hal & Carla in APX
238arg(h1).
239arg(c1).
240att(h1, c1).
241att(c1, h1).
242val(h1, life).
243val(c1, property).
244valpref(life, property).
245"#;
246        let (vaf, audience) = from_apx(input).unwrap();
247        assert_eq!(vaf.base().len(), 2);
248        assert!(audience.prefers(&Value::new("life"), &Value::new("property")));
249        // Round-trip: serialise then re-parse.
250        let serialised = to_apx(&vaf, &audience);
251        let (vaf2, audience2) = from_apx(&serialised).unwrap();
252        assert_eq!(vaf2.base().len(), 2);
253        assert!(audience2.prefers(&Value::new("life"), &Value::new("property")));
254    }
255
256    #[test]
257    fn parse_error_reports_line_and_predicate() {
258        let input = "arg(a).\nbogus(stuff).\n";
259        let err = from_apx(input).unwrap_err();
260        match err {
261            Error::ApxParse { line, reason } => {
262                assert_eq!(line, 2);
263                assert!(reason.contains("unknown predicate"));
264            }
265            other => panic!("wrong error variant: {other:?}"),
266        }
267    }
268
269    #[test]
270    fn comments_and_whitespace_ignored() {
271        let input = r#"
272% header comment
273arg(a).   % trailing comment
274arg(b).
275
276att(a, b).
277"#;
278        let (vaf, _) = from_apx(input).unwrap();
279        assert_eq!(vaf.base().len(), 2);
280    }
281
282    #[test]
283    fn empty_input_yields_empty_vaf() {
284        let (vaf, audience) = from_apx("").unwrap();
285        assert_eq!(vaf.base().len(), 0);
286        assert_eq!(audience.value_count(), 0);
287    }
288
289    #[test]
290    fn multi_tier_audience_emerges_from_chained_prefs() {
291        let input = r#"
292arg(a).
293arg(b).
294arg(c).
295val(a, life).
296val(b, fairness).
297val(c, property).
298valpref(life, fairness).
299valpref(fairness, property).
300"#;
301        let (_vaf, audience) = from_apx(input).unwrap();
302        assert_eq!(audience.tier_count(), 3);
303        assert!(audience.prefers(&Value::new("life"), &Value::new("fairness")));
304        assert!(audience.prefers(&Value::new("fairness"), &Value::new("property")));
305        assert!(audience.prefers(&Value::new("life"), &Value::new("property")));
306    }
307}