///|
/// PKL-131b: target-driven codegen dispatcher. Only `MoonBit` is wired
/// today; the enum + dispatcher pair keeps the public surface stable
/// when future targets (Java / Kotlin / Swift / Go / TypeScript) land
/// — embedders never need to swap entry points or learn a new
/// per-target function name.
pub(all) enum CodegenTarget {
MoonBit
} derive(Eq, Debug)
///|
/// PKL-131b: single entry point for codegen, parametric over the
/// target language. Returns the generated source as a String;
/// embedders write it to disk / a stream as they see fit. Each
/// target's lowering rules live in its own helper so the dispatcher
/// stays a one-line `match`.
pub fn codegen(program : Program, target : CodegenTarget) -> String {
match target {
MoonBit => codegen_moonbit(program)
}
}
///|
/// PKL-131: lower a parsed Pkl module into a MoonBit source skeleton
/// so embedders can round-trip schemas through both type systems.
///
/// Today's scope:
/// - `class C { p: T = ... }` → `pub(all) struct C { p : T_mbt } derive(Eq, Show)`
/// - `typealias A = T` → `pub typealias A = T_mbt`
/// - `function` declarations are skipped (no data-shape MoonBit
/// equivalent; configure them at the embedder side).
///
/// Type mapping is intentionally narrow — when a Pkl type doesn't
/// have a faithful MoonBit counterpart, the emitter falls back to a
/// commented `// TODO: ` line above the field so the
/// human-in-the-loop can decide the right shape.
fn codegen_moonbit(program : Program) -> String {
let buf = StringBuilder::new()
let module_label = match program.module_name {
Some(name) => name
None => ""
}
buf.write_string("// Generated from `\{module_label}` by pkl-mbt codegen.\n")
buf.write_string("// PKL-131 — Do not edit by hand.\n")
let mut first = true
for declaration in program.declarations {
match declaration {
ClassDeclaration(class_decl) => {
if first {
buf.write_char('\n')
first = false
} else {
buf.write_char('\n')
}
emit_class_struct(class_decl, buf)
}
TypeAliasDeclaration(type_alias) => {
if first {
buf.write_char('\n')
first = false
} else {
buf.write_char('\n')
}
emit_typealias(type_alias, buf)
}
// PKL-131: module-level functions don't have a MoonBit
// struct / enum analogue — they encode behaviour, not shape.
// The embedder typically rewrites them by hand against their
// existing function library.
FunctionDeclaration(_) => ()
}
}
buf.to_string()
}
///|
fn emit_class_struct(class_decl : ClassDecl, buf : StringBuilder) -> Unit {
buf.write_string("///|\n")
buf.write_string("pub(all) struct \{class_decl.name} {\n")
for prop in class_decl.properties {
let mbt_type = match prop.type_name {
Some(type_name) => pkl_type_to_moonbit(type_name)
None => "Unit /* TODO: untyped property */"
}
buf.write_string(" \{prop.name} : \{mbt_type}\n")
}
buf.write_string("} derive(Eq, Show)\n")
}
///|
fn emit_typealias(type_alias : TypeAliasDecl, buf : StringBuilder) -> Unit {
let target = pkl_type_to_moonbit(type_alias.target)
buf.write_string("///|\n")
buf.write_string("pub typealias \{target} as \{type_alias.name}\n")
}
///|
/// PKL-131: translate a Pkl type-annotation source string into the
/// closest MoonBit type. The mapping covers the common scalar and
/// container shapes; anything outside drops into a `Unknown` slot
/// with a TODO comment so the generated file still parses and the
/// embedder gets a flag they can grep for.
fn pkl_type_to_moonbit(text : String) -> String {
let trimmed = trim_codegen_spaces(text)
// Nullable suffix is treated structurally: `T?` → `T_mbt?`.
if trimmed.has_suffix("?") {
let inner = String::unsafe_substring(
trimmed,
start=0,
end=trimmed.length() - 1,
)
return "\{pkl_type_to_moonbit(inner)}?"
}
// Constraint suffix `Int(isPositive)` → `Int`. The constraint stays
// a Pkl-side validator; codegen strips it because MoonBit doesn't
// model dependent / refinement types here.
match strip_codegen_constraint(trimmed) {
Some(base) => return pkl_type_to_moonbit(base)
None => ()
}
// Generic shapes — recognise the head and recurse on the arguments.
match split_codegen_generic(trimmed) {
Some((head, args)) =>
match head {
"Listing" | "List" =>
if args.length() == 1 {
return "Array[\{pkl_type_to_moonbit(args[0])}]"
}
"Mapping" | "Map" =>
if args.length() == 2 {
let k = pkl_type_to_moonbit(args[0])
let v = pkl_type_to_moonbit(args[1])
return "Map[\{k}, \{v}]"
}
"Set" =>
if args.length() == 1 {
// No ordered-unique container in moonbitlang/core yet;
// emit an Array with a TODO note for the embedder.
return "Array[\{pkl_type_to_moonbit(args[0])}] /* Set */"
}
"Pair" =>
if args.length() == 2 {
let a = pkl_type_to_moonbit(args[0])
let b = pkl_type_to_moonbit(args[1])
return "(\{a}, \{b})"
}
_ => ()
}
None => ()
}
// Union types collapse to the first branch with a TODO comment
// because MoonBit's enums need names; the embedder usually wants
// to pick a discriminated form by hand.
if has_top_level_pipe(trimmed) {
return "Unit /* TODO: union \{trimmed} */"
}
// String-literal type (`"alpha"` etc.) — Pkl narrows at the type
// level; MoonBit uses plain String.
if trimmed.length() >= 2 &&
trimmed.has_prefix("\"") &&
trimmed.has_suffix("\"") {
return "String"
}
match trimmed {
"Int" => "Int"
"Float" => "Double"
"Number" => "Double /* Number */"
"Boolean" | "Bool" => "Bool"
"String" => "String"
"Null" => "Unit"
"Any" => "Unit /* TODO: Any */"
"Unknown" => "Unit /* TODO: Unknown */"
"IntSeq" => "Array[Int] /* IntSeq */"
"Duration" => "Double /* Duration */"
"DataSize" => "Double /* DataSize */"
"Regex" => "String /* Regex pattern */"
"Bytes" => "Bytes"
other => other
}
}
///|
fn trim_codegen_spaces(s : String) -> String {
let mut start = 0
while start < s.length() {
let c = s[start].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
start = start + 1
} else {
break
}
}
let mut endIdx = s.length()
while endIdx > start {
let c = s[endIdx - 1].to_int().unsafe_to_char()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
endIdx = endIdx - 1
} else {
break
}
}
if start == 0 && endIdx == s.length() {
s
} else {
String::unsafe_substring(s, start~, end=endIdx)
}
}
///|
fn strip_codegen_constraint(text : String) -> String? {
// Recognise `Base(...)` where the parens balance to the end. Returns
// the base name on a hit, `None` otherwise.
let mut idx = -1
for i = 0; i < text.length(); i = i + 1 {
if text[i].to_int().unsafe_to_char() == '(' {
idx = i
break
}
}
if idx <= 0 || !text.has_suffix(")") {
return None
}
let mut depth = 0
for i = idx; i < text.length(); i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c == '(' {
depth = depth + 1
} else if c == ')' {
depth = depth - 1
if depth == 0 && i != text.length() - 1 {
return None
}
}
}
Some(String::unsafe_substring(text, start=0, end=idx))
}
///|
fn split_codegen_generic(text : String) -> (String, Array[String])? {
let mut idx = -1
let mut parens = 0
let mut brackets = 0
for i = 0; i < text.length(); i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c == '(' {
parens = parens + 1
} else if c == ')' && parens > 0 {
parens = parens - 1
} else if c == '[' {
brackets = brackets + 1
} else if c == ']' && brackets > 0 {
brackets = brackets - 1
} else if c == '<' && parens == 0 && brackets == 0 {
idx = i
break
}
}
if idx <= 0 || !text.has_suffix(">") {
return None
}
let base = String::unsafe_substring(text, start=0, end=idx)
let inner = String::unsafe_substring(
text,
start=idx + 1,
end=text.length() - 1,
)
let parts : Array[String] = []
let buf = StringBuilder::new()
let mut depth_paren = 0
let mut depth_bracket = 0
let mut depth_angle = 0
for c in inner {
if c == ',' && depth_paren == 0 && depth_bracket == 0 && depth_angle == 0 {
parts.push(trim_codegen_spaces(buf.to_string()))
buf.reset()
} else {
if c == '(' {
depth_paren = depth_paren + 1
} else if c == ')' && depth_paren > 0 {
depth_paren = depth_paren - 1
} else if c == '[' {
depth_bracket = depth_bracket + 1
} else if c == ']' && depth_bracket > 0 {
depth_bracket = depth_bracket - 1
} else if c == '<' {
depth_angle = depth_angle + 1
} else if c == '>' && depth_angle > 0 {
depth_angle = depth_angle - 1
}
buf.write_char(c)
}
}
let last = trim_codegen_spaces(buf.to_string())
if last != "" || parts.length() > 0 {
parts.push(last)
}
Some((base, parts))
}
///|
fn has_top_level_pipe(text : String) -> Bool {
let mut parens = 0
let mut brackets = 0
let mut angles = 0
for i = 0; i < text.length(); i = i + 1 {
let c = text[i].to_int().unsafe_to_char()
if c == '(' {
parens = parens + 1
} else if c == ')' && parens > 0 {
parens = parens - 1
} else if c == '[' {
brackets = brackets + 1
} else if c == ']' && brackets > 0 {
brackets = brackets - 1
} else if c == '<' {
angles = angles + 1
} else if c == '>' && angles > 0 {
angles = angles - 1
} else if c == '|' && parens == 0 && brackets == 0 && angles == 0 {
return true
}
}
false
}