Re: Inconsistent Multiline String Handling in Lua REPL (Direct Input vs. History Recall)

mathieu stumpf-guntz <[email protected]> Mon, 3 Aug 2026 22:50:52 -0700 (PDT)
Newsgroups gmane.comp.lang.lua.general
Message-ID <[email protected]>
------=_Part_32884_804580086.1785822652600
Content-Type: multipart/alternative; 
	boundary="----=_Part_32885_1476891830.1785822652600"

------=_Part_32885_1476891830.1785822652600
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Hi TopchetoEU,

Thanks for this feedback. It was genuinely valuable and highlighted a fully=
=20
relevant point.

So, yes, as we seems to agree, multiline input with valid expression should=
=20
keep expression semantics. And yes, the previous patch instead was=20
implicitly treating them as statement-only and losing return-value printing=
.

That behavior has been corrected in the attached patch: multiline=20
expression handling is now aligned with single-line expectations.

In the same spirit, the test suite was further hardened around multiline=20
REPL behavior, including additional corner cases beyond the original=20
example, so this class of regression stays covered.

For those curious to play with it, the attached patch should be applied=20
against 7579fc9d7ed90240487251dfb69168f8e64e9294


On Sunday, August 2, 2026 at 9:33:04=E2=80=AFPM UTC TopchetoEU wrote:

> Wouldn't trying to parse multiline inputs as expressions be a more useful=
=20
> behavior here? Under the current circumstances a perfectly valid expressi=
on=20
> would be "promoted" to a statement, provided it is multiline. So for this=
=20
> input:
>
> > function some_func(...) --[[ do something useful... ]] return ... end
> > some_func(1, 2,
> >> 3, 4)
>
> You would never see the return values of `some_func`, because it was=20
> promoted to a statement.
>
> Also, you have this weirdness where a perfectly valid expression is not a=
=20
> valid statement, so lua complains. However, I can't think of a valid reas=
on=20
> to break up non-call expressions, other than tables (of course writting o=
ut=20
> a plain table in the REPL seems rather misguided...)
>
>
> On Thursday, July 30th, 2026 at 7:20 PM, Roberto Ierusalimschy <
> [email protected]> wrote:
>
> > > In Lua REPL, direct input of multiline strings fails with a syntax=20
> error,
> > > while recalling the same input via history navigation (up arrow)=20
> executes
> > > correctly. This behavior is observed starting from Lua 5.3.x. In Lua=
=20
> 5.2.4,
> > > history navigation as a feature did not exist, so the issue cannot be
> > > verified in earlier versions.
> >=20
> > This is documented behavior:
> >=20
> > In interactive mode, Lua repeatedly prompts and waits for a line.
> > After reading a line, Lua first tries to interpret the line as an
> > expression. If it succeeds, it prints its value. Otherwise, it
> > interprets the line as a chunk. If you write an incomplete chunk,
> > the interpreter waits for its completion by issuing a different
> > prompt.
> >=20
> > Note the "tries to interpret THE LINE as an expression" and "If you
> > write an incomplete CHUNK": Lua only accepts entire chunks as multiline
> > input; it does not accept expressions. So,
> >=20
> > > a =3D 10 +
> > >> 20
> >=20
> > works, but
> >=20
> > > 10 +
> > >> 20
> >=20
> > doesn't.
> >=20
> > The idiosyncrasy is the fact that it accepts when it comes through
> > history navigation; it is because then the input, although having
> > multiple lines, is not really a "multiline" input (e.g., no different
> > prompt); it goes all in one step.
> >=20
> > -- Roberto
> >=20
> > --
> > You received this message because you are subscribed to the Google=20
> Groups "lua-l" group.
> > To unsubscribe from this group and stop receiving emails from it, send=
=20
> an email to [email protected].
> > To view this discussion visit=20
> https://groups.google.com/d/msgid/lua-l/20260730161948.GA11249%40arraial.=
inf.puc-rio.br
> .
> >=20
>

--=20
You received this message because you are subscribed to the Google Groups "=
lua-l" group.
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/lua-l/53bac=
3e2-ae29-42be-9828-65f0b1c23440n%40googlegroups.com.

------=_Part_32885_1476891830.1785822652600
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div>Hi TopchetoEU,</div><div><br /></div><div>Thanks for this feedback. It=
 was genuinely valuable and highlighted a fully relevant point.</div><div><=
