Skip to content

Alternate Export-by-Value Semantics#179

Open
MagmaBurnsV wants to merge 3 commits intoluau-lang:masterfrom
MagmaBurnsV:Alternative-Exports-RFC
Open

Alternate Export-by-Value Semantics#179
MagmaBurnsV wants to merge 3 commits intoluau-lang:masterfrom
MagmaBurnsV:Alternative-Exports-RFC

Conversation

@MagmaBurnsV
Copy link
Contributor

@MagmaBurnsV MagmaBurnsV commented Feb 25, 2026

This proposes alternative semantics for #42, which treats exports as syntax sugar for assigning keys to an export table before sealing.

This allows reassigning exported values before the module is returned, solving the original RFC's issues with mutually dependent functions and shorthand aliasing. Furthermore, it introduces export semantics that are already representable in the language, dismissing the need for introducing const-ness to the language.

Rendered

@MagmaBurnsV MagmaBurnsV marked this pull request as ready for review February 25, 2026 01:08
@Cooldude2606
Copy link

The following cases are currently unclear in their behaviour, root causing being a lack of definition for the interaction of export statements with existing variables. I.e. you currently only explain that two exports cannot share an identifier.

-- This is mentioned in alternatives, but not in body
local function foo() end
export foo -- syntax error?

local bar = 0
export bar = 1 -- syntax error?
print(bar) -- 0 or 1?

fruit = "apple" -- global
export fruit -- shadowing or reference?
fruit = "pear" -- is fruit now local?
print(fruit) -- pear?
print(_G.fruit) -- apple?

export animal
animal = "dog"
local animal = "cat" -- syntax error?
print(animal) -- cat or dog?

@MagmaBurnsV
Copy link
Contributor Author

MagmaBurnsV commented Feb 26, 2026

I've clarified that the restriction only applies to exported variables, and exports follow conventional shadowing rules in regard to non-exported variables, so your example would desugar into:

local _EXP = {}

local function foo() end
_EXP.foo = nil

local bar = 0
_EXP.bar = 1
print(_EXP.bar) -- 1

fruit = "apple"
_EXP.fruit = nil
_EXP.fruit = "pear"
print(_EXP.fruit) -- pear
print(_G.fruit) -- apple

_EXP.animal = nil
_EXP.animal = "dog"
local animal = "cat"
print(animal) -- cat

-- {foo = nil, bar = 1, fruit = "pear", animal = "dog"}
return table.freeze(_EXP)

Or alternatively if optimized into locals by the compiler:

local function foo() end
local foo

local bar = 0
local bar = 1
print(bar)

fruit = "apple"
local fruit
fruit = "pear"
print(fruit)
print(_G.fruit)

local _animal = nil
_animal = "dog"
local animal = "cat"
print(animal)

return table.freeze({foo = foo, bar = bar, fruit = fruit, animal = _animal})

(Uninitialized exports looking like shorthand exports may be a good reason not to support them. The only reason I allowed them in the design was to obtain parity with how the local keyword works, but I am fine with removing them if that's what the team wants.)

@Cooldude2606
Copy link

Cooldude2606 commented Feb 26, 2026

Uninitialized exports looking like shorthand exports may be a good reason not to support them.

While this is true, it should be really easy to detect most cases through linting because export follows existing shadowing rules. In fact the same lint would catch another common error which could result from mistypes or copy paste errors.

I would propose "Uninitiated export is never assigned to" as a lint rule.
This also mirrors common lint rules such as unussed local variable, or variable is never accessed.

local function foo() end
export foo -- Uninitiated export is never assigned to
-- The above also catches the incorrect assumption of exporting locals
-- A common lint rule "no shadowing" would also catch this error

export bar -- No lint because bar is used in an assignment
if condition then
    bar = function() end
end

export f, g -- No lint because f and g is used in an assignment
function f() g() end
function g() f() end

export user -- Uninitiated export is never assigned to
User = {

}

export fruit = nil -- Uninitiated export is never assigned to
-- Although assigned nil explictly, there would never be a reason to export nil

Lint rules are not necessarily part of the langauge, but the possibilty of it being easily lintable helps to remove confussion for users and maintains feature parity with the local keyword as you described.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants