Skip to content

Commit 0396d29

Browse files
authored
Merge pull request #148 from engboris/playground
Add web playground
2 parents 82d1642 + 3b8ad1d commit 0396d29

File tree

10 files changed

+851
-1
lines changed

10 files changed

+851
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ _build/*
22
*.cmi
33
*.cmo
44
_opam/
5+
6+
# Web playground deployment directory
7+
web_deploy/

dune-project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
(name stellogen)
2020
(synopsis "Stellogen is a minimalistic and logic-agnostic programming
2121
language based on term unification.")
22-
(depends base menhir sedlex ppx_deriving)
22+
(depends base menhir sedlex ppx_deriving js_of_ocaml js_of_ocaml-ppx)
2323
(tags
2424
("transcendental syntax" "logic programming" "constraint programming" "resolution logic" "unification" "self-assembly")))
2525

src/sgen_parsing.ml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,22 @@ let preprocess_with_imports (source_file : string) (raw_exprs : Expr.Raw.t list)
276276

277277
(* Phase 2: Preprocess with the macro environment *)
278278
Expr.preprocess_with_macro_env macro_env raw_exprs
279+
280+
(* ---------------------------------------
281+
String-based API for Web Playground
282+
--------------------------------------- *)
283+
284+
let create_start_pos_for_string () =
285+
{ Lexing.pos_fname = "<playground>"; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 }
286+
287+
(* Parse from string instead of file *)
288+
let parse_from_string (code : string) : Expr.Raw.t list =
289+
let lexbuf = Sedlexing.Utf8.from_string code in
290+
Sedlexing.set_position lexbuf (create_start_pos_for_string ());
291+
parse_with_error "<playground>" lexbuf
292+
293+
(* Preprocess without file imports (for web playground) *)
294+
let preprocess_without_imports (raw_exprs : Expr.Raw.t list) :
295+
Expr.expr Expr.loc list =
296+
(* Just expand macros defined in the code itself, no file imports *)
297+
Expr.preprocess_with_macro_env [] raw_exprs

src/web_interface.ml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
open Base
2+
open Lsc_pretty
3+
open Sgen_ast
4+
5+
(* Global output buffer *)
6+
let output_buffer : string list ref = ref []
7+
8+
let add_output s = output_buffer := s :: !output_buffer
9+
10+
let get_output () = String.concat ~sep:"\n" (List.rev !output_buffer)
11+
12+
let clear_output () = output_buffer := []
13+
14+
(* Simple show implementation that writes to buffer *)
15+
let show_to_buffer constellation =
16+
let output = string_of_constellation constellation in
17+
add_output output
18+
19+
(* Simplified eval that uses buffer for output *)
20+
let eval_program_with_buffer (p : program) =
21+
clear_output ();
22+
23+
let rec eval_term env = function
24+
| Sgen_ast.Show expr -> (
25+
(* Evaluate and print to buffer *)
26+
match Sgen_eval.eval_sgen_expr env expr with
27+
| Ok (env', constellation) ->
28+
show_to_buffer (List.map constellation ~f:Lsc_ast.Marked.remove);
29+
Ok env'
30+
| Error e -> Error e )
31+
| term -> (
32+
(* For all other terms, use standard eval but discard constellation result *)
33+
match Sgen_eval.eval_sgen_expr env term with
34+
| Ok (env', _) -> Ok env'
35+
| Error e -> Error e )
36+
in
37+
38+
let rec eval_program_internal env = function
39+
| [] -> Ok env
40+
| term :: rest -> (
41+
match eval_term env term with
42+
| Ok env' -> eval_program_internal env' rest
43+
| Error e -> Error e )
44+
in
45+
46+
match eval_program_internal Sgen_ast.initial_env p with
47+
| Ok _env -> Ok ()
48+
| Error e -> Error e
49+
50+
(* Run Stellogen code from a string and return output *)
51+
let run_from_string (code : string) : (string, string) Result.t =
52+
try
53+
(* Parse from string *)
54+
let raw_exprs = Sgen_parsing.parse_from_string code in
55+
56+
(* Preprocess without imports *)
57+
let preprocessed = Sgen_parsing.preprocess_without_imports raw_exprs in
58+
59+
(* Convert to program *)
60+
match Expr.program_of_expr preprocessed with
61+
| Error (expr_error, loc) -> (
62+
match Sgen_eval.pp_err (Sgen_ast.ExprError (expr_error, loc)) with
63+
| Ok error_msg -> Error error_msg
64+
| Error _ -> Error "Parse error" )
65+
| Ok program -> (
66+
(* Evaluate with buffered output *)
67+
match eval_program_with_buffer program with
68+
| Ok () -> Ok (get_output ())
69+
| Error err -> (
70+
match Sgen_eval.pp_err err with
71+
| Ok error_msg ->
72+
let output = get_output () in
73+
if String.is_empty output then Error error_msg
74+
else Error (output ^ "\n" ^ error_msg)
75+
| Error _ -> Error "Evaluation error" ) )
76+
with
77+
| Failure msg -> Error ("Error: " ^ msg)
78+
| exn -> Error ("Exception: " ^ Exn.to_string exn)

stellogen.opam

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ depends: [
2323
"menhir"
2424
"sedlex"
2525
"ppx_deriving"
26+
"js_of_ocaml"
27+
"js_of_ocaml-ppx"
2628
"odoc" {with-doc}
2729
]
2830
build: [

web/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Stellogen Web Playground
2+
3+
A browser-based playground for experimenting with Stellogen code, compiled to JavaScript using js_of_ocaml.
4+
5+
## Features
6+
7+
- **No installation required** - runs entirely in the browser
8+
- **Instant feedback** - see results immediately
9+
- **Example programs** - learn from built-in examples
10+
- **Keyboard shortcuts** - Ctrl/Cmd+Enter to run code
11+
- **Clean interface** - dark theme, syntax-aware editor
12+
13+
## Building the Playground
14+
15+
### Prerequisites
16+
17+
Install the required OCaml packages:
18+
19+
```bash
20+
opam install js_of_ocaml js_of_ocaml-compiler js_of_ocaml-ppx
21+
```
22+
23+
### Build Steps
24+
25+
1. **Build the JavaScript file:**
26+
27+
```bash
28+
# From the project root
29+
dune build web/playground.bc.js
30+
```
31+
32+
This will create:
33+
- `_build/default/web/playground.bc.js` - The compiled JavaScript
34+
35+
2. **Copy files to serve:**
36+
37+
```bash
38+
# Create a deploy directory
39+
mkdir -p web_deploy
40+
cp _build/default/web/playground.bc.js web_deploy/playground.js
41+
cp web/index.html web_deploy/
42+
```
43+
44+
3. **Serve locally (for testing):**
45+
46+
```bash
47+
# Using Python
48+
cd web_deploy
49+
python3 -m http.server 8000
50+
51+
# Or using any other HTTP server
52+
# Then open http://localhost:8000 in your browser
53+
```
54+
55+
### Quick Build Script
56+
57+
You can also use this one-liner:
58+
59+
```bash
60+
dune build web/playground.bc.js && mkdir -p web_deploy && cp _build/default/web/playground.bc.js web_deploy/playground.js && cp web/index.html web_deploy/ && echo "Build complete! Serve the web_deploy/ directory."
61+
```
62+
63+
## Deployment
64+
65+
The web playground is entirely static (client-side only), so it can be deployed to any static hosting service:
66+
67+
- **GitHub Pages**: Push `web_deploy/` contents to gh-pages branch
68+
- **Netlify**: Drag-and-drop the `web_deploy/` folder
69+
- **Vercel**: Deploy the `web_deploy/` directory
70+
- **Any web server**: Copy files to your public directory
71+
72+
### GitHub Pages Example
73+
74+
```bash
75+
# Build
76+
dune build web/playground.bc.js
77+
78+
# Copy to deploy folder
79+
mkdir -p docs
80+
cp _build/default/web/playground.bc.js docs/playground.js
81+
cp web/index.html docs/
82+
83+
# Commit and push
84+
git add docs/
85+
git commit -m "Update web playground"
86+
git push
87+
88+
# Enable GitHub Pages to serve from /docs directory in repository settings
89+
```
90+
91+
## Architecture
92+
93+
```
94+
web/
95+
├── playground.ml # OCaml entry point with js_of_ocaml exports
96+
├── index.html # Web UI (editor + output panel)
97+
├── dune # Build configuration
98+
└── README.md # This file
99+
100+
src/
101+
├── web_interface.ml # String-based API for browser
102+
├── sgen_parsing.ml # Parser with string input support
103+
└── ... # Core Stellogen implementation
104+
```
105+
106+
### How It Works
107+
108+
1. **OCaml to JavaScript**: `playground.ml` is compiled to JavaScript using js_of_ocaml
109+
2. **API Export**: The `Stellogen.run()` function is exported to JavaScript
110+
3. **Web UI**: `index.html` provides the editor and calls `Stellogen.run()`
111+
4. **String-based**: All parsing and evaluation works on strings (no file I/O)
112+
5. **Output Capture**: Results are captured to a buffer instead of stdout
113+
114+
## Limitations
115+
116+
### Not Supported in Web Playground
117+
118+
- **File imports**: `(use "file.sg")` and `(use-macros "file.sg")` don't work
119+
- Reason: No filesystem in browser
120+
- Workaround: Copy/paste code directly into the editor
121+
122+
- **External libraries**: Can't load external .sg files
123+
- Workaround: Include all code in the editor
124+
125+
### Supported Features
126+
127+
✅ All core Stellogen features:
128+
- Definitions (`:=`)
129+
- Macros (`macro`)
130+
- Show output (`show`)
131+
- Assertions (`==`, `~=`)
132+
- Constellations, rays, stars
133+
- Interactions (`interact`, `fire`)
134+
- Process chaining (`process`)
135+
- Focus (`@`), calls (`#`)
136+
137+
## File Size Optimization
138+
139+
The generated JavaScript can be large (~5-10 MB). To optimize:
140+
141+
### 1. Use js_of_ocaml optimization flags
142+
143+
```bash
144+
# Edit web/dune to add optimization
145+
(executable
146+
(name playground)
147+
(modes js)
148+
(libraries stellogen js_of_ocaml)
149+
(preprocess (pps js_of_ocaml-ppx))
150+
(js_of_ocaml (flags (:standard --opt 3 --disable genprim))))
151+
```
152+
153+
### 2. Enable gzip compression
154+
155+
Most web servers automatically compress .js files, reducing size by ~70%.
156+
157+
### 3. Split loading (advanced)
158+
159+
For very large files, consider:
160+
- Lazy loading the compiler on first run
161+
- Web Workers for compilation
162+
- IndexedDB caching
163+
164+
## Development
165+
166+
### Testing Changes
167+
168+
After modifying `web_interface.ml` or `playground.ml`:
169+
170+
```bash
171+
# Rebuild
172+
dune build web/playground.bc.js
173+
174+
# Copy to deploy folder
175+
cp _build/default/web/playground.bc.js web_deploy/playground.js
176+
177+
# Reload browser (hard refresh: Ctrl+Shift+R)
178+
```
179+
180+
### Debugging
181+
182+
Open browser developer console (F12) to see:
183+
- JavaScript errors
184+
- `console.log()` output (add to `playground.ml`)
185+
- Network requests
186+
187+
Add debug output in `playground.ml`:
188+
189+
```ocaml
190+
let run_stellogen code_js =
191+
Firebug.console##log (Js.string "Running code...");
192+
(* ... *)
193+
```
194+
195+
## Browser Compatibility
196+
197+
Tested on:
198+
- Chrome 90+
199+
- Firefox 88+
200+
- Safari 14+
201+
- Edge 90+
202+
203+
Requires:
204+
- JavaScript enabled
205+
- ~10 MB available memory
206+
207+
## Contributing
208+
209+
To add features to the playground:
210+
211+
1. Modify `src/web_interface.ml` for core functionality
212+
2. Update `web/playground.ml` for JavaScript exports
213+
3. Enhance `web/index.html` for UI improvements
214+
4. Test in multiple browsers
215+
5. Update this README
216+
217+
## License
218+
219+
Same as Stellogen: GPL-3.0-only
220+
221+
## Links
222+
223+
- [Stellogen Repository](https://github.com/engboris/stellogen)
224+
- [js_of_ocaml Documentation](https://ocsigen.org/js_of_ocaml/)
225+
- [Report Issues](https://github.com/engboris/stellogen/issues)

web/build.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
# Build script for Stellogen Web Playground
3+
4+
set -e
5+
6+
echo "Building Stellogen Web Playground..."
7+
echo
8+
9+
# Build the JavaScript
10+
echo "Step 1: Compiling OCaml to JavaScript..."
11+
dune build web/playground.bc.js
12+
13+
# Create deploy directory
14+
echo "Step 2: Creating deployment directory..."
15+
mkdir -p web_deploy
16+
17+
# Copy files
18+
echo "Step 3: Copying files..."
19+
cp _build/default/web/playground.bc.js web_deploy/playground.js
20+
cp web/index.html web_deploy/
21+
22+
# Get file size
23+
JS_SIZE=$(du -h web_deploy/playground.js | cut -f1)
24+
25+
echo
26+
echo "✅ Build complete!"
27+
echo
28+
echo "Generated files:"
29+
echo " - web_deploy/playground.js ($JS_SIZE)"
30+
echo " - web_deploy/index.html"
31+
echo
32+
echo "To test locally:"
33+
echo " cd web_deploy && python3 -m http.server 8000"
34+
echo " Then open http://localhost:8000"
35+
echo

0 commit comments

Comments
 (0)