use harn_lexer::Span;
use crate::ast::{HitlArg, HitlKind, Node, ShapeField, TypeExpr};
use super::super::scope::TypeScope;
use super::super::TypeChecker;
struct HitlParamSpec {
name: &'static str,
required: bool,
}
fn params_for(kind: HitlKind) -> &'static [HitlParamSpec] {
match kind {
HitlKind::AskUser => &[
HitlParamSpec {
name: "prompt",
required: true,
},
HitlParamSpec {
name: "schema",
required: false,
},
HitlParamSpec {
name: "timeout",
required: false,
},
HitlParamSpec {
name: "default",
required: false,
},
],
HitlKind::RequestApproval => &[
HitlParamSpec {
name: "action",
required: true,
},
HitlParamSpec {
name: "args",
required: false,
},
HitlParamSpec {
name: "detail",
required: false,
},
HitlParamSpec {
name: "quorum",
required: false,
},
HitlParamSpec {
name: "reviewers",
required: false,
},
HitlParamSpec {
name: "deadline",
required: false,
},
HitlParamSpec {
name: "principal",
required: false,
},
HitlParamSpec {
name: "evidence_refs",
required: false,
},
HitlParamSpec {
name: "undo_metadata",
required: false,
},
HitlParamSpec {
name: "capabilities_requested",
required: false,
},
],
HitlKind::DualControl => &[
HitlParamSpec {
name: "n",
required: true,
},
HitlParamSpec {
name: "m",
required: true,
},
HitlParamSpec {
name: "action",
required: true,
},
HitlParamSpec {
name: "approvers",
required: false,
},
],
HitlKind::EscalateTo => &[
HitlParamSpec {
name: "role",
required: true,
},
HitlParamSpec {
name: "reason",
required: true,
},
],
}
}
impl TypeChecker {
pub(in crate::typechecker) fn check_hitl_expr(
&mut self,
kind: HitlKind,
args: &[HitlArg],
scope: &mut TypeScope,
span: Span,
) {
for arg in args {
self.check_node(&arg.value, scope);
}
let params = params_for(kind);
let kw = kind.as_keyword();
for arg in args {
if let Some(name) = arg.name.as_deref() {
if !params.iter().any(|p| p.name == name) {
let allowed = params.iter().map(|p| p.name).collect::<Vec<_>>().join(", ");
self.error_at(
format!("{kw}: unknown argument `{name}` (expected one of: {allowed})"),
arg.span,
);
}
}
}
let mut seen_named = false;
for arg in args {
match arg.name {
Some(_) => seen_named = true,
None if seen_named => {
self.error_at(
format!("{kw}: positional argument cannot follow a named argument"),
arg.span,
);
}
None => {}
}
}
for (i, arg) in args.iter().enumerate() {
if let Some(name) = arg.name.as_deref() {
if args
.iter()
.skip(i + 1)
.any(|other| other.name.as_deref() == Some(name))
{
self.error_at(format!("{kw}: duplicate argument `{name}`"), arg.span);
}
}
}
let positional_count = args.iter().take_while(|a| a.name.is_none()).count();
for (i, p) in params.iter().enumerate() {
if !p.required {
continue;
}
let by_position = i < positional_count;
let by_name = args.iter().any(|a| a.name.as_deref() == Some(p.name));
if !by_position && !by_name {
self.error_at(
format!("{kw}: missing required argument `{}`", p.name),
span,
);
}
}
if positional_count > params.len() {
self.error_at(
format!(
"{kw}: too many positional arguments (max {} positional, got {})",
params.len(),
positional_count
),
span,
);
}
}
pub(in crate::typechecker) fn hitl_envelope_type(kind: HitlKind) -> TypeExpr {
match kind {
HitlKind::AskUser => TypeExpr::Named("any".into()),
HitlKind::RequestApproval => approval_record_shape(),
HitlKind::DualControl => TypeExpr::Named("any".into()),
HitlKind::EscalateTo => escalation_handle_shape(),
}
}
pub(in crate::typechecker) fn hitl_expr_inferred_type(
&self,
kind: HitlKind,
args: &[HitlArg],
scope: &TypeScope,
) -> TypeExpr {
match kind {
HitlKind::AskUser => self
.hitl_named_or_positional(args, "schema", 1)
.and_then(|node| match &node.node {
Node::FunctionCall { name, args, .. }
if name == "schema_of" && args.len() == 1 =>
{
if let Node::Identifier(alias) = &args[0].node {
scope.resolve_type(alias).cloned()
} else {
None
}
}
_ => None,
})
.unwrap_or_else(|| Self::hitl_envelope_type(kind)),
HitlKind::DualControl => self
.hitl_named_or_positional(args, "action", 2)
.and_then(|node| match &node.node {
Node::Closure { body, .. } => {
body.last().and_then(|last| self.infer_type(last, scope))
}
Node::Identifier(name) => {
scope.get_fn(name).and_then(|sig| sig.return_type.clone())
}
_ => None,
})
.unwrap_or_else(|| Self::hitl_envelope_type(kind)),
HitlKind::RequestApproval | HitlKind::EscalateTo => Self::hitl_envelope_type(kind),
}
}
fn hitl_named_or_positional<'a>(
&self,
args: &'a [HitlArg],
name: &str,
position: usize,
) -> Option<&'a crate::ast::SNode> {
args.iter()
.find(|a| a.name.as_deref() == Some(name))
.or_else(|| args.iter().filter(|a| a.name.is_none()).nth(position))
.map(|a| &a.value)
}
}
fn approval_record_shape() -> TypeExpr {
TypeExpr::Shape(vec![
ShapeField {
name: "approved".into(),
type_expr: TypeExpr::Named("bool".into()),
optional: false,
},
ShapeField {
name: "reviewers".into(),
type_expr: TypeExpr::List(Box::new(TypeExpr::Named("string".into()))),
optional: false,
},
ShapeField {
name: "approved_at".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "reason".into(),
type_expr: TypeExpr::Union(vec![
TypeExpr::Named("string".into()),
TypeExpr::Named("nil".into()),
]),
optional: true,
},
ShapeField {
name: "signatures".into(),
type_expr: TypeExpr::List(Box::new(TypeExpr::Shape(vec![
ShapeField {
name: "reviewer".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "signed_at".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "signature".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
]))),
optional: false,
},
])
}
fn escalation_handle_shape() -> TypeExpr {
TypeExpr::Shape(vec![
ShapeField {
name: "request_id".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "role".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "reason".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "trace_id".into(),
type_expr: TypeExpr::Named("string".into()),
optional: false,
},
ShapeField {
name: "status".into(),
type_expr: TypeExpr::Union(vec![
TypeExpr::LitString("pending".into()),
TypeExpr::LitString("accepted".into()),
]),
optional: false,
},
ShapeField {
name: "accepted_at".into(),
type_expr: TypeExpr::Union(vec![
TypeExpr::Named("string".into()),
TypeExpr::Named("nil".into()),
]),
optional: true,
},
ShapeField {
name: "reviewer".into(),
type_expr: TypeExpr::Union(vec![
TypeExpr::Named("string".into()),
TypeExpr::Named("nil".into()),
]),
optional: true,
},
])
}