use super::*;
pub(super) fn synthesize_ambient_capability_repair(
diag: &harn_lint::LintDiagnostic,
source: &str,
program: &[SNode],
exported_names: &BTreeSet<String>,
context: &AmbientRepairContext,
escape: &mut ValueEscape<'_>,
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
ambient_capability_handle(diag.code)?;
let infos = collect_callable_infos(
program,
source,
exported_names,
escape.referenced_by_value,
escape.manifest_handlers,
);
let owner_idx = infos.iter().position(|info| {
info.ambient_capability_calls.iter().any(|call| {
call.code == diag.code
&& call.span.start == diag.span.start
&& call.span.end == diag.span.end
})
})?;
let reverse_callers = build_reverse_callers(&infos);
let owner = &infos[owner_idx];
let ambient = owner.ambient_capability_calls.iter().find(|call| {
call.code == diag.code
&& call.span.start == diag.span.start
&& call.span.end == diag.span.end
})?;
let replacement_binding = owner
.harness_binding
.clone()
.or_else(|| harness_param_name_for_insert(owner).map(str::to_string));
let replacement =
ambient_replacement(diag.code, &ambient.name, replacement_binding.as_deref())?;
let mut edits = ambient_call_rewrite(source, ambient, &replacement)?;
if owner.harness_binding.is_some() {
return Some((
Repair::from_template(diag.code.repair_template()?),
edits,
RepairImpactWire::local_ambient("existing-harness-binding"),
));
}
let needed = propagate_harness_requirements(&infos, &reverse_callers, owner_idx);
let primary_call_start = owner
.ambient_capability_calls
.iter()
.filter(|call| call.code == diag.code)
.map(|call| call.span.start)
.min()
.unwrap_or(diag.span.start);
if diag.span.start != primary_call_start {
if owner.frozen_cause.is_some() {
escape.record(owner);
return None;
}
return Some((
repair_for_ambient_capability_plan(diag.code, &infos, &reverse_callers, &needed)?,
edits,
repair_impact_for_signature_threading(
&infos,
&needed,
context.cross_module_importer_count,
),
));
}
for &idx in &needed {
let info = &infos[idx];
escape.record(info);
push_signature_edits(&mut edits, source, info)?;
}
for (callee_idx, callers) in reverse_callers.iter().enumerate() {
if !needed.contains(&callee_idx) {
continue;
}
for &(caller_idx, call_idx) in callers {
let caller = &infos[caller_idx];
let arg_name = match caller.harness_binding.as_deref() {
Some(binding) => binding,
None if needed.contains(&caller_idx) => harness_param_name_for_insert(caller)?,
None => continue,
};
edits.push(add_call_argument_edit(
source,
&caller.calls[call_idx].span,
arg_name,
)?);
}
}
Some((
repair_for_ambient_capability_plan(diag.code, &infos, &reverse_callers, &needed)?,
dedupe_edits(edits),
repair_impact_for_signature_threading(&infos, &needed, context.cross_module_importer_count),
))
}
pub(super) fn synthesize_missing_capability_argument_repair(
span: Span,
expected: &TypeExpr,
actual: &TypeExpr,
source: &str,
program: &[SNode],
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
let expected_name = match expected {
TypeExpr::Named(name) => Some(name.as_str()),
_ => None,
};
let capability = expected_name.and_then(harn_builtin_meta::CapabilityId::from_type_name);
let mut matched_argument = None;
visit::walk_program(program, &mut |node| {
let Node::FunctionCall { args, .. } = &node.node else {
return;
};
for candidate in args {
if candidate.span.start != span.start || candidate.span.end != span.end {
continue;
}
if matches!(
&candidate.node,
Node::Identifier(_) | Node::PropertyAccess { .. }
) {
matched_argument = source
.get(candidate.span.start..candidate.span.end)
.map(|text| (candidate.span, text.to_string()));
}
}
});
if matches!(actual, TypeExpr::Named(name) if name == "Harness") {
let (argument_span, binding) = matched_argument?;
if let Some(replacement) = capability_bundle_literal(expected, &binding) {
return Some((
Repair {
id: harn_parser::RepairId::from_owned(
"bindings/attenuate-capability-bundle-argument".to_string(),
),
summary:
"Pass the closed capability bundle required by the attenuated callable"
.to_string(),
safety: RepairSafety::SurfaceChanging,
},
vec![FixEdit {
span: argument_span,
replacement,
}],
RepairImpactWire::local_ambient("attenuate-capability-bundle-argument"),
));
}
let capability = capability?;
let expected_name = expected_name?;
return Some((
Repair {
id: harn_parser::RepairId::from_owned(
"bindings/attenuate-capability-argument".to_string(),
),
summary: format!(
"Pass the `{expected_name}` sub-grant required by the attenuated callable"
),
safety: RepairSafety::SurfaceChanging,
},
vec![FixEdit {
span: argument_span,
replacement: format!("{binding}.{}", capability.field_name()),
}],
RepairImpactWire::local_ambient("attenuate-capability-argument"),
));
}
let _capability = capability?;
let expected_name = expected_name?;
let argument = capability_argument_for_span(program, span, expected_name)?;
let edit = insert_call_argument_before_span(source, program, span, &argument)?;
Some((
Repair {
id: harn_parser::RepairId::from_owned(
"bindings/prepend-capability-argument".to_string(),
),
summary: format!(
"Pass the explicit `{expected_name}` capability required by the migrated callable"
),
safety: RepairSafety::SurfaceChanging,
},
vec![edit],
RepairImpactWire::local_ambient("prepend-capability-argument"),
))
}
pub(super) fn synthesize_unknown_type_validation(
span: Span,
expected: &TypeExpr,
actual: &TypeExpr,
source: &str,
program: &[harn_parser::SNode],
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
if !matches!(actual, TypeExpr::Named(name) if name == "unknown") {
return None;
}
let validation_span = nil_coalesce_left_span(program, span).unwrap_or(span);
let expression = source.get(validation_span.start..validation_span.end)?;
let schema = runtime_schema_for_type(expected)?;
Some((
Repair {
id: harn_parser::RepairId::from_owned("types/validate-unknown-value".to_string()),
summary: "Validate the unknown boundary value against the required static type"
.to_string(),
safety: RepairSafety::ScopeLocal,
},
vec![FixEdit {
span,
replacement: format!("schema_expect({expression}, {schema})"),
}],
RepairImpactWire::runtime_validation(validation_span != span),
))
}
pub(super) fn synthesize_schema_witness_refinement(
span: Span,
expected: &TypeExpr,
actual: &TypeExpr,
source: &str,
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
let TypeExpr::Applied { name, args } = expected else {
return None;
};
if name != "Schema"
|| args.len() != 1
|| !matches!(actual, TypeExpr::Named(name) if name == "dict")
{
return None;
}
let expression = source.get(span.start..span.end)?;
let witness = harn_parser::typechecker::format_type(&args[0]);
Some((
Repair {
id: harn_parser::RepairId::from_owned("types/refine-schema-witness".to_string()),
summary: "Refine a nominal schema witness with the dictionary constraint".to_string(),
safety: RepairSafety::ScopeLocal,
},
vec![FixEdit {
span,
replacement: format!("schema_refine(schema_of({witness}), {expression})"),
}],
RepairImpactWire::schema_refinement(),
))
}
fn nil_coalesce_left_span(program: &[harn_parser::SNode], span: Span) -> Option<Span> {
let mut left_span = None;
harn_parser::visit::walk_program(program, &mut |node| {
if node.span == span {
if let harn_parser::Node::BinaryOp { op, left, .. } = &node.node {
if op == "??" {
left_span = Some(left.span);
}
}
}
});
left_span
}
fn runtime_schema_for_type(expected: &TypeExpr) -> Option<String> {
match expected {
TypeExpr::Named(name) => match name.as_str() {
"string" | "int" | "float" | "bool" | "nil" | "dict" | "list" => {
Some(format!("{{type: \"{name}\"}}"))
}
"number" => Some("{union: [{type: \"int\"}, {type: \"float\"}]}".to_string()),
"any" | "unknown" | "never" | "closure" | "Harness" => None,
name if harn_builtin_meta::CapabilityId::from_type_name(name).is_some() => None,
name => Some(format!("schema_of({name})")),
},
TypeExpr::Union(members) => Some(format!(
"{{union: [{}]}}",
members
.iter()
.map(runtime_schema_for_type)
.collect::<Option<Vec<_>>>()?
.join(", ")
)),
TypeExpr::Intersection(members) => Some(format!(
"{{all_of: [{}]}}",
members
.iter()
.map(runtime_schema_for_type)
.collect::<Option<Vec<_>>>()?
.join(", ")
)),
TypeExpr::Shape(fields) => runtime_schema_for_shape(fields, None),
TypeExpr::OpenShape { fields, rests } => {
let [rest] = rests.as_slice() else {
return None;
};
runtime_schema_for_shape(fields, Some(rest))
}
TypeExpr::List(item) => Some(format!(
"{{type: \"list\", items: {}}}",
runtime_schema_for_type(item)?
)),
TypeExpr::DictType(key, value) if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") => {
Some(format!(
"{{type: \"dict\", additional_properties: {}}}",
runtime_schema_for_type(value)?
))
}
TypeExpr::Applied { name, args } if name == "Option" && args.len() == 1 => Some(format!(
"{{union: [{}, {{type: \"nil\"}}]}}",
runtime_schema_for_type(&args[0])?
)),
TypeExpr::LitString(value) => Some(format!(
"{{type: \"string\", const: {}}}",
serde_json::to_string(value).ok()?
)),
TypeExpr::LitInt(value) => Some(format!("{{type: \"int\", const: {value}}}")),
TypeExpr::Tuple(_)
| TypeExpr::DictType(_, _)
| TypeExpr::Iter(_)
| TypeExpr::Generator(_)
| TypeExpr::Stream(_)
| TypeExpr::Owned(_)
| TypeExpr::Applied { .. }
| TypeExpr::FnType { .. }
| TypeExpr::Never => None,
}
}
fn runtime_schema_for_shape(
fields: &[harn_parser::ShapeField],
rest: Option<&TypeExpr>,
) -> Option<String> {
let properties = fields
.iter()
.map(|field| {
Some(format!(
"{}: {}",
serde_json::to_string(&field.name).ok()?,
runtime_schema_for_type(&field.type_expr)?
))
})
.collect::<Option<Vec<_>>>()?;
let required = fields
.iter()
.filter(|field| !field.optional)
.map(|field| serde_json::to_string(&field.name).ok())
.collect::<Option<Vec<_>>>()?;
let additional = match rest {
None => "false".to_string(),
Some(TypeExpr::Named(name)) if name == "dict" => "true".to_string(),
Some(TypeExpr::DictType(key, value)) if matches!(key.as_ref(), TypeExpr::Named(name) if name == "string") => {
runtime_schema_for_type(value)?
}
Some(_) => return None,
};
Some(format!(
"{{type: \"dict\", properties: {{{}}}, required: [{}], additional_properties: {additional}}}",
properties.join(", "),
required.join(", ")
))
}
pub(super) fn synthesize_missing_zero_arg_capability_repair(
call_span: Span,
expected: &TypeExpr,
source: &str,
program: &[SNode],
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
let TypeExpr::Named(expected_name) = expected else {
return None;
};
harn_builtin_meta::CapabilityId::from_type_name(expected_name)?;
let argument = capability_argument_for_span(program, call_span, expected_name)?;
let edit = add_call_argument_edit(source, &call_span, &argument)?;
Some((
Repair {
id: harn_parser::RepairId::from_owned(
"bindings/prepend-capability-argument".to_string(),
),
summary: format!(
"Pass the explicit `{expected_name}` capability required by the migrated callable"
),
safety: RepairSafety::SurfaceChanging,
},
vec![edit],
RepairImpactWire::local_ambient("prepend-capability-argument"),
))
}
pub(super) fn capability_bundle_literal(expected: &TypeExpr, binding: &str) -> Option<String> {
let TypeExpr::Shape(fields) = expected else {
return None;
};
let fields = fields
.iter()
.map(|field| {
if field.optional {
return None;
}
let TypeExpr::Named(type_name) = &field.type_expr else {
return None;
};
let capability = harn_builtin_meta::CapabilityId::from_type_name(type_name)?;
(capability.field_name() == field.name)
.then(|| format!("{}: {binding}.{}", field.name, field.name))
})
.collect::<Option<Vec<_>>>()?;
(!fields.is_empty()).then(|| format!("{{{}}}", fields.join(", ")))
}
pub(super) fn push_signature_edits(
edits: &mut Vec<FixEdit>,
source: &str,
info: &CallableInfo,
) -> Option<()> {
edits.push(add_harness_param_edit(source, info)?);
for alias_edit in &info.alias_widening_edits {
if !edits.iter().any(|edit| edit.span == alias_edit.span) {
edits.push(alias_edit.clone());
}
}
Some(())
}
pub(super) fn synthesize_missing_harness_repair(
span: Span,
source: &str,
program: &[SNode],
exported_names: &BTreeSet<String>,
context: &AmbientRepairContext,
escape: &mut ValueEscape<'_>,
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
let infos = collect_callable_infos(
program,
source,
exported_names,
escape.referenced_by_value,
escape.manifest_handlers,
);
let owner_idx = infos
.iter()
.enumerate()
.filter(|(_, info)| info.span.start <= span.start && info.span.end >= span.end)
.min_by_key(|(_, info)| info.span.end.saturating_sub(info.span.start))
.map(|(index, _)| index)?;
if infos[owner_idx].harness_binding.is_some() {
return None;
}
let reverse_callers = build_reverse_callers(&infos);
let needed = propagate_harness_requirements(&infos, &reverse_callers, owner_idx);
let mut edits = Vec::new();
for &idx in &needed {
escape.record(&infos[idx]);
push_signature_edits(&mut edits, source, &infos[idx])?;
}
for (callee_idx, callers) in reverse_callers.iter().enumerate() {
if !needed.contains(&callee_idx) {
continue;
}
for &(caller_idx, call_idx) in callers {
let caller = &infos[caller_idx];
let arg_name = match caller.harness_binding.as_deref() {
Some(binding) => binding,
None if needed.contains(&caller_idx) => harness_param_name_for_insert(caller)?,
None => continue,
};
edits.push(add_call_argument_edit(
source,
&caller.calls[call_idx].span,
arg_name,
)?);
}
}
Some((
Repair {
id: harn_parser::RepairId::from_owned("bindings/thread-missing-harness".to_string()),
summary:
"Thread the explicit Harness grant through this callable and its local callers"
.to_string(),
safety: RepairSafety::SurfaceChanging,
},
dedupe_edits(edits),
repair_impact_for_signature_threading(&infos, &needed, context.cross_module_importer_count),
))
}
pub(super) fn synthesize_missing_root_argument_repair(
span: Span,
source: &str,
program: &[SNode],
exported_names: &BTreeSet<String>,
context: &AmbientRepairContext,
escape: &mut ValueEscape<'_>,
) -> Option<(Repair, Vec<FixEdit>, RepairImpactWire)> {
let infos = collect_callable_infos(
program,
source,
exported_names,
escape.referenced_by_value,
escape.manifest_handlers,
);
let owner_idx = infos
.iter()
.enumerate()
.filter(|(_, info)| info.span.start <= span.start && info.span.end >= span.end)
.min_by_key(|(_, info)| info.span.end.saturating_sub(info.span.start))
.map(|(index, _)| index)?;
if let Some(owner_binding) = infos[owner_idx].harness_binding.as_deref() {
let edit = insert_call_argument_before_span(source, program, span, owner_binding)?;
return Some((
Repair {
id: harn_parser::RepairId::from_owned("bindings/thread-root-argument".to_string()),
summary: "Pass the root Harness required by the migrated callable".to_string(),
safety: RepairSafety::SurfaceChanging,
},
vec![edit],
RepairImpactWire::local_ambient("existing-root-harness-binding"),
));
}
let reverse_callers = build_reverse_callers(&infos);
let needed = propagate_harness_requirements(&infos, &reverse_callers, owner_idx);
let owner_binding = harness_param_name_for_insert(&infos[owner_idx])?;
let mut edits = vec![insert_call_argument_before_span(
source,
program,
span,
owner_binding,
)?];
for &idx in &needed {
if infos[idx].harness_binding.is_none() {
escape.record(&infos[idx]);
push_signature_edits(&mut edits, source, &infos[idx])?;
}
}
for (callee_idx, callers) in reverse_callers.iter().enumerate() {
if !needed.contains(&callee_idx) {
continue;
}
for &(caller_idx, call_idx) in callers {
let caller = &infos[caller_idx];
let argument = match caller.harness_binding.as_deref() {
Some(binding) => binding,
None if needed.contains(&caller_idx) => harness_param_name_for_insert(caller)?,
None => continue,
};
edits.push(add_call_argument_edit(
source,
&caller.calls[call_idx].span,
argument,
)?);
}
}
Some((
Repair {
id: harn_parser::RepairId::from_owned("bindings/thread-root-argument".to_string()),
summary: "Thread the root Harness required by the migrated callable".to_string(),
safety: RepairSafety::SurfaceChanging,
},
dedupe_edits(edits),
repair_impact_for_signature_threading(&infos, &needed, context.cross_module_importer_count),
))
}
pub(super) fn repair_impact_for_signature_threading(
infos: &[CallableInfo],
needed: &BTreeSet<usize>,
cross_module_importer_count: usize,
) -> RepairImpactWire {
let signature_changes = needed
.iter()
.map(|&idx| {
let info = &infos[idx];
SignatureChangeWire {
callable: info.name.clone(),
is_exported: info.is_exported,
is_entrypoint: info.name == "main",
}
})
.collect::<Vec<_>>();
RepairImpactWire::signature_threading(signature_changes, cross_module_importer_count)
}