Skip to main content

bcgen_lang/
options.rs

1//! Compilation options and source locations.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use bytecode_lang::Hook;
7use hir_lang::Span;
8
9/// How arrays and maps behave when a value is copied.
10///
11/// # Examples
12///
13/// ```
14/// use bcgen_lang::ArraySemantics;
15///
16/// assert_eq!(ArraySemantics::default(), ArraySemantics::Php);
17/// ```
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum ArraySemantics {
20    /// PHP arrays: values. Assigning, passing, returning, capturing, or
21    /// storing an array copies it (an O(1) copy-on-write `dup`), a nested
22    /// write (`$a['x']['y'] = 1`) separates each intermediate array from any
23    /// copy that shares it, and writing through `null` creates the array
24    /// (auto-vivification).
25    #[default]
26    Php,
27    /// Shared references, as in Python, JavaScript, and Ruby: a copy is the
28    /// same object, nested writes go through, and writing into `null` is an
29    /// error.
30    Reference,
31}
32
33/// How map keys are normalized before a map is indexed.
34///
35/// # Examples
36///
37/// ```
38/// use bcgen_lang::KeyCoercion;
39///
40/// assert_eq!(KeyCoercion::default(), KeyCoercion::Php);
41/// ```
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
43pub enum KeyCoercion {
44    /// PHP's array-key rules: a string holding a canonical decimal `i64`
45    /// (`"7"`, `"-3"`, not `"07"`, `"-0"`, or `"+1"`) becomes that integer, a
46    /// float is truncated toward zero (saturating at the `i64` range; NaN
47    /// gives `0`), `true`/`false` become `1`/`0`, and `null` becomes `""`.
48    /// Other keys are left as they are.
49    #[default]
50    Php,
51    /// No normalization: keys compare by LSB's rules (`1` and `1.0` differ).
52    None,
53}
54
55/// What the module body (`Module.body`) compiles to.
56///
57/// # Examples
58///
59/// ```
60/// use bcgen_lang::BodyMode;
61///
62/// assert_eq!(BodyMode::default(), BodyMode::Start);
63/// ```
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
65pub enum BodyMode {
66    /// The body is part of the module's start function, which the VM runs
67    /// once before the first call into the module; its value is kept in a
68    /// global that the entry function returns. Scripts and modules.
69    #[default]
70    Start,
71    /// The body is the entry function itself and returns its value on every
72    /// call; the start function only initializes globals. REPL entries and
73    /// tests that run the body more than once.
74    Entry,
75}
76
77/// Compilation options: the dynamic profile's array and key semantics, the
78/// body's role, host linkage, and budgets.
79///
80/// The default is the PHP profile Mox needs: value arrays and PHP keys.
81///
82/// # Examples
83///
84/// ```
85/// use bcgen_lang::{ArraySemantics, Options};
86///
87/// let opts = Options::new()
88///     .with_arrays(ArraySemantics::Reference)
89///     .with_file("app.mox")
90///     .with_max_instructions(1_000_000);
91/// assert_eq!(opts.arrays(), ArraySemantics::Reference);
92/// assert_eq!(opts.file(), "app.mox");
93/// ```
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct Options {
96    arrays: ArraySemantics,
97    keys: KeyCoercion,
98    body: BodyMode,
99    file: String,
100    host_module: String,
101    hooks: Vec<(Hook, String, String)>,
102    max_instructions: u64,
103}
104
105impl Default for Options {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl Options {
112    /// The default budget: 64 Mi instructions per module.
113    pub const DEFAULT_MAX_INSTRUCTIONS: u64 = 64 << 20;
114
115    /// The PHP profile: [`ArraySemantics::Php`], [`KeyCoercion::Php`],
116    /// [`BodyMode::Start`], file name `<hir>`, host module `host`, no hooks.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use bcgen_lang::{BodyMode, Options};
122    ///
123    /// assert_eq!(Options::new().body(), BodyMode::Start);
124    /// assert_eq!(Options::new().host_module(), "host");
125    /// ```
126    #[must_use]
127    pub fn new() -> Self {
128        Self {
129            arrays: ArraySemantics::Php,
130            keys: KeyCoercion::Php,
131            body: BodyMode::Start,
132            file: String::from("<hir>"),
133            host_module: String::from("host"),
134            hooks: Vec::new(),
135            max_instructions: Self::DEFAULT_MAX_INSTRUCTIONS,
136        }
137    }
138
139    /// Sets the array semantics.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use bcgen_lang::{ArraySemantics, Options};
145    ///
146    /// let o = Options::new().with_arrays(ArraySemantics::Reference);
147    /// assert_eq!(o.arrays(), ArraySemantics::Reference);
148    /// ```
149    #[must_use]
150    pub fn with_arrays(mut self, arrays: ArraySemantics) -> Self {
151        self.arrays = arrays;
152        self
153    }
154
155    /// Sets the key coercion.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use bcgen_lang::{KeyCoercion, Options};
161    ///
162    /// assert_eq!(Options::new().with_keys(KeyCoercion::None).keys(), KeyCoercion::None);
163    /// ```
164    #[must_use]
165    pub fn with_keys(mut self, keys: KeyCoercion) -> Self {
166        self.keys = keys;
167        self
168    }
169
170    /// Sets what the module body compiles to.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use bcgen_lang::{BodyMode, Options};
176    ///
177    /// assert_eq!(Options::new().with_body(BodyMode::Entry).body(), BodyMode::Entry);
178    /// ```
179    #[must_use]
180    pub fn with_body(mut self, body: BodyMode) -> Self {
181        self.body = body;
182        self
183    }
184
185    /// Sets the file name written into line-table rows when no
186    /// [`Locate`] is given.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use bcgen_lang::Options;
192    ///
193    /// assert_eq!(Options::new().with_file("main.mox").file(), "main.mox");
194    /// ```
195    #[must_use]
196    pub fn with_file(mut self, file: &str) -> Self {
197        self.file = String::from(file);
198        self
199    }
200
201    /// Sets the import module for host symbols that have no `.` in their
202    /// name (`echo` imports as `host`.`echo` by default; `mox.concat` imports
203    /// as `mox`.`concat` whatever this is).
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use bcgen_lang::Options;
209    ///
210    /// assert_eq!(Options::new().with_host_module("env").host_module(), "env");
211    /// ```
212    #[must_use]
213    pub fn with_host_module(mut self, module: &str) -> Self {
214        self.host_module = String::from(module);
215        self
216    }
217
218    /// Binds an LSB hook (ยง5.8) to the host function `module`.`name`, with
219    /// the hook's signature (every operand `dyn`). A later binding of the same
220    /// hook replaces an earlier one.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use bcgen_lang::Options;
226    /// use bytecode_lang::Hook;
227    ///
228    /// let o = Options::new().with_hook(Hook::Eq, "mox", "loose_eq");
229    /// assert_eq!(o.hooks().len(), 1);
230    /// ```
231    #[must_use]
232    pub fn with_hook(mut self, hook: Hook, module: &str, name: &str) -> Self {
233        self.hooks.retain(|(h, _, _)| *h != hook);
234        self.hooks
235            .push((hook, String::from(module), String::from(name)));
236        self
237    }
238
239    /// Sets the instruction budget for the whole module; compilation stops
240    /// with [`CompileError::BudgetExceeded`](crate::CompileError) beyond it.
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use bcgen_lang::Options;
246    ///
247    /// assert_eq!(Options::new().with_max_instructions(10).max_instructions(), 10);
248    /// ```
249    #[must_use]
250    pub fn with_max_instructions(mut self, limit: u64) -> Self {
251        self.max_instructions = limit;
252        self
253    }
254
255    /// The array semantics.
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// assert_eq!(bcgen_lang::Options::new().arrays(), bcgen_lang::ArraySemantics::Php);
261    /// ```
262    #[must_use]
263    pub fn arrays(&self) -> ArraySemantics {
264        self.arrays
265    }
266
267    /// The key coercion.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// assert_eq!(bcgen_lang::Options::new().keys(), bcgen_lang::KeyCoercion::Php);
273    /// ```
274    #[must_use]
275    pub fn keys(&self) -> KeyCoercion {
276        self.keys
277    }
278
279    /// What the module body compiles to.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// assert_eq!(bcgen_lang::Options::new().body(), bcgen_lang::BodyMode::Start);
285    /// ```
286    #[must_use]
287    pub fn body(&self) -> BodyMode {
288        self.body
289    }
290
291    /// The default file name for line-table rows.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// assert_eq!(bcgen_lang::Options::new().file(), "<hir>");
297    /// ```
298    #[must_use]
299    pub fn file(&self) -> &str {
300        &self.file
301    }
302
303    /// The import module for host symbols without a `.`.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// assert_eq!(bcgen_lang::Options::new().host_module(), "host");
309    /// ```
310    #[must_use]
311    pub fn host_module(&self) -> &str {
312        &self.host_module
313    }
314
315    /// The hook bindings, as (hook, module, name).
316    ///
317    /// # Examples
318    ///
319    /// ```
320    /// assert!(bcgen_lang::Options::new().hooks().is_empty());
321    /// ```
322    #[must_use]
323    pub fn hooks(&self) -> &[(Hook, String, String)] {
324        &self.hooks
325    }
326
327    /// The instruction budget.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use bcgen_lang::Options;
333    ///
334    /// assert_eq!(Options::new().max_instructions(), Options::DEFAULT_MAX_INSTRUCTIONS);
335    /// ```
336    #[must_use]
337    pub fn max_instructions(&self) -> u64 {
338        self.max_instructions
339    }
340}
341
342/// A source position for a line-table row: file, line, and column, numbered
343/// as the [`Locate`] implementation numbers them.
344///
345/// # Examples
346///
347/// ```
348/// use bcgen_lang::Location;
349///
350/// let at = Location { file: "a.mox", line: 3, column: 7 };
351/// assert_eq!(at.line, 3);
352/// ```
353#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
354pub struct Location<'a> {
355    /// The file name.
356    pub file: &'a str,
357    /// The line.
358    pub line: u32,
359    /// The column.
360    pub column: u32,
361}
362
363/// Maps HIR spans to source positions for LSB line tables.
364///
365/// HIR spans are byte ranges in a session-wide byte space; only the host
366/// (typically a source-lang `SourceMap`) knows the files behind them. Without
367/// a locator, rows carry the [`Options::file`] name, line `0` ("unknown"),
368/// and the span's start byte offset as the column. The exact HIR node of
369/// every instruction is always available from [`Compiled::origin`].
370///
371/// [`Compiled::origin`]: crate::Compiled::origin
372///
373/// # Examples
374///
375/// ```
376/// use bcgen_lang::{Locate, Location};
377/// use hir_lang::Span;
378///
379/// struct OneLine;
380/// impl Locate for OneLine {
381///     fn locate(&self, span: Span) -> Location<'_> {
382///         Location { file: "x", line: 1, column: span.start().to_u32() + 1 }
383///     }
384/// }
385/// assert_eq!(OneLine.locate(Span::new(4, 5)).column, 5);
386/// ```
387pub trait Locate {
388    /// The position of `span`'s start.
389    fn locate(&self, span: Span) -> Location<'_>;
390}
391
392/// A [`Locate`] for one source file: 1-based lines and byte columns from the
393/// file's text, for spans that start at `base` in the session byte space.
394///
395/// # Examples
396///
397/// ```
398/// use bcgen_lang::{LineIndex, Locate};
399/// use hir_lang::Span;
400///
401/// let lines = LineIndex::new("app.mox", 0, "a = 1;\nb = 2;\n");
402/// let at = lines.locate(Span::new(7, 8));
403/// assert_eq!((at.file, at.line, at.column), ("app.mox", 2, 1));
404/// ```
405#[derive(Clone, Debug, PartialEq, Eq)]
406pub struct LineIndex {
407    file: String,
408    base: u32,
409    starts: Vec<u32>,
410}
411
412impl LineIndex {
413    /// Indexes `text`, whose first byte is at offset `base` of the session
414    /// byte space.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use bcgen_lang::LineIndex;
420    ///
421    /// assert_eq!(LineIndex::new("f", 100, "x\ny").lines(), 2);
422    /// ```
423    #[must_use]
424    pub fn new(file: &str, base: u32, text: &str) -> Self {
425        let mut starts = Vec::with_capacity(text.len() / 32 + 1);
426        starts.push(0);
427        for (i, b) in text.bytes().enumerate() {
428            if b == b'\n' {
429                starts.push(u32::try_from(i + 1).unwrap_or(u32::MAX));
430            }
431        }
432        Self {
433            file: String::from(file),
434            base,
435            starts,
436        }
437    }
438
439    /// The number of lines.
440    ///
441    /// # Examples
442    ///
443    /// ```
444    /// assert_eq!(bcgen_lang::LineIndex::new("f", 0, "").lines(), 1);
445    /// ```
446    #[must_use]
447    pub fn lines(&self) -> usize {
448        self.starts.len()
449    }
450}
451
452impl Locate for LineIndex {
453    fn locate(&self, span: Span) -> Location<'_> {
454        let offset = span.start().to_u32().saturating_sub(self.base);
455        // The last line start at or before the offset; offsets past the end
456        // land on the last line.
457        let line = self.starts.partition_point(|&s| s <= offset).max(1);
458        let start = self.starts.get(line - 1).copied().unwrap_or(0);
459        Location {
460            file: &self.file,
461            line: u32::try_from(line).unwrap_or(u32::MAX),
462            column: offset - start + 1,
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::{LineIndex, Locate, Options};
470    use hir_lang::Span;
471
472    #[test]
473    fn line_index_counts_from_the_base() {
474        let l = LineIndex::new("f", 10, "ab\ncd\n\nx");
475        let at = |o: u32| {
476            let p = l.locate(Span::new(o, o));
477            (p.line, p.column)
478        };
479        assert_eq!(at(10), (1, 1));
480        assert_eq!(at(12), (1, 3));
481        assert_eq!(at(13), (2, 1));
482        assert_eq!(at(16), (3, 1));
483        assert_eq!(at(17), (4, 1));
484        // Before the base clamps to the start; past the end stays on the last line.
485        assert_eq!(at(0), (1, 1));
486        assert_eq!(at(1_000), (4, 984));
487    }
488
489    #[test]
490    fn a_later_hook_binding_replaces_an_earlier_one() {
491        let o = Options::new()
492            .with_hook(bytecode_lang::Hook::Eq, "a", "b")
493            .with_hook(bytecode_lang::Hook::Eq, "c", "d");
494        assert_eq!(o.hooks().len(), 1);
495        assert_eq!(o.hooks()[0].1, "c");
496    }
497}