Skip to main content

xlog_logic/
list_normalize.rs

1//! Finite list normalization for the language-completeness surface.
2
3use std::collections::{HashMap, HashSet};
4
5use xlog_core::{Result, ScalarType, XlogError};
6
7use crate::ast::{
8    Atom, BodyLiteral, Comparison, DomainDecl, PredColumn, PredDecl, Program, Query, Rule, Term,
9    TypeRef,
10};
11
12const LIST_ID_TYPE: ScalarType = ScalarType::U64;
13
14/// Normalize finite list syntax into scalar list identifiers plus helper relations.
15///
16/// The normalized program uses ordinary XLOG relations:
17///
18/// - `__xlog_list_len(list_id, len)`
19/// - `__xlog_list_item_<type>(list_id, index, value)`
20/// - `__xlog_list_cons_<type>(list_id, head, tail_id)`
21/// - selected built-in helper relations such as `append`, `sort`, `msort`, and `list_to_set`
22///
23/// This keeps accepted list programs on the existing relational lowering/runtime path.
24pub fn normalize_list_builtins(program: &Program) -> Result<Program> {
25    let mut normalizer = ListNormalizer::new(program)?;
26    normalizer.normalize_program(program)
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30enum ListOp {
31    Append,
32    Sort,
33    MSort,
34    ListToSet,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38enum LiteralKey {
39    U32(u32),
40    U64(u64),
41    I32(i32),
42    I64(i64),
43    F32(u32),
44    F64(u64),
45    Bool(bool),
46    Symbol(String),
47}
48
49#[derive(Debug, Clone)]
50struct ListDef {
51    id: u64,
52    elem_type: ScalarType,
53    items: Vec<Term>,
54    keys: Vec<LiteralKey>,
55}
56
57#[derive(Debug, Clone)]
58struct ListArg {
59    term: Term,
60    id: Option<u64>,
61    elem_type: ScalarType,
62    is_literal: bool,
63}
64
65#[derive(Debug, Clone, Copy)]
66struct ListColumn {
67    elem_type: ScalarType,
68}
69
70struct ListNormalizer {
71    list_columns: HashMap<(String, usize), ListColumn>,
72    lists_by_key: HashMap<(ScalarType, Vec<LiteralKey>), u64>,
73    lists: Vec<ListDef>,
74    next_list_id: u64,
75    helper_preds: HashSet<String>,
76    operation_rows: HashSet<(ListOp, ScalarType, Vec<u64>)>,
77    fresh_counter: usize,
78}
79
80impl ListNormalizer {
81    fn new(program: &Program) -> Result<Self> {
82        let domains: HashMap<String, ScalarType> = program
83            .domains
84            .iter()
85            .map(|DomainDecl { name, typ }| (name.clone(), *typ))
86            .collect();
87
88        let mut list_columns = HashMap::new();
89        for pred in &program.predicates {
90            let columns = if pred.columns.is_empty() {
91                pred.types
92                    .iter()
93                    .cloned()
94                    .map(|typ| PredColumn { name: None, typ })
95                    .collect()
96            } else {
97                pred.columns.clone()
98            };
99            for (idx, col) in columns.iter().enumerate() {
100                if let TypeRef::List(inner) = &col.typ {
101                    let elem_type = Self::storage_type_for_type_ref(inner, &domains)?;
102                    list_columns.insert((pred.name.clone(), idx), ListColumn { elem_type });
103                }
104            }
105        }
106
107        Ok(Self {
108            list_columns,
109            lists_by_key: HashMap::new(),
110            lists: Vec::new(),
111            next_list_id: 1,
112            helper_preds: HashSet::new(),
113            operation_rows: HashSet::new(),
114            fresh_counter: 0,
115        })
116    }
117
118    fn normalize_program(&mut self, program: &Program) -> Result<Program> {
119        let mut out = program.clone();
120        out.rules = program
121            .rules
122            .iter()
123            .map(|rule| self.normalize_rule(rule))
124            .collect::<Result<Vec<_>>>()?
125            .into_iter()
126            .flatten()
127            .collect();
128        out.constraints = program
129            .constraints
130            .iter()
131            .map(|constraint| {
132                let body = self.normalize_body(&constraint.body)?;
133                Ok(crate::ast::Constraint { body })
134            })
135            .collect::<Result<Vec<_>>>()?;
136        out.queries = program
137            .queries
138            .iter()
139            .map(|query| self.normalize_query(query))
140            .collect::<Result<Vec<_>>>()?;
141
142        self.append_helper_declarations_and_facts(&mut out)?;
143        Ok(out)
144    }
145
146    fn normalize_query(&mut self, query: &Query) -> Result<Query> {
147        let mut var_list_types = HashMap::new();
148        let atoms = self.expand_atom(&query.atom, false, &mut var_list_types)?;
149        let atom = atoms
150            .into_iter()
151            .next()
152            .unwrap_or_else(|| query.atom.clone());
153        Ok(Query { atom })
154    }
155
156    fn normalize_rule(&mut self, rule: &Rule) -> Result<Vec<Rule>> {
157        let mut var_list_types = HashMap::new();
158        let head = self.normalize_head_atom(&rule.head)?;
159        let body = self.normalize_body_with_env(&rule.body, &mut var_list_types)?;
160        Ok(vec![Rule { head, body }])
161    }
162
163    fn normalize_body(&mut self, body: &[BodyLiteral]) -> Result<Vec<BodyLiteral>> {
164        let mut var_list_types = HashMap::new();
165        self.normalize_body_with_env(body, &mut var_list_types)
166    }
167
168    fn normalize_body_with_env(
169        &mut self,
170        body: &[BodyLiteral],
171        var_list_types: &mut HashMap<String, ScalarType>,
172    ) -> Result<Vec<BodyLiteral>> {
173        let mut out = Vec::new();
174        for lit in body {
175            match lit {
176                BodyLiteral::Positive(atom) => {
177                    for atom in self.expand_atom(atom, false, var_list_types)? {
178                        out.push(BodyLiteral::Positive(atom));
179                    }
180                }
181                BodyLiteral::Negated(atom) => {
182                    let atoms = self.expand_atom(atom, true, var_list_types)?;
183                    if atoms.len() != 1 {
184                        return Err(list_error(
185                            "negated list pattern expansion would require multiple atoms",
186                        ));
187                    }
188                    out.push(BodyLiteral::Negated(atoms.into_iter().next().unwrap()));
189                }
190                BodyLiteral::Comparison(cmp) => {
191                    out.push(BodyLiteral::Comparison(Comparison {
192                        op: cmp.op,
193                        left: self.normalize_value_term(&cmp.left, None)?,
194                        right: self.normalize_value_term(&cmp.right, None)?,
195                    }));
196                }
197                BodyLiteral::Epistemic(lit) => out.push(BodyLiteral::Epistemic(lit.clone())),
198                BodyLiteral::IsExpr(is_expr) => out.push(BodyLiteral::IsExpr(is_expr.clone())),
199                BodyLiteral::Univ(_) => {
200                    return Err(list_error(
201                        "univ literals must be normalized by the meta-term pass before list normalization",
202                    ));
203                }
204            }
205        }
206        Ok(out)
207    }
208
209    fn normalize_head_atom(&mut self, atom: &Atom) -> Result<Atom> {
210        let mut terms = Vec::with_capacity(atom.terms.len());
211        for (idx, term) in atom.terms.iter().enumerate() {
212            let expected = self
213                .list_columns
214                .get(&(atom.predicate.clone(), idx))
215                .map(|col| col.elem_type);
216            terms.push(self.normalize_value_term(term, expected)?);
217        }
218        Ok(Atom {
219            predicate: atom.predicate.clone(),
220            terms,
221        })
222    }
223
224    fn expand_atom(
225        &mut self,
226        atom: &Atom,
227        negated: bool,
228        var_list_types: &mut HashMap<String, ScalarType>,
229    ) -> Result<Vec<Atom>> {
230        if let Some(atoms) = self.expand_list_builtin(atom, var_list_types)? {
231            return Ok(atoms);
232        }
233        if is_reserved_pair_helper(atom) {
234            return Err(list_error(
235                "pair helpers are reserved for meta-term normalization and are not implemented in list normalization",
236            ));
237        }
238
239        let mut terms = Vec::with_capacity(atom.terms.len());
240        let mut extra_atoms = Vec::new();
241        for (idx, term) in atom.terms.iter().enumerate() {
242            let expected = self
243                .list_columns
244                .get(&(atom.predicate.clone(), idx))
245                .map(|col| col.elem_type);
246            match term {
247                Term::Variable(name) => {
248                    if let Some(elem_type) = expected {
249                        var_list_types.insert(name.clone(), elem_type);
250                    }
251                    terms.push(term.clone());
252                }
253                Term::Cons { head, tail } => {
254                    if negated {
255                        return Err(list_error(
256                            "cons patterns in negated atoms are unsupported before a positive binder",
257                        ));
258                    }
259                    let elem_type = expected.ok_or_else(|| {
260                        list_error("cons pattern requires a declared list<T> column")
261                    })?;
262                    let list_var = self.fresh_var("list_cons");
263                    let head_term = self.normalize_scalar_term(head, Some(elem_type))?.1;
264                    let tail_arg =
265                        self.normalize_list_arg(tail, Some(elem_type), var_list_types)?;
266                    if let Term::Variable(name) = &tail_arg.term {
267                        var_list_types.insert(name.clone(), elem_type);
268                    }
269                    terms.push(Term::Variable(list_var.clone()));
270                    extra_atoms.push(Atom {
271                        predicate: cons_pred(elem_type),
272                        terms: vec![Term::Variable(list_var), head_term, tail_arg.term],
273                    });
274                }
275                _ => {
276                    let normalized = self.normalize_value_term(term, expected)?;
277                    terms.push(normalized);
278                }
279            }
280        }
281
282        let mut atoms = vec![Atom {
283            predicate: atom.predicate.clone(),
284            terms,
285        }];
286        atoms.extend(extra_atoms);
287        Ok(atoms)
288    }
289
290    fn expand_list_builtin(
291        &mut self,
292        atom: &Atom,
293        var_list_types: &mut HashMap<String, ScalarType>,
294    ) -> Result<Option<Vec<Atom>>> {
295        let pred = atom.predicate.as_str();
296        match pred {
297            "is_list" => {
298                require_arity(atom, 1)?;
299                let arg = self.normalize_list_arg(&atom.terms[0], None, var_list_types)?;
300                Ok(Some(vec![Atom {
301                    predicate: len_pred().to_string(),
302                    terms: vec![arg.term, Term::Anonymous],
303                }]))
304            }
305            "member" | "memberchk" => {
306                require_arity(atom, 2)?;
307                let list = self.normalize_list_arg(&atom.terms[1], None, var_list_types)?;
308                let value = self
309                    .normalize_scalar_term(&atom.terms[0], Some(list.elem_type))?
310                    .1;
311                Ok(Some(vec![Atom {
312                    predicate: item_pred(list.elem_type),
313                    terms: vec![list.term, Term::Anonymous, value],
314                }]))
315            }
316            "length" => {
317                require_arity(atom, 2)?;
318                let list = self.normalize_list_arg(&atom.terms[0], None, var_list_types)?;
319                let len = self
320                    .normalize_scalar_term(&atom.terms[1], Some(ScalarType::U32))?
321                    .1;
322                Ok(Some(vec![Atom {
323                    predicate: len_pred().to_string(),
324                    terms: vec![list.term, len],
325                }]))
326            }
327            "nth" => {
328                require_arity(atom, 3)?;
329                let idx = self
330                    .normalize_scalar_term(&atom.terms[0], Some(ScalarType::U32))?
331                    .1;
332                let list = self.normalize_list_arg(&atom.terms[1], None, var_list_types)?;
333                let value = self
334                    .normalize_scalar_term(&atom.terms[2], Some(list.elem_type))?
335                    .1;
336                Ok(Some(vec![Atom {
337                    predicate: item_pred(list.elem_type),
338                    terms: vec![list.term, idx, value],
339                }]))
340            }
341            "append" => {
342                require_arity(atom, 3)?;
343                if matches!(atom.terms[2], Term::List(_) | Term::Cons { .. })
344                    && !matches!(atom.terms[0], Term::List(_) | Term::Cons { .. })
345                    && !matches!(atom.terms[1], Term::List(_) | Term::Cons { .. })
346                {
347                    return Err(list_error(
348                        "unbounded append/3 generation is unsupported during list normalization; bind the first two lists to finite literals",
349                    ));
350                }
351                let left = self.normalize_list_arg(&atom.terms[0], None, var_list_types)?;
352                let right =
353                    self.normalize_list_arg(&atom.terms[1], Some(left.elem_type), var_list_types)?;
354                if left.elem_type != right.elem_type {
355                    return Err(list_error(
356                        "append/3 list arguments must have the same element type",
357                    ));
358                }
359                let out =
360                    self.normalize_list_arg(&atom.terms[2], Some(left.elem_type), var_list_types)?;
361                if !left.is_literal || !right.is_literal {
362                    return Err(list_error(
363                        "unbounded append/3 generation is unsupported during list normalization; bind the first two lists to finite literals",
364                    ));
365                }
366                let left_id = left.id.expect("literal list has id");
367                let right_id = right.id.expect("literal list has id");
368                let out_id = match out.id {
369                    Some(id) => id,
370                    None => self.concat_lists(left_id, right_id)?,
371                };
372                self.operation_rows.insert((
373                    ListOp::Append,
374                    left.elem_type,
375                    vec![left_id, right_id, out_id],
376                ));
377                if let Term::Variable(name) = &out.term {
378                    var_list_types.insert(name.clone(), left.elem_type);
379                }
380                Ok(Some(vec![Atom {
381                    predicate: append_pred(left.elem_type),
382                    terms: vec![left.term, right.term, out.term],
383                }]))
384            }
385            "sort" | "msort" | "list_to_set" => {
386                require_arity(atom, 2)?;
387                let input = self.normalize_list_arg(&atom.terms[0], None, var_list_types)?;
388                if !input.is_literal {
389                    return Err(list_error(
390                        "sort/msort/list_to_set require a finite list literal during list normalization",
391                    ));
392                }
393                let op = match pred {
394                    "sort" => ListOp::Sort,
395                    "msort" => ListOp::MSort,
396                    "list_to_set" => ListOp::ListToSet,
397                    _ => unreachable!(),
398                };
399                let out =
400                    self.normalize_list_arg(&atom.terms[1], Some(input.elem_type), var_list_types)?;
401                let input_id = input.id.expect("literal list has id");
402                let out_id = match out.id {
403                    Some(id) => id,
404                    None => self.derived_ordered_list(input_id, op)?,
405                };
406                self.operation_rows
407                    .insert((op, input.elem_type, vec![input_id, out_id]));
408                if let Term::Variable(name) = &out.term {
409                    var_list_types.insert(name.clone(), input.elem_type);
410                }
411                let helper = match op {
412                    ListOp::Sort => sort_pred(input.elem_type),
413                    ListOp::MSort => msort_pred(input.elem_type),
414                    ListOp::ListToSet => list_to_set_pred(input.elem_type),
415                    ListOp::Append => unreachable!(),
416                };
417                Ok(Some(vec![Atom {
418                    predicate: helper,
419                    terms: vec![input.term, out.term],
420                }]))
421            }
422            _ => Ok(None),
423        }
424    }
425
426    fn normalize_value_term(
427        &mut self,
428        term: &Term,
429        expected_list_elem: Option<ScalarType>,
430    ) -> Result<Term> {
431        match term {
432            Term::List(_) => Ok(self
433                .normalize_list_arg(term, expected_list_elem, &mut HashMap::new())?
434                .term),
435            Term::Cons { head, tail } => {
436                let elem_type = expected_list_elem.ok_or_else(|| {
437                    list_error("cons literal requires a declared list<T> context")
438                })?;
439                let mut items = vec![self.normalize_scalar_term(head, Some(elem_type))?.1];
440                let tail_arg =
441                    self.normalize_list_arg(tail, Some(elem_type), &mut HashMap::new())?;
442                let tail_id = tail_arg
443                    .id
444                    .ok_or_else(|| list_error("cons literal tail must be a finite list literal"))?;
445                let tail_items = self.list_by_id(tail_id)?.items.clone();
446                items.extend(tail_items);
447                let id = self.register_list_terms(items, Some(elem_type))?;
448                Ok(Term::Integer(id as i64))
449            }
450            _ => Ok(term.clone()),
451        }
452    }
453
454    fn normalize_list_arg(
455        &mut self,
456        term: &Term,
457        expected_elem_type: Option<ScalarType>,
458        var_list_types: &mut HashMap<String, ScalarType>,
459    ) -> Result<ListArg> {
460        match term {
461            Term::List(items) => {
462                let id = self.register_list_terms(items.clone(), expected_elem_type)?;
463                let elem_type = self.list_by_id(id)?.elem_type;
464                Ok(ListArg {
465                    term: Term::Integer(id as i64),
466                    id: Some(id),
467                    elem_type,
468                    is_literal: true,
469                })
470            }
471            Term::Cons { .. } => {
472                let value = self.normalize_value_term(term, expected_elem_type)?;
473                let id = match value {
474                    Term::Integer(id) if id >= 0 => id as u64,
475                    _ => {
476                        return Err(list_error(
477                            "cons list argument must normalize to a finite list id",
478                        ));
479                    }
480                };
481                let elem_type = self.list_by_id(id)?.elem_type;
482                Ok(ListArg {
483                    term: Term::Integer(id as i64),
484                    id: Some(id),
485                    elem_type,
486                    is_literal: true,
487                })
488            }
489            Term::Integer(id) if *id > 0 => {
490                let elem_type = expected_elem_type
491                    .or_else(|| self.list_by_id(*id as u64).ok().map(|list| list.elem_type))
492                    .ok_or_else(|| list_error("list id requires a known list<T> type"))?;
493                Ok(ListArg {
494                    term: term.clone(),
495                    id: Some(*id as u64),
496                    elem_type,
497                    is_literal: false,
498                })
499            }
500            Term::Variable(name) => {
501                let elem_type = expected_elem_type
502                    .or_else(|| var_list_types.get(name).copied())
503                    .ok_or_else(|| {
504                        list_error(
505                            "list built-in requires a finite literal or known list<T> variable",
506                        )
507                    })?;
508                var_list_types.insert(name.clone(), elem_type);
509                Ok(ListArg {
510                    term: term.clone(),
511                    id: None,
512                    elem_type,
513                    is_literal: false,
514                })
515            }
516            Term::Anonymous => {
517                let elem_type = expected_elem_type.ok_or_else(|| {
518                    list_error("anonymous list argument requires a known list<T> type")
519                })?;
520                Ok(ListArg {
521                    term: term.clone(),
522                    id: None,
523                    elem_type,
524                    is_literal: false,
525                })
526            }
527            _ => Err(list_error(
528                "expected finite list literal or known list<T> variable",
529            )),
530        }
531    }
532
533    fn normalize_scalar_term(
534        &mut self,
535        term: &Term,
536        expected: Option<ScalarType>,
537    ) -> Result<(ScalarType, Term)> {
538        match term {
539            Term::Variable(_) | Term::Anonymous => {
540                let typ = expected.ok_or_else(|| {
541                    list_error("list element variable requires a known list<T> element type")
542                })?;
543                Ok((typ, term.clone()))
544            }
545            Term::Integer(i) => match expected {
546                Some(ScalarType::U32) => {
547                    let value = u32::try_from(*i)
548                        .map_err(|_| list_error("integer list element is out of range for u32"))?;
549                    Ok((ScalarType::U32, Term::Integer(value as i64)))
550                }
551                Some(ScalarType::U64) => {
552                    let value = u64::try_from(*i)
553                        .map_err(|_| list_error("integer list element is out of range for u64"))?;
554                    Ok((ScalarType::U64, Term::Integer(value as i64)))
555                }
556                Some(ScalarType::I32) => {
557                    let value = i32::try_from(*i)
558                        .map_err(|_| list_error("integer list element is out of range for i32"))?;
559                    Ok((ScalarType::I32, Term::Integer(value as i64)))
560                }
561                Some(ScalarType::I64) | None if *i < 0 || *i > u32::MAX as i64 => {
562                    Ok((ScalarType::I64, Term::Integer(*i)))
563                }
564                Some(ScalarType::I64) => Ok((ScalarType::I64, Term::Integer(*i))),
565                None => Ok((ScalarType::U32, Term::Integer(*i))),
566                Some(ScalarType::F32) => Ok((ScalarType::F32, Term::Float(*i as f64))),
567                Some(ScalarType::F64) => Ok((ScalarType::F64, Term::Float(*i as f64))),
568                Some(ScalarType::Bool) if *i == 0 || *i == 1 => {
569                    Ok((ScalarType::Bool, Term::Integer(*i)))
570                }
571                Some(ScalarType::Bool) => Err(list_error("bool list elements must be 0 or 1")),
572                Some(ScalarType::Symbol) => {
573                    Err(list_error("integer list element is not valid for symbol"))
574                }
575            },
576            Term::Float(f) => match expected {
577                Some(ScalarType::F32) => Ok((ScalarType::F32, Term::Float(*f))),
578                Some(ScalarType::F64) | None => Ok((ScalarType::F64, Term::Float(*f))),
579                Some(_) => Err(list_error(
580                    "float list element is not valid for expected type",
581                )),
582            },
583            Term::String(s) => {
584                if expected.is_none() || expected == Some(ScalarType::Symbol) {
585                    Ok((ScalarType::Symbol, Term::String(s.clone())))
586                } else {
587                    Err(list_error(
588                        "string list element is only valid for symbol lists",
589                    ))
590                }
591            }
592            Term::Symbol(id) => {
593                if expected.is_none() || expected == Some(ScalarType::Symbol) {
594                    Ok((ScalarType::Symbol, Term::Symbol(*id)))
595                } else {
596                    Err(list_error(
597                        "symbol list element is not valid for expected type",
598                    ))
599                }
600            }
601            Term::List(_) | Term::Cons { .. } => {
602                let id = self
603                    .normalize_list_arg(term, None, &mut HashMap::new())?
604                    .id
605                    .expect("literal list has id");
606                let typ = expected.unwrap_or(LIST_ID_TYPE);
607                if typ != LIST_ID_TYPE {
608                    return Err(list_error(
609                        "nested list element requires list<list<T>> or list<u64> context",
610                    ));
611                }
612                Ok((LIST_ID_TYPE, Term::Integer(id as i64)))
613            }
614            Term::Compound { .. } | Term::PredRef(_) | Term::Aggregate(_) => Err(list_error(
615                "compound, predref, and aggregate list elements require meta-term normalization support",
616            )),
617        }
618    }
619
620    fn register_list_terms(
621        &mut self,
622        items: Vec<Term>,
623        expected_elem_type: Option<ScalarType>,
624    ) -> Result<u64> {
625        let mut elem_type = expected_elem_type;
626        let mut normalized = Vec::with_capacity(items.len());
627        let mut keys = Vec::with_capacity(items.len());
628
629        for item in &items {
630            let (item_type, term) = match self.normalize_scalar_term(item, elem_type) {
631                Ok(value) => value,
632                Err(err) if elem_type.is_some() => {
633                    return Err(list_error(format!("heterogeneous list literal: {}", err)));
634                }
635                Err(err) => return Err(err),
636            };
637            if let Some(existing) = elem_type {
638                if existing != item_type {
639                    return Err(list_error(
640                        "heterogeneous list literals require a declared finite term type",
641                    ));
642                }
643            } else {
644                elem_type = Some(item_type);
645            }
646            keys.push(literal_key(&term, item_type)?);
647            normalized.push(term);
648        }
649
650        let elem_type = elem_type.unwrap_or(LIST_ID_TYPE);
651        let key = (elem_type, keys.clone());
652        if let Some(id) = self.lists_by_key.get(&key) {
653            return Ok(*id);
654        }
655
656        let id = self.next_list_id;
657        self.next_list_id += 1;
658        self.lists_by_key.insert(key, id);
659        self.lists.push(ListDef {
660            id,
661            elem_type,
662            items: normalized,
663            keys,
664        });
665        Ok(id)
666    }
667
668    fn concat_lists(&mut self, left_id: u64, right_id: u64) -> Result<u64> {
669        let left = self.list_by_id(left_id)?.clone();
670        let right = self.list_by_id(right_id)?.clone();
671        if left.elem_type != right.elem_type {
672            return Err(list_error(
673                "append/3 list arguments must have the same element type",
674            ));
675        }
676        let mut items = left.items;
677        items.extend(right.items);
678        self.register_list_terms(items, Some(right.elem_type))
679    }
680
681    fn derived_ordered_list(&mut self, input_id: u64, op: ListOp) -> Result<u64> {
682        let input = self.list_by_id(input_id)?.clone();
683        let mut pairs: Vec<(LiteralKey, Term)> = input.keys.into_iter().zip(input.items).collect();
684        pairs.sort_by(|(a, _), (b, _)| format!("{a:?}").cmp(&format!("{b:?}")));
685        if matches!(op, ListOp::Sort | ListOp::ListToSet) {
686            pairs.dedup_by(|(a, _), (b, _)| a == b);
687        }
688        let items = pairs.into_iter().map(|(_, term)| term).collect();
689        self.register_list_terms(items, Some(input.elem_type))
690    }
691
692    fn append_helper_declarations_and_facts(&mut self, program: &mut Program) -> Result<()> {
693        self.ensure_tail_lists()?;
694
695        let mut helper_rules = Vec::new();
696        for list in self.lists.clone() {
697            self.helper_preds.insert(len_pred().to_string());
698            helper_rules.push(fact(
699                len_pred(),
700                vec![
701                    Term::Integer(list.id as i64),
702                    Term::Integer(list.items.len() as i64),
703                ],
704            ));
705
706            let item_name = item_pred(list.elem_type);
707            self.helper_preds.insert(item_name.clone());
708            for (idx, item) in list.items.iter().enumerate() {
709                helper_rules.push(fact(
710                    &item_name,
711                    vec![
712                        Term::Integer(list.id as i64),
713                        Term::Integer(idx as i64),
714                        item.clone(),
715                    ],
716                ));
717            }
718
719            if let Some((head, tail_id)) = self.cons_fact_parts(list.id)? {
720                let cons_name = cons_pred(list.elem_type);
721                self.helper_preds.insert(cons_name.clone());
722                helper_rules.push(fact(
723                    &cons_name,
724                    vec![
725                        Term::Integer(list.id as i64),
726                        head,
727                        Term::Integer(tail_id as i64),
728                    ],
729                ));
730            }
731        }
732
733        for (op, elem_type, ids) in self.operation_rows.clone() {
734            let pred = match op {
735                ListOp::Append => append_pred(elem_type),
736                ListOp::Sort => sort_pred(elem_type),
737                ListOp::MSort => msort_pred(elem_type),
738                ListOp::ListToSet => list_to_set_pred(elem_type),
739            };
740            self.helper_preds.insert(pred.clone());
741            helper_rules.push(fact(
742                &pred,
743                ids.into_iter().map(|id| Term::Integer(id as i64)).collect(),
744            ));
745        }
746
747        self.append_helper_pred_decls(program);
748        program.rules.extend(helper_rules);
749        Ok(())
750    }
751
752    fn ensure_tail_lists(&mut self) -> Result<()> {
753        loop {
754            let before = self.lists.len();
755            for list in self.lists.clone() {
756                if list.items.is_empty() {
757                    continue;
758                }
759                self.register_list_terms(list.items[1..].to_vec(), Some(list.elem_type))?;
760            }
761            if self.lists.len() == before {
762                break;
763            }
764        }
765        Ok(())
766    }
767
768    fn cons_fact_parts(&self, list_id: u64) -> Result<Option<(Term, u64)>> {
769        let list = self.list_by_id(list_id)?;
770        let Some(head) = list.items.first() else {
771            return Ok(None);
772        };
773        let tail_key = (list.elem_type, list.keys[1..].to_vec());
774        let tail_id = self
775            .lists_by_key
776            .get(&tail_key)
777            .copied()
778            .ok_or_else(|| list_error("missing normalized list tail"))?;
779        Ok(Some((head.clone(), tail_id)))
780    }
781
782    fn append_helper_pred_decls(&self, program: &mut Program) {
783        let mut existing: HashSet<String> = program
784            .predicates
785            .iter()
786            .map(|pred| pred.name.clone())
787            .collect();
788
789        for pred in &self.helper_preds {
790            if existing.contains(pred) {
791                continue;
792            }
793            let columns = helper_columns(pred);
794            program.predicates.push(PredDecl {
795                name: pred.clone(),
796                types: columns.iter().map(|col| col.typ.clone()).collect(),
797                columns,
798                is_private: true,
799            });
800            existing.insert(pred.clone());
801        }
802    }
803
804    fn list_by_id(&self, id: u64) -> Result<&ListDef> {
805        self.lists
806            .iter()
807            .find(|list| list.id == id)
808            .ok_or_else(|| list_error("unknown normalized list id"))
809    }
810
811    fn fresh_var(&mut self, prefix: &str) -> String {
812        let var = format!(
813            "__XLOG_{}_{}",
814            prefix.to_ascii_uppercase(),
815            self.fresh_counter
816        );
817        self.fresh_counter += 1;
818        var
819    }
820
821    fn storage_type_for_type_ref(
822        typ: &TypeRef,
823        domains: &HashMap<String, ScalarType>,
824    ) -> Result<ScalarType> {
825        match typ {
826            TypeRef::Scalar(ty) => Ok(*ty),
827            TypeRef::Domain(name) => domains
828                .get(name)
829                .copied()
830                .ok_or_else(|| list_error(format!("unknown domain alias '{}' in list<T>", name))),
831            TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
832                Ok(LIST_ID_TYPE)
833            }
834        }
835    }
836}
837
838fn fact(pred: &str, terms: Vec<Term>) -> Rule {
839    Rule {
840        head: Atom {
841            predicate: pred.to_string(),
842            terms,
843        },
844        body: vec![],
845    }
846}
847
848fn require_arity(atom: &Atom, expected: usize) -> Result<()> {
849    if atom.terms.len() == expected {
850        Ok(())
851    } else {
852        Err(list_error(format!(
853            "{} expects {} arguments, got {}",
854            atom.predicate,
855            expected,
856            atom.terms.len()
857        )))
858    }
859}
860
861fn literal_key(term: &Term, typ: ScalarType) -> Result<LiteralKey> {
862    match (typ, term) {
863        (ScalarType::U32, Term::Integer(v)) => Ok(LiteralKey::U32(*v as u32)),
864        (ScalarType::U64, Term::Integer(v)) => Ok(LiteralKey::U64(*v as u64)),
865        (ScalarType::I32, Term::Integer(v)) => Ok(LiteralKey::I32(*v as i32)),
866        (ScalarType::I64, Term::Integer(v)) => Ok(LiteralKey::I64(*v)),
867        (ScalarType::F32, Term::Float(v)) => Ok(LiteralKey::F32((*v as f32).to_bits())),
868        (ScalarType::F64, Term::Float(v)) => Ok(LiteralKey::F64(v.to_bits())),
869        (ScalarType::Bool, Term::Integer(v)) => Ok(LiteralKey::Bool(*v != 0)),
870        (ScalarType::Symbol, Term::String(s)) => Ok(LiteralKey::Symbol(s.clone())),
871        (ScalarType::Symbol, Term::Symbol(id)) => {
872            Ok(LiteralKey::Symbol(xlog_core::symbol::resolve(*id)))
873        }
874        _ => Err(list_error(
875            "list literal element did not normalize to expected scalar type",
876        )),
877    }
878}
879
880fn helper_columns(pred: &str) -> Vec<PredColumn> {
881    let scalar = |name: &str, typ| PredColumn {
882        name: Some(name.to_string()),
883        typ: TypeRef::Scalar(typ),
884    };
885    if pred == len_pred() {
886        return vec![
887            scalar("list_id", LIST_ID_TYPE),
888            scalar("len", ScalarType::U32),
889        ];
890    }
891    if let Some(elem_type) = helper_elem_type(pred, "__xlog_list_item_") {
892        return vec![
893            scalar("list_id", LIST_ID_TYPE),
894            scalar("idx", ScalarType::U32),
895            scalar("value", elem_type),
896        ];
897    }
898    if let Some(elem_type) = helper_elem_type(pred, "__xlog_list_cons_") {
899        return vec![
900            scalar("list_id", LIST_ID_TYPE),
901            scalar("head", elem_type),
902            scalar("tail_id", LIST_ID_TYPE),
903        ];
904    }
905    if helper_elem_type(pred, "__xlog_list_append_").is_some() {
906        return vec![
907            scalar("left_id", LIST_ID_TYPE),
908            scalar("right_id", LIST_ID_TYPE),
909            scalar("out_id", LIST_ID_TYPE),
910        ];
911    }
912    vec![
913        scalar("input_id", LIST_ID_TYPE),
914        scalar("out_id", LIST_ID_TYPE),
915    ]
916}
917
918fn helper_elem_type(pred: &str, prefix: &str) -> Option<ScalarType> {
919    pred.strip_prefix(prefix).and_then(scalar_type_from_suffix)
920}
921
922fn scalar_type_from_suffix(s: &str) -> Option<ScalarType> {
923    match s {
924        "u32" => Some(ScalarType::U32),
925        "u64" => Some(ScalarType::U64),
926        "i32" => Some(ScalarType::I32),
927        "i64" => Some(ScalarType::I64),
928        "f32" => Some(ScalarType::F32),
929        "f64" => Some(ScalarType::F64),
930        "bool" => Some(ScalarType::Bool),
931        "symbol" => Some(ScalarType::Symbol),
932        _ => None,
933    }
934}
935
936fn scalar_suffix(typ: ScalarType) -> &'static str {
937    match typ {
938        ScalarType::U32 => "u32",
939        ScalarType::U64 => "u64",
940        ScalarType::I32 => "i32",
941        ScalarType::I64 => "i64",
942        ScalarType::F32 => "f32",
943        ScalarType::F64 => "f64",
944        ScalarType::Bool => "bool",
945        ScalarType::Symbol => "symbol",
946    }
947}
948
949fn len_pred() -> &'static str {
950    "__xlog_list_len"
951}
952
953fn item_pred(typ: ScalarType) -> String {
954    format!("__xlog_list_item_{}", scalar_suffix(typ))
955}
956
957fn cons_pred(typ: ScalarType) -> String {
958    format!("__xlog_list_cons_{}", scalar_suffix(typ))
959}
960
961fn append_pred(typ: ScalarType) -> String {
962    format!("__xlog_list_append_{}", scalar_suffix(typ))
963}
964
965fn sort_pred(typ: ScalarType) -> String {
966    format!("__xlog_list_sort_{}", scalar_suffix(typ))
967}
968
969fn msort_pred(typ: ScalarType) -> String {
970    format!("__xlog_list_msort_{}", scalar_suffix(typ))
971}
972
973fn list_to_set_pred(typ: ScalarType) -> String {
974    format!("__xlog_list_to_set_{}", scalar_suffix(typ))
975}
976
977fn is_reserved_pair_helper(atom: &Atom) -> bool {
978    matches!(
979        (atom.predicate.as_str(), atom.terms.len()),
980        ("pair", 3) | ("pairs", 2) | ("zip", 3) | ("zip_with_index", 2) | ("enumerate", 2)
981    )
982}
983
984fn list_error(message: impl Into<String>) -> XlogError {
985    XlogError::Compilation(format!("list normalization error: {}", message.into()))
986}