DanielXMoore / Civet

A TypeScript superset that favors more types and less typing
https://civet.dev
MIT License
1.33k stars 28 forks source link

Declaration inside unless/until adds declaration after block #1228

Closed edemaine closed 1 month ago

edemaine commented 1 month ago

So I implemented the idea that declarations of unless and until go after the block instead of inside:

unless x := f()
  return
console.log x
↓↓↓
let ref
if ((ref = f()) {
  return
}
const x = ref
console.log(x)

However, I'm no longer sure this is a good idea. For example, what if we have an else block?

unless x := f()
  return
else
  // should x be bound here?
// should x be bound here?

I'm pretty sure x should be bound inside the else, and for symmetry with the other situations, I think also afterward? So my temptation would be to put the declaration of x above the unless instead of after it, so that it's accessible everywhere. It doesn't seem particularly bad to expose it to the "then" clause — this is actually more symmetric with if — what's important is that it's exposed in the "else clause and outside block, because it can be useful there. Whereas if keeps the protection of the binding to the "then" clause, because that's the only place it is helpful. What do you think?

Fixes #1215, fixes #756

STRd6 commented 1 month ago

I think moving the declaration above the block sounds good. Having it available inside the then, else, and following blocks seems to make sense in the case of unless. It might be a little strange but we can try it and see how it goes.

edemaine commented 1 month ago

Thinking about this more: Moving the declaration above the block works well for a simple assignment:

unless x := f()
  return
else
  console.log 'good', x
process(x)
↓↓↓
const x = f()
if (!x) {
  return
} else {
  console.log('good', x)
}
process(x)

But it's trickier for pattern matching assignment:

unless [x, {type}] := f()
  return
else
  console.log 'good', x, type
process x, type
↓↓↓
const ref = f()
const [x, {type}] = ref
if (!(Array.isArray(ref) && ref.length === 2 && typeof ref[1] === "object" && ref[1] != null && "type" in ref[1])) {
  return
} else {
  console.log('good', x, type)
}
process(x, type)

We can't do the destructuring until after the if check. Then it's difficult to do it (especially with const) for both the else block and after the else block. Even worse, it doesn't really make to do the destructuring declaration after the else block, because in the then case (unless the then clause has a guaranteed exit) there's no meaning for x and type. I see now that #1215 mentions this about Swift guards:

  1. If the condition creates any bindings (such as guard x? := getX() else { STATEMENTS }), the bindings are only available outside the else block.
  2. STATEMENTS must be an "exit" of some sort, in order for 1 to be possible.

This is a little more conditional than I had in mind. Here's one possible behavior:

I think this makes some sense: the declaration goes in the place(s) where it makes sense. But wouldn't hurt to get more input on this before I go ahead and implement it.

By the way, until behaves very differently: there, we always want the declaration after the loop (just like currently implemented). Hmm, but if there's a break, this won't work... So I guess we need to fail in that case?

edemaine commented 1 month ago

I implemented the above plan, and fixed the missing negation of pattern matching mentioned in https://github.com/DanielXMoore/Civet/issues/756#issuecomment-2097279047 This should be ready to go!