]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
ucode: fix two compiler issues
authorFelix Fietkau <nbd@nbd.name>
Tue, 7 Jul 2026 08:31:42 +0000 (10:31 +0200)
committerFelix Fietkau <nbd@nbd.name>
Tue, 7 Jul 2026 08:46:12 +0000 (10:46 +0200)
- When working with deeply nested imports, compile errors led to long
  error messages or complete hangs by compiling the same module over
  and over again.
- Fix for a function expression scope issue.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/utils/ucode/patches/100-compiler-scope-named-function-expression-names-to-th.patch [new file with mode: 0644]
package/utils/ucode/patches/101-lib-avoid-allocating-print-buffer-before-NULL-check-.patch [new file with mode: 0644]
package/utils/ucode/patches/102-compiler-prevent-unbounded-recompilation-on-failed-m.patch [new file with mode: 0644]

diff --git a/package/utils/ucode/patches/100-compiler-scope-named-function-expression-names-to-th.patch b/package/utils/ucode/patches/100-compiler-scope-named-function-expression-names-to-th.patch
new file mode 100644 (file)
index 0000000..d788a7c
--- /dev/null
@@ -0,0 +1,80 @@
+From: Felix Fietkau <nbd@nbd.name>
+Date: Mon, 6 Jul 2026 18:23:08 +0200
+Subject: [PATCH] compiler: scope named function expression names to their own
+ body
+
+A named function expression (e.g. `!function e(){...}()`) shares its parsing
+path with function declarations, and declared its name in the enclosing scope
+just as a declaration does. For an expression this is wrong: the name is only
+meant to be visible inside its own body for self-reference. Binding it in the
+enclosing scope leaked the name and, by pushing an extra local, shifted the
+slot count so the initialize_local for a let/const initialised by the
+expression marked the wrong slot -- the real variable stayed uninitialised and
+its first use raised "Can't access lexical declaration 'x' before
+initialization". Self-recursion was likewise broken.
+
+Bind the name in the enclosing scope only for declarations. For a named
+expression, rename the function's own callee slot to the name after
+initialising its compiler, so the body can reference itself while the enclosing
+scope is left untouched.
+
+Signed-off-by: Felix Fietkau <nbd@nbd.name>
+---
+
+--- a/compiler.c
++++ b/compiler.c
+@@ -1949,20 +1949,27 @@ uc_compiler_compile_funcexpr_common(uc_c
+                       return;
+               }
+-              slot = uc_compiler_resolve_funcstub(compiler, name);
++              /* A function declaration binds its name in the enclosing scope. A named
++               * function expression must not: its name is only visible inside its own
++               * body (for self-reference), and declaring it in the enclosing scope
++               * would both leak it and shift the local slots, corrupting the
++               * initialisation state of a `let`/`const` the expression initialises. */
++              if (require_name) {
++                      slot = uc_compiler_resolve_funcstub(compiler, name);
+-              if (slot > -1) {
+-                      compiler->locals.entries[slot].funcstub = false;
+-              }
+-              else {
+-                      slot = uc_compiler_declare_local(compiler, name, false);
++                      if (slot > -1) {
++                              compiler->locals.entries[slot].funcstub = false;
++                      }
++                      else {
++                              slot = uc_compiler_declare_local(compiler, name, false);
+-                      if (slot == -1)
+-                              uc_compiler_initialize_local(compiler);
+-                      else if (compiler->locals.entries[slot].constant)
+-                              uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
+-                                      "Redeclaration of constant '%s'",
+-                                      ucv_string_get(name));
++                              if (slot == -1)
++                                      uc_compiler_initialize_local(compiler);
++                              else if (compiler->locals.entries[slot].constant)
++                                      uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
++                                              "Redeclaration of constant '%s'",
++                                              ucv_string_get(name));
++                      }
+               }
+       }
+       else if (require_name) {
+@@ -1976,6 +1983,14 @@ uc_compiler_compile_funcexpr_common(uc_c
+               compiler->program,
+               uc_compiler_is_strict(compiler));
++      /* Bind a named function expression's name to its own callee slot so the
++       * body can reference itself for recursion, without touching the enclosing
++       * scope. Declarations keep the name in the enclosing scope (handled above). */
++      if (name && !require_name) {
++              ucv_put(fncompiler.locals.entries[0].name);
++              fncompiler.locals.entries[0].name = ucv_get(name);
++      }
++
+       fncompiler.parent = compiler;
+       fncompiler.parser = compiler->parser;
+       fncompiler.exprstack = compiler->exprstack;
diff --git a/package/utils/ucode/patches/101-lib-avoid-allocating-print-buffer-before-NULL-check-.patch b/package/utils/ucode/patches/101-lib-avoid-allocating-print-buffer-before-NULL-check-.patch
new file mode 100644 (file)
index 0000000..1902006
--- /dev/null
@@ -0,0 +1,31 @@
+From: Felix Fietkau <nbd@nbd.name>
+Date: Tue, 7 Jul 2026 09:34:16 +0200
+Subject: [PATCH] lib: avoid allocating print buffer before NULL check in
+ uc_error_message_indent
+
+Move the print buffer allocation after the guard that returns early on a
+NULL message, so no buffer is allocated on that path. This keeps the
+function leak-free when callers invoke it without having produced an error
+string.
+
+Signed-off-by: Felix Fietkau <nbd@nbd.name>
+---
+
+--- a/lib.c
++++ b/lib.c
+@@ -220,13 +220,14 @@ uc_error_context_format(uc_stringbuf_t *
+ void
+ uc_error_message_indent(char **msg) {
+-      uc_stringbuf_t *buf = xprintbuf_new();
++      uc_stringbuf_t *buf;
+       char *s, *p, *nl;
+       size_t len;
+       if (!msg || !*msg)
+               return;
++      buf = xprintbuf_new();
+       s = *msg;
+       len = strlen(s);
diff --git a/package/utils/ucode/patches/102-compiler-prevent-unbounded-recompilation-on-failed-m.patch b/package/utils/ucode/patches/102-compiler-prevent-unbounded-recompilation-on-failed-m.patch
new file mode 100644 (file)
index 0000000..02590f6
--- /dev/null
@@ -0,0 +1,53 @@
+From: Felix Fietkau <nbd@nbd.name>
+Date: Mon, 6 Jul 2026 11:12:32 +0200
+Subject: [PATCH] compiler: prevent unbounded recompilation on failed
+ module imports
+
+A source's exports.offset of -1 marks a module whose compilation has been
+entered but not completed. When a module failed to compile, uc_compiler_finish
+freed its function and the error cascaded up through the importers via error
+recovery, leaving each involved source at offset -1 with its function gone. The
+re-entry guard only recognised an in-progress module by finding its function, so
+those freed modules were recompiled again on every subsequent import. Across a
+wide import graph this recompilation multiplies and the compiler appears to hang.
+
+Detect the offset -1 marker directly instead of relying on the presence of a
+function: report a circular dependency when the function is still around, and
+propagate the failure otherwise, rather than recursing into the module again.
+
+Signed-off-by: Felix Fietkau <nbd@nbd.name>
+---
+
+--- a/compiler.c
++++ b/compiler.c
+@@ -3479,15 +3479,26 @@ uc_compiler_compile_module_source(uc_com
+       uc_program_function_foreach(compiler->program, fn) {
+               if (uc_program_function_source(fn) == source) {
+-                      if (source->exports.offset == (size_t)-1)
+-                              uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
+-                                      "Circular dependency");
+-
+                       loaded = true;
+                       break;
+               }
+       }
++      /* An offset of -1 marks a source whose compilation was entered but not yet
++       * completed. Re-entering it is either a circular dependency (its function is
++       * still present) or a module whose earlier compilation failed and freed its
++       * function. Recompiling in either case recurses without bound across a large
++       * import graph, so report the cycle and propagate the failure instead. */
++      if (source->exports.offset == (size_t)-1) {
++              if (loaded)
++                      uc_compiler_syntax_error(compiler, compiler->parser->prev.pos,
++                              "Circular dependency");
++              else if (errp)
++                      xasprintf(errp, "Module previously failed to compile\n");
++
++              return false;
++      }
++
+       if (!loaded) {
+               /* We do not yet support linking precompiled modules at compile time,
+                  turn static import operation into dynamic load one. */