diff --git a/.luarc.json b/.luarc.json index 5556ee08..b5f87ac9 100644 --- a/.luarc.json +++ b/.luarc.json @@ -20,7 +20,12 @@ "code-after-break": "Warning", "empty-block": "Warning", "trailing-space": "Warning" - } + }, + "ignoredFiles": "Disable" }, - "workspace.checkThirdParty": false + "workspace.checkThirdParty": false, + "workspace.ignoreDir": [ + ".vscode", + "test/test_optional_chain.lua" + ] } diff --git a/3rd/lua-patch/apply_patch.lua b/3rd/lua-patch/apply_patch.lua new file mode 100644 index 00000000..28631105 --- /dev/null +++ b/3rd/lua-patch/apply_patch.lua @@ -0,0 +1,29 @@ +-- 3rd/lua-patch/apply_patch.lua +-- 复制官方 Lua 源码目录到构建目录,并应用可选链补丁(git apply)。 +-- 用法: apply_patch.lua +local src, patch, dst = ... +assert(src and patch and dst, "usage: apply_patch.lua ") + +local is_windows = package.config:sub(1, 1) == "\\" + +-- 清空并重建目标目录 +if is_windows then + os.execute(('if exist "%s" rmdir /s /q "%s"'):format(dst, dst)) + os.execute(('mkdir "%s"'):format(dst)) +else + os.execute(('rm -rf "%s"'):format(dst)) + os.execute(('mkdir -p "%s"'):format(dst)) +end + +-- 复制官方源码目录(保持完整,编译时 include 自包含) +if is_windows then + os.execute(('xcopy /e /i /y /q "%s" "%s"'):format(src, dst)) +else + os.execute(('cp -r "%s/." "%s/"'):format(src, dst)) +end + +-- 应用补丁(patch 内为相对路径,--directory 指定目标目录) +local dst_str = dst:gsub("\\", "/") +local ok = os.execute(('git apply --directory="%s" "%s"'):format(dst_str, patch)) +assert(ok, "git apply failed for " .. patch) +print("apply_patch: OK") diff --git a/3rd/lua-patch/optchain/lua54.patch b/3rd/lua-patch/optchain/lua54.patch new file mode 100644 index 00000000..260d5a89 --- /dev/null +++ b/3rd/lua-patch/optchain/lua54.patch @@ -0,0 +1,252 @@ +diff --git a/lopcodes.c b/lopcodes.c +index c67aa22..0db8fed 100644 +--- a/lopcodes.c ++++ b/lopcodes.c +@@ -100,5 +100,8 @@ LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { + ,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */ + ,opmode(0, 0, 1, 0, 1, iABC) /* OP_VARARGPREP */ + ,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */ ++#if defined(BEE_OPTCHAIN) ++ ,opmode(0, 0, 0, 0, 1, iABC) /* OP_SETTOP */ ++#endif + }; + +diff --git a/lopcodes.h b/lopcodes.h +index 46911ca..4a4c84c 100644 +--- a/lopcodes.h ++++ b/lopcodes.h +@@ -307,10 +307,17 @@ OP_VARARG,/* A C R[A], R[A+1], ..., R[A+C-2] = vararg */ + OP_VARARGPREP,/*A (adjust vararg parameters) */ + + OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */ ++#if defined(BEE_OPTCHAIN) ++,OP_SETTOP/* A B R[A], ..., R[A+B] := nil; top := A+B+1 */ ++#endif + } OpCode; + + ++#if defined(BEE_OPTCHAIN) ++#define NUM_OPCODES ((int)(OP_SETTOP) + 1) ++#else + #define NUM_OPCODES ((int)(OP_EXTRAARG) + 1) ++#endif + + + +diff --git a/lopnames.h b/lopnames.h +index 965cec9..44843b1 100644 +--- a/lopnames.h ++++ b/lopnames.h +@@ -96,6 +96,9 @@ static const char *const opnames[] = { + "VARARG", + "VARARGPREP", + "EXTRAARG", ++#if defined(BEE_OPTCHAIN) ++ "SETTOP", ++#endif + NULL + }; + +diff --git a/lparser.c b/lparser.c +index eed008c..9b90ebd 100644 +--- a/lparser.c ++++ b/lparser.c +@@ -37,6 +37,29 @@ + + #define hasmultret(k) ((k) == VCALL || (k) == VVARARG) + ++#if defined(BEE_OPTCHAIN) ++/* ++** Patch the short-circuit path of an optional-chain call so that it ++** produces 'nresults' nil values instead of a single one. This allows ++** chains ending in a call to yield multiple results (e.g. ++** 'local a, b = f?()'). The short-circuit path is a fixed layout right ++** after the call (see suffixedexp): CALL (with 'k' flag set) / JMP / ++** OP_SETTOP. The OP_SETTOP (which also fixes the stack top; see lvm.c) ++** holds the number of nil slots minus one in its B field. ++*/ ++static void luaK_setreturns_optchain (FuncState *fs, expdesc *e, int nresults) { ++ if (e->k == VCALL) { /* open function call? */ ++ int pc = e->u.info; /* position of the call */ ++ if (TESTARG_k(fs->f->code[pc])) { /* optional-chain call (fixed layout)? */ ++ /* A fixed 'nresults' needs exactly that many nils, while ++ LUA_MULTRET needs exactly one (B stays 0). */ ++ if (nresults != LUA_MULTRET) /* fixed number of results? */ ++ SETARG_B(fs->f->code[pc + 2], nresults - 1); /* widen OP_SETTOP */ ++ } ++ } ++} ++#endif ++ + + /* because all strings are unified by the scanner, the parser + can use pointer equality for string equality */ +@@ -486,6 +509,9 @@ static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) { + int extra = needed + 1; /* discount last expression itself */ + if (extra < 0) + extra = 0; ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, e, extra); ++#endif + luaK_setreturns(fs, e, extra); /* last exp. provides the difference */ + } + else { +@@ -879,6 +905,9 @@ static void closelistfield (FuncState *fs, ConsControl *cc) { + static void lastlistfield (FuncState *fs, ConsControl *cc) { + if (cc->tostore == 0) return; + if (hasmultret(cc->v.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &cc->v, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, LUA_MULTRET); + cc->na--; /* do not count last expression (unknown number of elements) */ +@@ -1035,8 +1064,12 @@ static void funcargs (LexState *ls, expdesc *f) { + args.k = VVOID; + else { + explist(ls, &args); +- if (hasmultret(args.k)) ++ if (hasmultret(args.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &args, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &args); ++ } + } + check_match(ls, ')', '(', line); + break; +@@ -1105,9 +1138,33 @@ static void suffixedexp (LexState *ls, expdesc *v) { + /* suffixedexp -> + primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */ + FuncState *fs = ls->fs; ++#if defined(BEE_OPTCHAIN) ++ int niljumps = NO_JUMP; /* patch list of optional-chain exits (nil) */ ++ int chain_base = fs->freereg; /* result register of the chain */ ++#endif + primaryexp(ls, v); + for (;;) { + switch (ls->t.token) { ++#if defined(BEE_OPTCHAIN) ++ case '?': { /* optional chain: '?.' '?:' '?[' '?(' */ ++ int reg, nilreg; ++ luaX_next(ls); /* consume '?' */ ++ if (ls->t.token != '.' && ls->t.token != ':' && ++ ls->t.token != '[' && ls->t.token != '(') ++ luaX_syntaxerror(ls, "unexpected symbol near '?'"); ++ reg = luaK_exp2anyreg(fs, v); /* evaluate receiver only once */ ++ nilreg = fs->freereg; ++ luaK_nil(fs, nilreg, 1); /* ensure it is nil at runtime */ ++ /* Reserve the temp through luaK_reserveregs (which also grows ++ 'maxstacksize') instead of a raw freereg++ that would bypass ++ luaK_checkstack and leave 'maxstacksize' stale. */ ++ luaK_reserveregs(fs, 1); ++ luaK_codeABCk(fs, OP_EQ, reg, nilreg, 0, 1); /* jump when nil */ ++ luaK_concat(fs, &niljumps, luaK_jump(fs)); ++ fs->freereg--; /* release the nil slot; only used by OP_EQ */ ++ break; /* next iteration handles '.' ':' '[' '(' */ ++ } ++#endif + case '.': { /* fieldsel */ + fieldsel(ls, v); + break; +@@ -1132,7 +1189,50 @@ static void suffixedexp (LexState *ls, expdesc *v) { + funcargs(ls, v); + break; + } +- default: return; ++ default: ++#if defined(BEE_OPTCHAIN) ++ if (niljumps != NO_JUMP) { ++ int skip, nilpc; ++ if (v->k == VCALL) { ++ /* A chain ending in a call can yield multiple results: keep ++ the call open instead of collapsing it to a single value. ++ We emit a fixed layout so that later consumers can patch ++ the short-circuit path without storing extra info in 'e': ++ CALL base ... (with the 'k' flag set, marking the chain) ++ JMP skip ++ OP_SETTOP base 0 (fills 1 nil and fixes the stack top) ++ skip: ++ The OP_SETTOP is always at call+2; luaK_setreturns_optchain ++ widens its B field when more nils are needed. 'e' keeps the ++ plain VCALL semantics (t/f stay NO_JUMP), so single-value ++ consumers work unchanged. */ ++ int base = GETARG_A(fs->f->code[v->u.info]); /* call base */ ++ SETARG_k(fs->f->code[v->u.info], 1); /* mark optional chain */ ++ skip = luaK_jump(fs); /* non-nil path skips the nil fill */ ++ nilpc = luaK_codeABC(fs, OP_SETTOP, base, 0, 0); ++ luaK_patchtohere(fs, skip); ++ fs->freereg = base + 1; ++ luaK_patchlist(fs, niljumps, nilpc); /* nil exits jump here */ ++ } ++ else { ++ int r; ++ if (vkisindexed(v->k)) ++ luaK_exp2anyreg(fs, v); /* make it a value, not a var */ ++ r = v->u.info; /* now a VNONRELOC register */ ++ if (r != chain_base) { /* move result to the chain base register */ ++ luaK_codeABC(fs, OP_MOVE, chain_base, r, 0); ++ v->u.info = chain_base; ++ v->k = VNONRELOC; ++ } ++ skip = luaK_jump(fs); /* non-nil path skips the nil fill */ ++ nilpc = luaK_codeABC(fs, OP_LOADNIL, chain_base, 0, 0); ++ luaK_patchtohere(fs, skip); ++ fs->freereg = chain_base + 1; /* free chain temporaries */ ++ luaK_patchlist(fs, niljumps, nilpc); /* nil exits jump to LOADNIL */ ++ } ++ } ++#endif ++ return; + } + } + } +@@ -1822,9 +1922,16 @@ static void retstat (LexState *ls) { + else { + nret = explist(ls, &e); /* optional return values */ + if (hasmultret(e.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &e, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &e); + #if defined(NDEBUG) +- if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc) { /* tail call? */ ++ if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc ++#if defined(BEE_OPTCHAIN) ++ && !TESTARG_k(fs->f->code[e.u.info]) /* no tail call for optional-chain calls */ ++#endif ++ ) { /* tail call? */ + SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL); + lua_assert(GETARG_A(getinstruction(fs,&e)) == luaY_nvarstack(fs)); + } +@@ -1967,4 +2074,3 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, + L->top.p--; /* remove scanner's table */ + return cl; /* closure is on the stack, too */ + } +- +diff --git a/lvm.c b/lvm.c +index 7023a04..57dcd17 100644 +--- a/lvm.c ++++ b/lvm.c +@@ -1236,6 +1236,21 @@ void luaV_execute (lua_State *L, CallInfo *ci) { + } while (b--); + vmbreak; + } ++#if defined(BEE_OPTCHAIN) ++ vmcase(OP_SETTOP) { ++ /* Optional-chain short circuit: fill R[A..A+B] with nils and ++ also fix the stack top so that open instructions (OP_RETURN, ++ OP_CALL, OP_SETLIST) read exactly the nils produced here. ++ (Only the optional-chain compiler emits this instruction.) */ ++ StkId ra = RA(i); ++ int b = GETARG_B(i); ++ do { ++ setnilvalue(s2v(ra++)); ++ } while (b--); ++ L->top.p = RA(i) + GETARG_B(i) + 1; ++ vmbreak; ++ } ++#endif + vmcase(OP_GETUPVAL) { + StkId ra = RA(i); + int b = GETARG_B(i); diff --git a/3rd/lua-patch/optchain/lua55.patch b/3rd/lua-patch/optchain/lua55.patch new file mode 100644 index 00000000..39d42442 --- /dev/null +++ b/3rd/lua-patch/optchain/lua55.patch @@ -0,0 +1,255 @@ +diff --git a/lopcodes.c b/lopcodes.c +index 7e18231..c8e3ad1 100644 +--- a/lopcodes.c ++++ b/lopcodes.c +@@ -106,6 +106,9 @@ LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { + ,opmode(0, 0, 0, 0, 0, iABx) /* OP_ERRNNIL */ + ,opmode(0, 0, 1, 0, 1, iABC) /* OP_VARARGPREP */ + ,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */ ++#if defined(BEE_OPTCHAIN) ++ ,opmode(0, 0, 0, 0, 1, iABC) /* OP_SETTOP */ ++#endif + }; + + +diff --git a/lopcodes.h b/lopcodes.h +index b6bd182..4ce694f 100644 +--- a/lopcodes.h ++++ b/lopcodes.h +@@ -345,10 +345,17 @@ OP_ERRNNIL,/* A Bx raise error if R[A] ~= nil (K[Bx - 1] is global name)*/ + OP_VARARGPREP,/* (adjust varargs) */ + + OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */ ++#if defined(BEE_OPTCHAIN) ++,OP_SETTOP/* A B R[A], ..., R[A+B] := nil; top := A+B+1 */ ++#endif + } OpCode; + + ++#if defined(BEE_OPTCHAIN) ++#define NUM_OPCODES ((int)(OP_SETTOP) + 1) ++#else + #define NUM_OPCODES ((int)(OP_EXTRAARG) + 1) ++#endif + + + +diff --git a/lopnames.h b/lopnames.h +index 0554a2e..e82a66f 100644 +--- a/lopnames.h ++++ b/lopnames.h +@@ -98,6 +98,9 @@ static const char *const opnames[] = { + "ERRNNIL", + "VARARGPREP", + "EXTRAARG", ++#if defined(BEE_OPTCHAIN) ++ "SETTOP", ++#endif + NULL + }; + +diff --git a/lparser.c b/lparser.c +index 090e150..1c2485f 100644 +--- a/lparser.c ++++ b/lparser.c +@@ -37,6 +37,31 @@ + + #define hasmultret(k) ((k) == VCALL || (k) == VVARARG) + ++#if defined(BEE_OPTCHAIN) ++/* ++** Patch the short-circuit path of an optional-chain call so that it ++** produces 'nresults' nil values instead of a single one. This allows ++** chains ending in a call to yield multiple results (e.g. ++** 'local a, b = f?()'). The placeholder LOADNIL (with the 'k' flag ++** set, which also fixes the stack top; see lvm.c) was recorded in ++** 'e->t' by suffixedexp. ++*/ ++static void luaK_setreturns_optchain (FuncState *fs, expdesc *e, int nresults) { ++ if (e->k == VCALL) { /* open function call? */ ++ int pc = e->u.info; /* position of the call */ ++ if (TESTARG_k(fs->f->code[pc])) { /* optional-chain call (fixed layout)? */ ++ /* The short-circuit path is a fixed layout right after the call: ++ CALL (with 'k' flag set) / JMP / OP_SETTOP. The OP_SETTOP (which ++ also fixes the stack top; see lvm.c) holds the number of nil ++ slots minus one in its B field: a fixed 'nresults' needs exactly ++ that many nils, while LUA_MULTRET needs exactly one (B stays 0). */ ++ if (nresults != LUA_MULTRET) /* fixed number of results? */ ++ SETARG_B(fs->f->code[pc + 2], nresults - 1); /* widen OP_SETTOP */ ++ } ++ } ++} ++#endif ++ + + /* because all strings are unified by the scanner, the parser + can use pointer equality for string equality */ +@@ -552,6 +577,9 @@ static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) { + int extra = needed + 1; /* discount last expression itself */ + if (extra < 0) + extra = 0; ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, e, extra); ++#endif + luaK_setreturns(fs, e, extra); /* last exp. provides the difference */ + } + else { +@@ -967,6 +995,9 @@ static void closelistfield (FuncState *fs, ConsControl *cc) { + static void lastlistfield (FuncState *fs, ConsControl *cc) { + if (cc->tostore == 0) return; + if (hasmultret(cc->v.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &cc->v, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, LUA_MULTRET); + cc->na--; /* do not count last expression (unknown number of elements) */ +@@ -1147,8 +1178,12 @@ static void funcargs (LexState *ls, expdesc *f) { + args.k = VVOID; + else { + explist(ls, &args); +- if (hasmultret(args.k)) ++ if (hasmultret(args.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &args, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &args); ++ } + } + check_match(ls, ')', '(', line); + break; +@@ -1218,9 +1253,33 @@ static void suffixedexp (LexState *ls, expdesc *v) { + /* suffixedexp -> + primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */ + FuncState *fs = ls->fs; ++#if defined(BEE_OPTCHAIN) ++ int niljumps = NO_JUMP; /* patch list of optional-chain exits (nil) */ ++ int chain_base = fs->freereg; /* result register of the chain */ ++#endif + primaryexp(ls, v); + for (;;) { + switch (ls->t.token) { ++#if defined(BEE_OPTCHAIN) ++ case '?': { /* optional chain: '?.' '?:' '?[' '?(' */ ++ int reg, nilreg; ++ luaX_next(ls); /* consume '?' */ ++ if (ls->t.token != '.' && ls->t.token != ':' && ++ ls->t.token != '[' && ls->t.token != '(') ++ luaX_syntaxerror(ls, "unexpected symbol near '?'"); ++ reg = luaK_exp2anyreg(fs, v); /* evaluate receiver only once */ ++ nilreg = fs->freereg; ++ luaK_nil(fs, nilreg, 1); /* ensure it is nil at runtime */ ++ /* Reserve the temp through luaK_reserveregs (which also grows ++ 'maxstacksize') instead of a raw freereg++ that would bypass ++ luaK_checkstack and leave 'maxstacksize' stale. */ ++ luaK_reserveregs(fs, 1); ++ luaK_codeABCk(fs, OP_EQ, reg, nilreg, 0, 1); /* jump when nil */ ++ luaK_concat(fs, &niljumps, luaK_jump(fs)); ++ fs->freereg--; /* release the nil slot; only used by OP_EQ */ ++ break; /* next iteration handles '.' ':' '[' '(' */ ++ } ++#endif + case '.': { /* fieldsel */ + fieldsel(ls, v); + break; +@@ -1245,7 +1304,50 @@ static void suffixedexp (LexState *ls, expdesc *v) { + funcargs(ls, v); + break; + } +- default: return; ++ default: ++#if defined(BEE_OPTCHAIN) ++ if (niljumps != NO_JUMP) { ++ int skip, nilpc; ++ if (v->k == VCALL) { ++ /* A chain ending in a call can yield multiple results: keep ++ the call open instead of collapsing it to a single value. ++ We emit a fixed layout so that later consumers can patch ++ the short-circuit path without storing extra info in 'e': ++ CALL base ... (with the 'k' flag set, marking the chain) ++ JMP skip ++ OP_SETTOP base 0 (fills 1 nil and fixes the stack top) ++ skip: ++ The OP_SETTOP is always at call+2; luaK_setreturns_optchain ++ widens its B field when more nils are needed. 'e' keeps the ++ plain VCALL semantics (t/f stay NO_JUMP), so single-value ++ consumers work unchanged. */ ++ int base = GETARG_A(fs->f->code[v->u.info]); /* call base */ ++ SETARG_k(fs->f->code[v->u.info], 1); /* mark optional chain */ ++ skip = luaK_jump(fs); /* non-nil path skips the nil fill */ ++ nilpc = luaK_codeABC(fs, OP_SETTOP, base, 0, 0); ++ luaK_patchtohere(fs, skip); ++ fs->freereg = base + 1; ++ luaK_patchlist(fs, niljumps, nilpc); /* nil exits jump here */ ++ } ++ else { ++ int r; ++ if (vkisindexed(v->k)) ++ luaK_exp2anyreg(fs, v); /* make it a value, not a var */ ++ r = v->u.info; /* now a VNONRELOC register */ ++ if (r != chain_base) { /* move result to the chain base register */ ++ luaK_codeABC(fs, OP_MOVE, chain_base, r, 0); ++ v->u.info = chain_base; ++ v->k = VNONRELOC; ++ } ++ skip = luaK_jump(fs); /* non-nil path skips the nil fill */ ++ nilpc = luaK_codeABC(fs, OP_LOADNIL, chain_base, 0, 0); ++ luaK_patchtohere(fs, skip); ++ fs->freereg = chain_base + 1; /* free chain temporaries */ ++ luaK_patchlist(fs, niljumps, nilpc); /* nil exits jump to LOADNIL */ ++ } ++ } ++#endif ++ return; + } + } + } +@@ -2034,9 +2136,16 @@ static void retstat (LexState *ls) { + else { + nret = explist(ls, &e); /* optional return values */ + if (hasmultret(e.k)) { ++#if defined(BEE_OPTCHAIN) ++ luaK_setreturns_optchain(fs, &e, LUA_MULTRET); ++#endif + luaK_setmultret(fs, &e); + #if defined(NDEBUG) +- if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc) { /* tail call? */ ++ if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc ++#if defined(BEE_OPTCHAIN) ++ && !TESTARG_k(fs->f->code[e.u.info]) /* no tail call for optional-chain calls */ ++#endif ++ ) { /* tail call? */ + SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL); + lua_assert(GETARG_A(getinstruction(fs,&e)) == luaY_nvarstack(fs)); + } +@@ -2201,4 +2310,3 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, + L->top.p--; /* remove scanner's table */ + return cl; /* closure is on the stack, too */ + } +- +diff --git a/lvm.c b/lvm.c +index c70e2b8..195c9df 100644 +--- a/lvm.c ++++ b/lvm.c +@@ -1284,6 +1284,21 @@ void luaV_execute (lua_State *L, CallInfo *ci) { + } while (b--); + vmbreak; + } ++#if defined(BEE_OPTCHAIN) ++ vmcase(OP_SETTOP) { ++ /* Optional-chain short circuit: fill R[A..A+B] with nils and ++ also fix the stack top so that open instructions (OP_RETURN, ++ OP_CALL, OP_SETLIST) read exactly the nils produced here. ++ (Only the optional-chain compiler emits this instruction.) */ ++ StkId ra = RA(i); ++ int b = GETARG_B(i); ++ do { ++ setnilvalue(s2v(ra++)); ++ } while (b--); ++ L->top.p = RA(i) + GETARG_B(i) + 1; ++ vmbreak; ++ } ++#endif + vmcase(OP_GETUPVAL) { + StkId ra = RA(i); + int b = GETARG_B(i); diff --git a/compile/common.lua b/compile/common.lua index 4e04c89f..9ca775aa 100644 --- a/compile/common.lua +++ b/compile/common.lua @@ -7,6 +7,39 @@ lm.compile_commands = "$builddir" lm.lua = lm.lua or "55" lm.luadir = lm:path("3rd/lua"..lm.lua) +-- 可选链补丁:将官方源码复制到构建目录并应用补丁(git apply) +if lm.optchain then + lm:runlua "apply_optchain_patch" { + script = "3rd/lua-patch/apply_patch.lua", + args = { + "3rd/lua" .. lm.lua, + "3rd/lua-patch/optchain/lua" .. lm.lua .. ".patch", + "$builddir/patched/lua" .. lm.lua, + }, + inputs = { + "3rd/lua"..lm.lua.."/onelua.c", + "3rd/lua"..lm.lua.."/linit.c", + "3rd/lua"..lm.lua.."/lparser.c", + "3rd/lua"..lm.lua.."/lvm.c", + "3rd/lua"..lm.lua.."/lopcodes.c", + "3rd/lua"..lm.lua.."/lopcodes.h", + "3rd/lua"..lm.lua.."/lopnames.h", + "3rd/lua-patch/apply_patch.lua", + "3rd/lua-patch/optchain/lua" .. lm.lua .. ".patch", + }, + outputs = { + "$builddir/patched/lua"..lm.lua.."/onelua.c", + "$builddir/patched/lua"..lm.lua.."/linit.c", + "$builddir/patched/lua"..lm.lua.."/lparser.c", + "$builddir/patched/lua"..lm.lua.."/lvm.c", + "$builddir/patched/lua"..lm.lua.."/lopcodes.c", + "$builddir/patched/lua"..lm.lua.."/lopcodes.h", + "$builddir/patched/lua"..lm.lua.."/lopnames.h", + }, + } + lm.luadir = lm:path("$builddir/patched/lua" .. lm.lua) +end + local function macos_version() local cxx = lm.cxx or "c++17" local version = cxx:match "^c%+%+(.+)$" @@ -80,11 +113,11 @@ if lm.sanitize then end lm:source_set "source_lua" { - includes = lm.luadir, + includes = lm.optchain and { lm.luadir, lm:path("3rd/lua"..lm.lua) } or lm.luadir, sources = { lm.luadir / "onelua.c", }, - defines = "MAKE_LIB", + defines = lm.optchain and { "MAKE_LIB", "BEE_OPTCHAIN" } or "MAKE_LIB", visibility = "default", windows = { defines = "LUA_BUILD_AS_DLL", diff --git a/compile/lua.lua b/compile/lua.lua index 94822f33..305d9f1d 100644 --- a/compile/lua.lua +++ b/compile/lua.lua @@ -3,11 +3,16 @@ local lm = require "luamake" if lm.os == "windows" then lm:shared_library("lua"..lm.lua) { deps = "bee_utf8_crt", + includes = lm.optchain and { lm.luadir, lm:path("3rd/lua"..lm.lua) } or lm.luadir, sources = { lm.luadir / "onelua.c", lm.luadir / "linit.c", }, - defines = { + defines = lm.optchain and { + "MAKE_LIB", + "LUA_BUILD_AS_DLL", + "BEE_OPTCHAIN", + } or { "MAKE_LIB", "LUA_BUILD_AS_DLL", }, @@ -21,7 +26,11 @@ if lm.os == "windows" then "bee_utf8_crt", "lua"..lm.lua, }, - includes = { + includes = lm.optchain and { + ".", + lm.luadir, + lm:path("3rd/lua"..lm.lua), + } or { ".", lm.luadir, }, @@ -33,12 +42,15 @@ if lm.os == "windows" then } lm:executable "luac" { deps = "bee_utf8_crt", - includes = ".", + includes = lm.optchain and { ".", lm.luadir, lm:path("3rd/lua"..lm.lua) } or ".", sources = { lm.luadir / "onelua.c", "3rd/lua-patch/bee_utf8_main.c", }, - defines = { + defines = lm.optchain and { + "MAKE_LUAC", + "BEE_OPTCHAIN", + } or { "MAKE_LUAC", }, msvc = lm.fast_setjmp ~= "off" and { @@ -61,7 +73,7 @@ end lm:executable "lua" { deps = "source_lua", - includes = lm.luadir, + includes = lm.optchain and { lm.luadir, lm:path("3rd/lua"..lm.lua) } or lm.luadir, sources = { lm.luadir / "lua.c", lm.luadir / "linit.c", diff --git a/test/test.lua b/test/test.lua index 24456811..f24c6cf0 100644 --- a/test/test.lua +++ b/test/test.lua @@ -33,6 +33,14 @@ local _ = crash.create_handler "-" require "test_skip" require "test_lua" + +do -- optional chain is enabled via BEE_OPTCHAIN at build time + local loadable = load "local x; return x?.y" + if loadable then + require "test_optional_chain" + end +end + require "test_serialization" require "test_filesystem" require "test_thread" diff --git a/test/test_optional_chain.lua b/test/test_optional_chain.lua new file mode 100644 index 00000000..0fa9da89 --- /dev/null +++ b/test/test_optional_chain.lua @@ -0,0 +1,204 @@ +-- Optional chaining (?.) is a custom syntax enabled at build time via the +-- BEE_OPTCHAIN macro (see 3rd/lua-patch/optchain/). The '?.' syntax errors +-- shown by LuaLS below are expected: the language server does not know this +-- extension. These tests only run when the interpreter was built with +-- BEE_OPTCHAIN (see the loader in test/test.lua). +local lt = require "ltest" + +local test_optchain = lt.test "optional_chain" + +function test_optchain:test_field() + local obj = { a = { b = 42 } } + lt.assertEquals(obj?.a?.b, 42) + local nothing + lt.assertNil(nothing?.a?.b) + lt.assertNil(obj?.x?.y) + lt.assertEquals((nil)?.a, nil) +end + +function test_optchain:test_field_chain() + local obj = { a = { b = { c = 1 } } } + lt.assertEquals(obj?.a.b.c, 1) + local nothing + lt.assertNil(nothing?.a.b.c) + lt.assertEquals(obj?.a?.b.c, 1) + lt.assertNil(obj?.x?.y?.z) + lt.assertEquals(obj?.a?.b?.c, 1) +end + +function test_optchain:test_index() + local t = { [1] = { [2] = "hi" } } + lt.assertEquals(t?[1]?[2], "hi") + local nothing + lt.assertNil(nothing?[1]) + lt.assertNil(t?[2]?[1]) + lt.assertEquals(t?[1][2], "hi") +end + +function test_optchain:test_method() + local obj = { x = 1, get = function(self) return self.x end } + lt.assertEquals(obj?:get(), 1) + local nothing + lt.assertNil(nothing?:get()) +end + +function test_optchain:test_call() + local f = function() return "ok" end + lt.assertEquals(f?(), "ok") + local nothing + lt.assertNil(nothing?()) +end + +function test_optchain:test_call_args() + local f = function(a, b, c) return a + b + c end + lt.assertEquals(f?(1, 2, 3), 6) + local nothing + lt.assertNil(nothing?(1, 2, 3)) +end + +function test_optchain:test_call_args_short_circuit() + -- short-circuit must not evaluate the arguments (no side effects) + local calls = 0 + local function arg(v) calls = calls + 1; return v end + local f = function(a, b, c) return a + b + c end + lt.assertEquals(f?(arg(1), arg(2), arg(3)), 6) + lt.assertEquals(calls, 3) + local nothing + lt.assertNil(nothing?(arg(1), arg(2), arg(3))) + lt.assertEquals(calls, 3) -- args not evaluated on short-circuit +end + +function test_optchain:test_call_args_multi() + local g = function(a, b) return a, b end + local x, y = g?(10, 20) + lt.assertEquals(x, 10) + lt.assertEquals(y, 20) + local nothing + local n1, n2 = nothing?(10, 20) + lt.assertNil(n1) + lt.assertNil(n2) +end + +function test_optchain:test_short_circuit_key() + local calls = 0 + local function key() calls = calls + 1; return 1 end + local nothing + local t = { [1] = "v" } + lt.assertEquals(nothing?[key()], nil) + lt.assertEquals(calls, 0) + lt.assertEquals(t?[key()], "v") + lt.assertEquals(calls, 1) +end + +function test_optchain:test_short_circuit_args() + local calls = 0 + local function arg() calls = calls + 1; return 1 end + local obj = { f = function(self, x) return x end } + local nothing + lt.assertEquals(nothing?:f(arg()), nil) + lt.assertEquals(calls, 0) + lt.assertEquals(obj?:f(arg()), 1) + lt.assertEquals(calls, 1) +end + +function test_optchain:test_eval_once() + local calls = 0 + local function recv() calls = calls + 1; return { a = { b = 1 } } end + lt.assertEquals(recv()?.a?.b, 1) + lt.assertEquals(calls, 1) + lt.assertNil(recv()?.x?.y) + lt.assertEquals(calls, 2) +end + +function test_optchain:test_false_not_short_circuit() + local f = false + lt.assertError(function () return f?.a end) + lt.assertError(function () return f?[1] end) +end + +function test_optchain:test_not_assignable() + local ok + ok, _ = load("obj?.a = 1") + lt.assertTrue(not ok) + ok, _ = load("obj?[1] = 2") + lt.assertTrue(not ok) + ok, _ = load("obj?:f = 3") + lt.assertTrue(not ok) +end + +function test_optchain:test_bad_syntax() + local ok + ok, _ = load("local a; return a?") + lt.assertTrue(not ok) + ok, _ = load("local a; return a ?? 1") + lt.assertTrue(not ok) + ok, _ = load("local a; return a?b") + lt.assertTrue(not ok) +end + +-- Multiple results: a chain ending in a call can yield several values +-- (the short-circuit path fills the whole result range with nils). + +function test_optchain:test_multi_value_assign() + local obj = { getSize = function() return 100, 200 end } + local w, h = obj?:getSize() + lt.assertEquals(w, 100) + lt.assertEquals(h, 200) + local nothing + local a, b, c = nothing?:getSize() + lt.assertNil(a) + lt.assertNil(b) + lt.assertNil(c) +end + +function test_optchain:test_multi_value_method() + local o = { pair = function(self) return 1, 2, 3 end } + local x, y, z = o?:pair() + lt.assertEquals(x, 1) + lt.assertEquals(y, 2) + lt.assertEquals(z, 3) +end + +function test_optchain:test_multi_value_return() + local obj = { getSize = function() return 7, 8 end } + local function f() + return obj?:getSize() + end + local r1, r2 = f() + lt.assertEquals(r1, 7) + lt.assertEquals(r2, 8) + local function g() + local n + return n?:getSize() + end + local s1, s2 = g() + lt.assertNil(s1) + lt.assertNil(s2) +end + +function test_optchain:test_multi_value_table() + local obj = { getSize = function() return 5, 6 end } + local t = { obj?:getSize() } + lt.assertEquals(t[1], 5) + lt.assertEquals(t[2], 6) + local nothing + local tn = { nothing?:getSize() } + lt.assertEquals(#tn, 0) -- short-circuit: a single nil element +end + +function test_optchain:test_multi_value_single() + -- Single-value contexts still collapse to one value. + local obj = { getSize = function() return 100, 200 end } + local s = obj?:getSize() + lt.assertEquals(s, 100) +end + +function test_optchain:test_multi_value_args() + -- A chain ending in a call, used as call arguments, yields exactly + -- the produced values (short-circuit: exactly one nil argument). + local function count(...) return select("#", ...) end + local obj = { getSize = function() return 100, 200 end } + lt.assertEquals(count(obj?:getSize()), 2) + local nothing + lt.assertEquals(count(nothing?:getSize()), 1) +end