///| Semantic checks that can be performed without executing a module. Parsing
///| establishes binary framing; validation relates the independently decoded
///|
/// index spaces and returns all findings instead of stopping at the first one.
pub enum IssueLevel {
ErrorIssue
WarningIssue
} derive(Eq)
///|
pub fn IssueLevel::label(self : IssueLevel) -> String {
match self {
ErrorIssue => "error"
WarningIssue => "warning"
}
}
///|
pub struct ValidationIssue {
level : IssueLevel
rule : String
message : String
}
///|
pub fn ValidationIssue::description(self : ValidationIssue) -> String {
self.level.label() + "[" + self.rule + "]: " + self.message
}
///|
pub struct ValidationReport {
issues : Array[ValidationIssue]
}
///|
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ValidationIssue] {
self.issues
}
///|
pub fn ValidationReport::has_errors(self : ValidationReport) -> Bool {
for issue in self.issues {
if issue.level == ErrorIssue {
return true
}
}
false
}
///|
pub fn ValidationReport::render(self : ValidationReport) -> String {
if self.issues.length() == 0 {
return "validation passed\n"
}
let mut text = "validation findings:\n"
for issue in self.issues {
text = text + " - " + issue.description() + "\n"
}
text
}
///|
fn imported_count(decoded : DecodedModule, kind : ExternalKind) -> Int {
let mut count = 0
for item in decoded.imports() {
if item.kind == kind {
count = count + 1
}
}
count
}
///|
fn push_index_issue(
issues : Array[ValidationIssue],
rule : String,
kind : String,
index : Int,
limit : Int,
) -> Unit {
if index < 0 || index >= limit {
issues.push({
level: ErrorIssue,
rule,
message: kind +
" index " +
index.to_string() +
" is outside 0.." +
limit.to_string(),
})
}
}
///|
fn validate_function_types(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
let type_limit = decoded.types().length()
for type_index in decoded.function_types {
push_index_issue(issues, "function-type", "type", type_index, type_limit)
}
for item in decoded.imports() {
match item.kind {
FunctionExternal =>
match item.type_index {
Some(type_index) =>
push_index_issue(
issues, "import-function-type", "type", type_index, type_limit,
)
None => ()
}
TagExternal =>
match item.type_index {
Some(type_index) =>
push_index_issue(
issues, "import-tag-type", "type", type_index, type_limit,
)
None => ()
}
_ => ()
}
}
for tag in decoded.tags {
push_index_issue(issues, "tag-type", "type", tag.type_index, type_limit)
}
}
///|
fn validate_exports(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
let function_limit = decoded.imported_function_count() +
decoded.defined_function_count()
let table_limit = imported_count(decoded, TableExternal) +
decoded.tables.length()
let memory_limit = imported_count(decoded, MemoryExternal) +
decoded.memories.length()
let global_limit = imported_count(decoded, GlobalExternal) +
decoded.globals.length()
let tag_limit = imported_count(decoded, TagExternal) +
decoded.defined_tag_count()
let names : Array[String] = []
for item in decoded.exports() {
for prior_name in names {
if item.name == prior_name {
issues.push({
level: ErrorIssue,
rule: "export-name",
message: "export name '" + item.name + "' is duplicated",
})
}
}
names.push(item.name)
match item.kind {
FunctionExternal =>
push_index_issue(
issues,
"export-index",
"function",
item.index,
function_limit,
)
TableExternal =>
push_index_issue(
issues,
"export-index",
"table",
item.index,
table_limit,
)
MemoryExternal =>
push_index_issue(
issues,
"export-index",
"memory",
item.index,
memory_limit,
)
GlobalExternal =>
push_index_issue(
issues,
"export-index",
"global",
item.index,
global_limit,
)
TagExternal =>
push_index_issue(issues, "export-index", "tag", item.index, tag_limit)
}
}
}
///|
fn validate_element_segments(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
let table_limit = imported_count(decoded, TableExternal) +
decoded.tables.length()
let function_limit = decoded.imported_function_count() +
decoded.defined_function_count()
for segment in decoded.element_segments {
match segment.mode {
ActiveElement(table, _) =>
push_index_issue(
issues, "element-table-index", "table", table, table_limit,
)
PassiveElement | DeclarativeElement => ()
}
for function in segment.functions {
push_index_issue(
issues, "element-function-index", "function", function, function_limit,
)
}
}
}
///|
fn validate_data_segments(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
let memory_limit = imported_count(decoded, MemoryExternal) +
decoded.memories.length()
match decoded.data_count {
Some(count) if count != decoded.data_segments.length() =>
issues.push({
level: ErrorIssue,
rule: "data-count",
message: "data count section has " +
count.to_string() +
" entries but data section has " +
decoded.data_segments.length().to_string(),
})
_ => ()
}
for segment in decoded.data_segments {
match segment.mode {
ActiveData(memory, _) =>
push_index_issue(
issues, "data-memory-index", "memory", memory, memory_limit,
)
PassiveData => ()
}
}
}
///|
fn validate_start(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
match decoded.start_function {
None => ()
Some(index) => {
let limit = decoded.imported_function_count() +
decoded.defined_function_count()
push_index_issue(issues, "start-index", "function", index, limit)
if index >= decoded.imported_function_count() && index < limit {
let type_slot = index - decoded.imported_function_count()
let type_index = decoded.function_types[type_slot]
if type_index >= 0 && type_index < decoded.types().length() {
let type_ = decoded.types()[type_index]
if type_.params().length() != 0 || type_.results().length() != 0 {
issues.push({
level: ErrorIssue,
rule: "start-signature",
message: "defined start function must have type () -> ()",
})
}
}
}
}
}
}
///|
fn validate_code_bodies(
decoded : DecodedModule,
issues : Array[ValidationIssue],
) -> Unit {
if decoded.code_bodies.length() != decoded.defined_function_count() {
issues.push({
level: ErrorIssue,
rule: "function-code-count",
message: "function and code sections have different entry counts",
})
}
for body in decoded.code_bodies {
if body.instruction_span.size() == 0 {
issues.push({
level: ErrorIssue,
rule: "function-body",
message: "function body has no terminating instruction sequence",
})
}
}
}
///|
pub fn validate(decoded : DecodedModule) -> ValidationReport {
let issues : Array[ValidationIssue] = []
validate_function_types(decoded, issues)
validate_exports(decoded, issues)
validate_start(decoded, issues)
validate_code_bodies(decoded, issues)
validate_element_segments(decoded, issues)
validate_data_segments(decoded, issues)
if decoded.memories.length() > 1 {
issues.push({
level: WarningIssue,
rule: "multi-memory",
message: "multiple memories require consumers to support the multi-memory proposal",
})
}
{ issues, }
}