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