Client-Side Prolog with WebAssembly

Traditionally, incorporating a Prolog-based reasoning engine into a web application required a backend server running SWI-Prolog or another dialect. The web front-end would communicate with this server via HTTP REST APIs, WebSockets, or a language-specific bridge like Janus in Python. While this is a standard design, it introduces server overhead, hosting costs, network latency, and requires an internet connection to function.

WebAssembly (WASM) changes this paradigm. By compiling the entire SWI-Prolog engine (written in C) into a highly optimized WASM binary, we can download and run Prolog directly in the user’s web browser.

This architecture offers several key advantages:

  1. Zero Server Cost: The logic engine runs entirely on client CPU cycles. The server only needs to host static files (HTML, CSS, JS, WASM).
  2. Sub-millisecond Latency: Queries execute locally in microsecond loops, allowing interface elements to update instantly as users toggle options.
  3. Offline Resilience: Once the static files are loaded, the expert system works without any network connection, making it suitable for progressive web applications (PWAs).

In this chapter, we explore SipLogic, a Wine Advisor expert system dashboard. It loads SWI-Prolog WASM, reads a local Prolog rule-base, and runs recommendations interactively on the client side.

Prolog WASM Web App Demo
Figure 21. Prolog WASM Web App Demo

Architecture of a WASM Prolog Application

The execution model inside the browser is simple. The browser downloads the SWI-Prolog WASM runtime and our expert system rule file. It then uses the Emscripten virtual file system to load the rules inside the compiled runtime, after which JavaScript communicates with the engine via queries:

  1. WASM Initialization: The browser loads swipl-web.js (from a CDN or local assets), which instantiates the SWI-Prolog engine.
  2. Virtual FS Write: The application fetches the text of rules.pl and writes it to Emscripten’s virtual memory filesystem (e.g., at /rules.pl).
  3. Engine Consultation: JavaScript queries the engine to run consult('/rules.pl').
  4. Interactive Query Loop: Whenever the user updates UI filters (food, body preference, sweetness), a JavaScript event listener triggers, constructing and executing a Prolog query. The engine returns bindings as native JavaScript objects.
Architecture diagram for the WebAssembly Prolog example
Figure 22. Architecture diagram for the WebAssembly Prolog example

Recommender System Logic

Our advisor is defined by a Prolog database containing wine facts, food pairing rules, and a recommendation predicate.

Here is the code in source-code/prolog_wasm_web/rules.pl:

 1 % rules.pl - Wine Recommendation Expert System for WASM
 2 
 3 % Database of Wines: wine(Name, Color, Body, Sweetness)
 4 wine(cabernet_sauvignon, red, full_body, dry).
 5 wine(merlot, red, medium_body, dry).
 6 wine(pinot_noir, red, light_body, dry).
 7 wine(chardonnay, white, full_body, dry).
 8 wine(sauvignon_blanc, white, medium_body, dry).
 9 wine(riesling, white, light_body, sweet).
10 wine(moscato, white, light_body, sweet).
11 wine(port, red, full_body, sweet).
12 wine(sauternes, white, full_body, sweet).
13 
14 % Pairing rules: pair(WineColor, FoodType)
15 pair(red, meat).
16 pair(red, cheese).
17 pair(white, fish).
18 pair(white, poultry).
19 pair(white, spicy_food).
20 pair(white, dessert).
21 pair(red, dessert).
22 
23 % Recommend a wine based on food, body preference, and sweetness
24 % preference.
25 % Returns Wine name, its Color, and a justification string.
26 recommend(Food, PreferredBody, PreferredSweetness, Wine, Color,
27     Explanation) :-
28     wine(Wine, Color, Body, Sweetness),
29     % Check food pairing compatibility
30     pair(Color, Food),
31     % Match preferences if specified (or allow any if 'any' is selected)
32     (PreferredBody == any ; Body == PreferredBody),
33     (PreferredSweetness == any ; Sweetness == PreferredSweetness),
34     % Generate a human-readable explanation
35     generate_explanation(Wine, Color, Body, Sweetness, Food,
36         Explanation).
37 
38 % Generate a beautiful explanation sentence
39 generate_explanation(Wine, Color, Body, Sweetness, Food, Explanation) :-
40     format(string(Explanation), 
41            "Because you are eating ~w, a ~w wine is a classic pairing. ~w is a ~w, ~w ~w wine that perfectly matches your taste preferences.",
42            [Food, Color, Wine, Body, Sweetness, Color]).

