pigpigyyy / Yuescript

A Moonscript dialect compiles to Lua.
http://yuescript.org
MIT License
443 stars 38 forks source link

How about move Yue auto generated anonymous function to upvalue with name prefix '__' #162

Open GokuHiki opened 6 months ago

GokuHiki commented 6 months ago

Yue have some nice features/syntax that make it a joy to work with like existence ?, nil coalescing ??, backcalls, destruct vargs... . But it also has big draw back that make it a bad choice for me to writing performance code with it because it auto create a new function every time a function called, this is a big performance issue. So I intent to avoid using it as much as possible.

It is a big shame that I can not use it when it very nice to have. So I intent to make a proposal to fix this issue. We can move yue auto generated anonymous function to a upvalue named with prefix '__' in the same scope with parent function so that it with not generate a new function every time parent called, this generate a more better performance.

In many cases, we could send variables of closures as function parameters and get modified variable as function return results. Well... let take the look at the example below to see what I mean.

buff_strength = (char, item) ->
 item.buffer.strength? char.stats.strength?::ref()

To default Lua:

local buff_strength
buff_strength = function(char, item)
  local _obj_0 = item.buffer.strength
  if _obj_0 ~= nil then
    return _obj_0((function()
      local _obj_1 = char.stats.strength
      if _obj_1 ~= nil then
        return _obj_1:ref()
      end
      return nil
    end)())
  end
  return nil
end

Maybe to Lua without inner function:

local __buff_strength__stub_0 = function(char)
  local _obj_1 = char.stats.strength
  if _obj_1 ~= nil then
    return _obj_1:ref()
  end
  return nil
end
local buff_strength
buff_strength = function(char, item)
  local _obj_0 = item.buffer.strength
  if _obj_0 ~= nil then
    return _obj_0(__buff_strength__stub_0(char))
  end
  return nil
end

\ Another more complex example:

exe_func = (func, env) ->
  ok, ... = try
    debug_env_before(env)
    func(env)
    debug_env_after(env)
  catch ex
    -- only access ex
    error ex
    return ex
  if ok
    return ...
  else
    os.exit(1)

To Lua with poor performance:

local exe_func
exe_func = function(func, env)
  return (function(_arg_0, ...)
    local ok = _arg_0
    if ok then
      return ...
    else
      return os.exit(1)
    end
  end)(xpcall(function()
    debug_env_before(env)
    func(env)
    return debug_env_after(env)
  end, function(ex)
    error(ex)
    return ex
  end))
end

To Lua with better performance:

local __exe_func__stub_0 = function(_arg_0, ...)
  local ok = _arg_0
  if ok then
    return ...
  else
    return os.exit(1)
  end
end
local __exe_func__stub_1 = function(env)
  debug_env_before(env)
  func(env)
  return debug_env_after(env)
end
local __exe_func__stub_2 = function(ex)
  error(ex)
  return ex
end
local exe_func
exe_func = function(func, env)
  return __exe_func__stub_0(xpcall(__exe_func__stub_1, __exe_func__stub_2, env))
end

I may make some mistake in the rush, but I hope you can get the idea. \ Well... this solution is not work with all cases but aleast it will save a lot of performance in some cases. And I think it is worth to try. Thanks and regards.

pigpigyyy commented 6 months ago

It is a brilliant idea! And it seems the TypescriptToLua compiler is doing similar optimizations too. Just tried implementing this feature. And I encountered a few more issues during coding.

  1. Since we can alter the environment table for a function block to access different global variables, we have to pass those accessed global variables to the upvalue functions. For example:

    f = ->
    func if cond
    print 123
    true
    else
    false

    is generating to:

    local _anon_func_0 = function(cond, print)
    if cond then
    print(123)
    return true
    else
    return false
    end
    end
    local f
    f = function()
    -- passing the accessed global variable "print" from the call site
    return func(_anon_func_0(cond, print))
    end
  2. When the formerly generated anonymous function contains codes that are creating new closures that are capturing local variables, we can no longer optimize them out. For example:

    onEvent "start", ->
    -- the "with" expression below that generating anonymous function can be optimized
    gameScene\addChild with ScoreBoard!
    gameScore = 100
    \schedule (deltaTime) ->
      .updateScore gameScore

    compiles to:

    local _anon_func_0 = function(ScoreBoard)
    local _with_0 = ScoreBoard()
    local gameScore = 100
    _with_0:schedule(function(deltaTime)
    return _with_0.updateScore(gameScore)
    end)
    return _with_0
    end
    onEvent("start", function()
    return gameScene:addChild(_anon_func_0(ScoreBoard))
    end)

    But in another case:

    onEvent "start", ->
    gameScore = 100
    -- the "with" expression below can not be optimized due to capturing the upvalue "gameScore"
    gameScene\addChild with ScoreBoard!
    \schedule (deltaTime) ->
      .updateScore gameScore

    compiles to:

    onEvent("start", function()
    local gameScore = 100
    return gameScene:addChild((function()
    local _with_0 = ScoreBoard()
    _with_0:schedule(function(deltaTime)
      return _with_0.updateScore(gameScore)
    end)
    return _with_0
    end)())
    end)
  3. The try expression case can only be compiled this way:

    -- the case to optimize
    exe_func = (func, env) ->
    ok, ... = try
    debug_env_before(env)
    func(env)
    debug_env_after(env)
    catch ex
    -- accessed both 'ex' and 'error'
    error ex
    return ex
    if ok
    return ...
    else
    os.exit(1)

    compiles to:

    
    local _anon_func_0 = function(os, _arg_0, ...)
    do
    local ok = _arg_0
    if ok then
      return ...
    else
      return os.exit(1)
    end
    end
    end
    local _anon_func_1 = function(debug_env_after, debug_env_before, env, func)
    do
    debug_env_before(env)
    func(env)
    return debug_env_after(env)
    end
    end
    local exe_func
    exe_func = function(func, env)
    -- get no way to pass the global variable 'error'
    -- so we have to keep this anonymous callback function below
    return _anon_func_0(os, xpcall(_anon_func_1, function(ex)
    error(ex)
    return ex
    end, debug_env_after, debug_env_before, env, func))
    end
pigpigyyy commented 6 months ago

The test cases can be found here. https://github.com/pigpigyyy/Yuescript/blob/main/spec/inputs/upvalue_func.yue https://github.com/pigpigyyy/Yuescript/blob/main/spec/outputs/upvalue_func.lua

GokuHiki commented 6 months ago

Yes! There is the case that Yue does not call the function itself, but if function require access and modify the closure, then we can not do anything about it. Uhm... except in a very hacky way, by passing the variable into upper scope inside a holder-weak table. Well... haha, this has a lot of limitations, a very bad practice and, of course, is not acceptable!