If your language has block expressions, why not use block expressions for all grouping! be free, parentheses! `{ x + 10 } * y`. parens can be unambiguously tuples, just like they always wanted.
@dotstdy I showed this to @ianh and he managed to temporarily convince me (without necessarily holding this opinion himself) that function calls and tuples should use `[...]` instead of `(...)`, and then `(...)` can be used for blocks, after which your same idea applies. Were Lisp M-expressions right all along?

@zwarich @ianh instead of using `[..]` for calls you can use the Lua trick of having functions accept an anonymous structure. `do_stuff { 1, 2, 3 }`

Thus `(..)` is grouping / block, `{..}` is a struct / tuple, and `[..]` is an array.

now we're cooking

@dotstdy @ianh Has any recent language adopted uncurried single arg style where product types are used for multiple args rather than implicit currying? I know that Swift originally started in this direction, but I forget the actual reason why they backed away from this. Can you refresh my memory, @joe ?

@zwarich @dotstdy @ianh @joe Related, but why isn't first class multiple return values a thing more often?

For example, the C++26 standard library has senders/receivers which essentially work via continuations, so you can write async functions that take N inputs and M outputs natively without resorting to tuples. You can even send outputs of different types without having to box them into a sum type (because they just statically dispatch to different overloads).

@foonathan @dotstdy @ianh @joe My guess has always been that first-class multiple return values are overlooked because tuples serve their use case fairly well. When I think of use cases for them, my mind always goes to alternate ABIs like returning bools in status flags, etc.
@zwarich @foonathan @dotstdy @ianh I’m finding that ownership and lifetime dependent values really stretch the equivocation between tuples and multiple values. a tuple of borrows can’t always be used as a borrowed tuple, for instance

@joe @foonathan @dotstdy @ianh I think most languages that would allow you to return borrows would let you also store them as struct fields, etc. and just add a borrowed pointer type, in which case you could just make a tuple of borrows. I guess you could take a purist "parameter modes" approach and define borrowed struct fields via parameter modes on the struct's constructor? However, I think it might be tricky to make this work well with generics.

Another place where a similar distinction comes up is in-place construction of return values. Rust doesn't have this, but I assume that some successor language will want this to support internal self-reference, e.g. for efficient containers with inline capacity

@zwarich @foonathan @dotstdy @ianh yeah return value emplacement was the other thing i had in mind where a tuple (in its naive unexploded representation) isn't the same thing as multiple values.

even with first-class borrows, the way swift tries to allow for tuples to be magically exploded and imploded by the implementation fights against the very concept of a borrow-of-tuple ever existing, since you really want a contiguous representation for that borrow to refer to

@joe @foonathan @dotstdy @ianh Another funny realization is that if all return values are returned by writing to a passed reference, then you could have functions with no actual return values and only out-params (with the appropriate pointer type that must be written before returning). It's the use of resources like registers (which may be implicitly used by other code in the function) that necessitates presenting a value at the point of return.

You could take this to the next level and actually have out-params that "steal" registers when written to, but at that point you're probably in meme language territory.

@zwarich @joe @foonathan @dotstdy @ianh My prediction is that at some point in the next 5 years this whole “systems language” fad will blow over as programmers get tired of constantly being forced to deal with value semantics and unique ownership, and they’re going to rediscover the simplicity and elegance of garbage collection with uniform tagged pointers, at which point they’ll give this paradigm some really dumb name and pretend it’s brand new
@slava @zwarich @foonathan @dotstdy @ianh i think there's another local optimum in the Rustlike space if you steered the ecosystem toward coarse-grained ownership, where most values are owned in bulk by arenas or GC regions, and individually managed resources are the minority

@joe @slava @foonathan @dotstdy @ianh This is one of the things I'm trying to do in my new language. There are a few new features required, e.g. variable-dependent types (so that indices into an arena can depend on the arena instance), coeffects/implicit context and coeffect polymorphism, "linear" types, etc.

I am hoping this will remove a lot of the complexity of a language like Rust and look more like the C/Zig one would write (except now with static checking), but I'm sure there will be ways in which things are more complicated compared to an "isolated ownership" language like Rust.

@zwarich @joe @slava @foonathan @dotstdy @ianh I hope The Zwarich Programming Language is simple enough for dumb guys like me who can’t follow along with this discussion (i.e., not compiler engineers)
@shac @joe @slava @foonathan @dotstdy @ianh I can't guarantee that, but I can probably get my wife to draw art for cute cat-themed tutorials.

