use super::*;
pub(super) type FieldSchema = (Vec<String>, Vec<(String, String)>);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParseSite {
pub stage: usize,
pub node: a::NodeId,
}
pub fn rewrite_parse_calls(stages: &mut [a::Stage], pt: &ProgramTypes) {
if pt.parse_required_fields.is_empty() {
return;
}
for (stage_idx, stage) in stages.iter_mut().enumerate() {
if !pt.parse_required_fields.keys().any(|s| s.stage == stage_idx) {
continue;
}
let ptr_of: HashMap<a::NodeId, usize> = a::expr_ids(&*stage)
.into_iter()
.map(|(p, id)| (id, p as usize))
.collect();
let resolve = |site: &ParseSite| -> Option<usize> {
(site.stage == stage_idx).then(|| {
*ptr_of.get(&site.node).unwrap_or_else(|| panic!(
"rewrite_parse_calls: {:?} names no expression in stage {stage_idx}; \
the stages differ from the ones that were type-checked",
site.node))
})
};
let required: HashMap<usize, Vec<String>> = pt.parse_required_fields.iter()
.filter_map(|(site, f)| resolve(site).map(|p| (p, f.clone())))
.collect();
let schemas: HashMap<usize, Vec<(String, String)>> = pt.parse_type_schemas.iter()
.filter_map(|(site, s)| resolve(site).map(|p| (p, s.clone())))
.collect();
if let a::Stage::FnDecl(fd) = stage {
rewrite_in_expr(&mut fd.body, &required, &schemas);
}
}
}
pub(super) fn rewrite_in_expr(
expr: &mut a::CExpr,
required: &HashMap<usize, Vec<String>>,
schemas: &HashMap<usize, Vec<(String, String)>>,
) {
let ptr = expr as *const a::CExpr as usize;
let do_rewrite = required.get(&ptr).cloned();
let do_schema = schemas.get(&ptr).cloned();
match expr {
a::CExpr::Call { callee, args } => {
rewrite_in_expr(callee, required, schemas);
for a in args.iter_mut() { rewrite_in_expr(a, required, schemas); }
}
a::CExpr::Let { value, body, .. } => {
rewrite_in_expr(value, required, schemas);
rewrite_in_expr(body, required, schemas);
}
a::CExpr::Match { scrutinee, arms } => {
rewrite_in_expr(scrutinee, required, schemas);
for arm in arms.iter_mut() { rewrite_in_expr(&mut arm.body, required, schemas); }
}
a::CExpr::Block { statements, result } => {
for s in statements.iter_mut() { rewrite_in_expr(s, required, schemas); }
rewrite_in_expr(result, required, schemas);
}
a::CExpr::Constructor { args, .. } => {
for a in args.iter_mut() { rewrite_in_expr(a, required, schemas); }
}
a::CExpr::RecordLit { fields } => {
for f in fields.iter_mut() { rewrite_in_expr(&mut f.value, required, schemas); }
}
a::CExpr::TupleLit { items } | a::CExpr::ListLit { items } => {
for it in items.iter_mut() { rewrite_in_expr(it, required, schemas); }
}
a::CExpr::FieldAccess { value, .. } => rewrite_in_expr(value, required, schemas),
a::CExpr::Lambda { body, .. } => rewrite_in_expr(body, required, schemas),
a::CExpr::BinOp { lhs, rhs, .. } => {
rewrite_in_expr(lhs, required, schemas);
rewrite_in_expr(rhs, required, schemas);
}
a::CExpr::UnaryOp { expr, .. } => rewrite_in_expr(expr, required, schemas),
a::CExpr::Return { value } => rewrite_in_expr(value, required, schemas),
a::CExpr::Literal { .. } | a::CExpr::Var { .. } => {}
}
if let Some(fields) = do_rewrite {
match expr {
a::CExpr::Call { callee, args } => {
if let a::CExpr::FieldAccess { field, .. } = callee.as_mut() {
let typed = match field.as_str() {
"parse" => "parse_strict_typed", "json_body" => "json_body_typed", other => unreachable!(
"rewrite_in_expr: unexpected decode field `{other}`"),
};
*field = typed.to_string();
}
args.push(a::CExpr::ListLit {
items: fields.into_iter()
.map(|f| a::CExpr::Literal {
value: a::CLit::Str { value: f },
})
.collect(),
});
let schema = do_schema.unwrap_or_default();
args.push(a::CExpr::ListLit {
items: schema.into_iter()
.map(|(name, tag)| a::CExpr::TupleLit {
items: vec![
a::CExpr::Literal { value: a::CLit::Str { value: name } },
a::CExpr::Literal { value: a::CLit::Str { value: tag } },
],
})
.collect(),
});
}
_ => unreachable!("rewrite table key must point to a Call expression"),
}
}
}
pub(super) fn extract_record_fields_and_schema(
u: &Unifier,
env: &TypeEnv,
ty: &Ty,
) -> Option<FieldSchema> {
let resolved = u.resolve(ty);
let Ty::Con(ref name, ref args) = resolved else { return None; };
if name != "Result" || args.len() != 2 { return None; }
let ok_ty = u.resolve(&args[0]);
let unfolded = unfold_record_alias_static(env, ok_ty);
if let Ty::Record(fields) = unfolded {
let schema: Vec<(String, String)> = fields.iter()
.map(|(k, v)| (k.clone(), ty_to_tag(u, v)))
.collect();
let names: Vec<String> = schema.iter()
.filter(|(_, tag)| !tag.starts_with("Option["))
.map(|(k, _)| k.clone())
.collect();
Some((names, schema))
} else {
None
}
}
pub(super) fn ty_to_tag(u: &Unifier, ty: &Ty) -> String {
let resolved = u.resolve(ty);
match &resolved {
Ty::Prim(Prim::Int) => "Int".to_string(),
Ty::Prim(Prim::Float) => "Float".to_string(),
Ty::Prim(Prim::Bool) => "Bool".to_string(),
Ty::Prim(Prim::Str) => "Str".to_string(),
Ty::Con(name, args) if name == "Option" && args.len() == 1 => {
format!("Option[{}]", ty_to_tag(u, &args[0]))
}
Ty::List(inner) => {
format!("List[{}]", ty_to_tag(u, inner))
}
Ty::Record(_) => "Record".to_string(),
_ => "Any".to_string(),
}
}
pub(super) fn unfold_record_alias_static(env: &TypeEnv, ty: Ty) -> Ty {
if let Ty::Con(ref n, ref args) = ty {
if let Some(td) = env.types.get(n) {
if let TypeDefKind::Alias(inner) = &td.kind {
if td.params.len() != args.len() {
return ty;
}
if td.params.is_empty() {
return inner.clone();
}
let mut subst = IndexMap::new();
for (i, a) in args.iter().enumerate() {
subst.insert(i as u32, a.clone());
}
return subst_vars(inner, &subst, &IndexMap::new());
}
}
}
ty
}
impl Checker {
pub(super) fn has_parse_capable_imports(&self) -> bool {
self.module_aliases.values()
.any(|m| matches!(m.as_str(), "json" | "toml" | "yaml" | "http"))
}
pub(super) fn parse_site_of(&self, call_expr: &a::CExpr) -> Option<ParseSite> {
let (stage, ids) = self.stage_ids.as_ref()?;
let node = ids.get(&(call_expr as *const a::CExpr))?.clone();
Some(ParseSite { stage: *stage, node })
}
pub(super) fn is_module_parse_call(&self, callee: &a::CExpr) -> bool {
if let a::CExpr::FieldAccess { value, field } = callee {
if let a::CExpr::Var { name } = value.as_ref() {
if let Some(module) = self.module_aliases.get(name) {
return matches!(
(module.as_str(), field.as_str()),
("json" | "toml" | "yaml", "parse") | ("http", "json_body")
);
}
}
}
false
}
}