br /></div><div>So, yes, as we seems to agree, multiline input with valid e=
xpression should keep expression semantics. And yes, the previous patch ins=
tead was implicitly treating them as statement-only and losing return-value=
 printing.</div><div><br /></div><div>That behavior has been corrected in t=
he attached patch: multiline expression handling is now aligned with single=
-line expectations.</div><div><br /></div>In the same spirit, the test suit=
e was further hardened around multiline REPL behavior, including additional=
 corner cases beyond the original example, so this class of regression stay=
s covered.<br /><div><br /></div><div>For those curious to play with it, th=
e attached patch should be applied against=C2=A07579fc9d7ed90240487251dfb69=
168f8e64e9294</div><div><br /></div><div><br /></div><div class=3D"gmail_qu=
ote"><div dir=3D"auto" class=3D"gmail_attr">On Sunday, August 2, 2026 at 9:=
33:04=E2=80=AFPM UTC TopchetoEU wrote:<br/></div><blockquote class=3D"gmail=
_quote" style=3D"margin: 0 0 0 0.8ex; border-left: 1px solid rgb(204, 204, =
204); padding-left: 1ex;">Wouldn&#39;t trying to parse multiline inputs as =
expressions be a more useful behavior here? Under the current circumstances=
 a perfectly valid expression would be &quot;promoted&quot; to a statement,=
 provided it is multiline. So for this input:
<br>
<br>    &gt; function some_func(...) --[[ do something useful... ]] return =
... end
<br>    &gt; some_func(1, 2,
<br>    &gt;&gt; 3, 4)
<br>
<br>You would never see the return values of `some_func`, because it was pr=
omoted to a statement.
<br>
<br>Also, you have this weirdness where a perfectly valid expression is not=
 a valid statement, so lua complains. However, I can&#39;t think of a valid=
 reason to break up non-call expressions, other than tables (of course writ=
ting out a plain table in the REPL seems rather misguided...)
<br>
<br>
<br>On Thursday, July 30th, 2026 at 7:20 PM, Roberto Ierusalimschy &lt;<a h=
ref data-email-masked rel=3D"nofollow">[email protected]</a>&gt; wrote:
<br>
<br>&gt; &gt; In Lua REPL, direct input of multiline strings fails with a s=
yntax error,
<br>&gt; &gt; while recalling the same input via history navigation (up arr=
ow) executes
<br>&gt; &gt; correctly. This behavior is observed starting from Lua 5.3.x.=
 In Lua 5.2.4,
<br>&gt; &gt; history navigation as a feature did not exist, so the issue c=
annot be
<br>&gt; &gt; verified in earlier versions.
<br>&gt;=20
<br>&gt; This is documented behavior:
<br>&gt;=20
<br>&gt;     In interactive mode, Lua repeatedly prompts and waits for a li=
ne.
<br>&gt;     After reading a line, Lua first tries to interpret the line as=
 an
<br>&gt;     expression.  If it succeeds, it prints its value.  Otherwise, =
it
<br>&gt;     interprets the line as a chunk.  If you write an incomplete ch=
unk,
<br>&gt;     the interpreter waits for its completion by issuing a differen=
t
<br>&gt;     prompt.
<br>&gt;=20
<br>&gt; Note the &quot;tries to interpret THE LINE as an expression&quot; =
and &quot;If you
<br>&gt; write an incomplete CHUNK&quot;: Lua only accepts entire chunks as=
 multiline
<br>&gt; input; it does not accept expressions. So,
<br>&gt;=20
<br>&gt;   &gt; a =3D 10 +
<br>&gt;   &gt;&gt; 20
<br>&gt;=20
<br>&gt; works, but
<br>&gt;=20
<br>&gt;   &gt; 10 +
<br>&gt;   &gt;&gt; 20
<br>&gt;=20
<br>&gt; doesn&#39;t.
<br>&gt;=20
<br>&gt; The idiosyncrasy is the fact that it accepts when it comes through
<br>&gt; history navigation; it is because then the input, although having
<br>&gt; multiple lines, is not really a &quot;multiline&quot; input (e.g.,=
 no different
<br>&gt; prompt); it goes all in one step.
<br>&gt;=20
<br>&gt; -- Roberto
<br>&gt;=20
<br>&gt; --
<br>&gt; You received this message because you are subscribed to the Google=
 Groups &quot;lua-l&quot; group.
