1mod value;
15mod parser;
16mod engine;
17mod calc;
18pub(crate) mod rng;
19pub mod binary;
20pub mod diff;
21pub mod schema_json;
22pub mod synxl;
23#[cfg(feature = "wasm")]
24pub mod wasm;
25#[cfg(feature = "signing")]
26pub mod signing;
27
28pub use value::{Value, Mode, ParseResult, Meta, MetaMap, Options, Constraints, IncludeDirective, UseDirective};
29pub use schema_json::{metadata_to_json_schema, value_to_json_value};
30#[cfg(feature = "jsonschema")]
31pub use schema_json::{validate_serde_json, validate_with_json_schema};
32pub use parser::{parse, parse_with, reshape_tool_output, ParserOptions};
33pub use synxl::{
34 parse_lines, parse_lines_with, records_to_json_array, records_to_ndjson, write_document,
35 write_lines, Diagnostic, DiagnosticKind, FieldDecl, FieldList, SynxlDocument, SynxlError,
36 SynxlErrorKind, SynxlOptions, SynxlReader, SynxlReaderOwned, SynxlRecord, SynxlStreamError,
37 SynxlStreamReader,
38};
39pub use engine::resolve;
40pub use calc::safe_calc;
41pub use diff::{diff as diff_objects, DiffResult, DiffChange, diff_to_value};
42
43pub struct Synx;
45
46impl Synx {
47 pub fn parse(text: &str) -> std::collections::HashMap<String, Value> {
49 let result = parse(text);
50 match result.root {
51 Value::Object(map) => map,
52 _ => std::collections::HashMap::new(),
53 }
54 }
55
56 pub fn parse_active(text: &str, opts: &Options) -> std::collections::HashMap<String, Value> {
58 let mut result = parse(text);
59 if result.mode == Mode::Active {
60 resolve(&mut result, opts);
61 }
62 match result.root {
63 Value::Object(map) => map,
64 _ => std::collections::HashMap::new(),
65 }
66 }
67
68 pub fn parse_full(text: &str) -> ParseResult {
70 parse(text)
71 }
72
73 pub fn parse_tool(text: &str, opts: &Options) -> std::collections::HashMap<String, Value> {
78 let mut result = parse(text);
79 if result.mode == Mode::Active {
80 resolve(&mut result, opts);
81 }
82 let shaped = reshape_tool_output(&result.root, result.schema);
83 match shaped {
84 Value::Object(map) => map,
85 _ => std::collections::HashMap::new(),
86 }
87 }
88
89 pub fn stringify(value: &Value) -> String {
91 serialize(value, 0)
92 }
93
94 pub fn format(text: &str) -> String {
104 fmt_canonical(text)
105 }
106
107 pub fn compile(text: &str, resolved: bool) -> Vec<u8> {
112 let mut result = parse(text);
113 if resolved && result.mode == Mode::Active {
114 resolve(&mut result, &Options::default());
115 }
116 binary::compile(&result, resolved)
117 }
118
119 pub fn decompile(data: &[u8]) -> Result<String, String> {
121 let result = binary::decompile(data)?;
122 let mut out = String::new();
123 if result.tool {
124 out.push_str("!tool\n");
125 }
126 if result.schema {
127 out.push_str("!schema\n");
128 }
129 if result.llm {
130 out.push_str("!llm\n");
131 }
132 if result.mode == Mode::Active {
133 out.push_str("!active\n");
134 }
135 if result.locked {
136 out.push_str("!lock\n");
137 }
138 if !out.is_empty() {
139 out.push('\n');
140 }
141 out.push_str(&serialize(&result.root, 0));
142 Ok(out)
143 }
144
145 pub fn is_synxb(data: &[u8]) -> bool {
147 binary::is_synxb(data)
148 }
149
150 pub fn diff(
154 a: &std::collections::HashMap<String, Value>,
155 b: &std::collections::HashMap<String, Value>,
156 ) -> DiffResult {
157 diff::diff(a, b)
158 }
159}
160
161const MAX_SERIALIZE_DEPTH: usize = 128;
163
164fn serialize(value: &Value, depth_lvl: usize) -> String {
165 if depth_lvl > MAX_SERIALIZE_DEPTH {
166 return "[synx:max-depth]\n".to_string();
167 }
168 let indent = depth_lvl * 2;
169 match value {
170 Value::Object(map) => {
171 let mut out = String::new();
172 let spaces = " ".repeat(indent);
173 let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
175 keys.sort_unstable();
176 for key in keys {
177 let val = &map[key];
178 match val {
179 Value::Array(arr) => {
180 out.push_str(&spaces);
181 out.push_str(key);
182 out.push('\n');
183 for item in arr {
184 match item {
185 Value::Object(inner) => {
186 let entries: Vec<_> = inner.iter().collect();
187 if let Some((k, v)) = entries.first() {
188 out.push_str(&spaces);
189 out.push_str(" - ");
190 out.push_str(k);
191 out.push(' ');
192 out.push_str(&format_primitive(v));
193 out.push('\n');
194 for (k, v) in entries.iter().skip(1) {
195 out.push_str(&spaces);
196 out.push_str(" ");
197 out.push_str(k);
198 out.push(' ');
199 out.push_str(&format_primitive(v));
200 out.push('\n');
201 }
202 }
203 }
204 _ => {
205 out.push_str(&spaces);
206 out.push_str(" - ");
207 out.push_str(&format_primitive(item));
208 out.push('\n');
209 }
210 }
211 }
212 }
213 Value::Object(_) => {
214 out.push_str(&spaces);
215 out.push_str(key);
216 out.push('\n');
217 out.push_str(&serialize(val, depth_lvl + 1));
218 }
219 Value::String(s) if s.contains('\n') => {
220 out.push_str(&spaces);
221 out.push_str(key);
222 out.push_str(" |\n");
223 for line in s.lines() {
224 out.push_str(&spaces);
225 out.push_str(" ");
226 out.push_str(line);
227 out.push('\n');
228 }
229 }
230 _ => {
231 out.push_str(&spaces);
232 out.push_str(key);
233 out.push(' ');
234 out.push_str(&format_primitive(val));
235 out.push('\n');
236 }
237 }
238 }
239 out
240 }
241 _ => format_primitive(value),
242 }
243}
244
245fn format_primitive(value: &Value) -> String {
246 match value {
247 Value::String(s) => s.clone(),
248 Value::Int(n) => n.to_string(),
249 Value::Float(f) => {
250 let s = f.to_string();
251 if s.contains('.') { s } else { format!("{}.0", s) }
252 }
253 Value::Bool(b) => b.to_string(),
254 Value::Null => "null".to_string(),
255 Value::Array(arr) => {
256 let items: Vec<String> = arr.iter().map(format_primitive).collect();
257 format!("[{}]", items.join(", "))
258 }
259 Value::Object(_) => "[Object]".to_string(),
260 Value::Secret(_) => "[SECRET]".to_string(),
261 }
262}
263
264const MAX_JSON_DEPTH: usize = 128;
266
267pub fn write_json(out: &mut String, val: &Value) {
269 write_json_depth(out, val, 0);
270}
271
272fn write_json_depth(out: &mut String, val: &Value, depth: usize) {
273 if depth > MAX_JSON_DEPTH {
274 out.push_str("null");
275 return;
276 }
277 match val {
278 Value::Null => out.push_str("null"),
279 Value::Bool(true) => out.push_str("true"),
280 Value::Bool(false) => out.push_str("false"),
281 Value::Int(n) => {
282 let mut buf = itoa::Buffer::new();
283 out.push_str(buf.format(*n));
284 }
285 Value::Float(f) => {
286 if f.is_nan() || f.is_infinite() {
288 out.push_str("null");
289 } else {
290 let mut buf = ryu::Buffer::new();
291 out.push_str(buf.format(*f));
292 }
293 }
294 Value::String(s) => {
295 out.push('"');
296 for ch in s.chars() {
297 match ch {
298 '"' => out.push_str("\\\""),
299 '\\' => out.push_str("\\\\"),
300 '\n' => out.push_str("\\n"),
301 '\r' => out.push_str("\\r"),
302 '\t' => out.push_str("\\t"),
303 c if (c as u32) < 0x20 => {
304 out.push_str(&format!("\\u{:04x}", c as u32));
305 }
306 c => out.push(c),
307 }
308 }
309 out.push('"');
310 }
311 Value::Secret(_) => {
312 out.push_str("\"[SECRET]\"");
317 }
318 Value::Array(arr) => {
319 out.push('[');
320 for (i, item) in arr.iter().enumerate() {
321 if i > 0 { out.push(','); }
322 write_json_depth(out, item, depth + 1);
323 }
324 out.push(']');
325 }
326 Value::Object(map) => {
327 out.push('{');
328 let mut first = true;
329 let mut entries: Vec<(&str, &Value)> =
331 map.iter().map(|(k, v)| (k.as_str(), v)).collect();
332 entries.sort_unstable_by_key(|(k, _)| *k);
333 for (key, val) in entries {
334 if !first { out.push(','); }
335 first = false;
336 out.push('"');
338 for ch in key.chars() {
339 match ch {
340 '"' => out.push_str("\\\""),
341 '\\' => out.push_str("\\\\"),
342 '\n' => out.push_str("\\n"),
343 '\r' => out.push_str("\\r"),
344 '\t' => out.push_str("\\t"),
345 c if (c as u32) < 0x20 => {
346 out.push_str(&format!("\\u{:04x}", c as u32));
347 }
348 c => out.push(c),
349 }
350 }
351 out.push_str("\":");
352 write_json_depth(out, val, depth + 1);
353 }
354 out.push('}');
355 }
356 }
357}
358
359pub fn to_json(val: &Value) -> String {
361 let mut out = String::with_capacity(2048);
362 write_json(&mut out, val);
363 out
364}
365
366struct FmtNode {
369 header: String,
370 children: Vec<FmtNode>,
371 list_items: Vec<String>,
372 is_multiline: bool,
373}
374
375fn fmt_indent(line: &str) -> usize {
376 line.len() - line.trim_start().len()
377}
378
379const MAX_FMT_PARSE_DEPTH: usize = 128;
380
381fn fmt_parse(lines: &[&str], start: usize, base: usize, depth: usize) -> (Vec<FmtNode>, usize) {
382 if depth > MAX_FMT_PARSE_DEPTH {
383 return (Vec::new(), start);
384 }
385 let mut nodes = Vec::new();
386 let mut i = start;
387 while i < lines.len() {
388 let raw = lines[i];
389 let t = raw.trim();
390 if t.is_empty() { i += 1; continue; }
391 let ind = fmt_indent(raw);
392 if ind < base { break; }
393 if ind > base { i += 1; continue; }
394 if t.starts_with("- ") || t.starts_with('#') || t.starts_with("//") { i += 1; continue; }
395 let is_multiline = t.ends_with(" |") || t == "|";
396 let mut node = FmtNode {
397 header: t.to_string(),
398 children: Vec::new(),
399 list_items: Vec::new(),
400 is_multiline,
401 };
402 i += 1;
403 while i < lines.len() {
404 let cr = lines[i];
405 let ct = cr.trim();
406 if ct.is_empty() { i += 1; continue; }
407 let ci = fmt_indent(cr);
408 if ci <= base { break; }
409 if node.is_multiline || ct.starts_with("- ") {
410 node.list_items.push(ct.to_string());
411 i += 1;
412 } else if ct.starts_with('#') || ct.starts_with("//") {
413 i += 1;
414 } else {
415 let (subs, ni) = fmt_parse(lines, i, ci, depth + 1);
416 node.children.extend(subs);
417 i = ni;
418 }
419 }
420 nodes.push(node);
421 }
422 (nodes, i)
423}
424
425fn fmt_sort(nodes: &mut Vec<FmtNode>) {
426 nodes.sort_unstable_by(|a, b| {
427 let ka = a.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
428 .next().unwrap_or("").to_lowercase();
429 let kb = b.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
430 .next().unwrap_or("").to_lowercase();
431 ka.cmp(&kb)
432 });
433 for node in nodes.iter_mut() {
434 fmt_sort(&mut node.children);
435 }
436}
437
438fn fmt_emit(nodes: &[FmtNode], indent: usize, out: &mut String) {
439 let sp = " ".repeat(indent);
440 let item_sp = " ".repeat(indent + 2);
441 for n in nodes {
442 out.push_str(&sp);
443 out.push_str(&n.header);
444 out.push('\n');
445 if !n.children.is_empty() {
446 fmt_emit(&n.children, indent + 2, out);
447 }
448 for li in &n.list_items {
449 out.push_str(&item_sp);
450 out.push_str(li);
451 out.push('\n');
452 }
453 if indent == 0 && (!n.children.is_empty() || !n.list_items.is_empty()) {
454 out.push('\n');
455 }
456 }
457}
458
459fn fmt_canonical(text: &str) -> String {
460 let text = parser::clamp_synx_text(text);
461 let lines: Vec<&str> = text.lines().collect();
462 let mut directives: Vec<&str> = Vec::new();
463 let mut body_start = 0usize;
464
465 for (i, &line) in lines.iter().enumerate() {
466 let t = line.trim();
467 if t == "!active"
468 || t == "!lock"
469 || t == "!tool"
470 || t == "!schema"
471 || t == "!llm"
472 || t == "#!mode:active"
473 {
474 directives.push(t);
475 body_start = i + 1;
476 } else if t.is_empty() || t.starts_with('#') || t.starts_with("//") {
477 body_start = i + 1;
478 } else {
479 break;
480 }
481 }
482
483 let (mut nodes, _) = fmt_parse(&lines, body_start, 0, 0);
484 fmt_sort(&mut nodes);
485
486 let cap = text.len().min(parser::MAX_SYNX_INPUT_BYTES).max(64);
487 let mut out = String::with_capacity(cap);
488 if !directives.is_empty() {
489 out.push_str(&directives.join("\n"));
490 out.push_str("\n\n");
491 }
492 fmt_emit(&nodes, 0, &mut out);
493 let trimmed = out.trim_end();
495 let mut result = trimmed.to_string();
496 result.push('\n');
497 result
498}