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:
- Zero Server Cost: The logic engine runs entirely on client CPU cycles. The server only needs to host static files (HTML, CSS, JS, WASM).
- Sub-millisecond Latency: Queries execute locally in microsecond loops, allowing interface elements to update instantly as users toggle options.
- 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.

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:
- WASM Initialization: The browser loads
swipl-web.js(from a CDN or local assets), which instantiates the SWI-Prolog engine. - Virtual FS Write: The application fetches the text of
rules.pland writes it to Emscripten’s virtual memory filesystem (e.g., at/rules.pl). - Engine Consultation: JavaScript queries the engine to run
consult('/rules.pl'). - 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.

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.