// Emission of MoonBit source from the OpenCC snapshot.
//
// Dictionaries are embedded as their own text, split into chunks of raw string
// lines, and indexed at run time by `core/Dict::parse`. That keeps the generated
// files small, keeps the compiler fast, and — unlike one literal per entry —
// stays inside the plain wasm backend's limits. The embedded text is
// byte-identical to the file named in the header, so it can be audited.
///|
/// Lines per generated string chunk.
let chunk_lines : Int = 4000
///|
/// Derive a MoonBit identifier from a dictionary file name.
///
/// `STPhrases.txt` -> `st_phrases`, `CJK_Compatibility_Ideographs.txt` ->
/// `cjk_compatibility_ideographs`.
pub fn module_name(file : String) -> String {
let stem = match file.strip_suffix(".txt") {
Some(stem) => stem
None => file.to_string_view()
}
let chars = stem.to_array()
let out = StringBuilder()
for index, ch in chars.iter2() {
if ch >= 'A' && ch <= 'Z' {
// Insert a boundary before an acronym tail (`STPhrases` -> `st_phrases`)
// and before an uppercase letter that follows a lowercase one.
let previous = if index > 0 { chars[index - 1] } else { ' ' }
let next = if index + 1 < chars.length() { chars[index + 1] } else { ' ' }
let after_word = (previous >= 'a' && previous <= 'z') ||
(previous >= '0' && previous <= '9')
let after_acronym = previous >= 'A' &&
previous <= 'Z' &&
next >= 'a' &&
next <= 'z'
if !out.is_empty() && (after_word || after_acronym) {
out.write_char('_')
}
out.write_char(ch)
} else if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') {
out.write_char(ch)
} else {
out.write_char('_')
}
}
let mut name = out.to_string().to_lower()
while name.contains("__") {
name = name.replace_all(old="__", new="_")
}
name.trim(chars="_").to_owned()
}
///|
/// Escape a string for use inside a MoonBit string literal.
pub fn escape(text : String) -> String {
let out = StringBuilder()
for ch in text.iter() {
if ch == '"' || ch == '\\' {
out.write_char('\\')
}
out.write_char(ch)
}
out.to_string()
}
///|
/// Split text into lines, dropping the empty piece after a trailing newline.
pub fn lines_of(text : String) -> Array[String] {
let lines : Array[String] = []
for raw in text.split("\n") {
lines.push(raw.to_owned())
}
if lines.length() > 0 && lines[lines.length() - 1].is_empty() {
ignore(lines.pop())
}
lines
}
///|
/// Emit the data module for one dictionary.
///
/// The text is embedded verbatim through `#|` raw strings. Those strings drop a
/// single leading space, so a line that starts with a space would lose a
/// character; `tools/gen_dict` refuses such data instead of shipping it.
pub fn emit_dict_data(
binding : String,
source_label : String,
text : String,
) -> String {
let lines = lines_of(text)
let out = StringBuilder()
out.write_string("// Generated by tools/gen_dict from ")
out.write_string(source_label)
out.write_string(" — do not edit by hand.\n")
out.write_string("//\n")
out.write_string(
"// The embedded text is byte-identical to that file; its SHA-256 is recorded\n",
)
out.write_string(
"// in dict/manifest.mbt. Entries are indexed by core/Dict::parse.\n",
)
out.write_string("//\n")
out.write_string(
"// Only the first candidate of a multi-value entry is used for conversion;\n",
)
out.write_string(
"// the remaining candidates take part in segmentation scoring upstream.\n\n",
)
let mut chunks = 0
let mut index = 0
while index < lines.length() {
let stop = if index + chunk_lines < lines.length() {
index + chunk_lines
} else {
lines.length()
}
out.write_string("///|\n")
out.write_string("let ")
out.write_string(binding)
out.write_string("_chunk_")
out.write_string(chunks.to_string())
out.write_string(" : String =\n")
let mut line_index = index
while line_index < stop {
out.write_string(" #|")
out.write_string(lines[line_index])
out.write_string("\n")
line_index = line_index + 1
}
chunks = chunks + 1
index = stop
}
if chunks == 0 {
out.write_string("///|\nlet ")
out.write_string(binding)
out.write_string("_chunk_0 : String = \"\"\n")
chunks = 1
}
out.write_string("\n///|\n")
out.write_string("pub let ")
out.write_string(binding)
out.write_string(" : Array[String] = [\n")
let mut chunk_index = 0
while chunk_index < chunks {
out.write_string(" ")
out.write_string(binding)
out.write_string("_chunk_")
out.write_string(chunk_index.to_string())
out.write_string(",\n")
chunk_index = chunk_index + 1
}
out.write_string("]\n")
out.to_string()
}
///|
/// Emit the package file of one dictionary package.
///
/// Each dictionary lives in its own package so that a consumer of a single
/// configuration links only the dictionaries that configuration references.
/// `dict` itself re-exports everything for name-based lookups.
pub fn emit_dict_subpkg() -> String {
"// Generated by tools/gen_dict — do not edit by hand.\nformatter(ignore: [\"data.mbt\"])\n"
}
///|
/// Emit the package file of the aggregate `dict` package.
pub fn emit_dict_aggregate_pkg(
module_path : String,
bindings : Array[String],
) -> String {
let out = StringBuilder()
out.write_string("// Generated by tools/gen_dict — do not edit by hand.\n")
out.write_string("//\n")
out.write_string(
"// This aggregate re-exports every dictionary, which is what the CLI, the tests\n",
)
out.write_string(
"// and `lookup` need. Size-sensitive consumers should import a `config/`\n",
)
out.write_string(
"// package instead: it references only the dictionaries that configuration uses.\n\n",
)
out.write_string("import {\n")
for binding in bindings {
out.write_string(" \"")
out.write_string(module_path)
out.write_string("/dict/")
out.write_string(binding)
out.write_string("\",\n")
}
out.write_string("}\n\n")
out.write_string("formatter(ignore: [\"manifest.mbt\", \"reexport.mbt\"])\n")
out.to_string()
}
///|
/// Emit the aggregate re-exports, so `@dict.st_phrases` keeps working.
pub fn emit_reexport(bindings : Array[String]) -> String {
let out = StringBuilder()
out.write_string(
"// Generated by tools/gen_dict — do not edit by hand.\n\n",
)
for binding in bindings {
out.write_string("///| Re-export of the `")
out.write_string(binding)
out.write_string("` dictionary.\npub using @")
out.write_string(binding)
out.write_string(" {")
out.write_string(binding)
out.write_string("}\n\n")
}
out.to_string()
}
///|
/// Emit the `lookup` function that resolves a binding name to its text chunks.
pub fn emit_lookup(bindings : Array[String]) -> String {
let out = StringBuilder()
out.write_string("\n///|\n")
out.write_string("/// Resolve a dictionary binding by name.\n")
out.write_string("///\n")
out.write_string(
"/// Generated from the snapshot: unknown names return `None` instead of\n",
)
out.write_string(
"/// failing, so a configuration that references a dictionary this snapshot\n",
)
out.write_string("/// does not ship degrades to \"that pass is skipped\".\n")
out.write_string("pub fn lookup(binding : String) -> Array[String]? {\n")
out.write_string(" match binding {\n")
for binding in bindings {
out.write_string(" \"")
out.write_string(binding)
out.write_string("\" => Some(")
out.write_string(binding)
out.write_string(")\n")
}
out.write_string(" _ => None\n")
out.write_string(" }\n")
out.write_string("}\n")
out.to_string()
}
///|
/// Write one pass node as MoonBit source.
fn emit_spec(out : StringBuilder, spec : Spec) -> Unit {
match spec {
Leaf(name) => {
out.write_string("Spec::Leaf(\"")
out.write_string(name)
out.write_string("\")")
}
Group(policy, children) => {
out.write_string("Spec::Group(")
out.write_string(
match policy {
Union => "Policy::Union"
ShortCircuit => "Policy::ShortCircuit"
},
)
out.write_string(", [")
for child in children {
emit_spec(out, child)
out.write_string(", ")
}
out.write_string("])")
}
}
}
///|
/// Record one leaf name if it is missing from the snapshot.
fn note_missing(
pass : Spec,
available : Array[String],
missing : Array[String],
) -> Unit {
for name in pass.leaves() {
if !available.contains(name) && !missing.contains(name) {
missing.push(name)
}
}
}
///|
/// Dictionaries a configuration asks for that the snapshot does not ship.
fn missing_of(spec : ChainSpec, available : Array[String]) -> Array[String] {
let missing : Array[String] = []
for pass in spec.normalization {
note_missing(pass, available, missing)
}
match spec.segmentation {
Some(pass) => note_missing(pass, available, missing)
None => ()
}
for pass in spec.conversion {
note_missing(pass, available, missing)
}
missing
}
///|
/// Write a list of passes as MoonBit source.
fn emit_passes(
out : StringBuilder,
specs : Array[Spec],
indent : String,
) -> Unit {
for spec in specs {
out.write_string(indent)
emit_spec(out, spec)
out.write_string(",\n")
}
}
///|
/// Emit `config/chains.mbt` from the parsed configurations.
pub fn emit_chains(
specs : Array[ChainSpec],
available : Array[String],
) -> String {
let out = StringBuilder()
out.write_string(
"// Generated by tools/gen_dict from data/opencc/config/*.json — do not edit by hand.\n",
)
out.write_string("//\n")
out.write_string(
"// `passes` is the normalisation pass followed by the conversion chain, in the\n",
)
out.write_string(
"// order OpenCC applies them. `missing` lists dictionaries the configuration\n",
)
out.write_string(
"// references but the pinned snapshot does not ship (OpenCC produces them during\n",
)
out.write_string(
"// its own build); the runtime builder skips those leaves and reports them.\n\n",
)
out.write_string("///|\n")
out.write_string("pub(all) enum Policy {\n")
out.write_string(" Union\n")
out.write_string(" ShortCircuit\n")
out.write_string("}\n\n")
out.write_string("///|\n")
out.write_string("pub(all) enum Spec {\n")
out.write_string(" Leaf(String)\n")
out.write_string(" Group(Policy, Array[Spec])\n")
out.write_string("}\n\n")
out.write_string("///|\n")
out.write_string("pub struct ChainSpec {\n")
out.write_string(" name : String\n")
out.write_string(" title : String\n")
out.write_string(" missing : Array[String]\n")
out.write_string(" normalization : Array[Spec]\n")
out.write_string(" segmentation : Spec?\n")
out.write_string(" conversion : Array[Spec]\n")
out.write_string("}\n\n")
out.write_string("///| Every configuration of the pinned snapshot.\n")
out.write_string("pub let chains : Array[ChainSpec] = [\n")
for spec in specs {
out.write_string(" {\n")
out.write_string(" name: \"")
out.write_string(spec.name)
out.write_string("\",\n")
out.write_string(" title: \"")
out.write_string(escape(spec.title))
out.write_string("\",\n")
out.write_string(" missing: [")
for name in missing_of(spec, available) {
out.write_string("\"")
out.write_string(name)
out.write_string("\", ")
}
out.write_string("],\n")
out.write_string(" normalization: [\n")
emit_passes(out, spec.normalization, " ")
out.write_string(" ],\n")
out.write_string(" segmentation: ")
match spec.segmentation {
Some(pass) => {
out.write_string("Some(")
emit_spec(out, pass)
out.write_string(")")
}
None => out.write_string("None")
}
out.write_string(",\n")
out.write_string(" conversion: [\n")
emit_passes(out, spec.conversion, " ")
out.write_string(" ],\n")
out.write_string(" },\n")
}
out.write_string("]\n")
out.to_string()
}
///|
/// Record the dictionaries of a pass that the snapshot provides.
fn note_used(
pass : Spec,
available : Array[String],
used : Array[String],
) -> Unit {
for name in pass.leaves() {
if available.contains(name) && !used.contains(name) {
used.push(name)
}
}
}
///|
/// Dictionaries a configuration actually links, in first-use order.
fn used_bindings(spec : ChainSpec, available : Array[String]) -> Array[String] {
let used : Array[String] = []
for pass in spec.normalization {
note_used(pass, available, used)
}
match spec.segmentation {
Some(pass) => note_used(pass, available, used)
None => ()
}
for pass in spec.conversion {
note_used(pass, available, used)
}
used
}
///|
/// Emit one pass node as a runtime expression, or nothing when no leaf resolves.
fn runtime_node(spec : Spec, available : Array[String]) -> String? {
match spec {
Leaf(binding) =>
if available.contains(binding) {
Some(
"@core.Node::leaf(@core.Dict::parse(\"" +
binding +
"\", @" +
binding +
"." +
binding +
"))",
)
} else {
None
}
Group(policy, children) => {
let parts : Array[String] = []
for child in children {
match runtime_node(child, available) {
Some(text) => parts.push(text)
None => ()
}
}
if parts.is_empty() {
None
} else {
let policy_text = match policy {
Union => "@core.Union"
ShortCircuit => "@core.ShortCircuit"
}
Some(
"@core.Node::group(" + policy_text + ", [" + parts.join(", ") + "])",
)
}
}
}
}
///|
/// Emit a list of passes as runtime expressions.
fn emit_runtime_list(
out : StringBuilder,
indent : String,
specs : Array[Spec],
available : Array[String],
) -> Unit {
out.write_string(indent)
out.write_string("[\n")
for spec in specs {
match runtime_node(spec, available) {
Some(text) => {
out.write_string(indent)
out.write_string(" ")
out.write_string(text)
out.write_string(",\n")
}
None => ()
}
}
out.write_string(indent)
out.write_string("],\n")
}
///|
/// Emit `config//moon.pkg`.
pub fn emit_config_subpkg(module_path : String, used : Array[String]) -> String {
let out = StringBuilder()
out.write_string("// Generated by tools/gen_dict — do not edit by hand.\n")
out.write_string("//\n")
out.write_string(
"// This package builds one configuration and references only the dictionaries it\n",
)
out.write_string(
"// needs, so a program that wants one direction links one direction's worth of\n",
)
out.write_string(
"// data. Use the aggregate `config` package when the name is only known at run\n",
)
out.write_string("// time.\n\n")
out.write_string("import {\n \"")
out.write_string(module_path)
out.write_string("/core\",\n")
for binding in used {
out.write_string(" \"")
out.write_string(module_path)
out.write_string("/dict/")
out.write_string(binding)
out.write_string("\",\n")
}
out.write_string("}\n\nformatter(ignore: [\"chain.mbt\"])\n")
out.to_string()
}
///|
/// Emit `config//chain.mbt`.
pub fn emit_config_chain(
spec : ChainSpec,
available : Array[String],
missing : Array[String],
) -> String {
let out = StringBuilder()
out.write_string("// Generated by tools/gen_dict from data/opencc/config/")
out.write_string(spec.name)
out.write_string(".json — do not edit by hand.\n\n")
out.write_string("///| Configuration name as OpenCC spells it.\n")
out.write_string("pub let name : String = \"")
out.write_string(spec.name)
out.write_string("\"\n\n")
out.write_string(
"///| Human-readable name from the upstream configuration.\n",
)
out.write_string("pub let title : String = \"")
out.write_string(escape(spec.title))
out.write_string("\"\n\n")
out.write_string(
"///| Dictionaries this configuration references but the snapshot lacks.\n",
)
out.write_string("pub let missing : Array[String] = [")
for name in missing {
out.write_string("\"")
out.write_string(name)
out.write_string("\", ")
}
out.write_string("]\n\n")
out.write_string(
"///| Build the conversion chain. Only the dictionaries listed in the package\n",
)
out.write_string("/// import are linked in.\n")
out.write_string("pub fn chain() -> @core.Chain {\n")
out.write_string(" @core.Chain::new(\n \"")
out.write_string(spec.name)
out.write_string("\",\n")
emit_runtime_list(out, " ", spec.normalization, available)
out.write_string(" ")
match spec.segmentation {
Some(pass) =>
match runtime_node(pass, available) {
Some(text) => {
out.write_string("Some(")
out.write_string(text)
out.write_string(")")
}
None => out.write_string("None")
}
None => out.write_string("None")
}
out.write_string(",\n")
emit_runtime_list(out, " ", spec.conversion, available)
out.write_string(" )\n}\n")
out.to_string()
}