`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.

Unfortunately I don’t have for now the toolchain to compile the wasm part (I’m on nixos and I’m waiting on an upstream bug for emscripten with zlib), so I won’t be able to address this shortly (also, I’m not too familiar with the c part of the codebase). So if anyone else wants to jump in, please feel free as this is a fairly significant bug for the wasm port (it means that if you use query, after some time your program will crash).

Behind my desktop I could figure it out. It is fixed with 9343a4d38cc7d943f18c55125f993bdd915b2b96 and 4954dfa961c2d70f26e990d90815289c373e9e0e

I guess that will go to the npm version with the next release.

1 Like

Thank you @jan for the fix. I didn’t have a suitable build method at the time but now I’m using the npm-swipl-wasm repo, point it at a fork of swipl-devel, build it from there, then splice the dist directory in the npm-modules folder of my project.

So I have been able to test the change, and I think I still have the same error as the beginning of the thread (what am I missing?). When I was looking at the code before your fix I had this question: how do you know that the function to patch is get_chars and not some variant of query?

Either we test something different or your attempt to update failed. Surely, the problem reproduced for me and vanished after applying the patch. Attached is the code I used. Possibly you can compare that with your code and/or upload a complete example we can simply run to verify your findings.

strings.html.log (1.4 KB)

In theory, any API that internally manages temporary strings could be subject. If all is right, API calls that do not return a string should be protected internally. I’m not 100% sure this is the case everywhere. The problem is that these issues only show up if overall control of the application is not in Prolog. This particular case is surely subject though as it requests an UTF-8 string for an atom. A quick search through prolog.js didn’t reveal any other suspects, but I may have missed one.

Thank you @jan, you were right; I tested your code, that worked, then traced back my problems to bad caching of the vite tool (bundler for my js project). I was unaware that there was a layer of caching there at all.

Anyway, it seems to me that the problem is gone now :partying_face: .