///|
/// Evidence for local source claims, separate from ecosystem MCK verification.
pub(all) enum SourceEvidence {
Declared
LocalAcceptance
}
///|
pub fn SourceEvidence::name(self : SourceEvidence) -> String {
match self {
Declared => "declared"
LocalAcceptance => "local-acceptance"
}
}
///|
/// Every source file of one language in a project, for a `PerProject` profile. `package_name` and
/// `exposed_modules` are as written in the project configuration (`project.name` and `project.exposed_modules`);
/// `exposed_modules` is empty when the key is missing. The sources are in path order.
pub(all) struct ProjectSources {
package_name : @ids.SourcePackageName
exposed_modules : Array[@ids.SourceModuleName]
sources : Array[Source]
}
///|
/// How a source profile compiles. `PerFile` compiles each source file to its own unit; the callback gets the
/// source, the unit id and the cancellation check. `PerProject` compiles every source file of a project to one
/// unit, in one call; the unit id is the canonical project name.
pub(all) enum SourceCompile {
PerFile((Source, String, () -> Bool) -> @data.IRUnit raise)
PerProject((ProjectSources, String, () -> Bool) -> @data.IRUnit raise)
}
///|
/// `max_documents` is the number of source documents one unit can hold; `None` means no limit.
/// Source behavior descriptions are not an IR feature vocabulary or wire protocol.
pub(all) struct SourceCapabilities {
summary : String
supported : Array[String]
excluded : Array[String]
max_documents : Int32?
incremental : Bool
limits : Map[String, Int32]
}
///|
pub(all) struct SourceProfile {
id : @ids.ProfileId
capabilities : SourceCapabilities
evidence : SourceEvidence
dependencies : Map[String, String]
compile : SourceCompile
unit_limits : @data.Limits
}
///|
pub(all) struct SourceLanguage {
id : @ids.LanguageId
extensions : Array[@ids.FileExtension]
default_profile : @ids.ProfileId
profiles : Array[SourceProfile]
}
///|
pub(all) struct SourceProvider {
id : @ids.ProviderId
languages : Array[SourceLanguage]
}
///|
fn source_constraint(text : String, kind : String) -> String raise {
if text == "" ||
text.length() > 256 ||
text.to_array().any(c => c.to_int() < 32 || c.to_int() == 127) {
raise @configuration.Invalid(
"frontend.invalid_" + kind + ": Expected a bounded nonempty identifier",
)
}
text
}
///|
fn SourceCapabilities::snapshot(
self : SourceCapabilities,
) -> SourceCapabilities {
{
..self,
supported: self.supported.copy(),
excluded: self.excluded.copy(),
limits: self.limits.copy(),
}
}
///|
/// Explicit in-process claims; registration and planning never invoke compilers.
pub fn Engine::register_source_provider(
self : Engine,
provider : SourceProvider,
) -> Unit raise {
let provider_id = provider.id.to_string()
if self.source_providers.contains(provider_id) ||
self.frontends.contains(provider_id) {
raise @configuration.Invalid(
"frontend.registration_conflict: " + provider_id,
)
}
if provider.languages.is_empty() || provider.languages.length() > 64 {
raise @configuration.Invalid("frontend.invalid_languages: " + provider_id)
}
let languages = []
let seen_languages : Map[@ids.LanguageId, Bool] = Map([])
for language in provider.languages {
let language_id = language.id.to_string()
if seen_languages.contains(language.id) ||
self.data_formats.contains(language_id) ||
language_id == "ir-json" {
raise @configuration.Invalid("frontend.invalid_language: " + language_id)
}
seen_languages[language.id] = true
if language.extensions.is_empty() || language.extensions.length() > 64 {
raise @configuration.Invalid(
"frontend.invalid_extensions: " + language_id,
)
}
if language.profiles.is_empty() || language.profiles.length() > 64 {
raise @configuration.Invalid("frontend.invalid_profiles: " + provider_id)
}
let seen : Map[@ids.ProfileId, Bool] = Map([])
let profiles = []
for profile in language.profiles {
if seen.contains(profile.id) {
raise @configuration.Invalid(
"frontend.duplicate_profile: " + profile.id.to_string(),
)
}
seen[profile.id] = true
let c = profile.capabilities
ignore(source_constraint(c.summary, "capabilities"))
// No limit is the default. A declared limit is a positive number, with no upper bound.
let documents_valid = match c.max_documents {
Some(n) => n > 0
None => true
}
if !documents_valid ||
c.supported.length() > 64 ||
c.excluded.length() > 64 ||
c.limits.length() > 64 ||
profile.dependencies.length() > 64 {
raise @configuration.Invalid(
"frontend.invalid_capabilities: " + profile.id.to_string(),
)
}
for text in c.supported + c.excluded {
ignore(source_constraint(text, "capabilities"))
}
for key, value in c.limits {
ignore(source_constraint(key, "limits"))
if value < 0 {
raise @configuration.Invalid(
"frontend.invalid_limits: " + profile.id.to_string(),
)
}
}
for key, value in profile.dependencies {
ignore(source_constraint(key, "dependency"))
ignore(source_constraint(value, "dependency"))
}
let limits = profile.unit_limits
if limits.max_bytes <= 0 || limits.max_depth < 0 || limits.max_values <= 0 {
raise @configuration.Invalid(
"frontend.invalid_limits: " + profile.id.to_string(),
)
}
profiles.push({
..profile,
capabilities: c.snapshot(),
dependencies: profile.dependencies.copy(),
})
}
if !seen.contains(language.default_profile) {
raise @configuration.Invalid(
"frontend.unknown_default_profile: " +
language.default_profile.to_string(),
)
}
languages.push({
..language,
profiles,
extensions: language.extensions.copy(),
})
}
self.source_providers[provider_id] = { ..provider, languages, }
}
///|
fn Engine::select_source_frontend(
self : Engine,
settings : Config,
) -> Frontend? raise {
let language = settings.frontend
let legacy = self.frontends.get(language)
let offers : Map[String, SourceLanguage] = Map([])
for id, provider in self.source_providers {
for offered in provider.languages {
if offered.id.to_string() == language {
offers[id] = offered
}
}
}
let ids = offers.keys().collect()
if legacy is Some(frontend) {
if self.source_providers.contains(frontend.provider) {
raise @configuration.Invalid(
"frontend.registration_conflict: " + frontend.provider,
)
}
ids.push(frontend.provider)
}
ids.sort()
let id = match settings.frontend_provider {
Some(wanted) => {
if !ids.contains(wanted) {
let code = if self.source_providers.contains(wanted) ||
self.frontends.contains(wanted) {
"provider_language_mismatch"
} else {
"unknown_provider"
}
raise @configuration.Invalid(
"frontend." +
code +
": " +
wanted +
" for language " +
language +
"; available " +
ids.join(", "),
)
}
wanted
}
None => {
if ids.is_empty() {
return None
}
if ids.length() != 1 {
raise @configuration.Invalid(
"frontend.ambiguous_provider: " +
language +
"; choose " +
ids.join(", "),
)
}
ids[0]
}
}
if offers.get(id) is Some(offered) {
let available = offered.profiles.map(p => p.id.to_string())
available.sort()
let wanted = settings.frontend_profile.unwrap_or(
offered.default_profile.to_string(),
)
let profiles = offered.profiles.filter(p => p.id.to_string() == wanted)
if profiles.is_empty() {
raise @configuration.Invalid(
"frontend.unknown_profile: " +
wanted +
" for provider " +
id +
" and language " +
language +
"; available " +
available.join(", "),
)
}
let profile = profiles[0]
return Some({
extensions: offered.extensions.map(e => e.to_string()),
compiler: match profile.compile {
PerFile(run) => UnitCompiler(run, profile.unit_limits)
PerProject(run) => ProjectCompiler(run, profile.unit_limits)
},
provider: id,
profile: Some(profile.id.to_string()),
default_profile: profile.id == offered.default_profile,
available_profiles: available,
capabilities: Some(profile.capabilities.snapshot()),
dependencies: profile.dependencies.copy(),
evidence: profile.evidence,
})
}
let frontend = legacy.unwrap()
if settings.frontend_profile is Some(wanted) &&
frontend.profile != Some(wanted) {
raise @configuration.Invalid(
"frontend.unknown_profile: " +
wanted +
" for provider " +
id +
"; available " +
frontend.available_profiles.join(", "),
)
}
Some(frontend)
}