1use std::collections::BTreeMap;
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
30impl SchemaFragment {
31 fn unconstrained() -> Self {
38 SchemaFragment::Dict(BTreeMap::new())
39 }
40
41 fn node_count(&self) -> usize {
45 match self {
46 SchemaFragment::Value(value) => vm_value_node_count(value),
47 SchemaFragment::Ref(_) => 1,
48 SchemaFragment::Apply { args, .. } => {
49 2 + args.iter().map(SchemaFragment::node_count).sum::<usize>()
50 }
51 SchemaFragment::Dict(entries) => {
52 1 + entries
53 .values()
54 .map(SchemaFragment::node_count)
55 .sum::<usize>()
56 }
57 SchemaFragment::List(items) => {
58 1 + items.iter().map(SchemaFragment::node_count).sum::<usize>()
59 }
60 }
61 }
62
63 fn schema_depth(&self) -> usize {
67 match self {
68 SchemaFragment::Value(_) | SchemaFragment::Ref(_) => 1,
69 SchemaFragment::Apply { args, .. } => {
70 1 + args
71 .iter()
72 .map(SchemaFragment::schema_depth)
73 .max()
74 .unwrap_or(0)
75 }
76 SchemaFragment::Dict(entries) => {
77 1 + entries
78 .iter()
79 .map(|(key, value)| match (key.as_str(), value) {
80 ("properties", SchemaFragment::Dict(fields)) => fields
81 .values()
82 .map(SchemaFragment::schema_depth)
83 .max()
84 .unwrap_or(0),
85 ("union" | "all_of", SchemaFragment::List(members)) => members
86 .iter()
87 .map(SchemaFragment::schema_depth)
88 .max()
89 .unwrap_or(0),
90 ("items" | "additional_properties", schema) => schema.schema_depth(),
91 _ => 0,
92 })
93 .max()
94 .unwrap_or(0)
95 }
96 SchemaFragment::List(_) => 1,
97 }
98 }
99
100 fn pruned_schema(&self, depth_limit: usize) -> SchemaFragment {
108 if depth_limit == 0 {
109 return SchemaFragment::Value(VmValue::dict(std::collections::BTreeMap::<
110 String,
111 VmValue,
112 >::new()));
113 }
114 match self {
115 SchemaFragment::Value(_) | SchemaFragment::Ref(_) => self.clone(),
116 SchemaFragment::Apply { name, args } => SchemaFragment::Apply {
117 name: name.clone(),
118 args: args
119 .iter()
120 .map(|arg| arg.pruned_schema(depth_limit - 1))
121 .collect(),
122 },
123 SchemaFragment::Dict(entries) => SchemaFragment::Dict(
124 entries
125 .iter()
126 .map(|(key, value)| {
127 let pruned = match (key.as_str(), value) {
128 ("properties", SchemaFragment::Dict(fields)) => SchemaFragment::Dict(
129 fields
130 .iter()
131 .map(|(field, schema)| {
132 (field.clone(), schema.pruned_schema(depth_limit - 1))
133 })
134 .collect(),
135 ),
136 ("union" | "all_of", SchemaFragment::List(members)) => {
137 SchemaFragment::List(
138 members
139 .iter()
140 .map(|member| member.pruned_schema(depth_limit - 1))
141 .collect(),
142 )
143 }
144 ("items" | "additional_properties", schema) => {
145 schema.pruned_schema(depth_limit - 1)
146 }
147 _ => value.clone(),
148 };
149 (key.clone(), pruned)
150 })
151 .collect(),
152 ),
153 SchemaFragment::List(_) => self.clone(),
154 }
155 }
156
157 fn bounded(self) -> SchemaFragment {
164 if self.node_count() <= SCHEMA_FRAGMENT_NODE_BUDGET {
165 return self;
166 }
167 let mut limit = self.schema_depth();
168 while limit > 1 {
169 limit -= 1;
170 let candidate = self.pruned_schema(limit);
171 if candidate.node_count() <= SCHEMA_FRAGMENT_NODE_BUDGET {
172 return candidate;
173 }
174 }
175 self.pruned_schema(1)
176 }
177}
178
179fn vm_value_node_count(value: &VmValue) -> usize {
181 match value {
182 VmValue::Dict(entries) => 1 + entries.values().map(vm_value_node_count).sum::<usize>(),
183 VmValue::List(items) => 1 + items.iter().map(vm_value_node_count).sum::<usize>(),
184 _ => 1,
185 }
186}
187
188#[derive(Clone)]
189pub(super) enum SchemaFragment {
190 Value(VmValue),
191 Ref(String),
192 Apply {
195 name: String,
196 args: Vec<SchemaFragment>,
197 },
198 Dict(BTreeMap<String, SchemaFragment>),
199 List(Vec<SchemaFragment>),
200}
201
202impl Compiler {
203 pub fn compile_public_type_schema_initializers(
210 program: &[SNode],
211 source_file: Option<String>,
212 ) -> Result<Vec<Chunk>, CompileError> {
213 Self::new().compile_type_schema_initializers(program, source_file, None)
214 }
215
216 pub fn compile_selected_public_type_schema_initializers(
220 program: &[SNode],
221 source_file: Option<String>,
222 selected_names: Option<&std::collections::BTreeSet<String>>,
223 ) -> Result<Vec<Chunk>, CompileError> {
224 Self::new().compile_type_schema_initializers(program, source_file, selected_names)
225 }
226
227 pub fn compile_type_schema_initializers(
230 mut self,
231 program: &[SNode],
232 source_file: Option<String>,
233 selected_names: Option<&std::collections::BTreeSet<String>>,
234 ) -> Result<Vec<Chunk>, CompileError> {
235 let compiler = &mut self;
236 compiler.collect_type_aliases(program);
237 let mut chunks = Vec::new();
238 for sn in program {
239 let Node::TypeDecl {
240 name, is_pub: true, ..
241 } = peel_node(sn)
242 else {
243 continue;
244 };
245 if selected_names.is_some_and(|selected| !selected.contains(name)) {
246 continue;
247 }
248 compiler.chunk = Chunk::new();
249 compiler.string_constants.clear();
250 compiler.chunk.source_file.clone_from(&source_file);
251 if compiler.emit_schema_for_alias(name)
252 || compiler.emit_generic_schema_factory(name, source_file.clone())?
253 {
254 compiler.emit_define_binding(name, false);
255 compiler.chunk.emit(Op::Nil, compiler.line);
256 compiler.chunk.emit(Op::Return, compiler.line);
257 super::ensure_chunk_addressable(
258 &compiler.chunk,
259 &format!("the public type-schema initializer for `{name}`"),
260 compiler.line,
261 )?;
262 chunks.push(std::mem::take(&mut compiler.chunk));
263 }
264 }
265 Ok(chunks)
266 }
267
268 pub(super) fn schema_fragment_for_alias(&self, name: &str) -> Option<SchemaFragment> {
269 self.unbounded_schema_fragment_for_alias(name)
270 .map(SchemaFragment::bounded)
271 }
272
273 fn unbounded_schema_fragment_for_alias(&self, name: &str) -> Option<SchemaFragment> {
274 let alias = self.type_aliases.get(name)?;
275 if !alias.type_params.is_empty() {
276 return None;
277 }
278 match alias.body.as_ref() {
279 Some(body) => self.schema_fragment_for_type(body, &mut Vec::new()),
280 None => Some(SchemaFragment::Ref(name.to_string())),
281 }
282 }
283
284 fn schema_fragment_under_guard(
297 &self,
298 guard_key: &TypeExpr,
299 body: &TypeExpr,
300 visiting: &mut Vec<TypeExpr>,
301 ) -> Option<SchemaFragment> {
302 if visiting.contains(guard_key) {
312 return Some(SchemaFragment::unconstrained());
313 }
314 if visiting.len() >= MAX_SCHEMA_ALIAS_NEST {
319 return None;
320 }
321 visiting.push(guard_key.clone());
322 let fragment = self.schema_fragment_for_type(body, visiting);
323 visiting.pop();
324 fragment
325 }
326
327 fn schema_fragment_for_type(
328 &self,
329 ty: &TypeExpr,
330 visiting: &mut Vec<TypeExpr>,
331 ) -> Option<SchemaFragment> {
332 if let Some(value) = Self::type_expr_to_schema_value(ty) {
333 return Some(SchemaFragment::Value(value));
334 }
335 match ty {
336 TypeExpr::Named(name) => {
337 let alias = self.type_aliases.get(name)?;
338 let Some(body) = alias.body.as_ref() else {
339 return Some(SchemaFragment::Ref(name.clone()));
340 };
341 if !alias.type_params.is_empty() {
342 return None;
343 }
344 self.schema_fragment_under_guard(ty, body, visiting)
345 }
346 TypeExpr::Shape(fields) => self.schema_fragment_for_shape(fields, None, visiting),
347 TypeExpr::OpenShape { fields, rests } => {
348 let mut additional = None;
349 let mut row_fragments = Vec::new();
350 for rest in rests {
351 if let TypeExpr::DictType(key, value) = rest {
352 if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") {
353 additional = Some(value.as_ref());
354 continue;
355 }
356 }
357 if let TypeExpr::Named(name) = rest {
364 let is_bound_alias = self
365 .type_aliases
366 .get(name)
367 .is_some_and(|alias| alias.body.is_some());
368 if !is_bound_alias {
369 continue;
370 }
371 }
372 row_fragments.push(self.schema_fragment_for_type(rest, visiting)?);
373 }
374 let base = self.schema_fragment_for_shape(fields, additional, visiting)?;
375 if row_fragments.is_empty() {
376 Some(base)
377 } else {
378 let mut branches = Vec::with_capacity(row_fragments.len() + 1);
379 branches.push(base);
380 branches.extend(row_fragments);
381 Some(SchemaFragment::Dict(BTreeMap::from([(
382 "all_of".to_string(),
383 SchemaFragment::List(branches),
384 )])))
385 }
386 }
387 TypeExpr::List(inner) => Some(SchemaFragment::Dict(BTreeMap::from([
388 (
389 "type".to_string(),
390 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("list"))),
391 ),
392 (
393 "items".to_string(),
394 self.schema_fragment_for_type(inner, visiting)?,
395 ),
396 ]))),
397 TypeExpr::DictType(key, value) if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") =>
398 {
399 let mut entries = BTreeMap::from([(
400 "type".to_string(),
401 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("dict"))),
402 )]);
403 if let Some(value_fragment) = self.schema_fragment_for_type(value, visiting) {
408 entries.insert("additional_properties".to_string(), value_fragment);
409 }
410 Some(SchemaFragment::Dict(entries))
411 }
412 TypeExpr::Union(members) | TypeExpr::Intersection(members) => {
413 let key = if matches!(ty, TypeExpr::Union(_)) {
414 "union"
415 } else {
416 "all_of"
417 };
418 let branches = members
419 .iter()
420 .map(|member| self.schema_fragment_for_type(member, visiting))
421 .collect::<Option<Vec<_>>>()?;
422 Some(SchemaFragment::Dict(BTreeMap::from([(
423 key.to_string(),
424 SchemaFragment::List(branches),
425 )])))
426 }
427 TypeExpr::Applied { name, args }
431 if name == "Schema" && args.len() == 1 && !self.type_aliases.contains_key(name) =>
432 {
433 Self::type_expr_to_schema_value(&TypeExpr::Named("dict".to_string()))
434 .map(SchemaFragment::Value)
435 }
436 TypeExpr::Applied { name, args } => {
437 let alias = self.type_aliases.get(name)?;
438 let Some(body) = alias.body.as_ref() else {
439 if args.len() > u8::MAX as usize {
440 return None;
441 }
442 return Some(SchemaFragment::Apply {
443 name: name.clone(),
444 args: args
445 .iter()
446 .map(|arg| self.schema_fragment_for_type(arg, visiting))
447 .collect::<Option<Vec<_>>>()?,
448 });
449 };
450 if alias.type_params.len() != args.len() {
451 return None;
452 }
453 let bindings = alias
454 .type_params
455 .iter()
456 .zip(args)
457 .map(|(param, arg)| (param.name.clone(), arg.clone()))
458 .collect();
459 let instantiated = substitute_type_expr(body, &bindings);
460 let guard_key = TypeExpr::Named(name.clone());
464 self.schema_fragment_under_guard(&guard_key, &instantiated, visiting)
465 }
466 TypeExpr::Owned(inner) => self.schema_fragment_for_type(inner, visiting),
467 _ => None,
468 }
469 }
470
471 fn schema_fragment_for_shape(
472 &self,
473 fields: &[ShapeField],
474 additional: Option<&TypeExpr>,
475 visiting: &mut Vec<TypeExpr>,
476 ) -> Option<SchemaFragment> {
477 let mut properties = BTreeMap::new();
478 let mut required = Vec::new();
479 for field in fields {
480 let mut field_schema = self.schema_fragment_for_type(&field.type_expr, visiting)?;
481 if field.optional {
482 field_schema = SchemaFragment::Dict(BTreeMap::from([(
483 "union".to_string(),
484 SchemaFragment::List(vec![
485 field_schema,
486 SchemaFragment::Value(VmValue::dict(BTreeMap::from([(
487 "type".to_string(),
488 VmValue::String(arcstr::ArcStr::from("nil")),
489 )]))),
490 ]),
491 )]));
492 } else {
493 required.push(SchemaFragment::Value(VmValue::String(
494 arcstr::ArcStr::from(field.name.as_str()),
495 )));
496 }
497 properties.insert(field.name.clone(), field_schema);
498 }
499 let mut schema = BTreeMap::from([
500 (
501 "type".to_string(),
502 SchemaFragment::Value(VmValue::String(arcstr::ArcStr::from("dict"))),
503 ),
504 ("properties".to_string(), SchemaFragment::Dict(properties)),
505 ]);
506 if !required.is_empty() {
507 schema.insert("required".to_string(), SchemaFragment::List(required));
508 }
509 if let Some(additional) = additional {
510 schema.insert(
511 "additional_properties".to_string(),
512 self.schema_fragment_for_type(additional, visiting)?,
513 );
514 }
515 Some(SchemaFragment::Dict(schema))
516 }
517
518 pub(super) fn emit_schema_fragment(&mut self, fragment: &SchemaFragment) {
519 match fragment {
520 SchemaFragment::Value(value) => self.emit_vm_value_literal(value),
521 SchemaFragment::Ref(name) => self.emit_get_binding(name),
522 SchemaFragment::Apply { name, args } => {
523 self.emit_get_binding(name);
524 for arg in args {
525 self.emit_schema_fragment(arg);
526 }
527 self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
528 }
529 SchemaFragment::Dict(entries) => {
530 for (key, value) in entries {
531 let key_idx = self.string_constant(key);
532 self.chunk.emit_u16(Op::Constant, key_idx, self.line);
533 self.emit_schema_fragment(value);
534 }
535 self.chunk
536 .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
537 }
538 SchemaFragment::List(items) => {
539 for item in items {
540 self.emit_schema_fragment(item);
541 }
542 self.chunk
543 .emit_u16(Op::BuildList, items.len() as u16, self.line);
544 }
545 }
546 }
547
548 pub(super) fn emit_schema_for_alias(&mut self, name: &str) -> bool {
549 let Some(fragment) = self.schema_fragment_for_alias(name) else {
550 return false;
551 };
552 self.emit_schema_fragment(&fragment);
553 true
554 }
555
556 pub(super) fn emit_generic_schema_factory(
560 &mut self,
561 name: &str,
562 source_file: Option<String>,
563 ) -> Result<bool, super::CompileError> {
564 let Some(alias) = self.type_aliases.get(name).cloned() else {
565 return Ok(false);
566 };
567 let Some(body) = alias.body else {
568 return Ok(false);
569 };
570 if alias.type_params.is_empty() || alias.type_params.len() > u8::MAX as usize {
571 return Ok(false);
572 }
573 let params = alias
574 .type_params
575 .iter()
576 .map(|param| harn_parser::TypedParam::untyped(¶m.name))
577 .collect::<Vec<_>>();
578 let mut compiler = self.nested_body();
579 compiler.type_aliases = self.type_aliases.clone();
580 for param in ¶ms {
581 compiler.type_aliases.insert(
582 param.name.clone(),
583 super::TypeAliasDefinition {
584 type_params: Vec::new(),
585 body: None,
586 },
587 );
588 }
589 compiler.declare_param_slots(¶ms);
590 let Some(fragment) =
591 compiler.schema_fragment_for_type(&body, &mut vec![TypeExpr::Named(name.to_string())])
592 else {
593 return Ok(false);
594 };
595 compiler.emit_schema_fragment(&fragment.bounded());
596 compiler.chunk.emit(Op::Return, self.line);
597 compiler.chunk.source_file = source_file;
598 super::ensure_chunk_addressable(
599 &compiler.chunk,
600 &format!("generic schema `{name}`"),
601 self.line,
602 )?;
603 let function = crate::CompiledFunction {
604 name: format!("<schema:{name}>"),
605 type_params: Vec::new(),
606 nominal_type_names: Vec::new(),
607 params: compiler.compile_param_slots(¶ms),
608 default_start: None,
609 chunk: std::sync::Arc::new(compiler.chunk),
610 is_generator: false,
611 is_stream: false,
612 has_rest_param: false,
613 has_runtime_type_checks: false,
614 };
615 let index = self.chunk.functions.len();
616 self.chunk.functions.push(std::sync::Arc::new(function));
617 self.chunk.emit_u16(Op::Closure, index as u16, self.line);
618 Ok(true)
619 }
620}