Sure, it is basically a dynamic language at compile time, but I feel like the downsides of that are significantly mitigated by the fact that it's running at, you know, compile time -- you'll still get compile-time errors for any mistakes!
(Some people even try to write entire programs in dynamic languages, as crazy as that is.)
Regarding "might as well have a makefile rule," it's not really fair -- in practical terms having a seemless way to evaluate functions in your code at compile time is way different than doing codegen. Like, in practice, how would you write a type-safe format function using codegen? You could do it, but it would be very, very gnarly. Very different.
Even Go -- which famously for a while tried to advocate codegen as an alternative to generics (w/ stuff like stringer to codegen print functions for human names for constants) -- resorts to dynamic types & runtime errors for formatting.
So let's see:
formatting approaches we've seen so far:
– dynamic w/ runtime errors (e.g. Go, C#, C, ...)
– static via baked into compiler support
– static via special-case restricted compile-time evaluated language (e.g. rust macros)
- static via full-lang compile-time eval (e.g. zig)
I think you said all those are awful, so, I'm curious -- what's the better approach you have in mind?
IMO the least-bad vaguely mainstream approach is what people do in Haskell with e.g. formatting or fmt. Your formatter should be a first-class strongly typed value, and the way you form/construct that value should be the normal way you form values in your language.
I do think there's space for a "write a complex value literal by writing a string in a DSL and embedding that into the source" feature. But that shouldn't be specific to format strings, and it shouldn't be by running arbitrary code at compile time; rather it should be a language feature. I haven't seen a version of that that I'm really happy with yet, but Haskell's OverloadedStrings or Scala's StringContexts are some baby steps in the right direction.
My haskell knowledge is minimal, but looking at formatting and fmt library examples, what stands out is you're operating in the syntax of the language, so it's clumsier to use (imo) than a string with interpolations. (It seems not-dissimilar to C++ streams, to me.)
I guess your second paragraph gets at that. I think you're arguing for, "build it into the language, but flexibly." Fair enough!
(Some people even try to write entire programs in dynamic languages, as crazy as that is.)
Regarding "might as well have a makefile rule," it's not really fair -- in practical terms having a seemless way to evaluate functions in your code at compile time is way different than doing codegen. Like, in practice, how would you write a type-safe format function using codegen? You could do it, but it would be very, very gnarly. Very different.
Even Go -- which famously for a while tried to advocate codegen as an alternative to generics (w/ stuff like stringer to codegen print functions for human names for constants) -- resorts to dynamic types & runtime errors for formatting.
So let's see:
formatting approaches we've seen so far:
– dynamic w/ runtime errors (e.g. Go, C#, C, ...)
– static via baked into compiler support
– static via special-case restricted compile-time evaluated language (e.g. rust macros)
- static via full-lang compile-time eval (e.g. zig)
I think you said all those are awful, so, I'm curious -- what's the better approach you have in mind?