<br>&gt; To unsubscribe from this group and stop receiving emails from it, =
send an email to <a href data-email-masked rel=3D"nofollow">lua-l+un...@goo=
glegroups.com</a>.
<br>&gt; To view this discussion visit <a href=3D"https://groups.google.com=
/d/msgid/lua-l/20260730161948.GA11249%40arraial.inf.puc-rio.br" target=3D"_=
blank" rel=3D"nofollow" data-saferedirecturl=3D"https://www.google.com/url?=
hl=3Den&amp;q=3Dhttps://groups.google.com/d/msgid/lua-l/20260730161948.GA11=
249%2540arraial.inf.puc-rio.br&amp;source=3Dgmail&amp;ust=3D178590666802200=
0&amp;usg=3DAOvVaw1zUvov7S2DCrCMlMOECs_S">https://groups.google.com/d/msgid=
/lua-l/20260730161948.GA11249%40arraial.inf.puc-rio.br</a>.
<br>&gt;=20
<br></blockquote></div>

<p></p>

-- <br />
You received this message because you are subscribed to the Google Groups &=
quot;lua-l&quot; group.<br />
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to <a href=3D"mailto:[email protected]">lua-l+unsubsc=
[email protected]</a>.<br />
To view this discussion visit <a href=3D"https://groups.google.com/d/msgid/=
lua-l/53bac3e2-ae29-42be-9828-65f0b1c23440n%40googlegroups.com?utm_medium=
=3Demail&utm_source=3Dfooter">https://groups.google.com/d/msgid/lua-l/53bac=
3e2-ae29-42be-9828-65f0b1c23440n%40googlegroups.com</a>.<br />

------=_Part_32885_1476891830.1785822652600--

------=_Part_32884_804580086.1785822652600
Content-Type: text/x-diff; charset=US-ASCII; 
	name=flexible-multiline-repl.diff
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename=flexible-multiline-repl.diff
X-Attachment-Id: 5e1cb168-46cc-4cda-8776-261a2bf68620
Content-ID: <5e1cb168-46cc-4cda-8776-261a2bf68620>

