~icefox/garnet#53: 
Syntax bikeshed

This is the place to ponder the syntax things that I kindasorta dislike or want to think more about. The list so far:

  • sum token for sum types, it's not uncommon to have variables named sum imo, but enum is reserved for things that are actually enumerable and there really aren't any other good words for it
  • Struct decls via struct x: T, y: T end, rather ugly to put inline as the type in let exprs and such..
  • Postfix vs prefix type composition; T^[4] vs [4]^T for array[4] of pointer to T. To me the former Feels Better, but the latter is slightly easier to parse (I think?) and maps better to how you would write it in English.
  • Related, syntax for type parameters. Say Vec(T) vs Vec[T]. The former looks like a function call, the latter looks like an array index, and I'm 100% happy with neither. If we do the latter it gets weird with our array syntax as well, and raises the prefix/postfix question again; one way or another we need some kind of nesting. For a complex thought experiment, Result(T^[4], E)^ is "pointer to a Result( Array[4] of pointer to T, E)`. ...In the end I think I'll keep the postfix composition.
  • Colons for type annotations; let foo: T = ... vs let foo T = .... And, more significantly, fn foo(x: T, y: T2): T3 vs fn foo(x T, y T2) T3. I originally had colons, then I said "aren't these actually unnecessary clutter?" and took them away, and lo and behold it works fine.
  • Closure and function type syntax. Right now it's fn(T, T2) T3 for the type which is... okay but kinda noisy, and fn(x T, y T2) T3 = ... end for a function expression which is a lot more noisy when you're going to have these things inline in a function call or something. So, we need a shortcut syntax or something. But I kinda hate the Rust/Ruby |x| expr style for uncertain reasons -- they don't nest well, they don't look like other function decls, doing pattern matching in the args results in |(a, b), c| which my eye dislikes, etc. But I haven't bothered putting a lot of thought into what might be better.
Status
REPORTED
Submitter
~icefox
Assigned to
No-one
Submitted
1 year, 14 days ago
Updated
9 days ago
Labels
T-DESIGN

~icefox 1 year, 13 days ago*

Asked various people about postfix vs. prefix type composition. Zig and Go both do prefix, so it's maybe worth doing that and making it a trend. Rust does somewhat bonkers prefix+delimited, which is consistent but which I'd like to avoid, though Zesterer had some semi-sane points in its favor. The conclusion seems to just be be "prefix matches how you would speak the type in English" and "anything is better than C's spiral of madness".

I should probably stick with what Go and Zig do, and just write Zig code until it no longer feels like it's backwards.

~icefox 11 months ago

Import syntax keywords! From #52: matklad proposes "promote pub(crate) visibility, it absolutely should be one keyword. I would suggest the following set of visibility: api for "visible outside the current CU", equivalent to Rust's pub with -Dunreachable-pub, pub for "visible throughout CU" (= pub(crate)), and nothing for "visible in current file"."

So basically:

-- private
fn foo() = ... end

-- public to the crate/lib/whatever
pub fn bar() = ... end

-- part of the exposed API
api fn bop() = ... end

I love it. BUT, it takes pub/public which is used for "part of the exposed API" in basically every other language that uses it, and makes it mean something else. I might ponder using pub for "visible outside the current CU" because that's what it means in lots of other langage, and maybe loc (local) or something for pub(crate). But the siren song of api as a keyword is strong, especially with its semantic link to signature files using the .api extension. So that would look like:

-- private
fn foo() = ... end

-- public to the crate/lib/whatever
loc fn bar() = ... end

-- part of the exposed API
pub fn bop() = ... end

Can we split the difference by just not using pub at all anywhere?

-- private
fn foo() = ... end

-- public to the crate/lib/whatever
loc fn bar() = ... end

-- part of the exposed API
api fn bop() = ... end

This seems like a decent local minimum, albeit a little weirdly foreign-looking. Still, worth pondering to see if we can find a better local minimum.

~icefox 11 months ago

Prefix types implemented in https://hg.sr.ht/~icefox/garnet/rev/337fed657920b1f1f08d93dbcc78ef65ad3b05be . Easier than I expected and tbh a little nicer to parse.

~icefox 11 months ago*

Syntax for type parameters passed to functions or declared in functions is fn foo(|T| x T, y I32) and is called foo(|Bool| thing, thing2). I would quite like to get rid of the preceeding | but I tried in commit bb0c04c9db21 and hoo boy does it turn out bad. Our type syntax is actually quite close to our expression syntax, with Foo(thing) being a function call or a type constructor and [4]Bool being a single-element array followed by an ident or an array of 4 bools. In contrast, having || as delimiters is literally trivial. So if some parser genius comes by and figures out how we can call it foo(Bool | thing, thing) then I'd be a fan, but until then I will stick to my "do absolutely nothing fancy" guns and just put up with the slightly cluttery syntax.

