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