diff --git c/lua.c w/lua.c
index 858a04c0..b7e28140 100644
--- c/lua.c
+++ w/lua.c
@@ -608,7 +608,7 @@ static int pushline (lua_State *L, int firstline) {
 */
 static int addreturn (lua_State *L) {
   const char *line = lua_tostring(L, -1);  /* original line */
-  const char *retline = lua_pushfstring(L, "return %s;", line);
+  const char *retline = lua_pushfstring(L, "return %s", line);
   int status = luaL_loadbufferx(L, retline, strlen(retline), "=stdin", "t");
   if (status == LUA_OK)
     lua_remove(L, -2);  /* remove modified line */
@@ -641,8 +641,25 @@ static int multiline (lua_State *L) {
   checklocal(line);
   for (;;) {  /* repeat until gets a complete statement */
     int status = luaL_loadbufferx(L, line, len, "=stdin", "t");  /* try it */
-    if (!incomplete(L, status) || !pushline(L, 0))
+    int stmt_incomplete = incomplete(L, status);
+    int expr_incomplete = 0;
+    int exprstatus = LUA_OK;
+    if (!stmt_incomplete && status != LUA_OK) {
+      const char *retline = lua_pushfstring(L, "return %s", line);
+      exprstatus = luaL_loadbufferx(L, retline, strlen(retline), "=stdin", "t");
+      expr_incomplete = incomplete(L, exprstatus);
+      lua_pop(L, 2);  /* remove expression result and generated "return" line */
+    }
+    if (!(stmt_incomplete || expr_incomplete) || !pushline(L, 0)) {
+      if (expr_incomplete && !stmt_incomplete && status != LUA_OK) {
+        lua_pop(L, 1);  /* remove statement error */
+        const char *retline = lua_pushfstring(L, "return %s;", line);
+        exprstatus = luaL_loadbufferx(L, retline, strlen(retline), "=stdin", "t");
+        lua_remove(L, -2);  /* remove generated "return" line */
+        return exprstatus;  /* report expression-side incomplete error */
+      }
       return status;  /* should not or cannot try to add continuation line */
+    }
     lua_remove(L, -2);  /* remove error message (from incomplete line) */
     lua_pushliteral(L, "\n");  /* add newline... */
     lua_insert(L, -2);  /* ...between the two lines */
@@ -664,8 +681,22 @@ static int loadline (lua_State *L) {
   lua_settop(L, 0);
   if (!pushline(L, 1))
     return -1;  /* no input */
-  if ((status = addreturn(L)) != LUA_OK)  /* 'return ...' did not work? */
+  if ((status = addreturn(L)) != LUA_OK) {  /* 'return ...' did not work? */
+    size_t len;
+    const char *s;
     status = multiline(L);  /* try as command, maybe with continuation lines */
+    /*
+    ** With assembled multiline input, prefer expression semantics whenever
+    ** possible (so calls print return values), then fall back to statement.
+    */
+    s = lua_tolstring(L, 1, &len);
+    lua_pop(L, 1);  /* remove previous statement result/error */
+    if (addreturn(L) == LUA_OK)
+      status = LUA_OK;  /* stack: [1]=line, [2]=compiled expression */
+    else
+      status = luaL_loadbufferx(L, s, len, "=stdin", "t");
+      /* stack: [1]=line, [2]=statement result/error */
+  }
   line = lua_tostring(L, 1);
   if (line[0] != '\0')  /* non empty? */
     lua_saveline(line);  /* keep history */
@@ -791,4 +822,3 @@ int main (int argc, char **argv) {
   lua_close(L);
   return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
 }
-
diff --git c/testes/main.lua w/testes/main.lua
index 98d36951..56d12803 100644
--- c/testes/main.lua
+++ w/testes/main.lua
@@ -382,6 +382,121 @@ prepfile("a = [[b\nc\nd\ne]]\na")
 RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i -- < %s > %s]], prog, out)
 checkprogout("b\nc\nd\ne\n\n")
 
+-- test that multi-line expressions work through continuation lines
+-- (not only when recalled from history as a single buffer)
+
+-- multi-line long string used in an expression
+prepfile('[[\ntest\n]] == "test\\n"\n')
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("true\n")
+
+-- multi-line parenthesized arithmetic expression
+prepfile("(\n1 +\n2 +\n3\n)\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("6\n")
+
+-- multi-line function call expression should print returned values
+prepfile[[
+function some_func(...) return ... end
+some_func(1, 2,
+3, 4)
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\t2\t3\t4\n")
+
+-- multi-line statement should still run through statement fallback
+prepfile("a =\n1\na\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\n")
+
+-- nested multiline call expression should still print returned values
+prepfile[[
+function g(a, b) return a + b end
+function f(x, y) return x, y end
+f(g(1,
+2),
+3)
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("3\t3\n")
+
+-- multiline method-call expression should print returned values
+prepfile[[
+t = {tag = "ok", foo = function (self, a, b) return a, b, self.tag end}
+t:foo(
+1, 2)
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\t2\tok\n")
+
+-- multiline table-constructor expression should print evaluated result
+prepfile[[
+({
+  a = 1,
+  b = 2,
+}).a
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\n")
+
+-- parenthesized multiline prefix expression should print call result
+prepfile[[
+(
+  function () return 1 end
+)()
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\n")
+
+-- multiline local declaration should still compile via statement path
+prepfile[[
+do
+  local x =
+    1
+  print(x)
+end
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("1\n")
+
+-- multiline assignment still favors statement fallback when needed
+prepfile[[
+a = 40 +
+  2
+a
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("42\n")
+
+-- comments in continued call should not break multiline expression handling
+prepfile[[
+function h(a, b) return a + b end
+h(1,  -- keep parsing through continuation
+2)
+]]
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s]], prog, out)
+checkprogout("3\n")
+
+-- genuinely invalid multi-line input should still error
+prepfile("(\n1 +\n)\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s 2>&1]], prog, out)
+assert(string.find(getoutput(), "near"))
+
+-- unfinished long construct should still report eof marker
+prepfile("[[\nabc\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s 2>&1]], prog, out)
+assert(string.find(getoutput(), "near <eof>", 1, true))
+
+-- unfinished call should still report eof marker
+prepfile("f(\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s 2>&1]], prog, out)
+assert(string.find(getoutput(), "near <eof>", 1, true))
+
+-- if both expression and statement forms fail, keep pointing at offending token
+prepfile("(\n1 +\n$\n)\n")
+RUN([[lua -e"_PROMPT='' _PROMPT2=''" -i < %s > %s 2>&1]], prog, out)
+assert(string.find(getoutput(), "near '$'", 1, true))
+
 -- input interrupted in continuation line
 prepfile("a.\n")
 RUN([[lua -i < %s > /dev/null 2> %s]], prog, out)

------=_Part_32884_804580086.1785822652600--