Skip to main content

typr_core/processes/spg/
model.rs

1#![allow(dead_code)]
2
3use serde::{Deserialize, Serialize};
4
5/// Source location for an SPG node (file + byte offset + 1-based line).
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct SourceLoc {
8    pub file: String,
9    pub offset: usize,
10    pub line: u32,
11}
12
13impl SourceLoc {
14    /// Compute line number by counting '\n' in the source up to `offset`.
15    pub fn from_offset(file: impl Into<String>, offset: usize, source: &str) -> Self {
16        let line = source[..offset.min(source.len())]
17            .chars()
18            .filter(|&c| c == '\n')
19            .count() as u32
20            + 1;
21        SourceLoc {
22            file: file.into(),
23            offset,
24            line,
25        }
26    }
27}
28
29/// Coarse kind of a documented entity.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
31#[serde(rename_all = "snake_case")]
32pub enum NodeKind {
33    Function,
34    Alias,
35    TypeDef,
36    Module,
37    Variable,
38}
39
40impl NodeKind {
41    pub fn prefix(&self) -> &'static str {
42        match self {
43            NodeKind::Function => "function",
44            NodeKind::Alias => "alias",
45            NodeKind::TypeDef => "type",
46            NodeKind::Module => "module",
47            NodeKind::Variable => "variable",
48        }
49    }
50}
51
52/// Visibility of a binding.
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum Visibility {
56    Private,
57    Public,
58    Export,
59}
60
61/// Per-kind extra data carried by a node.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(tag = "kind", rename_all = "snake_case")]
64pub enum NodePayload {
65    Function {
66        /// (param_name, type_str) pairs, in declaration order.
67        params: Vec<(String, String)>,
68        returns: String,
69    },
70    Record {
71        /// (field_name, type_str) pairs, sorted alphabetically for snapshot stability.
72        fields: Vec<(String, String)>,
73    },
74    Alias {
75        underlying: String,
76        opaque: bool,
77    },
78    Union {
79        variants: Vec<(String, Option<String>)>,
80    },
81    Module {
82        exports: Vec<String>,
83    },
84    /// A plain (non-function) `let` binding with an explicit type annotation —
85    /// e.g. `@export let PI: num <- 3.14159;`. Un-annotated bindings carry no
86    /// static type on the AST alone (see `builder.rs`'s `Lang::Let` arm), so
87    /// they are not represented here; this mirrors the same annotated-only
88    /// scope `--checked` uses for the same reason (soundness_transpilation.md
89    /// Phase A).
90    Variable {
91        type_str: String,
92    },
93    None,
94}
95
96/// A node in the Semantic Package Graph.
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98pub struct Node {
99    /// Stable deterministic id: `<kind>:<module_path>/<name>`
100    pub id: String,
101    pub kind: NodeKind,
102    pub name: String,
103    /// Nesting path of module names (empty for top-level).
104    pub module_path: Vec<String>,
105    pub visibility: Visibility,
106    /// Doc-comment attached by `doc_attach.rs` (Phase 4). `None` until then.
107    pub doc: Option<String>,
108    pub source: Option<SourceLoc>,
109    pub payload: NodePayload,
110}
111
112impl Node {
113    /// Build the canonical `id` from kind, module path, and name.
114    pub fn make_id(kind: &NodeKind, module_path: &[String], name: &str) -> String {
115        if module_path.is_empty() {
116            format!("{}:{}", kind.prefix(), name)
117        } else {
118            format!("{}:{}/{}", kind.prefix(), module_path.join("/"), name)
119        }
120    }
121}
122
123/// Kind of a directed edge in the graph.
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125#[serde(rename_all = "kebab-case")]
126pub enum EdgeKind {
127    HasField,
128    Returns,
129    ProducesType,
130    ParameterOf,
131    ConsumesType,
132    BelongsToModule,
133    Uses,
134    UsedBy,
135}
136
137/// A directed edge between two nodes.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139pub struct Edge {
140    pub from: String,
141    pub to: String,
142    pub kind: EdgeKind,
143}
144
145/// Root of the Semantic Package Graph (serialises to `spg.json`).
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Spg {
148    /// JSON-LD `@context` — placeholder until a proper vocabulary is published.
149    #[serde(rename = "@context")]
150    pub context: String,
151    pub package: String,
152    pub version: String,
153    pub nodes: Vec<Node>,
154    pub edges: Vec<Edge>,
155}
156
157impl Spg {
158    pub fn new(package: impl Into<String>, version: impl Into<String>) -> Self {
159        Spg {
160            context: "https://typr-lang.dev/spg/v1/context.jsonld".into(),
161            package: package.into(),
162            version: version.into(),
163            nodes: Vec::new(),
164            edges: Vec::new(),
165        }
166    }
167
168    pub fn add_node(&mut self, node: Node) {
169        self.nodes.push(node);
170    }
171
172    pub fn add_edge(&mut self, edge: Edge) {
173        self.edges.push(edge);
174    }
175}