Skip to main content

vihaco_module/
lowering.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4//! Function scopes and lookups for metadata-backed instruction lowering.
5
6use crate::module::ProgramMetadata;
7use std::collections::HashMap;
8
9/// Prepare a function's local symbols before lowering any of its instructions.
10///
11/// Consumers with additional lowering capabilities can implement this trait on
12/// their own context and delegate to [`MetadataLoweringContext`]. Label offsets
13/// count instructions within the function; declarations occupy no code slots.
14pub trait FunctionScope {
15    /// Replace the active function's labels, including forward references.
16    fn enter_function<'a>(
17        &mut self,
18        name: &str,
19        labels: impl IntoIterator<Item = (&'a str, usize)>,
20    ) -> eyre::Result<()>;
21}
22
23/// Symbol lookups backed by explicit, immutable program metadata.
24///
25/// Function indices retain metadata table order, independently of executable
26/// addresses. String lookups never append to or reorder the supplied table.
27/// The context is independent of any instruction set; dialect crates provide
28/// implementations of their lowering-capability traits for it.
29#[derive(Debug)]
30pub struct MetadataLoweringContext {
31    strings: HashMap<String, u32>,
32    functions: HashMap<String, (u32, u32)>,
33    labels: HashMap<String, u32>,
34}
35
36impl MetadataLoweringContext {
37    /// Prepare section-wide string and function lookups.
38    ///
39    /// # Errors
40    /// Rejects invalid function-name indices, duplicate function names, and
41    /// tables whose indices cannot be represented by `u32`. Full program
42    /// validation remains the responsibility of the metadata loading driver.
43    pub fn new<V, Ty, Info>(metadata: &ProgramMetadata<V, Ty, Info>) -> eyre::Result<Self> {
44        let mut strings = HashMap::new();
45        for (index, text) in metadata.strings.iter().enumerate() {
46            strings.entry(text.clone()).or_insert(u32::try_from(index)?);
47        }
48        let mut functions = HashMap::new();
49        for (index, function) in metadata.functions.iter().enumerate() {
50            let name = metadata
51                .strings
52                .get(function.name as usize)
53                .ok_or_else(|| {
54                    eyre::eyre!(
55                        "metadata.functions[{index}].name: invalid string index {}",
56                        function.name
57                    )
58                })?;
59            eyre::ensure!(
60                functions
61                    .insert(
62                        name.clone(),
63                        (u32::try_from(index)?, function.start_address)
64                    )
65                    .is_none(),
66                "function `@{name}` has duplicate metadata"
67            );
68        }
69        Ok(Self {
70            strings,
71            functions,
72            labels: HashMap::new(),
73        })
74    }
75
76    /// Look up a function's executable start address.
77    pub fn function_address(&self, name: &str) -> eyre::Result<u32> {
78        self.functions
79            .get(name)
80            .map(|function| function.1)
81            .ok_or_else(|| eyre::eyre!("undefined function `@{name}`"))
82    }
83
84    /// Look up a function's index in the supplied metadata table.
85    pub fn function_index(&self, name: &str) -> eyre::Result<u32> {
86        self.functions
87            .get(name)
88            .map(|function| function.0)
89            .ok_or_else(|| eyre::eyre!("undefined function `@{name}`"))
90    }
91
92    /// Look up a label in the current function's scope.
93    pub fn label_address(&self, name: &str) -> eyre::Result<u32> {
94        self.labels
95            .get(name)
96            .copied()
97            .ok_or_else(|| eyre::eyre!("undefined label `@{name}`"))
98    }
99
100    /// Look up decoded string contents without modifying the supplied table.
101    /// Equal strings resolve to the first matching table entry.
102    pub fn string_index(&self, text: &str) -> eyre::Result<u32> {
103        self.strings
104            .get(text)
105            .copied()
106            .ok_or_else(|| eyre::eyre!("string literal `{text}` is missing from metadata.strings"))
107    }
108}
109
110impl FunctionScope for MetadataLoweringContext {
111    fn enter_function<'a>(
112        &mut self,
113        name: &str,
114        labels: impl IntoIterator<Item = (&'a str, usize)>,
115    ) -> eyre::Result<()> {
116        self.labels.clear();
117        let start = self.function_address(name)?;
118        for (label, offset) in labels {
119            let address = start
120                .checked_add(u32::try_from(offset)?)
121                .ok_or_else(|| eyre::eyre!("label address overflow"))?;
122            eyre::ensure!(
123                self.labels.insert(label.to_owned(), address).is_none(),
124                "duplicate label `@{label}` in function `@{name}`"
125            );
126        }
127        Ok(())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::module::{FunctionInfo, NoInfo, Signature};
135
136    fn metadata() -> ProgramMetadata {
137        ProgramMetadata {
138            strings: vec![
139                "later".into(),
140                "first".into(),
141                "hello".into(),
142                "hello".into(),
143            ],
144            functions: vec![
145                FunctionInfo {
146                    name: 0,
147                    signature: Signature {
148                        params: vec![],
149                        ret: vec![],
150                    },
151                    local_count: 3,
152                    start_address: 7,
153                    end_address: 9,
154                    file: 0,
155                },
156                FunctionInfo {
157                    name: 1,
158                    signature: Signature {
159                        params: vec![],
160                        ret: vec![],
161                    },
162                    local_count: 0,
163                    start_address: 0,
164                    end_address: 7,
165                    file: 0,
166                },
167            ],
168            labels: vec![],
169            constants: vec![],
170            main_function: Some(0),
171            source_symbols: vec![],
172            extra: NoInfo,
173        }
174    }
175
176    #[test]
177    fn lookups_preserve_table_indices_and_function_local_scopes() -> eyre::Result<()> {
178        let metadata = metadata();
179        let mut context = MetadataLoweringContext::new(&metadata)?;
180        assert_eq!(context.function_index("later")?, 0);
181        assert_eq!(context.function_address("later")?, 7);
182        assert_eq!(context.function_index("first")?, 1);
183        assert_eq!(context.function_address("first")?, 0);
184        assert_eq!(context.string_index("hello")?, 2);
185        context.enter_function("first", [("done", 6), ("only_first", 1)])?;
186        assert_eq!(context.label_address("done")?, 6);
187        context.enter_function("later", [("done", 1)])?;
188        assert_eq!(context.label_address("done")?, 8);
189        assert!(context.label_address("only_first").is_err());
190        assert!(context.string_index("missing").is_err());
191        assert_eq!(metadata.strings.len(), 4);
192        Ok(())
193    }
194
195    #[test]
196    fn invalid_symbols_and_address_overflow_return_errors() -> eyre::Result<()> {
197        let mut metadata = metadata();
198        let mut context = MetadataLoweringContext::new(&metadata)?;
199        assert!(context.function_index("missing").is_err());
200        assert!(context.enter_function("missing", []).is_err());
201        assert!(
202            context
203                .enter_function("first", [("same", 0), ("same", 1)])
204                .is_err()
205        );
206        metadata.functions[0].start_address = u32::MAX;
207        let mut context = MetadataLoweringContext::new(&metadata)?;
208        assert!(context.enter_function("later", [("overflow", 1)]).is_err());
209        metadata.functions[0].name = 99;
210        assert!(MetadataLoweringContext::new(&metadata).is_err());
211        metadata.functions[0].name = 1;
212        assert!(MetadataLoweringContext::new(&metadata).is_err());
213        Ok(())
214    }
215
216    #[test]
217    fn map_extra_preserves_all_tables_without_requiring_clone() {
218        struct Extra(String);
219        let original = metadata();
220        let converted = metadata().map_extra(|_| Extra("device".into()));
221        let converted = converted.map_extra(|extra| extra.0.len());
222        assert_eq!(converted.extra, 6);
223        assert_eq!(converted.functions, original.functions);
224        assert_eq!(converted.labels, original.labels);
225        assert_eq!(converted.strings, original.strings);
226        assert_eq!(converted.constants, original.constants);
227        assert_eq!(converted.source_symbols, original.source_symbols);
228        assert_eq!(converted.main_function, original.main_function);
229    }
230}