Skip to main content

lua_types/
version.rs

1//! `LuaVersion` — the single source of truth for which Lua language version a
2//! runtime instance speaks.
3//!
4//! This lives in `lua-types`, the lowest shared crate, so every layer above
5//! (parser, compiler, VM, stdlib, runtime) can name the version without a
6//! dependency cycle. Per the multi-version architecture decision
7//! (`specs/MULTIVERSION_ARCHITECTURE_DECISION.md` §4, §5), the version is a
8//! *backend selector* threaded from construction; it never appears in a public
9//! embedding-API type.
10
11/// The numeric model a version uses for Lua numbers.
12///
13/// This is the single sharpest behavioral axis across versions: 5.1/5.2 are
14/// float-only (one `number` type, every value an `f64`, no `math.type`), while
15/// 5.3/5.4/5.5 carry the dual integer/float subtype.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum NumberModel {
18    /// One `number` type; every numeric value is an `f64`. Lua 5.1/5.2.
19    FloatOnly,
20    /// Distinct integer (`i64`) and float (`f64`) subtypes. Lua 5.3/5.4/5.5.
21    Dual,
22}
23
24/// Which Lua language version a runtime instance speaks.
25///
26/// `Default` is [`LuaVersion::V54`] — the version this codebase currently
27/// implements end-to-end — so that `Lua::new()` and any other defaulted
28/// construction keeps the existing 5.4 behavior.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30#[non_exhaustive]
31pub enum LuaVersion {
32    /// Lua 5.1 — float-only, `fenv`-based globals. Deferred (separate core).
33    V51,
34    /// Lua 5.2 — float-only, modern `_ENV` globals. Deferred (separate core).
35    V52,
36    /// Lua 5.3 — dual subtype, modern `_ENV`. Deferred.
37    V53,
38    /// Lua 5.4 — the implemented baseline today.
39    V54,
40    /// Lua 5.5 — dual subtype, declared-globals scope model. Deferred.
41    V55,
42}
43
44impl Default for LuaVersion {
45    fn default() -> Self {
46        LuaVersion::V54
47    }
48}
49
50impl LuaVersion {
51    /// The family-level numeric model for this version.
52    pub fn number_model(self) -> NumberModel {
53        match self {
54            LuaVersion::V51 | LuaVersion::V52 => NumberModel::FloatOnly,
55            LuaVersion::V53 | LuaVersion::V54 | LuaVersion::V55 => NumberModel::Dual,
56        }
57    }
58
59    /// Whether the `__name` metafield overrides an object's type name (in
60    /// `tostring` and in type-error messages). `__name` is a 5.3 addition; 5.1
61    /// and 5.2 ignore it and always report the primitive type name.
62    pub fn honors_name_metafield(self) -> bool {
63        !matches!(self, LuaVersion::V51 | LuaVersion::V52)
64    }
65
66    /// Whether `io.lines(filename, ...)` returns the file as a fourth
67    /// to-be-closed result. The to-be-closed value (`<close>`) mechanism is a
68    /// 5.4 addition: 5.4/5.5 return four values (iterator, nil, nil, file) so a
69    /// generic `for` can close the file on loop exit; 5.1–5.3 return only the
70    /// iterator (one value).
71    pub fn lines_returns_to_be_closed(self) -> bool {
72        matches!(self, LuaVersion::V54 | LuaVersion::V55)
73    }
74
75    /// Whether this version has a real backend. The modern family (5.3/5.4/5.5)
76    /// and 5.2 (float-only + `_ENV`) are complete. 5.1 reuses the 5.2 float-only
77    /// core plus three faithful 5.1-specific axes:
78    /// - **fenv globals** — `getfenv`/`setfenv` (the per-function environment
79    ///   model, Option B over the reused `_ENV` upvalue; `specs/followup/5.1-fenv.md`).
80    /// - **metamethod diffs** — `#t` never consults a table `__len`, no
81    ///   `__pairs`/`__ipairs`, no `__gc` on tables (userdata only).
82    /// - **roster + syntax** — `unpack` is global (no `table.unpack`/`pack`/
83    ///   `move`); `loadstring` + reader-only `load`; `table.getn`/`setn`(stub)/
84    ///   `maxn`/`foreach`/`foreachi`; `module`/`package.seeall`/`package.loaders`
85    ///   (no `package.searchers`); `string.gfind`; `math.log` 1-arg +
86    ///   `log10`/`atan2`/`pow`/`mod` (no `math.type`); `gcinfo`; `newproxy`;
87    ///   `xpcall`-no-extra-args; `coroutine.running` nil in main; no `bit32`/
88    ///   `utf8`/`rawlen`; `goto`/labels/`//`/bitwise/`<const>`/`\x`-`\z` escapes
89    ///   rejected (`goto` stays a valid identifier). See
90    ///   `specs/followup/5.1-roster-syntax.md`. Documented divergences: the
91    ///   `math.random` C-`rand()` sequence and `os.execute` raw-status byte are
92    ///   host-dependent (contract matches; exact bytes do not).
93    pub fn is_supported(self) -> bool {
94        matches!(
95            self,
96            LuaVersion::V51 | LuaVersion::V52 | LuaVersion::V53 | LuaVersion::V54 | LuaVersion::V55
97        )
98    }
99
100    /// The `_VERSION` global string for this version (e.g. `"Lua 5.4"`).
101    pub fn version_str(self) -> &'static str {
102        match self {
103            LuaVersion::V51 => "Lua 5.1",
104            LuaVersion::V52 => "Lua 5.2",
105            LuaVersion::V53 => "Lua 5.3",
106            LuaVersion::V54 => "Lua 5.4",
107            LuaVersion::V55 => "Lua 5.5",
108        }
109    }
110
111    /// The `LUAC_VERSION` byte written into a `luac`/`string.dump` header for
112    /// this version. Upstream encodes the version as `(major << 4) | minor`,
113    /// e.g. 5.4 → `0x54`.
114    pub fn luac_version_byte(self) -> u8 {
115        match self {
116            LuaVersion::V51 => 0x51,
117            LuaVersion::V52 => 0x52,
118            LuaVersion::V53 => 0x53,
119            LuaVersion::V54 => 0x54,
120            LuaVersion::V55 => 0x55,
121        }
122    }
123
124    /// Whether this version has a given language [`Feature`] — the pure,
125    /// build-independent capability matrix (issue #234). This is the version
126    /// dimension only; an embedding instance additionally narrows
127    /// library-backed features by what was compiled in (see
128    /// `omnilua::Lua::supports`).
129    ///
130    /// The rows are the source of record `ANALYSES/version_feature_matrix.tsv`,
131    /// generated by probing the reference binaries
132    /// (`specs/oracle/gen_feature_matrix.sh`); a test asserts this function
133    /// against that fixture so the matrix can never drift from upstream.
134    pub fn supports(self, f: Feature) -> bool {
135        use Feature::*;
136        use LuaVersion::*;
137        match f {
138            IntegerSubtype | NativeBitwise | Utf8Lib | StringPack => {
139                matches!(self, V53 | V54 | V55)
140            }
141            EnvSandbox | GotoLabels | TableLenMetamethod | GcIsRunning => {
142                matches!(self, V52 | V53 | V54 | V55)
143            }
144            FenvSandbox => self == V51,
145            Bit32Lib => matches!(self, V52 | V53),
146            CloseAttribute | ConstAttribute | CoroutineClose | WarnFunction => {
147                matches!(self, V54 | V55)
148            }
149            GcParam | GlobalKeyword | NamedVararg | TableCreate => self == V55,
150        }
151    }
152
153    /// Iterate the [`Feature`]s this version supports (version dimension only).
154    pub fn features(self) -> impl Iterator<Item = Feature> {
155        Feature::ALL.iter().copied().filter(move |f| self.supports(*f))
156    }
157}
158
159/// A version-divergent language capability — one *present-or-absent* row of the
160/// support matrix (issue #234). Behavioral divergences (same call, different
161/// result — e.g. `<=`-from-`__lt`, integer `for`-loop wraparound, the RNG stream)
162/// are deliberately **not** features: they are resolved inside the core, not
163/// gated. Query with [`LuaVersion::supports`].
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165#[non_exhaustive]
166pub enum Feature {
167    /// Integer/float subtypes and `math.type` — 5.3+.
168    IntegerSubtype,
169    /// `_ENV` and `load(.., env)` lexical-environment sandboxing — 5.2+.
170    EnvSandbox,
171    /// `setfenv`/`getfenv` function-environment access — 5.1 only.
172    FenvSandbox,
173    /// `goto` and `::labels::` — 5.2+.
174    GotoLabels,
175    /// Native bitwise operators `& | ~ << >>` and integer division `//` — 5.3+.
176    NativeBitwise,
177    /// The `bit32` library — 5.2 and 5.3 (removed in 5.4).
178    Bit32Lib,
179    /// The `utf8` library — 5.3+.
180    Utf8Lib,
181    /// `string.pack`/`string.unpack`/`string.packsize` — 5.3+.
182    StringPack,
183    /// `<close>` to-be-closed variables and the `__close` metamethod — 5.4+.
184    CloseAttribute,
185    /// `<const>` variables — 5.4+.
186    ConstAttribute,
187    /// `coroutine.close` — 5.4+.
188    CoroutineClose,
189    /// The `warn` function — 5.4+.
190    WarnFunction,
191    /// The `__len` metamethod honored on tables (not just userdata) — 5.2+.
192    TableLenMetamethod,
193    /// `collectgarbage("isrunning")` — 5.2+.
194    GcIsRunning,
195    /// `collectgarbage("param", name [, value])` — 5.5.
196    GcParam,
197    /// The `global` declaration keyword and declared-global scope model — 5.5.
198    GlobalKeyword,
199    /// Named vararg table parameters `function f(a, ...t)` — 5.5.
200    NamedVararg,
201    /// `table.create` — 5.5.
202    TableCreate,
203}
204
205impl Feature {
206    /// Every [`Feature`], the iteration source for [`LuaVersion::features`] and
207    /// the matrix-vs-reference test. A `match` in the test asserts this is
208    /// exhaustive, so adding a variant without adding it here fails to compile.
209    pub const ALL: [Feature; 18] = [
210        Feature::IntegerSubtype,
211        Feature::EnvSandbox,
212        Feature::FenvSandbox,
213        Feature::GotoLabels,
214        Feature::NativeBitwise,
215        Feature::Bit32Lib,
216        Feature::Utf8Lib,
217        Feature::StringPack,
218        Feature::CloseAttribute,
219        Feature::ConstAttribute,
220        Feature::CoroutineClose,
221        Feature::WarnFunction,
222        Feature::TableLenMetamethod,
223        Feature::GcIsRunning,
224        Feature::GcParam,
225        Feature::GlobalKeyword,
226        Feature::NamedVararg,
227        Feature::TableCreate,
228    ];
229
230    /// A short, stable token naming this feature, used in the fixture and in the
231    /// [`Unsupported`] error message.
232    pub fn name(self) -> &'static str {
233        match self {
234            Feature::IntegerSubtype => "integer subtype (math.type)",
235            Feature::EnvSandbox => "_ENV sandboxing",
236            Feature::FenvSandbox => "setfenv/getfenv",
237            Feature::GotoLabels => "goto/labels",
238            Feature::NativeBitwise => "native bitwise operators",
239            Feature::Bit32Lib => "bit32 library",
240            Feature::Utf8Lib => "utf8 library",
241            Feature::StringPack => "string.pack",
242            Feature::CloseAttribute => "<close> attribute",
243            Feature::ConstAttribute => "<const> attribute",
244            Feature::CoroutineClose => "coroutine.close",
245            Feature::WarnFunction => "warn",
246            Feature::TableLenMetamethod => "__len on tables",
247            Feature::GcIsRunning => "collectgarbage('isrunning')",
248            Feature::GcParam => "collectgarbage('param')",
249            Feature::GlobalKeyword => "global declarations",
250            Feature::NamedVararg => "named vararg tables",
251            Feature::TableCreate => "table.create",
252        }
253    }
254}
255
256impl core::fmt::Display for Feature {
257    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258        f.write_str(self.name())
259    }
260}
261
262/// A typed record that a [`Feature`] absent on a given [`LuaVersion`] was
263/// requested at a host-API verb (issue #234). Carried by the public embedding
264/// error so a host can match the cause; see `omnilua::Error::as_unsupported`.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct Unsupported {
267    /// The feature that was requested.
268    pub feature: Feature,
269    /// The version that lacks it.
270    pub version: LuaVersion,
271}
272
273impl core::fmt::Display for Unsupported {
274    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
275        write!(
276            f,
277            "{} is not available in {}",
278            self.feature,
279            self.version.version_str()
280        )
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn feature_all_has_no_duplicates() {
290        // The fixed-size `[Feature; 18]` plus the exhaustive (wildcard-free)
291        // matches in `name()`/`supports()` are the compile-time completeness
292        // guard: a new variant forces both to be updated and the array resized.
293        // This test guards the remaining gap — a duplicate slipping into ALL.
294        for (i, a) in Feature::ALL.iter().enumerate() {
295            for b in &Feature::ALL[i + 1..] {
296                assert_ne!(a, b, "duplicate feature in Feature::ALL");
297            }
298        }
299    }
300
301    #[test]
302    fn default_is_v54() {
303        assert_eq!(LuaVersion::default(), LuaVersion::V54);
304    }
305
306    #[test]
307    fn number_model_split() {
308        assert_eq!(LuaVersion::V51.number_model(), NumberModel::FloatOnly);
309        assert_eq!(LuaVersion::V52.number_model(), NumberModel::FloatOnly);
310        assert_eq!(LuaVersion::V53.number_model(), NumberModel::Dual);
311        assert_eq!(LuaVersion::V54.number_model(), NumberModel::Dual);
312        assert_eq!(LuaVersion::V55.number_model(), NumberModel::Dual);
313    }
314
315    #[test]
316    fn version_str_and_byte() {
317        assert_eq!(LuaVersion::V54.version_str(), "Lua 5.4");
318        assert_eq!(LuaVersion::V54.luac_version_byte(), 0x54);
319        assert_eq!(LuaVersion::V53.version_str(), "Lua 5.3");
320        assert_eq!(LuaVersion::V53.luac_version_byte(), 0x53);
321    }
322}
323
324// ──────────────────────────────────────────────────────────────────────────
325// PORT STATUS
326//   source:        (foundation — multi-version seam, not ported from .c)
327//   target_crate:  lua-types
328//   confidence:    high
329//   todos:         0
330//   port_notes:    0
331//   unsafe_blocks: 0
332//   notes:         LuaVersion + NumberModel. Default = V54 preserves the
333//                  existing single-version behavior. V51-V55 complete; V51
334//                  reuses the 5.2 float-only core plus the fenv-globals,
335//                  metamethod-diff, and roster/syntax axes (all V51-gated).
336// ──────────────────────────────────────────────────────────────────────────