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) | Value::Secret(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::Array(arr) => {
300 out.push('[');
301 for (i, item) in arr.iter().enumerate() {
302 if i > 0 { out.push(','); }
303 write_json_depth(out, item, depth + 1);
304 }
305 out.push(']');
306 }
307 Value::Object(map) => {
308 out.push('{');
309 let mut first = true;
310 let mut entries: Vec<(&str, &Value)> =
312 map.iter().map(|(k, v)| (k.as_str(), v)).collect();
313 entries.sort_unstable_by_key(|(k, _)| *k);
314 for (key, val) in entries {
315 if !first { out.push(','); }
316 first = false;
317 out.push('"');
319 for ch in key.chars() {
320 match ch {
321 '"' => out.push_str("\\\""),
322 '\\' => out.push_str("\\\\"),
323 '\n' => out.push_str("\\n"),
324 '\r' => out.push_str("\\r"),
325 '\t' => out.push_str("\\t"),
326 c if (c as u32) < 0x20 => {
327 out.push_str(&format!("\\u{:04x}", c as u32));
328 }
329 c => out.push(c),
330 }
331 }
332 out.push_str("\":");
333 write_json_depth(out, val, depth + 1);
334 }
335 out.push('}');
336 }
337 }
338}
339
340pub fn to_json(val: &Value) -> String {
342 let mut out = String::with_capacity(2048);
343 write_json(&mut out, val);
344 out
345}
346
347struct FmtNode {
350 header: String,
351 children: Vec<FmtNode>,
352 list_items: Vec<String>,
353 is_multiline: bool,
354}
355
356fn fmt_indent(line: &str) -> usize {
357 line.len() - line.trim_start().len()
358}
359
360const MAX_FMT_PARSE_DEPTH: usize = 128;
361
362fn fmt_parse(lines: &[&str], start: usize, base: usize, depth: usize) -> (Vec<FmtNode>, usize) {
363 if depth > MAX_FMT_PARSE_DEPTH {
364 return (Vec::new(), start);
365 }
366 let mut nodes = Vec::new();
367 let mut i = start;
368 while i < lines.len() {
369 let raw = lines[i];
370 let t = raw.trim();
371 if t.is_empty() { i += 1; continue; }
372 let ind = fmt_indent(raw);
373 if ind < base { break; }
374 if ind > base { i += 1; continue; }
375 if t.starts_with("- ") || t.starts_with('#') || t.starts_with("//") { i += 1; continue; }
376 let is_multiline = t.ends_with(" |") || t == "|";
377 let mut node = FmtNode {
378 header: t.to_string(),
379 children: Vec::new(),
380 list_items: Vec::new(),
381 is_multiline,
382 };
383 i += 1;
384 while i < lines.len() {
385 let cr = lines[i];
386 let ct = cr.trim();
387 if ct.is_empty() { i += 1; continue; }
388 let ci = fmt_indent(cr);
389 if ci <= base { break; }
390 if node.is_multiline || ct.starts_with("- ") {
391 node.list_items.push(ct.to_string());
392 i += 1;
393 } else if ct.starts_with('#') || ct.starts_with("//") {
394 i += 1;
395 } else {
396 let (subs, ni) = fmt_parse(lines, i, ci, depth + 1);
397 node.children.extend(subs);
398 i = ni;
399 }
400 }
401 nodes.push(node);
402 }
403 (nodes, i)
404}
405
406fn fmt_sort(nodes: &mut Vec<FmtNode>) {
407 nodes.sort_unstable_by(|a, b| {
408 let ka = a.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
409 .next().unwrap_or("").to_lowercase();
410 let kb = b.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
411 .next().unwrap_or("").to_lowercase();
412 ka.cmp(&kb)
413 });
414 for node in nodes.iter_mut() {
415 fmt_sort(&mut node.children);
416 }
417}
418
419fn fmt_emit(nodes: &[FmtNode], indent: usize, out: &mut String) {
420 let sp = " ".repeat(indent);
421 let item_sp = " ".repeat(indent + 2);
422 for n in nodes {
423 out.push_str(&sp);
424 out.push_str(&n.header);
425 out.push('\n');
426 if !n.children.is_empty() {
427 fmt_emit(&n.children, indent + 2, out);
428 }
429 for li in &n.list_items {
430 out.push_str(&item_sp);
431 out.push_str(li);
432 out.push('\n');
433 }
434 if indent == 0 && (!n.children.is_empty() || !n.list_items.is_empty()) {
435 out.push('\n');
436 }
437 }
438}
439
440fn fmt_canonical(text: &str) -> String {
441 let text = parser::clamp_synx_text(text);
442 let lines: Vec<&str> = text.lines().collect();
443 let mut directives: Vec<&str> = Vec::new();
444 let mut body_start = 0usize;
445
446 for (i, &line) in lines.iter().enumerate() {
447 let t = line.trim();
448 if t == "!active"
449 || t == "!lock"
450 || t == "!tool"
451 || t == "!schema"
452 || t == "!llm"
453 || t == "#!mode:active"
454 {
455 directives.push(t);
456 body_start = i + 1;
457 } else if t.is_empty() || t.starts_with('#') || t.starts_with("//") {
458 body_start = i + 1;
459 } else {
460 break;
461 }
462 }
463
464 let (mut nodes, _) = fmt_parse(&lines, body_start, 0, 0);
465 fmt_sort(&mut nodes);
466
467 let cap = text.len().min(parser::MAX_SYNX_INPUT_BYTES).max(64);
468 let mut out = String::with_capacity(cap);
469 if !directives.is_empty() {
470 out.push_str(&directives.join("\n"));
471 out.push_str("\n\n");
472 }
473 fmt_emit(&nodes, 0, &mut out);
474 let trimmed = out.trim_end();
476 let mut result = trimmed.to_string();
477 result.push('\n');
478 result
479}