The recommendation logic matches the user’s food selection with compatible wine colors, verifies matching body and sweetness preferences (using the fallback term any), and calls format/3 to return a customized justification sentence.

* * *

JavaScript Integration

The JavaScript layer manages the lifecycle of the WASM engine: downloading the loader, fetching the rules, initializing the query handler, and responding to DOM input events.

Here is the implementation in source-code/prolog_wasm_web/app.js:

  1 let prologEngine = null;
  2 
  3 // DOM Elements
  4 const statusBadge = document.getElementById('statusBadge');
  5 const statusText = document.getElementById('statusText');
  6 const resultsContainer = document.getElementById('resultsContainer');
  7 const foodSelect = document.getElementById('foodSelect');
  8 const bodySelect = document.getElementById('bodySelect');
  9 const sweetnessSelect = document.getElementById('sweetnessSelect');
 10 
 11 // Initialize the SWI-Prolog WASM engine
 12 async function initProlog() {
 13     try {
 14         console.log("Initializing SWI-Prolog WASM...");
 15         
 16         // 1. Initialize SWIPL loader
 17         const swipl = await SWIPL({
 18             arguments: ["-q"],
 19             locateFile: (path) => `https://unpkg.com/swipl-wasm@latest/dist/swipl/${path}`
 20         });
 21         
 22         prologEngine = swipl.prolog;
 23         console.log("SWI-Prolog engine loaded. Fetching rules.pl...");
 24 
 25         // 2. Fetch local rules.pl content
 26         const response = await fetch('rules.pl');
 27         if (!response.ok) {
 28             throw new Error(`Failed to fetch rules.pl: ${response.statusText}`);
 29         }
 30         const rulesText = await response.text();
 31 
 32         // 3. Write rules.pl to Emscripten virtual filesystem
 33         swipl.FS.writeFile('/rules.pl', rulesText);
 34         console.log("rules.pl written to virtual FS. Consulting...");
 35 
 36         // 4. Consult the rules inside Prolog
 37         prologEngine.query("consult('/rules.pl').").once();
 38         console.log("Consult complete. Engine is online!");
 39 
 40         // 5. Update UI status
 41         statusBadge.classList.add('online');
 42         statusText.textContent = "Prolog WASM Online";
 43 
 44         // Enable inputs
 45         [foodSelect, bodySelect, sweetnessSelect].forEach(select => {
 46             select.disabled = false;
 47         });
 48 
 49         // Run initial recommendation
 50         runRecommendation();
 51 
 52         // Add event listeners
 53         [foodSelect, bodySelect, sweetnessSelect].forEach(select => {
 54             select.addEventListener('change', runRecommendation);
 55         });
 56 
 57     } catch (error) {
 58         console.error("Failed to initialize Prolog WASM:", error);
 59         statusText.textContent = "Error Loading Prolog";
 60         resultsContainer.innerHTML = `
 61             <div class="empty-state">
 62                 <span class="empty-icon">⚠️</span>
 63                 <p>Failed to initialize the SWI-Prolog engine.</p>
 64                 <p style="font-size: 0.85rem; color: var(--text-secondary);">${error.message}</p>
 65             </div>
 66         `;
 67     }
 68 }
 69 
 70 // Run query and display results
 71 function runRecommendation() {
 72     if (!prologEngine) return;
 73 
 74     const food = foodSelect.value;
 75     const body = bodySelect.value;
 76     const sweetness = sweetnessSelect.value;
 77 
 78     resultsContainer.innerHTML = '';
 79 
 80     // Construct Prolog query
 81     // Example: recommend('meat', 'full_body', 'dry', Wine, Color, Explanation).
 82     const queryStr = `recommend('${food}', '${body}', '${sweetness}', Wine, Color, Explanation).`;
 83     console.log("Executing Query:", queryStr);
 84 
 85     try {
 86         const query = prologEngine.query(queryStr);
 87         const recommendations = [];
 88 
 89         // Fetch all matching solutions
 90         let result = query.next();
 91         while (result && !result.done) {
 92             // Unpack variables (Prolog bindings are returned as JS values)
 93             // String values are decoded/retrieved
 94             const wine = formatPrologValue(result.value.Wine);
 95             const color = formatPrologValue(result.value.Color);
 96             const explanation = formatPrologValue(result.value.Explanation);
 97 
 98             recommendations.push({ wine, color, explanation });
 99             result = query.next();
100         }
101         query.close();
102 
103         // Display results
104         if (recommendations.length === 0) {
105             resultsContainer.innerHTML = `
106                 <div class="empty-state">
107                     <span class="empty-icon">🍷</span>
108                     <p>No perfect pairings found matching your specific preferences.</p>
109                     <p style="font-size: 0.85rem; color: var(--text-secondary);">Try selecting 'Any Body' or 'Any Sweetness' to expand choices.</p>
110                 </div>
111             `;
112         } else {
113             recommendations.forEach(rec => {
114                 const card = document.createElement('div');
115                 card.className = `wine-card ${rec.color}`;
116                 
117                 // Format wine name for presentation (replace underscores with spaces)
118                 const formattedName = rec.wine.replace(/_/g, ' ');
119 
120                 card.innerHTML = `
121                     <div class="wine-header">
122                         <h3 class="wine-name">${formattedName}</h3>
123                         <span class="wine-type-badge">${rec.color}</span>
124                     </div>
125                     <p class="wine-justification">${rec.explanation}</p>
126                 `;
127                 resultsContainer.appendChild(card);
128             });
129         }
130 
131     } catch (err) {
132         console.error("Query execution error:", err);
133         resultsContainer.innerHTML = `
134             <div class="empty-state">
135                 <span class="empty-icon">⚠️</span>
136                 <p>Query execution failed.</p>
137                 <p style="font-size: 0.85rem; color: var(--text-secondary);">${err.message}</p>
138             </div>
139         `;
140     }
141 }
142 
143 // Convert Prolog terms to clean JS strings
144 function formatPrologValue(val) {
145     if (typeof val === 'string') return val;
146     // Handle atoms represented as objects or arrays of codes
147     if (val && typeof val === 'object' && val.toString) {
148         return val.toString();
149     }
150     return String(val);
151 }
152 
153 // Start on page load
154 window.addEventListener('DOMContentLoaded', initProlog);
* * *

