Skip to main content

stack_assembly/
script.rs

1use std::{collections::BTreeMap, fmt, ops::Range};
2
3use crate::Effect;
4
5/// # A compiled script
6///
7/// To evaluate a script, you must first compile its textual representation into
8/// an instance of this struct, using [`Script::compile`]. Afterwards, you can
9/// evaluate the script using [`Eval`].
10///
11/// [`Eval`]: crate::Eval
12#[derive(Debug)]
13pub struct Script {
14    operators: Vec<Operator>,
15    labels: Vec<Label>,
16    source_map: BTreeMap<OperatorIndex, Range<usize>>,
17}
18
19impl Script {
20    /// # Compile the source text of a script into an instance of `Script`
21    pub fn compile(script: &str) -> Self {
22        let mut next_index = OperatorIndex::default();
23
24        let mut operators = Vec::new();
25        let mut labels = Vec::new();
26        let mut source_map = BTreeMap::new();
27
28        enum State {
29            Initial,
30            Comment,
31            Token { start: usize },
32        }
33        let mut state = State::Initial;
34
35        for (i, ch) in script.char_indices() {
36            match (&state, ch) {
37                (State::Initial, '#') => {
38                    state = State::Comment;
39                }
40                (State::Initial, ch) if !ch.is_whitespace() => {
41                    state = State::Token { start: i };
42                }
43                (State::Initial, _) => {
44                    // Token won't start until we're past the whitespace.
45                }
46                (State::Comment, '\n') => {
47                    state = State::Initial;
48                }
49                (State::Comment, _) => {
50                    // Ignoring characters in comments.
51                }
52                (State::Token { start }, ch) if ch.is_whitespace() => {
53                    parse_token(
54                        script,
55                        *start..i,
56                        &mut operators,
57                        &mut labels,
58                        &mut next_index,
59                        &mut source_map,
60                    );
61                    state = State::Initial;
62                }
63                (State::Token { start: _ }, _) => {
64                    // We already remembered the start of the token. Nothing
65                    // else to do until it's over.
66                }
67            }
68        }
69
70        if let State::Token { start } = state {
71            parse_token(
72                script,
73                start..script.len(),
74                &mut operators,
75                &mut labels,
76                &mut next_index,
77                &mut source_map,
78            );
79        }
80
81        Self {
82            operators,
83            labels,
84            source_map,
85        }
86    }
87
88    pub(crate) fn get_operator(
89        &self,
90        index: OperatorIndex,
91    ) -> Result<&Operator, InvalidOperatorIndex> {
92        let Ok(index): Result<usize, _> = index.value.try_into() else {
93            // We can at most store `usize::MAX` operators, so if we can't make
94            // this conversion, then the index definitely doesn't point to an
95            // operator.
96            return Err(InvalidOperatorIndex);
97        };
98
99        let Some(operator) = self.operators.get(index) else {
100            return Err(InvalidOperatorIndex);
101        };
102
103        Ok(operator)
104    }
105
106    pub(crate) fn resolve_reference(
107        &self,
108        name: &str,
109    ) -> Result<OperatorIndex, InvalidReference> {
110        let label = self.labels.iter().find(|label| label.name == name);
111
112        let Some(&Label { name: _, operator }) = label else {
113            return Err(InvalidReference);
114        };
115
116        Ok(operator)
117    }
118
119    /// # Map the operator identified by the provided index to the source code
120    ///
121    /// The returned range can be used to index into the source string
122    /// originally provided to [`Script::compile`], to get the sub-string that
123    /// was compiled into the operator identified by the provided index.
124    ///
125    /// Returns `None`, if the provided [`OperatorIndex`] does not refer to an
126    /// operator in the script.
127    pub fn map_operator_to_source(
128        &self,
129        operator: &OperatorIndex,
130    ) -> Result<Range<usize>, InvalidOperatorIndex> {
131        let Some(range) = self.source_map.get(operator).cloned() else {
132            return Err(InvalidOperatorIndex);
133        };
134
135        Ok(range)
136    }
137
138    #[cfg(test)]
139    fn operators(&self) -> impl Iterator<Item = (OperatorIndex, &Operator)> {
140        use std::iter;
141
142        let indices =
143            iter::successors(Some(OperatorIndex::default()), |index| {
144                Some(OperatorIndex {
145                    value: index.value + 1,
146                })
147            });
148
149        indices.zip(&self.operators)
150    }
151}
152
153fn parse_token(
154    script: &str,
155    range: Range<usize>,
156    operators: &mut Vec<Operator>,
157    labels: &mut Vec<Label>,
158    next_index: &mut OperatorIndex,
159    source_map: &mut BTreeMap<OperatorIndex, Range<usize>>,
160) {
161    let token = &script[range.clone()];
162
163    let operator = if let Some((name, "")) = token.rsplit_once(":") {
164        let Ok(index) = operators.len().try_into() else {
165            panic!(
166                "Trying to create a label for an operator whose index can't be \
167                represented as `u32`. This is only possible on 64-bit \
168                platforms, when there are more than `u32::MAX` operators in a \
169                script.\n\
170                \n\
171                That this limit can practically be reached with the language \
172                as it currently is, seems highly unlikely. This makes this \
173                panic an acceptable outcome.\n\
174                \n\
175                Long-term, once the API supports compiler errors, this case \
176                should result in an such an error instead."
177            );
178        };
179
180        labels.push(Label {
181            name: name.to_string(),
182            operator: OperatorIndex { value: index },
183        });
184
185        return;
186    } else if let Some(("", name)) = token.split_once("@") {
187        Operator::Reference {
188            name: name.to_string(),
189        }
190    } else if let Some(("", value)) = token.split_once("0x")
191        && let Ok(value) = i32::from_str_radix(value, 16)
192    {
193        Operator::Integer { value }
194    } else if let Some(("", value)) = token.split_once("0x")
195        && let Ok(value) = u32::from_str_radix(value, 16)
196    {
197        Operator::integer_u32(value)
198    } else if let Ok(value) = token.parse::<i32>() {
199        Operator::Integer { value }
200    } else if let Ok(value) = token.parse::<u32>() {
201        Operator::integer_u32(value)
202    } else {
203        Operator::Identifier {
204            value: token.to_string(),
205        }
206    };
207
208    operators.push(operator);
209
210    source_map.insert(*next_index, range);
211    next_index.value += 1;
212}
213
214#[derive(Debug)]
215pub enum Operator {
216    Identifier { value: String },
217    Integer { value: i32 },
218    Reference { name: String },
219}
220
221impl Operator {
222    pub fn integer_u32(value: u32) -> Self {
223        Self::Integer {
224            value: i32::from_le_bytes(value.to_le_bytes()),
225        }
226    }
227}
228
229/// # Refers to an operator in a script
230///
231/// See [`Script`].
232#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
233pub struct OperatorIndex {
234    pub(crate) value: u32,
235}
236
237impl fmt::Display for OperatorIndex {
238    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
239        write!(f, "{}", self.value)
240    }
241}
242
243#[derive(Debug)]
244pub struct Label {
245    pub name: String,
246    pub operator: OperatorIndex,
247}
248
249#[derive(Debug)]
250pub struct InvalidOperatorIndex;
251
252impl From<InvalidOperatorIndex> for Effect {
253    fn from(InvalidOperatorIndex: InvalidOperatorIndex) -> Self {
254        Effect::OutOfOperators
255    }
256}
257
258#[derive(Debug)]
259pub struct InvalidReference;
260
261impl From<InvalidReference> for Effect {
262    fn from(InvalidReference: InvalidReference) -> Self {
263        Effect::InvalidReference
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use crate::Script;
270
271    #[test]
272    fn map_operator_to_source() {
273        let source = "0 loop: 1 + @loop jump";
274        let script = Script::compile(source);
275
276        let operators = script
277            .operators()
278            .map(|(operator, _)| {
279                let Ok(range) = script.map_operator_to_source(&operator) else {
280                    unreachable!(
281                        "Using `OperatorIndex` that definitely refers to an \
282                        operator, as it was returned by `Script::operators`."
283                    );
284                };
285                &source[range]
286            })
287            .collect::<Vec<_>>();
288
289        assert_eq!(operators, vec!["0", "1", "+", "@loop", "jump"]);
290    }
291}