edubart / nelua-lang

Minimal, efficient, statically-typed and meta-programmable systems programming language heavily inspired by Lua, which compiles to C and native code.
https://nelua.io
MIT License
1.99k stars 64 forks source link

Preprocessor can't generate switch cases #235

Closed jrfondren closed 10 months ago

jrfondren commented 10 months ago

I get syntax errors when trying to generate the cases of a switch statement, when very similar code can emit other conditionals and even parts of other conditionals.

Code example

local Weekdays = @enum{
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

-- syntax error: expected `case` keyword in `switch` statement
function Weekdays.show(wd: Weekdays): string
  switch wd
  ## for _, field in ipairs(Weekdays.value.fields) do
    case Weekdays.#|field.name|# then return #['Weekdays.' .. field.name]#
  ## end
  end
  assert(false, 'invalid weekday')
  return ''
end

-- this is fine
function Weekdays.show(wd: Weekdays): string
  ## for _, field in ipairs(Weekdays.value.fields) do
    if wd == Weekdays.#|field.name|# then return #['Weekdays.' .. field.name]# end
  ## end
  assert(false, 'invalid weekday')
  return ''
end

-- this is fine too:
if false then
  print 'a'
## if true then
  else print 'b'
## end
end

Environment

x86_64 linux Nelua 0.2.0-dev Build number: 1588 Git date: 2023-09-16 16:20:44 -0300 Git hash: 596fcca5c77932da8a07c249de59a9dff3099495 Semantic version: 0.2.0-dev.1588+596fcca5 Copyright (C) 2019-2022 Eduardo Bart (https://nelua.io/)

stefanos82 commented 10 months ago

At first, I thought it was the missing do from switch wd, but it seems switch is not supported with preprocessor's parsing yet.

edubart commented 10 months ago

Nelua processor is very different from other programming languages preprocessors in a unique way, it does not work at syntax and token level, but at AST (abstract syntax tree) level and with type inference. This has unique advantages like being able to introspect AST nodes and variables while in the preprocessor context, not many languages can do this. But there are drawbacks, like you cannot insert any set of tokens between preprocessor directives. This is specially true for ifelse blocks and switches, you cannot break them apart between preprocessor directives, the code between the directives must be a complete and valid AST node.

So your last example, is not fine:

if false then
  print 'a'
  ## if true then
else
   print 'b'
   ## end
end

This is how the compiler understands, luckily it will compile, but it was not doing what you expected.

Given this limitation, you have two choices, one is to use multiple if conditions like you suggested, and it's fine to do this, because switches are converted to ifs most of the time anyway when compiling. The second, if you really want to emit a switch, is to generate AST by hand, like the following example:

local Weekdays = @enum{
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

-- way 1
function Weekdays.show1(wd: Weekdays): string
  ##[[
  local cases = {}
  for _, field in ipairs(Weekdays.value.fields) do
    table.insert(cases, {aster.Number{tostring(field.value)}})
    table.insert(cases, aster.Block{aster.Return{aster.String{'Weekdays.'..field.name}}})
  end
  inject_statement(aster.Switch{aster.Id{'wd'}, cases})
  ]]
  assert(false, 'invalid weekday')
  return ''
end

print(Weekdays.Sunday:show1())

But this way you have to manually emit AST nodes by hand, after reading astdefs.lua to understand the AST schema. This can be more difficult but also more powerful, because you can generate any kind of code.

But what if you want to mix manual AST node creation with usual code, thinking of this, in commit https://github.com/edubart/nelua-lang/commit/cd6986b26dba1ed63b7b5a2cebd108720b9335fd I introduced wrap_statement macro, so there is a new way:

local Weekdays = @enum{
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

-- way 2
function Weekdays.show2(wd: Weekdays): string
  ## local cases = {}
  ## for _, field in ipairs(Weekdays.value.fields) do
    ## table.insert(cases, {aster.Number{tostring(field.value)}})
    ## table.insert(cases, wrap_statement(function()
      -- back to normal code context
      return #['Weekdays.'..field.name]#
    ## end))
  ## end
  ## inject_statement(aster.Switch{aster.Id{'wd'}, cases})
  assert(false, 'invalid weekday')
  return ''
end

print(Weekdays.Saturday:show2())
stefanos82 commented 10 months ago

...so, remind me when are you going to write that Compiler book? /jk :rofl: I honestly wish I had the financial resources to fund you in any project you would like to invest your time and effort, because I learn so many things from you.