juicy_fruit

Version, currently main branch1 version
  • main branchlatestFeb 3, 2026

github.com/konjac-lang/juicy_fruit

A WIP JavaScript based compiler for the Konjac Programming Language and a WebSocket-based debugger interface for the X Virtual Machine.

3 stars
0 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  juicy_fruit:
    github: konjac-lang/juicy_fruit
    branch: main

main is a branch, not a release, so this tracks it rather than pinning a version.

Then run:

shards install

shard.yml

Crystal
>= 0.36.0
License
MIT
Author
Giorgi Kavrelishvili
Target
  • juicy_fruit from src/juicy_fruit.cr

Dependencies

Runtime Dependencies

  • grip*github: grip-framework/grip
  • jelly*github: grkek/jelly
  • spectator*github: grip-framework/spectator

Development Dependencies

  • ameba*github: crystal-ameba/amebadev

README

Juicy Fruit

A WIP JavaScript based compiler for the Konjac Programming Language and a WebSocket-based debugger interface for the X Virtual Machine.

Quick Start

docker compose up

Goal

The current goal is to be able to parse, compile and run the code mentioned below.

-- An example HTTP server written in Konjac
module Rum.Request
  struct [
    method: String;
    path: String;
    headers: Map(String, String);
  ]
end

module Rum.Response
  struct [
    body: Binary = Binary { };
    statusCode: Int64 = 200;
    headers: Map(String, String) = {};
  ]
  
  export function encode(response : Rum.Response) : Binary
    reasonPhrase : String = case response.statusCode
      when 200, do: "OK"
      when 201, do: "Created"
      when 301, do: "Moved Permanently"
      when 400, do: "Bad Request"
      when 404, do: "Not Found"
      when 500, do: "Internal Server Error"
      else do: "Unknown"
    end
    
    statusLine : String = "HTTP/1.1 " + Int64.toString(response.statusCode) + " " + reasonPhrase + "\r\n"

    updatedHeaders : Map(String, String) = if Map.hasKey(response.headers, "Content-Length")
      response.headers
    else
      Map.put(response.headers, "Content-Length", String.fromInt(Binary.length(response.body)))
    end
    
    headersString : String = 
      updatedHeaders
      |> Map.toArray()  
      |> Array.map(function([key, value] : Array(String)) do
        -- String interpolation with the expansion
        <<key, ": ", value, "\r\n>>
      end)
      |> String.join()
      |> String.concatenate("\r\n\r\n")
      
    statusLine
    |> String.concatenate(headersString)
    |> Binary.fromString()
    |> Binary.concatenate(response.body)
  end
end

module Rum.Context
  struct [
    request: Rum.Request;
    response: Rum.Response;
  ]

  export function getRequestHeader(context : Rum.Context, key : String, value : String? = nil) : String?,
    do: Map.get(context.request.headers, key, value)

  export function setResponseHeader(context : Rum.Context, key : String, value : String) : Rum.Context
    Rum.Context { 
      response: Rum.Response { 
        headers: Map.put(context.response.headers, key, value),
        ...context.response
      }
      ...context
    }
  end

  export function sendResponse(context : Rum.Context, body : Any, statusCode : Int64 = 200) : Rum.Context
    Rum.Context { 
      response: {
        body: body,
        statusCode: statusCode,
        ...context.response
      },
      ...context
    }
  end

  export function json(context : Rum.Context, data : JSON::Any) : Rum.Context
    body = data |> JSON.encode() |> Binary.fromString()

    context
    |> Rum.Context.setResponseHeader("Content-Type", "application/json")
    |> Rum.Context.sendResponse(body)
  end

  export function html(context : Rum.Context, data : String) : Rum.Context
    body = data |> Binary.fromString()

    context 
    |> Rum.Context.setResponseHeader("Content-Type", "text/html")
    |> Rum.Context.sendResponse(body)
  end
end

behaviour Rum.Handler
  abstract function call(handler : Rum.Handler, context : Rum.Context) : Rum.Context
end

