maybe figure out the wasm thing?

This commit is contained in:
Scott Richmond 2025-06-25 22:56:39 -04:00
parent b78063ba07
commit 44f7ce7b06
16 changed files with 1847 additions and 169 deletions

View File

@ -12,13 +12,13 @@ crate-type = ["cdylib", "rlib"]
ariadne = { git = "https://github.com/zesterer/ariadne" }
chumsky = { git = "https://github.com/zesterer/chumsky", features = ["label"] }
imbl = "3.0.0"
# struct_scalpel = "0.1.1"
ran = "2.0.1"
# rust-embed = "8.5.0"
# boxing = "0.1.2"
# ordered-float = "4.5.0"
# index_vec = "0.1.4"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.11.1"
wasm-bindgen = "0.2"
# struct_scalpel = "0.1.1"
# rust-embed = "8.5.0"
# boxing = "0.1.2"
# ordered-float = "4.5.0"
# index_vec = "0.1.4"

View File

@ -522,117 +522,6 @@ SOLUTION: test to see if the function has been forward-declared, and if it has,
NEW PROBLEM: a lot of instructions in the VM don't properly offset from the call frame's stack base, which leads to weirdness when doing things inside function calls.
NEW SOLUTION: create a function that does the offset properly, and replace everywhere we directly access the stack.
<<<<<<< Updated upstream
This is the thing I am about to do
||||||| Stash base
This is the thing I am about to do.
### I think the interpreter, uh, works?
#### 2025-06-24
I'm sure I'll find some small problems.
But right now the thing works.
At the moment, I'm thinking about how to get good error messages.
Panics are difficult.
And I'm worried about ariadne as the error reporting crate.
Since it writes to stdout, it has all kinds of escape codes.
I need a plain ass string, at least for the web frontend.
So.
Current task, however, is how to get reasonable panic error messages.
Let's simplify the problem.
First, let's get tracebacks and line numbers.
We use Chumsky spanned ASTs.
The span is just a range into an str.
What I can do is pretty stupidly just generate line numbers from the spans in the compiler, and from there, get a reasonable traceback.
So instead of using Ariadne's fancy report builder, let's just do something more what we already have here:
```
Ludus panicked! no match
on line 1 in input,
calling: add
with arguments: ("foo")
expected match with one of:
()
(x as :number)
(x as :number, y as :number)
(x, y, ...zs)
((x1, y1), (x2, y2))
>>> add ("foo")
......^
```
We need:
* the location of the function call in terms of the line number
* the arguments used
* the patterns expected to match (special cases: `let` vs `match` vs `fn`)
That means, for bookkeeping, we need:
* In the compiler, line number
* In the VM, the arguments
* In the VM, the pattern AST node.
In Janet-Ludus, there are only a few types of panic:
* `fn-no-match`: no match against a function call
* `let-no-match`: no match against a `let` binding
* `match-no-match`: no match against a `match` form
* `generic-panic`: everything else
* `runtime-error`: an internal Ludus error
The first three are simply formatting differences.
There are no tracebacks.
Tracebacks should be easy enough, although there's some fiddly bits.
While it's nice to have the carret, the brutalist attempt here should be just to give us the line--since the carret isn't exactly precise in the Janet interpereter.
And the traceback should look something like:
```
calling foo with (:bar, :baz)
at line 12 in input
calling bar with ()
at line 34 in prelude
calling baz with (1, 2, 3)
at line 12 in input
```
Which means, again: function names, ip->line conversion, and arguments.
The runtime needs a representation of the patterns in _any_ matching form.
The speed is so much greater now that I'm not so concerned about little optimizations.
So: a chunk needs a vec of patterns-representations. (I'm thinking simply a `Vec<String>`.)
So does a function, for `doc!`.
Same same re: `Vec<String>`.
A VM needs a register for the scrutinee (which with function calls is just the arguments, already captured).
A VM also needs a register for the pattern.
So when there's a no match, we just yank the pattern and the scrutinee out of these registers.
This all seems very straightforward compared to the compiling & VM stuff.
Here's some stub code I wrote for dealing with ranges, source, line numbers:
```rust
let str = "foo bar baz\nquux frob\nthing thing thing";
let range = 0..4;
println!("{}", str.get(range).unwrap());
let lines: Vec<&str> = str.split_terminator("\n").collect();
println!("{:?}", lines);
let idx = 20;
let mut line_no = 1;
for i in 0..idx {
if str.chars().nth(i).unwrap() == '\n' {
line_no += 1;
}
}
println!("line {line_no}: {}", lines[line_no - 1]);
```
=======
This is the thing I am about to do.
### I think the interpreter, uh, works?
@ -756,4 +645,23 @@ println!("line {line_no}: {}", lines[line_no - 1]);
* Users can create their own (public) repos and put stuff in there.
* We still want saving text output from web Ludus
* Later, with perceptrons & the book, we'll need additional solutions.
>>>>>>> Stashed changes
#### Integration hell
As predicted, Javascript is the devil.
wasm-pack has several targets:
* nodejs -> this should be what we want
* web -> this could be what we want
* bundler -> webpack confuses me
The simplest, shortest route should be to create a viable nodejs library.
It works.
I can wire up the wasm-pack output with a package.json, pull it down from npm, and it work.
However, because of course, vite, which svelte uses, doesn't like this.
We get an error that `TextEncoder is not a constructor`.
This, apparently, has something to do with the way that vite packages up node libraries?
See https://github.com/vitejs/vite/discussions/12826.
Web, in some ways, is even more straightforward.
It produces an ESM that just works in the browser.
And

1594
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,9 @@
"keywords": [],
"author": "Scott Richmond",
"license": "GPL-3.0",
"scripts": {
"build": "webpack"
},
"files": [
"pkg/rudus.js",
"pkg/ludus.js",
@ -15,5 +18,9 @@
"pkg/rudus_bg.wasm.d.ts",
"pkg/rudus.d.ts"
],
"devDependencies": {}
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}
}

