1use std::fmt::Write as _;
20use std::path::Path;
21
22use harn_parser::{
23 peel_attributes, AssociatedType, EnumVariant, InterfaceMethod, Node, Parser, SNode, ShapeField,
24 StructField, TypeExpr, TypeParam, TypedParam, Variance, WhereClause,
25};
26
27use crate::read_module_source;
28
29pub type Fingerprint = [u8; 32];
31
32pub fn fingerprint_program(program: &[SNode]) -> Fingerprint {
36 let canonical = canonicalize_program(program);
37 blake3::hash(canonical.as_bytes()).into()
38}
39
40pub fn fingerprint_file(path: &Path) -> Option<Fingerprint> {
45 let source = read_module_source(path)?;
46 fingerprint_source(&source)
47}
48
49pub fn fingerprint_source(source: &str) -> Option<Fingerprint> {
52 let mut lexer = harn_lexer::Lexer::new(source);
53 let tokens = lexer.tokenize().ok()?;
54 let program = Parser::new(tokens).parse().ok()?;
55 Some(fingerprint_program(&program))
56}
57
58pub fn fingerprint_hex(fp: &Fingerprint) -> String {
61 let mut out = String::with_capacity(fp.len() * 2);
62 for b in fp {
63 write!(&mut out, "{b:02x}").expect("write to String is infallible");
64 }
65 out
66}
67
68fn canonicalize_program(program: &[SNode]) -> String {
69 let mut lines: Vec<String> = program.iter().filter_map(canonicalize_top_level).collect();
70 lines.sort();
71 lines.join("\n")
72}
73
74fn canonicalize_top_level(snode: &SNode) -> Option<String> {
75 let (_attrs, inner) = peel_attributes(snode);
76 match &inner.node {
77 Node::FnDecl {
78 name,
79 type_params,
80 params,
81 return_type,
82 type_predicate,
83 where_clauses,
84 is_pub,
85 is_stream,
86 ..
87 } => is_pub.then(|| {
88 format!(
89 "fn{stream}:{name}{generics}({params}){ret}{wheres}",
90 stream = if *is_stream { "*" } else { "" },
91 generics = format_type_params(type_params),
92 params = format_typed_params(params),
93 ret = format_return_contract(return_type, type_predicate.as_ref()),
94 wheres = format_where_clauses(where_clauses),
95 )
96 }),
97 Node::Pipeline {
98 name,
99 params,
100 return_type,
101 is_pub,
102 extends,
103 ..
104 } => is_pub.then(|| {
105 format!(
106 "pipeline:{name}({params}){ret}{extends}",
107 params = format_typed_params(params),
108 ret = format_return(return_type),
109 extends = extends
110 .as_deref()
111 .map(|e| format!(" extends {e}"))
112 .unwrap_or_default(),
113 )
114 }),
115 Node::ToolDecl {
116 name,
117 params,
118 return_type,
119 is_pub,
120 ..
121 } => is_pub.then(|| {
122 format!(
123 "tool:{name}({params}){ret}",
124 params = format_typed_params(params),
125 ret = format_return(return_type),
126 )
127 }),
128 Node::SkillDecl { name, is_pub, .. } => {
129 is_pub.then(|| format!("skill:{name}"))
136 }
137 Node::StructDecl {
138 name,
139 type_params,
140 fields,
141 is_pub,
142 } => is_pub.then(|| {
143 format!(
144 "struct:{name}{generics}{{{fields}}}",
145 generics = format_type_params(type_params),
146 fields = format_struct_fields(fields),
147 )
148 }),
149 Node::EnumDecl {
150 name,
151 type_params,
152 variants,
153 is_pub,
154 } => is_pub.then(|| {
155 format!(
156 "enum:{name}{generics}{{{variants}}}",
157 generics = format_type_params(type_params),
158 variants = format_enum_variants(variants),
159 )
160 }),
161 Node::InterfaceDecl {
162 name,
163 type_params,
164 associated_types,
165 methods,
166 } => Some(format!(
167 "interface:{name}{generics}{{assoc=[{assoc}]methods=[{methods}]}}",
168 generics = format_type_params(type_params),
169 assoc = format_associated_types(associated_types),
170 methods = format_interface_methods(methods),
171 )),
172 Node::TypeDecl {
173 name,
174 type_params,
175 type_expr,
176 is_pub,
177 } => is_pub.then(|| {
178 format!(
179 "type:{name}{generics}={ty}",
180 generics = format_type_params(type_params),
181 ty = format_type_expr(type_expr),
182 )
183 }),
184 Node::ImportDecl { path, is_pub } => is_pub.then(|| format!("pub_import_wildcard:{path}")),
185 Node::SelectiveImport {
186 names,
187 path,
188 is_pub,
189 } => is_pub.then(|| {
190 let mut sorted = names.clone();
191 sorted.sort();
192 format!("pub_import_selective:{path}::{}", sorted.join(","))
193 }),
194 Node::NamespaceImport {
195 alias,
196 path,
197 is_pub,
198 } => is_pub.then(|| format!("pub_import_namespace:{path}::{alias}")),
199 _ => None,
200 }
201}
202
203fn format_type_params(params: &[TypeParam]) -> String {
204 if params.is_empty() {
205 return String::new();
206 }
207 let parts: Vec<String> = params
208 .iter()
209 .map(|p| {
210 let var = match p.variance {
211 Variance::Invariant => "",
212 Variance::Covariant => "out ",
213 Variance::Contravariant => "in ",
214 };
215 format!("{var}{}", p.name)
216 })
217 .collect();
218 format!("<{}>", parts.join(","))
219}
220
221fn format_typed_params(params: &[TypedParam]) -> String {
222 params
223 .iter()
224 .map(|p| {
225 let mut s = String::new();
226 if p.rest {
227 s.push_str("...");
228 }
229 s.push_str(&p.name);
230 if let Some(ty) = &p.type_expr {
231 s.push(':');
232 s.push_str(&format_type_expr(ty));
233 }
234 if p.default_value.is_some() {
238 s.push_str("=?");
239 }
240 s
241 })
242 .collect::<Vec<_>>()
243 .join(",")
244}
245
246fn format_return(ret: &Option<TypeExpr>) -> String {
247 match ret {
248 Some(ty) => format!("->{}", format_type_expr(ty)),
249 None => String::new(),
250 }
251}
252
253fn format_return_contract(
254 ret: &Option<TypeExpr>,
255 predicate: Option<&harn_parser::TypePredicate>,
256) -> String {
257 match predicate {
258 Some(predicate) => format!(
259 "->{}{} is {}",
260 if predicate.one_sided { "implies " } else { "" },
261 predicate.parameter,
262 format_type_expr(&predicate.type_expr)
263 ),
264 None => format_return(ret),
265 }
266}
267
268fn format_where_clauses(clauses: &[WhereClause]) -> String {
269 if clauses.is_empty() {
270 return String::new();
271 }
272 let mut parts: Vec<String> = clauses
273 .iter()
274 .map(|w| format!("{}:{}", w.type_name, format_type_expr(&w.bound)))
275 .collect();
276 parts.sort();
277 format!(" where {}", parts.join(","))
278}
279
280fn format_struct_fields(fields: &[StructField]) -> String {
281 let mut rendered: Vec<String> = fields
282 .iter()
283 .map(|f| {
284 let opt = if f.optional { "?" } else { "" };
285 let ty = f
286 .type_expr
287 .as_ref()
288 .map(format_type_expr)
289 .unwrap_or_default();
290 format!("{}{opt}:{ty}", f.name)
291 })
292 .collect();
293 rendered.sort();
294 rendered.join(",")
295}
296
297fn format_enum_variants(variants: &[EnumVariant]) -> String {
298 let mut rendered: Vec<String> = variants
299 .iter()
300 .map(|v| format!("{}({})", v.name, format_typed_params(&v.fields)))
301 .collect();
302 rendered.sort();
303 rendered.join(",")
304}
305
306fn format_associated_types(items: &[AssociatedType]) -> String {
309 let mut rendered: Vec<String> = items
310 .iter()
311 .map(|item| match &item.default {
312 Some(ty) => format!("{}:{}", item.name, format_type_expr(ty)),
313 None => item.name.clone(),
314 })
315 .collect();
316 rendered.sort();
317 rendered.join(",")
318}
319
320fn format_interface_methods(methods: &[InterfaceMethod]) -> String {
321 let mut rendered: Vec<String> = methods
322 .iter()
323 .map(|m| {
324 format!(
325 "{}{}({}){}",
326 m.name,
327 format_type_params(&m.type_params),
328 format_typed_params(&m.params),
329 format_return(&m.return_type),
330 )
331 })
332 .collect();
333 rendered.sort();
334 rendered.join(",")
335}
336
337fn format_type_expr(ty: &TypeExpr) -> String {
338 match ty {
339 TypeExpr::Named(name) => name.clone(),
340 TypeExpr::Union(parts) => {
341 let mut rendered: Vec<String> = parts.iter().map(format_type_expr).collect();
342 rendered.sort();
343 format!("({})", rendered.join("|"))
344 }
345 TypeExpr::Intersection(parts) => {
346 let mut rendered: Vec<String> = parts.iter().map(format_type_expr).collect();
347 rendered.sort();
348 format!("({})", rendered.join("&"))
349 }
350 TypeExpr::Shape(fields) => format!("{{{}}}", format_shape_fields(fields)),
351 TypeExpr::OpenShape { fields, rests } => {
352 let tails: Vec<String> = rests
353 .iter()
354 .map(|r| format!("...{}", format_type_expr(r)))
355 .collect();
356 format!("{{{}|{}}}", format_shape_fields(fields), tails.join(","))
357 }
358 TypeExpr::List(inner) => format!("list<{}>", format_type_expr(inner)),
359 TypeExpr::Tuple(items) => {
360 let rendered: Vec<String> = items.iter().map(format_type_expr).collect();
361 format!("tuple<{}>", rendered.join(","))
362 }
363 TypeExpr::DictType(k, v) => {
364 format!("dict<{},{}>", format_type_expr(k), format_type_expr(v))
365 }
366 TypeExpr::Iter(inner) => format!("iter<{}>", format_type_expr(inner)),
367 TypeExpr::Generator(inner) => format!("Generator<{}>", format_type_expr(inner)),
368 TypeExpr::Stream(inner) => format!("Stream<{}>", format_type_expr(inner)),
369 TypeExpr::Applied { name, args } => {
370 let rendered: Vec<String> = args.iter().map(format_type_expr).collect();
371 format!("{name}<{}>", rendered.join(","))
372 }
373 TypeExpr::FnType {
374 params,
375 return_type,
376 } => {
377 let rendered: Vec<String> = params.iter().map(format_type_expr).collect();
378 format!(
379 "fn({})->{}",
380 rendered.join(","),
381 format_type_expr(return_type)
382 )
383 }
384 TypeExpr::Never => "Never".to_string(),
385 TypeExpr::LitString(s) => format!("\"{s}\""),
386 TypeExpr::LitInt(n) => n.to_string(),
387 TypeExpr::Owned(inner) => format!("owned<{}>", format_type_expr(inner)),
388 }
389}
390
391fn format_shape_fields(fields: &[ShapeField]) -> String {
392 let mut rendered: Vec<String> = fields
393 .iter()
394 .map(|f| {
395 let opt = if f.optional { "?" } else { "" };
396 format!("{}{opt}:{}", f.name, format_type_expr(&f.type_expr))
397 })
398 .collect();
399 rendered.sort();
400 rendered.join(",")
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn fp(source: &str) -> Fingerprint {
408 fingerprint_source(source).expect("source parses")
409 }
410
411 #[test]
412 fn private_body_change_does_not_flip_fingerprint() {
413 let before = fp("pub fn add(a: int, b: int) -> int { a + b }\n");
414 let after = fp("pub fn add(a: int, b: int) -> int { const s = a + b; s }\n");
415 assert_eq!(before, after);
416 }
417
418 #[test]
419 fn private_helper_does_not_flip_fingerprint() {
420 let before = fp("pub fn entry() { internal() }\nfn internal() { 1 }\n");
421 let after = fp("pub fn entry() { internal() }\nfn internal() { 2 }\nfn extra() { 3 }\n");
422 assert_eq!(before, after);
423 }
424
425 #[test]
426 fn reordering_public_decls_does_not_flip_fingerprint() {
427 let a = fp("pub fn alpha() {}\npub fn beta() {}\n");
428 let b = fp("pub fn beta() {}\npub fn alpha() {}\n");
429 assert_eq!(a, b);
430 }
431
432 #[test]
433 fn changing_public_signature_flips_fingerprint() {
434 let before = fp("pub fn add(a: int, b: int) -> int { a + b }\n");
435 let after = fp("pub fn add(a: int, b: int, c: int) -> int { a + b + c }\n");
436 assert_ne!(before, after);
437 }
438
439 #[test]
440 fn changing_public_return_type_flips_fingerprint() {
441 let before = fp("pub fn make() -> string { \"x\" }\n");
442 let after = fp("pub fn make() -> int { 1 }\n");
443 assert_ne!(before, after);
444 }
445
446 #[test]
447 fn changing_public_type_predicate_flips_fingerprint() {
448 let before = fp("pub fn check(value: string | int) -> value is string { return true }\n");
449 let after =
450 fp("pub fn check(value: string | int) -> implies value is string { return true }\n");
451 assert_ne!(before, after);
452 }
453
454 #[test]
455 fn changing_public_pipeline_parameter_type_flips_fingerprint() {
456 let before = fp("pub pipeline deploy(config: string) -> bool { true }\n");
457 let after = fp("pub pipeline deploy(config: dict) -> bool { true }\n");
458 assert_ne!(before, after);
459 }
460
461 #[test]
462 fn adding_pub_struct_field_flips_fingerprint() {
463 let before = fp("pub struct Point { x: int, y: int }\n");
464 let after = fp("pub struct Point { x: int, y: int, z: int }\n");
465 assert_ne!(before, after);
466 }
467
468 #[test]
469 fn pub_re_export_change_flips_fingerprint() {
470 let before = fp("pub import { foo } from \"./a\"\n");
471 let after = fp("pub import { foo, bar } from \"./a\"\n");
472 assert_ne!(before, after);
473 }
474
475 #[test]
476 fn adding_pub_decl_flips_fingerprint() {
477 let before = fp("pub fn alpha() {}\n");
478 let after = fp("pub fn alpha() {}\npub fn beta() {}\n");
479 assert_ne!(before, after);
480 }
481
482 #[test]
483 fn changing_only_non_pub_imports_does_not_flip_fingerprint() {
484 let before = fp("import \"./a\"\npub fn entry() {}\n");
485 let after = fp("import \"./b\"\npub fn entry() {}\n");
486 assert_eq!(before, after);
489 }
490}