This relates to the "syntax for type parameters, say Vec(T) vs Vec[T]" bullet-point as well, since that's part of what makes this hard. I want to keep Vec(T) for now but you could imagine it being Vec|T| if you try hard enough. Don't especially want it right now.

~icefox 11 months ago

We now have syntax-significant newlines! See #48 for details and #38 for motivation.

~icefox referenced this from #35 10 months ago

~icefox 10 months ago

Added a Rust-y implicit function return type of unit in commit b081842f4b88, so fn foo() {} = ... end can now just be fn foo() = ... end. I've been thinking about it for long enough that it's probably a good idea.

Thonk for closure syntax: Haskell-y \a, b -> c = ... end? The -> and lack of parens is foreign. \(a I32, b F32) c = ... end maybe? That'd basically allow \ or lambda maybe to be a synonym for fn. Allow the types of the params to be inferred? This is super convenient but it is also a gigantic pain in the ass to actually do, and is occasionally somewhat error-prone. Allow the end to be elided if it's only one expr?

Not sure I have any good solutions there.

~akavel 7 months ago*

Re: api/loc: FWIW IIUC in Go, the loc kinda got called internal. This sounds reasonable to me, in that it marks an entity as "internal to the module/package/library/crate/whatever".

~icefox 7 months ago

Hmmm, internal is reasonable, though a little long for something that's going to be written frequently. And the obvious shorthand of int is... a bit too ambiguous.

I'm probably just being too hard to please.

~icefox referenced this from #44 7 months ago

~icefox 4 months ago*

We need a closure syntax more and more and https://brevzin.github.io/c++/2020/06/18/lambda-lambda-lambda/ is a bit of a nice case study. Looking at it, I still for some reason like Haskell's \a, b -> ... or something along those lines, of all things. And we don't need an end at the end of it either, we just do the Rust-ish thing and make it take one expression that can be a do ... end block; this is a case where consistency should bow to convenience.

Here's a thonk about implicits, which always scare me: what if calling a function with implicit args used different syntax than without? So say you have:

fn foo(implicit x Thing, y Bool) = ...

let my_thing = Thing ...
foo(my_thing, true)   -- No implicit args passed
\foo(true)            -- equivalent to above call

The goal being that the caller (or really the reader) knows whether or not there's implicit arguments being given to the function.

Another take on it might be annotating the values that are allowed to be used for implicits. (thanks thegreatcatadorer). So the above code would instead be:

implicit my_thing = Thing ...
let other_thing = Thing ...
foo(my_thing, true)   -- No implicit args passed
foo(true)             -- calls foo(my_thing, true), instead of failing with a resolution error.

~icefox 2 months ago

Hmmm, so for lambdas, what if we had fn(...) require a block but \... be for a single expression? So you have:

fn(x I32) I32 = x end
\(x I32) I32 = x

The = might be ambiguous or confusing if you have something like:

let mut y = thing
let set_y = \(x I32) I32 = y = x

though that's a little insane anyway, but one might consider -> instead a la

let mut y = thing
let set_y = \(x I32) I32 -> y = x

which is a nice nod to the ML/Haskell-y origin as well. I'm not sure whether I would want to elide the () and just have:

let set_y = \x I32, other F32 I32 -> y = x

but instead maybe add a delimiter?

let set_y = \x I32, other F32: I32 -> y = x

That starts feeling un-Garnet-ish though, functions are always () and types are always val Type instead of val: Type as it used to be. Maybe do something like Rust closures and infer type params:

let set_y = \x, other -> y = x

but that also kinda feels un-Garnet-ish.

So for now let's keep it as:

let set_y = \(x I32, other F32) I32 -> y = x

and maybe remain open to trimming down type annotations and punctuation further in the future.

~icefox 9 days ago*

Right now type parameters are written as Foo(T1, T2), which gets rather visually cluttered to me when you end up writing lots of them. Can we make them be Foo[T1, T2] without being ambiguous with []T and [3]T for arrays?

Let's see:

  • []Foo[T]
  • [3]Foo[T]
  • Foo[[]T]
  • Foo[Bar[[3]^Something[T]]]

You know... I actually think that works.

Edit: Implemented this, nothing has exploded yet. Straw poll on /r/pldev discord suggests most people like it.

Register here or Log in to comment, or comment via email.