Skip to main content

elenchus_compiler/
data.rs

1//! Data-only sources: read a `.vrf` of `PROVIDE` values into port bindings.
2use crate::error::CompileError;
3use crate::ir::PortBinding;
4use crate::resolver::parse_tagged;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use elenchus_parser::Statement;
8
9/// Parse a data-only `.vrf` source and extract its `PROVIDE` bindings as
10/// `(name, value)` pairs. A data file carries only values: any statement other
11/// than `PROVIDE` (or a `DOMAIN`) is a [`CompileError::DataFileStatement`]. Used to
12/// load a `--data <file>` of port values without compiling it as a program.
13pub fn read_data_source(file: &str, src: &str) -> Result<Vec<(String, bool)>, CompileError> {
14    let program = parse_tagged(file, src)?;
15    let mut out = Vec::new();
16    for stmt in &program.statements {
17        match stmt {
18            Statement::Provide { atom, value } => {
19                // Serialize the target into a key string (`[domain.]subject[
20                // predicate[ object]]`) that `parse_port_ref` re-parses uniformly,
21                // so a data file and a `--set` key share one resolution path.
22                let a = &atom.data;
23                let mut key = String::new();
24                if let Some(d) = a.domain {
25                    key.push_str(d);
26                    key.push('.');
27                }
28                key.push_str(a.subject);
29                if let Some(p) = a.predicate {
30                    key.push(' ');
31                    key.push_str(p);
32                }
33                if let Some(o) = a.object {
34                    key.push(' ');
35                    key.push_str(o);
36                }
37                out.push((key, *value));
38            }
39            Statement::Domain(_) => {}
40            other => {
41                return Err(CompileError::DataFileStatement {
42                    file: file.to_string(),
43                    line: statement_line(other),
44                });
45            }
46        }
47    }
48    Ok(out)
49}
50
51/// Parse a data-only `.vrf` source into ready-to-merge port [`PortBinding`]s, each
52/// tagged with origin `data:<file>`. The shared bridge every surface uses to turn a
53/// `--data` / data-map source into engine inputs, so a data file behaves identically
54/// whether it arrives from the CLI, wasm, or MCP.
55pub fn read_data_bindings(
56    file: &str,
57    src: &str,
58) -> Result<Vec<(String, PortBinding)>, CompileError> {
59    Ok(read_data_source(file, src)?
60        .into_iter()
61        .map(|(name, value)| {
62            (
63                name,
64                PortBinding {
65                    value,
66                    origin: alloc::format!("data:{file}"),
67                },
68            )
69        })
70        .collect())
71}
72
73/// The 1-based source line a statement begins on (for diagnostics).
74fn statement_line(s: &Statement) -> u32 {
75    match s {
76        Statement::Domain(n) => n.span.location_line(),
77        Statement::Import { path, .. } => path.span.location_line(),
78        Statement::Fact(a) | Statement::Negation(a) => a.span.location_line(),
79        Statement::Assume(l) => l.span.location_line(),
80        Statement::Set { name, .. } => name.span.location_line(),
81        Statement::Close { relation, .. } => relation.span.location_line(),
82        Statement::Var { name, .. } => name.span.location_line(),
83        Statement::Provide { atom, .. } => atom.span.location_line(),
84        Statement::Premise { name, .. } | Statement::Rule { name, .. } => name.span.location_line(),
85        Statement::Check { subject, .. } => subject
86            .as_ref()
87            .map(|s| s.span.location_line())
88            .unwrap_or(0),
89    }
90}