Skip to main content

submilli_engine/
mangle.rs

1//! Fully-qualified symbol naming: `<package>#<symbol>` for public/top-level
2//! package symbols. Internal module symbols use a separate `mod:` namespace so
3//! they cannot collide with package-public names. Treat [`MangledName`] as
4//! opaque; use the constructors rather than depending on the textual format.
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10pub const PRELUDE_PACKAGE: &str = "submilli:prelude";
11pub const USER_PACKAGE: &str = "main";
12pub const SEP: char = '#';
13const MODULE_PREFIX: &str = "mod:";
14
15/// Opaque newtype; construct via the module builders to keep the format centralised.
16#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17pub struct MangledName(String);
18
19impl MangledName {
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23}
24
25impl fmt::Display for MangledName {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(&self.0)
28    }
29}
30
31impl AsRef<str> for MangledName {
32    fn as_ref(&self) -> &str {
33        &self.0
34    }
35}
36
37/// Canonical public package symbol.
38pub fn package_symbol(package: &str, symbol: &str) -> MangledName {
39    MangledName(format!("{package}{SEP}{symbol}"))
40}
41
42/// The export that checks a stored finalizer argument against the package's
43/// `finalizer` parameter. The `:` keeps it out of reach of every import.
44pub fn finalizer_argument(package: &str) -> MangledName {
45    MangledName(format!("{package}{SEP}finalizer:argument"))
46}
47
48/// Internal module symbol for a declaration in a multi-file package. `module` is
49/// the canonicalized package-relative path (`util`, `internal/math`) — no
50/// leading `./`, no `.subm` suffix. Root-exported (public) symbols use
51/// [`package_symbol`] instead.
52pub fn package_module_symbol(package: &str, module: &str, symbol: &str) -> MangledName {
53    MangledName(format!(
54        "{MODULE_PREFIX}{package}{SEP}{module}{SEP}{symbol}"
55    ))
56}
57
58pub fn prelude(symbol: &str) -> MangledName {
59    package_symbol(PRELUDE_PACKAGE, symbol)
60}
61
62/// Whether a symbol is declared by Submilli itself (the prelude or a
63/// `submilli:` standard-library module) rather than by a program or package.
64pub fn is_builtin(name: &MangledName) -> bool {
65    let unprefixed = name
66        .as_str()
67        .strip_prefix(MODULE_PREFIX)
68        .unwrap_or(name.as_str());
69    unprefixed.starts_with("submilli:")
70}
71
72pub fn extend(parent: &MangledName, suffix: &str) -> MangledName {
73    MangledName(format!("{}{}{}", parent.as_str(), SEP, suffix))
74}
75
76/// The member name of a host-implemented property's setter, `@set:<property>`.
77/// Codegen lowers `receiver.property = value` on a prelude interface to a call
78/// of `<iface>#@set:<property>` when the prelude declares one.
79pub fn property_setter(property: &str) -> String {
80    format!("@set:{property}")
81}
82
83/// Dispatch key for a static class member: `Class#static#name`. The extra
84/// `static` segment keeps statics disjoint from instance-method export keys
85/// (`Class#name`) — a static and an instance method may share a name.
86pub fn static_member(class: &MangledName, member: &str) -> MangledName {
87    extend(&extend(class, "static"), member)
88}
89
90/// Export key of the global that caches a function's closure, shared by every
91/// module that reads the function as a value. The angle brackets keep it apart
92/// from any source identifier.
93pub fn closure_cache(function: &MangledName) -> MangledName {
94    extend(function, "<closure>")
95}
96
97pub fn host(host_module: &str, symbol: &str) -> MangledName {
98    package_symbol(host_module, symbol)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::collections::BTreeMap;
105
106    #[test]
107    fn package_symbol_shape() {
108        assert_eq!(package_symbol("main", "foo").as_str(), "main#foo");
109        assert_eq!(
110            package_symbol("submilli:uuid", "v4").as_str(),
111            "submilli:uuid#v4"
112        );
113    }
114
115    #[test]
116    fn package_module_symbol_shape() {
117        assert_eq!(
118            package_module_symbol("main", "util", "foo").as_str(),
119            "mod:main#util#foo"
120        );
121        assert_eq!(
122            package_module_symbol("@pied-piper/stripe", "internal/math", "helper").as_str(),
123            "mod:@pied-piper/stripe#internal/math#helper",
124        );
125    }
126
127    #[test]
128    fn package_and_module_symbols_cannot_collide() {
129        assert_ne!(
130            package_symbol("main#util", "foo"),
131            package_module_symbol("main", "util", "foo"),
132        );
133    }
134
135    #[test]
136    fn prelude_shape() {
137        assert_eq!(
138            prelude("string_concat").as_str(),
139            "submilli:prelude#string_concat"
140        );
141    }
142
143    #[test]
144    fn extend_composes_three_segments() {
145        let iface = prelude("String");
146        assert_eq!(
147            extend(&iface, "concat").as_str(),
148            "submilli:prelude#String#concat",
149        );
150    }
151
152    #[test]
153    fn host_shape() {
154        assert_eq!(
155            host("submilli:console", "log").as_str(),
156            "submilli:console#log"
157        );
158    }
159
160    #[test]
161    fn newtype_keyed_lookup() {
162        let mut map: BTreeMap<MangledName, u32> = BTreeMap::new();
163        map.insert(package_symbol("main", "foo"), 42);
164        assert_eq!(map.get(&package_symbol("main", "foo")), Some(&42));
165        assert_eq!(map.get(&package_symbol("main", "absent")), None);
166    }
167}