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