@shac @joe @slava @foonathan @dotstdy @ianh To be a bit more serious, I think it’s hard to predict how easy things will be to understand in advance. When you implement a language, you are usually spending a lot of your time thinking about the dark corners of features or rare interactions between them rather than the phenomenology of actually writing programs.

As an example, I never would have guessed that Rust would reach the level of adoption it currently has outside of its original niche. While a lot of this is due to features outside of its initial value proposition or the wider ecosystem, the people that come for these reasons still have to put up with everything else.

Since I’m making an inherently weird language, I’ve adopted a few principles to make it more understandable:

1) Avoid prematurely adding features like type classes/traits that invite an endless horizon of potential generalizations but don’t have much to do with my basic thesis.

2) Constantly look at everything I have in the language and try to refactor it by reusing common technology, even if this enables users to do things that seem useless to me. To go back to Rust, since its development is the result of a sometimes contentious social process and constrained by strict backwards compatibility, it is incredibly asymmetric.

3) Plan the development of the language in stages, where at each stage I am building something that is actually useful and internally consistent, but is also deliberately missing features that I plan to add in later stages.

I guess I should actually write a post about my goals and ideas. I previously told myself I don’t want to release anything until I have a bootstrapped self-hosted compiler, but maybe that’s too conservative.

@zwarich @shac @joe @slava @foonathan @dotstdy @ianh what is the basic thesis?

@doctorgoktor @shac @joe @slava @foonathan @dotstdy @ianh My basic thesis is that it's possible to make a safe programming language that enables many of the standard programming patterns for improving performance that might be labelled "data-oriented design", e.g. using arenas (including with indices rather than pointers), stealing bits from pointers, AoS/SoA transposition, rare parts of structs that live externally, etc.

I have lots of other smaller theses (including some just related to the implementation techniques rather than the user experience), but this one is the core because if I fail to achieve it then I don't think there is enough to distinguish it from other languages.

@slava can I pin someone else's post on Mastodon?
@slava @zwarich @joe @foonathan @dotstdy @ianh @wingo Honestly, damn I really hope so. While I’m obvs very glad Rust has got memory safety (and much more) into the systems languages space I’m less happy that it’s encouraged a whole lot of new software written in a language that is extremely low-level for the task it’s being used for. The next task is to convince programmers not to choose the thing that’s fastest, but the thing that’s fast enough for the task and easiest on our human noggins
@slava @zwarich @joe @foonathan @dotstdy @ianh @wingo (We could have had a memory safe rewrite of coreutils 15 years ago or more! Would garbage collection really cause a problem for the performance of ‘cp’ or ‘tee’‽)
@dpk @slava @joe @wingo the GC itself is probably the least of one's concerns in that space. Namely you really don't want that layer of applications to depend on a chonky language runtime.
@dotstdy @dpk @slava @joe @wingo how chonky is fil-c? my understanding is that it's designed to address this exact issue (by giving C a reasonably-lightweight GC'd runtime so it can be memory-safe without having to rewrite anything)
@ianh @dpk @slava @joe @wingo yea as long as you can tank the perf and memory, but fil-c didn't exist 15 years ago. I don't know there's much else in the same ballpark?
@slava @joe @foonathan @dotstdy @ianh I thought about this some more the past few days and one of the things that I keep coming back to is that if you add mutation to your language you’re faced with the problem of data races, and the type system features for the elimination of data races don’t look all the different from ownership systems used by safe systems languages. There is Midori (https://www.cs.drexel.edu/~csg63/papers/oopsla12.pdf), Pony (https://www.ponylang.io/media/papers/fast-cheap.pdf), Verona (https://www.microsoft.com/en-us/research/publication/reference-capabilities-for-flexible-memory-management/), etc. Of course, you could take the Java/C# copout of making data races memory safe (and maybe tack on the Go approach of optional dynamic race detection), but the issues that arise from this model are not very easy to debug.
@zwarich @slava @foonathan @dotstdy @ianh yeah we tried the cop-out approach in Swift for a minute and didn't get too far either. debugging aside, aliasable mutations also pretty much totally kill any non-global optimization. i think "mutation requires exclusivity" is a good default, with Atomic/Cell/RefCell/etc. on the side for cases where you need less stricture, but maybe you can integrate those alternative mutations more smoothly into the language
@joe @zwarich @foonathan @dotstdy @ianh As far as I understand it, Swift's data race safety prevents two kinds of problems:
- the race where you load a reference counted pointer, and someone else releases the last reference and frees the object before you retain it
- tearing when you load or store a value that's larger than a word
In Java, GC prevents the first bug and all values fit in a machine word so they define away the second bug. You might still have a "high level" data race in your application logic, but Swift doesn't prevent those either, right? What data race safety problems does Java have that Swift doesn't, which are difficult to debug?
@slava @zwarich @foonathan @dotstdy @ianh you can never define away logical races, of course, but at least in theory, defining away even the potential for races within code that should never be subject to concurrent accesses reduces the number of places you would have to reason about concurrency correctness. just like having less unsafety is better than everything-is-unsafe, even if you don't eliminate unsafe code entirely
@slava @joe @foonathan @dotstdy @ianh Java-style data race semantics introduce behavior that can’t be explained by a global order of events. Now it’s a bit funny for me to criticize this while I also want to add language support for “relativistic” techniques (e.g. epoch-based reclamation and RCU) that have the same problem, but I feel like it’s something that you should opt into explicitly rather than something that silently permeates your entire program.
@zwarich @slava @foonathan @dotstdy @ianh with strict non-aliasing you can also reduce in-place mutation down to pure consume-in, produce-out functions with linearity, and a pre-assigned place to do the in-place update optimization

@joe @slava @foonathan @dotstdy @ianh There is another approach that I didn’t mention but that Swift also dabbles in, which is copy-on-write based on dynamic reference uniqueness (as determined by a reference count). We also used this in Lean, in a more “pure” form where the mutable updates were made more explicit, but it has a few major drawbacks:

1) You are forced to use immediate reference counting, which means immediate *atomic* reference counting in a multithreaded environment. Lean goes a step further than Swift and uses non-atomic reference updates if the object hasn’t yet escaped to multiple threads, but in a way this makes things worse because the gap between single-threaded and multi-threaded code gets even wider.

