use std::collections::HashSet;
use crate::dsl::ast::*;
use crate::dsl::error::DiagnosticReport;
use crate::dsl::registry;
pub(crate) fn validate_ast(file: &GkFile, report: &mut DiagnosticReport) {
let mut defined: HashSet<String> = HashSet::new();
let mut referenced: HashSet<String> = HashSet::new();
let mut input_names: HashSet<String> = HashSet::new();
let mut has_explicit_coords = false;
let mut definition_order: Vec<(String, crate::dsl::lexer::Span)> = Vec::new();
for stmt in &file.statements {
match stmt {
Statement::InputDecl(d) => {
has_explicit_coords = true;
input_names.insert(d.name.clone());
defined.insert(d.name.clone());
}
Statement::Binding(b) => {
for t in &b.targets {
defined.insert(t.clone());
definition_order.push((t.clone(), b.span));
}
}
Statement::ModuleDef(m) => {
defined.insert(m.name.clone());
}
Statement::ExternPort(p) => {
defined.insert(p.name.clone());
}
Statement::Cursor(_) => {}
Statement::Pragma { .. } => {}
}
}
for stmt in &file.statements {
let expr = match stmt {
Statement::InputDecl(_) | Statement::ModuleDef(_) | Statement::ExternPort(_) | Statement::Cursor(_) | Statement::Pragma { .. } => continue,
Statement::Binding(b) => &b.value,
};
validate_expr(expr, &defined, &input_names, &mut referenced, report);
}
if has_explicit_coords {
for name in &referenced {
if !defined.contains(name) {
report.error_with_hint(
crate::dsl::lexer::Span { line: 1, col: 1 },
format!("undefined wire reference: '{name}'"),
if input_names.contains(name) {
"internal error".into()
} else if let Some(suggestion) = find_close_name(name, &defined) {
format!("did you mean '{suggestion}'?")
} else {
format!("'{name}' is not declared — add `input {name}: <type>`, or define it as a binding")
},
);
}
}
} else {
let mut inferred: Vec<String> = referenced.iter()
.filter(|name| !defined.contains(*name))
.cloned()
.collect();
inferred.sort();
if inferred.is_empty() && !file.statements.is_empty() {
report.error_with_hint(
crate::dsl::lexer::Span { line: 1, col: 1 },
"no kernel inputs found",
"reference at least one unbound name (e.g., 'cycle') or declare one with `input <name>: <type>`",
);
} else {
for name in &inferred {
input_names.insert(name.clone());
defined.insert(name.clone());
}
}
}
for (name, _span) in &definition_order {
if !referenced.contains(name) && !input_names.contains(name) {
}
}
let mut seen_defs: HashSet<String> = input_names.clone();
for stmt in &file.statements {
match stmt {
Statement::InputDecl(_) => {}
Statement::Binding(b) => {
check_forward_refs(&b.value, &seen_defs, b.span, report);
for t in &b.targets {
seen_defs.insert(t.clone());
}
}
Statement::ModuleDef(m) => {
seen_defs.insert(m.name.clone());
}
Statement::ExternPort(p) => {
seen_defs.insert(p.name.clone());
}
Statement::Cursor(_) => {}
Statement::Pragma { .. } => {}
}
}
}
pub(crate) fn validate_expr(
expr: &Expr,
defined: &HashSet<String>,
coords: &HashSet<String>,
referenced: &mut HashSet<String>,
report: &mut DiagnosticReport,
) {
match expr {
Expr::Ident(name, _) => {
referenced.insert(name.clone());
}
Expr::Call(call) => {
if registry::lookup(&call.func).is_none() {
let msg = format!("unknown function: '{}'", call.func);
let hint = if let Some(suggestion) = registry::suggest_function(&call.func) {
format!("did you mean '{suggestion}'?")
} else {
"check the function name or see the function reference".into()
};
report.error_with_hint(call.span, msg, hint);
}
for arg in &call.args {
let inner = match arg {
Arg::Positional(e) => e,
Arg::Named(_, e) => e,
};
validate_expr(inner, defined, coords, referenced, report);
}
}
Expr::BinOp(lhs, _, rhs) => {
validate_expr(lhs, defined, coords, referenced, report);
validate_expr(rhs, defined, coords, referenced, report);
}
Expr::UnaryNeg(inner, _) | Expr::UnaryBitNot(inner, _) => {
validate_expr(inner, defined, coords, referenced, report);
}
Expr::ArrayLit(elems, _) => {
for e in elems {
validate_expr(e, defined, coords, referenced, report);
}
}
Expr::StringLit(s, _) => {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '{' {
i += 1;
let start = i;
while i < chars.len() && chars[i] != '}' { i += 1; }
let name: String = chars[start..i].iter().collect();
let is_ident = name.chars().next()
.map(|c| c.is_alphabetic() || c == '_')
.unwrap_or(false);
if is_ident {
referenced.insert(name);
}
i += 1;
} else {
i += 1;
}
}
}
_ => {}
}
}
pub(crate) fn literal_type(expr: &Expr) -> Option<String> {
match expr {
Expr::IntLit(_, _) => Some("u64".into()),
Expr::FloatLit(_, _) => Some("f64".into()),
Expr::StringLit(_, _) => Some("String".into()),
_ => None, }
}
pub(crate) fn types_compatible(lit_type: &str, declared: &str) -> bool {
match (lit_type, declared) {
("u64", "u64") => true,
("f64", "f64") => true,
("String", "String") => true,
("u64", "f64") => true,
_ => false,
}
}
pub(crate) fn collect_references(expr: &Expr, referenced: &mut HashSet<String>) {
match expr {
Expr::Ident(name, _) => {
if name != "true" && name != "false" {
referenced.insert(name.clone());
}
}
Expr::Call(call) => {
for arg in &call.args {
let inner = match arg {
Arg::Positional(e) => e,
Arg::Named(_, e) => e,
};
collect_references(inner, referenced);
}
}
Expr::BinOp(lhs, _, rhs) => {
collect_references(lhs, referenced);
collect_references(rhs, referenced);
}
Expr::UnaryNeg(inner, _) | Expr::UnaryBitNot(inner, _) => {
collect_references(inner, referenced);
}
Expr::ArrayLit(elems, _) => {
for e in elems { collect_references(e, referenced); }
}
Expr::StringLit(s, _) => {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '{' {
i += 1;
let start = i;
while i < chars.len() && chars[i] != '}' { i += 1; }
let name: String = chars[start..i].iter().collect();
let is_ident = name.chars().next()
.map(|c| c.is_alphabetic() || c == '_')
.unwrap_or(false);
if is_ident { referenced.insert(name); }
i += 1;
} else {
i += 1;
}
}
}
_ => {}
}
}
pub(crate) fn check_forward_refs(
expr: &Expr,
seen: &HashSet<String>,
stmt_span: crate::dsl::lexer::Span,
report: &mut DiagnosticReport,
) {
match expr {
Expr::Ident(name, span) => {
if !seen.contains(name) {
report.warning_with_hint(
*span,
format!("forward reference: '{name}' is used before it is defined"),
"consider reordering bindings so definitions come before uses",
);
}
}
Expr::Call(call) => {
for arg in &call.args {
let inner = match arg {
Arg::Positional(e) => e,
Arg::Named(_, e) => e,
};
check_forward_refs(inner, seen, stmt_span, report);
}
}
Expr::BinOp(lhs, _, rhs) => {
check_forward_refs(lhs, seen, stmt_span, report);
check_forward_refs(rhs, seen, stmt_span, report);
}
Expr::UnaryNeg(inner, _) | Expr::UnaryBitNot(inner, _) => {
check_forward_refs(inner, seen, stmt_span, report);
}
Expr::ArrayLit(elems, _) => {
for e in elems {
check_forward_refs(e, seen, stmt_span, report);
}
}
_ => {}
}
}
pub(crate) fn find_close_name(name: &str, defined: &HashSet<String>) -> Option<String> {
let mut best: Option<(String, usize)> = None;
for d in defined {
let dist = simple_edit_distance(name, d);
if dist <= 3 && (best.is_none() || dist < best.as_ref().unwrap().1) {
best = Some((d.clone(), dist));
}
}
best.map(|(n, _)| n)
}
pub(crate) fn simple_edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut m = vec![vec![0; b.len() + 1]; a.len() + 1];
for i in 0..=a.len() { m[i][0] = i; }
for j in 0..=b.len() { m[0][j] = j; }
for i in 1..=a.len() {
for j in 1..=b.len() {
let c = if a[i-1] == b[j-1] { 0 } else { 1 };
m[i][j] = (m[i-1][j]+1).min(m[i][j-1]+1).min(m[i-1][j-1]+c);
}
}
m[a.len()][b.len()]
}