Skip to content

Commit a0cebf0

Browse files
committed
fix: support Lua 5.5 prefix local attributes
1 parent 9d320a5 commit a0cebf0

File tree

6 files changed

+212
-48
lines changed

6 files changed

+212
-48
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<!-- Add all new changes here. They will be moved under a version at release -->
55
* `CHG` Modified the `ResolveRequire` function to pass the source URI as a third argument.
66
* `CHG` Improved the output of test failures during development
7+
* `FIX` Support Lua 5.5 prefix local attributes such as `local <close>x` and `local <const>x`
78

89
## 3.17.1
910
`2026-01-20`

script/parser/compile.lua

Lines changed: 113 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,8 @@ local function expectAssign(isAction)
652652
return false
653653
end
654654

655-
local function parseLocalAttrs()
655+
---@param kind? '"prefix"'|'"suffix"'
656+
local function parseLocalAttrs(kind)
656657
local attrs
657658
while true do
658659
skipSpace()
@@ -672,6 +673,7 @@ local function parseLocalAttrs()
672673
parent = attrs,
673674
start = getPosition(Tokens[Index], 'left'),
674675
finish = getPosition(Tokens[Index], 'right'),
676+
kind = kind or 'suffix',
675677
}
676678
attrs[#attrs+1] = attr
677679
Index = Index + 2
@@ -725,6 +727,20 @@ local function parseLocalAttrs()
725727
return attrs
726728
end
727729

730+
local function mergeLocalAttrs(attrsBefore, attrsAfter)
731+
if not attrsBefore then
732+
return attrsAfter
733+
end
734+
if not attrsAfter then
735+
return attrsBefore
736+
end
737+
for i = 1, #attrsAfter do
738+
attrsBefore[#attrsBefore + 1] = attrsAfter[i]
739+
end
740+
attrsBefore.finish = attrsAfter[#attrsAfter].finish
741+
return attrsBefore
742+
end
743+
728744
---@param obj table
729745
local function createLocal(obj, attrs)
730746
obj.type = 'local'
@@ -951,6 +967,79 @@ local function hasAttr(attrs, attrName)
951967
return false
952968
end
953969

970+
local function hasAttrKind(attrs, attrName, attrKind)
971+
if not attrs then
972+
return false
973+
end
974+
for i = 1, #attrs do
975+
if attrs[i][1] == attrName and attrs[i].kind == attrKind then
976+
return true
977+
end
978+
end
979+
return false
980+
end
981+
982+
local function checkLocalCloseList(n1, n2, nrest)
983+
if State.version ~= 'Lua 5.4' and State.version ~= 'Lua 5.5' then
984+
return
985+
end
986+
987+
if State.version == 'Lua 5.5' and hasAttrKind(n1 and n1.attrs, 'close', 'prefix') then
988+
local extra
989+
if n2 and not n2.attrs then
990+
extra = n2
991+
elseif nrest then
992+
for i = 1, #nrest do
993+
if not nrest[i].attrs then
994+
extra = nrest[i]
995+
break
996+
end
997+
end
998+
end
999+
if extra then
1000+
pushError {
1001+
type = 'MULTI_CLOSE',
1002+
start = extra.start,
1003+
finish = extra.finish,
1004+
}
1005+
return
1006+
end
1007+
end
1008+
1009+
local function collectCloseAttrs(node, list)
1010+
local attrs = node and node.attrs
1011+
if not attrs then
1012+
return
1013+
end
1014+
for i = 1, #attrs do
1015+
local a = attrs[i]
1016+
if a[1] == 'close' then
1017+
list[#list + 1] = a
1018+
end
1019+
end
1020+
end
1021+
1022+
local closeList = {}
1023+
collectCloseAttrs(n1, closeList)
1024+
if n2 then
1025+
collectCloseAttrs(n2, closeList)
1026+
end
1027+
if nrest then
1028+
for i = 1, #nrest do
1029+
collectCloseAttrs(nrest[i], closeList)
1030+
end
1031+
end
1032+
1033+
if #closeList > 1 then
1034+
local second = closeList[2]
1035+
pushError {
1036+
type = 'MULTI_CLOSE',
1037+
start = second.start,
1038+
finish = second.finish,
1039+
}
1040+
end
1041+
end
1042+
9541043
local function resolveLable(label, obj)
9551044
if not label.ref then
9561045
label.ref = {}
@@ -3208,43 +3297,8 @@ local function parseMultiVars(n1, parser, isLocal)
32083297
end
32093298
end
32103299

3211-
do
3212-
-- Lua 5.4: only one <close> attribute allowed across a local declaration
3213-
-- Lua 5.5: multiple <close> are allowed
3214-
if State.version == 'Lua 5.4' then
3215-
local function collectCloseAttrs(node, list)
3216-
local attrs = node and node.attrs
3217-
if not attrs then
3218-
return
3219-
end
3220-
for i = 1, #attrs do
3221-
local a = attrs[i]
3222-
if a[1] == 'close' then
3223-
list[#list + 1] = a
3224-
end
3225-
end
3226-
end
3227-
3228-
local closeList = {}
3229-
collectCloseAttrs(n1, closeList)
3230-
if n2 then
3231-
collectCloseAttrs(n2, closeList)
3232-
end
3233-
if nrest then
3234-
for i = 1, #nrest do
3235-
collectCloseAttrs(nrest[i], closeList)
3236-
end
3237-
end
3238-
3239-
if #closeList > 1 then
3240-
local second = closeList[2]
3241-
pushError {
3242-
type = 'MULTI_CLOSE',
3243-
start = second.start,
3244-
finish = second.finish,
3245-
}
3246-
end
3247-
end
3300+
if isLocal then
3301+
checkLocalCloseList(n1, n2, nrest)
32483302
end
32493303

32503304
if v2 and not n2 then
@@ -3343,13 +3397,30 @@ local function parseLocal()
33433397
local locPos = getPosition(Tokens[Index], 'left')
33443398
Index = Index + 2
33453399
skipSpace()
3346-
local word = peekWord()
3400+
local attrsBefore = parseLocalAttrs('prefix')
3401+
if attrsBefore and State.version ~= 'Lua 5.5' then
3402+
pushError {
3403+
type = 'UNSUPPORT_SYMBOL',
3404+
start = attrsBefore.start,
3405+
finish = attrsBefore.finish,
3406+
version = 'Lua 5.5',
3407+
}
3408+
end
3409+
skipSpace()
3410+
local word, wstart, wfinish = peekWord()
33473411
if not word then
33483412
missName()
33493413
return nil
33503414
end
33513415

33523416
if word == 'function' then
3417+
if attrsBefore then
3418+
pushError {
3419+
type = 'MISS_NAME',
3420+
start = wstart,
3421+
finish = wfinish,
3422+
}
3423+
end
33533424
local func = parseFunction('local', true)
33543425
local name = func.name
33553426
if name then
@@ -3373,7 +3444,9 @@ local function parseLocal()
33733444
missName()
33743445
return nil
33753446
end
3376-
local loc = createLocal(name, parseLocalAttrs())
3447+
local attrsAfter = parseLocalAttrs('suffix')
3448+
local attrs = mergeLocalAttrs(attrsBefore, attrsAfter)
3449+
local loc = createLocal(name, attrs)
33773450
loc.locPos = locPos
33783451
loc.effect = maxinteger
33793452
pushActionIntoCurrentChunk(loc)

test/parser_test/ast/Action.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ CHECK'local x <close> <const> = 1'
7979
parent = "<IGNORE>",
8080
[1] = {
8181
type = "localattr",
82+
kind = "suffix",
8283
start = 8,
8384
finish = 15,
8485
parent = "<IGNORE>",
8586
[1] = "close",
8687
},
8788
[2] = {
8889
type = "localattr",
90+
kind = "suffix",
8991
start = 16,
9092
finish = 23,
9193
parent = "<IGNORE>",
@@ -116,6 +118,7 @@ CHECK'local x < const > = 1'
116118
parent = "<IGNORE>",
117119
[1] = {
118120
type = "localattr",
121+
kind = "suffix",
119122
start = 8,
120123
finish = 17,
121124
parent = "<IGNORE>",

test/parser_test/ast/Lua.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ CHECK'local x <const>, y <close> = 1'
521521
parent = "<IGNORE>",
522522
[1] = {
523523
type = "localattr",
524+
kind = "suffix",
524525
start = 8,
525526
finish = 15,
526527
parent = "<IGNORE>",
@@ -542,6 +543,7 @@ CHECK'local x <const>, y <close> = 1'
542543
parent = "<IGNORE>",
543544
[1] = {
544545
type = "localattr",
546+
kind = "suffix",
545547
start = 19,
546548
finish = 26,
547549
parent = "<IGNORE>",

test/parser_test/grammar_check.lua

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,11 +1685,4 @@ x = 0b1<!2!>
16851685
]]
16861686
{
16871687
type = 'MALFORMED_NUMBER',
1688-
}
1689-
1690-
Version = 'Lua 5.5'
1691-
1692-
TEST[[
1693-
local x <close>, y <close> = 1
1694-
]]
1695-
(nil)
1688+
}

test/parser_test/syntax_check.lua

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,98 @@ global <const> x = 1
867867
]]
868868
(nil)
869869

870+
TEST [[
871+
local <close>x = setmetatable({}, { __close = function () end })
872+
]]
873+
(nil)
874+
875+
TEST [[
876+
local <close> x = setmetatable({}, { __close = function () end })
877+
]]
878+
(nil)
879+
880+
Version = 'Lua 5.4'
881+
TEST [[
882+
local <!<close>!>x = setmetatable({}, { __close = function () end })
883+
]]
884+
{
885+
type = 'UNSUPPORT_SYMBOL',
886+
version = 'Lua 5.5',
887+
}
888+
889+
TEST [[
890+
local <!<const>!>x = 1
891+
]]
892+
{
893+
type = 'UNSUPPORT_SYMBOL',
894+
version = 'Lua 5.5',
895+
}
896+
Version = 'Lua 5.5'
897+
898+
TEST [[
899+
local <close>a, <!b!> = setmetatable({}, { __close = function () end }), {}
900+
]]
901+
{
902+
type = 'MULTI_CLOSE',
903+
}
904+
905+
TEST [[
906+
local <close>a, b <!<close>!> = setmetatable({}, { __close = function () end }), setmetatable({}, { __close = function () end })
907+
]]
908+
{
909+
type = 'MULTI_CLOSE',
910+
}
911+
912+
TEST [[
913+
local <close>a, <!<!>close>b = setmetatable({}, { __close = function () end }), setmetatable({}, { __close = function () end })
914+
]]
915+
{
916+
type = 'UNKNOWN_SYMBOL',
917+
multi = 2,
918+
info = {
919+
symbol = '<',
920+
}
921+
}
922+
923+
TEST [[
924+
local <const>x = 1
925+
]]
926+
(nil)
927+
928+
TEST [[
929+
local <const> x = 1
930+
]]
931+
(nil)
932+
933+
TEST [[
934+
local <const>x, y = 1, 2
935+
]]
936+
(nil)
937+
938+
TEST [[
939+
local <close>a, b <const> = setmetatable({}, { __close = function () end }), 1
940+
]]
941+
(nil)
942+
943+
TEST [[
944+
local <!function!> f() end
945+
]]
946+
(nil)
947+
948+
TEST [[
949+
local <close> <!function!> f() end
950+
]]
951+
{
952+
type = 'MISS_NAME',
953+
}
954+
955+
TEST [[
956+
local <const> <!function!> f() end
957+
]]
958+
{
959+
type = 'MISS_NAME',
960+
}
961+
870962
-- Shadowing rules (Lua 5.5): local variables shadow globals within lexical scope
871963

872964
TEST [[

0 commit comments

Comments
 (0)