///|
/// Private worker input descriptions; these are not an upstream RPC schema.
fn input_schema(
tpe : @ir.Type,
types : Map[String, @ir.TypeDefinition],
variables : Map[@ir.Name, Json],
depth : Int32,
) -> Json raise {
if depth > 24 {
raise @scheme.Unsupported(
"Recursive or deeply nested input type: " + type_label(tpe),
)
}
fn shape(
kind : String,
default_value : Json,
extra : Array[(String, Json)],
) -> Json {
let fields = [
("kind", @wire.text(kind)),
("label", @wire.text(type_label(tpe))),
("default", default_value),
]
fields.append(extra)
@wire.obj(fields)
}
match tpe {
Union(_, _) =>
raise @scheme.Unsupported("Input editor does not support untagged unions")
Variable(_, name) =>
match variables.get(name) {
Some(schema) => schema
None =>
raise @scheme.Unsupported(
"Unresolved type parameter: " + name.to_canonical(),
)
}
Unit(_) => shape("unit", Json::null(), [])
Tuple(_, members) => {
let schemas = members.map(item => {
input_schema(item, types, variables, depth + 1)
})
shape(
"tuple",
Json::array(schemas.map(schema => @wire.field(schema, "default"))),
[("items", Json::array(schemas))],
)
}
Record(_, fields) => {
let entries = fields.iter().collect()
entries.sort_by((a, b) => {
a.0.to_canonical().lexical_compare(b.0.to_canonical())
})
let schemas = entries.map(pair => {
(
pair.0.to_canonical(),
input_schema(pair.1, types, variables, depth + 1),
)
})
shape(
"record",
@wire.obj(schemas.map(pair => (pair.0, @wire.field(pair.1, "default")))),
[("fields", @wire.obj(schemas))],
)
}
Reference(_, name, args) => {
let id = name.to_canonical()
if args.is_empty() {
match id {
"morphir/SDK:basics#bool" =>
return shape("bool", Json::boolean(false), [])
"morphir/SDK:basics#int" | "morphir/SDK:int#int" =>
return shape("int", @wire.text("0"), [])
"morphir/SDK:basics#float" =>
return shape("float", Json::number(0), [])
"morphir/SDK:string#string" =>
return shape("string", @wire.text(""), [])
"morphir/SDK:char#char" => return shape("char", @wire.text("a"), [])
_ => ()
}
}
let schemas = args.map(arg => {
input_schema(arg, types, variables, depth + 1)
})
if id == "morphir/SDK:list#list" && schemas.length() == 1 {
return shape("list", Json::array([]), [("item", schemas[0])])
}
let definition = match types.get(id) {
Some(definition) => definition
None =>
raise @scheme.Unsupported(
"Input type is not available locally: " + id,
)
}
let (params, alias_type, constructors) = match definition {
TypeAliasDefinition(params, body) => (params, Some(body), None)
CustomTypeDefinition(params, constructors) if constructors.access ==
Public => (params, None, Some(constructors.value))
_ =>
raise @scheme.Unsupported(
"Input type has private or incomplete constructors: " + id,
)
}
if params.length() != schemas.length() {
raise @scheme.Invalid("Type argument count for " + id)
}
let scope : Map[@ir.Name, Json] = Map([])
for i, param in params {
scope[param] = schemas[i]
}
if alias_type is Some(body) {
return input_schema(body, types, scope, depth + 1)
}
let variants = constructors.unwrap().iter().collect()
variants.sort_by((a, b) => {
a.0.to_canonical().lexical_compare(b.0.to_canonical())
})
let choices = variants.map(pair => {
let tag = name.package_name.to_canonical() +
":" +
name.module_name.to_canonical() +
"#" +
pair.0.to_canonical()
let arguments = pair.1.map(arg => {
input_schema(arg.1, types, scope, depth + 1)
})
@wire.obj([
("constructor", @wire.text(tag)),
("label", @wire.text(display_name(pair.0, upper=true))),
(
"argumentLabels",
Json::array(pair.1.map(arg => @wire.text(display_name(arg.0)))),
),
("arguments", Json::array(arguments)),
(
"default",
@wire.obj([
("constructor", @wire.text(tag)),
(
"arguments",
Json::array(
arguments.map(schema => @wire.field(schema, "default")),
),
),
]),
),
])
})
if choices.is_empty() {
raise @scheme.Unsupported("Input type has no constructors: " + id)
}
shape("custom", @wire.field(choices[0], "default"), [
("variants", Json::array(choices)),
])
}
Function(_, _, _) | ExtensibleRecord(_, _, _) =>
raise @scheme.Unsupported(
"Input editor does not support " + type_label(tpe),
)
}
}
///|
fn evaluation_types(file : @ir.IRFile) -> Map[String, @ir.TypeDefinition] {
let result : Map[String, @ir.TypeDefinition] = Map([])
match file.distribution {
Library(package_name, _, package_def)
| Application(package_name, _, package_def, _) =>
for module_name, module_def in package_def.modules {
for name, item in module_def.value.types {
result[package_name.to_canonical() +
":" +
module_name.to_canonical() +
"#" +
name.to_canonical()] = item.value.value
}
}
Specs(_, _, _) => ()
}
result
}
///|
fn evaluation_definitions(
file : @ir.IRFile,
) -> Map[String, @ir.ValueDefinition] {
let result : Map[String, @ir.ValueDefinition] = Map([])
match file.distribution {
Library(package_name, _, package_def)
| Application(package_name, _, package_def, _) =>
for module_name, module_def in package_def.modules {
for name, item in module_def.value.values {
if module_def.access != @ir.Access::Public ||
item.access != @ir.Access::Public {
continue
}
result[package_name.to_canonical() +
":" +
module_name.to_canonical() +
"#" +
name.to_canonical()] = item.value.value
}
}
Specs(_, _, _) => ()
}
result
}
///|
fn evaluation_profile(
definition : @ir.ValueDefinition,
types : Map[String, @ir.TypeDefinition],
) -> Json {
try {
let inputs = match definition {
ExpressionBody(inputs, _, _) => inputs
_ =>
raise @scheme.Unsupported(
"Local evaluation requires a portable definition",
)
}
let schemas = inputs.map(input => {
@wire.obj([
("name", @wire.text(input.0.to_canonical())),
("label", @wire.text(display_name(input.0))),
("schema", input_schema(input.1, types, Map([]), 0)),
])
})
@wire.obj([
("supported", Json::boolean(true)),
("inputs", Json::array(schemas)),
(
"default",
Json::array(
schemas.map(input => {
@wire.field(@wire.field(input, "schema"), "default")
}),
),
),
])
} catch {
error =>
@wire.obj([
("supported", Json::boolean(false)),
("reason", @wire.text(@scheme.error_message(error))),
])
}
}
///|
fn evaluation_profiles(file : @ir.IRFile) -> Json {
let types = evaluation_types(file)
@wire.obj(
evaluation_definitions(file)
.iter()
.map(pair => (pair.0, evaluation_profile(pair.1, types)))
.collect(),
)
}
///|
/// Construct runtime data directly. Input text is never evaluated as Scheme.
fn input_value(
schema : Json,
value : Json,
path : String,
) -> @scheme.Value raise {
input_value_inner(schema, value, path) catch {
@wire.Invalid(_, message) => raise @scheme.Invalid(path + ": " + message)
error => raise error
}
}
///|
fn input_value_inner(
schema : Json,
value : Json,
path : String,
) -> @scheme.Value raise {
fn invalid() -> @scheme.Value raise {
raise @scheme.Invalid(
path + ": expected " + @wire.string(@wire.field(schema, "label")),
)
}
match @wire.string(@wire.field(schema, "kind")) {
"unit" => if value == Json::null() { @scheme.Unit } else { invalid() }
"bool" =>
match value {
True => @scheme.Boolean(true)
False => @scheme.Boolean(false)
_ => invalid()
}
"string" =>
match value {
String(text) => @scheme.Text(text)
_ => invalid()
}
"char" => @scheme.Character(@wire.character(value))
"int" =>
match value {
String(text) => {
ignore(@wire.write_integer(text))
@scheme.integer(text)
}
_ =>
raise @scheme.Invalid(
path + ": Int requires a decimal string to preserve precision",
)
}
"float" => @scheme.Number(@scheme.Number::Real(@wire.float(value)))
"record" => {
let fields = @wire.object(@wire.field(schema, "fields"))
let data = @wire.object(value)
if fields.length() != data.length() {
return invalid()
}
let result : Map[String, @scheme.Value] = Map([])
for name, child in fields {
let item = match data.get(name) {
Some(item) => item
None => return invalid()
}
result[name] = input_value(child, item, path + "." + name)
}
@scheme.Record(result)
}
"list" =>
@scheme.list(
@wire.array(value).mapi((i, item) => {
input_value(
@wire.field(schema, "item"),
item,
path + "[" + i.to_string() + "]",
)
}),
)
"tuple" => {
let schemas = @wire.array(@wire.field(schema, "items"))
let data = @wire.array(value)
if schemas.length() != data.length() {
return invalid()
}
@scheme.Vector(
schemas.mapi((i, child) => {
input_value(child, data[i], path + "[" + i.to_string() + "]")
}),
)
}
"custom" => {
let fields = @wire.object(value)
if fields.length() != 2 {
return invalid()
}
let tag = @wire.string(@wire.field(value, "constructor"))
let variant = @wire.array(@wire.field(schema, "variants"))
.iter()
.find_first(variant => node_text(variant, "constructor") == tag)
let variant = match variant {
Some(variant) => variant
None => return invalid()
}
let schemas = @wire.array(@wire.field(variant, "arguments"))
let data = @wire.array(@wire.field(value, "arguments"))
if schemas.length() != data.length() {
return invalid()
}
@scheme.Tagged(
tag,
schemas.mapi((i, child) => {
input_value(
child,
data[i],
path + ".arguments[" + i.to_string() + "]",
)
}),
)
}
_ => invalid()
}
}
///|
fn evaluate_model(file : @ir.IRFile, request : Json) -> Json raise {
let entry = @wire.string(@wire.field(request, "entry"))
let definition = match evaluation_definitions(file).get(entry) {
Some(definition) => definition
None => raise @scheme.Unsupported("No local definition for " + entry)
}
let profile = evaluation_profile(definition, evaluation_types(file))
if @wire.field(profile, "supported") != Json::boolean(true) {
raise @scheme.Unsupported(@wire.string(@wire.field(profile, "reason")))
}
let inputs = @wire.array(@wire.field(profile, "inputs"))
let data = match @wire.optional(request, "argumentsIon") {
Some(String(source)) =>
@wire.array(@arguments.ion_arguments(profile, source, false))
_ => @wire.array(@wire.field(request, "arguments"))
}
if inputs.length() != data.length() {
raise @scheme.Invalid(
"Expected " +
inputs.length().to_string() +
" arguments in parameter order",
)
}
let arguments = inputs.mapi((i, input) => {
input_value(
@wire.field(input, "schema"),
data[i],
@wire.string(@wire.field(input, "name")),
)
})
let value = @backend.run_file(file, @ir.FQName::parse(entry), arguments)
@wire.obj([
("success", Json::boolean(true)),
("value", @wire.text(@scheme.value_to_string(value))),
("structured", result_tree(value)),
])
}