2
pkg/.gitignore vendored
View File

@ -1,2 +0,0 @@
*.wasm
*.wasm*

3
pkg/README.md Normal file
View File

@ -0,0 +1,3 @@
# rudus
A Rust implementation of Ludus.

21
pkg/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<title>Testing Ludus/WASM integration</title>
</head>
<body>
<script type="module">
import {run} from "./ludus.js";
window.ludus = run;
console.log(run(":foobar"));
</script>
<p>
Open the console. All the action's in there.
</p>
</body>
</html>

View File

@ -1,27 +1,29 @@
const mod = require("./rudus.js");
import init, {ludus} from "./rudus.js";
await init();
let res = null
let code = null
function run (source) {
export function run (source) {
code = source
const output = mod.run(source)
const output = ludus(source)
res = JSON.parse(output)
return res
}
function stdout () {
export function stdout () {
if (!res) return ""
return res.io.stdout.data
}
function turtle_commands () {
export function turtle_commands () {
if (!res) return []
return res.io.turtle.data
}
function result () {
export function result () {
return res
}
@ -240,7 +242,7 @@ function escape_svg (svg) {
.replace(/'/g, "&apos;")
}
function extract_ludus (svg) {
export function extract_ludus (svg) {
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
return code
.replace(/&amp;/g, "&")
@ -280,7 +282,7 @@ function svg_render_turtle (state) {
`
}
function svg (commands) {
export function svg (commands) {
// console.log(commands)
const states = [turtle_init]
commands.reduce((prev_state, command) => {
@ -349,7 +351,7 @@ function p5_render_turtle (state, calls) {
return calls
}
function p5 (commands) {
export function p5 (commands) {
const states = [turtle_init]
commands.reduce((prev_state, command) => {
const new_state = command_to_state(prev_state, command)
@ -374,12 +376,3 @@ function p5 (commands) {
return p5_calls
}
module.exports = {
run,
stdout,
turtle_commands,
result,
extract_ludus,
p5,
svg,
}

15
pkg/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "rudus",
"type": "module",
"version": "0.0.1",
"files": [
"rudus_bg.wasm",
"rudus.js",
"rudus.d.ts"
],
"main": "rudus.js",
"types": "rudus.d.ts",
"sideEffects": [
"./snippets/*"
]
}

35
pkg/rudus.d.ts vendored
View File

@ -1,3 +1,36 @@
/* tslint:disable */
/* eslint-disable */
export function run(src: string): string;
export function ludus(src: string): 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 __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_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

View File

@ -1,8 +1,4 @@
let imports = {};
imports['__wbindgen_placeholder__'] = module.exports;
let wasm;
const { TextEncoder, TextDecoder } = require(`util`);
let WASM_VECTOR_LEN = 0;
@ -15,7 +11,7 @@ function getUint8ArrayMemory0() {
return cachedUint8ArrayMemory0;
}
let cachedTextEncoder = new TextEncoder('utf-8');
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
@ -69,9 +65,9 @@ function passStringToWasm0(arg, malloc, realloc) {
return ptr;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
cachedTextDecoder.decode();
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
@ -81,22 +77,56 @@ function getStringFromWasm0(ptr, len) {
* @param {string} src
* @returns {string}
*/
module.exports.run = function(src) {
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.run(ptr0, len0);
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);
}
};
}
module.exports.__wbindgen_init_externref_table = function() {
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_0;
const offset = table.grow(4);
table.set(0, undefined);
@ -107,13 +137,75 @@ module.exports.__wbindgen_init_externref_table = function() {
;
};
const path = require('path').join(__dirname, 'rudus_bg.wasm');
const bytes = require('fs').readFileSync(path);
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedUint8ArrayMemory0 = null;
const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
wasm = wasmInstance.exports;
module.exports.__wasm = wasm;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('rudus_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

BIN
pkg/rudus_bg.wasm Normal file

Binary file not shown.

9
pkg/rudus_bg.wasm.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/* 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 __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_start: () => void;

5
pkg/test.js Normal file
View File

@ -0,0 +1,5 @@
import * as mod from "./ludus.js";
console.log(mod.run(`
:foobar
`));

View File

@ -90,7 +90,7 @@ fn prelude() -> HashMap<&'static str, Value> {
}
#[wasm_bindgen]
pub fn run(src: String) -> String {
pub fn ludus(src: String) -> String {
let src = src.to_string().leak();
let (tokens, lex_errs) = lexer().parse(src).into_output_errors();
if !lex_errs.is_empty() {

View File

@ -1,10 +1,10 @@
use rudus::run;
use rudus::ludus;
use std::env;
use std::fs;
pub fn main() {
env::set_var("RUST_BACKTRACE", "1");
let src = fs::read_to_string("sandbox.ld").unwrap();
let json = run(src);
let json = ludus(src);
println!("{json}");
}