Skip to main content

superfluid_executor/
grammar.rs

1//! Grammar-constrained decoding for primitive runtimes, through llguidance.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use superfluid_abi::Status;
7use llguidance::api::TopLevelGrammar;
8use llguidance::toktrie::{ApproximateTokEnv, SimpleVob, TokEnv, TokRxInfo, TokTrie};
9use llguidance::{Matcher, ParserFactory, TokenParser};
10use serde_json::Value;
11
12use crate::primitives::Vocabulary;
13
14struct Compiler {
15    factory: ParserFactory,
16    specials: Vec<(String, u32)>,
17    eos: Arc<Vec<u32>>,
18}
19
20impl Compiler {
21    fn new(vocab: Vocabulary) -> Result<Compiler, Status> {
22        let Some(&primary) = vocab.eos.first() else {
23            return Err(Status::Unsupported);
24        };
25        let info = TokRxInfo {
26            vocab_size: vocab.tokens.len() as u32,
27            tok_eos: primary,
28            tok_bos: None,
29            tok_pad: None,
30            tok_unk: None,
31            tok_end_of_turn: None,
32        };
33        let trie = TokTrie::from(&info, &vocab.tokens);
34        let env: TokEnv = Arc::new(ApproximateTokEnv::new(trie));
35        let mut factory = ParserFactory::new_simple(&env).map_err(|_| Status::Fatal)?;
36        factory.quiet();
37        let mut specials = vocab.specials;
38        specials.retain(|(s, _)| !s.is_empty());
39        specials.sort_by_key(|(s, _)| std::cmp::Reverse(s.len()));
40        Ok(Compiler { factory, specials, eos: Arc::new(vocab.eos) })
41    }
42
43    fn compile(&self, grammar: TopLevelGrammar) -> Result<TokenParser, Status> {
44        self.factory.create_parser(grammar).map_err(|_| Status::RejectBadStruct)
45    }
46}
47
48struct Entry {
49    proto: TokenParser,
50    open_ended: bool,
51    in_use: bool,
52    pending_free: bool,
53}
54
55#[derive(Default)]
56pub(crate) struct Grammars {
57    compiler: Option<Compiler>,
58    entries: HashMap<u32, Entry>,
59    next: u32,
60}
61
62impl Grammars {
63    fn compiler(&mut self, vocab: impl FnOnce() -> Option<Vocabulary>) -> Result<&Compiler, Status> {
64        if self.compiler.is_none() {
65            let v = vocab().ok_or(Status::Unsupported)?;
66            self.compiler = Some(Compiler::new(v)?);
67        }
68        Ok(self.compiler.as_ref().expect("built above"))
69    }
70
71    fn register(&mut self, proto: TokenParser, open_ended: bool) -> u32 {
72        self.next += 1;
73        let h = self.next;
74        self.entries.insert(h, Entry { proto, open_ended, in_use: false, pending_free: false });
75        h
76    }
77
78    pub(crate) fn create_schema(
79        &mut self,
80        vocab: impl FnOnce() -> Option<Vocabulary>,
81        json_schema: &str,
82    ) -> Result<u32, Status> {
83        let schema: Value = serde_json::from_str(json_schema).map_err(|_| Status::RejectBadStruct)?;
84        let proto = self.compiler(vocab)?.compile(TopLevelGrammar::from_json_schema(prepared(schema)))?;
85        Ok(self.register(proto, false))
86    }
87
88    pub(crate) fn create_structural(
89        &mut self,
90        vocab: impl FnOnce() -> Option<Vocabulary>,
91        tag_json: &str,
92    ) -> Result<u32, Status> {
93        let tag: Value = serde_json::from_str(tag_json).map_err(|_| Status::RejectBadStruct)?;
94        let c = self.compiler(vocab)?;
95        let lark = structural_to_lark(&tag, &c.specials, &c.eos)?;
96        let proto = c.compile(TopLevelGrammar::from_lark(lark))?;
97        Ok(self.register(proto, true))
98    }
99
100    pub(crate) fn free(&mut self, handle: u32) -> Result<(), Status> {
101        let e = self.entries.get_mut(&handle).ok_or(Status::UnknownHandle)?;
102        if e.pending_free {
103            return Err(Status::UnknownHandle);
104        }
105        if e.in_use {
106            e.pending_free = true;
107        } else {
108            self.entries.remove(&handle);
109        }
110        Ok(())
111    }
112
113    pub(crate) fn available(&self, handle: u32) -> bool {
114        self.entries.get(&handle).is_some_and(|e| !e.in_use && !e.pending_free)
115    }
116
117    pub(crate) fn cursor(&self, handle: u32, replay: &[u32]) -> Option<Cursor> {
118        let e = self.entries.get(&handle)?;
119        let mut matcher = Matcher::new(Ok(e.proto.deep_clone()));
120        for &t in replay {
121            matcher.consume_token(t).ok()?;
122        }
123        Some(Cursor(matcher))
124    }
125
126    pub(crate) fn borrow(&mut self, handle: u32, cursor: Cursor) -> Option<LaneGrammar> {
127        let eos = Arc::clone(&self.compiler.as_ref()?.eos);
128        let e = self.entries.get_mut(&handle)?;
129        e.in_use = true;
130        Some(LaneGrammar { handle, matcher: cursor.0, eos, open_ended: e.open_ended, next: None })
131    }
132
133    pub(crate) fn release(&mut self, handle: u32) {
134        if let Some(e) = self.entries.get_mut(&handle) {
135            e.in_use = false;
136            if e.pending_free {
137                self.entries.remove(&handle);
138            }
139        }
140    }
141}
142
143pub(crate) struct Cursor(Matcher);
144
145pub(crate) struct LaneGrammar {
146    pub(crate) handle: u32,
147    matcher: Matcher,
148    eos: Arc<Vec<u32>>,
149    open_ended: bool,
150    next: Option<SimpleVob>,
151}
152
153impl LaneGrammar {
154    fn allowed(&mut self) -> Result<SimpleVob, Status> {
155        if let Some(m) = self.next.take() {
156            return Ok(m);
157        }
158        let stopped = self.matcher.is_stopped();
159        let mut m = self.matcher.compute_mask_or_eos().map_err(|_| Status::Fatal)?;
160        if stopped || self.matcher.is_accepting().unwrap_or(false) {
161            for &id in self.eos.iter() {
162                if (id as usize) < m.len() {
163                    m.allow_token(id);
164                }
165            }
166        }
167        Ok(m)
168    }
169
170    pub(crate) fn mask(&mut self, row: &mut [f32]) -> Result<(), Status> {
171        let allowed = self.allowed()?;
172        if allowed.is_zero() {
173            return Err(Status::Fatal);
174        }
175        let n = allowed.len().min(row.len());
176        allowed.iter_unset_entries(|i| {
177            if i < n {
178                row[i] = f32::NEG_INFINITY;
179            }
180        });
181        for v in row.iter_mut().skip(n) {
182            *v = f32::NEG_INFINITY;
183        }
184        Ok(())
185    }
186
187    pub(crate) fn accept(&mut self, token: u32) -> Result<bool, Status> {
188        if self.eos.contains(&token) {
189            return Ok(true);
190        }
191        self.matcher.consume_token(token).map_err(|_| Status::Fatal)?;
192        if self.matcher.is_stopped() && !self.open_ended {
193            return Ok(true);
194        }
195        let next = self.allowed()?;
196        let ending = self.eos.iter().filter(|&&id| (id as usize) < next.len() && next.is_allowed(id)).count();
197        let only_ending = next.num_set() == ending;
198        self.next = Some(next);
199        Ok(only_ending && !self.open_ended)
200    }
201}
202
203pub fn structural_to_lark(tag: &Value, specials: &[(String, u32)], eos: &[u32]) -> Result<String, Status> {
204    let mut t = Translator { specials, eos, ends: Vec::new(), rules: Vec::new() };
205    let format = match tag.get("type").and_then(Value::as_str) {
206        Some("structural_tag") => tag.get("format").ok_or(Status::RejectBadStruct)?,
207        _ => tag,
208    };
209    let start = t.format(format)?;
210    let mut out = format!("start: {start}\n");
211    for (name, body) in &t.rules {
212        out.push_str(&format!("{name}: {body}\n"));
213    }
214    Ok(out)
215}
216
217const WS: &str = r"(/[ \t\r\n]{1,4}/)";
218
219const JSON_WHITESPACE: &str = r"[\x20\x0A\x0D\x09]{1,64}";
220
221const ANNOTATIONS: [&str; 9] =
222    ["title", "default", "description", "examples", "deprecated", "readOnly", "writeOnly", "$comment", "$schema"];
223
224fn native_strict(schema: &mut Value) {
225    let Value::Object(o) = schema else { return };
226    for defs in ["$defs", "definitions"] {
227        if let Some(Value::Object(d)) = o.get_mut(defs) {
228            d.values_mut().for_each(native_strict);
229        }
230    }
231    if ["$ref", "const", "enum"].iter().any(|k| o.contains_key(*k)) {
232        return;
233    }
234    for key in ["anyOf", "oneOf"] {
235        if let Some(Value::Array(branches)) = o.get_mut(key) {
236            branches.iter_mut().for_each(native_strict);
237            return;
238        }
239    }
240    if let Some(Value::Array(branches)) = o.get_mut("allOf") {
241        if let [only] = branches.as_mut_slice() {
242            native_strict(only);
243        }
244        return;
245    }
246    if o.keys().all(|k| k == "type" || ANNOTATIONS.contains(&k.as_str())) {
247        return;
248    }
249    let (object, array) = {
250        let types: Vec<&str> = match o.get("type") {
251            Some(Value::String(t)) => vec![t.as_str()],
252            Some(Value::Array(ts)) => ts.iter().filter_map(Value::as_str).collect(),
253            _ => Vec::new(),
254        };
255        let untyped = !o.contains_key("type");
256        let has = |keys: &[&str]| keys.iter().any(|k| o.contains_key(*k));
257        let object = types.contains(&"object")
258            || (untyped && has(&["properties", "additionalProperties", "unevaluatedProperties"]));
259        let array = types.contains(&"array") || (untyped && !object && has(&["items", "prefixItems", "unevaluatedItems"]));
260        (object, array)
261    };
262    if object {
263        for key in ["properties", "patternProperties"] {
264            if let Some(Value::Object(props)) = o.get_mut(key) {
265                props.values_mut().for_each(native_strict);
266            }
267        }
268        for key in ["additionalProperties", "unevaluatedProperties"] {
269            if let Some(v @ Value::Object(_)) = o.get_mut(key) {
270                native_strict(v);
271            }
272        }
273        if !o.contains_key("additionalProperties") && !o.contains_key("unevaluatedProperties") {
274            o.insert("additionalProperties".to_string(), Value::Bool(false));
275        }
276    }
277    if array {
278        if let Some(Value::Array(prefix)) = o.get_mut("prefixItems") {
279            prefix.iter_mut().for_each(native_strict);
280        }
281        for key in ["items", "unevaluatedItems"] {
282            if let Some(v @ Value::Object(_)) = o.get_mut(key) {
283                native_strict(v);
284            }
285        }
286        if !o.contains_key("items") && !o.contains_key("unevaluatedItems") {
287            o.insert("items".to_string(), Value::Bool(false));
288        }
289    }
290}
291
292fn prepared(mut schema: Value) -> Value {
293    native_strict(&mut schema);
294    bounded_whitespace(schema)
295}
296
297fn bounded_whitespace(mut schema: Value) -> Value {
298    if let Value::Object(o) = &mut schema {
299        let guidance = o.entry("x-guidance").or_insert_with(|| Value::Object(Default::default()));
300        if let Value::Object(g) = guidance {
301            if !g.contains_key("whitespace_pattern") && !g.contains_key("whitespace_flexible") {
302                g.insert("whitespace_pattern".to_string(), Value::String(JSON_WHITESPACE.to_string()));
303            }
304        }
305    }
306    schema
307}
308
309struct Translator<'a> {
310    specials: &'a [(String, u32)],
311    eos: &'a [u32],
312    ends: Vec<u32>,
313    rules: Vec<(String, String)>,
314}
315
316impl Translator<'_> {
317    fn rule(&mut self, stem: &str, body: String) -> String {
318        let name = format!("{stem}_{}", self.rules.len());
319        self.rules.push((name.clone(), body));
320        name
321    }
322
323    fn literal(&self, s: &str) -> String {
324        let mut parts = Vec::new();
325        let mut text = String::new();
326        let mut rest = s;
327        while !rest.is_empty() {
328            if let Some((lit, id)) = self.specials.iter().find(|(lit, _)| rest.starts_with(lit.as_str())) {
329                if !text.is_empty() {
330                    parts.push(serde_json::to_string(&text).expect("a string serializes"));
331                    text.clear();
332                }
333                parts.push(format!("<[{id}]>"));
334                rest = &rest[lit.len()..];
335            } else {
336                let ch = rest.chars().next().expect("non-empty");
337                text.push(ch);
338                rest = &rest[ch.len_utf8()..];
339            }
340        }
341        if !text.is_empty() {
342            parts.push(serde_json::to_string(&text).expect("a string serializes"));
343        }
344        if parts.is_empty() {
345            "\"\"".to_string()
346        } else {
347            parts.join(" ")
348        }
349    }
350
351    fn token_id(&self, v: &Value) -> Result<u32, Status> {
352        match v {
353            Value::Number(n) => n.as_u64().and_then(|n| u32::try_from(n).ok()).ok_or(Status::RejectBadStruct),
354            Value::String(s) => {
355                self.specials.iter().find(|(lit, _)| lit == s).map(|(_, id)| *id).ok_or(Status::Unsupported)
356            }
357            _ => Err(Status::RejectBadStruct),
358        }
359    }
360
361    fn token_of(&self, v: &Value) -> Result<Option<u32>, Status> {
362        match v {
363            Value::Object(o) if o.get("type").and_then(Value::as_str) == Some("token") => {
364                self.token_id(o.get("token").ok_or(Status::RejectBadStruct)?).map(Some)
365            }
366            _ => Ok(None),
367        }
368    }
369
370    fn boundary(&self, v: &Value) -> Result<String, Status> {
371        if let Value::String(s) = v {
372            return Ok(self.literal(s));
373        }
374        let id = self.token_of(v)?.ok_or(Status::RejectBadStruct)?;
375        Ok(format!("<[{id}]>"))
376    }
377
378    fn elements(&mut self, v: &Value) -> Result<Vec<String>, Status> {
379        let els = v.get("elements").and_then(Value::as_array).ok_or(Status::RejectBadStruct)?;
380        els.iter().map(|e| self.format(e)).collect()
381    }
382
383    fn content(&mut self, v: &Value) -> Result<String, Status> {
384        self.format(v.get("content").ok_or(Status::RejectBadStruct)?)
385    }
386
387    fn tag(&mut self, v: &Value) -> Result<String, Status> {
388        let begin = self.boundary(v.get("begin").ok_or(Status::RejectBadStruct)?)?;
389        let end_v = v.get("end").ok_or(Status::RejectBadStruct)?;
390        let end = self.boundary(end_v)?;
391        let end_token = self.token_of(end_v)?;
392        self.ends.extend(end_token);
393        let content = self.content(v);
394        if end_token.is_some() {
395            self.ends.pop();
396        }
397        Ok(self.rule("tag", format!("{begin} {} {end}", content?)))
398    }
399
400    fn format(&mut self, v: &Value) -> Result<String, Status> {
401        let kind = v.get("type").and_then(Value::as_str).ok_or(Status::RejectBadStruct)?;
402        Ok(match kind {
403            "const_string" => self.literal(v.get("value").and_then(Value::as_str).ok_or(Status::RejectBadStruct)?),
404            "json_schema" => {
405                if v.get("style").and_then(Value::as_str).is_some_and(|s| s != "json") {
406                    return Err(Status::Unsupported);
407                }
408                let schema = v.get("json_schema").ok_or(Status::RejectBadStruct)?;
409                let json = self.rule("json", format!("%json {}", prepared(schema.clone())));
410                format!("{WS}? {json} {WS}?")
411            }
412            "regex" => {
413                let p = v.get("pattern").and_then(Value::as_str).ok_or(Status::RejectBadStruct)?;
414                format!("/{}/", p.replace('/', "\\/"))
415            }
416            "sequence" => format!("({})", self.elements(v)?.join(" ")),
417            "or" => format!("({})", self.elements(v)?.join(" | ")),
418            "optional" => format!("({})?", self.content(v)?),
419            "plus" => format!("({})+", self.content(v)?),
420            "star" => format!("({})*", self.content(v)?),
421            "tag" => self.tag(v)?,
422            "triggered_tags" => self.triggered(v)?,
423            "token" => format!("<[{}]>", self.token_id(v.get("token").ok_or(Status::RejectBadStruct)?)?),
424            "any_tokens" => {
425                let mut excluded: Vec<u32> = self.eos.to_vec();
426                excluded.extend(self.ends.last().copied());
427                if let Some(list) = v.get("exclude_tokens") {
428                    for t in list.as_array().ok_or(Status::RejectBadStruct)? {
429                        excluded.push(self.token_id(t)?);
430                    }
431                }
432                excluded.sort_unstable();
433                excluded.dedup();
434                let ids: Vec<String> = excluded.iter().map(u32::to_string).collect();
435                format!("<[^{}]>*", ids.join(","))
436            }
437            _ => return Err(Status::Unsupported),
438        })
439    }
440
441    fn triggered(&mut self, v: &Value) -> Result<String, Status> {
442        let triggers = v.get("triggers").and_then(Value::as_array).ok_or(Status::RejectBadStruct)?;
443        let mut excluded: Vec<u32> = self.eos.to_vec();
444        for trig in triggers {
445            let s = trig.as_str().ok_or(Status::RejectBadStruct)?;
446            let id = self
447                .specials
448                .iter()
449                .find(|(lit, _)| s.starts_with(lit.as_str()))
450                .map(|(_, id)| *id)
451                .ok_or(Status::Unsupported)?;
452            if !excluded.contains(&id) {
453                excluded.push(id);
454            }
455        }
456        let tags = v.get("tags").and_then(Value::as_array).ok_or(Status::RejectBadStruct)?;
457        if tags.is_empty() {
458            return Err(Status::RejectBadStruct);
459        }
460        let calls: Vec<String> = tags.iter().map(|t| self.tag(t)).collect::<Result<_, _>>()?;
461        let call = self.rule("call", calls.join(" | "));
462        let ids: Vec<String> = excluded.iter().map(u32::to_string).collect();
463        let text = self.rule("text", format!("<[^{}]>*", ids.join(",")));
464        let at_least_one = v.get("at_least_one").and_then(Value::as_bool).unwrap_or(false);
465        let stop_after_first = v.get("stop_after_first").and_then(Value::as_bool).unwrap_or(false);
466        Ok(match (at_least_one, stop_after_first) {
467            (false, false) => format!("{text} ({call} {text})*"),
468            (true, false) => format!("{call} ({text} {call})* {text}"),
469            (false, true) => format!("{text} {call}?"),
470            (true, true) => call,
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    fn specials() -> Vec<(String, u32)> {
480        vec![("</tool_call>".to_string(), 101), ("<tool_call>".to_string(), 100)]
481    }
482
483    fn envelope_tag(at_least_one: bool) -> Value {
484        serde_json::json!({"type": "structural_tag", "format": {
485            "type": "triggered_tags",
486            "triggers": ["<tool_call>"],
487            "tags": [{"type": "tag", "begin": "<tool_call>", "content": {"type": "json_schema",
488                "json_schema": {"type": "object", "properties": {"name": {"const": "f"}}}}, "end": "</tool_call>"}],
489            "at_least_one": at_least_one,
490            "stop_after_first": false,
491        }})
492    }
493
494    #[test]
495    fn a_tool_call_tag_becomes_free_text_around_framed_calls() {
496        let lark = structural_to_lark(&envelope_tag(false), &specials(), &[7]).unwrap();
497        assert!(lark.starts_with("start: text_3 (call_2 text_3)*\n"), "{lark}");
498        assert!(
499            lark.contains(&format!("tag_1: <[100]> {WS}? json_0 {WS}? <[101]>")),
500            "the frame is the marker tokens: {lark}"
501        );
502        assert!(lark.contains("text_3: <[^7,100]>*"), "free text excludes the stop set and the trigger: {lark}");
503    }
504
505    #[test]
506    fn a_forced_call_starts_with_the_call() {
507        let lark = structural_to_lark(&envelope_tag(true), &specials(), &[7]).unwrap();
508        assert!(lark.starts_with("start: call_2 (text_3 call_2)* text_3\n"), "{lark}");
509    }
510
511    #[test]
512    fn text_between_markers_stays_text() {
513        let sp = specials();
514        let t = Translator { specials: &sp, eos: &[], ends: Vec::new(), rules: Vec::new() };
515        assert_eq!(t.literal("<tool_call>\n{\"name\": \"f\""), r#"<[100]> "\n{\"name\": \"f\"""#);
516        assert_eq!(t.literal("plain"), "\"plain\"");
517        assert_eq!(t.literal(""), "\"\"");
518    }
519
520    #[test]
521    fn a_trigger_that_is_not_a_marker_is_refused() {
522        let mut tag = envelope_tag(false);
523        tag["format"]["triggers"] = serde_json::json!(["<function="]);
524        assert_eq!(structural_to_lark(&tag, &specials(), &[7]).unwrap_err(), Status::Unsupported);
525    }
526
527    #[test]
528    fn an_xml_style_body_is_refused() {
529        let mut tag = envelope_tag(false);
530        tag["format"]["tags"][0]["content"]["style"] = serde_json::json!("qwen_xml");
531        assert_eq!(structural_to_lark(&tag, &specials(), &[7]).unwrap_err(), Status::Unsupported);
532    }
533
534    #[test]
535    fn markers_named_by_id_and_a_body_up_to_its_end_token() {
536        let tag = serde_json::json!({"type": "structural_tag", "format": {"type": "sequence", "elements": [
537            {"type": "optional", "content": {"type": "sequence", "elements": [
538                {"type": "token", "token": 200}, {"type": "const_string", "value": "analysis"},
539                {"type": "tag", "begin": {"type": "token", "token": 201},
540                 "content": {"type": "any_tokens", "exclude_tokens": [200, 203]},
541                 "end": {"type": "token", "token": 202}},
542                {"type": "token", "token": 203}, {"type": "const_string", "value": "assistant"}]}},
543            {"type": "sequence", "elements": [
544                {"type": "token", "token": 200}, {"type": "const_string", "value": "commentary to=functions.f"},
545                {"type": "token", "token": 201}, {"type": "json_schema", "json_schema": {"type": "object"}}]}]}});
546        let lark = structural_to_lark(&tag, &specials(), &[7]).unwrap();
547        assert!(lark.contains(r#"((<[200]> "analysis" tag_0 <[203]> "assistant"))?"#), "the analysis may be left out: {lark}");
548        assert!(
549            lark.contains("tag_0: <[201]> <[^7,200,202,203]>* <[202]>"),
550            "the body stops at its end token and at the stop set: {lark}"
551        );
552        assert!(lark.contains(r#"(<[200]> "commentary to=functions.f" <[201]> "#), "{lark}");
553
554        let named = serde_json::json!({"type": "token", "token": "<tool_call>"});
555        assert_eq!(structural_to_lark(&named, &specials(), &[7]).unwrap(), "start: <[100]>\n");
556        let unknown = serde_json::json!({"type": "token", "token": "<nope>"});
557        assert_eq!(structural_to_lark(&unknown, &specials(), &[7]).unwrap_err(), Status::Unsupported);
558    }
559
560    #[test]
561    fn composite_bodies_translate() {
562        let tag = serde_json::json!({"type": "triggered_tags", "triggers": ["<tool_call>"], "tags": [{
563            "begin": "<tool_call>call:f{", "end": "}</tool_call>",
564            "content": {"type": "sequence", "elements": [
565                {"type": "const_string", "value": "city:"},
566                {"type": "or", "elements": [{"type": "regex", "pattern": "[^,}]*"}, {"type": "const_string", "value": "x/y"}]},
567            ]}}], "at_least_one": true});
568        let lark = structural_to_lark(&tag, &specials(), &[7]).unwrap();
569        assert!(lark.contains(r#"<[100]> "call:f{" ("city:" (/[^,}]*/ | "x/y")) "}" <[101]>"#), "{lark}");
570    }
571
572    fn strict(v: Value) -> Value {
573        let mut v = v;
574        native_strict(&mut v);
575        v
576    }
577
578    #[test]
579    fn schemas_close_where_xgrammar_closes_them() {
580        use serde_json::json;
581        let cases = [
582            (json!({"type": "object", "properties": {"a": {}}}),
583             json!({"type": "object", "properties": {"a": {}}, "additionalProperties": false})),
584            (json!({"properties": {"a": {}}}), json!({"properties": {"a": {}}, "additionalProperties": false})),
585            (json!({"type": "object", "unevaluatedProperties": true}), json!({"type": "object", "unevaluatedProperties": true})),
586            (json!({"type": "object", "title": "t", "description": "d"}), json!({"type": "object", "title": "t", "description": "d"})),
587            (json!({"type": "object", "$defs": {}}), json!({"type": "object", "$defs": {}, "additionalProperties": false})),
588            (json!({"type": "array", "minItems": 0}), json!({"type": "array", "minItems": 0, "items": false})),
589            (json!({"prefixItems": [{"type": "object", "required": []}]}),
590             json!({"prefixItems": [{"type": "object", "required": [], "additionalProperties": false}], "items": false})),
591            (json!({"type": "array", "items": {"type": "object", "properties": {}}}),
592             json!({"type": "array", "items": {"type": "object", "properties": {}, "additionalProperties": false}})),
593            (json!({"type": ["object", "null"]}), json!({"type": ["object", "null"]})),
594            (json!({"type": ["object", "null"], "required": ["a"]}),
595             json!({"type": ["object", "null"], "required": ["a"], "additionalProperties": false})),
596            (json!({"$ref": "#/$defs/p", "properties": {}}), json!({"$ref": "#/$defs/p", "properties": {}})),
597            (json!({"const": {"a": 1}}), json!({"const": {"a": 1}})),
598            (json!({"enum": [{"a": 1}]}), json!({"enum": [{"a": 1}]})),
599            (json!({"anyOf": [{"type": "object", "required": []}, {"type": "null"}], "properties": {}}),
600             json!({"anyOf": [{"type": "object", "required": [], "additionalProperties": false}, {"type": "null"}], "properties": {}})),
601            (json!({"allOf": [{"type": "object", "required": []}]}),
602             json!({"allOf": [{"type": "object", "required": [], "additionalProperties": false}]})),
603            (json!({"allOf": [{"type": "object", "required": []}, {"required": []}]}),
604             json!({"allOf": [{"type": "object", "required": []}, {"required": []}]})),
605            (json!({"$defs": {"p": {"type": "object", "required": []}}, "$ref": "#/$defs/p"}),
606             json!({"$defs": {"p": {"type": "object", "required": [], "additionalProperties": false}}, "$ref": "#/$defs/p"})),
607            (json!({"type": "object", "additionalProperties": {"type": "object", "required": []}}),
608             json!({"type": "object", "additionalProperties": {"type": "object", "required": [], "additionalProperties": false}})),
609            (json!(true), json!(true)),
610        ];
611        for (input, want) in cases {
612            assert_eq!(strict(input.clone()), want, "{input}");
613        }
614    }
615}