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