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