Hi Borislav,

thanks for reporting, and thanks for building the WASM playground — that is
a nice project!

Both bugs are fixed in SVN 2020.

Bug 1 (apl_exec exception safety): Command::process_line() can throw Error
for APL-level errors such as a top-level branch.  apl_exec() and
apl_exec_ucs() now wrap process_line() in a try/catch block and return the
appropriate LIBAPL_error code instead of propagating the exception.

Bug 2 (use after destruction in ScalarFunction.cc): The four explicit
destructor calls job_B->~PJob_scalar_B() and job_AB->~PJob_scalar_AB() end
the object's lifetime while the storage remains in Thread_context::joblist_*.
The subsequent next_job() call then copy-assigns into the dead object, which
is undefined behaviour — benign on x86 but fatal under WASM's typed
call_indirect.  Replaced with *job_B = PJob_scalar_B() (and the AB variant)
so that the object stays live and the Value_P members are released cleanly
by copy-assignment.

Your diagnosis and the proposed patches were exactly right.  Please do not
hesitate to report if you find more issues with the WASM build.

One note that may help for future builds: GNU APL ships the autoconf-generated
files (configure, Makefile.in, etc.) in the release tarballs, so it is not
necessary to run autoreconf or have the Autotools installed. Running
./configure directly from the tarball should be sufficient.

Best Regards,
Jürgen

P.S. The review and fixes for these bugs were developed with the assistance
of Claude Code (claude.ai/code) by Anthropic.


On 6/23/26 02:06, borislav wrote:
Hi

I wanted to learn APL and wanted to have a local wasm playground to do some 
exercises, so I asked Opus to compile gnu-apl-2.0 core (libapl) to wasm, in the 
process it found two issues:

1) apl_exec() not catching Command::process_line() which crashes the caller for 
top level error like `←3`, example code to reproduce from python:

```
import ctypes, sys
lib = ctypes.CDLL(sys.argv[1])
lib.init_libapl.argtypes = [ctypes.c_char_p, ctypes.c_int]
lib.apl_exec.argtypes    = [ctypes.c_char_p]
lib.init_libapl(b"apl", 0)
print(">>> apl_exec('2+2')"); sys.stdout.flush()
lib.apl_exec("2+2".encode("utf-8")); sys.stdout.flush()
print(">>> apl_exec('→3')  (a branch at the top level)"); sys.stdout.flush()
lib.apl_exec("→3".encode("utf-8")); sys.stdout.flush()   # →3
print(">>> PYTHON SURVIVED →3")   # only reached if apl_exec is exception-safe
```

It doesnt happen from the apl binary because it goes through other codepath.

The fix it did was basically wrapping apl_exec's process_line() in libapl.cc in 
try catch:
```
    try { Command::process_line(line_ucs, 0); }
    catch (Error & err)
       { if (err.get_print_loc() == 0) err.print_em(UERR, LOC);
         return LIBAPL_error(err.get_error_code()); }
    catch (...) { return LAE_UNKNOWN_ERROR; }
```

2) use after destruction ub

I dont know enough WASM to know if it is bullshitting me, but, it was 
corrupting the vtable and there were deterministic random crashes e.g. ÷2 works 
but  ÷3 crashes ÷4 works and so on.

The fix it did was:

```
do_scalar_B / do_scalar_AB end each job with an explicit destructor call
(job->~PJob_scalar_B()) on Thread_context::get_master().joblist_B's persistent
`current_job` member. The next call's next_job() then does
`current_job = jobs.back()` — copy-assignment INTO an object whose lifetime has
already ended. On native x86 this is benign (the Value_P members were just
cleared), but under WASM's typed call_indirect it corrupts a Cell vtable
pointer after a couple of reuses, trapping the 3rd monadic scalar call with
"null function or function signature mismatch".

Replacing the explicit destructor with assignment of a fresh empty job
releases ownership identically (PJob's destructor only clears its Value_P
members) while leaving `current_job` a valid, assignable object.
--- apl-2.0/src/ScalarFunction.cc
+++ apl-2.0/src/ScalarFunction.cc
@@ -191,7 +191,7 @@
                          ec = (cell_B.*fun)(&cell_Z);
                          if (ec != E_NO_ERROR)
                             {
-                             job_B->~PJob_scalar_B();   // ownership of B, and 
Z
+                             *job_B = PJob_scalar_B();   // ownership of B, 
and Z
                               job_B = 0;
                               return Value_P();
                             }
@@ -200,7 +200,7 @@
                   }
             }
          job_B->value_Z->check_value(LOC);
-        job_B->~PJob_scalar_B();   // give up ownership of B, and Z.
+        *job_B = PJob_scalar_B();   // give up ownership of B, and Z.
          job_B = 0;
        }

@@ -503,7 +503,7 @@
                          if (ec != E_NO_ERROR)
                             {
                               // give up the ownership of A, B, Z.
-                             job_AB->~PJob_scalar_AB();
+                             *job_AB = PJob_scalar_AB();
                               job_AB = 0;
                               return Value_P();
                             }
@@ -512,7 +512,7 @@
                   }
             }
          job_AB->value_Z->check_value(LOC);
-        job_AB->~PJob_scalar_AB();   // give up ownership of A, B, and Z.
+        *job_AB = PJob_scalar_AB();   // give up ownership of A, B, and Z.
          job_AB = 0;
        }
```


I dont know the code enough if this is the best way to fix this, or maybe just call 
job_B->value_B.reset();  job_B->value_Z.reset();..

Anyway, now the playground works and I can learn :) https://punkx.org/apl/ if 
you want to try it out. or build it yourself 
https://github.com/jackdoe/gnu-apl-wasm

Thank you for the hard work, and sorry if the bug report was not useful.
I am really not sure if I should've reported it at all given that it is AI 
generated, but I did test it and it does fix the WASM code.

PS: If you want to read its tokens: 
https://github.com/jackdoe/gnu-apl-wasm/tree/master/patches/bug-reports



  • gnu apl wasm: ... borislav
    • Re: gnu a... Dr . Jürgen Sauermann via Bugs and suggestions for GNU APL

Reply via email to