The Crystal Programming Language Forum

Proposal: New "ends" keyword

I’m posting this first in the forum to see response before posting as a formal issue.

Proposal
This is a proposal to introduce a new keyword ends (or endall) as a terminal point to resolve the end of nested loops|conditionals.

Why
It’s a common code occurrence to have multiple levels of loops and/or conditionals, which require separate end keywords to designate their termination points. The end statements themselves are merely for syntactic purposes.

It would be a benefit to programmers, and code readers, to be able to produce|read more concise code, by reducing the code noise of these nested multiple end keywords with a shorter|cleaner syntax.

Thus, I propose creating the keyword ends as a shorter|cleaner syntax to replace having to write multiple end keywords.

Example

Below is an example of real code which performs nested loops. With “standard” format it looks like this.

def render(scene, image, screenWidth, screenHeight)
  screenHeight.times do |y|
    screenWidth.times do |x|
      color = self.traceRay(....)
      r, g, b = Color.toDrawingColor(color)
      image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    end 
  end 
end

However, from the point of view of the parser, these are all legal|equivalent.

def render(scene, image, screenWidth, screenHeight)
  screenHeight.times do |y|
    screenWidth.times do |x|
      color = self.traceRay(....)
      r, g, b = Color.toDrawingColor(color)
      image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    end     end         end     end end end
  end         end       end
end             end     end

This proposal would allow this type of code to be written simply as:

def render(scene, image, screenWidth, screenHeight)
  screenHeight.times do |y|
    screenWidth.times do |x|
      color = self.traceRay(....)
      r, g, b = Color.toDrawingColor(color)
      image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
ends

Pros

  1. code conciseness
  2. better readability
  3. no whitespace dependencies
  4. no conflict with legacy code

Cons
No technical implementation restrictions I can think of.
Maybe alternative name (endall)?

Thanks for consideration.

Why? The formatter should enforce the placement of the end keyword and I imagine it would be harder to parse given it’s not as simple as like skip_whitespace_or_newline anymore.

2 Likes

Also, I don’t think it would be as useful as it seems. Consider the following code:

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
        end
      end
    end

    def do_something_else
      . . .
    end
  end
end

Where does ends go? I’d want it to go at the end of render, but that would end the class and module, too, right?

Jabari: are you using an editor with an integrated formatter? If not, please do! I’m sure you’ll love it

I think at this point I vaguely remember disliking begin / end instead of { / } like in many other languages when I just started to learn Ruby. This was one of the small annoyances for me back then with otherwise great, even life changing experience with Ruby and it’s syntax.

Funny thing it stopped to annoy me very soon. The brain just filters it out very easily as just a structure of the code not related to the “real” code itself. I guess brain is really good at noticing what’s important and filtering out “unrelated” stuff.

In fact now when my eyesight is not so good anymore having end instead of } is helpful for me in terms of being easy to spot the code structure.

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    ends

    def do_something_else
      . . .
    end
ends

Here is another real example.

def sozpg(val, res_0)
  # Compute the primes r0..sqrt(input_num) and store in 'primes' array.
  # Any algorithm (fast|small) is usable. Here the SoZ for P5 is used.
  md, rscnt = 30u64, 8              # P5's modulus and residues count
  res  = [7,11,13,17,19,23,29,31]   # P5's residues
  posn = [0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,3,0,4,0,0,0,5,0,0,0,0,0,6,0,7]

  kmax = (val - 7) // md + 1        # number of resgroups upto input value
  prms = Array(UInt8).new(kmax, 0)  # byte array of prime candidates, init '0'
  sqrt_n = Math.sqrt(val).to_u32    # compute integer sqrt of val
  modk, r, k = 0, -1, 0             # initialize residue parameters

  while true                        # for r0..sqrtN primes mark their multiples
    if (r += 1) == rscnt; r = 0; modk += md; k += 1 end
    next if (prms[k] & (1 << r)) != 0 # skip pc if not prime
    prm_r = res[r]                  # if prime save its residue value
    prime = modk + prm_r            # numerate the prime value
    break if prime > sqrt_n         # we're finished when it's > sqrtN
    res.each do |ri|                # mark prime's multiples in prms
      prod = prm_r * ri - 2         # compute cross-product for prm_r|ri pair
      bit_r = 1 << posn[prod % md]         # bit mask for prod's residue
      kpm = k * (prime + ri) + prod // md  # 1st resgroup for prime mult
      while kpm < kmax; prms[kpm] |= bit_r; kpm += prime end
  ends # end end
  # prms now contains the nonprime positions for the prime candidates r0..N
  # extract primes into ssoz var 'primes'
  primes = [] of UInt64             # create empty dynamic array for primes
  kmax.times do |k|                 # for each resgroup
    rscnt.times do |r|              # numerate|store primes from pcs list
      if (prms[k] & (1 << r)) == 0  # if bit location a prime
        prime = md * k + res[r]     # numerate its value, store if in range
        primes << prime if (res_0..val).includes? prime
  ends # end end end
  primes
