feat: add web playground with Docker sandbox
- playground/backend/main.go — Go HTTP server with /compile endpoint - playground/frontend/index.html — Monaco Editor with 8 code examples - playground/sandbox/Dockerfile — isolated Docker container with buxc2 - playground/docker-compose.yml — orchestrates backend + sandbox - playground/Makefile — build and run targets - playground/README.md — deployment guide for VPS with nginx + SSL
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# Bux Playground — Makefile
|
||||
# Run from project root: make -C playground <target>
|
||||
|
||||
.PHONY: all build-sandbox build-backend run stop clean
|
||||
|
||||
SANDBOX_IMAGE = bux-playground-sandbox
|
||||
BACKEND_IMAGE = bux-playground-backend
|
||||
PORT = 8080
|
||||
|
||||
all: build-sandbox build-backend
|
||||
|
||||
# Build the sandbox Docker image (needs buxc2 + lib + rt)
|
||||
build-sandbox:
|
||||
@echo "Building sandbox image..."
|
||||
@docker build \
|
||||
-f sandbox/Dockerfile \
|
||||
-t $(SANDBOX_IMAGE) \
|
||||
..
|
||||
|
||||
# Build the backend Go server
|
||||
build-backend:
|
||||
@echo "Building backend image..."
|
||||
@docker build \
|
||||
-f backend/Dockerfile \
|
||||
-t $(BACKEND_IMAGE) \
|
||||
.
|
||||
|
||||
# Run everything with docker-compose
|
||||
run:
|
||||
@echo "Starting Bux Playground on http://localhost:$(PORT)"
|
||||
@docker-compose up
|
||||
|
||||
# Run in background
|
||||
run-detached:
|
||||
@echo "Starting Bux Playground in background on http://localhost:$(PORT)"
|
||||
@docker-compose up -d
|
||||
|
||||
# Stop
|
||||
stop:
|
||||
@docker-compose down
|
||||
|
||||
# Clean images and containers
|
||||
clean:
|
||||
@docker-compose down --rmi all -v
|
||||
@docker rmi $(SANDBOX_IMAGE) $(BACKEND_IMAGE) 2>/dev/null || true
|
||||
|
||||
# Development mode (local backend, no Docker sandbox)
|
||||
dev:
|
||||
@echo "Starting backend in dev mode..."
|
||||
cd backend && go run main.go
|
||||
|
||||
# Quick test
|
||||
test:
|
||||
@echo "Testing /health endpoint..."
|
||||
@curl -s http://localhost:$(PORT)/health | jq .
|
||||
@@ -0,0 +1,165 @@
|
||||
# Bux Playground
|
||||
|
||||
Interactive web playground for the Bux programming language. Compile and run Bux code directly in your browser.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ POST /compile ┌──────────────┐ docker run ┌─────────────┐
|
||||
│ Browser │ ────────────────────► │ Go Backend │ ────────────────► │ Sandbox │
|
||||
│ (Monaco) │ ◄─── JSON output │ (8080) │ │ (Docker) │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Build both images (from project root)
|
||||
make -C playground all
|
||||
|
||||
# 2. Start the playground
|
||||
make -C playground run
|
||||
|
||||
# 3. Open http://localhost:8080
|
||||
```
|
||||
|
||||
## Development Mode
|
||||
|
||||
```bash
|
||||
# Run backend locally without Docker sandbox
|
||||
# (requires buxc2 in PATH)
|
||||
cd playground/backend
|
||||
go run main.go
|
||||
```
|
||||
|
||||
Then open `playground/frontend/index.html` directly in browser.
|
||||
|
||||
## Deployment on VPS
|
||||
|
||||
### 1. Build and copy to VPS
|
||||
|
||||
```bash
|
||||
# Local machine
|
||||
make selfhost # Build buxc2
|
||||
make -C playground all # Build Docker images
|
||||
docker save bux-playground-sandbox | gzip > sandbox.tar.gz
|
||||
docker save bux-playground-backend | gzip > backend.tar.gz
|
||||
|
||||
# Copy to VPS
|
||||
scp sandbox.tar.gz backend.tar.gz user@vps:/tmp/
|
||||
```
|
||||
|
||||
### 2. On VPS
|
||||
|
||||
```bash
|
||||
# Load images
|
||||
docker load < /tmp/sandbox.tar.gz
|
||||
docker load < /tmp/backend.tar.gz
|
||||
|
||||
# Run with docker-compose
|
||||
cd /opt/bux-playground
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 3. nginx reverse proxy + SSL
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name play.bux-lang.org;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name play.bux-lang.org;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/play.bux-lang.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/play.bux-lang.org/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. systemd service
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/bux-playground.service
|
||||
[Unit]
|
||||
Description=Bux Playground
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/bux-playground
|
||||
ExecStart=/usr/bin/docker-compose up -d
|
||||
ExecStop=/usr/bin/docker-compose down
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl enable bux-playground
|
||||
sudo systemctl start bux-playground
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- **Sandbox**: Each compile runs in a fresh Docker container with:
|
||||
- No network access (`--network=none`)
|
||||
- 128MB memory limit
|
||||
- 1 CPU core limit
|
||||
- 64 process limit
|
||||
- Read-only filesystem
|
||||
- `timeout 5s` for program execution
|
||||
- Temporary files auto-deleted
|
||||
|
||||
- **Code limits**:
|
||||
- Max code size: 64KB
|
||||
- Max output size: 64KB
|
||||
- Compile timeout: 10s
|
||||
- Run timeout: 5s
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `8080` | HTTP server port |
|
||||
| `USE_DOCKER` | `1` | Use Docker sandbox (0 = local mode) |
|
||||
| `SANDBOX_IMAGE` | `bux-playground-sandbox` | Docker image name |
|
||||
| `BUXC2_PATH` | `buxc2` | Path to buxc2 binary (local mode) |
|
||||
|
||||
## API
|
||||
|
||||
### POST /compile
|
||||
|
||||
Compile and run Bux code.
|
||||
|
||||
**Request:** `text/plain` — Bux source code
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"output": "Hello, Bux!\n",
|
||||
"isError": false
|
||||
}
|
||||
```
|
||||
|
||||
### GET /health
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY main.go .
|
||||
RUN go mod init bux-playground 2>/dev/null || true
|
||||
RUN go build -o playground-server main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates docker-cli
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/playground-server .
|
||||
COPY frontend ./frontend
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./playground-server"]
|
||||
@@ -0,0 +1,240 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
compileTimeout = 10 * time.Second
|
||||
runTimeout = 5 * time.Second
|
||||
maxOutputSize = 64 * 1024 // 64KB
|
||||
maxCodeSize = 64 * 1024 // 64KB
|
||||
)
|
||||
|
||||
type CompileRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type CompileResponse struct {
|
||||
Output string `json:"output"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/compile", handleCompile)
|
||||
mux.HandleFunc("/health", handleHealth)
|
||||
mux.Handle("/", http.FileServer(http.Dir("./frontend")))
|
||||
|
||||
log.Printf("Bux Playground server starting on :%s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, cors(mux)))
|
||||
}
|
||||
|
||||
func cors(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleCompile(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read code from body
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, maxCodeSize+1))
|
||||
if err != nil {
|
||||
writeError(w, "Failed to read request body")
|
||||
return
|
||||
}
|
||||
if len(body) > maxCodeSize {
|
||||
writeError(w, "Code exceeds maximum size (64KB)")
|
||||
return
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(string(body))
|
||||
if code == "" {
|
||||
writeError(w, "Empty code")
|
||||
return
|
||||
}
|
||||
|
||||
// Create temp project directory
|
||||
tmpDir, err := os.MkdirTemp("", "bux-playground-*")
|
||||
if err != nil {
|
||||
writeError(w, "Failed to create temp directory")
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Write code to src/Main.bux
|
||||
srcDir := filepath.Join(tmpDir, "src")
|
||||
if err := os.MkdirAll(srcDir, 0755); err != nil {
|
||||
writeError(w, "Failed to create src directory")
|
||||
return
|
||||
}
|
||||
|
||||
mainFile := filepath.Join(srcDir, "Main.bux")
|
||||
if err := os.WriteFile(mainFile, []byte(code), 0644); err != nil {
|
||||
writeError(w, "Failed to write source file")
|
||||
return
|
||||
}
|
||||
|
||||
// Write bux.toml
|
||||
buxToml := `[Package]
|
||||
Name = "playground"
|
||||
Version = "0.1.0"
|
||||
Type = "bin"
|
||||
|
||||
[Build]
|
||||
Output = "Bin"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "bux.toml"), []byte(buxToml), 0644); err != nil {
|
||||
writeError(w, "Failed to write bux.toml")
|
||||
return
|
||||
}
|
||||
|
||||
// Run buxc2 build inside Docker sandbox
|
||||
output, isError := runInSandbox(tmpDir)
|
||||
|
||||
// Truncate output if too large
|
||||
if len(output) > maxOutputSize {
|
||||
output = output[:maxOutputSize] + "\n... (output truncated)"
|
||||
}
|
||||
|
||||
resp := CompileResponse{
|
||||
Output: output,
|
||||
IsError: isError,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func runInSandbox(projectDir string) (string, bool) {
|
||||
// Check if running inside Docker (sandbox container available)
|
||||
sandboxImage := os.Getenv("SANDBOX_IMAGE")
|
||||
if sandboxImage == "" {
|
||||
sandboxImage = "bux-playground-sandbox"
|
||||
}
|
||||
|
||||
// Determine if we use Docker or local buxc2
|
||||
useDocker := os.Getenv("USE_DOCKER") != "0"
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
||||
if useDocker {
|
||||
// Run compilation inside Docker container
|
||||
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd = exec.CommandContext(ctx, "docker", "run",
|
||||
"--rm",
|
||||
"--network=none",
|
||||
"--memory=128m",
|
||||
"--memory-swap=128m",
|
||||
"--cpus=1.0",
|
||||
"--pids-limit=64",
|
||||
"--read-only",
|
||||
"--tmpfs", "/tmp:noexec,nosuid,size=50m",
|
||||
"-v", projectDir+":/project:ro",
|
||||
"-w", "/project",
|
||||
sandboxImage,
|
||||
"sh", "-c",
|
||||
"buxc2 build 2>&1 && timeout 5s ./build/playground 2>&1 || true",
|
||||
)
|
||||
} else {
|
||||
// Local mode (for development)
|
||||
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
|
||||
defer cancel()
|
||||
|
||||
buxc2Path := os.Getenv("BUXC2_PATH")
|
||||
if buxc2Path == "" {
|
||||
buxc2Path = "buxc2"
|
||||
}
|
||||
|
||||
// First compile
|
||||
compileCmd := exec.CommandContext(ctx, buxc2Path, "build")
|
||||
compileCmd.Dir = projectDir
|
||||
compileCmd.Env = append(os.Environ(),
|
||||
"HOME=/tmp",
|
||||
)
|
||||
|
||||
compileOut, compileErr := compileCmd.CombinedOutput()
|
||||
output := string(compileOut)
|
||||
if compileErr != nil {
|
||||
return output, true
|
||||
}
|
||||
|
||||
// Then run the binary
|
||||
runCtx, runCancel := context.WithTimeout(context.Background(), runTimeout)
|
||||
defer runCancel()
|
||||
|
||||
binPath := filepath.Join(projectDir, "build", "playground")
|
||||
runCmd := exec.CommandContext(runCtx, binPath)
|
||||
runCmd.Dir = projectDir
|
||||
runOut, runErr := runCmd.CombinedOutput()
|
||||
output += string(runOut)
|
||||
if runErr != nil && runCtx.Err() != context.DeadlineExceeded {
|
||||
output += "\nRuntime error: " + runErr.Error()
|
||||
}
|
||||
|
||||
return output, false
|
||||
}
|
||||
|
||||
// Run Docker command
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
err := cmd.Run()
|
||||
output := out.String()
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
output += "\nTimeout: compilation or execution took too long"
|
||||
return output, true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return output, true
|
||||
}
|
||||
|
||||
return output, false
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string) {
|
||||
resp := CompileResponse{
|
||||
Output: msg,
|
||||
IsError: true,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- PORT=8080
|
||||
- USE_DOCKER=1
|
||||
- SANDBOX_IMAGE=bux-playground-sandbox
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
- sandbox
|
||||
restart: unless-stopped
|
||||
|
||||
# Sandbox image is built separately and used by backend
|
||||
sandbox:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: playground/sandbox/Dockerfile
|
||||
image: bux-playground-sandbox
|
||||
# This container is not run directly — backend spawns it per-request
|
||||
command: ["echo", "sandbox image ready"]
|
||||
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bux Playground</title>
|
||||
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/editor/editor.main.min.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
background: #161b22;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #30363d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header .logo {
|
||||
font-weight: 700;
|
||||
color: #58a6ff;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.header .logo::before {
|
||||
content: '◈';
|
||||
color: #7ee787;
|
||||
}
|
||||
.header .version {
|
||||
color: #8b949e;
|
||||
font-size: 12px;
|
||||
}
|
||||
.header .examples {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.header select {
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toolbar button {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #30363d;
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toolbar button:hover {
|
||||
background: #30363d;
|
||||
}
|
||||
.toolbar button.run {
|
||||
background: #238636;
|
||||
color: #fff;
|
||||
border-color: #238636;
|
||||
font-weight: 600;
|
||||
}
|
||||
.toolbar button.run:hover {
|
||||
background: #2ea043;
|
||||
}
|
||||
.toolbar button.run:disabled {
|
||||
background: #1f4d2e;
|
||||
border-color: #1f4d2e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.output-panel {
|
||||
width: 45%;
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
background: #0d1117;
|
||||
border-left: 1px solid #30363d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.output-header {
|
||||
padding: 8px 16px;
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #8b949e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-family: 'SFMono-Regular', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
#output.error { color: #f85149; }
|
||||
#output.success { color: #7ee787; }
|
||||
#output.running { color: #8b949e; }
|
||||
.status-bar {
|
||||
padding: 4px 16px;
|
||||
background: #161b22;
|
||||
border-top: 1px solid #30363d;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #30363d;
|
||||
border-top-color: #58a6ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<span class="logo">Bux Playground</span>
|
||||
<span class="version">Try the Bux programming language in your browser</span>
|
||||
<div class="examples">
|
||||
<select id="exampleSelect">
|
||||
<option value="">Load example...</option>
|
||||
<option value="hello">Hello World</option>
|
||||
<option value="factorial">Factorial</option>
|
||||
<option value="fibonacci">Fibonacci</option>
|
||||
<option value="structs">Structs & Methods</option>
|
||||
<option value="generics">Generics</option>
|
||||
<option value="channels">Channels</option>
|
||||
<option value="async">Async/Await</option>
|
||||
<option value="result">Error Handling</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="editor-panel">
|
||||
<div class="toolbar">
|
||||
<button class="run" id="runBtn" onclick="runCode()">▶ Run</button>
|
||||
<button onclick="clearOutput()">Clear Output</button>
|
||||
<button onclick="formatCode()">Format</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div class="output-panel">
|
||||
<div class="output-header">Output</div>
|
||||
<div id="output"></div>
|
||||
<div class="status-bar">
|
||||
<span>Ctrl+Enter to run</span>
|
||||
<span id="statusText">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
|
||||
<script>
|
||||
const examples = {
|
||||
hello: `import Std::Io::PrintLine;
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Hello, Bux!");
|
||||
return 0;
|
||||
}`,
|
||||
factorial: `import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Fmt::Fmt_Fmt2;
|
||||
|
||||
func Factorial(n: int) -> int {
|
||||
if n <= 1 {
|
||||
return 1;
|
||||
}
|
||||
return n * Factorial(n - 1);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Factorials:");
|
||||
var i: int = 1;
|
||||
while i <= 10 {
|
||||
let fact: int = Factorial(i);
|
||||
PrintLine(Fmt_Fmt2("{0}! = {1}", String_FromInt(i), String_FromInt(fact)));
|
||||
i = i + 1;
|
||||
}
|
||||
return 0;
|
||||
}`,
|
||||
fibonacci: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
func Fibonacci(n: int) -> int {
|
||||
if n <= 1 {
|
||||
return n;
|
||||
}
|
||||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
PrintLine("Fibonacci sequence:");
|
||||
var i: int = 0;
|
||||
while i < 15 {
|
||||
PrintInt(Fibonacci(i));
|
||||
PrintLine("");
|
||||
i = i + 1;
|
||||
}
|
||||
return 0;
|
||||
}`,
|
||||
structs: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
struct Rectangle {
|
||||
width: int;
|
||||
height: int;
|
||||
}
|
||||
|
||||
extend Rectangle {
|
||||
func Area(self: Rectangle) -> int {
|
||||
return self.width * self.height;
|
||||
}
|
||||
func Perimeter(self: Rectangle) -> int {
|
||||
return 2 * (self.width + self.height);
|
||||
}
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let rect: Rectangle = Rectangle { width: 10, height: 5 };
|
||||
PrintLine("Rectangle:");
|
||||
PrintLine(Fmt_Fmt2(" Area = {0}", String_FromInt(rect.Area())));
|
||||
PrintLine(Fmt_Fmt2(" Perimeter = {0}", String_FromInt(rect.Perimeter())));
|
||||
return 0;
|
||||
}`,
|
||||
generics: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
func Max<T>(a: T, b: T) -> T {
|
||||
if a > b {
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let m1: int = Max<int>(10, 20);
|
||||
PrintLine("Max of 10 and 20:");
|
||||
PrintInt(m1);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}`,
|
||||
channels: `import Std::Io::{PrintLine, PrintInt};
|
||||
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
|
||||
|
||||
func Producer(chPtr: *Channel<int>) {
|
||||
var i: int = 1;
|
||||
while i <= 5 {
|
||||
Channel_SendInt(chPtr, i * 10);
|
||||
i = i + 1;
|
||||
}
|
||||
Channel_Close<int>(chPtr);
|
||||
}
|
||||
|
||||
func Consumer(chPtr: *Channel<int>) {
|
||||
var total: int = 0;
|
||||
while true {
|
||||
let val: int = Channel_RecvInt(chPtr);
|
||||
if val == 0 { break; }
|
||||
total = total + val;
|
||||
PrintInt(val);
|
||||
PrintLine("");
|
||||
}
|
||||
PrintLine("Total:");
|
||||
PrintInt(total);
|
||||
PrintLine("");
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let ch: Channel<int> = Channel_New<int>(3);
|
||||
let p: *void = spawn Producer(&ch);
|
||||
let c: *void = spawn Consumer(&ch);
|
||||
return 0;
|
||||
}`,
|
||||
async: `import Std::Io::{PrintLine, PrintInt};
|
||||
|
||||
async func Compute() -> int {
|
||||
PrintLine("Compute: step 1");
|
||||
bux_async_yield();
|
||||
PrintLine("Compute: step 2");
|
||||
return 42;
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let h1 = spawn Compute();
|
||||
let r1: int = h1.await as int;
|
||||
PrintLine("Result:");
|
||||
PrintInt(r1);
|
||||
PrintLine("");
|
||||
return 0;
|
||||
}`,
|
||||
result: `import Std::Io::PrintLine;
|
||||
|
||||
enum Result {
|
||||
Ok(int),
|
||||
Err(String)
|
||||
}
|
||||
|
||||
func Divide(a: int, b: int) -> Result {
|
||||
if b == 0 {
|
||||
return Result_NewErr("division by zero");
|
||||
}
|
||||
return Result_NewOk(a / b);
|
||||
}
|
||||
|
||||
func Compute() -> Result {
|
||||
let x: int = Divide(10, 2)?;
|
||||
let y: int = Divide(x, 5)?;
|
||||
return Result_NewOk(y);
|
||||
}
|
||||
|
||||
func Main() -> int {
|
||||
let result: Result = Compute();
|
||||
match result {
|
||||
case Ok(val) => PrintLine("Success!");
|
||||
case Err(msg) => PrintLine("Error!");
|
||||
}
|
||||
return 0;
|
||||
}`
|
||||
};
|
||||
|
||||
// Monaco Editor setup
|
||||
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
|
||||
let editor;
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Simple Bux language definition
|
||||
monaco.languages.register({ id: 'bux' });
|
||||
monaco.languages.setMonarchTokensProvider('bux', {
|
||||
keywords: [
|
||||
'func', 'let', 'var', 'const', 'type', 'struct', 'enum', 'union',
|
||||
'interface', 'extend', 'module', 'import', 'pub', 'extern',
|
||||
'if', 'else', 'while', 'do', 'loop', 'for', 'in', 'break',
|
||||
'continue', 'return', 'match', 'as', 'is', 'null', 'self',
|
||||
'super', 'sizeof', 'async', 'await', 'spawn', 'defer',
|
||||
'switch', 'case', 'default', 'checked'
|
||||
],
|
||||
operators: [
|
||||
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
|
||||
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^',
|
||||
'%', '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=',
|
||||
'^=', '%=', '<<=', '>>=', '>>>='
|
||||
],
|
||||
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[a-zA-Z_]\w*/, {
|
||||
cases: {
|
||||
'@keywords': 'keyword',
|
||||
'@default': 'identifier'
|
||||
}
|
||||
}],
|
||||
[/\d+/, 'number'],
|
||||
[/"[^"]*"/, 'string'],
|
||||
[/`[^`]*`/, 'string'],
|
||||
[/[{}()\[\]]/, '@brackets'],
|
||||
[/@symbols/, {
|
||||
cases: {
|
||||
'@operators': 'operator',
|
||||
'@default': ''
|
||||
}
|
||||
}],
|
||||
[/\/\/.*$/, 'comment'],
|
||||
[/\/\*/, 'comment', '@comment'],
|
||||
],
|
||||
comment: [
|
||||
[/[^\/*]+/, 'comment'],
|
||||
[/\*\//, 'comment', '@pop'],
|
||||
[/[\/*]/, 'comment']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: examples.hello,
|
||||
language: 'bux',
|
||||
theme: 'vs-dark',
|
||||
fontSize: 14,
|
||||
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: false,
|
||||
lineNumbers: 'on',
|
||||
renderWhitespace: 'selection',
|
||||
folding: true,
|
||||
wordWrap: 'on'
|
||||
});
|
||||
|
||||
// Ctrl+Enter shortcut
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, runCode);
|
||||
});
|
||||
|
||||
// Example selector
|
||||
document.getElementById('exampleSelect').addEventListener('change', function(e) {
|
||||
const key = e.target.value;
|
||||
if (key && examples[key] && editor) {
|
||||
editor.setValue(examples[key]);
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
const outputDiv = document.getElementById('output');
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
async function runCode() {
|
||||
if (!editor) return;
|
||||
const code = editor.getValue();
|
||||
outputDiv.textContent = '';
|
||||
outputDiv.className = 'output running';
|
||||
runBtn.disabled = true;
|
||||
statusText.innerHTML = '<span class="spinner"></span>Compiling...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/compile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: code
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
let output = data.output || '';
|
||||
// Filter verbose build lines
|
||||
let lines = output.split('\n');
|
||||
let filtered = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
if (line.includes('[Package]') || line.includes('[Build]') ||
|
||||
line.includes('Hint:') || line.includes('Bux compiler') ||
|
||||
line.startsWith('Compiling') || line.startsWith('Linking') ||
|
||||
line.startsWith('Build successful')) continue;
|
||||
filtered.push(line);
|
||||
}
|
||||
output = filtered.join('\n').trim() || '(no output)';
|
||||
|
||||
outputDiv.textContent = output;
|
||||
outputDiv.className = 'output' + (data.isError ? ' error' : ' success');
|
||||
statusText.textContent = data.isError ? 'Error' : 'Done';
|
||||
} catch (e) {
|
||||
outputDiv.textContent = 'Error: ' + e.message;
|
||||
outputDiv.className = 'output error';
|
||||
statusText.textContent = 'Error';
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearOutput() {
|
||||
outputDiv.textContent = '';
|
||||
outputDiv.className = 'output';
|
||||
statusText.textContent = 'Ready';
|
||||
}
|
||||
|
||||
function formatCode() {
|
||||
if (!editor) return;
|
||||
// Simple auto-indent (Monaco formatter would be better)
|
||||
const code = editor.getValue();
|
||||
editor.setValue(code);
|
||||
statusText.textContent = 'Formatted';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
# Bux Playground Sandbox
|
||||
# Minimal container with Bux compiler + C toolchain
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
clang \
|
||||
libc6-dev \
|
||||
libssl-dev \
|
||||
make \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create sandbox user (no root)
|
||||
RUN useradd -m -s /bin/bash sandbox
|
||||
|
||||
# Copy Bux compiler binary
|
||||
COPY buxc2 /usr/local/bin/buxc2
|
||||
RUN chmod +x /usr/local/bin/buxc2
|
||||
|
||||
# Copy stdlib and runtime
|
||||
COPY lib /usr/local/share/bux/lib
|
||||
COPY rt /usr/local/share/bux/rt
|
||||
|
||||
# Set up working directory
|
||||
WORKDIR /tmp/bux-run
|
||||
RUN chown sandbox:sandbox /tmp/bux-run
|
||||
|
||||
# Security: drop privileges, limit resources
|
||||
USER sandbox
|
||||
|
||||
# Default command (backend will override)
|
||||
CMD ["/bin/bash"]
|
||||
Reference in New Issue
Block a user