`Too many stacked strings` bug in WASM, with repro example

I’m using swipl compiled as a WASM package, that I include in a js project. I was getting this error:

01f4a08e:0xe351e [FATAL ERROR: at Fri Aug  8 08:57:17 2025
01f4a08e:0xe351e 	Too many stacked strings]

and I narrowed it down to a memory leak in the prolog.query function. Here is some reproduction and some thoughts.

Minimal reproduction:

say(Thing) :-
    format(string(Command), `console.log("~w")`, [Thing]),
    js_run_script(Command).

adhoc(Result) :-
    findall(_{id: X, label:'some_long_label'}, between(1, 100000, X), Result),
    say(Result),
    say('adhoc').

On the javascript side, this is called as:

<button
  class=“button”
  onclick={() => {
    let r = prolog.query(‘adhoc(Rs)’).once();
    console.log(r);
  }}>ad hoc</button>

The code I just posted doesn’t work, and it results in the Too many stacked strings error after 4 times you press the adhoc button. Or, if you increase the number to 400000, in just 1 press.

Now, I understand there could be an overall limit in the strings you transfer, but I doubt this should be cumulative. If the number is 100000, once the first invocation is completed, memory should be released.

A version that works

If we don’t want communication via prolog.query, the code works (adhoc has arity 0 now):

say(Thing) :-
    format(string(Command), `console.log("~w")`, [Thing]),
    js_run_script(Command).

adhoc :-
    findall(_{id: X, label:'some_long_label'}, between(1, 100000, X), Result),
    say(Result),
    say('adhoc').
<button
  class=“button”
  onclick={() => {
    prolog.query(‘adhoc()’).once();
  }}>ad hoc</button>

The thing I find interesting is that the logging I do from the prolog side (say(Result)) contains the same information as the communication through prolog.query, and yet the first always works while the second doesn’t.

@jan, @jeswr any thoughts about this? Who should I tag here that’s familiar with the WASM port?

It indicates a lack of using c('PL_STRINGS_MARK') at some place. If I recall well, there is also a JavaScript API around this. That needs to be added to the API that causes this. I’m traveling though and I do not have good access to look into this. Feel free to do some research :slight_smile:

Thank you @jan, I’ll respond here with some observations and questions I have while I read this part of the swipl codebase, which is new to me. Given your answer it seems to be that js_run_script works because it’s correctly bracketed between PL_STRINGS_MARK and PL_STRINGS_RELEASE, here.

Also, if those functions are wrapped on the js side, I think they could be in prolog.js, and probably under __bind_foreign_function here. But that doesn’t seem to be the case, could they be elsewhere? Or should I try to bind them with something like:

PL_strings_mark: this.module.cwrap( 'PL_strings_mark', ???, ???),
PL_strings_release: this.module.cwrap( 'PL_strings_release', ???, ???),

I’m not sure that it makes sense since these are macros defined as:

#define PL_STRINGS_MARK() \
	{ buf_mark_t __PL_mark; \
	  PL_mark_string_buffers(&__PL_mark);
#define PL_STRINGS_RELEASE() \
	  PL_release_string_buffers_from_mark(__PL_mark); \
	}

Then there’s the question of where this bracketing should be used. I guess the relevant function is query2 here, but I’m not sure where bracketing should be put, around all of it? Who is allocating the strings here?

Edit: I guess in another part of the codebase we have:

#define	PL_mark_string_buffers(mark)  LDFUNC(PL_mark_string_buffers, mark)
#define	PL_release_string_buffers_from_mark(mark) LDFUNC(PL_release_string_buffers_from_mark, mark)

which are the ingredients of the PL_STRINGS_MARK and PL_STRINGS_RELEASE macros, and I can probably cwrap those. Still, I’m not sure where that mark parameter should come from.

The original idea was that functions returning a char* from C put these in a ring, which implies they are recovered but you need to copy them away if you allocate more than the ring size in between. This was of course a little naive and in the end not a good idea. So, now strings returned from Prolog are stacked and you can wrap an arbitrary block of code in mark/release and be sure all temporary strings created inside the block are released.

I thought the Javascript API was using this, but I’m afraid it does not and I mixed up in my head with the Python one. Probably the best idea is to define a Javascript API for that and use it wherever you get a string from Prolog. If you like sorting it out, please do! Basically the mark gives you a handle and the release reclaims all strings created after the mark.