///|
/// Local adapter envelopes are private to the worker, not Morphir RPC methods.
/// Decode/migrate imports explicitly; the inspector uses the current semantic model.
pub fn execute_with(engine : @engine.Engine, request : Json) -> Json {
// Where a failure happened, for its reply: reading the request, compiling, building the reply, or reading a model.
let mut stage = "request"
try {
let operation = @wire.string(@wire.field(request, "operation"))
if operation == "evaluate-scheme-execution" {
return @executions.evaluate_scheme_claim(request)
}
if operation == "catalog" {
return catalog(engine)
}
if operation == "import-folder" {
return import_folder(request)
}
if operation == "compile-workspace" || operation == "run-project" {
stage = "compile"
return compile_workspace(engine, request)
}
if operation == "decode-ion" {
stage = "decode"
return decode_ion(request)
}
if operation == "compile" || operation == "run" {
let project = project_request(request, engine.languages())
if operation == "run" && project.language.to_string() != "scheme" {
raise @scheme.Invalid(
"Compile, then evaluate a public function in Model Explorer",
)
}
let target = optional_text(request, "target").unwrap_or("scheme")
stage = "compile"
let engine_run = run_engine(engine, project, target)
stage = "reply"
let result = compile_reply(engine_run, project.language, target) catch {
CompileFailed(reply) => return reply
error => raise error
}
if operation == "run" && engine_run.unit is Some(unit) {
result.push(
(
"value",
@wire.text(
@scheme.value_to_string(
@backend.run_file(
unit.distribution,
@ir.FQName::parse("user:main#main"),
[],
),
),
),
),
)
}
return @wire.obj(result)
}
let source = @wire.string(@wire.field(request, "source"))
let language = match @wire.optional(request, "languageId") {
None => "scheme"
Some(String(language)) => language
_ => raise @scheme.Invalid("This local input language is unavailable")
}
if operation == "worksheet" {
if language != "scheme" {
raise @scheme.Invalid("The local worksheet supports Scheme only")
}
let captured = @worksheet.WorksheetRequest::decode(
@wire.string(@wire.field(request, "worksheetRequest")),
)
if captured.source().text() != source {
raise @protocol.Invalid("worksheet.source_mismatch")
}
let report = captured.evaluate(prepare=runtime => {
prepare_worksheet_project(engine, request, runtime) catch {
@config.Invalid(message) => raise @scheme.Invalid(message)
error => raise error
}
})
return @wire.obj([
("success", Json::boolean(true)),
("worksheetReport", Json::string(report.to_json().stringify())),
])
}
let connected_projection = operation == "inspect-connected-projection"
let imported = if connected_projection {
// Private fixed worker operation. The page adapter admits only nominal
// receiver-owned bytes; the worker independently enforces profile/parser
// bounds and typed support. This changes no ordinary import limit.
let encoded = @wire.string(@wire.field(request, "sourceBase64"))
guard encoded.length() <= 89478488 else {
raise @scheme.Invalid("Projection exceeds the 64-MiB profile limit")
}
let bytes = @base64.decode(encoded[:])
guard @base64.encode(bytes[:]) == encoded else {
raise @scheme.Invalid("Projection bytes must use canonical base64")
}
let limits : @data.Limits = {
max_bytes: 64 * 1024 * 1024,
max_depth: 128,
max_values: 100000,
}
stage = "model"
ignore(@data.recognize_projection_header(bytes, limits))
Some(@data.inspect_model_source(@data.JSON, bytes, limits~))
} else if operation == "inspect" &&
@wire.optional(request, "sourceTree") != None {
stage = "model"
Some(inspect_tree(request))
} else if operation == "inspect" &&
@wire.optional(request, "sourceCodec") != None {
let codec = @wire.string(@wire.field(request, "sourceCodec"))
let format = @data.ModelFormat::from_control(codec)
let encoded = @wire.string(@wire.field(request, "sourceBase64"))
guard encoded.length() <= 22369624 else {
raise @scheme.Invalid("Model import exceeds the 16-MiB limit")
}
let bytes = @base64.decode(encoded[:])
guard bytes.length() <= 16 * 1024 * 1024 else {
raise @scheme.Invalid("Model import exceeds the 16-MiB limit")
}
stage = "model"
Some(
@data.inspect_model_source(format, bytes, limits={
max_bytes: 16 * 1024 * 1024,
max_depth: 128,
max_values: 100000,
}),
)
} else {
None
}
stage = "model"
let (file, source_format_version) = if imported is Some(checkpoint) {
(checkpoint.unit.distribution, checkpoint.original_format_version)
} else if operation == "inspect" || operation == "evaluate" {
let original = @versioned.read(source)
(@migration.to_current(original), original.format_version())
} else {
raise @scheme.Invalid("Unknown operation: " + operation)
}
if operation == "evaluate" {
stage = "evaluate"
if @wire.optional(request, "executionMode") ==
Some(@wire.text("capability")) {
return evaluate_capabilities(file, request)
}
if @wire.optional(request, "admission").unwrap_or(Json::null()) !=
Json::null() {
raise @scheme.Invalid("Checking policy is unavailable for this runtime")
}
if @wire.optional(request, "executionMode") == Some(@wire.text("typed")) {
return evaluate_typed(file, request)
}
return evaluate_model(file, request)
}
let (package_name, distribution_kind) = match file.distribution {
Library(name, _, _) => (name, "Library")
Specs(name, _, _) => (name, "Specification")
Application(name, _, _, _) => (name, "Application")
}
let result = [
("success", Json::boolean(true)),
("ir", @codec.to_json(file)),
("irSource", @wire.text(@codec.to_json(file).stringify())),
("nodes", Json::array(describe(file))),
("evaluations", evaluation_profiles(file)),
("typedEvaluations", typed_profiles(file)),
("capabilityEvaluations", capability_profiles(file)),
("package", @wire.text(package_name.to_canonical())),
("distributionKind", @wire.text(distribution_kind)),
("formatVersion", @wire.text(file.format_version.to_release_string())),
(
"sourceFormatVersion",
@wire.text(source_format_version.to_release_string()),
),
]
if connected_projection {
result.push(
(
"modelSource",
{ "kind": "semantic-projection", "reason": "originals-not-provided" },
),
)
} else if imported is Some(checkpoint) {
let unit = checkpoint.unit
source_ui(result, unit.metadata)
let registry = @data.Registry::new(limits={
max_bytes: 16 * 1024 * 1024,
max_depth: 128,
max_values: 100000,
})
match registry.encode("ion-text", unit) {
Text(source) => result.push(("sourceUnitIon", @wire.text(source)))
Binary(_) => ()
}
}
@wire.obj(result)
} catch {
@ion.Syntax(message) => failure("Ion: " + message, "ion.syntax", stage)
@ion.DataModel(message) =>
failure("Ion: " + message, "ion.data_model", stage)
@ion.Unsupported(message) =>
failure("Ion: " + message, "ion.unsupported", stage)
CompileFailed(reply) => reply
@config.Invalid(message) =>
failure(message, leading_code(message, "workbench.invalid_input"), stage)
@protocol.Invalid(code) => failure(code, code, stage)
@wire.Invalid(code, detail) => codec_failure(stage, code, detail)
@data.Invalid(code, detail) => codec_failure(stage, code, detail)
error => failure(@scheme.error_message(error), scheme_code(error), stage)
}
}
///|
/// A failure reply: the message for a person, a stable code and the stage where it happened.
fn failure(message : String, code : String, stage : String) -> Json {
@wire.obj([
("success", Json::boolean(false)),
("message", @wire.text(if message == "" { code } else { message })),
("code", @wire.text(code)),
("stage", @wire.text(stage)),
])
}
///|
/// The code a message starts with, as in `frontend.invalid_source_path: src/../x`, else `fallback`.
fn leading_code(message : String, fallback : String) -> String {
match message.find(": ") {
Some(colon) => {
let head = message.unsafe_substring(start=0, end=colon)
let code_like = head.contains(".") &&
head
.to_array()
.all(c => {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '.' ||
c == '_' ||
c == '-'
})
if code_like {
head
} else {
fallback
}
}
None => fallback
}
}
///|
fn scheme_code(error : Error) -> String {
match error {
@scheme.Syntax(_, _) => "scheme.syntax"
@scheme.Unbound(_) => "scheme.unbound"
@scheme.Invalid(_) => "scheme.invalid"
@scheme.Domain(code, _) => if code == "" { "scheme.domain" } else { code }
@scheme.Unsupported(_) => "scheme.unsupported"
@scheme.Limit(_) => "scheme.limit"
@scheme.Stage(_, _) => "scheme.stage"
_ => "workbench.error"
}
}
///|
/// A codec error, such as a missing member. In a request or a model it is invalid input; anywhere else the workbench
/// read its own data wrongly, so it is an internal error. The reply never shows a bare key name: it names the stage
/// and keeps the codec's code and detail.
fn codec_failure(stage : String, code : String, detail : String) -> Json {
let (failure_code, summary) = match stage {
"request" => ("workbench.invalid_request", "The request is not valid")
"model" => ("workbench.invalid_model", "The model is not valid")
"compile" =>
("workbench.internal_error", "The workbench failed while compiling")
"reply" =>
(
"workbench.internal_error", "The workbench failed while preparing the compile result",
)
_ =>
(
"workbench.internal_error",
"The workbench failed in the " + stage + " stage",
)
}
@wire.obj([
("success", Json::boolean(false)),
("message", @wire.text(summary + " (" + code + ": " + detail + ")")),
("code", @wire.text(failure_code)),
("stage", @wire.text(stage)),
(
"cause",
@wire.obj([("code", @wire.text(code)), ("detail", @wire.text(detail))]),
),
])
}
///|
/// The local worker's entry: one engine with the built-in providers for each request.
pub fn execute(request : Json) -> Json {
let engine = workbench_engine() catch {
error =>
return @wire.obj([
("success", Json::boolean(false)),
(
"message",
@wire.text("Engine setup failed: " + @scheme.error_message(error)),
),
("code", @wire.text("workbench.engine_setup")),
("stage", @wire.text("setup")),
])
}
execute_with(engine, request)
}