Skip to content

Commit e79ef0d

Browse files
committed
Restore js_of_ocaml web interpreter
1 parent 186c56a commit e79ef0d

File tree

12 files changed

+1263
-21
lines changed

12 files changed

+1263
-21
lines changed

.gitattributes

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
*.md linguist-documentation
44
*.hints linguist-generated
55

6-
**/package-lock.json linguist-generated
7-
**/yarn.lock linguist-generated
6+
**/package-lock.json linguist-generated -diff
7+
**/yarn.lock linguist-generated -diff

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ jobs:
112112
- name: Validate Python runtime
113113
if: ${{ always() }}
114114
run: cd /home/ocaml/catala && opam exec -- make validate-py-runtime
115+
- name: Test web interpreter (jsoo)
116+
if: ${{ always() }}
117+
run: |
118+
cd /home/ocaml/catala
119+
opam exec -- dune build compiler/catala_web_interpreter.bc.js
120+
node compiler/test_web_interpreter.js
115121
- name: Test Summary
116122
uses: test-summary/action@v2
117123
with:

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ CLERK_TEST=$(CLERK_BIN) test --exe $(CATALA_BIN) \
208208
unit-tests: .FORCE
209209
dune build @for-tests @runtest
210210

211+
web-interpreter-tests: .FORCE
212+
dune build compiler/catala_web_interpreter.bc.js
213+
node compiler/test_web_interpreter.js
214+
211215
BACKEND_TEST_DIRS = arithmetic array bool date dec default enum exception func io money monomorphisation name_resolution parsing scope struct tuples typing variable_state
212216