Running the Application Locally

Because modern web browsers block asynchronous network requests (such as fetch) when pages are loaded from local file paths (file://), you cannot test the project by double-clicking the index.html file. Instead, you must run a simple local HTTP server from the project directory.

Open a terminal, navigate to the source-code/prolog_wasm_web directory, and run the built-in Python HTTP server module:

1 $ python -m http.server 8000 --directory .

Then, open http://localhost:8000 in your web browser. You will see the status badge transition from red (Prolog Loading…) to a glowing emerald green (Prolog WASM Online). Changing any of the pairing selectors dynamically triggers queries that instantaneously update the recommended list.

* * *

Key Design Decisions

Loading from CDN vs. Self-Hosting WASM. In this example, the Emscripten JS and WASM assets are loaded via the unpkg CDN (https://unpkg.com/swipl-wasm@latest/dist/swipl/). For production applications, it is usually better to self-host these assets on your own web server or Content Delivery Network to avoid dependencies on external CDN infrastructure and to enforce Strict Content Security Policies (CSP).

File System Emulation. Emscripten maps virtual memory structures to regular file system logic. The call swipl.FS.writeFile('/rules.pl', rulesText) creates a virtual file that SWI-Prolog’s core consult predicate reads as if it were a physical file on disk. This is a powerful feature: it means you can reuse existing complex Prolog databases without modifying the Prolog codebase to load from strings.

The Query Lifecycle. Unlike a traditional long-running command-line loop, querying WASM Prolog in JavaScript uses an iterator pattern:

  • prologEngine.query(queryStr) returns an active query handle.
  • Calling .next() returns a dictionary of the variable bindings (e.g., { Wine: "merlot", Color: "red" }) or a state object indicating the query is complete.
  • Always call query.close() once you are done fetching results to free the internal memory structures allocated in the WASM heap.