///|
fn Definition::span(self : Definition) -> Span {
match self {
Const(span~, ..)
| Typedef(span~, ..)
| Enum(span~, ..)
| Struct(span~, ..)
| Union(span~, ..)
| Exception(span~, ..)
| Service(span~, ..) => span
}
}
///|
fn diagnostic(
code : String,
message : String,
span : Span,
severity? : Severity = Error,
) -> Diagnostic {
{ code, severity, message, span, }
}
///|
fn definition_names(schema : Schema) -> Array[String] {
schema.definitions.map(definition => definition.name())
}
///|
fn contains_name(names : ArrayView[String], name : String) -> Bool {
names.any(item => item == name)
}
///|
fn TypeRef::walk_named(self : TypeRef, visit : (String) -> Unit) -> Unit {
match self {
Named(name) => visit(name)
List(element) | Set(element) => element.walk_named(visit)
Map(key, value) => {
key.walk_named(visit)
value.walk_named(visit)
}
Base(_) => ()
}
}
///|
fn validate_type(
ty : TypeRef,
names : ArrayView[String],
span : Span,
diagnostics : Array[Diagnostic],
allow_void? : Bool = false,
) -> Unit {
match ty {
Base(Void) if !allow_void =>
diagnostics.push(
diagnostic(
"MTH103", "void is only valid as a service return type", span,
),
)
_ =>
ty.walk_named(name => {
if !contains_name(names, name) && !name.contains(".") {
diagnostics.push(
diagnostic("MTH102", "unresolved type `\{name}`", span),
)
}
})
}
}
///|
fn validate_field_set(
fields : ArrayView[Field],
names : ArrayView[String],
diagnostics : Array[Diagnostic],
union? : Bool = false,
) -> Unit {
for index, field in fields {
validate_type(field.ty, names, field.span, diagnostics)
if field.id == 0 {
diagnostics.push(
diagnostic(
"MTH106",
"field `\{field.name}` uses reserved id 0",
field.span,
),
)
}
if union && field.requiredness is Required {
diagnostics.push(
diagnostic(
"MTH107",
"union field `\{field.name}` cannot be required",
field.span,
),
)
}
for previous in fields[:index] {
if previous.id == field.id {
diagnostics.push(
diagnostic("MTH104", "duplicate field id \{field.id}", field.span),
)
}
if previous.name == field.name {
diagnostics.push(
diagnostic(
"MTH105",
"duplicate field name `\{field.name}`",
field.span,
),
)
}
}
}
}
///|
fn is_service(schema : Schema, name : String) -> Bool {
schema.definitions.any(definition => {
definition is Service(name=actual, ..) && actual == name
})
}
///|
fn validate_service(
schema : Schema,
service_name : String,
extends : String?,
functions : ArrayView[FunctionDef],
names : ArrayView[String],
span : Span,
diagnostics : Array[Diagnostic],
) -> Unit {
if extends is Some(base) && !base.contains(".") && !is_service(schema, base) {
diagnostics.push(
diagnostic(
"MTH111",
"service `\{service_name}` extends unknown service `\{base}`",
span,
),
)
}
for index, function in functions {
validate_type(
function.return_type,
names,
function.span,
diagnostics,
allow_void=true,
)
validate_field_set(function.arguments, names, diagnostics)
validate_field_set(function.throws, names, diagnostics)
if function.oneway {
if function.return_type != Base(Void) {
diagnostics.push(
diagnostic(
"MTH109",
"oneway function `\{function.name}` must return void",
function.span,
),
)
}
if function.throws.length() > 0 {
diagnostics.push(
diagnostic(
"MTH110",
"oneway function `\{function.name}` cannot declare throws",
function.span,
),
)
}
}
for previous in functions[:index] {
if previous.name == function.name {
diagnostics.push(
diagnostic(
"MTH108",
"duplicate service function `\{function.name}`",
function.span,
),
)
}
}
}
}
///|
fn validate_enum(
members : ArrayView[EnumMember],
diagnostics : Array[Diagnostic],
) -> Unit {
for index, current_member in members {
for previous in members[:index] {
if previous.name == current_member.name {
diagnostics.push(
diagnostic(
"MTH112",
"duplicate enum member `\{current_member.name}`",
current_member.span,
),
)
}
if previous.value == current_member.value {
diagnostics.push(
diagnostic(
"MTH113",
"enum value \{current_member.value} is assigned more than once",
current_member.span,
severity=Warning,
),
)
}
}
}
}
///|
/// Performs deterministic semantic checks without file-system access.
///
/// Qualified names are accepted as references into an included schema. The
/// caller can resolve and check include graphs separately.
pub fn check_schema(schema : Schema) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
let names = definition_names(schema)
for index, definition in schema.definitions {
for previous in schema.definitions[:index] {
if previous.name() == definition.name() {
diagnostics.push(
diagnostic(
"MTH101",
"duplicate definition `\{definition.name()}`",
definition.span(),
),
)
}
}
match definition {
Const(ty~, span~, ..) => validate_type(ty, names, span, diagnostics)
Typedef(target~, span~, ..) => {
validate_type(target, names, span, diagnostics)
if target == Named(definition.name()) {
diagnostics.push(
diagnostic("MTH114", "typedef cannot directly name itself", span),
)
}
}
Enum(members~, ..) => validate_enum(members, diagnostics)
Struct(fields~, ..) | Exception(fields~, ..) =>
validate_field_set(fields, names, diagnostics)
Union(fields~, ..) =>
validate_field_set(fields, names, diagnostics, union=true)
Service(name~, extends~, functions~, span~, ..) =>
validate_service(
schema, name, extends, functions, names, span, diagnostics,
)
}
}
diagnostics
}
///|
/// Parses and validates a document in one call.
pub fn compile_idl(
input : String,
source? : String = "",
) -> (Schema, Array[Diagnostic]) raise IdlError {
let schema = parse_idl(input, source~)
(schema, check_schema(schema))
}