2) I’m not sure it’s possible to make a compiler that always meets user expectations of when copies will occur. Ensuring uniqueness often requires a linear flow of use-to-def edges, and it is not obvious when a def should “depend” on a prior use, especially across function calls (and especially across ABI boundaries). There are also situations where the optimizer can recognize that a value is non-unique anyways and thus all use-to-def edges can be ignored, although at least this has heavy overlap with RC-related optimizations that you probably want to do anyways.

I had some conversations with Andy and Michael on the Swift team long ago about these problems, and am somewhat familiar with the “ownership SSA” solution that they came up with, but I don’t know how it turned out in practice. When I worked on Lean, some of my biggest improvements were in this area, but the logical next steps seemed like a huge upgrade in compiler complexity. It felt like dealing with this problem would slowly become the main focus of the compiler.

3) It’s difficult to make concurrency primitives (e.g. a mutex owning its contents) that stay on the happy path of single-threaded reference uniqueness. We never did this for Lean, but I think it would require some dynamic escape analysis that goes back and poisons the value stored in the mutex if any sufficiently derived value escapes.

@zwarich @slava @foonathan @dotstdy @ianh yeah, i don't think dynamic COW totally eliminates the desire for reference capabilities or something similar to also provide static control over what values need atomic refcounting. unless you're ok with accepting straight-up isolated "heaps" per thread/task/other execution unit for value types
@joe @slava @foonathan @dotstdy @ianh I realized that there is a point in the tradeoff space here that (afaik) nobody has ever evaluated. Unlike linear types where even getters have dependency edges to the next use (even if it doesn’t modify the underlying data), you could instead have dependency edges that only need to be collected and connected to the next “def” (i.e. mutating use). You still need to linearly thread defs or risk needing to copy everything.
@zwarich @slava @foonathan @dotstdy @ianh I was also recently reminded of another use case for specializing in-place and new-value-producing variants of an operation with COW, since you can integrate both into the uniqueness check and use the new-value-producing variant to initialize your new copy in cases where you need to copy
@zwarich And of course Go is not actually memory safe in the presence of data races, e.g. https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
Golang data races to break memory safety

Go is becoming more and more popular as a programming language and getting more scrutiny from a security point of view. You might remember m...

@pervognsen I wonder how difficult it would be for Go to achieve Java-style race safety, at least on architectures with 16B relaxed atomic accesses, but I’m not proficient enough with Go’s implementation to list all of the edge cases.
@zwarich Yeah, and AMD and Intel semi-recently started guaranteeing 16-byte atomic loads/stores: https://lobste.rs/s/omasxh/there_is_no_memory_safety_without_thread#c_1nrsdu.
There is no memory safety without thread safety

64 comments

Lobsters