integration work continues

This commit is contained in:
Scott Richmond 2025-06-30 18:59:59 -04:00
parent 173fdb913c
commit 4eceb62ce5
19 changed files with 726 additions and 235 deletions

View File

@ -11,7 +11,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
chumsky = "0.10.1"
imbl = "3.0.0"
ran = "2.0.1"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.11.1"
@ -19,5 +18,4 @@ wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.50"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
tokio = {version = "1.45.1", features = ["macros", "rt-multi-thread"]}
console_error_panic_hook = "0.1.7"

View File

@ -1,3 +1,7 @@
&&& buffers: shared memory with Rust
box console = []
box input = ""
& the very base: know something's type
fn type {
"Returns a keyword representing the type of the value passed in."
@ -408,8 +412,6 @@ fn to_number {
(num as :string) -> base :number (num)
}
box console = []
fn print! {
"Sends a text representation of Ludus values to the console."
(...args) -> {
@ -1269,6 +1271,9 @@ fn sleep! {
link!
console
input
abs
abs
add

View File

@ -1,11 +1,23 @@
wasm:
wasm: && clean-wasm-pack
# build with wasm-pack
wasm-pack build --target web
rm pkg/.gitignore
cp pkg/rudus.js pkg/rudus.js.backup
echo 'import {io} from "../worker.js"' > rudus.js
cat rudus.js.backup | tail -n+2>> rudus.js
rm rudus.js.backup
wasm-dev: && clean-wasm-pack
wasm-pack build --dev --target web
clean-wasm-pack:
# delete cruft from wasm-pack
rm pkg/.gitignore pkg/package.json pkg/README.md
rm -rf pkg/snippets
# fix imports of rudus.js
cp pkg/rudus.js pkg/rudus.js.backup
echo 'import { io } from "./worker.js"' > pkg/rudus.js
cat pkg/rudus.js.backup | tail -n+2>> pkg/rudus.js
rm pkg/rudus.js.backup
serve:
miniserve pkg && open http://localhost:8080/index.html
default:
@just --list

0
pkg/.gitignore vendored
View File

View File

@ -6,13 +6,7 @@
</head>
<body>
<script type="module">
import {run} from "./ludus.js";
window.ludus = run;
console.log(run(":foobar"));
</script>
<script src="./ludus.js" type="module"></script>
<p>
Open the console. All the action's in there.
</p>

View File

@ -1,18 +1,72 @@
import init, {ludus} from "./rudus.js";
if (window) window.ludus = {run, kill, flush_console, p5, svg, turtle_commands, result, input}
const worker = new Worker("worker.js", {type: "module"})
let res = null
let outbox = []
let code = null
export function run (source) {
code = source
const output = ludus(source)
res = JSON.parse(output)
return res
worker.onmessage = async (e) => {
let msgs
try {
msgs = JSON.parse(e.data)
} catch {
console.log(e.data)
throw Error("bad json from Ludus")
}
for (const msg of msgs) {
console.log("Main: message received from worker:", msg);
switch (msg.verb) {
case "complete": {
console.log("completed ludus run!")
console.log("with", msg.data)
res = msg.data
running = false
break
}
case "console": {
console.log("console msg from msg.data")
console.log(msg.data)
break
}
}
}
}
export function stdout () {
let res = null
let code = null
let running = false
let io_interval_id = null
const io_poller = () => {
if (io_interval_id && !running) {
clearInterval(io_interval_id)
return
}
worker.postMessage(outbox)
outbox = []
}
function poll_io () {
io_interval_id = setInterval(io_poller, 10)
}
export function run (source) {
if (running) "TODO: handle this? should not be running"
running = true
code = source
outbox.push({verb: "run", data: source})
poll_io()
}
export function kill () {
running = false
outbox.push({verb: "kill"})
}
export function input (text) {
outbox.push({verb: "input", data: text})
}
export function flush_console () {
if (!res) return ""
return res.io.stdout.data
}
@ -375,25 +429,4 @@ export function p5 (commands) {
return p5_calls
}
window.ludus = {run, console, p5, svg, stdout, turtle_commands, result}
await init()
const worker = new Worker("worker.js", {type: "module"})
let outbox = {}
setInterval(() => {
worker.postMessage(outbox)
outbox = {}
})
worker.onmessage = async (msgs) => {
for (const msg of msgs) {
switch (msg[0]) {
case "stdout": {
stdout = msg[1]
}
}
}
}

13
pkg/rudus.d.ts vendored
View File

@ -1,16 +1,21 @@
/* tslint:disable */
/* eslint-disable */
export function ludus(src: string): string;
export function ludus(src: string): Promise<string>;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly ludus: (a: number, b: number) => [number, number];
readonly __wbindgen_export_0: WebAssembly.Table;
readonly ludus: (a: number, b: number) => any;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_export_6: WebAssembly.Table;
readonly closure347_externref_shim: (a: number, b: number, c: any) => void;
readonly closure371_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly __wbindgen_start: () => void;
}

View File

@ -1,6 +1,41 @@
import { io } from "./worker.js"
let wasm;
let WASM_VECTOR_LEN = 0;
function addToExternrefTable0(obj) {
const idx = wasm.__externref_table_alloc();
wasm.__wbindgen_export_2.set(idx, obj);
return idx;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const idx = addToExternrefTable0(e);
wasm.__wbindgen_exn_store(idx);
}
}
function logError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
let error = (function () {
try {
return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString();
} catch(_) {
return "<failed to stringify thrown value>";
}
}());
console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error);
throw e;
}
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
@ -11,6 +46,13 @@ function getUint8ArrayMemory0() {
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
@ -28,6 +70,8 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
function passStringToWasm0(arg, malloc, realloc) {
if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`);
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
@ -56,7 +100,7 @@ function passStringToWasm0(arg, malloc, realloc) {
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
if (ret.read !== arg.length) throw new Error('failed to pass whole string');
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
@ -65,31 +109,144 @@ function passStringToWasm0(arg, malloc, realloc) {
return ptr;
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
let cachedDataViewMemory0 = null;
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
function isLikeNone(x) {
return x === undefined || x === null;
}
function _assertBoolean(n) {
if (typeof(n) !== 'boolean') {
throw new Error(`expected a boolean argument, found ${typeof(n)}`);
}
}
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b)
});
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_6.get(state.dtor)(a, state.b);
CLOSURE_DTORS.unregister(state);
} else {
state.a = a;
}
}
};
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches && builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
/**
* @param {string} src
* @returns {string}
* @returns {Promise<string>}
*/
export function ludus(src) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ludus(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
return ret;
}
function _assertNum(n) {
if (typeof(n) !== 'number') throw new Error(`expected a number argument, found ${typeof(n)}`);
}
function __wbg_adapter_22(arg0, arg1, arg2) {
_assertNum(arg0);
_assertNum(arg1);
wasm.closure347_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_50(arg0, arg1, arg2, arg3) {
_assertNum(arg0);
_assertNum(arg1);
wasm.closure371_externref_shim(arg0, arg1, arg2, arg3);
}
async function __wbg_load(module, imports) {
@ -126,8 +283,150 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
const ret = arg0.call(arg1);
return ret;
}, arguments) };
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.call(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function() { return logError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments) };
imports.wbg.__wbg_io_4b41f8089de924df = function() { return logError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
const ret = io(getStringFromWasm0(arg0, arg1));
return ret;
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments) };
imports.wbg.__wbg_log_86d603e98cc11395 = function() { return logError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.log(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments) };
imports.wbg.__wbg_log_9e426f8e841e42d3 = function() { return logError(function (arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
}, arguments) };
imports.wbg.__wbg_new_23a2665fac83c611 = function() { return logError(function (arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_50(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return ret;
} finally {
state0.a = state0.b = 0;
}
}, arguments) };
imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { return logError(function () {
const ret = new Error();
return ret;
}, arguments) };
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function() { return logError(function (arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return ret;
}, arguments) };
imports.wbg.__wbg_now_8dddb61fa4928554 = function() { return logError(function () {
const ret = Date.now();
return ret;
}, arguments) };
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function() { return logError(function (arg0) {
queueMicrotask(arg0);
}, arguments) };
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function() { return logError(function (arg0) {
const ret = arg0.queueMicrotask;
return ret;
}, arguments) };
imports.wbg.__wbg_random_57c118f142535bb6 = function() { return logError(function () {
const ret = Math.random();
return ret;
}, arguments) };
imports.wbg.__wbg_resolve_4851785c9c5f573d = function() { return logError(function (arg0) {
const ret = Promise.resolve(arg0);
return ret;
}, arguments) };
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function() { return logError(function (arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments) };
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { return logError(function () {
const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
}, arguments) };
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { return logError(function () {
const ret = typeof globalThis === 'undefined' ? null : globalThis;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
}, arguments) };
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { return logError(function () {
const ret = typeof self === 'undefined' ? null : self;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
}, arguments) };
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { return logError(function () {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
}, arguments) };
imports.wbg.__wbg_then_44b73946d2fb3e7d = function() { return logError(function (arg0, arg1) {
const ret = arg0.then(arg1);
return ret;
}, arguments) };
imports.wbg.__wbg_then_48b406749878a531 = function() { return logError(function (arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = arg0.original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper7663 = function() { return logError(function (arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 348, __wbg_adapter_22);
return ret;
}, arguments) };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
const ret = debugString(arg1);
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_0;
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
@ -136,6 +435,31 @@ function __wbg_get_imports() {
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(arg0) === 'function';
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = arg0 === undefined;
_assertBoolean(ret);
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = arg1;
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return ret;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
@ -147,6 +471,7 @@ function __wbg_init_memory(imports, memory) {
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;

Binary file not shown.

View File

@ -1,9 +1,14 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const ludus: (a: number, b: number) => [number, number];
export const __wbindgen_export_0: WebAssembly.Table;
export const ludus: (a: number, b: number) => any;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_2: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_export_6: WebAssembly.Table;
export const closure347_externref_shim: (a: number, b: number, c: any) => void;
export const closure371_externref_shim: (a: number, b: number, c: any, d: any) => void;
export const __wbindgen_start: () => void;

View File

@ -1,15 +1,40 @@
import init from "./rudus.js";
import init, {ludus} from "./rudus.js";
console.log("Worker: starting Ludus VM.")
export function io (out) {
if (Object.keys(out).length > 0) postMessage(out)
if (out.length > 0) postMessage(out)
return new Promise((resolve, _) => {
onmessage = (e) => resolve(e.data)
onmessage = (e) => {
console.log(e.data)
resolve(JSON.stringify(e.data))
}
})
}
await init()
let loaded_wasm = false
async function run(e) {
if (!loaded_wasm) {
await init()
loaded_wasm = true
}
let msgs = e.data
for (const msg of msgs) {
if (msg.verb === "run" && typeof msg.data === 'string') {
console.log("running ludus!")
onmessage = () => {}
let result = await ludus(msg.data)
console.log(result)
onmessage = run
} else {
console.log("Did not get valid startup message. Instead got:")
console.log(e.data)
}
}
}
onmessage = run
console.log("Worker: Ludus VM is running.")

View File

@ -1,31 +1 @@
fn agent (val) -> receive {
(:set, new) -> agent (new)
(:get, pid) -> {
send (pid, (:response, val))
agent (val)
}
(:update, f) -> agent (f (val))
}
fn agent/set (pid, val) -> {
send (pid, (:set, val))
val
}
fn agent/get (pid) -> {
send (pid, (:get, self ()))
receive {
(:response, val) -> val
}
}
fn agent/update (pid, f) -> {
send (pid, (:update, f))
agent/get (pid)
}
let myagent = spawn! (fn () -> agent (42))
print! ("incrementing agent value to", agent/update (myagent, inc))
:done!
:foobar

View File

@ -1,7 +1,7 @@
use crate::value::*;
use imbl::*;
use ran::ran_f64;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[derive(Clone, Debug)]
pub enum BaseFn {
@ -481,8 +481,14 @@ pub fn floor(x: &Value) -> Value {
}
}
pub fn random() -> Value {
Value::Number(ran_f64())
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
}
pub fn base_random() -> Value {
Value::Number(random())
}
pub fn round(x: &Value) -> Value {
@ -610,7 +616,10 @@ pub fn make_base() -> Value {
("pi", Value::Number(std::f64::consts::PI)),
("print!", Value::BaseFn(BaseFn::Unary("print!", print))),
("process", Value::Process),
("random", Value::BaseFn(BaseFn::Nullary("random", random))),
(
"random",
Value::BaseFn(BaseFn::Nullary("random", base_random)),
),
("range", Value::BaseFn(BaseFn::Binary("range", range))),
("rest", Value::BaseFn(BaseFn::Unary("rest", rest))),
("round", Value::BaseFn(BaseFn::Unary("round", round))),

View File

@ -11,6 +11,12 @@ extern "C" {
async fn io (output: String) -> JsValue;
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: String);
}
type Lines = Value; // expect a list of values
type Commands = Value; // expect a list of values
type Url = Value; // expect a string representing a URL
@ -25,7 +31,7 @@ pub enum MsgOut {
Console(Lines),
Commands(Commands),
SlurpRequest(Url),
Complete(Result<Value, Panic>),
Complete(FinalValue),
}
impl std::fmt::Display for MsgOut {
@ -38,7 +44,10 @@ impl MsgOut {
pub fn to_json(&self) -> String {
match self {
MsgOut::Complete(value) => match value {
Ok(value) => make_json_payload("complete", value.to_json().unwrap()),
Ok(value) => {
log(format!("value is: {}", value.show()));
make_json_payload("complete", serde_json::to_string(&value.show()).unwrap())
},
Err(_) => make_json_payload("complete", "\"null\"".to_string())
},
MsgOut::Commands(commands) => {
@ -55,7 +64,7 @@ impl MsgOut {
}
MsgOut::Console(lines) => {
let lines = lines.as_list();
let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\n");
let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\\n");
let json_lines = format!("\"{json_lines}\"");
make_json_payload("console", json_lines)
}
@ -72,6 +81,16 @@ pub enum MsgIn {
Keyboard(Vec<String>),
}
impl std::fmt::Display for MsgIn {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MsgIn::Input(str) => write!(f, "input: {str}"),
MsgIn::Kill => write!(f, "kill"),
_ => todo!()
}
}
}
impl MsgIn {
pub fn to_value(self) -> Value {
match self {
@ -99,8 +118,14 @@ pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
let outbox = format!("[{}]", msgs.iter().map(|msg| msg.to_json()).collect::<Vec<_>>().join(","));
let inbox = io (outbox).await;
let inbox = inbox.as_string().expect("response should be a string");
log(format!("response is: {inbox}"));
let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");
if !inbox.is_empty() {
log("got messages in ludus!".to_string());
for msg in inbox.iter() {
log(format!("{}", msg));
}
}
inbox
}

View File

@ -1,6 +1,8 @@
use chumsky::{input::Stream, prelude::*};
use imbl::HashMap;
use wasm_bindgen::prelude::*;
use std::rc::Rc;
use std::cell::RefCell;
const DEBUG_SCRIPT_COMPILE: bool = false;
const DEBUG_SCRIPT_RUN: bool = false;
@ -15,7 +17,7 @@ use crate::ast::Ast;
mod base;
mod world;
use crate::world::World;
use crate::world::{World, Zoo};
mod spans;
use crate::spans::Spanned;
@ -42,10 +44,11 @@ mod value;
use value::Value;
mod vm;
use vm::Creature;
const PRELUDE: &str = include_str!("../assets/test_prelude.ld");
async fn prelude() -> HashMap<&'static str, Value> {
fn prelude() -> HashMap<&'static str, Value> {
let tokens = lexer().parse(PRELUDE).into_output_errors().0.unwrap();
let (parsed, parse_errors) = parser()
.parse(Stream::from_iter(tokens).map((0..PRELUDE.len()).into(), |(t, s)| (t, s)))
@ -71,7 +74,7 @@ async fn prelude() -> HashMap<&'static str, Value> {
if !validator.errors.is_empty() {
println!("VALIDATION ERRORS IN PRLUDE:");
report_invalidation(validator.errors);
panic!();
panic!("validator errors in prelude");
}
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parsed));
@ -88,25 +91,37 @@ async fn prelude() -> HashMap<&'static str, Value> {
compiler.compile();
let chunk = compiler.chunk;
let mut world = World::new(chunk, DEBUG_PRELUDE_RUN);
let stub_console = Value::r#box(Value::new_list());
world.run(stub_console).await;
let prelude = world.result.unwrap().unwrap();
log("compiled prelude");
let stub_zoo = Rc::new(RefCell::new(Zoo::new()));
let mut prld_sync = Creature::new(chunk, stub_zoo, DEBUG_PRELUDE_RUN);
prld_sync.interpret();
log("run prelude synchronously");
let prelude = prld_sync.result.unwrap().unwrap();
match prelude {
Value::Dict(hashmap) => *hashmap,
_ => unreachable!(),
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub async fn ludus(src: String) -> String {
console_error_panic_hook::set_once();
log("successfully entered ludus fn in Rust");
let src = src.to_string().leak();
log(src);
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {
return format!("{:?}", lex_errs);
}
let tokens = tokens.unwrap();
log("successfully tokenized source");
let (parse_result, parse_errors) = parser()
.parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s)))
@ -119,8 +134,10 @@ pub async fn ludus(src: String) -> String {
// This simplifies lifetimes, and
// in any event, the AST should live forever
let parsed: &'static Spanned<Ast> = Box::leak(Box::new(parse_result.unwrap()));
log("successfully parsed source");
let prelude = prelude().await;
let prelude = prelude();
log("successfully loaded prelude");
let postlude = prelude.clone();
// let prelude = imbl::HashMap::new();
@ -146,6 +163,7 @@ pub async fn ludus(src: String) -> String {
// compiler.bind("base");
compiler.compile();
log("successfully compiled source");
if DEBUG_SCRIPT_COMPILE {
println!("=== source code ===");
println!("{src}");
@ -159,13 +177,16 @@ pub async fn ludus(src: String) -> String {
let vm_chunk = compiler.chunk;
let mut world = World::new(vm_chunk, DEBUG_SCRIPT_RUN);
let mut world = World::new(vm_chunk, prelude.clone(), DEBUG_SCRIPT_RUN);
let console = prelude
.get("console")
.expect("prelude must have a console")
.clone();
world.run(console).await;
log("loaded world and console");
world.run().await;
let result = world.result.clone().unwrap();
log("ran script");
log(format!("{:?}", result).as_str());
let console = postlude.get("console").unwrap();
let Value::Box(console) = console else {

View File

@ -2,10 +2,9 @@ use rudus::ludus;
use std::env;
use std::fs;
#[tokio::main]
pub async fn main() {
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
let src = fs::read_to_string("sandbox.ld").unwrap();
let json = ludus(src).await;
println!("{json}");
let json = ludus(src);
// println!("{json}");
}

View File

@ -259,7 +259,9 @@ impl Value {
pub fn to_json(&self) -> Option<String> {
use Value::*;
match self {
True | False | String(..) | Interned(..) | Number(..) => Some(self.show()),
True | False | Number(..) => Some(self.show()),
String(string) => Some(serde_json::to_string(string.as_ref()).unwrap()),
Interned(str) => Some(serde_json::to_string(str).unwrap()),
Keyword(str) => Some(format!("\"{str}\"")),
List(members) => {
let mut joined = "".to_string();

View File

@ -339,7 +339,7 @@ impl Creature {
let Value::Number(ms) = args[1] else {
unreachable!()
};
self.zoo.as_ref().borrow_mut().sleep(self.pid, ms as usize);
self.zoo.as_ref().borrow_mut().sleep(self.pid, ms);
self.r#yield = true;
self.push(Value::Keyword("ok"));
}

View File

@ -2,14 +2,26 @@ use crate::chunk::Chunk;
use crate::value::Value;
use crate::vm::{Creature, Panic};
use crate::io::{MsgOut, MsgIn, do_io};
use ran::ran_u8;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::mem::swap;
use std::rc::Rc;
use std::time::{Duration, Instant};
use wasm_bindgen::prelude::*;
const ANIMALS: [&str; 24] = [
// Grab some JS stuff
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
#[wasm_bindgen(js_namespace = Date)]
fn now() -> f64;
}
const ANIMALS: [&str; 32] = [
"tortoise",
"hare",
"squirrel",
@ -31,9 +43,17 @@ const ANIMALS: [&str; 24] = [
"zebra",
"hyena",
"giraffe",
"leopard",
"lion",
"hippopotamus",
"capybara",
"python",
"gopher",
"crab",
"trout",
"osprey",
"lemur",
"wobbegong",
"walrus",
"opossum",
];
#[derive(Debug, Clone, PartialEq)]
@ -70,7 +90,7 @@ pub struct Zoo {
ids: HashMap<&'static str, usize>,
dead: HashSet<&'static str>,
kill_list: Vec<&'static str>,
sleeping: HashMap<&'static str, (Instant, Duration)>,
sleeping: HashMap<&'static str, f64>,
active_idx: usize,
active_id: &'static str,
}
@ -90,20 +110,27 @@ impl Zoo {
}
fn random_id(&self) -> String {
let rand = ran_u8() as usize % 24;
log("generating random id");
let rand_idx = (random() * 32.0) as usize;
log("random number!");
let idx = self.procs.len();
format!("{}_{idx}", ANIMALS[rand])
log("procs len");
format!("{}_{idx}", ANIMALS[rand_idx])
}
fn new_id(&self) -> &'static str {
log("creating new id");
let mut new = self.random_id();
log("got new ramdom id");
while self.dead.iter().any(|old| *old == new) {
new = self.random_id();
}
log(format!("got new id: {}", new).as_str());
new.leak()
}
pub fn put(&mut self, mut proc: Creature) -> &'static str {
log("putting creature");
if self.empty.is_empty() {
let id = self.new_id();
let idx = self.procs.len();
@ -113,7 +140,7 @@ impl Zoo {
id
} else {
let idx = self.empty.pop().unwrap();
let rand = ran_u8() as usize % 24;
let rand = (random() * 32.0) as usize;
let id = format!("{}_{idx}", ANIMALS[rand]).leak();
proc.pid = id;
self.ids.insert(id, idx);
@ -126,9 +153,9 @@ impl Zoo {
self.kill_list.push(id);
}
pub fn sleep(&mut self, id: &'static str, ms: usize) {
pub fn sleep(&mut self, id: &'static str, ms: f64) {
self.sleeping
.insert(id, (Instant::now(), Duration::from_millis(ms as u64)));
.insert(id, now() + ms);
}
pub fn is_alive(&self, id: &'static str) -> bool {
@ -161,7 +188,7 @@ impl Zoo {
}
self.sleeping
.retain(|_, (instant, duration)| instant.elapsed() < *duration);
.retain(|_, wakeup_time| now() < *wakeup_time);
println!(
"currently sleeping processes: {}",
@ -232,25 +259,72 @@ impl Zoo {
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Buffers {
console: Value,
// commands: Value,
// fetch_outbox: Value,
// fetch_inbox: Value,
input: Value,
}
impl Buffers {
pub fn new (prelude: imbl::HashMap<&'static str, Value>) -> Buffers {
Buffers {
console: prelude.get("console").unwrap().clone(),
// commands: prelude.get("commands").unwrap().clone(),
// fetch_outbox: prelude.get("fetch_outbox").unwrap().clone(),
// fetch_inbox: prelude.get("fetch_inbox").unwrap().clone(),
input: prelude.get("input").unwrap().clone(),
}
}
pub fn console (&self) -> Rc<RefCell<Value>> {
self.console.as_box()
}
pub fn input (&self) -> Rc<RefCell<Value>> {
self.input.as_box()
}
// pub fn commands (&self) -> Rc<RefCell<Value>> {
// self.commands.as_box()
// }
// pub fn fetch_outbox (&self) -> Rc<RefCell<Value>> {
// self.fetch_outbox.as_box()
// }
// pub fn fetch_inbox (&self) -> Rc<RefCell<Value>> {
// self.fetch_inbox.as_box()
// }
}
#[derive(Debug, Clone, PartialEq)]
pub struct World {
zoo: Rc<RefCell<Zoo>>,
active: Option<Creature>,
main: &'static str,
pub result: Option<Result<Value, Panic>>,
buffers: Buffers,
last_io: f64,
kill_signal: bool,
}
impl World {
pub fn new(chunk: Chunk, debug: bool) -> World {
pub fn new(chunk: Chunk, prelude: imbl::HashMap<&'static str, Value>, debug: bool) -> World {
let zoo = Rc::new(RefCell::new(Zoo::new()));
let main = Creature::new(chunk, zoo.clone(), debug);
let id = zoo.as_ref().borrow_mut().put(main);
let id = zoo.borrow_mut().put(main);
let buffers = Buffers::new(prelude);
World {
zoo,
active: None,
main: id,
result: None,
buffers,
last_io: 0.0,
kill_signal: false,
}
}
@ -266,111 +340,100 @@ impl World {
swap(&mut new_active_opt, &mut self.active);
}
pub fn activate_main(&mut self) {
let main = self.zoo.as_ref().borrow_mut().catch(self.main);
fn activate_main(&mut self) {
let main = self.zoo.borrow_mut().catch(self.main);
self.active = Some(main);
}
pub fn active_id(&mut self) -> &'static str {
fn active_id(&mut self) -> &'static str {
self.active.as_ref().unwrap().pid
}
pub fn kill_active(&mut self) {
fn kill_active(&mut self) {
let id = self.active_id();
self.zoo.as_ref().borrow_mut().kill(id);
}
pub fn active_result(&mut self) -> &Option<Result<Value, Panic>> {
fn active_result(&mut self) -> &Option<Result<Value, Panic>> {
&self.active.as_ref().unwrap().result
}
// TODO: add memory io places to this signature
// * console
// * input
// * commands
// * slurp
pub async fn run(
&mut self,
console: Value,
// input: Value,
// commands: Value,
// slurp_out: Value,
// slurp_in: Value,
) {
self.activate_main();
// let Value::Box(input) = input else {unreachable!()};
// let Value::Box(commands) = commands else {unreachable!()};
// let Value::Box(slurp) = slurp else {
// unreachable!()};
let mut last_io = Instant::now();
let mut kill_signal = false;
loop {
if kill_signal {
// TODO: send a last message to the console
println!("received KILL signal");
return;
}
println!(
"entering world loop; active process is {}",
self.active_id()
);
self.active.as_mut().unwrap().interpret();
println!("yielded from {}", self.active_id());
match self.active_result() {
None => (),
Some(_) => {
if self.active_id() == self.main {
let result = self.active_result().clone().unwrap();
self.result = Some(result.clone());
//TODO: capture any remaining console or command values
do_io(vec![MsgOut::Complete(result)]);
return;
}
println!(
"process {} died with {:?}",
self.active_id(),
self.active_result().clone()
);
self.kill_active();
}
}
println!("getting next process");
self.next();
// TODO:: if enough time has elapsed (how much?) run i/o
// 10 ms is 100hz, so that's a nice refresh rate
if Instant::now().duration_since(last_io) > Duration::from_millis(10) {
// gather io
// compile it into messages
// serialize it
fn flush_buffers(&mut self) -> Vec<MsgOut> {
let mut outbox = vec![];
if let Some(console) = flush_console(&console) {
if let Some(console) = self.flush_console() {
outbox.push(console);
};
// TODO: slurp
// TODO: commands
// send it
// await the response
let inbox = do_io(outbox).await;
// unpack the response into messages
for msg in inbox {
match msg {
MsgIn::Kill => kill_signal = true,
_ => todo!()
}
outbox
}
// update
last_io = Instant::now();
}
}
}
}
fn flush_console(console: &Value) -> Option<MsgOut> {
let console = console.as_box();
fn flush_console(&self) -> Option<MsgOut> {
let console = self.buffers.console();
let working_copy = RefCell::new(Value::new_list());
console.swap(&working_copy);
let working_value = working_copy.borrow();
if working_value.as_list().is_empty() { return None; }
Some(MsgOut::Console(working_value.clone()))
}
fn complete_main(&mut self) -> Vec<MsgOut> {
let mut outbox = self.flush_buffers();
// TODO: if we have a panic, actually add the panic message to the console
let result = self.active_result().clone().unwrap();
self.result = Some(result.clone());
outbox.push(MsgOut::Complete(result));
outbox
}
fn interpret_active(&mut self) {
self.active.as_mut().unwrap().interpret();
}
async fn maybe_do_io(&mut self) {
if self.last_io + 10.0 > now () {
let outbox = self.flush_buffers();
let inbox = do_io(outbox).await;
self.fill_buffers(inbox);
}
self.last_io = now();
}
fn fill_input(&mut self, str: String) {
let value = Value::string(str);
let working = RefCell::new(value);
let input = self.buffers.input();
input.swap(&working);
}
fn fill_buffers(&mut self, inbox: Vec<MsgIn>) {
for msg in inbox {
match msg {
MsgIn::Input(str) => self.fill_input(str),
MsgIn::Kill => self.kill_signal = true,
_ => todo!()
}
}
}
pub async fn run(&mut self) {
self.activate_main();
loop {
if self.kill_signal {
let outbox = self.flush_buffers();
do_io(outbox).await;
return;
}
self.interpret_active();
if self.active_result().is_some() {
if self.active_id() == self.main {
let outbox = self.complete_main();
do_io(outbox).await;
return;
}
self.kill_active();
}
self.next();
self.maybe_do_io().await;
}
}
}