///|
/// A frontend failure, as the reply the worker returns.
priv suberror CompileFailed {
CompileFailed(Json)
}
///|
priv struct EngineRun {
unit : @data.IRUnit?
report : @engine.ContentReport
selection : @engine.SourceFrontendSelection?
}
///|
fn backend_for(target : String) -> String raise {
match target {
"" => "checkpoint"
"scheme" | "elm" | "moonbit" => target
_ =>
raise @wire.Invalid(
"workbench.unavailable_target",
"This generation target is unavailable: " + target,
)
}
}
///|
/// One compile of `project` through the engine. The unit comes from `on_unit`, so the model is there even when
/// generation fails.
fn run_engine(
engine : @engine.Engine,
project : ProjectRequest,
target : String,
) -> EngineRun raise {
let tree = project_tree(project)
let host = @engine.Host::memory(tree.files)
let request = {
..@engine.Request::new(),
overlay: overlay(project, backend_for(target)),
}
let plan = engine.plan(host, @workspace.discover(tree), request)
let selection = plan.source_frontends().get(0)
let units : Array[@data.IRUnit] = []
// A pipe cannot feed `plan` here: it is the first argument of a method of another receiver.
let report = engine.execute_content(
plan,
fn(path, _) raise { @data.Text((host.read)(path)) },
on_unit=compiled => units.push(compiled.unit),
)
{ unit: units.get(0), report, selection, }
}
///|
fn coordinate_profile(language : @ids.LanguageId) -> String {
match language.to_string() {
"moonbit" => "moonbit-parser-position-v1"
"elm" => "elm-source-position-v1"
_ => "morphir-source-location-v1"
}
}
///|
fn location_json(location : @ir.SourceLocation?) -> Json {
match location {
Some(loc) =>
@wire.obj([
("startLine", Json::number(loc.start_line.to_double())),
("startColumn", Json::number(loc.start_column.to_double())),
("endLine", Json::number(loc.end_line.to_double())),
("endColumn", Json::number(loc.end_column.to_double())),
])
None => Json::null()
}
}
///|
fn problem_detail(diagnostic : @engine.Diagnostic) -> String {
match diagnostic.problem {
Some(Source(p)) => p.detail
Some(Generation(p)) => p.detail
None => diagnostic.message
}
}
///|
fn problem_json(diagnostic : @engine.Diagnostic, profile : String) -> Json {
let (code, detail) = match diagnostic.problem {
Some(Source(p)) => (p.code, p.detail)
Some(Generation(p)) => (p.code, p.detail)
None => ("", diagnostic.message)
}
@wire.obj([
("code", @wire.text(code)),
("coordinateProfile", @wire.text(profile)),
("detail", @wire.text(detail)),
("path", @wire.text(diagnostic.path)),
("source", location_json(diagnostic.location())),
("stage", @wire.text(diagnostic.stage)),
])
}
///|
/// The failure reply of a compile that produced no unit. Problems are grouped by file, in report order.
fn frontend_failure(
diagnostics : Array[@engine.Diagnostic],
language : @ids.LanguageId,
) -> Json {
let profile = coordinate_profile(language)
guard diagnostics is [first, ..] else {
return @wire.obj([
("success", Json::boolean(false)),
("message", @wire.text("The engine compiled no unit")),
("code", @wire.text("workbench.no_unit")),
("stage", @wire.text("frontend")),
])
}
let files : Array[(String, Array[Json])] = []
for diagnostic in diagnostics {
match files.search_by(entry => entry.0 == diagnostic.path) {
Some(i) => files[i].1.push(problem_json(diagnostic, profile))
None => files.push((diagnostic.path, [problem_json(diagnostic, profile)]))
}
}
let message = match first.code() {
Some(code) => code + ": " + problem_detail(first)
None => first.message
}
@wire.obj([
("success", Json::boolean(false)),
("message", @wire.text(message)),
// The first problem's engine code, so a failure always names one.
("code", @wire.text(first.code().unwrap_or("workbench.compile_failed"))),
(
"stage",
@wire.text(if first.stage == "" { "frontend" } else { first.stage }),
),
("frontendDiagnostic", problem_json(first, profile)),
(
"frontendDiagnostics",
Json::array(
files.map(entry => {
@wire.obj([
("path", @wire.text(entry.0)),
("problems", Json::array(entry.1)),
])
}),
),
),
])
}
///|
fn target_label(target : String) -> String {
match target {
"elm" => "Elm"
"moonbit" => "MoonBit"
_ => "Scheme"
}
}
///|
/// The generated files of `report`, with paths relative to the generated project root.
fn artifacts_json(report : @engine.ContentReport) -> Json {
let prefix = match report.projects.get(0) {
Some(project) => project.root + "/"
None => ""
}
Json::array(
report.artifacts.map(artifact => {
let path = match artifact.path.strip_prefix(prefix) {
Some(rest) => rest.to_owned()
None => artifact.path
}
let (content, binary, encoding) = match artifact.content {
Text(text) => (text, false, "utf-8")
Binary(bytes) => (@base64.encode(bytes[:]), true, "base64")
}
@wire.obj([
("path", @wire.text(path)),
("content", @wire.text(content)),
("binary", Json::boolean(binary)),
("encoding", @wire.text(encoding)),
])
}),
)
}
///|
/// The generation fields of a reply: artifacts and warnings, or the refusal with its problems.
fn generation_fields(
report : @engine.ContentReport,
target : String,
) -> Array[(String, Json)] {
let refused = report.diagnostics.filter(d => d.stage == "backend")
if !refused.is_empty() {
let problems = refused.map(d => {
match d.problem {
Some(Generation(p)) =>
@wire.obj([
("code", @wire.text(p.code)),
("nodePath", @wire.text(p.node_path)),
("detail", @wire.text(p.detail)),
])
_ =>
@wire.obj([
("code", @wire.text("")),
("nodePath", @wire.text("")),
("detail", @wire.text(d.message)),
])
}
})
return [
("artifacts", Json::array([])),
("generationDiagnostics", Json::array(problems)),
(
"generationMessage",
@wire.text(
target_label(target) +
" generation failed: " +
refused.map(d => d.message).join("; "),
),
),
]
}
let fields = [("artifacts", artifacts_json(report))]
let source = report.artifacts.filter(a => {
a.format == "elm-source" || a.format == "scheme"
})
if target != "moonbit" && source is [first, ..] && first.content is Text(text) {
fields.push(("generated", @wire.text(text)))
}
let warnings = report.warnings
.filter(w => w.stage != "frontend")
.map(w => {
match w.details {
Some(warning) => naming_warning(warning)
None => @wire.text(w.message)
}
})
fields.push(("generationWarnings", Json::array(warnings)))
fields
}
///|
fn compile_reply(
run : EngineRun,
language : @ids.LanguageId,
target : String,
) -> Array[(String, Json)] raise {
// A rejected file fails the compile even when another file produced a unit.
let rejected = run.report.diagnostics.filter(d => d.stage != "backend")
guard run.unit is Some(unit) && rejected.is_empty() else {
raise CompileFailed(
frontend_failure(
if rejected.is_empty() {
run.report.diagnostics
} else {
rejected
},
language,
),
)
}
let file = unit.distribution
let (package_name, distribution_kind) = match file.distribution {
Library(name, _, _) => (name, "Library")
Specs(name, _, _) => (name, "Specification")
Application(name, _, _, _) => (name, "Application")
}
let typed = typed_profiles(file)
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),
("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())),
]
if run.selection is Some(selection) && selection.profile is Some(profile) {
source_ui(result, unit.metadata)
result.push(("sourceWarnings", source_warnings(unit.provenance)))
match @data.Registry::new().encode("ion-text", unit) {
Text(text) => result.push(("sourceUnitIon", @wire.text(text)))
Binary(_) => ()
}
result.push(("sourceProfile", @wire.text(profile)))
result.push(("sourceProvider", @wire.text(selection.provider)))
// A module whose types typed evaluation cannot take still compiles: its manifest holds a `reason` in place of
// `entries`, Model Explorer shows that reason, and no entry is typed.
let entries : Array[String] = match typed {
Object(fields) =>
match fields.get("entries") {
Some(Object(entries)) => entries.keys().collect()
_ => []
}
_ => []
}
entries.sort()
result.push(("sourceEntries", Json::array(entries.map(@wire.text))))
}
if target != "" {
result.append(generation_fields(run.report, target))
}
result
}