module Rum.Server
  struct [
    host: String;
    port: UInt16;
    handlers: Array(Rum.Handler);
  ]

  function handleClient(server : Rum.Server, client : TCP.Client) : Nil
    data : Binary = TCP.receive(client)

    [headerSection, messageBody] = 
      data
      |> Binary.toString()
      |> String.split("\r\n\r\n")

    [startLine, ...unprocessedHeaders]
      headerSection
      |> String.split("\r\n")

    headers =
      unprocessedHeaders 
      |> Array.map(function(header : String) do
        [key, value] = String.split(header)
        
        {key: value}
      end)
      |> Map.zip()
    
    -- Short hand function call
    [method, path, "HTTP/1.1"] = startLine |> String.split(" ") |> Array.map(&String.stripSuffix/1)

    context = 
      Rum.Context { 
        request: Rum.Request { 
          method: method,
          path: path,
          headers: headers 
        }, 
        response: Rum.Response { }
      }

    updatedContext = 
      server.handlers
      |> Array.reduce(function(handler : Rum.Handler, accumulator : Rum.Context) do 
        Rum.Handler.call(handler, accumulator)
      end)
      
    encodedResponse = Rum.Response.encode(updatedContext.response)

    TCP.send(client, encodedResponse)
  end

  export function bind(server : Rum.Server) : Rum.Response
    loop do
      client : TCP.Client? = TCP.accept(server.host, server.port)
      
      if client do
        Process.spawn do
          handleClient(server, client)
        end
      else
        Process.killSelf()
      end
    end
  end
end

module Untitled.Handler
  implement Rum.Handler

  export function call(handler : Rum.Handler, context : Rum.Context) : Rum.Context
    context
    |> Rum.Context.setResponseHeader("Server", "Rum/0.1.0")
    |> Rum.Context.json({"success": true, "message": "Hello, World!", "data": nil})
  end
end

module Rum.Application
  export function main(arguments : Array(String)) : Nil
    server = Rum.Server { host: "127.0.0.1", port: 8080 as UInt16, handlers: [Untitled.Handler] }

    supervisor = 
      Supervisor {
        strategy: "OneForOne",
        maximumRestarts: 5,
        restartWindow: 10
      }

    process = Process.spawn do
      Rum.Server.bind(server)
    end

    child = Child {
      id: "worker",
      process: process,
      restart: "Transiet",
      maxRestarts: 3,
      restartWindow: 5
    }
    
    Supervisor.addChild(supervisor, child)
  end
end

HTTP Protocol

Connect to http://localhost:4004/

WebSocket Protocol

Connect to ws://localhost:4004/debugger

Commands (Client → Server)

CommandParametersDescription
inititerationLimit?Initialize VM engine
loadinstructionsLoad instructions into process
run-Start execution
step-Execute one instruction
continue-Continue to next breakpoint
abort-Stop execution
addBreakpointconditionType, valueAdd breakpoint
removeBreakpointidRemove breakpoint
getState-Get current VM state
listBreakpoints-List all breakpoints
evaluatetarget, limit?Evaluate stack/locals

Events (Server → Client)

EventDescription
connectedSession initialized
initializedEngine ready
loadedInstructions loaded
runningExecution started
breakpointHitPaused at breakpoint
executionCompleteFinished execution
executionErrorRuntime error
errorCommand error

Instruction Format

{
  "command": "load",
  "instructions": [
    {"code": "PUSH_STRING", "value": {"type": "string", "value": "Hello"}},
    {"code": "PUSH_INTEGER", "value": {"type": "integer", "value": 42}},
    {"code": "PRINT_LINE"}
  ]
}

Value Types

TypeExample
string{"type": "string", "value": "hello"}
integer{"type": "integer", "value": 42}
float{"type": "float", "value": 3.14}
symbol{"type": "symbol", "value": "error"}
uint{"type": "uint", "value": 0}
bool{"type": "bool", "value": true}

Breakpoint Conditions

TypeDescription
counterBreak at instruction index
minStackDepthBreak when call stack >= value
maxStackDepthBreak when call stack <= value
stackSizeBreak when data stack == value

Example Session

const ws = new WebSocket('ws://localhost:4004/debugger');

ws.onmessage = (e) => console.log(JSON.parse(e.data));

// Initialize
ws.send(JSON.stringify({command: 'init', iterationLimit: 10000}));

// Load program
ws.send(JSON.stringify({
  command: 'load',
  instructions: [
    {code: 'PUSH_STRING', value: {type: 'string', value: 'Hello, World!'}},
    {code: 'PRINT_LINE'},
    {code: 'EXIT_SELF'}
  ]
}));

// Add breakpoint at instruction 1
ws.send(JSON.stringify({
  command: 'addBreakpoint',
  conditionType: 'counter',
  value: 1
}));

// Run
ws.send(JSON.stringify({command: 'run'}));

// When breakpoint hits, step or continue
ws.send(JSON.stringify({command: 'step'}));

Security

The container runs with:

  • Non-root user (uid 1000)
  • Read-only filesystem
  • Dropped capabilities
  • Memory limits (512MB)
  • CPU limits (1 core)
  • Process limits (100 pids)
  • No privilege escalation