Skip to content

Local Table Destructuring - #223

Open
xHeptc wants to merge 5 commits into
luau-lang:masterfrom
xHeptc:master
Open

Local Table Destructuring#223
xHeptc wants to merge 5 commits into
luau-lang:masterfrom
xHeptc:master

Conversation

@xHeptc

@xHeptc xHeptc commented Jul 11, 2026

Copy link
Copy Markdown

rendered

Note: I am fully aware of RFC 'Granural Imports', 'Table Destructing Syntax' proposals made by other people.
My RFC proposes simpler syntax, doesn't require new keywords, and is not limited to top-level scope with the usage of require().

@jackhexed

Copy link
Copy Markdown
Contributor

const { foo } is valid syntax today, and while it is possible to parse, its really annoying.

@xHeptc

xHeptc commented Jul 11, 2026

Copy link
Copy Markdown
Author

const { foo } is valid syntax today, and while it is possible to parse, its really annoying.

Is this syntax reserved for future?

@deviaze

deviaze commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Could you put some simpler examples for clarity, and include notes on how this works with array-likes, classes, and extern types on the RHS?

@jackhexed

Copy link
Copy Markdown
Contributor

const { foo } is valid syntax today, and while it is possible to parse, its really annoying.

Is this syntax reserved for future?

No, its a function call.

@deviaze

deviaze commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

const { foo } is valid syntax today, and while it is possible to parse, its really annoying.

Is this syntax reserved for future?

This is already valid syntax for a table call on a function named const

const const = function(t: { s: string })
    print(t.s)
    -- do smth with t
end

const { s = "meow" } -- this is a function call

edit: jack hit post first and don't trust the syntax highlighter (it's wrong)

@xHeptc

xHeptc commented Jul 13, 2026

Copy link
Copy Markdown
Author

const { foo } is valid syntax today, and while it is possible to parse, its really annoying.

Is this syntax reserved for future?

This is already valid syntax for a table call on a function named const

const const = function(t: { s: string })
    print(t.s)
    -- do smth with t
end

const { s = "meow" } -- this is a function call

edit: jack hit post first and don't trust the syntax highlighter (it's wrong)

What about a special character indicating destructuring?

local @{ print, warn } = log
const @{ print, warn } = log

or, if this is not optimal, restricting this destructuring syntax to local declarations (exclude constants)

@Bottersnike

Bottersnike commented Jul 13, 2026

Copy link
Copy Markdown

Optimal way to distinguish destructuring syntax from function call now is to peek after } token and look for = token.

This isn't possible as outlined, at least not trivially. We could consider const {foo=bar} as the simplest case where looking for a = token has failed us. Parsing decisions would need to be made then later decide what it was that was actually parsed.

@xHeptc

xHeptc commented Jul 15, 2026

Copy link
Copy Markdown
Author

Optimal way to distinguish destructuring syntax from function call now is to peek after } token and look for = token.

This isn't possible as outlined, at least not trivially. We could consider const {foo=bar} as the simplest case where looking for a = token has failed us. Parsing decisions would need to be made then later decide what it was that was actually parsed.

Got it. I've been brainstorming for some time and got two somewhat solutions.

  1. Fully restrict destructuring to local declarations, excluding const

  2. Currently, the order of parsing is parsePrimaryExpr and then parseStat, which is perfectly normal, what I propose is: pre-check in parseStat, before calling parsePrimaryExpr, when lexer.current() is Name with text const and lexer.lookahead() is {, save a lexer checkpoint, do the scan to the matching }, peek for =, then either restore and parse as a pattern or restore and fall through to the normal parsePrimaryExpr path.

First one is much simpler, but you lose the chance of having restrictions against rebinding your keys.

And the second one is much more complex to implement, even with type annotations,

@0x57e11a

0x57e11a commented Aug 1, 2026

Copy link
Copy Markdown
  1. Currently, the order of parsing is parsePrimaryExpr and then parseStat, which is perfectly normal, what I propose is: pre-check in parseStat, before calling parsePrimaryExpr, when lexer.current() is Name with text const and lexer.lookahead() is {, save a lexer checkpoint, do the scan to the matching }, peek for =, then either restore and parse as a pattern or restore and fall through to the normal parsePrimaryExpr path.

you'll probably want to peek for , in addition to =, to handle the case of const { foo = bar }, baz = a, b, avoiding it being parsed as a primary expr (function call) followed by a syntax error

@xHeptc

xHeptc commented Aug 6, 2026

Copy link
Copy Markdown
Author

Could you put some simpler examples for clarity, and include notes on how this works with array-likes, classes, and extern types on the RHS?

Sorry for the very late reply.

Basic examples:

local { x, y } = Point

--// equivelent to
local _temp1 = Point
local x = _temp1.x
local y = _temp1.y
local player, { health, mana } = GetPlayer(), GetStats()
--// equivelent to
local player = GetPlayer()
local _temp1= GetStats()
local health = _temp1.health
local mana = _temp1.mana

Array-like

--// not supported
local { 1, 2 } = list

--// not supported
local { first = 1 } = list

--// if users want array unpacking, they should continue using:
local first, second = list[1], list[2]
-- or table.unpack

Classes

--// NOTE: I havent used classes yet, so I can only give few examples

--// basic class instance destructuring is same as the basic example

--// assuming you have a class
class Point
    public x: number
    public y: number

    function length(self)
 
    end

    function fromAxisLength(...)

    end
end

--// there are three possibilities

--// option 1: 
--// if "obj.field" is valid, then { field } is valid too.

--// option 2: only fields
--// only allow declared fields ("public x", "public y"), rejecting methods.

--// option 3: public members
--// allow both public fields and public methods, but reject private ones once private members exist.

--// this is probably what would naturally happen anyway if the language already prevents accessing private members via ----// obj.member

Extern types

type Config = {
    enabled: boolean,
    timeout: number,
}

local config: Config = GetConfig()

--// the destructured variables inherit the field types
local { enabled, timeout } = config
-- enabled -- boolean
-- timeout -- number

@xHeptc

xHeptc commented Aug 6, 2026

Copy link
Copy Markdown
Author

although for array destructuring, I might edit the RFC and propose that too with syntax similar to JS [] would be for arrays, {} for objects, or at worse case scenario [ ] for objects, and arrays not supported, finally removing ambiguity between sugar function calls", but I need opinions here guys :)

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.

5 participants