Skip to main content

triblespace_core/value/schemas/
linelocation.rs

1use crate::id::ExclusiveId;
2use crate::id::Id;
3use crate::id_hex;
4use crate::macros::entity;
5use crate::metadata;
6use crate::metadata::{ConstDescribe, ConstId};
7use crate::repo::BlobStore;
8use crate::trible::Fragment;
9use crate::value::schemas::hash::Blake3;
10use crate::value::TryFromValue;
11use crate::value::RawValue;
12use crate::value::ToValue;
13use crate::value::Value;
14use crate::value::ValueSchema;
15use proc_macro::Span;
16use std::convert::Infallible;
17
18/// A value schema for representing a span using explicit line and column
19/// coordinates.
20#[derive(Debug, Clone, Copy)]
21pub struct LineLocation;
22
23impl ConstId for LineLocation {
24    const ID: Id = id_hex!("DFAED173A908498CB893A076EAD3E578");
25}
26
27impl ConstDescribe for LineLocation {
28    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
29    where
30        B: BlobStore<Blake3>,
31    {
32        let id = Self::ID;
33        let description = blobs.put(
34            "Line/column span encoded as four big-endian u64 values (start_line, start_col, end_line, end_col). This captures explicit source positions rather than byte offsets.\n\nUse for editor diagnostics, source maps, or human-facing spans. If you need byte offsets or length-based ranges, use RangeU128 instead.\n\nColumns are raw counts and do not account for variable-width graphemes. Store any display conventions separately if needed.",
35        )?;
36        let tribles = entity! {
37            ExclusiveId::force_ref(&id) @
38                metadata::name: blobs.put("line_location")?,
39                metadata::description: description,
40                metadata::tag: metadata::KIND_VALUE_SCHEMA,
41        };
42
43        #[cfg(feature = "wasm")]
44        let tribles = {
45            let mut tribles = tribles;
46            tribles += entity! { ExclusiveId::force_ref(&id) @
47                metadata::value_formatter: blobs.put(wasm_formatter::LINELOCATION_WASM)?,
48            };
49            tribles
50        };
51        Ok(tribles)
52    }
53}
54
55#[cfg(feature = "wasm")]
56mod wasm_formatter {
57    use core::fmt::Write;
58
59    use triblespace_core_macros::value_formatter;
60
61    #[value_formatter]
62    pub(crate) fn linelocation(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
63        let mut buf = [0u8; 8];
64        buf.copy_from_slice(&raw[..8]);
65        let start_line = u64::from_be_bytes(buf);
66        buf.copy_from_slice(&raw[8..16]);
67        let start_col = u64::from_be_bytes(buf);
68        buf.copy_from_slice(&raw[16..24]);
69        let end_line = u64::from_be_bytes(buf);
70        buf.copy_from_slice(&raw[24..]);
71        let end_col = u64::from_be_bytes(buf);
72
73        write!(out, "{start_line}:{start_col}..{end_line}:{end_col}").map_err(|_| 1u32)?;
74        Ok(())
75    }
76}
77
78impl ValueSchema for LineLocation {
79    type ValidationError = Infallible;
80}
81
82fn encode_location(lines: (u64, u64, u64, u64)) -> RawValue {
83    let mut raw = [0u8; 32];
84    raw[..8].copy_from_slice(&lines.0.to_be_bytes());
85    raw[8..16].copy_from_slice(&lines.1.to_be_bytes());
86    raw[16..24].copy_from_slice(&lines.2.to_be_bytes());
87    raw[24..].copy_from_slice(&lines.3.to_be_bytes());
88    raw
89}
90
91fn decode_location(raw: &RawValue) -> (u64, u64, u64, u64) {
92    let mut first = [0u8; 8];
93    let mut second = [0u8; 8];
94    let mut third = [0u8; 8];
95    let mut fourth = [0u8; 8];
96    first.copy_from_slice(&raw[..8]);
97    second.copy_from_slice(&raw[8..16]);
98    third.copy_from_slice(&raw[16..24]);
99    fourth.copy_from_slice(&raw[24..]);
100    (
101        u64::from_be_bytes(first),
102        u64::from_be_bytes(second),
103        u64::from_be_bytes(third),
104        u64::from_be_bytes(fourth),
105    )
106}
107
108impl ToValue<LineLocation> for (u64, u64, u64, u64) {
109    fn to_value(self) -> Value<LineLocation> {
110        Value::new(encode_location(self))
111    }
112}
113
114impl TryFromValue<'_, LineLocation> for (u64, u64, u64, u64) {
115    type Error = Infallible;
116    fn try_from_value(v: &Value<LineLocation>) -> Result<Self, Infallible> {
117        Ok(decode_location(&v.raw))
118    }
119}
120
121impl ToValue<LineLocation> for Span {
122    fn to_value(self) -> Value<LineLocation> {
123        (
124            self.start().line() as u64,
125            self.start().column() as u64,
126            self.end().line() as u64,
127            self.end().column() as u64,
128        )
129            .to_value()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::value::ToValue;
137    use proptest::prelude::*;
138
139    proptest! {
140        #[test]
141        fn tuple_roundtrip(a: u64, b: u64, c: u64, d: u64) {
142            let input = (a, b, c, d);
143            let value: Value<LineLocation> = input.to_value();
144            let output: (u64, u64, u64, u64) = value.from_value();
145            prop_assert_eq!(input, output);
146        }
147
148        #[test]
149        fn validates(a: u64, b: u64, c: u64, d: u64) {
150            let value: Value<LineLocation> = (a, b, c, d).to_value();
151            prop_assert!(LineLocation::validate(value).is_ok());
152        }
153    }
154}