Skip to content

Commit d9e24e4

Browse files
committed
compiler: add function forward declaration syntax
Add `function foo;` as a forward declaration that reserves a constant slot (initialized to null), later filled by a matching `function foo() {}` definition. This enables mutual recursion between functions at the same scope level without the `let foo; foo = function() {};` workaround. Forward-declared bindings are constant, preventing reassignment and redeclaration. Duplicate forward declarations, forward declarations after definitions, and unfilled stubs called at runtime are all handled with appropriate error messages. Signed-off-by: Felix Fietkau <nbd@nbd.name>
1 parent 552ca3c commit d9e24e4

File tree

3 files changed

+346
-5
lines changed

3 files changed

+346
-5
lines changed

compiler.c

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,13 @@ uc_compiler_declare_local(uc_compiler_t *compiler, uc_value_t *name, bool consta
874874
len2 = ucv_string_length(locals->entries[i - 1].name);
875875

876876
if (len1 == len2 && !strcmp(str1, str2)) {
877+
if (locals->entries[i - 1].funcstub) {
878+
uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
879+
"Variable '%s' redeclared", str2);
880+
881+
return -1;
882+
}
883+
877884
if (uc_compiler_is_strict(compiler)) {
878885
uc_compiler_syntax_error(compiler, 0, "Variable '%s' redeclared", str2);
879886

@@ -937,6 +944,31 @@ uc_compiler_resolve_local(uc_compiler_t *compiler, uc_value_t *name, bool *const
937944
return -1;
938945
}
939946

947+
static ssize_t
948+
uc_compiler_resolve_funcstub(uc_compiler_t *compiler, uc_value_t *name)
949+
{
950+
uc_locals_t *locals = &compiler->locals;
951+
const char *str1, *str2;
952+
size_t i, len1, len2;
953+
954+
str1 = ucv_string_get(name);
955+
len1 = ucv_string_length(name);
956+
957+
for (i = locals->count; i > 0; i--) {
958+
if (locals->entries[i - 1].depth != -1 &&
959+
locals->entries[i - 1].depth < (ssize_t)compiler->scope_depth)
960+
break;
961+
962+
str2 = ucv_string_get(locals->entries[i - 1].name);
963+
len2 = ucv_string_length(locals->entries[i - 1].name);
964+
965+
if (len1 == len2 && !strcmp(str1, str2))
966+
return locals->entries[i - 1].funcstub ? (ssize_t)(i - 1) : -1;
967+
}
968+
969+
return -1;
970+
}
971+
940972
static ssize_t
941973
uc_compiler_add_upval(uc_compiler_t *compiler, size_t idx, bool local, uc_value_t *name, bool constant)
942974
{
@@ -1890,13 +1922,48 @@ uc_compiler_compile_funcexpr_common(uc_compiler_t *compiler, bool require_name)
18901922
if (uc_compiler_parse_match(compiler, TK_LABEL)) {
18911923
name = compiler->parser->prev.uv;
18921924

1893-
/* Named functions are syntactic sugar for local variable declaration
1894-
* with function value assignment. If a name token was encountered,
1895-
* initialize a local variable for it... */
1896-
slot = uc_compiler_declare_local(compiler, name, false);
1925+
if (require_name && uc_compiler_parse_check(compiler, TK_SCOL)) {
1926+
slot = uc_compiler_resolve_funcstub(compiler, name);
1927+
1928+
if (slot > -1) {
1929+
uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
1930+
"Function '%s' redeclared", ucv_string_get(name));
1931+
1932+
return;
1933+
}
1934+
1935+
slot = uc_compiler_declare_local(compiler, name, true);
18971936

1898-
if (slot == -1)
1937+
if (slot > -1) {
1938+
uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
1939+
"Variable '%s' redeclared", ucv_string_get(name));
1940+
1941+
return;
1942+
}
1943+
1944+
compiler->locals.entries[compiler->locals.count - 1].funcstub = true;
1945+
uc_compiler_emit_insn(compiler, compiler->parser->prev.pos, I_LNULL);
18991946
uc_compiler_initialize_local(compiler);
1947+
uc_compiler_parse_consume(compiler, TK_SCOL);
1948+
1949+
return;
1950+
}
1951+
1952+
slot = uc_compiler_resolve_funcstub(compiler, name);
1953+
1954+
if (slot > -1) {
1955+
compiler->locals.entries[slot].funcstub = false;
1956+
}
1957+
else {
1958+
slot = uc_compiler_declare_local(compiler, name, false);
1959+
1960+
if (slot == -1)
1961+
uc_compiler_initialize_local(compiler);
1962+
else if (compiler->locals.entries[slot].constant)
1963+
uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
1964+
"Redeclaration of constant '%s'",
1965+
ucv_string_get(name));
1966+
}
19001967
}
19011968
else if (require_name) {
19021969
uc_compiler_syntax_error(compiler, compiler->parser->curr.pos, "Expecting function name");

include/ucode/compiler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ typedef struct {
8383
size_t from;
8484
bool captured;
8585
bool constant;
86+
bool funcstub;
8687
} uc_local_t;
8788

8889
typedef struct {
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
The `function foo;` syntax allows forward-declaring a function name
2+
before providing its definition. This enables mutual recursion between
3+
functions at the same scope level.
4+
5+
6+
1. A forward-declared function can be defined and called normally.
7+
8+
-- Expect stdout --
9+
hello
10+
-- End --
11+
12+
-- Testcase --
13+
{%
14+
function greet;
15+
16+
function greet() {
17+
return "hello";
18+
}
19+
20+
print(greet(), "\n");
21+
%}
22+
-- End --
23+
24+
25+
2. Forward declarations enable mutual recursion.
26+
27+
-- Expect stdout --
28+
[
29+
true,
30+
true,
31+
true,
32+
true
33+
]
34+
-- End --
35+
36+
-- Testcase --
37+
{%
38+
function is_even;
39+
function is_odd;
40+
41+
function is_even(n) {
42+
if (n == 0) return true;
43+
return is_odd(n - 1);
44+
}
45+
46+
function is_odd(n) {
47+
if (n == 0) return false;
48+
return is_even(n - 1);
49+
}
50+
51+
printf("%.J\n", [
52+
is_even(0),
53+
is_odd(1),
54+
is_even(10),
55+
is_odd(11)
56+
]);
57+
%}
58+
-- End --
59+
60+
61+
3. Assignment to a forward-declared function is forbidden (constant binding).
62+
63+
-- Expect stderr --
64+
Syntax error: Invalid assignment to constant 'foo'
65+
In line 4, byte 8:
66+
67+
` foo = function() { return 2; };`
68+
^-- Near here
69+
70+
71+
-- End --
72+
73+
-- Testcase --
74+
{%
75+
function foo;
76+
77+
foo = function() { return 2; };
78+
%}
79+
-- End --
80+
81+
82+
4. A `let` declaration after a forward declaration is forbidden.
83+
84+
-- Expect stderr --
85+
Syntax error: Variable 'foo' redeclared
86+
In line 4, byte 6:
87+
88+
` let foo = 1;`
89+
^-- Near here
90+
91+
92+
-- End --
93+
94+
-- Testcase --
95+
{%
96+
function foo;
97+
98+
let foo = 1;
99+
%}
100+
-- End --
101+
102+
103+
5. A `const` declaration after a forward declaration is forbidden.
104+
105+
-- Expect stderr --
106+
Syntax error: Variable 'foo' redeclared
107+
In line 4, byte 8:
108+
109+
` const foo = 1;`
110+
^-- Near here
111+
112+
113+
-- End --
114+
115+
-- Testcase --
116+
{%
117+
function foo;
118+
119+
const foo = 1;
120+
%}
121+
-- End --
122+
123+
124+
6. Defining a function twice after forward declaration is forbidden.
125+
126+
-- Expect stderr --
127+
Syntax error: Redeclaration of constant 'foo'
128+
In line 5, byte 11:
129+
130+
` function foo() { return 2; }`
131+
Near here ---^
132+
133+
134+
-- End --
135+
136+
-- Testcase --
137+
{%
138+
function foo;
139+
140+
function foo() { return 1; }
141+
function foo() { return 2; }
142+
%}
143+
-- End --
144+
145+
146+
7. Duplicate forward declarations are forbidden.
147+
148+
-- Expect stderr --
149+
Syntax error: Function 'foo' redeclared
150+
In line 3, byte 11:
151+
152+
` function foo;`
153+
Near here ---^
154+
155+
156+
-- End --
157+
158+
-- Testcase --
159+
{%
160+
function foo;
161+
function foo;
162+
%}
163+
-- End --
164+
165+
166+
8. Forward declaration after a definition is forbidden.
167+
168+
-- Expect stderr --
169+
Syntax error: Variable 'foo' redeclared
170+
In line 3, byte 11:
171+
172+
` function foo;`
173+
Near here ---^
174+
175+
176+
-- End --
177+
178+
-- Testcase --
179+
{%
180+
function foo() { return 1; }
181+
function foo;
182+
%}
183+
-- End --
184+
185+
186+
9. Calling an unfilled forward declaration raises a runtime error.
187+
188+
-- Expect stderr --
189+
Type error: left-hand side is not a function
190+
In line 4, byte 6:
191+
192+
` foo();`
193+
^-- Near here
194+
195+
196+
-- End --
197+
198+
-- Testcase --
199+
{%
200+
function foo;
201+
202+
foo();
203+
%}
204+
-- End --
205+
206+
207+
10. A nested function can capture a forward-declared name as upvalue.
208+
209+
-- Expect stdout --
210+
hello from foo
211+
-- End --
212+
213+
-- Testcase --
214+
{%
215+
function foo;
216+
217+
function call_foo() {
218+
return foo();
219+
}
220+
221+
function foo() {
222+
return "hello from foo";
223+
}
224+
225+
print(call_foo(), "\n");
226+
%}
227+
-- End --
228+
229+
230+
11. Forward declarations work with export.
231+
232+
-- File test-forward-decl.uc --
233+
export function foo;
234+
235+
function foo() {
236+
return "exported";
237+
}
238+
-- End --
239+
240+
-- Args --
241+
-R
242+
-- End --
243+
244+
-- Expect stdout --
245+
exported
246+
-- End --
247+
248+
-- Testcase --
249+
import { foo } from "./files/test-forward-decl.uc";
250+
251+
print(foo(), "\n");
252+
-- End --
253+
254+
255+
12. Increment/decrement of a forward-declared function is forbidden.
256+
257+
-- Expect stderr --
258+
Syntax error: Invalid increment/decrement of constant 'foo'
259+
In line 4, byte 5:
260+
261+
` foo++;`
262+
^-- Near here
263+
264+
265+
-- End --
266+
267+
-- Testcase --
268+
{%
269+
function foo;
270+
271+
foo++;
272+
%}
273+
-- End --

0 commit comments

Comments
 (0)