1use std::collections::{BTreeMap, BTreeSet};
2
3use harn_parser::{substitute_type_expr, Node, SNode, ShapeField, TypeExpr};
4
5use crate::chunk::{Chunk, Op};
6use crate::value::VmValue;
7
8use super::{peel_node, CompileError, Compiler};
9
10const MAX_SCHEMA_ALIAS_NEST: usize = 128;
17
18const SCHEMA_FRAGMENT_NODE_BUDGET: usize = 4096;
29
30#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
32pub struct TypeSchemaInitializer<C> {
33 pub name: String,
34 pub required_bindings: Vec<String>,
35 pub chunk: C,
36}
37
38impl<C> TypeSchemaInitializer<C> {
39 pub fn map_chunk<T>(self, map: impl FnOnce(C) -> T) -> TypeSchemaInitializer<T> {
40 TypeSchemaInitializer {
41 name: self.name,
42 required_bindings: self.required_bindings,
43 chunk: map(self.chunk),
44 }
45 }
46}
47
48impl SchemaFragment {
49 fn required_bindings(&self) -> BTreeSet<String> {
50 match self {
51 Self::Value(_) => BTreeSet::new(),
52 Self::Ref(name) => BTreeSet::from([name.clone()]),
53 Self::Apply { name, args } => std::iter::once(name.clone())
54 .chain(args.iter().flat_map(Self::required_bindings))
55 .collect(),
56 Self::Dict(entries) => entries.values().flat_map(Self::required_bindings).collect(),
57 Self::List(items) => items.iter().flat_map(Self::required_bindings).collect(),
58 }
59 }
60
61 fn unconstrained() -> Self {
68 SchemaFragment::Dict(BTreeMap::new())
69 }
70
71 fn node_count(&self) -> usize {
75 match self {
76 SchemaFragment::Value(value) => vm_value_node_count(value),
77 SchemaFragment::Ref(_) => 1,
78 SchemaFragment::Apply { args, .. } => {
79 2 + args.iter().map(SchemaFragment::node_count).sum::<usize>()
80 }
81 SchemaFragment::Dict(entries) => {
82 1 + entries
83 .values()
84 .map(SchemaFragment::node_count)
85 .sum::<usize>()
86 }
87 SchemaFragment::List(items) => {
88 1 + items.iter().map(SchemaFragment::node_count).sum::<usize>()
89 }
90 }
91 }
92
93 fn schema_depth(&self) -> usize {
97 match self {
98 SchemaFragment::Value(_) | SchemaFragment::Ref(_) => 1,
99 SchemaFragment::Apply { args, .. } => {
100 1 + args
101 .iter()
102 .map(SchemaFragment::schema_depth)
103 .max()
104 .unwrap_or(0)
105 }
106 SchemaFragment::Dict(entries) => {
107 1 + entries
108 .iter()
109 .map(|(key, value)| match (key.as_str(), value) {
110 ("properties", SchemaFragment::Dict(fields)) => fields
111 .values()
112 .map(SchemaFragment::schema_depth)
113 .max()
114 .unwrap_or(0),
115 ("union" | "all_of", SchemaFragment::List(members)) => members
116 .iter()
117 .map(SchemaFragment::schema_depth)
118 .max()
119 .unwrap_or(0),
120 ("items" | "additional_properties", schema) => schema.schema_depth(),
121 _ => 0,
122 })
123 .max()
124 .unwrap_or(0)
125 }
126 SchemaFragment::List(_) => 1,
127 }
128 }
129
130 fn pruned_schema(&self, depth_limit: usize) -> SchemaFragment {
138 if depth_limit == 0 {
139 return SchemaFragment::Value(VmValue::dict(std::collections::BTreeMap::<
140 String,
141 VmValue,
142 >::new()));
143 }
144 match self {
145 SchemaFragment::Value(_) | SchemaFragment::Ref(_) => self.clone(),
146 SchemaFragment::Apply { name, args } => SchemaFragment::Apply {
147 name: name.clone(),
148 args: args
149 .iter()
150 .map(|arg| arg.pruned_schema(depth_limit - 1))
151 .collect(),
152 },
153 SchemaFragment::Dict(entries) => SchemaFragment::Dict(
154 entries
155 .iter()
156 .map(|(key, value)| {
157 let pruned = match (key.as_str(), value) {
158 ("properties", SchemaFragment::Dict(fields)) => SchemaFragment::Dict(
159 fields
160 .iter()
161 .map(|(field, schema)| {
162 (field.clone(), schema.pruned_schema(depth_limit - 1))
163 })
164 .collect(),
165 ),
166 ("union" | "all_of", SchemaFragment::List(members)) => {
167 SchemaFragment::List(
168 members
169 .iter()
170 .map(|member| member.pruned_schema(depth_limit - 1))
171 .collect(),
172 )
173 }
174 ("items" | "additional_properties", schema) => {
175 schema.pruned_schema(depth_limit - 1)
176 }
177 _ => value.clone(),
178 };
179 (key.clone(), pruned)
180 })
181 .collect(),
182 ),
183 SchemaFragment::List(_) => self.clone(),
184 }
185 }
186
187 fn bounded(self) -> SchemaFragment {
194 if self.node_count() <= SCHEMA_FRAGMENT_NODE_BUDGET {
195 return self;
196 }
197 let mut limit = self.schema_depth();
198 while limit > 1 {
199 limit -= 1;
200 let candidate = self.pruned_schema(limit);
201 if candidate.node_count() <= SCHEMA_FRAGMENT_NODE_BUDGET {
202 return candidate;
203 }
204 }
205 self.pruned_schema(1)
206 }
207}
208
209fn vm_value_node_count(value: &VmValue) -> usize {
211 match value {
212 VmValue::Dict(entries) => 1 + entries.values().map(vm_value_node_count).sum::<usize>(),
213 VmValue::List(items) => 1 + items.iter().map(vm_value_node_count).sum::<usize>(),
214 _ => 1,
215 }
216}
217
218#[derive(Clone)]
219pub(super) enum SchemaFragment {
220 Value(VmValue),
221 Ref(String),
222 Apply {
225 name: String,
226 args: Vec<SchemaFragment>,
227 },
228 Dict(BTreeMap<String, SchemaFragment>),
229 List(Vec<SchemaFragment>),
230}
231
232impl Compiler {
233 pub fn compile_public_type_schema_initializers(
240 program: &[SNode],
241 source_file: Option<String>,
242 ) -> Result<Vec<TypeSchemaInitializer<Chunk>>, CompileError> {
243 Self::new().compile_type_schema_initializers(program, source_file, None)
244 }
245
246 pub fn compile_selected_public_type_schema_initializers(
250 program: &[SNode],
251 source_file: Option<String>,
252 selected_names: Option<&std::collections::BTreeSet<String>>,
253 ) -> Result<Vec<TypeSchemaInitializer<Chunk>>, CompileError> {
254 Self::new().compile_type_schema_initializers(program, source_file, selected_names)
255 }
256
257 pub fn compile_type_schema_initializers(
260 mut self,
261 program: &[SNode],
262 source_file: Option<String>,
263 selected_names: Option<&std::collections::BTreeSet<String>>,
264 ) -> Result<Vec<TypeSchemaInitializer<Chunk>>, CompileError> {
265 let compiler = &mut self;
266 compiler.collect_type_aliases(program);
267 let mut chunks = Vec::new();
268 for sn in program {
269 let Node::TypeDecl {
270 name, is_pub: true, ..
271 } = peel_node(sn)
272 else {
273 continue;
274 };
275 if selected_names.is_some_and(|selected| !selected.contains(name)) {
276 continue;
277 }
278 compiler.chunk = Chunk::new();
279 compiler.string_constants.clear();
280 compiler.chunk.source_file.clone_from(&source_file);
281 let required = if let Some(fragment) = compiler.schema_fragment_for_alias(name) {
282 let required = fragment.required_bindings().into_iter().collect();
283 compiler.emit_schema_fragment(&fragment);
284 Some(required)
285 } else {
286 compiler.emit_generic_schema_factory(name, source_file.clone())?
287 };
288 if let Some(required_bindings) = required {
289 compiler.emit_define_binding(name, false);
290 compiler.chunk.emit(Op::Nil, compiler.line);
291 compiler.chunk.emit(Op::Return, compiler.line);
292 super::ensure_chunk_addressable(
293 &compiler.chunk,
294 &format!("the public type-schema initializer for `{name}`"),
295 compiler.line,
296 )?;
297 chunks.push(TypeSchemaInitializer {
298 name: name.clone(),
299 required_bindings,
300 chunk: std::mem::take(&mut compiler.chunk),
301 });
302 }
303 }
304 Ok(chunks)
305 }
306
307 pub(super) fn schema_fragment_for_alias(&self, name: &str) -> Option<SchemaFragment> {
308 self.unbounded_schema_fragment_for_alias(name)
309 .map(SchemaFragment::bounded)
310 }
311
312 fn unbounded_schema_fragment_for_alias(&self, name: &str) -> Option<SchemaFragment> {
313 let alias = self.type_aliases.get(name)?;
314 if !alias.type_params.is_empty() {
315 return None;
316 }
317 match alias.body.as_ref() {
318 Some(body) => self.schema_fragment_for_type(body, &mut Vec::new()),
319 None => Some(SchemaFragment::Ref(name.to_string())),
320 }
321 }
322
323 fn schema_fragment_under_guard(
336 &self,
337 guard_key: &TypeExpr,
338 body: &TypeExpr,
339 visiting: &mut Vec<TypeExpr>,
340 ) -> Option<SchemaFragment> {
341 if visiting.contains(guard_key) {
351 return Some(SchemaFragment::unconstrained());
352 }
353 if visiting.len() >= MAX_SCHEMA_ALIAS_NEST {
358 return None;
359 }
360 visiting.push(guard_key.clone());
361 let fragment = self.schema_fragment_for_type(body, visiting);
362 visiting.pop();
363 fragment
364 }
365
366 fn schema_fragment_for_type(
367 &self,
368 ty: &TypeExpr,
369 visiting: &mut Vec<TypeExpr>,
370 ) -> Option<SchemaFragment> {
371 if let Some(value) = Self::type_expr_to_schema_value(ty) {
372 return Some(SchemaFragment::Value(value));
373 }
374 match ty {
375 TypeExpr::Named(name) => {
376 let alias = self.type_aliases.get(name)?;
377 let Some(body) = alias.body.as_ref() else {
378 return Some(SchemaFragment::Ref(name.clone()));
379 };
380 if !alias.type_params.is_empty() {
381 return None;
382 }
383 self.schema_fragment_under_guard(ty, body, visiting)
384 }
385 TypeExpr::Shape(fields) => self.schema_fragment_for_shape(fields, None, visiting),
386 TypeExpr::OpenShape { fields, rests } => {
387 let mut additional = None;
388 let mut row_fragments = Vec::new();
389 for rest in rests {
390 if let TypeExpr::DictType(key, value) = rest {
391 if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") {
392 additional = Some(value.as_ref());
393 continue;
394 }
395 }
396 if let TypeExpr::Named(name) = rest {
403 let is_bound_alias = self
404 .type_aliases
405 .get(name)
406 .is_some_and(|alias| alias.body.is_some());
407 if !is_bound_alias {
408 continue;
409 }
410 }
411 row_fragments.push(self.schema_fragment_for_type(rest, visiting)?);
412 }
413 let base = self.schema_fragment_for_shape(fields, additional, visiting)?;
414 if row_fragments.is_empty() {
415 Some(base)
416 } else {
417 let mut branches = Vec::with_capacity(row_fragments.len() + 1);
418 branches.push(base);
419 branches.extend(row_fragments);
420 Some(SchemaFragment::Dict(BTreeMap::from([(
421 "all_of".to_string(),
422 SchemaFragment::List(branches),
423 )])))
424 }
425 }
426 TypeExpr::List(inner) => Some(SchemaFragment::Dict(BTreeMap::from([
427 (
428 "type".to_string(),
429 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("list"))),
430 ),
431 (
432 "items".to_string(),
433 self.schema_fragment_for_type(inner, visiting)?,
434 ),
435 ]))),
436 TypeExpr::DictType(key, value) if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") =>
437 {
438 let mut entries = BTreeMap::from([(
439 "type".to_string(),
440 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("dict"))),
441 )]);
442 if let Some(value_fragment) = self.schema_fragment_for_type(value, visiting) {
447 entries.insert("additional_properties".to_string(), value_fragment);
448 }
449 Some(SchemaFragment::Dict(entries))
450 }
451 TypeExpr::Union(members) | TypeExpr::Intersection(members) => {
452 let key = if matches!(ty, TypeExpr::Union(_)) {
453 "union"
454 } else {
455 "all_of"
456 };
457 let branches = members
458 .iter()
459 .map(|member| self.schema_fragment_for_type(member, visiting))
460 .collect::<Option<Vec<_>>>()?;
461 Some(SchemaFragment::Dict(BTreeMap::from([(
462 key.to_string(),
463 SchemaFragment::List(branches),
464 )])))
465 }
466 TypeExpr::Applied { name, args }
470 if name == "Schema" && args.len() == 1 && !self.type_aliases.contains_key(name) =>
471 {
472 Self::type_expr_to_schema_value(&TypeExpr::Named("dict".to_string()))
473 .map(SchemaFragment::Value)
474 }
475 TypeExpr::Applied { name, args } => {
476 let alias = self.type_aliases.get(name)?;
477 let Some(body) = alias.body.as_ref() else {
478 if args.len() > u8::MAX as usize {
479 return None;
480 }
481 return Some(SchemaFragment::Apply {
482 name: name.clone(),
483 args: args
484 .iter()
485 .map(|arg| self.schema_fragment_for_type(arg, visiting))
486 .collect::<Option<Vec<_>>>()?,
487 });
488 };
489 if alias.type_params.len() != args.len() {
490 return None;
491 }
492 let bindings = alias
493 .type_params
494 .iter()
495 .zip(args)
496 .map(|(param, arg)| (param.name.clone(), arg.clone()))
497 .collect();
498 let instantiated = substitute_type_expr(body, &bindings);
499 let guard_key = TypeExpr::Named(name.clone());
503 self.schema_fragment_under_guard(&guard_key, &instantiated, visiting)
504 }
505 TypeExpr::Owned(inner) => self.schema_fragment_for_type(inner, visiting),
506 _ => None,
507 }
508 }
509
510 fn schema_fragment_for_shape(
511 &self,
512 fields: &[ShapeField],
513 additional: Option<&TypeExpr>,
514 visiting: &mut Vec<TypeExpr>,
515 ) -> Option<SchemaFragment> {
516 let mut properties = BTreeMap::new();
517 let mut required = Vec::new();
518 for field in fields {
519 let mut field_schema = self.schema_fragment_for_type(&field.type_expr, visiting)?;
520 if field.optional {
521 field_schema = SchemaFragment::Dict(BTreeMap::from([(
522 "union".to_string(),
523 SchemaFragment::List(vec![
524 field_schema,
525 SchemaFragment::Value(VmValue::dict(BTreeMap::from([(
526 "type".to_string(),
527 VmValue::String(arcstr::ArcStr::from("nil")),
528 )]))),
529 ]),
530 )]));
531 } else {
532 required.push(SchemaFragment::Value(VmValue::String(
533 arcstr::ArcStr::from(field.name.as_str()),
534 )));
535 }
536 properties.insert(field.name.clone(), field_schema);
537 }
538 let mut schema = BTreeMap::from([
539 (
540 "type".to_string(),
541 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("dict"))),
542 ),
543 ("properties".to_string(), SchemaFragment::Dict(properties)),
544 ]);
545 if !required.is_empty() {
546 schema.insert("required".to_string(), SchemaFragment::List(required));
547 }
548 if let Some(additional) = additional {
549 schema.insert(
550 "additional_properties".to_string(),
551 self.schema_fragment_for_type(additional, visiting)?,
552 );
553 }
554 Some(SchemaFragment::Dict(schema))
555 }
556
557 pub(super) fn emit_schema_fragment(&mut self, fragment: &SchemaFragment) {
558 match fragment {
559 SchemaFragment::Value(value) => self.emit_vm_value_literal(value),
560 SchemaFragment::Ref(name) => self.emit_get_binding(name),
561 SchemaFragment::Apply { name, args } => {
562 self.emit_get_binding(name);
563 for arg in args {
564 self.emit_schema_fragment(arg);
565 }
566 self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
567 }
568 SchemaFragment::Dict(entries) => {
569 for (key, value) in entries {
570 let key_idx = self.string_constant(key);
571 self.chunk.emit_u16(Op::Constant, key_idx, self.line);
572 self.emit_schema_fragment(value);
573 }
574 self.chunk
575 .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
576 }
577 SchemaFragment::List(items) => {
578 for item in items {
579 self.emit_schema_fragment(item);
580 }
581 self.chunk
582 .emit_u16(Op::BuildList, items.len() as u16, self.line);
583 }
584 }
585 }
586
587 pub(super) fn emit_schema_for_alias(&mut self, name: &str) -> bool {
588 let Some(fragment) = self.schema_fragment_for_alias(name) else {
589 return false;
590 };
591 self.emit_schema_fragment(&fragment);
592 true
593 }
594
595 pub(super) fn emit_generic_schema_factory(
599 &mut self,
600 name: &str,
601 source_file: Option<String>,
602 ) -> Result<Option<Vec<String>>, super::CompileError> {
603 let Some(alias) = self.type_aliases.get(name).cloned() else {
604 return Ok(None);
605 };
606 let Some(body) = alias.body else {
607 return Ok(None);
608 };
609 if alias.type_params.is_empty() || alias.type_params.len() > u8::MAX as usize {
610 return Ok(None);
611 }
612 let params = alias
613 .type_params
614 .iter()
615 .map(|param| harn_parser::TypedParam::untyped(¶m.name))
616 .collect::<Vec<_>>();
617 let mut compiler = self.nested_body();
618 compiler.type_aliases = self.type_aliases.clone();
619 for param in ¶ms {
620 compiler.type_aliases.insert(
621 param.name.clone(),
622 super::TypeAliasDefinition {
623 type_params: Vec::new(),
624 body: None,
625 },
626 );
627 }
628 compiler.declare_param_slots(¶ms);
629 let Some(fragment) =
630 compiler.schema_fragment_for_type(&body, &mut vec![TypeExpr::Named(name.to_string())])
631 else {
632 return Ok(None);
633 };
634 let fragment = fragment.bounded();
635 let required_bindings = fragment
636 .required_bindings()
637 .into_iter()
638 .filter(|name| !params.iter().any(|param| param.name == *name))
639 .collect();
640 compiler.emit_schema_fragment(&fragment);
641 compiler.chunk.emit(Op::Return, self.line);
642 compiler.chunk.source_file = source_file;
643 super::ensure_chunk_addressable(
644 &compiler.chunk,
645 &format!("generic schema `{name}`"),
646 self.line,
647 )?;
648 let function = crate::CompiledFunction {
649 name: format!("<schema:{name}>"),
650 type_params: Vec::new(),
651 nominal_type_names: Vec::new(),
652 params: compiler.compile_param_slots(¶ms),
653 default_start: None,
654 chunk: std::sync::Arc::new(compiler.chunk),
655 is_generator: false,
656 is_stream: false,
657 has_rest_param: false,
658 has_runtime_type_checks: false,
659 };
660 let index = self.chunk.functions.len();
661 self.chunk.functions.push(std::sync::Arc::new(function));
662 self.chunk.emit_u16(Op::Closure, index as u16, self.line);
663 Ok(Some(required_bindings))
664 }
665}