Skip to main content

zerodds_idl_python/
emitter.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! IDL → Python codegen — emit logic.
5//!
6//! Generates, for an IDL specification, a Python module that uses the
7//! `@idl_struct(typename=...)`/`@dataclass` convention of the
8//! `zerodds-py` crate. Generated classes are usable directly with
9//! `zerodds.IdlTopic(MyClass)`.
10
11use std::collections::BTreeSet;
12use std::fmt::Write as _;
13
14use zerodds_idl::ast::types::{
15    BitmaskDecl, BitsetDecl, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator, Definition, EnumDef,
16    ExceptDecl, FloatingType, Identifier, IntegerType, Literal, LiteralKind, Member, ModuleDef,
17    PrimitiveType, ScopedName, SequenceType, Specification, StringType, StructDcl, StructDef,
18    SwitchTypeSpec, TypeDecl, TypeSpec, TypedefDecl, UnaryOp, UnionDcl, UnionDef,
19};
20
21use crate::error::{IdlPythonError, Result};
22
23/// Codegen options for Python modules.
24#[derive(Debug, Clone, Default)]
25pub struct PythonGenOptions {
26    /// Optional header comment that lands at the top of the generated
27    /// file. Newlines are emitted as separate `#` comment lines.
28    pub header_comment: Option<String>,
29}
30
31/// Codegen entry point — IDL spec → complete Python source.
32///
33/// # Errors
34///
35/// `IdlPythonError::Unsupported` for IDL constructs that the Python
36/// mapping does not (yet) support: `valuetype`, `interface`,
37/// `fixed`, `map`, `any`, as well as union cases with non-literal
38/// const expressions.
39pub fn generate_python_module(spec: &Specification, opts: &PythonGenOptions) -> Result<String> {
40    let mut imports = ImportSet::default();
41    collect_imports(&spec.definitions, &mut imports)?;
42
43    let mut out = String::new();
44    out.push_str("# SPDX-License-Identifier: Apache-2.0\n");
45    if let Some(c) = &opts.header_comment {
46        for line in c.lines() {
47            out.push_str("# ");
48            out.push_str(line);
49            out.push('\n');
50        }
51    }
52    out.push_str("# Auto-generated by `zerodds-idl-python`. Do not edit by hand.\n");
53    out.push('\n');
54
55    imports.emit(&mut out);
56
57    let mut scope: Vec<String> = Vec::new();
58    emit_definitions(&mut out, &spec.definitions, &mut scope)?;
59    Ok(out)
60}
61
62// ---------------------------------------------------------------------------
63// Import-Sammlung
64// ---------------------------------------------------------------------------
65
66#[derive(Default)]
67struct ImportSet {
68    dataclass: bool,
69    int_enum: bool,
70    int_flag: bool,
71    typing_list: bool,
72    typing_type_alias: bool,
73    zerodds_brands: BTreeSet<&'static str>,
74    zerodds_idl_struct: bool,
75    zerodds_idl_union: bool,
76}
77
78impl ImportSet {
79    fn emit(&self, out: &mut String) {
80        if self.dataclass {
81            out.push_str("from dataclasses import dataclass\n");
82        }
83        if self.int_enum || self.int_flag {
84            out.push_str("from enum import ");
85            let mut parts: Vec<&str> = Vec::new();
86            if self.int_enum {
87                parts.push("IntEnum");
88            }
89            if self.int_flag {
90                parts.push("IntFlag");
91            }
92            out.push_str(&parts.join(", "));
93            out.push('\n');
94        }
95        if self.typing_list || self.typing_type_alias {
96            out.push_str("from typing import ");
97            let mut parts: Vec<&str> = Vec::new();
98            if self.typing_list {
99                parts.push("List");
100            }
101            if self.typing_type_alias {
102                parts.push("TypeAlias");
103            }
104            out.push_str(&parts.join(", "));
105            out.push('\n');
106        }
107        if self.zerodds_idl_struct || self.zerodds_idl_union || !self.zerodds_brands.is_empty() {
108            out.push_str("from zerodds.idl import ");
109            let mut parts: Vec<String> = Vec::new();
110            if self.zerodds_idl_struct {
111                parts.push("idl_struct".into());
112            }
113            if self.zerodds_idl_union {
114                parts.push("idl_union".into());
115            }
116            for brand in &self.zerodds_brands {
117                parts.push((*brand).to_string());
118            }
119            out.push_str(&parts.join(", "));
120            out.push('\n');
121        }
122        out.push('\n');
123    }
124}
125
126/// zerodds-lint: recursion-depth 32
127fn collect_imports(defs: &[Definition], imports: &mut ImportSet) -> Result<()> {
128    for d in defs {
129        match d {
130            Definition::Module(m) => collect_imports(&m.definitions, imports)?,
131            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
132                imports.dataclass = true;
133                imports.zerodds_idl_struct = true;
134                for m in &s.members {
135                    collect_type_imports(&m.type_spec, imports)?;
136                }
137            }
138            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(_))) => {
139                imports.int_enum = true;
140            }
141            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(_))) => {
142                imports.int_flag = true;
143            }
144            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(_))) => {
145                // Bitset: emit as an IntN alias + bit constants —
146                // needs only the underlying integer brand, no
147                // decorator.
148                imports.zerodds_brands.insert("Int64");
149            }
150            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
151                imports.zerodds_idl_union = true;
152                imports.int_enum = true;
153                collect_switch_type_imports(&u.switch_type, imports);
154                for case in &u.cases {
155                    collect_type_imports(&case.element.type_spec, imports)?;
156                }
157            }
158            Definition::Type(TypeDecl::Typedef(td)) => {
159                imports.typing_type_alias = true;
160                collect_type_imports(&td.type_spec, imports)?;
161            }
162            Definition::Except(e) => {
163                imports.dataclass = true;
164                imports.zerodds_idl_struct = true;
165                for m in &e.members {
166                    collect_type_imports(&m.type_spec, imports)?;
167                }
168            }
169            _ => {
170                // Forward decls + const + interface + valuetype + RTI
171                // vendor constructs are reported as Unsupported or
172                // ignored in emit_definitions. For the import
173                // pass we can silently skip them here.
174            }
175        }
176    }
177    Ok(())
178}
179
180/// zerodds-lint: recursion-depth 32
181fn collect_type_imports(ts: &TypeSpec, imports: &mut ImportSet) -> Result<()> {
182    match ts {
183        TypeSpec::Primitive(p) => {
184            imports.zerodds_brands.insert(brand_for_primitive(*p));
185        }
186        TypeSpec::String(s) => {
187            imports.zerodds_brands.insert(brand_for_string(s));
188        }
189        TypeSpec::Sequence(seq) => {
190            imports.typing_list = true;
191            collect_type_imports(&seq.elem, imports)?;
192        }
193        TypeSpec::Scoped(_) => {
194            // Local reference to another type in the same module —
195            // no import needed.
196        }
197        TypeSpec::Fixed(_) | TypeSpec::Map(_) | TypeSpec::Any => {
198            return Err(IdlPythonError::Unsupported(format!(
199                "type spec not yet supported in python codegen: {ts:?}"
200            )));
201        }
202    }
203    Ok(())
204}
205
206fn collect_switch_type_imports(switch: &SwitchTypeSpec, imports: &mut ImportSet) {
207    match switch {
208        SwitchTypeSpec::Integer(i) => {
209            imports.zerodds_brands.insert(brand_for_integer(*i));
210        }
211        SwitchTypeSpec::Boolean => {
212            // bool is a Python builtin, no brand import needed — the
213            // `_kind_from_annotation` fallback maps it to Bool.
214        }
215        SwitchTypeSpec::Octet => {
216            imports.zerodds_brands.insert("Octet");
217        }
218        SwitchTypeSpec::Char => {
219            imports.zerodds_brands.insert("Char");
220        }
221        SwitchTypeSpec::Scoped(_) => {
222            // Probably an IntEnum — referenced via the defined type,
223            // no extra brand import.
224        }
225    }
226}
227
228// ---------------------------------------------------------------------------
229// Definitions-Emit
230// ---------------------------------------------------------------------------
231
232/// zerodds-lint: recursion-depth 32
233fn emit_definitions(out: &mut String, defs: &[Definition], scope: &mut Vec<String>) -> Result<()> {
234    for d in defs {
235        match d {
236            Definition::Module(m) => emit_module(out, m, scope)?,
237            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
238                emit_struct(out, s, scope)?;
239            }
240            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Forward(_)))) => {
241                // Forward declarations need no code in Python.
242            }
243            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
244                emit_enum(out, e, scope)?;
245            }
246            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
247                emit_bitmask(out, b, scope)?;
248            }
249            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
250                emit_bitset(out, b, scope)?;
251            }
252            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
253                emit_union(out, u, scope)?;
254            }
255            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Forward(_)))) => {
256                // Forward declarations need no code in Python.
257            }
258            Definition::Type(TypeDecl::Typedef(td)) => {
259                emit_typedef(out, td, scope)?;
260            }
261            Definition::Except(e) => {
262                emit_exception(out, e, scope)?;
263            }
264            _ => {
265                return Err(IdlPythonError::Unsupported(format!(
266                    "definition variant not yet supported: {d:?}"
267                )));
268            }
269        }
270    }
271    Ok(())
272}
273
274/// zerodds-lint: recursion-depth 32
275fn emit_module(out: &mut String, m: &ModuleDef, scope: &mut Vec<String>) -> Result<()> {
276    scope.push(m.name.text.clone());
277    emit_definitions(out, &m.definitions, scope)?;
278    scope.pop();
279    Ok(())
280}
281
282fn emit_struct(out: &mut String, s: &StructDef, scope: &[String]) -> Result<()> {
283    let typename = qualified_typename(&s.name, scope);
284    let class_name = python_class_name(&s.name, scope);
285
286    writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
287    writeln!(out, "@dataclass").ok();
288    if let Some(base) = &s.base {
289        let base_class = python_scoped_name(base);
290        writeln!(out, "class {class_name}({base_class}):").ok();
291    } else {
292        writeln!(out, "class {class_name}:").ok();
293    }
294
295    if s.members.is_empty() && s.base.is_none() {
296        writeln!(out, "    pass").ok();
297    } else if s.members.is_empty() {
298        // With a base but no new fields: `pass` so the dataclass body
299        // is syntactically valid.
300        writeln!(out, "    pass").ok();
301    } else {
302        for m in &s.members {
303            emit_members_of(out, m, scope)?;
304        }
305    }
306    out.push('\n');
307    Ok(())
308}
309
310fn emit_exception(out: &mut String, e: &ExceptDecl, scope: &[String]) -> Result<()> {
311    let typename = qualified_typename(&e.name, scope);
312    let class_name = python_class_name(&e.name, scope);
313
314    writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
315    writeln!(out, "@dataclass").ok();
316    writeln!(out, "class {class_name}(Exception):").ok();
317
318    if e.members.is_empty() {
319        writeln!(out, "    pass").ok();
320    } else {
321        for m in &e.members {
322            emit_members_of(out, m, scope)?;
323        }
324    }
325    out.push('\n');
326    Ok(())
327}
328
329fn emit_members_of(out: &mut String, m: &Member, scope: &[String]) -> Result<()> {
330    let py_type = python_type_for(&m.type_spec, scope)?;
331    for d in &m.declarators {
332        let field_name = escape_python_keyword(d.name().text.as_str());
333        match d {
334            Declarator::Simple(_) => {
335                writeln!(out, "    {field_name}: {py_type}").ok();
336            }
337            Declarator::Array(arr) => {
338                let mut t = py_type.clone();
339                for _ in 0..arr.sizes.len() {
340                    t = format!("List[{t}]");
341                }
342                writeln!(out, "    {field_name}: {t}").ok();
343            }
344        }
345    }
346    Ok(())
347}
348
349fn emit_enum(out: &mut String, e: &EnumDef, scope: &[String]) -> Result<()> {
350    let class_name = python_class_name(&e.name, scope);
351    writeln!(out, "class {class_name}(IntEnum):").ok();
352    if e.enumerators.is_empty() {
353        writeln!(out, "    pass").ok();
354    } else {
355        for (idx, en) in e.enumerators.iter().enumerate() {
356            writeln!(out, "    {} = {idx}", en.name.text).ok();
357        }
358    }
359    out.push('\n');
360    Ok(())
361}
362
363fn emit_bitmask(out: &mut String, b: &BitmaskDecl, scope: &[String]) -> Result<()> {
364    let class_name = python_class_name(&b.name, scope);
365    writeln!(out, "class {class_name}(IntFlag):").ok();
366    if b.values.is_empty() {
367        writeln!(out, "    pass").ok();
368    } else {
369        for (idx, v) in b.values.iter().enumerate() {
370            writeln!(out, "    {} = 1 << {idx}", v.name.text).ok();
371        }
372    }
373    out.push('\n');
374    Ok(())
375}
376
377fn emit_bitset(out: &mut String, b: &BitsetDecl, scope: &[String]) -> Result<()> {
378    // OMG bitset: a container integer with packed sub-fields. We
379    // emit an Int64 alias plus a helper class with
380    // SHIFT/MASK constants per named bitfield. The `zerodds-py`
381    // runtime sees the alias as an Int64 brand and encodes accordingly;
382    // the application reads/writes fields via the helper constants:
383    //
384    //   value = SensorFlags_Bits.encode(sensor_id=42, quality=2)
385    //   sensor_id = (value >> SensorFlags_Bits.SENSOR_ID_SHIFT)
386    //               & SensorFlags_Bits.SENSOR_ID_MASK
387    let class_name = python_class_name(&b.name, scope);
388    writeln!(out, "{class_name}: TypeAlias = Int64").ok();
389    writeln!(out).ok();
390    writeln!(out, "class {class_name}_Bits:").ok();
391    if b.bitfields.is_empty() {
392        writeln!(out, "    pass").ok();
393    } else {
394        let mut shift: u64 = 0;
395        for bf in &b.bitfields {
396            let width = eval_const_int(&bf.spec.width).ok_or_else(|| {
397                IdlPythonError::Unsupported(format!(
398                    "bitset width is not a literal integer: {:?}",
399                    bf.spec.width
400                ))
401            })?;
402            if width <= 0 {
403                return Err(IdlPythonError::Unsupported(format!(
404                    "bitset width must be > 0, got {width}"
405                )));
406            }
407            let width = width as u64;
408            let mask = if width >= 64 {
409                u64::MAX
410            } else {
411                (1u64 << width) - 1
412            };
413            if let Some(name) = &bf.name {
414                let up = name.text.to_uppercase();
415                writeln!(out, "    {up}_SHIFT = {shift}").ok();
416                writeln!(out, "    {up}_WIDTH = {width}").ok();
417                writeln!(out, "    {up}_MASK  = 0x{mask:x}").ok();
418            }
419            shift += width;
420        }
421    }
422    out.push('\n');
423    Ok(())
424}
425
426fn emit_union(out: &mut String, u: &UnionDef, scope: &[String]) -> Result<()> {
427    let typename = qualified_typename(&u.name, scope);
428    let class_name = python_class_name(&u.name, scope);
429    let discriminator_repr = switch_type_python_repr(&u.switch_type);
430
431    // Collect cases: one entry per label in dict[int, tuple[str, Any]].
432    // For a `default:` label we collect the case separately for the
433    // `default=` parameter of the idl_union constructor.
434    let mut case_entries: Vec<(i64, String, String)> = Vec::new();
435    let mut default_entry: Option<(String, String)> = None;
436
437    for case in &u.cases {
438        let field_name = escape_python_keyword(case.element.declarator.name().text.as_str());
439        let py_type = python_type_for(&case.element.type_spec, scope)?;
440        for label in &case.labels {
441            match label {
442                CaseLabel::Value(expr) => {
443                    let v = eval_const_int(expr).ok_or_else(|| {
444                        IdlPythonError::Unsupported(format!(
445                            "union case label is not a literal integer (scoped/enum-ref \
446                             references will be supported in a follow-up): {expr:?}"
447                        ))
448                    })?;
449                    case_entries.push((v, field_name.clone(), py_type.clone()));
450                }
451                CaseLabel::Default => {
452                    default_entry = Some((field_name.clone(), py_type.clone()));
453                }
454            }
455        }
456    }
457
458    writeln!(out, "{class_name} = idl_union(").ok();
459    writeln!(out, "    typename=\"{typename}\",").ok();
460    writeln!(out, "    discriminator={discriminator_repr},").ok();
461    writeln!(out, "    cases={{").ok();
462    for (label_value, field, py_type) in &case_entries {
463        writeln!(out, "        {label_value}: (\"{field}\", {py_type}),").ok();
464    }
465    writeln!(out, "    }},").ok();
466    if let Some((field, py_type)) = &default_entry {
467        writeln!(out, "    default=(\"{field}\", {py_type}),").ok();
468    } else {
469        writeln!(out, "    default=None,").ok();
470    }
471    writeln!(out, ")").ok();
472    out.push('\n');
473    Ok(())
474}
475
476fn emit_typedef(out: &mut String, td: &TypedefDecl, scope: &[String]) -> Result<()> {
477    let py_type = python_type_for(&td.type_spec, scope)?;
478    for d in &td.declarators {
479        let alias_name = python_class_name(d.name(), scope);
480        match d {
481            Declarator::Simple(_) => {
482                writeln!(out, "{alias_name}: TypeAlias = {py_type}").ok();
483            }
484            Declarator::Array(arr) => {
485                let mut t = py_type.clone();
486                for _ in 0..arr.sizes.len() {
487                    t = format!("List[{t}]");
488                }
489                writeln!(out, "{alias_name}: TypeAlias = {t}").ok();
490            }
491        }
492    }
493    out.push('\n');
494    Ok(())
495}
496
497// ---------------------------------------------------------------------------
498// Type-Mapping
499// ---------------------------------------------------------------------------
500
501/// zerodds-lint: recursion-depth 32
502fn python_type_for(ts: &TypeSpec, scope: &[String]) -> Result<String> {
503    match ts {
504        TypeSpec::Primitive(p) => Ok(brand_for_primitive(*p).to_string()),
505        TypeSpec::String(s) => Ok(brand_for_string(s).to_string()),
506        TypeSpec::Sequence(seq) => {
507            let inner = python_type_for_seq_elem(seq, scope)?;
508            Ok(format!("List[{inner}]"))
509        }
510        TypeSpec::Scoped(name) => Ok(python_scoped_name(name)),
511        TypeSpec::Fixed(_) | TypeSpec::Map(_) | TypeSpec::Any => Err(IdlPythonError::Unsupported(
512            format!("type spec not yet supported in python codegen: {ts:?}"),
513        )),
514    }
515}
516
517/// zerodds-lint: recursion-depth 32
518fn python_type_for_seq_elem(seq: &SequenceType, scope: &[String]) -> Result<String> {
519    python_type_for(&seq.elem, scope)
520}
521
522fn brand_for_primitive(p: PrimitiveType) -> &'static str {
523    match p {
524        PrimitiveType::Boolean => "bool",
525        PrimitiveType::Octet => "Octet",
526        PrimitiveType::Char => "Char",
527        PrimitiveType::WideChar => "WChar",
528        PrimitiveType::Integer(i) => brand_for_integer(i),
529        PrimitiveType::Floating(f) => match f {
530            FloatingType::Float => "Float32",
531            FloatingType::Double => "Float64",
532            FloatingType::LongDouble => "LongDouble",
533        },
534    }
535}
536
537fn brand_for_integer(i: IntegerType) -> &'static str {
538    match i {
539        IntegerType::Short | IntegerType::Int16 => "Int16",
540        IntegerType::UShort | IntegerType::UInt16 => "UInt16",
541        IntegerType::Long | IntegerType::Int32 => "Int32",
542        IntegerType::ULong | IntegerType::UInt32 => "UInt32",
543        IntegerType::LongLong | IntegerType::Int64 => "Int64",
544        IntegerType::ULongLong | IntegerType::UInt64 => "UInt64",
545        IntegerType::Int8 => "Int8",
546        IntegerType::UInt8 => "UInt8",
547    }
548}
549
550fn brand_for_string(s: &StringType) -> &'static str {
551    if s.wide { "WString" } else { "String" }
552}
553
554fn switch_type_python_repr(switch: &SwitchTypeSpec) -> String {
555    match switch {
556        SwitchTypeSpec::Integer(i) => brand_for_integer(*i).to_string(),
557        SwitchTypeSpec::Boolean => "bool".to_string(),
558        SwitchTypeSpec::Octet => "Octet".to_string(),
559        SwitchTypeSpec::Char => "Char".to_string(),
560        SwitchTypeSpec::Scoped(s) => python_scoped_name(s),
561    }
562}
563
564fn python_scoped_name(name: &ScopedName) -> String {
565    name.parts
566        .iter()
567        .map(|p| p.text.clone())
568        .collect::<Vec<_>>()
569        .join("_")
570}
571
572fn qualified_typename(name: &Identifier, scope: &[String]) -> String {
573    if scope.is_empty() {
574        name.text.clone()
575    } else {
576        format!("{}::{}", scope.join("::"), name.text)
577    }
578}
579
580fn python_class_name(name: &Identifier, scope: &[String]) -> String {
581    if scope.is_empty() {
582        name.text.clone()
583    } else {
584        let mut parts = scope.to_vec();
585        parts.push(name.text.clone());
586        parts.join("_")
587    }
588}
589
590fn escape_python_keyword(name: &str) -> String {
591    const PYTHON_KEYWORDS: &[&str] = &[
592        "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
593        "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
594        "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
595        "try", "while", "with", "yield", "match", "case",
596    ];
597    if PYTHON_KEYWORDS.contains(&name) {
598        format!("{name}_")
599    } else {
600        name.to_string()
601    }
602}
603
604// ---------------------------------------------------------------------------
605// Const-expression evaluation (limited: integer literals + unary +/- only)
606// ---------------------------------------------------------------------------
607
608/// zerodds-lint: recursion-depth 32
609fn eval_const_int(expr: &ConstExpr) -> Option<i64> {
610    match expr {
611        ConstExpr::Literal(Literal {
612            kind: LiteralKind::Integer,
613            raw,
614            ..
615        }) => parse_integer_literal(raw),
616        ConstExpr::Unary { op, operand, .. } => {
617            let inner = eval_const_int(operand)?;
618            match op {
619                UnaryOp::Plus => Some(inner),
620                UnaryOp::Minus => Some(-inner),
621                UnaryOp::BitNot => Some(!inner),
622            }
623        }
624        _ => None,
625    }
626}
627
628fn parse_integer_literal(raw: &str) -> Option<i64> {
629    let s = raw.trim();
630    // OMG IDL allows hex (0x...), octal (0...) and decimal literals.
631    // Suffixes like `L`/`u` we ignore generously — the parser
632    // has already validated that this is an integer literal.
633    let s = s.trim_end_matches(['L', 'l', 'u', 'U']);
634    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
635        i64::from_str_radix(hex, 16).ok()
636    } else if let Some(rest) = s.strip_prefix("-0x").or_else(|| s.strip_prefix("-0X")) {
637        i64::from_str_radix(rest, 16).ok().map(|n| -n)
638    } else if s.starts_with('0') && s.len() > 1 && !s.contains(|c: char| !c.is_ascii_digit()) {
639        // Octal — intentionally strict, because OMG IDL accepts octal.
640        i64::from_str_radix(&s[1..], 8).ok()
641    } else {
642        s.parse::<i64>().ok()
643    }
644}