///|
/// Provides source text for a normalized logical Thrift path.
///
/// Returning `None` reports a source-not-found diagnostic. The loader remains
/// caller-owned so this package stays portable across every stable backend.
pub type SourceLoader = (String) -> String?
///|
/// A recursively loaded set of Thrift documents rooted at one entry schema.
/// Documents are ordered deterministically with the root first, followed by
/// includes in source order. Each normalized path is loaded at most once.
pub(all) struct SchemaWorkspace {
root : String
documents : Array[Schema]
} derive(Debug, Eq)
///|
fn normalize_schema_path(path : String) -> String {
let portable = path.replace_all(old="\\", new="/")
let absolute = portable.has_prefix("/")
let parts : Array[String] = []
for raw_part in portable.split("/") {
let part = raw_part.to_owned()
match part {
"" | "." => ()
".." =>
match parts.last() {
Some(previous) if previous != ".." => ignore(parts.pop())
_ => parts.push(part)
}
_ => parts.push(part)
}
}
let normalized = parts.join("/")
if absolute {
"/\{normalized}"
} else {
normalized
}
}
///|
fn schema_parent(path : String) -> String {
match path.rev_split_once("/") {
Some((parent, _)) => parent.to_owned()
None => ""
}
}
///|
fn resolve_include_path(importer : String, include_path : String) -> String {
let parent = schema_parent(importer)
normalize_schema_path(
if parent == "" {
include_path
} else {
"\{parent}/\{include_path}"
},
)
}
///|
fn include_alias(path : String) -> String {
let normalized = normalize_schema_path(path)
let filename = normalized
.split("/")
.last()
.unwrap_or(normalized[:])
.to_owned()
filename.strip_suffix(".thrift").unwrap_or(filename[:]).to_owned()
}
///|
fn find_document(documents : ArrayView[Schema], path : String) -> Schema? {
for document in documents {
if document.source == path {
return Some(document)
}
}
None
}
///|
fn push_workspace_diagnostic(
diagnostics : Array[Diagnostic],
code : String,
message : String,
span : Span,
) -> Unit {
diagnostics.push(diagnostic(code, message, span))
}
///|
fn load_workspace_document(
path : String,
requested_at : Span,
loader : SourceLoader,
documents : Array[Schema],
active : Array[String],
diagnostics : Array[Diagnostic],
) -> Unit raise IdlError {
let normalized = normalize_schema_path(path)
if active.contains(normalized) {
let chain = active.copy()
chain.push(normalized)
push_workspace_diagnostic(
diagnostics,
"MTH202",
"include cycle detected: \{chain.join(" -> ")}",
requested_at,
)
return
}
if find_document(documents, normalized) is Some(_) {
return
}
guard loader(normalized) is Some(input) else {
push_workspace_diagnostic(
diagnostics,
"MTH201",
"included source `\{normalized}` was not found",
requested_at,
)
return
}
let schema = parse_idl(input, source=normalized)
documents.push(schema)
active.push(normalized)
for header in schema.headers {
match header {
Include(path=child, span~) =>
load_workspace_document(
resolve_include_path(normalized, child),
span,
loader,
documents,
active,
diagnostics,
)
_ => ()
}
}
ignore(active.pop())
}
///|
fn direct_include_aliases(
schema : Schema,
diagnostics : Array[Diagnostic],
) -> Map[String, String] {
let aliases : Map[String, String] = Map([])
for header in schema.headers {
match header {
Include(path~, span~) => {
let include_name = include_alias(path)
let target = resolve_include_path(schema.source, path)
match aliases.get(include_name) {
Some(previous) if previous != target =>
push_workspace_diagnostic(
diagnostics,
"MTH203",
"include alias `\{include_name}` refers to both `\{previous}` and `\{target}`",
span,
)
_ => aliases[include_name] = target
}
}
_ => ()
}
}
aliases
}
///|
fn is_schema_type(definition : Definition) -> Bool {
match definition {
Typedef(..) | Enum(..) | Struct(..) | Union(..) | Exception(..) => true
Const(..) | Service(..) => false
}
}
///|
fn validate_qualified_reference(
name : String,
span : Span,
aliases : Map[String, String],
documents : ArrayView[Schema],
diagnostics : Array[Diagnostic],
service? : Bool = false,
) -> Unit {
guard name.split_once(".") is Some((alias_view, symbol_view)) else { return }
let include_name = alias_view.to_owned()
let symbol = symbol_view.to_owned()
guard aliases.get(include_name) is Some(target) else {
push_workspace_diagnostic(
diagnostics,
"MTH204",
"qualified reference `\{name}` uses unknown include alias `\{include_name}`",
span,
)
return
}
guard find_document(documents, target) is Some(target_schema) else { return }
guard find_definition(target_schema, symbol) is Some(definition) else {
push_workspace_diagnostic(
diagnostics,
"MTH205",
"included schema `\{target}` has no definition `\{symbol}`",
span,
)
return
}
let valid = if service {
definition is Service(..)
} else {
is_schema_type(definition)
}
if !valid {
push_workspace_diagnostic(
diagnostics,
"MTH206",
if service {
"qualified service `\{name}` does not name a service"
} else {
"qualified type `\{name}` does not name a Thrift type"
},
span,
)
}
}
///|
fn validate_workspace_type(
ty : TypeRef,
span : Span,
aliases : Map[String, String],
documents : ArrayView[Schema],
diagnostics : Array[Diagnostic],
) -> Unit {
ty.walk_named(name => {
if name.contains(".") {
validate_qualified_reference(name, span, aliases, documents, diagnostics)
}
})
}
///|
fn validate_workspace_fields(
fields : ArrayView[Field],
aliases : Map[String, String],
documents : ArrayView[Schema],
diagnostics : Array[Diagnostic],
) -> Unit {
for field in fields {
validate_workspace_type(
field.ty,
field.span,
aliases,
documents,
diagnostics,
)
}
}
///|
fn definition_reference(
schema : Schema,
name : String,
documents : ArrayView[Schema],
) -> (Schema, Definition)? {
match name.split_once(".") {
None =>
find_definition(schema, name).map(definition => (schema, definition))
Some((include_view, symbol_view)) => {
let include_name = include_view.to_owned()
let symbol = symbol_view.to_owned()
for header in schema.headers {
match header {
Include(path~, ..) if include_alias(path) == include_name => {
let target = resolve_include_path(schema.source, path)
guard find_document(documents, target) is Some(target_schema) else {
return None
}
return find_definition(target_schema, symbol).map(definition => {
(target_schema, definition)
})
}
_ => ()
}
}
None
}
}
}
///|
fn definition_key(schema : Schema, definition : Definition) -> String {
"\{schema.source}#\{definition.name()}"
}
///|
fn check_typedef_cycle_from(
schema : Schema,
definition : Definition,
documents : ArrayView[Schema],
active : Array[String],
diagnostics : Array[Diagnostic],
reported : Array[String],
) -> Bool {
guard definition is Typedef(target~, span~, ..) else { return false }
let key = definition_key(schema, definition)
if active.contains(key) {
if !reported.contains(key) {
let chain = active.copy()
chain.push(key)
push_workspace_diagnostic(
diagnostics,
"MTH207",
"typedef cycle detected: \{chain.join(" -> ")}",
span,
)
reported.push(key)
}
return true
}
active.push(key)
let mut found = false
target.walk_named(name => {
if !found {
match definition_reference(schema, name, documents) {
Some((target_schema, target_definition)) if target_definition
is Typedef(..) =>
found = check_typedef_cycle_from(
target_schema, target_definition, documents, active, diagnostics, reported,
)
_ => ()
}
}
})
ignore(active.pop())
found
}
///|
fn check_service_cycle_from(
schema : Schema,
definition : Definition,
documents : ArrayView[Schema],
active : Array[String],
diagnostics : Array[Diagnostic],
reported : Array[String],
) -> Bool {
guard definition is Service(extends~, span~, ..) else { return false }
let key = definition_key(schema, definition)
if active.contains(key) {
if !reported.contains(key) {
let chain = active.copy()
chain.push(key)
push_workspace_diagnostic(
diagnostics,
"MTH208",
"service inheritance cycle detected: \{chain.join(" -> ")}",
span,
)
reported.push(key)
}
return true
}
active.push(key)
let found = match extends {
None => false
Some(base) =>
match definition_reference(schema, base, documents) {
Some((base_schema, base_definition)) if base_definition is Service(..) =>
check_service_cycle_from(
base_schema, base_definition, documents, active, diagnostics, reported,
)
_ => false
}
}
ignore(active.pop())
found
}
///|
fn validate_workspace_cycles(
documents : ArrayView[Schema],
diagnostics : Array[Diagnostic],
) -> Unit {
let typedef_reports : Array[String] = []
let service_reports : Array[String] = []
for schema in documents {
for definition in schema.definitions {
match definition {
Typedef(..) =>
ignore(
check_typedef_cycle_from(
schema,
definition,
documents,
[],
diagnostics,
typedef_reports,
),
)
Service(..) =>
ignore(
check_service_cycle_from(
schema,
definition,
documents,
[],
diagnostics,
service_reports,
),
)
_ => ()
}
}
}
}
///|
fn validate_workspace_schema(
schema : Schema,
documents : ArrayView[Schema],
diagnostics : Array[Diagnostic],
) -> Unit {
diagnostics.append(check_schema(schema))
let aliases = direct_include_aliases(schema, diagnostics)
for definition in schema.definitions {
match definition {
Const(ty~, span~, ..) | Typedef(target=ty, span~, ..) =>
validate_workspace_type(ty, span, aliases, documents, diagnostics)
Enum(..) => ()
Struct(fields~, ..) | Union(fields~, ..) | Exception(fields~, ..) =>
validate_workspace_fields(fields, aliases, documents, diagnostics)
Service(extends~, functions~, span~, ..) => {
if extends is Some(base) && base.contains(".") {
validate_qualified_reference(
base,
span,
aliases,
documents,
diagnostics,
service=true,
)
}
for function in functions {
validate_workspace_type(
function.return_type,
function.span,
aliases,
documents,
diagnostics,
)
validate_workspace_fields(
function.arguments,
aliases,
documents,
diagnostics,
)
validate_workspace_fields(
function.throws,
aliases,
documents,
diagnostics,
)
}
}
}
}
}
///|
/// Loads, parses, and validates a complete Thrift include graph.
///
/// Paths passed to `loader` use `/`, contain no `.` segments, and resolve `..`
/// relative to the including document. Missing sources and link failures are
/// returned as diagnostics; lexical and syntactic failures raise `IdlError`.
pub fn compile_workspace(
root : String,
loader : SourceLoader,
) -> (SchemaWorkspace, Array[Diagnostic]) raise IdlError {
let normalized_root = normalize_schema_path(root)
let documents : Array[Schema] = []
let diagnostics : Array[Diagnostic] = []
load_workspace_document(
normalized_root,
Span::synthetic(source=normalized_root),
loader,
documents,
[],
diagnostics,
)
for schema in documents {
validate_workspace_schema(schema, documents, diagnostics)
}
validate_workspace_cycles(documents, diagnostics)
({ root: normalized_root, documents, }, diagnostics)
}