end

This is my point: how do you (and the compiler) know which end statements an ends represents? In your version of my example, you used ends to complete a method and all of its internal blocks. But then you use it to complete a class and its enclosing module. How does the compiler know that the first ends doesn’t end the class and the module as well? Are there "ends boundaries" that Crystal users would need to learn?

Would

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
        end
      end
    end
  end
end

become

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    ends
ends

?

The only two workable approaches I see to implementing ends would be

  1. ends ends all contexts (module, class, method, block, etc.) completely; this is easy to understand but not that useful
  2. ends has specific boundaries, so maybe it will complete all blocks and the method they’re in but stop there; this is potentially more useful but is confusing

Do you have a good way to make the semantics both useful and relatively unconfusing?

The proposal is for ends to match the beginning scope according to indentation.

The language has no concept of indentation, so there’s no way we are going to do something like this.

If you use a code formatter integrated with the editor this is not a problem at all. In fact, is the issue just that there are too many “superfluous” ends?

It could even become

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
ends

This would have nothing to do with the compiler. All that is happening is the parser is going down the AST tree (or whatever you call it) and resolving all the open .. end, statements at that point in the code, which we now tell the parser to do with explicit end statements for each case. The parser then produces the same resolved code, which the compiler then uses to process.

It should be actually simpler|easier to do than what Pything|Nim does, because they have to account for whitespace, whereas Crystal now, and for the proposal, could care less about whitespace. As long as you provide syntactically legal code it will parse it correctly.

Let’s annotate my original example with the open .. end statements at each line.

module Graphics # module
  class Renderer # module, class
    def render(scene, image, screenWidth, screenHeight) # module, class, def
      screenHeight.times do |y| # module, class, def, times y block
        screenWidth.times do |x| # module, class, def, times y block, times x block
          color = self.traceRay(....) # module, class, def, times y block, times x block
          r, g, b = Color.toDrawingColor(color) # module, class, def, times y block, times x block
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b)) # module, class, def, times y block, times x block
        end # module, class, def, times y block, times x block (we're including the block that this end completes)
      end # module, class, def, times y block
    end # module, class, def

    def do_something_else # module, class, def
      . . .
    end # module, class, def
  end # module, class
end # module

If ends resolves all open .. end statements where it’s added, then an ends at any line in the above example will end the module. That means that your two-ends example above is inconsistent with the example you gave in the post I’m replying to. Either it ends all contexts that could require an end, or it doesn’t.

No, no, no.
I think you’re making this harder than it is.

You have to start parsing from the instance of the most inner condition. This is exactly what the parser does now. You have to know which termination condition is open and needs to be resolved as you parse backwards. As you resolve the inner conditions, then you back out to the outer layers.

I’m open to you being right about me making this harder than it has to be. In the spirit of trying to understand, here’s where I’m getting tripped up by this ends thing.

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    ends # <----- This line

    def do_something_else
      . . .
    end
ends

I understand that the parser, at that point, will need to sort of “unroll” the contexts it’s in (I don’t know if this is implemented as a stack, but it reminds me of going up and down the call stack with method calls and then returns). What I don’t understand is how, in the process of that unrolling, it knows to stop unrolling once it gets to the class context. In particular, I don’t know how the parser is supposed to know, when it gets to that line, the difference between that code above and

