Skip to main content

harn_parser/
ast_json.rs

1//! Stable JSON projections for parser-facing tooling.
2//!
3//! The core AST keeps its Rust-native shape for compiler consumers.
4//! This module owns the CLI/tooling projection so `harn parse --json`
5//! and `harn tokens --json` can evolve under explicit schema versions.
6
7use harn_lexer::{Span, Token, TokenKind};
8use serde::Serialize;
9use serde_json::{Map, Value};
10
11use crate::SNode;
12
13pub const AST_JSON_SCHEMA_VERSION: u32 = 1;
14pub const TOKEN_JSON_SCHEMA_VERSION: u32 = 1;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub struct AstJsonProgram {
18    pub kind: &'static str,
19    #[serde(rename = "schemaVersion")]
20    pub schema_version: u32,
21    pub body: Vec<Value>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25pub struct TokenJson {
26    pub kind: &'static str,
27    pub lexeme: String,
28    pub start: usize,
29    pub end: usize,
30    pub line: usize,
31    pub column: usize,
32}
33
34pub fn program_to_json(nodes: &[SNode]) -> AstJsonProgram {
35    AstJsonProgram {
36        kind: "Program",
37        schema_version: AST_JSON_SCHEMA_VERSION,
38        body: nodes.iter().map(node_to_json).collect(),
39    }
40}
41
42pub fn node_to_json(node: &SNode) -> Value {
43    let raw = serde_json::to_value(node).expect("AST node should serialize to JSON");
44    normalize_value(raw)
45}
46
47pub fn tokens_to_json(source: &str, tokens: &[Token]) -> Vec<TokenJson> {
48    tokens
49        .iter()
50        .map(|token| token_to_json(source, token))
51        .collect()
52}
53
54pub fn token_to_json(source: &str, token: &Token) -> TokenJson {
55    TokenJson {
56        kind: token_kind_name(&token.kind),
57        lexeme: lexeme_for_span(source, token.span),
58        start: token.span.start,
59        end: token.span.end,
60        line: token.span.line,
61        column: token.span.column,
62    }
63}
64
65pub fn token_kind_name(kind: &TokenKind) -> &'static str {
66    match kind {
67        TokenKind::Pipeline => "Pipeline",
68        TokenKind::Extends => "Extends",
69        TokenKind::Override => "Override",
70        TokenKind::Let => "Let",
71        TokenKind::Const => "Const",
72        TokenKind::Var => "Var",
73        TokenKind::If => "If",
74        TokenKind::Else => "Else",
75        TokenKind::For => "For",
76        TokenKind::In => "In",
77        TokenKind::Match => "Match",
78        TokenKind::Retry => "Retry",
79        TokenKind::Parallel => "Parallel",
80        TokenKind::Return => "Return",
81        TokenKind::Import => "Import",
82        TokenKind::True => "True",
83        TokenKind::False => "False",
84        TokenKind::Nil => "Nil",
85        TokenKind::Try => "Try",
86        TokenKind::Catch => "Catch",
87        TokenKind::Throw => "Throw",
88        TokenKind::Finally => "Finally",
89        TokenKind::Fn => "Fn",
90        TokenKind::Spawn => "Spawn",
91        TokenKind::While => "While",
92        TokenKind::TypeKw => "TypeKw",
93        TokenKind::Enum => "Enum",
94        TokenKind::EvalPack => "EvalPack",
95        TokenKind::Struct => "Struct",
96        TokenKind::Interface => "Interface",
97        TokenKind::Emit => "Emit",
98        TokenKind::Pub => "Pub",
99        TokenKind::From => "From",
100        TokenKind::To => "To",
101        TokenKind::Tool => "Tool",
102        TokenKind::Exclusive => "Exclusive",
103        TokenKind::Guard => "Guard",
104        TokenKind::Require => "Require",
105        TokenKind::Deadline => "Deadline",
106        TokenKind::Defer => "Defer",
107        TokenKind::Yield => "Yield",
108        TokenKind::Mutex => "Mutex",
109        TokenKind::Break => "Break",
110        TokenKind::Continue => "Continue",
111        TokenKind::Select => "Select",
112        TokenKind::Impl => "Impl",
113        TokenKind::Skill => "Skill",
114        TokenKind::RequestApproval => "RequestApproval",
115        TokenKind::DualControl => "DualControl",
116        TokenKind::AskUser => "AskUser",
117        TokenKind::EscalateTo => "EscalateTo",
118        TokenKind::Throws => "Throws",
119        TokenKind::Identifier(_) => "Identifier",
120        TokenKind::StringLiteral(_) => "StringLiteral",
121        TokenKind::InterpolatedString(_) => "InterpolatedString",
122        TokenKind::RawStringLiteral(_) => "RawStringLiteral",
123        TokenKind::IntLiteral(_) => "IntLiteral",
124        TokenKind::FloatLiteral(_) => "FloatLiteral",
125        TokenKind::DurationLiteral(_) => "DurationLiteral",
126        TokenKind::Eq => "Eq",
127        TokenKind::Neq => "Neq",
128        TokenKind::And => "And",
129        TokenKind::Or => "Or",
130        TokenKind::Pipe => "Pipe",
131        TokenKind::NilCoal => "NilCoal",
132        TokenKind::Pow => "Pow",
133        TokenKind::QuestionDot => "QuestionDot",
134        TokenKind::Arrow => "Arrow",
135        TokenKind::Lte => "Lte",
136        TokenKind::Gte => "Gte",
137        TokenKind::PlusAssign => "PlusAssign",
138        TokenKind::MinusAssign => "MinusAssign",
139        TokenKind::StarAssign => "StarAssign",
140        TokenKind::SlashAssign => "SlashAssign",
141        TokenKind::PercentAssign => "PercentAssign",
142        TokenKind::Assign => "Assign",
143        TokenKind::Not => "Not",
144        TokenKind::Dot => "Dot",
145        TokenKind::Plus => "Plus",
146        TokenKind::Minus => "Minus",
147        TokenKind::Star => "Star",
148        TokenKind::Slash => "Slash",
149        TokenKind::Percent => "Percent",
150        TokenKind::Lt => "Lt",
151        TokenKind::Gt => "Gt",
152        TokenKind::Question => "Question",
153        TokenKind::Bar => "Bar",
154        TokenKind::Amp => "Amp",
155        TokenKind::LBrace => "LBrace",
156        TokenKind::RBrace => "RBrace",
157        TokenKind::LParen => "LParen",
158        TokenKind::RParen => "RParen",
159        TokenKind::LBracket => "LBracket",
160        TokenKind::RBracket => "RBracket",
161        TokenKind::Comma => "Comma",
162        TokenKind::Colon => "Colon",
163        TokenKind::Semicolon => "Semicolon",
164        TokenKind::At => "At",
165        TokenKind::LineComment { .. } => "LineComment",
166        TokenKind::BlockComment { .. } => "BlockComment",
167        TokenKind::Newline => "Newline",
168        TokenKind::Eof => "Eof",
169    }
170}
171
172fn lexeme_for_span(source: &str, span: Span) -> String {
173    source
174        .get(span.start..span.end)
175        .unwrap_or_default()
176        .to_string()
177}
178
179fn normalize_value(value: Value) -> Value {
180    match value {
181        Value::Array(values) => Value::Array(values.into_iter().map(normalize_value).collect()),
182        Value::Object(map) => normalize_object(map),
183        other => other,
184    }
185}
186
187fn normalize_object(mut map: Map<String, Value>) -> Value {
188    if is_span_map(&map) {
189        return normalize_span_map(map);
190    }
191
192    if is_spanned_map(&map) {
193        return normalize_spanned_map(map);
194    }
195
196    if map.len() == 1 {
197        let key = map.keys().next().cloned().expect("map has one key");
198        if is_variant_name(&key) {
199            let fields = map.remove(&key).expect("variant payload is present");
200            return tagged_value(key, normalize_value(fields));
201        }
202    }
203
204    for value in map.values_mut() {
205        let raw = std::mem::take(value);
206        *value = normalize_value(raw);
207    }
208    Value::Object(map)
209}
210
211fn normalize_spanned_map(mut map: Map<String, Value>) -> Value {
212    let node = map.remove("node").expect("spanned node is present");
213    let span = map.remove("span").expect("spanned span is present");
214    let mut normalized = normalize_enum_value(node);
215    if let Value::Object(object) = &mut normalized {
216        object.insert("span".to_string(), normalize_value(span));
217    }
218    normalized
219}
220
221fn normalize_enum_value(value: Value) -> Value {
222    match value {
223        Value::Object(mut map) if map.len() == 1 => {
224            let key = map.keys().next().cloned().expect("map has one key");
225            let fields = map.remove(&key).expect("variant payload is present");
226            tagged_value(key, normalize_value(fields))
227        }
228        Value::String(kind) if is_variant_name(&kind) => tagged_value(kind, Value::Null),
229        other => normalize_value(other),
230    }
231}
232
233fn tagged_value(kind: String, fields: Value) -> Value {
234    let mut object = Map::new();
235    object.insert("kind".to_string(), Value::String(kind));
236    object.insert("fields".to_string(), fields);
237    Value::Object(object)
238}
239
240fn normalize_span_map(mut map: Map<String, Value>) -> Value {
241    if let Some(end_line) = map.remove("end_line") {
242        map.insert("endLine".to_string(), end_line);
243    }
244    Value::Object(map)
245}
246
247fn is_spanned_map(map: &Map<String, Value>) -> bool {
248    map.len() == 2 && map.contains_key("node") && map.contains_key("span")
249}
250
251fn is_span_map(map: &Map<String, Value>) -> bool {
252    map.len() == 5
253        && map.contains_key("start")
254        && map.contains_key("end")
255        && map.contains_key("line")
256        && map.contains_key("column")
257        && map.contains_key("end_line")
258}
259
260fn is_variant_name(name: &str) -> bool {
261    name.chars()
262        .next()
263        .is_some_and(|ch| ch.is_ascii_uppercase())
264}
265
266#[cfg(test)]
267mod tests {
268    use harn_lexer::Lexer;
269
270    use super::*;
271    use crate::parse_source;
272
273    #[test]
274    fn program_json_projects_tagged_nodes_with_spans() {
275        let source = "pipeline main(task) {\n  return 1\n}\n";
276        let program = parse_source(source).expect("parse");
277
278        let json = program_to_json(&program);
279
280        assert_eq!(json.kind, "Program");
281        assert_eq!(json.schema_version, AST_JSON_SCHEMA_VERSION);
282        let pipeline = &json.body[0];
283        assert_eq!(pipeline["kind"], "Pipeline");
284        assert_eq!(pipeline["span"]["start"], 0);
285        assert_eq!(pipeline["span"]["line"], 1);
286        assert_eq!(pipeline["fields"]["name"], "main");
287        assert_eq!(pipeline["fields"]["body"][0]["kind"], "ReturnStmt");
288        assert_eq!(
289            pipeline["fields"]["body"][0]["fields"]["value"]["kind"],
290            "IntLiteral"
291        );
292    }
293
294    #[test]
295    fn token_json_preserves_byte_spans_and_lexemes() {
296        let source = "let x = \"é\"\n";
297        let mut lexer = Lexer::new(source);
298        let tokens = lexer.tokenize_with_comments().expect("tokenize");
299
300        let json = tokens_to_json(source, &tokens);
301        let string = json
302            .iter()
303            .find(|token| token.kind == "StringLiteral")
304            .expect("string literal token");
305
306        let quote = source.find('"').expect("opening quote");
307        assert_eq!(string.lexeme, "\"é\"");
308        assert_eq!(string.start, quote);
309        assert_eq!(string.end, quote + "\"é\"".len());
310        assert_eq!(string.line, 1);
311        assert_eq!(string.column, 9);
312    }
313}