use serde_json::Value;
use super::error::{HirError, invalid};
use super::types::{
Declaration, Expr, PROTOCOL_MAJOR, PROTOCOL_NAME, Position, Program, Rule, RuleEntry, Settings,
SettingsNode, Span, Stmt, SwitchArm, default_var_index,
};
use super::visit::{self, Visitor};
const DECLARATION_KINDS: &[&str] = &[
"globalVariable",
"playerVariable",
"subroutine",
"constant",
"macro",
];
const STMT_KINDS: &[&str] = &[
"expr",
"assign",
"if",
"for",
"while",
"doWhile",
"switch",
"delete",
"break",
"return",
"continue",
"goto",
"label",
"callSubroutine",
"pass",
];
const EXPR_KINDS: &[&str] = &[
"number",
"string",
"bool",
"null",
"array",
"dict",
"comprehension",
"lambda",
"stringModifier",
"local",
"vector",
"enum",
"globalVar",
"playerVar",
"member",
"eventPlayer",
"hostPlayer",
"constant",
"call",
"receiverCall",
"macroCall",
"macroParam",
"type",
"binary",
"conditional",
"unary",
"index",
"format",
];
const SETTINGS_NODE_KINDS: &[&str] = &["group", "number", "bool", "string", "raw", "list"];
struct NameTables<'a> {
globals: Vec<&'a str>,
players: Vec<&'a str>,
subroutines: Vec<&'a str>,
constants: Vec<&'a str>,
}
pub(crate) fn check_unknown_kinds(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
if let Some(declarations) = object.get("declarations").and_then(Value::as_array) {
for declaration in declarations {
check_declaration(declaration)?;
}
}
if let Some(rules) = object.get("rules").and_then(Value::as_array) {
for rule in rules {
check_rule_entry(rule)?;
}
}
if let Some(settings) = object.get("settings") {
check_settings_value(settings)?;
}
Ok(())
}
pub(crate) fn check_envelope(value: &Value) -> Result<(), HirError> {
let name = value
.get("protocol")
.and_then(|p| p.get("name"))
.and_then(Value::as_str)
.unwrap_or("");
let version = value
.get("protocol")
.and_then(|p| p.get("version"))
.and_then(Value::as_str)
.unwrap_or("");
let received = if name.is_empty() && version.is_empty() {
"<missing protocol>".to_string()
} else {
format!("{name}@{version}")
};
let expected = format!("{PROTOCOL_NAME}@v{PROTOCOL_MAJOR}");
if name != PROTOCOL_NAME {
return Err(HirError::IncompatibleProtocol { expected, received });
}
let major = version
.split('.')
.next()
.and_then(|part| part.parse::<u32>().ok())
.ok_or_else(|| HirError::IncompatibleProtocol {
expected: expected.clone(),
received: received.clone(),
})?;
if major != PROTOCOL_MAJOR {
return Err(HirError::IncompatibleProtocol { expected, received });
}
Ok(())
}
pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> {
let mut seen_files = std::collections::HashSet::new();
for file in &program.files {
if !seen_files.insert(file.id) {
return Err(invalid(
"invalid-identifier",
format!("duplicate file id {}", file.id),
None,
));
}
}
let mut tables = NameTables {
globals: Vec::new(),
players: Vec::new(),
subroutines: Vec::new(),
constants: Vec::new(),
};
let mut macro_names: Vec<&str> = Vec::new();
let mut define_names: Vec<&str> = Vec::new();
for declaration in &program.declarations {
match declaration {
Declaration::GlobalVariable {
name,
span,
initializer,
..
} => {
check_name(name, "global variable", *span)?;
check_unique(name, "global variable", *span, &mut tables.globals)?;
tables.globals.push(name);
if let Some(initializer) = initializer {
validate_exprs(std::iter::once(initializer.as_ref()), program, &tables)?;
}
}
Declaration::PlayerVariable {
name,
span,
initializer,
..
} => {
check_name(name, "player variable", *span)?;
check_unique(name, "player variable", *span, &mut tables.players)?;
tables.players.push(name);
if let Some(initializer) = initializer {
validate_exprs(std::iter::once(initializer.as_ref()), program, &tables)?;
}
}
Declaration::Subroutine { name, span, .. } => {
check_name(name, "subroutine", *span)?;
check_unique(name, "subroutine", *span, &mut tables.subroutines)?;
tables.subroutines.push(name);
}
Declaration::Constant { name, span, value } => {
check_name(name, "constant", *span)?;
check_unique(name, "constant", *span, &mut tables.constants)?;
tables.constants.push(name);
validate_exprs(std::iter::once(value.as_ref()), program, &tables)?;
}
Declaration::Macro {
name,
args,
span,
body,
} => {
check_name(name, "macro", *span)?;
check_unique(name, "macro", *span, &mut macro_names)?;
for arg in args {
if arg.is_empty() {
return Err(invalid(
"invalid-identifier",
"macro argument name must be non-empty",
*span,
));
}
}
validate_stmts(body, program, &tables)?;
}
}
}
for define in &program.defines {
if define.name.is_empty() {
return Err(invalid(
"invalid-identifier",
"define name must be non-empty",
define.span,
));
}
check_span(define.span, program.files.len())?;
if define_names.contains(&define.name.as_str()) {
return Err(invalid(
"invalid-identifier",
format!("duplicate define name '{}'", define.name),
define.span,
));
}
define_names.push(&define.name);
}
for entry in &program.rules {
match entry {
RuleEntry::Rule(rule) => validate_rule(rule, program, &tables)?,
RuleEntry::SubroutineDef {
name,
source_name,
span,
body,
..
} => {
check_name(name, "subroutine definition", *span)?;
if !source_name.is_empty() {
check_name(source_name, "subroutine source", *span)?;
}
validate_stmts(body, program, &tables)?;
}
}
}
if let Some(settings) = &program.settings {
validate_settings(program, settings)?;
}
Ok(())
}
fn validate_settings(program: &Program, settings: &Settings) -> Result<(), HirError> {
check_span(settings.span, program.files.len())?;
let mut has_gamemodes = false;
for child in &settings.children {
let SettingsNode::Group { name, .. } = child else {
return Err(invalid(
"settings-invalid",
"settings block children must be groups",
child.span().copied(),
));
};
if name == "gamemodes" {
has_gamemodes = true;
}
validate_settings_node(program, child)?;
}
if !has_gamemodes {
return Err(invalid(
"settings-invalid",
"settings block must contain a gamemodes group",
settings.span,
));
}
Ok(())
}
fn validate_settings_node(program: &Program, node: &SettingsNode) -> Result<(), HirError> {
check_span(node.span().copied(), program.files.len())?;
check_key(node_name(node), node.span().copied())?;
if let SettingsNode::Group { children, .. } = node {
for child in children {
validate_settings_node(program, child)?;
}
}
Ok(())
}
fn node_name(node: &SettingsNode) -> &str {
match node {
SettingsNode::Group { name, .. }
| SettingsNode::Number { name, .. }
| SettingsNode::Bool { name, .. }
| SettingsNode::String { name, .. }
| SettingsNode::Raw { name, .. }
| SettingsNode::List { name, .. } => name,
}
}
fn check_key(name: &str, span: Option<Span>) -> Result<(), HirError> {
if name.is_empty() {
return Err(invalid(
"invalid-identifier",
"settings key must be non-empty",
span,
));
}
Ok(())
}
fn validate_rule(rule: &Rule, program: &Program, tables: &NameTables<'_>) -> Result<(), HirError> {
check_span(rule.span, program.files.len())?;
if rule.event.name.is_empty() {
return Err(invalid(
"invalid-identifier",
"event name must be non-empty",
rule.event.span,
));
}
validate_exprs(&rule.event.args, program, tables)?;
check_span(rule.event.span, program.files.len())?;
validate_exprs(&rule.conditions, program, tables)?;
validate_stmts(&rule.actions, program, tables)
}
fn validate_stmts(
statements: &[Stmt],
program: &Program,
tables: &NameTables<'_>,
) -> Result<(), HirError> {
let mut validator = StatementValidator {
expressions: ExpressionValidator {
program,
tables,
errors: Vec::new(),
},
errors: Vec::new(),
};
visit::walk_stmts(&mut validator, statements);
let StatementValidator {
expressions,
errors,
} = validator;
expressions
.errors
.into_iter()
.chain(errors)
.next()
.map_or(Ok(()), Err)
}
fn validate_exprs<'expr>(
expressions: impl IntoIterator<Item = &'expr Expr>,
program: &Program,
tables: &NameTables<'_>,
) -> Result<(), HirError> {
let mut validator = ExpressionValidator {
program,
tables,
errors: Vec::new(),
};
for expression in expressions {
Visitor::visit_expr(&mut validator, expression);
}
validator.errors.into_iter().next().map_or(Ok(()), Err)
}
struct ExpressionValidator<'program, 'tables, 'names> {
program: &'program Program,
tables: &'tables NameTables<'names>,
errors: Vec<HirError>,
}
impl Visitor for ExpressionValidator<'_, '_, '_> {
fn visit_expr(&mut self, expression: &Expr) {
if let Err(error) = check_span(expression.span().copied(), self.program.files.len()) {
self.errors.push(error);
}
match expression {
Expr::GlobalVar { name, span }
if !self.tables.globals.contains(&name.as_str())
&& default_var_index(name).is_none() =>
{
self.errors.push(invalid(
"unresolved-reference",
format!("reference to unknown global variable '{name}'"),
*span,
));
}
Expr::PlayerVar {
player, name, span, ..
} if !self.tables.players.contains(&name.as_str())
&& !is_implicit_player_variable(player, name) =>
{
self.errors.push(invalid(
"unresolved-reference",
format!("reference to unknown player variable '{name}'"),
*span,
));
}
Expr::Constant { name, span } if !self.tables.constants.contains(&name.as_str()) => {
self.errors.push(invalid(
"unresolved-reference",
format!("reference to unknown constant '{name}'"),
*span,
));
}
_ => {}
}
visit::walk_expr(self, expression);
}
fn visit_comprehension(&mut self, element: &Expr, iterable: &Expr, condition: Option<&Expr>) {
Visitor::visit_expr(self, iterable);
Visitor::visit_expr(self, element);
if let Some(condition) = condition {
Visitor::visit_expr(self, condition);
}
}
}
struct StatementValidator<'program, 'tables, 'names> {
expressions: ExpressionValidator<'program, 'tables, 'names>,
errors: Vec<HirError>,
}
impl Visitor for StatementValidator<'_, '_, '_> {
fn visit_expr(&mut self, expression: &Expr) {
Visitor::visit_expr(&mut self.expressions, expression);
}
fn visit_stmt(&mut self, statement: &Stmt) {
if let Err(error) = check_span(
statement.span().copied(),
self.expressions.program.files.len(),
) {
self.errors.push(error);
}
match statement {
Stmt::CallSubroutine { name, span } => {
let known = self.expressions.tables.subroutines.contains(&name.as_str())
|| self.expressions.program.rules.iter().any(|entry| {
matches!(
entry,
RuleEntry::SubroutineDef {
name: presentation,
source_name,
..
} if presentation == name || source_name == name
)
});
if !known {
self.errors.push(invalid(
"unresolved-reference",
format!("call to unknown subroutine '{name}'"),
*span,
));
}
}
Stmt::For { variable, span, .. } => match variable.as_ref() {
Expr::GlobalVar { name, .. }
if self.expressions.tables.globals.contains(&name.as_str())
|| default_var_index(name).is_some() => {}
Expr::GlobalVar { name, .. } => self.errors.push(invalid(
"unresolved-reference",
format!("for-loop variable '{name}' is not a declared global variable"),
*span,
)),
Expr::PlayerVar { name, player, .. }
if self.expressions.tables.players.contains(&name.as_str())
|| is_implicit_player_variable(player, name) => {}
Expr::PlayerVar { name, .. } => self.errors.push(invalid(
"unresolved-reference",
format!("for-loop variable '{name}' is not a declared player variable"),
*span,
)),
other => self.errors.push(invalid(
"invalid-structure",
format!(
"for-loop variable must be a global variable reference, got '{}'",
other.kind_name()
),
*span,
)),
},
Stmt::Switch { arms, .. } => {
if arms.is_empty() {
self.errors.push(invalid(
"invalid-structure",
"a switch must contain at least one arm",
None,
));
}
let mut defaults = 0;
for arm in arms {
let arm_span = match arm {
SwitchArm::Case { span, .. } | SwitchArm::Default { span, .. } => span,
};
if let Err(error) = check_span(*arm_span, self.expressions.program.files.len())
{
self.errors.push(error);
}
if matches!(arm, SwitchArm::Default { .. }) {
defaults += 1;
if defaults > 1 {
self.errors.push(invalid(
"invalid-structure",
"a switch may contain at most one default arm",
*arm_span,
));
}
}
}
}
Stmt::Delete { target, span } => {
if !matches!(target.as_ref(), Expr::Index { .. }) {
self.errors.push(invalid(
"invalid-structure",
"a delete statement must target an array index",
*span,
));
}
}
Stmt::Goto {
label,
offset,
rule_start,
span,
} => {
let target_count =
u8::from(label.is_some()) + u8::from(offset.is_some()) + u8::from(*rule_start);
if target_count != 1 {
self.errors.push(invalid(
"invalid-structure",
"a goto must contain exactly one label, offset, or RULE_START target",
*span,
));
}
if let Some(label) = label {
if let Err(error) = check_name(label, "label", *span) {
self.errors.push(error);
}
}
}
Stmt::Label { name, span } => {
if let Err(error) = check_name(name, "label", *span) {
self.errors.push(error);
}
}
_ => {}
}
visit::walk_stmt(self, statement);
}
}
fn is_implicit_player_variable(_player: &Expr, name: &str) -> bool {
default_var_index(name).is_some()
}
fn check_name(name: &str, what: &str, span: Option<Span>) -> Result<(), HirError> {
if name.is_empty() {
return Err(invalid(
"invalid-identifier",
format!("{what} name must be non-empty"),
span,
));
}
Ok(())
}
fn check_unique(
name: &str,
what: &str,
span: Option<Span>,
seen: &mut Vec<&str>,
) -> Result<(), HirError> {
if seen.contains(&name) {
return Err(invalid(
"invalid-identifier",
format!("duplicate {what} name '{name}'"),
span,
));
}
Ok(())
}
fn check_span(span: Option<Span>, files: usize) -> Result<(), HirError> {
let Some(span) = span else {
return Ok(());
};
if span.file as usize >= files {
return Err(invalid(
"invalid-span",
format!("span references unknown file {}", span.file),
Some(span),
));
}
if !valid_position(&span.start) || !valid_position(&span.end) {
return Err(invalid(
"invalid-span",
"span positions are 1-based; line and column must be >= 1",
Some(span),
));
}
if span.end.line < span.start.line
|| (span.end.line == span.start.line && span.end.col < span.start.col)
{
return Err(invalid(
"invalid-span",
"span end precedes span start",
Some(span),
));
}
Ok(())
}
fn valid_position(position: &Position) -> bool {
position.line >= 1 && position.col >= 1
}
fn check_declaration(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
let kind = object.get("kind").and_then(Value::as_str).unwrap_or("");
if !DECLARATION_KINDS.contains(&kind) {
return Err(unsupported_node(kind, object));
}
if let Some(initializer) = object.get("initializer") {
check_expr(initializer)?;
}
if let Some(value_expr) = object.get("value") {
check_expr(value_expr)?;
}
if let Some(body) = object.get("body").and_then(Value::as_array) {
for statement in body {
check_stmt(statement)?;
}
}
Ok(())
}
fn check_rule_entry(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
match object.get("kind").and_then(Value::as_str) {
Some(kind) if kind != "subroutineDef" => Err(unsupported_node(kind, object)),
Some(_) => {
if let Some(body) = object.get("body").and_then(Value::as_array) {
for statement in body {
check_stmt(statement)?;
}
}
Ok(())
}
None => {
if let Some(event) = object.get("event").and_then(Value::as_object) {
if let Some(args) = event.get("args").and_then(Value::as_array) {
for arg in args {
check_expr(arg)?;
}
}
}
if let Some(conditions) = object.get("conditions").and_then(Value::as_array) {
for condition in conditions {
check_expr(condition)?;
}
}
if let Some(actions) = object.get("actions").and_then(Value::as_array) {
for action in actions {
check_stmt(action)?;
}
}
Ok(())
}
}
}
fn check_stmt(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
let kind = object.get("kind").and_then(Value::as_str).unwrap_or("");
if !STMT_KINDS.contains(&kind) {
return Err(unsupported_node(kind, object));
}
for field in [
"expr",
"target",
"value",
"condition",
"variable",
"iterable",
"offset",
] {
if let Some(child) = object.get(field) {
check_expr(child)?;
}
}
if let Some(branches) = object.get("branches").and_then(Value::as_array) {
for branch in branches {
if let Some(condition) = branch.get("condition") {
check_expr(condition)?;
}
if let Some(body) = branch.get("body").and_then(Value::as_array) {
for statement in body {
check_stmt(statement)?;
}
}
}
}
if let Some(arms) = object.get("arms").and_then(Value::as_array) {
for arm in arms {
let Some(arm_object) = arm.as_object() else {
return Err(unsupported_node("", object));
};
let arm_kind = arm_object.get("kind").and_then(Value::as_str).unwrap_or("");
match arm_kind {
"case" => {
if let Some(value) = arm_object.get("value") {
check_expr(value)?;
}
}
"default" => {}
_ => return Err(unsupported_node(arm_kind, arm_object)),
}
if let Some(body) = arm_object.get("body").and_then(Value::as_array) {
for statement in body {
check_stmt(statement)?;
}
}
}
}
for field in ["body", "else"] {
if let Some(statements) = object.get(field).and_then(Value::as_array) {
for statement in statements {
check_stmt(statement)?;
}
}
}
Ok(())
}
fn check_expr(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
let kind = object.get("kind").and_then(Value::as_str).unwrap_or("");
if !EXPR_KINDS.contains(&kind) {
return Err(unsupported_node(kind, object));
}
for field in [
"x",
"y",
"z",
"player",
"receiver",
"left",
"right",
"operand",
"array",
"index",
"thenValue",
"condition",
"elseValue",
] {
if let Some(child) = object.get(field) {
check_expr(child)?;
}
}
for field in ["args", "elements"] {
if let Some(children) = object.get(field).and_then(Value::as_array) {
for child in children {
check_expr(child)?;
}
}
}
if let Some(entries) = object.get("entries").and_then(Value::as_array) {
for entry in entries {
if let Some(key) = entry.get("key") {
check_expr(key)?;
}
if let Some(entry_value) = entry.get("value") {
check_expr(entry_value)?;
}
}
}
if let Some(body) = object.get("body") {
check_expr(body)?;
}
Ok(())
}
fn check_settings_value(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
let Some(children) = object.get("children").and_then(Value::as_array) else {
return Ok(());
};
for child in children {
check_settings_node(child)?;
}
Ok(())
}
fn check_settings_node(value: &Value) -> Result<(), HirError> {
let Some(object) = value.as_object() else {
return Ok(());
};
let kind = object.get("kind").and_then(Value::as_str).unwrap_or("");
if !SETTINGS_NODE_KINDS.contains(&kind) {
return Err(unsupported_node(kind, object));
}
if let Some(children) = object.get("children").and_then(Value::as_array) {
for child in children {
check_settings_node(child)?;
}
}
Ok(())
}
fn unsupported_node(kind: &str, object: &serde_json::Map<String, Value>) -> HirError {
let span = object
.get("span")
.and_then(|value| serde_json::from_value::<Span>(value.clone()).ok());
HirError::UnsupportedNode {
kind: kind.to_string(),
span,
}
}