b9ca8894a5
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:
Type::Fn { params, param_modes, ret, ret_mode, effects }
enum ParamMode { Implicit, Own, Borrow }
This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).
Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.
Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.
New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.
Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.
Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
122 lines
4.2 KiB
Rust
122 lines
4.2 KiB
Rust
//! Content-addressed hashing for definitions.
|
|
//!
|
|
//! Hash = BLAKE3 over the canonical JSON bytes of a [`Def`] (see
|
|
//! [`crate::canonical`]). The hash function takes a [`Def`] by
|
|
//! reference, so there is no `hash` field to strip — the in-memory
|
|
//! struct does not carry one.
|
|
//!
|
|
//! The single entry point at this level is [`def_hash`]. The parallel
|
|
//! entry point at module granularity is
|
|
//! [`crate::workspace::module_hash`].
|
|
|
|
use crate::ast::Def;
|
|
use crate::canonical;
|
|
|
|
/// Content hash of a single [`Def`] — the 16-hex-char (64-bit) prefix
|
|
/// of its BLAKE3 hash over canonical JSON bytes.
|
|
///
|
|
/// 64 bits is wide enough to be unique across realistic AILang
|
|
/// codebases and short enough to read at a glance in pretty-printed
|
|
/// manifests. The hash is computed over the **canonical JSON byte
|
|
/// pre-image**, not over the in-memory struct, so any change to the
|
|
/// canonical form (new fields, different `skip_serializing_if`
|
|
/// behaviour, key reordering bug) changes every hash. The Iter 13a
|
|
/// regression test below pins concrete hashes for two example
|
|
/// definitions to catch that.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```ignore
|
|
/// use ailang_core::{ast::*, def_hash};
|
|
///
|
|
/// let def = Def::Const(ConstDef {
|
|
/// name: "answer".into(),
|
|
/// ty: Type::int(),
|
|
/// value: Term::Lit { lit: Literal::Int { value: 42 } },
|
|
/// doc: None,
|
|
/// });
|
|
///
|
|
/// // Stable across runs: same canonical bytes -> same hash.
|
|
/// assert_eq!(def_hash(&def), def_hash(&def));
|
|
/// assert_eq!(def_hash(&def).len(), 16);
|
|
/// ```
|
|
pub fn def_hash(def: &Def) -> String {
|
|
let bytes = canonical::to_bytes(def);
|
|
let h = blake3::hash(&bytes);
|
|
let hex = h.to_hex();
|
|
hex.as_str()[..16].to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::ast::*;
|
|
|
|
fn sample_fn() -> Def {
|
|
Def::Fn(FnDef {
|
|
name: "add".into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::int(), Type::int()],
|
|
ret: Box::new(Type::int()),
|
|
effects: vec![],
|
|
param_modes: vec![],
|
|
ret_mode: ParamMode::Implicit,
|
|
},
|
|
params: vec!["a".into(), "b".into()],
|
|
body: Term::App {
|
|
callee: Box::new(Term::Var { name: "+".into() }),
|
|
args: vec![
|
|
Term::Var { name: "a".into() },
|
|
Term::Var { name: "b".into() },
|
|
],
|
|
tail: false,
|
|
},
|
|
doc: None,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn hash_is_stable() {
|
|
let h1 = def_hash(&sample_fn());
|
|
let h2 = def_hash(&sample_fn());
|
|
assert_eq!(h1, h2);
|
|
assert_eq!(h1.len(), 16);
|
|
}
|
|
|
|
#[test]
|
|
fn hash_changes_with_content() {
|
|
let mut def = sample_fn();
|
|
let h1 = def_hash(&def);
|
|
if let Def::Fn(ref mut f) = def {
|
|
f.name = "mul".into();
|
|
}
|
|
let h2 = def_hash(&def);
|
|
assert_ne!(h1, h2);
|
|
}
|
|
|
|
/// Iter 13a regression: adding `vars` to TypeDef and `args` to
|
|
/// `Type::Con` must NOT change canonical-JSON hashes of any pre-13a
|
|
/// definition. Recorded hashes were captured from on-disk modules
|
|
/// before the schema extension; if this fires, a
|
|
/// `skip_serializing_if` is missing or wrong. We deserialise the
|
|
/// real example modules from disk to avoid drift between the test
|
|
/// and the source-of-truth JSON.
|
|
#[test]
|
|
fn iter13a_schema_extension_preserves_pre_13a_hashes() {
|
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let examples = manifest_dir.join("../../examples");
|
|
|
|
let sum_src = std::fs::read(examples.join("sum.ail.json"))
|
|
.expect("examples/sum.ail.json present");
|
|
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
|
|
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
|
|
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
|
|
|
let list_src = std::fs::read(examples.join("list.ail.json"))
|
|
.expect("examples/list.ail.json present");
|
|
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
|
|
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
|
|
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
|
|
}
|
|
}
|