213217
BACKEND_TESTS = $(wildcard $(BACKEND_TEST_DIRS:%=tests/%/good/*.catala_*))

catala.opam

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ depends: [
4242
"uucp" {>= "10"}
4343
"ubase" {>= "0.05"}
4444
"zarith" {>= "1.12"}
45+
"zarith_stubs_js" {>= "v0.17.0"}
46+
"js_of_ocaml" {>= "6.0"}
47+
"js_of_ocaml-ppx" {>= "6.0"}
4548
"yojson" {>= "2.1.0" & < "3"}
4649
# Not a strict upper bound for catala, but other tools (adgen) have it and we
4750
# don't want them to need a catala recompile

compiler/catala_utils/message.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ module Content = struct
182182
let of_string (s : string) : t =
183183
[MainMessage (fun ppf -> Format.pp_print_text ppf s)]
184184

185+
let get_positions (content : t) : (Pos.t * message option) list =
186+
List.filter_map
187+
(function
188+
| Position { pos; pos_message } -> Some (pos, pos_message) | _ -> None)
189+
content
190+
185191
let basic_msg ?header ppf target content =
186192
let pp_header ppf = Option.iter (Format.fprintf ppf " %s: ") header in
187193
Format.pp_open_vbox ppf 0;

compiler/catala_utils/message.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ module Content : sig
5252
val add_suggestion : t -> string list -> t
5353
val add_position : t -> ?message:message -> Pos.t -> t
5454

55+
val get_positions : t -> (Pos.t * message option) list
56+
(** Extract all positions from the content *)
57+
5558
(** {2 Content emission}*)
5659

5760
val emit_n :

compiler/catala_web.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env node
2+
// CLI wrapper for the Catala web interpreter
3+
// Mimics a subset of the catala CLI for testing purposes
4+
//
5+
// Usage:
6+
// node catala_web.js interpret FILE -s SCOPE
7+
// node catala_web.js test-scope SCOPE FILE
8+
// node catala_web.js Typecheck FILE
9+
10+
const fs = require('fs');
11+
const path = require('path');
12+
13+
// Load the interpreter
14+
const interpreterPath = path.join(__dirname, '../_build/default/compiler/catala_web_interpreter.bc.js');
15+
if (!fs.existsSync(interpreterPath)) {
16+
console.error('Error: Interpreter not found. Run: dune build compiler/catala_web_interpreter.bc.js');
17+
process.exit(1);
18+
}
19+
eval(fs.readFileSync(interpreterPath, 'utf8'));
20+
21+
// Parse arguments
22+
const args = process.argv.slice(2);
23+
if (args.length === 0) {
24+
console.error('Usage: catala_web.js <command> [options] <file>');
25+
console.error('Commands: interpret, test-scope, Typecheck');
26+
process.exit(1);
27+
}
28+
29+
const command = args[0];
30+
31+
// Detect language from file extension
32+
function detectLanguage(file) {
33+
if (file.endsWith('.catala_en')) return 'en';
34+
if (file.endsWith('.catala_fr')) return 'fr';
35+
if (file.endsWith('.catala_pl')) return 'pl';
36+
return 'en'; // default
37+
}
38+
39+
// Extract test scopes from file (scopes with #[test] attribute)
40+
function extractTestScopes(content) {
41+
const regex = /#\[test\]\s*declaration\s+scope\s+(\w+)/g;
42+
const scopes = [];
43+
let match;
44+
while ((match = regex.exec(content)) !== null) {
45+
scopes.push(match[1]);
46+
}
47+
return scopes;
48+
}
49+
50+
// Format output like the CLI
51+
function formatOutput(scopeName, output) {
52+
if (!output.trim()) {
53+
return `┌─[RESULT]─\n│ Computation successful!\n└─`;
54+
}
55+
const lines = output.trim().split('\n');
56+
const formatted = lines.map(l => `│ ${l}`).join('\n');
57+
return `┌─[RESULT]─ ${scopeName} ─\n${formatted}\n└─`;
58+
}
59+
60+
// Main logic
61+
let file, scope, lang;
62+
63+
switch (command) {
64+
case 'interpret':
65+
case 'Interpret': {
66+
// catala interpret FILE -s SCOPE
67+
const sIdx = args.indexOf('-s');
68+
if (sIdx === -1 || sIdx + 1 >= args.length) {
69+
console.error('Error: -s SCOPE required for interpret');
70+
process.exit(1);
71+
}
72+
scope = args[sIdx + 1];
73+
file = args.find((a, i) => i > 0 && a !== '-s' && args[i-1] !== '-s' && !a.startsWith('-'));
74+
if (!file) {
75+
console.error('Error: FILE required');
76+
process.exit(1);
77+
}
78+
lang = detectLanguage(file);
79+
const content = fs.readFileSync(file, 'utf8');
80+
const result = exports.interpret({
81+
files: { [path.basename(file)]: content },
82+
scope: scope,
83+
language: lang
84+
});
85+
if (result.success) {
86+
console.log(formatOutput(scope, result.output));
87+
} else {
88+
console.error(result.error);
89+
process.exit(1);
90+
}
91+
break;
92+
}
93+
94+
case 'test-scope': {
95+
// catala test-scope SCOPE FILE (legacy)
96+
// or catala test-scope FILE (runs all #[test] scopes)
97+
if (args.length < 2) {
98+
console.error('Error: FILE required');
99+
process.exit(1);
100+
}
101+
102+
// Check if second arg is a file or scope name
103+
if (args.length >= 3 && fs.existsSync(args[2])) {
104+
// catala test-scope SCOPE FILE
105+
scope = args[1];
106+
file = args[2];
107+
} else if (fs.existsSync(args[1])) {
108+
// catala test-scope FILE (run all #[test] scopes)
109+
file = args[1];
110+
scope = null;
111+
} else {
112+
// Assume catala test-scope SCOPE FILE
113+
scope = args[1];
114+
file = args[2];
115+
}
116+
117+
lang = detectLanguage(file);
118+
const content = fs.readFileSync(file, 'utf8');
119+
120+
const scopes = scope ? [scope] : extractTestScopes(content);
121+
if (scopes.length === 0) {
122+
console.error('Error: No test scopes found');
123+
process.exit(1);
124+
}
125+
126+
let hasError = false;
127+
for (const s of scopes) {
128+
const result = exports.interpret({
129+
files: { [path.basename(file)]: content },
130+
scope: s,
131+
language: lang
132+
});
133+
if (result.success) {
134+
console.log(formatOutput(s, result.output));
135+
} else {
136+
console.error(result.error);
137+
hasError = true;
138+
}
139+
}
140+
if (hasError) process.exit(1);
141+
break;
142+
}
143+
144+
case 'Typecheck':
145+
case 'typecheck': {
146+
file = args[1];
147+
if (!file) {
148+
console.error('Error: FILE required');
149+
process.exit(1);
150+
}
151+
lang = detectLanguage(file);
152+
const content = fs.readFileSync(file, 'utf8');
153+
const result = exports.typecheck({
154+
files: { [path.basename(file)]: content },
155+
language: lang
156+
});
157+
if (result.success) {
158+
console.log('┌─[RESULT]─\n│ Typechecking successful!\n└─');
159+
} else {
160+
console.error(result.error);
161+
process.exit(1);
162+
}
163+
break;
164+
}
165+
166+
default:
167+
console.error(`Unknown command: ${command}`);
168+
console.error('Supported: interpret, test-scope, Typecheck');
169+
process.exit(1);
170+
}

0 commit comments

Comments
 (0)