emit.odin (18293B)
1 package jschema 2 3 import "core:strings" 4 5 @(private) 6 Kind :: enum u8 { 7 Any, // no usable constraints -> json.Value 8 Boolean, 9 Integer, 10 Number, 11 Str, 12 Struct, 13 Enum, 14 Union, // oneOf / anyOf 15 Array, 16 Map, // object keyed by arbitrary strings 17 Multi, // several simple types -> inline union 18 } 19 20 @(private) 21 Visit_State :: enum u8 { 22 Unvisited, 23 On_Stack, 24 Done, 25 } 26 27 @(private) 28 Member :: struct { 29 prop: u32, // global index into Pool.props 30 name: string, // original JSON name 31 node: Node_Index, 32 } 33 34 @(private) 35 Emitter :: struct { 36 pool: ^Pool, 37 opts: Options, 38 openapi: bool, // OpenAPI-idiom lowering enabled 39 def_names: map[Node_Index]string, // raw def key per node 40 names: []string, // assigned declaration name per node 41 visit: []Visit_State, 42 queued: []bool, 43 queue: [dynamic]Node_Index, 44 broken: map[u32]bool, // props that would form a by-value cycle 45 taken: map[string]bool, 46 body: strings.Builder, 47 uses_json: bool, 48 uses_or_reference: bool, 49 } 50 51 // Follows chains of nodes that are nothing but a $ref. 52 @(private) 53 effective :: proc(e: ^Emitter, idx: Node_Index) -> Node_Index { 54 idx := idx 55 for _ in 0 ..< len(e.pool.nodes) { 56 node := &e.pool.nodes[idx] 57 if node.ref == NIL_NODE { 58 break 59 } 60 pure := node.props.count == 0 && 61 node.all_of.count == 0 && 62 node.any_of.count == 0 && 63 node.one_of.count == 0 && 64 node.enum_kind == .None && 65 node.types == {} && 66 node.items == NIL_NODE && 67 node.additional == NIL_NODE 68 if !pure { 69 break 70 } 71 idx = node.ref 72 } 73 return idx 74 } 75 76 @(private) 77 is_null_only :: proc(e: ^Emitter, idx: Node_Index) -> bool { 78 node := &e.pool.nodes[idx] 79 return node.types == {.Null} && 80 node.props.count == 0 && 81 node.all_of.count == 0 && 82 node.any_of.count == 0 && 83 node.one_of.count == 0 && 84 node.enum_kind == .None 85 } 86 87 @(private) 88 classify :: proc(e: ^Emitter, idx: Node_Index) -> Kind { 89 node := &e.pool.nodes[idx] 90 if node.props.count > 0 || node.all_of.count > 0 { 91 return .Struct 92 } 93 if node.enum_kind == .Strings { 94 all_idents := node.enum_values.count > 0 95 for value in enum_values(e, idx) { 96 if !is_valid_ident(value) { 97 all_idents = false 98 break 99 } 100 } 101 if all_idents { 102 return .Enum 103 } 104 return .Str 105 } 106 if node.one_of.count > 0 || node.any_of.count > 0 { 107 return .Union 108 } 109 110 types := node.types - {.Null} 111 switch card(types) { 112 case 0: 113 if node.additional != NIL_NODE || node.additional_bool != .Unset { 114 return .Map 115 } 116 if node.items != NIL_NODE { 117 return .Array 118 } 119 return .Any 120 case 1: 121 switch { 122 case .String in types: 123 return .Str 124 case .Integer in types: 125 return .Integer 126 case .Number in types: 127 return .Number 128 case .Boolean in types: 129 return .Boolean 130 case .Array in types: 131 return .Array 132 } 133 return .Map // .Object 134 } 135 return .Multi 136 } 137 138 @(private) 139 enum_values :: proc(e: ^Emitter, idx: Node_Index) -> []string { 140 r := e.pool.nodes[idx].enum_values 141 return e.pool.strings[r.first:][:r.count] 142 } 143 144 @(private) 145 is_reference_def :: proc(e: ^Emitter, idx: Node_Index) -> bool { 146 name, ok := e.def_names[idx] 147 return ok && name == "reference" 148 } 149 150 // A 2-variant union shaped like OpenAPI's "X-or-reference" idiom: one 151 // branch resolves to the "reference" $def, the other to a concrete type. 152 // Lowered to the generic Or_Reference($T) instead of a dedicated decl. 153 // Only applies when the non-reference branch is itself a named decl 154 // (struct/enum/union); inline-only kinds (map, array, etc.) cannot be a 155 // polymorphic type argument and fall back to a dedicated union decl. 156 @(private) 157 is_or_reference :: proc(e: ^Emitter, idx: Node_Index) -> (ref: Node_Index, other: Node_Index, ok: bool) { 158 if !e.openapi { 159 return NIL_NODE, NIL_NODE, false 160 } 161 variants := union_variants(e, idx) 162 if len(variants) != 2 { 163 return NIL_NODE, NIL_NODE, false 164 } 165 a := effective(e, variants[0]) 166 b := effective(e, variants[1]) 167 a_ref := is_reference_def(e, a) 168 b_ref := is_reference_def(e, b) 169 if a_ref && !b_ref { 170 #partial switch classify(e, b) { 171 case .Struct, .Enum, .Union: 172 return a, b, true 173 } 174 } 175 if b_ref && !a_ref { 176 #partial switch classify(e, a) { 177 case .Struct, .Enum, .Union: 178 return b, a, true 179 } 180 } 181 return NIL_NODE, NIL_NODE, false 182 } 183 184 @(private) 185 union_variants :: proc(e: ^Emitter, idx: Node_Index) -> []Node_Index { 186 node := &e.pool.nodes[idx] 187 r := node.one_of if node.one_of.count > 0 else node.any_of 188 return e.pool.children[r.first:][:r.count] 189 } 190 191 // Collects the struct members of a node, merging $ref targets and allOf 192 // children depth-first. The first occurrence of a property name wins. 193 @(private) 194 collect_members :: proc( 195 e: ^Emitter, 196 idx: Node_Index, 197 members: ^[dynamic]Member, 198 required: ^map[string]bool, 199 ) { 200 seen := make(map[Node_Index]bool, context.temp_allocator) 201 have := make(map[string]bool, context.temp_allocator) 202 203 gather :: proc( 204 e: ^Emitter, 205 idx: Node_Index, 206 members: ^[dynamic]Member, 207 required: ^map[string]bool, 208 seen: ^map[Node_Index]bool, 209 have: ^map[string]bool, 210 ) { 211 if seen[idx] { 212 return 213 } 214 seen[idx] = true 215 node := e.pool.nodes[idx] 216 if node.ref != NIL_NODE { 217 gather(e, node.ref, members, required, seen, have) 218 } 219 for child in e.pool.children[node.all_of.first:][:node.all_of.count] { 220 gather(e, child, members, required, seen, have) 221 } 222 for name in e.pool.names[node.required.first:][:node.required.count] { 223 required[name] = true 224 } 225 for i in 0 ..< node.props.count { 226 prop_index := node.props.first + i 227 prop := e.pool.props[prop_index] 228 if have[prop.name] { 229 continue 230 } 231 have[prop.name] = true 232 append(members, Member{prop = prop_index, name = prop.name, node = prop.node}) 233 } 234 } 235 gather(e, idx, members, required, &seen, &have) 236 } 237 238 // --- by-value cycle detection ------------------------------------------------ 239 // 240 // Odin structs cannot contain themselves by value (including through Maybe or 241 // a union), so any property whose type chain reaches a struct that is already 242 // being laid out is emitted as json.Value instead. 243 244 @(private) 245 find_cycles :: proc(e: ^Emitter, idx: Node_Index) { 246 visit_indirect(e, idx) 247 } 248 249 // Follows a property's type by value; reports whether it hit a struct that is 250 // currently on the layout stack. 251 @(private) 252 visit_value :: proc(e: ^Emitter, idx: Node_Index) -> (cycles: bool) { 253 target := effective(e, idx) 254 switch classify(e, target) { 255 case .Struct: 256 switch e.visit[target] { 257 case .On_Stack: 258 return true 259 case .Done: 260 return false 261 case .Unvisited: 262 visit_struct(e, target) 263 } 264 case .Union: 265 for variant in union_variants(e, target) { 266 if is_null_only(e, effective(e, variant)) { 267 continue 268 } 269 if visit_value(e, variant) { 270 return true 271 } 272 } 273 case .Multi, .Array: 274 items := e.pool.nodes[target].items 275 if items != NIL_NODE { 276 visit_indirect(e, items) 277 } 278 if classify(e, target) == .Multi && .Object in e.pool.nodes[target].types { 279 additional := e.pool.nodes[target].additional 280 if additional != NIL_NODE { 281 visit_indirect(e, additional) 282 } 283 } 284 case .Map: 285 additional := e.pool.nodes[target].additional 286 if additional != NIL_NODE { 287 visit_indirect(e, additional) 288 } 289 case .Any, .Boolean, .Integer, .Number, .Str, .Enum: 290 } 291 return false 292 } 293 294 // Crosses an indirection (slice or map), which breaks value containment. 295 @(private) 296 visit_indirect :: proc(e: ^Emitter, idx: Node_Index) { 297 target := effective(e, idx) 298 switch classify(e, target) { 299 case .Struct: 300 if e.visit[target] == .Unvisited { 301 visit_struct(e, target) 302 } 303 case .Union: 304 for variant in union_variants(e, target) { 305 visit_indirect(e, variant) 306 } 307 case .Multi, .Array: 308 items := e.pool.nodes[target].items 309 if items != NIL_NODE { 310 visit_indirect(e, items) 311 } 312 additional := e.pool.nodes[target].additional 313 if additional != NIL_NODE { 314 visit_indirect(e, additional) 315 } 316 case .Map: 317 additional := e.pool.nodes[target].additional 318 if additional != NIL_NODE { 319 visit_indirect(e, additional) 320 } 321 case .Any, .Boolean, .Integer, .Number, .Str, .Enum: 322 } 323 } 324 325 @(private) 326 visit_struct :: proc(e: ^Emitter, idx: Node_Index) { 327 e.visit[idx] = .On_Stack 328 members := make([dynamic]Member, context.temp_allocator) 329 required := make(map[string]bool, context.temp_allocator) 330 collect_members(e, idx, &members, &required) 331 for member in members { 332 if visit_value(e, member.node) { 333 e.broken[member.prop] = true 334 } 335 } 336 e.visit[idx] = .Done 337 } 338 339 // --- naming ------------------------------------------------------------------ 340 341 @(private) 342 unique_name :: proc(e: ^Emitter, base: string) -> string { 343 if !e.taken[base] { 344 e.taken[base] = true 345 return base 346 } 347 for n := 2; ; n += 1 { 348 candidate := strings.concatenate({base, "_", itoa(n)}) 349 if !e.taken[candidate] { 350 e.taken[candidate] = true 351 return candidate 352 } 353 } 354 } 355 356 // Assigns (once) a declaration name to a node and queues it for emission. 357 @(private) 358 ensure_decl :: proc(e: ^Emitter, idx: Node_Index, hint: string) -> string { 359 if e.names[idx] != "" { 360 return e.names[idx] 361 } 362 base := hint 363 if def, is_def := e.def_names[idx]; is_def { 364 base = type_name(def) 365 } 366 name := unique_name(e, base) 367 e.names[idx] = name 368 if !e.queued[idx] { 369 e.queued[idx] = true 370 append(&e.queue, idx) 371 } 372 return name 373 } 374 375 // --- type expressions ---------------------------------------------------------- 376 377 // Produces the Odin type expression for a property or element schema. 378 // union_like types (unions and json.Value) already encode absence as nil, 379 // so they are never wrapped in Maybe. 380 @(private) 381 type_expr :: proc(e: ^Emitter, idx: Node_Index, hint: string) -> (expr: string, union_like: bool, nullable: bool) { 382 target := effective(e, idx) 383 node := &e.pool.nodes[target] 384 nullable = .Null in e.pool.nodes[idx].types || .Null in node.types 385 386 switch classify(e, target) { 387 case .Any: 388 e.uses_json = true 389 return "json.Value", true, nullable 390 case .Str: 391 return "string", false, nullable 392 case .Integer: 393 return "i64", false, nullable 394 case .Number: 395 return "f64", false, nullable 396 case .Boolean: 397 return "bool", false, nullable 398 case .Struct, .Enum: 399 return ensure_decl(e, target, hint), false, nullable 400 case .Union: 401 if ref, other, ok := is_or_reference(e, target); ok { 402 other_name := ensure_decl(e, other, hint) 403 ensure_decl(e, ref, "Reference") 404 e.uses_or_reference = true 405 return strings.concatenate({"Or_Reference(", other_name, ")"}), true, nullable 406 } 407 if _, is_def := e.def_names[target]; is_def || e.names[target] != "" { 408 return ensure_decl(e, target, hint), true, nullable 409 } 410 inline, inline_union_like, nullable_variant := union_expr(e, target, hint) 411 return inline, inline_union_like, nullable || nullable_variant 412 case .Multi: 413 return multi_expr(e, target, hint), true, nullable 414 case .Array: 415 item := "json.Value" 416 if node.items != NIL_NODE { 417 item, _, _ = type_expr(e, node.items, strings.concatenate({hint, "_Item"})) 418 } else { 419 e.uses_json = true 420 } 421 return strings.concatenate({"[]", item}), false, nullable 422 case .Map: 423 value := "json.Value" 424 if node.additional != NIL_NODE { 425 value, _, _ = type_expr(e, node.additional, strings.concatenate({hint, "_Value"})) 426 } else { 427 e.uses_json = true 428 } 429 return strings.concatenate({"map[string]", value}), false, nullable 430 } 431 e.uses_json = true 432 return "json.Value", true, nullable 433 } 434 435 @(private) 436 union_expr :: proc(e: ^Emitter, idx: Node_Index, hint: string) -> (expr: string, union_like: bool, nullable: bool) { 437 parts := make([dynamic]string, context.temp_allocator) 438 sole_union_like := false 439 for variant, i in union_variants(e, idx) { 440 if is_null_only(e, effective(e, variant)) { 441 nullable = true 442 continue 443 } 444 variant_hint := strings.concatenate({hint, "_Variant_", itoa(i + 1)}) 445 part, part_union_like, variant_nullable := type_expr(e, variant, variant_hint) 446 nullable |= variant_nullable 447 sole_union_like = part_union_like 448 append(&parts, part) 449 } 450 switch len(parts) { 451 case 0: 452 e.uses_json = true 453 return "json.Value", true, nullable 454 case 1: 455 return parts[0], sole_union_like, nullable 456 } 457 b: strings.Builder 458 strings.builder_init(&b) 459 strings.write_string(&b, "union {") 460 for part, i in parts { 461 if i > 0 { 462 strings.write_string(&b, ", ") 463 } 464 strings.write_string(&b, part) 465 } 466 strings.write_string(&b, "}") 467 return strings.to_string(b), true, nullable 468 } 469 470 // Emission order for multi-type schemas, so output is deterministic. 471 @(private) 472 MULTI_ORDER :: [?]Simple_Type{.String, .Integer, .Number, .Boolean, .Array, .Object} 473 474 @(private) 475 multi_expr :: proc(e: ^Emitter, idx: Node_Index, hint: string) -> string { 476 node := &e.pool.nodes[idx] 477 b: strings.Builder 478 strings.builder_init(&b) 479 strings.write_string(&b, "union {") 480 written := 0 481 for t in MULTI_ORDER { 482 if t not_in node.types { 483 continue 484 } 485 if written > 0 { 486 strings.write_string(&b, ", ") 487 } 488 switch t { 489 case .String: 490 strings.write_string(&b, "string") 491 case .Integer: 492 strings.write_string(&b, "i64") 493 case .Number: 494 strings.write_string(&b, "f64") 495 case .Boolean: 496 strings.write_string(&b, "bool") 497 case .Array: 498 item := "json.Value" 499 if node.items != NIL_NODE { 500 item, _, _ = type_expr(e, node.items, strings.concatenate({hint, "_Item"})) 501 } else { 502 e.uses_json = true 503 } 504 strings.write_string(&b, "[]") 505 strings.write_string(&b, item) 506 case .Object: 507 value := "json.Value" 508 if node.additional != NIL_NODE { 509 value, _, _ = type_expr(e, node.additional, strings.concatenate({hint, "_Value"})) 510 } else { 511 e.uses_json = true 512 } 513 strings.write_string(&b, "map[string]") 514 strings.write_string(&b, value) 515 case .Null: 516 } 517 written += 1 518 } 519 strings.write_string(&b, "}") 520 return strings.to_string(b) 521 } 522 523 // --- declarations ---------------------------------------------------------------- 524 525 @(private) 526 emit_struct :: proc(e: ^Emitter, idx: Node_Index) { 527 name := e.names[idx] 528 members := make([dynamic]Member, context.temp_allocator) 529 required := make(map[string]bool, context.temp_allocator) 530 collect_members(e, idx, &members, &required) 531 532 b := &e.body 533 if len(members) == 0 { 534 strings.write_string(b, name) 535 strings.write_string(b, " :: struct {}") 536 return 537 } 538 539 strings.write_string(b, name) 540 strings.write_string(b, " :: struct {\n") 541 field_names := make(map[string]bool, context.temp_allocator) 542 for member in members { 543 fname := field_name(member.name) 544 if field_names[fname] { 545 fname = unique_field(fname, &field_names) 546 } 547 field_names[fname] = true 548 549 expr: string 550 union_like: bool 551 nullable: bool 552 if e.broken[member.prop] { 553 e.uses_json = true 554 expr, union_like = "json.Value", true 555 } else { 556 hint := strings.concatenate({name, "_", type_name(member.name)}) 557 expr, union_like, nullable = type_expr(e, member.node, hint) 558 } 559 560 optional := !required[member.name] 561 strings.write_string(b, "\t") 562 strings.write_string(b, fname) 563 strings.write_string(b, ": ") 564 if !union_like && (optional || nullable) { 565 strings.write_string(b, "Maybe(") 566 strings.write_string(b, expr) 567 strings.write_string(b, ")") 568 } else { 569 strings.write_string(b, expr) 570 } 571 strings.write_string(b, " `json:\"") 572 strings.write_string(b, member.name) 573 strings.write_string(b, "\"`,\n") 574 } 575 strings.write_string(b, "}") 576 } 577 578 @(private) 579 unique_field :: proc(base: string, taken: ^map[string]bool) -> string { 580 for n := 2; ; n += 1 { 581 candidate := strings.concatenate({base, "_", itoa(n)}) 582 if !taken[candidate] { 583 return candidate 584 } 585 } 586 } 587 588 @(private) 589 emit_enum :: proc(e: ^Emitter, idx: Node_Index) { 590 b := &e.body 591 strings.write_string(b, e.names[idx]) 592 strings.write_string(b, " :: enum {\n") 593 for value in enum_values(e, idx) { 594 strings.write_string(b, "\t") 595 strings.write_string(b, value) 596 strings.write_string(b, ",\n") 597 } 598 strings.write_string(b, "}") 599 } 600 601 @(private) 602 emit_union :: proc(e: ^Emitter, idx: Node_Index) { 603 name := e.names[idx] 604 b := &e.body 605 if ref, other, ok := is_or_reference(e, idx); ok { 606 other_name := ensure_decl(e, other, name) 607 ensure_decl(e, ref, "Reference") 608 e.uses_or_reference = true 609 strings.write_string(b, name) 610 strings.write_string(b, " :: Or_Reference(") 611 strings.write_string(b, other_name) 612 strings.write_string(b, ")") 613 return 614 } 615 expr, _, _ := union_expr(e, idx, name) 616 strings.write_string(b, name) 617 strings.write_string(b, " :: ") 618 strings.write_string(b, expr) 619 } 620 621 // Emits the full Odin source for a resolved pool. 622 @(private) 623 emit :: proc(pool: ^Pool, opts: Options, openapi: bool) -> string { 624 e := Emitter { 625 pool = pool, 626 opts = opts, 627 openapi = openapi, 628 names = make([]string, len(pool.nodes)), 629 visit = make([]Visit_State, len(pool.nodes)), 630 queued = make([]bool, len(pool.nodes)), 631 } 632 strings.builder_init(&e.body) 633 634 for def in pool.defs { 635 if _, exists := e.def_names[def.node]; !exists { 636 e.def_names[def.node] = def.name 637 } 638 } 639 640 root := effective(&e, pool.root) 641 find_cycles(&e, root) 642 643 root_name := opts.root_name if opts.root_name != "" else "Root" 644 switch classify(&e, root) { 645 case .Struct, .Enum, .Union: 646 ensure_decl(&e, root, root_name) 647 case .Any, .Boolean, .Integer, .Number, .Str, .Array, .Map, .Multi: 648 // Root is an alias-style declaration. 649 expr, _, _ := type_expr(&e, root, root_name) 650 name := unique_name(&e, root_name) 651 strings.write_string(&e.body, name) 652 strings.write_string(&e.body, " :: ") 653 strings.write_string(&e.body, expr) 654 } 655 656 for i := 0; i < len(e.queue); i += 1 { 657 if strings.builder_len(e.body) > 0 { 658 strings.write_string(&e.body, "\n\n") 659 } 660 idx := e.queue[i] 661 switch classify(&e, idx) { 662 case .Struct: 663 emit_struct(&e, idx) 664 case .Enum: 665 emit_enum(&e, idx) 666 case .Union: 667 emit_union(&e, idx) 668 case .Any, .Boolean, .Integer, .Number, .Str, .Array, .Map, .Multi: 669 } 670 } 671 672 package_name := opts.package_name if opts.package_name != "" else "schema" 673 out: strings.Builder 674 strings.builder_init(&out) 675 strings.write_string(&out, "// Code generated by jschema. DO NOT EDIT.\npackage ") 676 strings.write_string(&out, package_name) 677 strings.write_string(&out, "\n\n") 678 if e.uses_json { 679 strings.write_string(&out, "import \"core:encoding/json\"\n\n") 680 } 681 if e.uses_or_reference { 682 strings.write_string(&out, "Or_Reference :: union($T: typeid) {Reference, T}\n\n") 683 } 684 strings.write_string(&out, strings.to_string(e.body)) 685 strings.write_string(&out, "\n") 686 return strings.to_string(out) 687 }