Copyright 2023 Travis J. West, https://traviswest.ca, Input Devices and Music Interaction Laboratory (IDMIL), Centre for Interdisciplinary Research in Music Media and Technology (CIRMMT), McGill University, Montréal, Canada, and Univ. Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRIStAL, F-59000 Lille, France
SPDX-License-Identifier: MIT
The base helpers used by components and endpoints to declare their names conveniently and concepts for bindings to access names generically are defined in sygah-metadata: Metadata Helpers and sygac-metadata: Text Metadata Reflection respectively. This document deals with transformations over names required for bindings.
Names are spelled differently when generating bindings. For example, a processor that applies a one pole exponential moving average low pass filter to its input might be called "Simple Lowpass", and an endpoints called "cutoff frequency". In its Open Sound Control binding, these might have to be spelled differently, such as "SimpleLowpass" and "SimpleLowpass/cutoff_frequency". In a command-line style binding or as a Pure Data externail, it might be more convenient and idiomatic to spell them "simple-lowpass" and "cutoff-frequency". In other contexts, different spellings might be required.
To support all these use cases, one approach is for the author of the processor to specify different spellings for alternative applications. For example, the name of the processor might be specified in this way:
This obviously has some shortcomings, not least of all that the author has to manually repeat the name with however many spellings are required. Generating an address space, such as for Open Sound Control bindings, might be especially cumbersome. The various spelling conventions are reasonably regular, so it should be possible to do the conversion from one canonical natural language name to various technical spellings with an algorithm. Since the name is presumably known at compile time, these conversion can be performed at compile time, imposing no runtime cost.
Basic transformations
Snake case is achieved by replacing spaces with underscores. Kebab case is achieved by replacing spaces with dashes. Lowercase is achieved by replacing uppercase letters with their lowercase equivalent. Uppercase, also called all caps, also called yelling, does the inverse of lowercase. These are all simple one-to-one transformations that are easily defined on a character by character basis.
// @='mappings'
constexprchar snake(char c) {return c == ' ' ? '_' : c;}
constexprchar kebab(char c) {return c == ' ' ? '-' : c;}
constexprchar lower(char c)
{
if ('A' <= c && c <= 'Z') return c+('a'-'A');
return c;
}
constexprchar upper(char c)
{
if ('a' <= c && c <= 'z') return c-('a'-'A');
return c;
}
// @/
Taking one or more of these mappings as template arguments, we can generate a mapping that composes them sequentially:
constexprchar operator()(char c) { return compose<mappings...>{}(mapping(c)); }
};
// @/
Compile-time strings
The main challenge then is where to put the transformed string at compile time. We're not allowed to allocate memory and then return a pointer to that memory, so we can't return const char *. We could use std::array instead, but then the user would have to extract the character pointer anytime they want to use the data as a string, e.g. snake_case("example name").data(), which we would like to avoid. It's also not possible to use a template variable e.g. snake_case<"example name">, since string literals cannot be used as non-type template parameters easily, and even if we use the string_literal type defined in helpers/metadata.lili, we will eventually run into problems when someone accidentally tries to pass a decayed string literal to our transformation. The problem is that string literals decay to points at the slightest provocation, losing essential information about their length in the process.
One solution comes from the fact that the strings we want to process are assumed to be static _consteval member functions of the named components and endpoints we are reflecting over. From this, we can use the type of the processor as a template parameter to a struct with its own static constexpr string data, called value below. We can initialize this string with a lambda that applies the mapping to the name of the component, which gives us the following implementation:
In case there are no mappings given, which may be useful for certain templates e.g. as a default parameter, we can save some space by providing a specialization:
In order to statically allocate enough memory for the transformed string, we need to know its size. We can write a simple compile-time evaluated function to count how many characters there are in the string. We trust that the compiler will catch if a non-null-terminated string is passed in, e.g. by recognizing out of bounds access on the compile-time constant input.
// @='string length function'
template<typename Device>
requiresrequires {Device::name();}
_consteval auto name_length()
{
size_t ret = 0;
while (Device::name()[ret] != 0) ret++;
return ret;
}
// @/
Syntax sugar
To save the user having to write out respeller with all of its template arguments, we provide some "aliases" for expected use cases, so that the user can write snake_case(x) or snake_case_v<T> as seen above.
My initial thought here was to literally use a template type alias for the function-like syntax–
template<typename NamedType> using snake_case = respeller<NamedType, snake>;
–and this works fine with gcc. Unfortunately, clang doesn't allow argument deduction for template aliases:
...thetest.cpp:18:24: error: alias template
'snake_case' requires template arguments; argument deduction only allowed for
The respeller defined above assumes that the textual mapping functions are one-to-one, e.g. spaces become underscores, lower case becomes uppercase, etc. However, some common spelling conventions, e.g. CamelCase and dromedaryCase can reduce the length of the output string. This will require some modification of our approach, to allocate the correct string length, and to iterate through the input string. In the most general framework, it might be useful to replace arbitrary regular expression matches with given replacements, or to add strings where matches are located. Such extensions should be added if these features become necessary.