///|
fn value_documentation(doc : String?) -> String {
guard doc is Some(content) && content != "" else { return "///|\n" }
content
.split("\r\n")
.join("\n")
.split("\r")
.join("\n")
.split("\n")
.map(line => if line == "" { "///" } else { "/// " + line.to_owned() })
.collect()
.join("\n") +
"\n"
}
///|
pub fn generate(
input : Input,
limits~ : Limits? = None,
naming~ : NamingStyle = Readable,
metadata_capabilities~ : @linked_metadata.Capabilities = @linked_metadata.Capabilities::unavailable(),
cancelled~ : () -> Bool = fn() { false },
) -> Output raise GenerationError {
if input.ir.required_types().any(@ir.Type::has_union) {
raise Invalid(
"moonbit.unsupported_type",
input.unit_id,
"Untagged unions are not supported by MoonBit generation",
)
}
generate_library(
input,
limits.unwrap_or(Limits::default()),
cancelled,
naming,
metadata_capabilities,
) catch {
Invalid(_, _, _) as error => raise error
@linked_metadata.Invalid(code, owner, message) =>
raise Invalid(code, owner, message)
@wire.Invalid(code, message) => raise Invalid(code, input.unit_id, message)
error =>
raise Invalid("moonbit.invalid_input", input.unit_id, error.to_string())
}
}
///|
fn generate_library(
input : Input,
limits : Limits,
cancelled : () -> Bool,
naming : NamingStyle,
metadata_capabilities : @linked_metadata.Capabilities,
) -> Output raise {
fn check_cancelled() -> Unit raise {
if cancelled() {
raise Invalid("moonbit.cancelled", input.unit_id, "Cancelled")
}
}
check_cancelled()
if input.unit_id == "" ||
limits.max_declarations <= 0 ||
limits.max_bytes <= 0 {
raise Invalid(
"moonbit.invalid_input",
input.unit_id,
"Nonempty unit identity and positive limits are required",
)
}
let (package_name, dependencies, package_def) = match input.ir.distribution {
Library(name, dependencies, definition) => (name, dependencies, definition)
_ =>
raise Invalid(
"moonbit.unsupported_distribution",
input.unit_id,
"A complete library implementation is required",
)
}
validate_path(package_name)
let source_json = @ir_json.to_json(input.ir)
let source_ir = canonical_json(source_json)
if @utf8.encode(source_ir).length() > limits.max_bytes {
raise Invalid(
"moonbit.byte_limit",
input.unit_id,
"Source IR snapshot exceeds the output budget",
)
}
let owned = @identities.node_set(input.unit_id, input.ir, json=source_json)
let index : Map[@ir.FQName, Declaration] = Map([])
let source_extensions = Map([])
for dependency_name, specification in dependencies {
if dependency_name.to_canonical() == "morphir/moonbit-source" {
for module_name, _ in specification.modules {
source_extensions[module_name.to_canonical()] = true
}
}
}
let error_contract = if package_def.modules
.values()
.any(m => {
m.value.values
.values()
.any(v => checked_decimal_definition_errors(v.value.value))
}) {
ErrorContract::CheckedDecimal
} else {
ErrorContract::Sdk
}
let lowering : Lowering = {
error_contract,
names: TargetNames::new(input, naming, metadata_capabilities, owned),
locals: None,
types: Map([]),
constructors: Map([]),
values: index,
shapes: Map([]),
exported_shapes: Map([]),
abstract_types: Map([]),
imports: Map::from_array([("sdk", true)]),
source_extensions,
}
for module_name, access in package_def.modules {
for name, entry in access.value.types {
let fqname : @ir.FQName = { package_name, module_name, local_name: name, }
validate_fqname(fqname)
lowering.types[fqname] = {
definition: entry.value.value,
public: access.access == @ir.Access::Public &&
entry.access == @ir.Access::Public,
exposed: entry.access == @ir.Access::Public,
}
}
}
let type_names = lowering.types.keys().collect()
type_names.sort()
let enums : Array[(@ir.FQName, Bool, Array[String])] = []
for name in type_names {
let context = "type:" + name.to_canonical()
owned[context] = true
let entry = lowering.types[name]
match entry.definition {
TypeAliasDefinition(params, target) => {
if !params.is_empty() {
raise Invalid("moonbit.unsupported_type", context, "Generic alias")
}
ignore(lower_type(lowering, target, context, trail=[name]))
}
CustomTypeDefinition(params, constructors) => {
if !params.is_empty() {
raise Invalid(
"moonbit.unsupported_type", context, "Generic custom type",
)
}
let constructor_names = constructors.value.keys().collect()
constructor_names.sort()
let cases = []
for constructor_name in constructor_names {
let fqname = { ..name, local_name: constructor_name, }
let inputs = constructors.value[constructor_name].map(p => {
lower_type(lowering, p.1, context)
})
// MoonBit checks the payload types of a `pub` type even when its constructors are hidden.
if entry.public {
for t in inputs {
export_type(lowering, t)
}
}
if lowering.constructors.contains(fqname) {
raise Invalid(
"moonbit.name_collision",
context,
fqname.to_canonical(),
)
}
lowering.constructors[fqname] = {
inputs,
output: Nominal(name),
module_name: name.module_name,
public: entry.public && constructors.access == @ir.Access::Public,
exposed: entry.exposed && constructors.access == @ir.Access::Public,
}
owned["constructor:" + fqname.to_canonical()] = true
cases.push(
" " +
lowering.names.names["constructor:" + fqname.to_canonical()] +
(if inputs.is_empty() {
""
} else {
"(" + inputs.map(t => type_source(lowering, t)).join(", ") + ")"
}),
)
}
enums.push((name, constructors.access == @ir.Access::Public, cases))
}
_ =>
raise Invalid(
"moonbit.incomplete_type", context, "Incomplete type definition",
)
}
}
let module_names = package_def.modules.keys().collect()
module_names.sort()
for module_name in module_names {
check_cancelled()
validate_path(module_name)
let access = package_def.modules[module_name]
let module_def = access.value
let module_id = "module:" +
package_name.to_canonical() +
":" +
module_name.to_canonical()
owned[module_id] = true
for name, value in module_def.values {
check_cancelled()
validate_name(name)
let fqname : @ir.FQName = { package_name, module_name, local_name: name, }
let context = value_node(fqname)
let (inputs, output, body) = match value.value.value {
ExpressionBody(inputs, output, body) =>
(
inputs.map(p => {
(
p.0,
lower_type(
lowering,
p.1,
context + "/input/" + p.0.to_canonical(),
),
)
}),
lower_type(lowering, output, context + "/output"),
body,
)
IncompleteBody(_, _, _, _) =>
raise Invalid(
"moonbit.incomplete_body", context, "Incomplete definition",
)
NativeBody(_, _, _) | ExternalBody(_, _, _, _) =>
raise Invalid(
"moonbit.unresolved_implementation", context, "No target implementation is registered",
)
}
let seen_inputs = Map([])
for parameter in inputs {
validate_name(parameter.0)
if seen_inputs.contains(parameter.0) {
raise Invalid(
"moonbit.duplicate_parameter",
context,
parameter.0.to_canonical(),
)
}
seen_inputs[parameter.0] = true
owned[context + "/input/" + parameter.0.to_canonical()] = true
}
index[fqname] = {
fqname,
doc: value.value.doc,
module_name,
output,
inputs,
body,
public: access.access == @ir.Access::Public &&
value.access == @ir.Access::Public,
exposed: value.access == @ir.Access::Public,
}
for node in [context, context + "/output", context + "/body"] {
owned[node] = true
}
if index.length() > limits.max_declarations {
raise Invalid(
"moonbit.declaration_limit", context, "Too many declarations",
)
}
}
}
let names = index.keys().collect()
names.sort()
let imports = lowering.imports
let definitions = []
let symbols = []
let generated = Map([])
for name in names {
check_cancelled()
let declaration = index[name]
let identifier = lowering.names.names[value_node(name)]
lowering.locals = Some(LocalNames::new(lowering.names, declaration))
if generated.contains(identifier) {
raise Invalid("moonbit.name_collision", name.to_canonical(), identifier)
}
generated[identifier] = true
if declaration.public {
export_type(lowering, declaration.output)
for parameter in declaration.inputs {
export_type(lowering, parameter.1)
}
}
let output = type_source(
lowering,
curry_type(declaration.inputs.map(p => p.1), declaration.output),
)
let env = Map::from_array(declaration.inputs)
let body = curry_source(
lowering,
declaration.inputs.map(p => (lowering.locals.unwrap().names[p.0], p.1)),
declaration.output,
expression(
lowering,
declaration.body,
declaration.output,
env,
declaration.fqname,
value_node(name) + "/body",
),
)
definitions.push(
value_documentation(declaration.doc) +
(if declaration.public { "pub " } else { "" }) +
"fn " +
identifier +
"() -> " +
output +
" raise " +
lowering.error_contract.source() +
" {\n " +
body +
"\n}\n",
)
let source_node = value_node(name)
symbols.push({
fqname: name.to_canonical(),
generated_name: identifier,
public: declaration.public,
source_node,
generated_node: "moonbit:fn:" + identifier,
origins: [source_node, source_node + "/body"],
})
}
// Rendered after the values, because their signatures can make a type abstract.
let type_definitions = enums.map(e => {
let (name, open, cases) = e
let entry = lowering.types[name]
"///|\n" +
(if entry.public && open {
"pub(all) "
} else if entry.public {
"pub "
} else if lowering.abstract_types.contains(name) {
""
} else {
"priv "
}) +
"enum " +
lowering.names.names["type:" + name.to_canonical()] +
" {\n" +
cases.join("\n") +
"\n}\n"
})
let shape_names = lowering.shapes.keys().collect()
shape_names.sort()
for name in shape_names {
type_definitions.push(
"///|\n" +
(if lowering.exported_shapes.contains(name) {
"pub(all) "
} else {
"priv "
}) +
"struct " +
name +
" {\n" +
lowering.shapes[name]
.map(p => {
" " +
field_name(lowering, name, p.0) +
" : " +
type_source(lowering, p.1)
})
.join("\n") +
"\n}\n",
)
}
let import_names = imports
.keys()
.filter(name => name != "source_string_index")
.collect()
import_names.sort()
let package_manifest = "import {\n" +
import_names
.map(name => {
" " +
quote("finos/morphir-sdk" + (if name == "sdk" { "" } else { "/" + name })) +
" @" +
name +
","
})
.join("\n") +
"\n}\n"
let module_name = "morphir-generated/p_" + hex(package_name.to_canonical())
let module_manifest = "name = " +
quote(module_name) +
"\nversion = \"0.0.0\"\nimport {\n \"finos/morphir-sdk@0.1.0\",\n}\n"
check_cancelled()
lowering.names.allocations.sort_by(fn(a, b) { a.owner.compare(b.owner) })
lowering.names.warnings.sort_by(fn(a, b) { a.key().compare(b.key()) })
let metadata = symbol_metadata(
input,
source_ir,
symbols,
owned,
lowering.names,
lowering.error_contract,
)
let artifacts : Array[Artifact] = [
{ path: "moon.mod", role: ModuleManifest, content: Text(module_manifest), },
{
path: "moon.pkg",
role: PackageManifest,
content: Text(package_manifest),
},
{
path: "library.mbt",
role: Source,
content: Text((type_definitions + definitions).join("\n")),
},
{ path: "symbols.10n", role: Metadata, content: Binary(metadata), },
]
let mut bytes = 0
for artifact in artifacts {
bytes += match artifact.content {
Text(text) => @utf8.encode(text).length()
Binary(data) => data.length()
}
}
if bytes > limits.max_bytes {
raise Invalid(
"moonbit.byte_limit",
input.unit_id,
"Generated artifacts exceed the byte budget",
)
}
check_cancelled()
let invocations = names.filter_map(name => {
invocation_entry(lowering, index[name])
})
let (invocation_codecs, invocation_types) = invocation_codecs(
lowering, invocations,
)
// One lookup for each name: a search of the invocations for each name would
// make generation quadratic in the number of values.
let invoked : Map[String, Bool] = Map::from_array(
invocations.map(e => (e.fqname, true)),
)
{
artifacts,
symbols,
naming_style: naming,
naming_facts: lowering.names.facts,
naming_allocations: lowering.names.allocations,
warnings: lowering.names.warnings,
invocations,
invocation_codecs,
invocation_types,
invocation_rejections: names.filter_map(name => {
let d = index[name]
let fqname = name.to_canonical()
if d.public && !invoked.contains(fqname) {
Some(
(
fqname,
if boundary_type(d.output) is None ||
d.inputs.any(p => boundary_type(p.1) is None) {
"execution.boundary_function"
} else {
"execution.constructor_visibility"
},
),
)
} else {
None
}
}),
project: {
module_name,
root: "",
members: artifacts.map(artifact => artifact.path),
dependencies: [
{
module_name: "finos/morphir-sdk",
version: "0.1.0",
api_profile: "morphir-sdk-concrete-v1" +
(if lowering.imports.contains("source_string") {
"+moonbit-source-string-v1"
} else {
""
}) +
(if lowering.imports.contains("source_int32") {
"+moonbit-source-int32-v1"
} else {
""
}) +
(if lowering.imports.contains("source_integer") {
"+moonbit-source-integer-v1"
} else {
""
}) +
(if lowering.imports.contains("source_float") {
"+moonbit-source-float-v1"
} else {
""
}) +
(if lowering.imports.contains("source_string_index") {
"+moonbit-source-string-index-v1"
} else {
""
}) +
(match lowering.error_contract {
CheckedDecimal => "+decimal-checked-v1"
Sdk => ""
}),
semantic_pin: "bc99af69a8b24d391311fae3822a87eafef3c334",
},
],
},
}
}