From 6161ad065e5d575bd79a1b3ac862354a468e2a03 Mon Sep 17 00:00:00 2001
From: Aleksandr Lyapunov <alyapunov@tarantool.org>
Date: Tue, 16 May 2023 01:12:21 +0300
Subject: [PATCH] box: support brackets in name resolution for Lua calls

Fefactor lua name resolution, simplify and comment.
Add an ability to specify path with brackets, for example in
'box.space[512]:get' or 'box.space["test"]:get'.
Only literals (strings and numbers) are supported.

Closes #8604

@TarantoolBot document
Title: square brackets in procedure resolution for Lua calls

Square brackets are now supported in Lua call procedure resolution. This is
applicable to `net.box` connection objects `call` method as well as
`box.schema.func.call`.

Examples of function calls with square brackets can be found in the test to
this patch.
---
 ...s-in-procedure-resolution-for-lua-calls.md |   4 +
 src/box/lua/call.c                            | 122 ++++++++++--------
 ...rocedure_resolution_for_lua_calls_test.lua | 108 ++++++++++++++++
 3 files changed, 179 insertions(+), 55 deletions(-)
 create mode 100644 changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md
 create mode 100644 test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua

diff --git a/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md b/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md
new file mode 100644
index 0000000000..cf69477bba
--- /dev/null
+++ b/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md
@@ -0,0 +1,4 @@
+## feature/box
+
+* Added support for square brackets in procedure resolution for Lua calls
+  (gh-8604).
diff --git a/src/box/lua/call.c b/src/box/lua/call.c
index 59c30bb73d..9fbd35c09b 100644
--- a/src/box/lua/call.c
+++ b/src/box/lua/call.c
@@ -93,74 +93,86 @@ get_call_serializer(void)
 }
 
 /**
- * A helper to find a Lua function by name and put it
- * on top of the stack.
+ * A helper to resolve a Lua function by full name, for example like:
+ * foo.bar['biz']["baz"][3].object:function
+ * Puts the function on top of the stack, followed by an object (if present).
+ * Returns number of items pushed (1 or 2) or -1 in case of error (diag is set).
  */
 static int
 box_lua_find(lua_State *L, const char *name, const char *name_end)
 {
-	int index = LUA_GLOBALSINDEX;
-	int objstack = 0, top = lua_gettop(L);
-	const char *start = name, *end;
-
-	while ((end = (const char *) memchr(start, '.', name_end - start))) {
-		lua_checkstack(L, 3);
-		lua_pushlstring(L, start, end - start);
-		lua_gettable(L, index);
-		if (! lua_istable(L, -1)) {
-			diag_set(ClientError, ER_NO_SUCH_PROC,
-				 name_end - name, name);
-			return -1;
-		}
-		start = end + 1; /* next piece of a.b.c */
-		index = lua_gettop(L); /* top of the stack */
-	}
+	lua_checkstack(L, 2); /* No more than 2 entries are needed. */
+	int top = lua_gettop(L);
 
-	/* box.something:method */
-	if ((end = (const char *) memchr(start, ':', name_end - start))) {
-		lua_checkstack(L, 3);
-		lua_pushlstring(L, start, end - start);
-		lua_gettable(L, index);
-		if (! (lua_istable(L, -1) ||
-			lua_islightuserdata(L, -1) || lua_isuserdata(L, -1) )) {
-				diag_set(ClientError, ER_NO_SUCH_PROC,
-					  name_end - name, name);
-				return -1;
+	/* Take the first token. */
+	const char *start = name;
+	while (start != name_end && *start != '.' &&
+	       *start != ':' && *start != '[')
+		start++;
+	lua_pushlstring(L, name, start - name);
+	lua_gettable(L, LUA_GLOBALSINDEX);
+
+	/* Take the rest tokens. */
+	while (start != name_end) {
+		if (!lua_istable(L, -1) &&
+		    !lua_islightuserdata(L, -1) && !lua_isuserdata(L, -1))
+			goto no_such_proc;
+
+		char delim = *start++; /* skip delimiter. */
+		if (delim == '.') {
+			/* Look for the next token. */
+			const char *end = start;
+			while (end != name_end && *end != '.' &&
+			       *end != ':' && *end != '[')
+				end++;
+			lua_pushlstring(L, start, end - start);
+			start = end;
+		} else if (delim == ':') {
+			lua_pushlstring(L, start, name_end - start);
+			lua_gettable(L, -2); /* get function from object. */
+			lua_insert(L, -2); /* swap function and object. */
+			break;
+		} else if (delim == '[') {
+			const char *end = memchr(start, ']', name_end - start);
+			if (end == NULL)
+				goto no_such_proc;
+
+			if (end - start >= 2 && start[0] == end[-1] &&
+			    (start[0] == '"' || start[0] == '\'')) {
+				/* Quoted string, just extract it. */
+				lua_pushlstring(L, start + 1, end - start - 2);
+			} else {
+				/* Must be a number, convert from string. */
+				lua_pushlstring(L, start, end - start);
+				int success;
+				lua_Number num = lua_tonumberx(L, -1, &success);
+				if (!success)
+					goto no_such_proc;
+				lua_pop(L, 1);
+				lua_pushnumber(L, num);
+			}
+			start = end + 1; /* skip closing bracket. */
+		} else {
+			goto no_such_proc;
 		}
 
-		start = end + 1; /* next piece of a.b.c */
-		index = lua_gettop(L); /* top of the stack */
-		objstack = index - top;
+		lua_gettable(L, -2); /* get child object from parent object. */
+		lua_remove(L, -2); /* drop previous parent object. */
 	}
 
-
-	lua_pushlstring(L, start, name_end - start);
-	lua_gettable(L, index);
-	if (!lua_isfunction(L, -1) && !lua_istable(L, -1)) {
+	/* Now at top+1 must be the function, and at top+2 may be the object. */
+	assert(lua_gettop(L) - top >= 1 && lua_gettop(L) - top <= 2);
+	if (!lua_isfunction(L, top + 1) && !lua_istable(L, top + 1)) {
 		/* lua_call or lua_gettable would raise a type error
 		 * for us, but our own message is more verbose. */
-		diag_set(ClientError, ER_NO_SUCH_PROC,
-			  name_end - name, name);
-		return -1;
+		goto no_such_proc;
 	}
 
-	/* setting stack that it would contain only
-	 * the function pointer. */
-	if (index != LUA_GLOBALSINDEX) {
-		if (objstack == 0) {        /* no object, only a function */
-			lua_replace(L, top + 1);
-			lua_pop(L, lua_gettop(L) - top - 1);
-		} else if (objstack == 1) { /* just two values, swap them */
-			lua_insert(L, -2);
-			lua_pop(L, lua_gettop(L) - top - 2);
-		} else {		    /* long path */
-			lua_insert(L, top + 1);
-			lua_insert(L, top + 2);
-			lua_pop(L, objstack - 1);
-			objstack = 1;
-		}
-	}
-	return 1 + objstack;
+	return lua_gettop(L) - top;
+
+no_such_proc:
+	diag_set(ClientError, ER_NO_SUCH_PROC, name_end - name, name);
+	return -1;
 }
 
 /**
diff --git a/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua b/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua
new file mode 100644
index 0000000000..8275d480e8
--- /dev/null
+++ b/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua
@@ -0,0 +1,108 @@
+local t = require('luatest')
+
+local g = t.group()
+g.before_all(function()
+    local netbox = require('net.box')
+
+    local a = {
+        b = {
+                c = function() return 'c' end,
+                [555] = function() return 555 end
+             },
+        [777] = {
+                    d = {
+                            [444] = function() return 444 end,
+                            e = function() return 'e' end
+                        },
+                    [666] = function() return 666 end
+                },
+        [555] = function() return 555 end,
+        [-1] = function() return -1 end,
+        [333] = netbox.self,
+        f = function() return 'f' end,
+        g = netbox.self
+    }
+    rawset(_G, 'a', a)
+end)
+
+g.after_all(function()
+    rawset(_G, 'a', nil)
+end)
+
+-- Checks that procedure resolution for Lua calls works correctly.
+g.test_procedure_resolution = function()
+    local netbox = require('net.box')
+    local function test(proc)
+        t.assert_equals(netbox.self:call(proc),
+                        netbox.self:eval('return ' .. proc .. '()'))
+    end
+
+    test('a.b.c')
+    test('a.b.c')
+    test('a.b["c"]')
+    test('a.b[\'c\']')
+    test('a.b[555]')
+    test('a[777].d[444]')
+    test('a[777].d.e')
+    test('a[777][666]')
+    test('a[555]')
+    test('a[555.]')
+    test('a[-1]')
+    test('a[333]:ping')
+    test('a.f')
+    test('a.g:ping')
+end
+
+-- Checks that error detection in procedure resolution for Lua calls works
+-- correctly.
+g.test_procedure_resolution_errors = function()
+    local netbox = require('net.box')
+    local function test(proc)
+        t.assert_error(function() netbox.self:call(proc) end)
+    end
+
+    test('')
+    test('.')
+    test(':')
+    test('[')
+    test(']')
+    test('[]')
+    test('a.')
+    test('l:')
+    test('a.b.')
+    test('a[b]')
+    test('a[[]')
+    test('a[[777]')
+    test('a["b]')
+    test('a["b\']')
+    test('a[\'b]')
+    test('a[\'b"]')
+    test('a[\'\']')
+    test('a[""]')
+    test('a[\'\']')
+    test('a["b""]')
+    test('a["b"\']')
+    test('a[\'b"\']')
+    test('a["b\'"]')
+    test('a[333]:')
+    test('a[333]:ping:')
+    test('a:[333]:ping:')
+    test('a:[333]:')
+    test('a[555].')
+    test('a[555].')
+    test('a[777].[666]')
+    test('a[777]d[444]')
+    test('a[777].d.[444]')
+    test('a[777][666]e')
+    test('a[555')
+    test('a[555]..')
+    test('a[555]..')
+    test('a[777]..[666]')
+    test('a[777].][666]')
+    test('a]555[')
+    test('a]555]')
+    test('a]]')
+    test('a[[555]')
+    test('a[[555]]')
+    test('a.b[c]')
+end
-- 
GitLab