Just foxing about.
Ticket created by ~icefox on ~icefox/garnet
I just discovered that bloody Haskell has a
LANGUAGE
directive that runs the file through the bloody C preprocessor, so it seems safe to say that while it is often pretty cursed, conditional compilation is one of those things where if you don't do it, someone else will do it for you and badly.So idk exactly what form this will take --maybe just a macro?-- but you can bet your ass it will be a core feature.
Seriously. cpp. Literally any templating language is better than that.
Comment by ~icefox on ~icefox/garnet
"it feels like there's a smaller, simpler [language] hiding inside [Rust] and wanting to escape"
hell yeah.
I usually think of the tradeoff as "convenience vs. control". Maybe with "complexity" as a third independent axis. "Control" is usually what people area really talking about when they talk about performance; usually it's really about needing the control to improve performance where it's necessary. Some tasks almost can't be done without a high level of control, things like writing memory managers or operating system kernels. Also sometimes performance is about giving up control and letting the compiler make decisions about inlining or memory layout or such that are better than yours. Though I guess that's another sort of convenience; you could always do what the compiler does, you just have to do it awkwardly by hand because you can't explore the majillions of possibilities as quickly as the compiler.
"Safety" is yet another axis, which makes my mental graph a hypercube, which is where thinking of it as a scatter plot or such stops being very useful. :-P In your chart I'd maybe rename "security" to "safety", but that's nitpicking.
But ANYway, I first want to see how convenient Garnet can be just by trying to find the "smaller, simpler language hiding inside Rust", and then once we get that right we can add cosmetics. Cosmetics do matter; the syntax is the user interface for the language, and UI is important. It is annoying but important to realize that Garnet isn't really trying to be "a more convenient Rust that gives up some control". I think that languages in that category really really needs to exist; Swift is the main one I know of, but there's room for more. But Garnet's goal is "a smaller Rust that simplifies the hairy parts where possible".
One of Rust's unspoken rules, at least as I understand it, is "never make the programmer guess what type something is"; if you write
if x { ... }
then you know for a fact thatx
is abool
, not "bool or some other random falsey value that might be user-extendable at runtime". If you writex + y
then you know thatx
andy
are always numbers of the same type, or you look up theAdd
trait onx
's type to find out whaty
can be. You always know where theAdd
trait is allowed to be defined, you can't just conjure up new ones at runtime. To me this is one of the inconvenient things that is actually really useful for maintaining safety without giving up control;+
can almost always be monomorph'ed, it doesn't need runtime type checks or dynamic dispatch, etc. So I'm pretty hesitant to violate that unspoken rule without being pretty damn sure that it's worth the trouble.aaaanyway. This has turned into me rambling about philosophy. More convenient pattern matching or unwrapping would be nice to have.
Comment by ~icefox on ~icefox/garnet
So, something to think about for aborting is that one key difference between Rust's
panic=unwind
andpanic=abort
is that unwinds can be caught (ie at thread boundaries), and aborts cannot. Aborting is a one-way trip; once the panic has happened there's no way for control flow to enter back into the original program. But it doesn't have to necessarily be an "immediately ask OS to kill this process" trip; it could in fact preserve the call stack, walk down it, and print out a backtrace. However it can't run destructors because that would make it no longer a one-way trip. Hell, this "diverge control flow without touching state" is basically what the OS does already when it saves a core dump.So really, the hard part is maintaining consistent state. If you abort, you don't have to care about maintaining consistent state anymore, because you're about to blow it up. If you have a way to catch unwinds, or otherwise deal with threads that may panic and don't want them to take down the entire process with it, then maintaining consistent state (ie trying to unlock mutexes, not leak memory, etc) becomes Problematic.
I guess I'm kinda restating what has already been said. Panics are simple and nice because they are unrecoverable. Once you start trying to recover from them shit gets complicated. Erlang deals with it by not sharing state but making it easy to encapsulate state. But part of the whole appeal of threads is being able to isolate state. But... threads also really suck at that because they don't have the guard rails that exist around OS processes. So maybe the answer is just "threads aren't the right tool for isolating state".
Maybe "panic program" and "panic thread" can be separate operations, somehow? That has some appeal. From their own point of view, each diverges, but something outside of it (the OS or the parent thread(???)) is free to do some kind of cleanup.
Comment by ~icefox on ~icefox/garnet
...made sure to allow code written in "top-to-bottom" order...
Agreed... though I tend to do things in the exact opposite order of you, with the main function at the bottom and things getting smaller and narrower as you go towards the start of the file. :-P But I get equally miffed with things like OCaml that forces me to always have things in that order, especially if they involve forward declarations. C'mon, everyone learns a topological sort function in their algorithms class! Are we really forcing humans to do this work instead of computers?!
There's semi-good reasons why someone would want to build their language this way, but I don't care about those reasons for Garnet. It's not like the order independence is especially hard to implement.
Comment by ~icefox on ~icefox/garnet
I think one thing to note here could be how to handle fields shared between some variants only...
Yeah, in my brain this basically becomes a subtyping problem, and then I stick a "not worth the effort of solving" label on it and put it on a shelf. :-P In Rust-y "never let someone dodge errors just 'cause it's tedious" style there's only really two options: you know this pattern match will never fail, so you don't actually need the pattern match, or you know this pattern match might fail sometimes, so they always have to do the whole thing. Declaring the data is a whole lot less annoying to me than having to pattern match on it every single time, like so:
match thing { A { foo } => ..., B { foo, bar, zing } => ..., C { foo, bar } => ..., D { foo, zing } => ..., }especially when what I really want to do is just do something to
foo
. All I want to write isdo_stuff(thing.foo)
. I can't really think of any sensible way to make it so you can writedo_stuff(thing.bar)
. If you insist that it only works when all fields have the same type though, perhaps you can write:match thing { _ { bar } => do_stuff(bar), _ => ..., }and it would match on any variant that had a field
bar
. Again, assuming that they all have the same type.(Also I appreciate the hell out of you making a heckin' diagram about this. :D)
That said, there's plenty room for clever tricks. Like in Crystal if you have its equivalent of an
Option
type, you can write:x : String? = something() if x do_stuff_with(x) endOutside the
if
block,x
has typeString | Nil
and inside theif
block,x
has typeString
. So it's literally justlet x: Option<String> = something(); if let Some(x2) = x { do_stuff_with(x2); }but with like 90% less ceremony to it. I dunno that I actually want to do something like that in Garnet --Rust does a very good job of making questionable things a bit inconvenient to write so that you have some time to think about how much you really need them-- but I appreciate how slick it is.
Ticket created by ~icefox on ~icefox/garnet
neko: ne thing that I've thought about in the past is mixing records and enums into some sort of hybrid type that has its variant (like an enum) and also fields regardless of the variant. I've also thought about allowing enum variants to be their own type, so you can e.g. have a function return IPAddress.IPv4.
icefox: I saw a lang recently that actually kinda did this, what was it... like if you did
enum Foo { A { a: i32, y: String }, B { a: i32 }, C { a: i32, something_else: Thing, whatever: Whatever } }you could write
let x: Foo = make_some_foo(); x.a
and it would access the a field on whatever variant it was.Hmm, what if you wrote it like this:
enum Foo { val a: i32; A { y: String }, B { }, C { something_else: Thing, whatever: Whatever } }and then you could also tell it what and were to put the discriminant and what type to use for it, just by making it a field. shit this is actually a good idea
neko: what syntax would you use to make a field the discriminator complex discriminators that are derived from more than one field yes or no? (probably better to say no due to complexity)
icefox: no (not yet)
Comment by ~icefox on ~icefox/garnet
Yeah you should, but overloading operations on booleans and integers Feels A Little Wrong. If I were awake I could construct some case of logic and comparison along the lines of
((a == b) and c) == d
that would do Non-Obvious Things if logical and bitwise operations were the same. It might work, I'm just cautious.You're right,
oldstate :rshift(18) :bxor(oldstate) :rshift(27) :to_i32()
honestly isn't the worst. These days I'm leaning away from UFCS and towards Elixir/ML/Elm style pipeline operators, so it would look something like:
oldstate |> rshift(18) |> bxor(oldstate) |> rshift(27) |> to_i32()
The
|>
sigil here feels a little weird, but is fine for now. Lemme dig some bitwise-heavy code out of my tests and see how it looks. And update it to the proposed borrowing syntax...-- part of leb128.gt fn read_unsigned(r Read) Result[U64, ReadError] = let mut result: U64 = 0 let mut shift: U32 = 0 loop let mut buf Arr[U8] = [0] Read.read_exact(r, buf&!) if shift == 63 and buf[0] != 0x00 and buf[0] != 0x01 then return Err(ReadError.Overflow) end let low_bits = low_bits_of_byte(buf[0]) as U64 result = result |> bitand(low_bits |> shl(shift)) if buf[0] |> bitand(CONTINUATION_BIT) == 0 then return Ok(result) end shift = shift + 7 end end
Hmmm. Gets kinda weird when you have to start nesting, like
result |> bitand(low_bits |> shl(shift))
With your syntax that would be something likeresult:bitand(low_bits:shl(shift))
which does feel less noisy?Let me try it out on my PRNG test code:
fn rand32_i32(rand Rand32) {Rand32, I32} = let oldstate = rand$.state let mut newrng = rand newrng$.state = oldstate * RAND32_MULTIPLIER + rand$.inc let xorshifted = oldstate |> shr(18) |> bitxor(oldstate) |> shr(27) |> to_u32() let rot = oldstate |> shr(59) |> to_i32() let num = xorshifted |> bitror(rot) |> to_i32() {newrng, num} end
Eyyyy that's kinda weird but... honestly not terrible, I think?
Comment by ~icefox on ~icefox/garnet
could it be there's kinda a reason it's not used in modern programming languages?
There is, it was the only real option for scoping in a lot of early Lisp's an it suuuuuuuuucked. (I think Emacs Lisp still uses it?) That said it's used in bits and pieces in a lot of languages... printing out a backtrace is basically walking down the dynamic scope stack. So using it for things like passing around logging state makes sense to me. Other language features that operate essentially by inspecting dynamic scope (usually implemented by the call stack, one way or another): exception handlers, destructors, etc.
_ENV
in Lua is a cogent association, since as far as I can tell that's basically how it implements scoping.That said, if we have destructors, we can implement contexts pretty nicely in userland code, just as (locked) mutable globals for particular types. In my mind those are used for different things than implicit modules or such are though, and require some noticeable amount of setup beforehand.
...I had an idea for using this to set scoped panic modes inside functions, so you can call
with_unwind(|| thing())
and ifthing()
panicked then it would unwind back to that point and maybe provide some metadata. Soooooo I just reinvented exceptions. As one does.