Skip to main content

sim/
compat.rs

1#![forbid(unsafe_code)]
2
3use sim_kernel::{AbiVersion, CapabilityName, Export, LibManifest, LibTarget, Symbol, Version};
4
5/// Order-independent, runtime-id-independent digest of a lib manifest.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct ManifestHash(pub [u8; 32]);
8
9/// Digest of a shape's stable identity and contract flags.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct ShapeHash(pub [u8; 32]);
12
13/// Digest of a codec's surface contract, independent of its runtime ids.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub struct CodecHash(pub [u8; 32]);
16
17/// Computes the stable [`ManifestHash`] of a lib manifest, ignoring
18/// declaration order and runtime-assigned ids.
19pub fn hash_manifest(manifest: &LibManifest) -> ManifestHash {
20    let mut hasher = CanonicalHasher::new("sim.manifest.v1");
21    hash_manifest_into(&mut hasher, manifest);
22    ManifestHash(hasher.finish())
23}
24
25/// Computes the stable [`ShapeHash`] of a shape registered under `symbol`.
26#[cfg(feature = "shape")]
27pub fn hash_shape(symbol: &Symbol, shape: &dyn sim_shape::Shape) -> ShapeHash {
28    let mut hasher = CanonicalHasher::new("sim.shape.v1");
29    write_symbol(&mut hasher, symbol);
30    write_optional_u32(&mut hasher, shape.id().map(|id| id.0));
31    write_bool(&mut hasher, shape.is_effectful());
32    ShapeHash(hasher.finish())
33}
34
35#[cfg(any(
36    feature = "codec-lisp",
37    feature = "codec-json",
38    feature = "codec-binary",
39    feature = "codec-binary-base64",
40    feature = "codec-chat",
41    feature = "codec-algol"
42))]
43/// Computes the stable [`CodecHash`] of a runtime codec from its surface
44/// contract, ignoring runtime-assigned ids.
45pub fn hash_codec(
46    cx: &mut sim_kernel::Cx,
47    codec: &sim_codec::CodecRuntime,
48) -> sim_kernel::Result<CodecHash> {
49    use sim_kernel::Expr;
50
51    fn shape_symbol(
52        cx: &mut sim_kernel::Cx,
53        value: &sim_kernel::Value,
54    ) -> sim_kernel::Result<Symbol> {
55        match value.object().as_expr(cx)? {
56            Expr::Symbol(symbol) => Ok(symbol),
57            other => Err(sim_kernel::Error::HostError(format!(
58                "shape ref did not lower to a symbol: {:?}",
59                other
60            ))),
61        }
62    }
63
64    let mut hasher = CanonicalHasher::new("sim.codec.v1");
65    write_symbol(&mut hasher, &codec.symbol);
66    write_bool(&mut hasher, codec.decoder.is_some());
67    write_bool(&mut hasher, codec.located_decoder.is_some());
68    write_bool(&mut hasher, codec.tree_decoder.is_some());
69    write_bool(&mut hasher, codec.encoder.is_some());
70    write_bool(&mut hasher, codec.located_encoder.is_some());
71    write_bool(&mut hasher, codec.tree_encoder.is_some());
72    write_str(&mut hasher, codec.default_decode.as_symbol_name());
73    write_symbol(&mut hasher, &shape_symbol(cx, &codec.expr_shape)?);
74    write_symbol(&mut hasher, &shape_symbol(cx, &codec.options_shape)?);
75    Ok(CodecHash(hasher.finish()))
76}
77
78#[derive(Clone, Copy)]
79struct CanonicalHasher {
80    state: [u64; 4],
81}
82
83impl CanonicalHasher {
84    fn new(domain: &'static str) -> Self {
85        let mut hasher = Self {
86            state: [
87                0x243f_6a88_85a3_08d3,
88                0x1319_8a2e_0370_7344,
89                0xa409_3822_299f_31d0,
90                0x082e_fa98_ec4e_6c89,
91            ],
92        };
93        hasher.write_bytes(domain.as_bytes());
94        hasher
95    }
96
97    fn write_bytes(&mut self, bytes: &[u8]) {
98        self.mix_len(bytes.len() as u64);
99        for (index, byte) in bytes.iter().enumerate() {
100            let lane = index & 3;
101            self.state[lane] ^= u64::from(*byte) + ((index as u64) << 8);
102            self.state[lane] = self.state[lane]
103                .rotate_left(13)
104                .wrapping_mul(0x1000_0000_01b3);
105            self.state[(lane + 1) & 3] ^= self.state[lane].rotate_right(7);
106        }
107    }
108
109    fn mix_len(&mut self, len: u64) {
110        for lane in 0..4 {
111            self.state[lane] ^= len.wrapping_mul((lane as u64) + 0x9e37_79b9);
112            self.state[lane] = self.state[lane].rotate_left(17);
113        }
114    }
115
116    fn finish(mut self) -> [u8; 32] {
117        self.mix_len(0xff);
118        for lane in 0..4 {
119            let other = self.state[(lane + 1) & 3];
120            self.state[lane] ^= other.rotate_left(11);
121            self.state[lane] = self.state[lane].wrapping_mul(0x9e37_79b1_85eb_ca87);
122        }
123
124        let mut out = [0u8; 32];
125        for (index, lane) in self.state.into_iter().enumerate() {
126            out[index * 8..(index + 1) * 8].copy_from_slice(&lane.to_le_bytes());
127        }
128        out
129    }
130}
131
132fn hash_manifest_into(hasher: &mut CanonicalHasher, manifest: &LibManifest) {
133    write_symbol(hasher, &manifest.id);
134    write_version(hasher, &manifest.version);
135    write_abi_version(hasher, manifest.abi);
136    write_lib_target(hasher, manifest.target.clone());
137
138    let mut requires = manifest.requires.clone();
139    requires.sort_by(|left, right| {
140        (
141            left.id.namespace.as_ref().map(|value| value.as_ref()),
142            left.id.name.as_ref(),
143            left.minimum_version.as_ref().map(|value| value.0.as_str()),
144        )
145            .cmp(&(
146                right.id.namespace.as_ref().map(|value| value.as_ref()),
147                right.id.name.as_ref(),
148                right.minimum_version.as_ref().map(|value| value.0.as_str()),
149            ))
150    });
151    write_usize(hasher, requires.len());
152    for dependency in &requires {
153        write_symbol(hasher, &dependency.id);
154        write_optional_version(hasher, dependency.minimum_version.as_ref());
155    }
156
157    let mut capabilities = manifest.capabilities.clone();
158    capabilities.sort_by(|left, right| left.as_str().cmp(right.as_str()));
159    write_usize(hasher, capabilities.len());
160    for capability in &capabilities {
161        write_capability(hasher, capability);
162    }
163
164    let mut exports = manifest.exports.clone();
165    exports.sort_by(|left, right| {
166        (left.kind(), left.symbol().as_qualified_str())
167            .cmp(&(right.kind(), right.symbol().as_qualified_str()))
168    });
169    write_usize(hasher, exports.len());
170    for export in &exports {
171        hash_export_into(hasher, export);
172    }
173}
174
175fn hash_export_into(hasher: &mut CanonicalHasher, export: &Export) {
176    write_str(hasher, export.kind());
177    write_symbol(hasher, export.symbol());
178}
179
180fn write_lib_target(hasher: &mut CanonicalHasher, target: LibTarget) {
181    write_str(hasher, &target.to_symbol().as_qualified_str());
182}
183
184fn write_symbol(hasher: &mut CanonicalHasher, symbol: &Symbol) {
185    write_optional_str(hasher, symbol.namespace.as_deref());
186    write_str(hasher, symbol.name.as_ref());
187}
188
189fn write_version(hasher: &mut CanonicalHasher, version: &Version) {
190    write_str(hasher, &version.0);
191}
192
193fn write_optional_version(hasher: &mut CanonicalHasher, version: Option<&Version>) {
194    match version {
195        Some(version) => {
196            write_bool(hasher, true);
197            write_version(hasher, version);
198        }
199        None => write_bool(hasher, false),
200    }
201}
202
203fn write_abi_version(hasher: &mut CanonicalHasher, abi: AbiVersion) {
204    write_u16(hasher, abi.major);
205    write_u16(hasher, abi.minor);
206}
207
208fn write_capability(hasher: &mut CanonicalHasher, capability: &CapabilityName) {
209    write_str(hasher, capability.as_str());
210}
211
212fn write_optional_str(hasher: &mut CanonicalHasher, value: Option<&str>) {
213    match value {
214        Some(value) => {
215            write_bool(hasher, true);
216            write_str(hasher, value);
217        }
218        None => write_bool(hasher, false),
219    }
220}
221
222#[cfg(feature = "shape")]
223fn write_optional_u32(hasher: &mut CanonicalHasher, value: Option<u32>) {
224    match value {
225        Some(value) => {
226            write_bool(hasher, true);
227            write_u32(hasher, value);
228        }
229        None => write_bool(hasher, false),
230    }
231}
232
233fn write_bool(hasher: &mut CanonicalHasher, value: bool) {
234    hasher.write_bytes(&[u8::from(value)]);
235}
236
237fn write_u16(hasher: &mut CanonicalHasher, value: u16) {
238    hasher.write_bytes(&value.to_le_bytes());
239}
240
241#[cfg(feature = "shape")]
242fn write_u32(hasher: &mut CanonicalHasher, value: u32) {
243    hasher.write_bytes(&value.to_le_bytes());
244}
245
246fn write_usize(hasher: &mut CanonicalHasher, value: usize) {
247    hasher.write_bytes(&(value as u64).to_le_bytes());
248}
249
250fn write_str(hasher: &mut CanonicalHasher, value: &str) {
251    hasher.write_bytes(value.as_bytes());
252}
253
254#[cfg(test)]
255mod tests {
256    use sim_kernel::{CapabilityName, Export, LibManifest, LibTarget, Symbol, Version};
257
258    use crate::compat::hash_manifest;
259
260    fn sample_manifest() -> LibManifest {
261        LibManifest {
262            id: Symbol::qualified("demo", "lib"),
263            version: Version("1.2.3".to_owned()),
264            abi: sim_kernel::AbiVersion { major: 1, minor: 0 },
265            target: LibTarget::CodecSource(Symbol::qualified("codec", "lisp")),
266            requires: vec![
267                sim_kernel::Dependency {
268                    id: Symbol::qualified("z", "later"),
269                    minimum_version: Some(Version("2.0.0".to_owned())),
270                },
271                sim_kernel::Dependency {
272                    id: Symbol::qualified("a", "first"),
273                    minimum_version: None,
274                },
275            ],
276            capabilities: vec![
277                CapabilityName::new("macro.expand"),
278                CapabilityName::new("read-construct"),
279            ],
280            exports: vec![
281                Export::Function {
282                    symbol: Symbol::qualified("demo", "f"),
283                    function_id: Some(sim_kernel::FunctionId(7)),
284                },
285                Export::Class {
286                    symbol: Symbol::qualified("demo", "C"),
287                    class_id: Some(sim_kernel::ClassId(222)),
288                },
289            ],
290        }
291    }
292
293    #[test]
294    fn manifest_hash_ignores_declaration_order_and_runtime_ids() {
295        let left = sample_manifest();
296        let mut right = sample_manifest();
297        right.requires.reverse();
298        right.capabilities.reverse();
299        right.exports.reverse();
300        right.exports = vec![
301            Export::Class {
302                symbol: Symbol::qualified("demo", "C"),
303                class_id: None,
304            },
305            Export::Function {
306                symbol: Symbol::qualified("demo", "f"),
307                function_id: None,
308            },
309        ];
310
311        assert_eq!(hash_manifest(&left), hash_manifest(&right));
312    }
313
314    #[test]
315    #[cfg(feature = "shape")]
316    fn shape_hash_uses_symbol_and_canonical_shape_flags() {
317        use std::sync::Arc;
318
319        use crate::compat::hash_shape;
320        use crate::shape::{AnyShape, EffectfulShape};
321
322        let plain = AnyShape;
323        let effectful = EffectfulShape::new(Arc::new(AnyShape));
324
325        assert_eq!(
326            hash_shape(&Symbol::qualified("demo", "shape"), &plain),
327            hash_shape(&Symbol::qualified("demo", "shape"), &AnyShape)
328        );
329        assert_ne!(
330            hash_shape(&Symbol::qualified("demo", "shape"), &plain),
331            hash_shape(&Symbol::qualified("demo", "shape"), &effectful)
332        );
333    }
334
335    #[test]
336    #[cfg(all(
337        feature = "shape",
338        any(
339            feature = "codec-lisp",
340            feature = "codec-json",
341            feature = "codec-binary",
342            feature = "codec-binary-base64",
343            feature = "codec-chat",
344            feature = "codec-algol"
345        )
346    ))]
347    fn codec_hash_ignores_runtime_ids_and_uses_surface_contract() {
348        use std::sync::Arc;
349
350        use sim_kernel::{DefaultFactory, EagerPolicy};
351
352        use crate::{
353            codec::{CodecDefaultDecode, CodecRuntime},
354            compat::hash_codec,
355            runtime::install_core_runtime,
356        };
357
358        let mut cx = sim_kernel::Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
359        install_core_runtime(&mut cx);
360        let expr_shape = cx
361            .registry()
362            .shape_by_symbol(&Symbol::qualified("core", "Expr"))
363            .cloned()
364            .unwrap();
365        let options_shape = cx
366            .registry()
367            .shape_by_symbol(&Symbol::qualified("core", "EncodeOptions"))
368            .cloned()
369            .unwrap();
370
371        let left = CodecRuntime {
372            id: sim_kernel::CodecId(1),
373            symbol: Symbol::qualified("codec", "demo"),
374            decoder: None,
375            located_decoder: None,
376            tree_decoder: None,
377            encoder: None,
378            located_encoder: None,
379            tree_encoder: None,
380            expr_shape: expr_shape.clone(),
381            options_shape: options_shape.clone(),
382            default_decode: CodecDefaultDecode::Datum,
383        };
384        let right = CodecRuntime {
385            id: sim_kernel::CodecId(99),
386            symbol: Symbol::qualified("codec", "demo"),
387            decoder: None,
388            located_decoder: None,
389            tree_decoder: None,
390            encoder: None,
391            located_encoder: None,
392            tree_encoder: None,
393            expr_shape,
394            options_shape,
395            default_decode: CodecDefaultDecode::Datum,
396        };
397
398        assert_eq!(
399            hash_codec(&mut cx, &left).unwrap(),
400            hash_codec(&mut cx, &right).unwrap()
401        );
402    }
403}