// Linking: a file plus everything it includes, folded into one set of declarations.
//
// Thrift names a type from an included file by that file's basename — `shared.Person`.
// MoonBit has one namespace per package, so the link step gives each included file's
// types a prefix: `shared.Person` becomes `SharedPerson`. Names stay stable whatever
// order the includes are read in, and the wire format does not care either way.
///|
/// How an include is read: the path as written, resolved by the caller.
pub type Loader = (String) -> String?
///|
/// A document with its includes folded in.
pub(all) struct Linked {
structs : Array[Struct]
enums : Array[Enum]
// The root file's services, with their base resolved.
services : Array[Service]
// Every service, the included ones too, by the name the output uses.
by_service : Map[String, Service]
// The constants of every file, since one package holds them all.
constants : Array[Constant]
}
///|
fn split_dot(name : String) -> (String, String)? {
for i in 0.. String {
let mut cut = -1
for i in 0.. String {
let dir = dir_of(path)
if dir == "" {
path
} else {
path[dir.length() + 1:path.length()].to_owned()
}
}
///|
/// `shared.thrift` → `shared`, the prefix its types take.
fn include_prefix(path : String) -> String {
let name = basename(path)
match name.has_suffix(".thrift") {
true => name[0:name.length() - 7].to_owned()
false => name
}
}
///|
fn join(dir : String, path : String) -> String {
let absolute = path.has_prefix("/") ||
path.has_prefix("\\") ||
(path.length() > 1 && path.get_char(1) is Some(':'))
if dir == "" || absolute {
path
} else {
dir + "/" + path
}
}
///|
fn capitalise(name : String) -> String {
if name.length() == 0 {
return name
}
let first = name.get_char(0).unwrap_or('a')
first.to_ascii_uppercase().to_string() + name[1:name.length()].to_owned()
}
///|
/// The name a declaration takes in the output: prefixed files get `SharedPerson`, the
/// root file keeps `Person`.
fn global_name(prefix : String, name : String) -> String {
if prefix == "" {
name
} else {
capitalise(prefix) + capitalise(name)
}
}
///|
fn all_upper(name : String) -> Bool {
for i in 0.. String {
if name.length() == 0 {
return name
}
if !name.contains("_") && !all_upper(name) {
let first = name.get_char(0).unwrap_or('a')
return first.to_ascii_lowercase().to_string() +
name[1:name.length()].to_owned()
}
let mut out = ""
let mut word = 0
let mut at_word_start = true
for i in 0.. String {
let value = camel_case(name)
if prefix == "" {
value
} else {
camel_case(prefix) + capitalise(value)
}
}
///|
struct Linker {
load : Loader
// Resolved path → prefix, so a file included twice is read once.
seen : Map[String, String]
// Prefix → resolved path, so two files with the same basename are refused.
prefix_path : Map[String, String]
structs : Array[Struct]
enums : Array[Enum]
// Every `typedef`, by the name the output would give it.
aliases : Map[String, TType]
// Their names in the order the files declare them, so a broken one is named the same
// way on every run.
alias_order : Array[String]
constants : Array[Constant]
// The output name of each constant, so two that would land on one name are refused.
const_names : Map[String, String]
// Every service, by the name the output uses, so `extends` can find its base.
all : Map[String, Service]
// The services of the file being compiled — the ones that get emitted.
root : Array[Service]
}
///|
fn Linker::rename_type(
self : Linker,
ty : TType,
file : String,
prefix : String,
locals : Array[String],
includes : Array[String],
) -> TType raise Refused {
ignore(self)
match ty {
Named(name) =>
if locals.contains(name) {
Named(global_name(prefix, name))
} else {
match split_dot(name) {
Some((qualifier, rest)) =>
if includes.contains(qualifier) {
Named(global_name(qualifier, rest))
} else {
refuse(
file,
0,
"`\{name}`: no `include` in this file brings in `\{qualifier}`",
)
}
None => Named(name)
}
}
List(inner) => List(self.rename_type(inner, file, prefix, locals, includes))
Set(inner) => Set(self.rename_type(inner, file, prefix, locals, includes))
Map(key, value) =>
Map(
self.rename_type(key, file, prefix, locals, includes),
self.rename_type(value, file, prefix, locals, includes),
)
_ => ty
}
}
///|
/// A value's `Enum.VALUE` names the enum the same way a type does, so it takes the same
/// prefix; the containers around it are visited for that.
fn Linker::rename_value(
self : Linker,
value : ConstValue,
file : String,
prefix : String,
locals : Array[String],
includes : Array[String],
) -> ConstValue raise Refused {
ignore(self)
match value {
CEnum(name, tag) =>
match self.rename_type(Named(name), file, prefix, locals, includes) {
Named(renamed) => CEnum(renamed, tag)
_ => CEnum(name, tag)
}
CList(items) =>
CList(
items.map(v => self.rename_value(v, file, prefix, locals, includes)),
)
CMap(entries) =>
CMap(
entries.map(pair => {
(
self.rename_value(pair.0, file, prefix, locals, includes),
self.rename_value(pair.1, file, prefix, locals, includes),
)
}),
)
_ => value
}
}
///|
fn Linker::read(
self : Linker,
file : String,
source : String,
prefix : String,
) -> Unit raise Refused {
let doc = parse(file, source)
let locals = doc.structs.map(s => s.name)
let names = doc.enums.map(e => e.name)
let aliases = doc.typedefs.map(t => t.name)
let declared = locals + names + aliases
let includes = doc.includes.map(p => include_prefix(p))
// Read the includes first: a reference into one needs that file to have been given
// its prefix already.
for path in doc.includes {
let resolved = join(dir_of(file), path)
let inc_prefix = include_prefix(path)
let known = self.prefix_path.get(inc_prefix)
match known {
Some(other) if other != resolved =>
refuse(
file,
0,
"two included files are both named `\{basename(path)}`; rename one of them",
)
_ => ()
}
match self.seen.get(resolved) {
Some(existing) if existing != inc_prefix =>
refuse(file, 0, "`\{path}` is included twice under different names")
Some(_) => ()
None => {
self.seen[resolved] = inc_prefix
self.prefix_path[inc_prefix] = resolved
match (self.load)(resolved) {
Some(text) => self.read(resolved, text, inc_prefix)
None =>
refuse(
file,
0,
"cannot read the included file `\{path}` (looked for \{resolved})",
)
}
}
}
}
for item in doc.structs {
let fields = item.fields.map(f => {
let ty = self.rename_type(f.ty, file, prefix, declared, includes)
{ id: f.id, name: f.name, ty, optional: f.optional, }
})
self.structs.push({
name: global_name(prefix, item.name),
fields,
kind: item.kind,
})
}
for item in doc.enums {
self.enums.push({
name: global_name(prefix, item.name),
values: item.values,
})
}
// An alias keeps a name here so a reference to it can be resolved later; the name is
// substituted away before anything is generated.
for item in doc.typedefs {
let ty = self.rename_type(item.ty, file, prefix, declared, includes)
let name = global_name(prefix, item.name)
self.aliases[name] = ty
self.alias_order.push(name)
}
for item in doc.constants {
let ty = self.rename_type(item.ty, file, prefix, declared, includes)
let value = self.rename_value(item.value, file, prefix, declared, includes)
let name = const_name(prefix, item.name)
match self.const_names.get(name) {
Some(other) =>
refuse(
file,
item.line,
"`\{item.name}` and `\{other}` are both written as `\{name}` in the output; rename one",
)
None => self.const_names[name] = item.name
}
self.constants.push({ name, ty, value, line: item.line, })
}
for service in doc.services {
let base = match service.base {
Some(name) =>
match split_dot(name) {
Some((qualifier, rest)) =>
if includes.contains(qualifier) {
Some(global_name(qualifier, rest))
} else {
refuse(
file,
0,
"`\{service.name}` extends `\{name}`, but no include brings in `\{qualifier}`",
)
}
None => Some(global_name(prefix, name))
}
None => None
}
let functions = service.functions.map(f => {
let params = f.params.map(p => {
let ty = self.rename_type(p.ty, file, prefix, declared, includes)
{ id: p.id, name: p.name, ty, }
})
let ret = match f.ret {
Some(ty) => Some(self.rename_type(ty, file, prefix, declared, includes))
None => None
}
let exceptions = f.exceptions.map(x => {
let ty = self.rename_type(x.ty, file, prefix, declared, includes)
{ id: x.id, name: x.name, ty, }
})
{ name: f.name, ret, params, exceptions, }
})
let named = { name: global_name(prefix, service.name), base, functions, }
self.all[named.name] = named
if prefix == "" {
self.root.push(named)
}
}
}
///|
/// Substitute aliases until a real type is reached. `stack` is the chain being expanded,
/// which is how an alias of itself is caught instead of looping.
fn expand(
ty : TType,
aliases : Map[String, TType],
stack : Array[String],
file : String,
) -> TType raise Refused {
match ty {
Named(name) =>
match aliases.get(name) {
Some(target) => {
if stack.contains(name) {
refuse(
file,
0,
"`\{name}` is an alias of itself (\{stack.join(" -> ")} -> \{name})",
)
}
stack.push(name)
let out = expand(target, aliases, stack, file)
ignore(stack.pop())
out
}
None => Named(name)
}
List(inner) => List(expand(inner, aliases, stack, file))
Set(inner) => Set(expand(inner, aliases, stack, file))
Map(key, value) =>
Map(
expand(key, aliases, stack, file),
expand(value, aliases, stack, file),
)
_ => ty
}
}
///|
fn expand_function(
func : Function,
aliases : Map[String, TType],
file : String,
) -> Function raise Refused {
let params = func.params.map(p => {
let ty = expand(p.ty, aliases, [], file)
({ id: p.id, name: p.name, ty, } : Param)
})
let ret = match func.ret {
Some(ty) => Some(expand(ty, aliases, [], file))
None => None
}
let exceptions = func.exceptions.map(x => {
let ty = expand(x.ty, aliases, [], file)
({ id: x.id, name: x.name, ty, } : Param)
})
({ name: func.name, ret, params, exceptions, } : Function)
}
///|
fn expand_service(
service : Service,
aliases : Map[String, TType],
file : String,
) -> Service raise Refused {
let functions = service.functions.map(f => expand_function(f, aliases, file))
({ name: service.name, base: service.base, functions, } : Service)
}
///|
/// Fold `file`'s includes into it. `load` is what reads an included file, so the
/// compiler itself stays free of a file system and a test can hand in a map.
pub fn link(
file : String,
source : String,
load : Loader,
) -> Linked raise Refused {
let linker : Linker = {
load,
seen: Map([]),
prefix_path: Map([]),
structs: [],
enums: [],
aliases: Map([]),
alias_order: [],
constants: [],
const_names: Map([]),
all: Map([]),
root: [],
}
linker.seen[file] = ""
linker.read(file, source, "")
let aliases = linker.aliases
// Expand every alias once, so one that loops is refused even if nothing uses it yet.
for name in linker.alias_order {
ignore(expand(Named(name), aliases, [], file))
}
let structs = linker.structs.map(s => {
let fields = s.fields.map(f => {
let ty = expand(f.ty, aliases, [], file)
({ id: f.id, name: f.name, ty, optional: f.optional, } : Field)
})
({ name: s.name, kind: s.kind, fields, } : Struct)
})
let by_service : Map[String, Service] = Map([])
for name, service in linker.all {
by_service[name] = expand_service(service, aliases, file)
}
let services = linker.root.map(s => expand_service(s, aliases, file))
let constants = linker.constants.map(c => {
let ty = expand(c.ty, aliases, [], file)
({ name: c.name, ty, value: c.value, line: c.line, } : Constant)
})
{ structs, enums: linker.enums, services, by_service, constants, }
}