module Graphics
  class Renderer
    def render(scene, image, screenWidth, screenHeight)
      screenHeight.times do |y|
        screenWidth.times do |x|
          color = self.traceRay(....)
          r, g, b = Color.toDrawingColor(color)
          image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
ends

The compiler needs to be able to know (in the first of my two code examples) whether that next method is in the class or not. It doesn’t know about the indentation, and as far as I’m aware it doesn’t know about the ends after that other method. I just don’t see how it has the context to know that it’s still in the class. Even worse, how does the compiler (and how do I) know whether that second method is in the class or the module?

Please, stop say compiler. My proposal takes place at the source code parsing stage, before its put into the format for compilation.

So in your code example, starting at the beginning (outer most layer) the parser starts counting how many things (module|class|method names, loops, conditionals, etc) are currently open (haven’t been resolved as terminated). At some point, it counts the last thing that needs to be resolved. When it encounters the first end it tries to resolve it with the last (highest count) thing that’s still open. It then continues backing up the tree count, until all the unresolved things count is zero. Bada bing!.

So now the source code AST is fully resolved, and w|should look just like it does now. This is what is then feed to the compiler (the devs can provide the details on how its actually done). But there has to be a way to know how many things need to be terminated and how many are still open.

I would assume for Crystal, the parsing stage is separate from compilation, as it is in Ruby, and probably every other language. You have to go through some process to turn raw source code, through whatever number of stages of processing, before it’s fed to a compiler (gcc, clang, llvm, etc) to produce machine code.

@jzakiya Could you explain what this ends end?

  class Foo
           def foo
while true
                    puts 4
    ends
                    end

Or how does the compiler figure out how to solve this?

Here’s how it would be parsed.

 class Foo
   def foo
     while true
       puts 4
   ends
 end

Here’s how it would be expanded into standard format.

 class Foo
   def foo
     while true
       puts 4
     end
   end
 end

I think the biggest thing here is that if you’ve made a mistake in how you’ve nested your code, this wouldn’t be able to catch it.

def call
  if x

  if y
  end
ends

In this example, you don’t know if I meant the if statements to be in the same level scope, or if I meant to have if y nested inside of the if x scope. I believe this would eventually lead to worse code as people could use it to be “lazy”, and just start skipping some thing.

4 Likes

People make mistakes now. That’s why you write a compiler to generate good error messages. :blush:

Actually, your example would compile, but just might not be what you intended

def call
  if x
    if y
    end
  end
end

Look, with every new thing comes a learning period, especially if you’re used to the old thing. No one would be required to use this, so if people don’t like it they just won’t use it.

However, I know people coming from Python|Nim, and other whitespace languages, will love this. Once you grok it, it will soon become second nature.

And after time, even some old dogs will begin to learn some new tricks, to make their lives easier. :laughing:

Here’s another challenge. How is this parsed?

class Foo
  def bar
    while true
      ends
        if foo
          while true
            ends

It is this one?

class Foo
  def bar
    while true
  ends

  if foo
    while true
ends

Or this one?

class Foo
  def bar
    while true
    ends

    if foo
      while true
ends

Oww… I like challenges. :blush:

All 3 examples are syntactically equivalent, whose standard format would be:

class Foo
  def bar
    while true
    end
  end

  if foo
    while true
    end
  end
end

If you are ever unsure, first write it in standard format and then reduce it.

People make mistakes now

Very true. I’m definitely one of those :joy:

but just might not be what you intended

That’s exactly my fear. It would compile with the wrong logic, and that might be a lot more difficult to track down. As opposed to just not compiling with the “missing end” keyword. If you were missing an end due to copy/paste issue, but you had ends, my guess is it would compile when it shouldn’t.

people coming from Python|Nim, and other whitespace languages, will love this.

My python use has been super minimal, but I’d be curious to see if others would be open to this sort of thing :thinking: I know there was also a request to just allow for not having the end at all which would appease the whitespace devs. I guess if this really became a consideration, then whitespace would also have to be taken in to consideration. Like, if you used ends, then you’d have to have it lined up like how python and those do it.