diff --git a/examples/demo/whisp/meta.ts b/examples/demo/whisp/meta.ts deleted file mode 100644 index e69de29..0000000 diff --git a/examples/whisp/index.ts b/examples/whisp/index.ts new file mode 100644 index 0000000..8d0f765 --- /dev/null +++ b/examples/whisp/index.ts @@ -0,0 +1,8 @@ +import whisp from './whisp'; +import { readFile } from 'fs/promises'; + +const file = process.argv[2]; +whisp`(write "running ${file}:\n\n"`; + +const source = await readFile(file, 'utf8'); +whisp(source); diff --git a/examples/demo/whisp/parser.ts b/examples/whisp/parser.ts similarity index 76% rename from examples/demo/whisp/parser.ts rename to examples/whisp/parser.ts index 50d6dae..1be2d12 100644 --- a/examples/demo/whisp/parser.ts +++ b/examples/whisp/parser.ts @@ -11,6 +11,10 @@ interface ParseContext { } const openParen = (ctx: ParseContext): void => { + if (!ctx.head) { + const around = ctx.source.substring(Math.max(0, ctx.i - 20), ctx.i + 20); + throw new Error(`Unexpected '(' at position ${ctx.i} (stack empty): ...${around}...`); + } const temp: Types.Expr[] = []; ctx.head.push(temp); ctx.stack.push(ctx.head); @@ -19,6 +23,10 @@ const openParen = (ctx: ParseContext): void => { const closeParen = (ctx: ParseContext): void => { flushToken(ctx); + if (ctx.stack.length === 0) { + const around = ctx.source.substring(Math.max(0, ctx.i - 20), ctx.i + 20); + throw new Error(`Unmatched ')' at position ${ctx.i}: ...${around}...`); + } ctx.head = ctx.stack.pop()!; }; @@ -45,10 +53,16 @@ const parseString = (ctx: ParseContext): void => { ctx.head.push(Types.Leaf.string(str)); }; +const skipComment = (ctx: ParseContext): void => { + flushToken(ctx); + while (ctx.i < ctx.source.length && ctx.source[ctx.i] !== '\n') ctx.i++; +}; + const tokenTable: Record = { '"': parseString, '(': openParen, ')': closeParen, + ';': skipComment, ' ': flushToken, '\n': flushToken, '\r': flushToken, diff --git a/examples/whisp/tests/bunny.wsp b/examples/whisp/tests/bunny.wsp new file mode 100644 index 0000000..3b5da48 --- /dev/null +++ b/examples/whisp/tests/bunny.wsp @@ -0,0 +1,339 @@ +;; === opcodes === +(let CONST 0) +(let LOAD 1) +(let ADD 2) +(let CALL 3) +(let RETURN 4) +(let EXTERN 5) +(let HALT 6) + +;; === token types === +(let T_EOF 0) +(let T_FN 1) +(let T_RET 2) +(let T_IDENT 3) +(let T_NUM 4) +(let T_STR 5) +(let T_LPAREN 6) +(let T_RPAREN 7) +(let T_LBRACE 8) +(let T_RBRACE 9) +(let T_COMMA 10) +(let T_SEMI 11) +(let T_PLUS 12) +(let T_DOT 13) + +;; === vm state === +(let code (array)) +(let stack (array)) +(let callstack (array)) +(let functions (array)) +(let output (array)) +(let entry 0) + +;; === extern table === +(let externs (array + (array "std.io.println" 1 0) + (array "bunny.squeak" 0 1))) + +;; === lexer state === +(let src "") +(let pos 0) +(let cur_type 0) +(let cur_val "") +(let cur_num 0) + +(fn is_alpha (c) + (let cc (. c charCodeAt 0)) + (or (and (>= cc 65) (<= cc 90)) + (and (>= cc 97) (<= cc 122)))) + +(fn is_digit (c) + (let cc (. c charCodeAt 0)) + (and (>= cc 48) (<= cc 57))) + +(fn is_alnum (c) + (or (is_alpha c) (is_digit c))) + +(fn is_space (c) + (or (= c " ") (or (= c "\n") (or (= c "\r") (= c "\t"))))) + +(fn char_at (i) + (if (< i (. src length)) (. src charAt i) "")) + +(fn skip_ws () + (loop (and (< pos (. src length)) (is_space (char_at pos))) + (mut! pos (+ pos 1)))) + +(fn next_token () + (skip_ws) + (if (>= pos (. src length)) + (do (mut! cur_type T_EOF) (mut! cur_val "") 0) + (do + (let c (char_at pos)) + (cond + (is_alpha c) (do + (let start pos) + (loop (and (< pos (. src length)) (or (is_alnum (char_at pos)) (= (char_at pos) "_"))) + (mut! pos (+ pos 1))) + (let word (. src slice start pos)) + (cond + (= word "fn") (do (mut! cur_type T_FN) (mut! cur_val word)) + (= word "return") (do (mut! cur_type T_RET) (mut! cur_val word)) + (do (mut! cur_type T_IDENT) (mut! cur_val word)))) + + (is_digit c) (do + (let start pos) + (loop (and (< pos (. src length)) (is_digit (char_at pos))) + (mut! pos (+ pos 1))) + (mut! cur_type T_NUM) + (mut! cur_num (Number.parseInt (. src slice start pos))) + (mut! cur_val "")) + + (= c "\"") (do + (mut! pos (+ pos 1)) + (let start pos) + (loop (and (< pos (. src length)) (!= (char_at pos) "\"")) + (mut! pos (+ pos 1))) + (mut! cur_type T_STR) + (mut! cur_val (. src slice start pos)) + (mut! pos (+ pos 1))) + + (do + (mut! pos (+ pos 1)) + (cond + (= c "(") (mut! cur_type T_LPAREN) + (= c ")") (mut! cur_type T_RPAREN) + (= c "{") (mut! cur_type T_LBRACE) + (= c "}") (mut! cur_type T_RBRACE) + (= c ",") (mut! cur_type T_COMMA) + (= c ";") (mut! cur_type T_SEMI) + (= c "+") (mut! cur_type T_PLUS) + (= c ".") (mut! cur_type T_DOT) + (mut! cur_type T_EOF)) + (mut! cur_val "")))))) + +(fn advance () (next_token)) + +(fn emit (op operand) + (push! code (array op operand))) + +(fn find_function (name) + (let i 0) + (let result -1) + (loop (and (< i (. functions length)) (= result -1)) + (do + (if (= (get (get functions i) 0) name) + (mut! result i) 0) + (mut! i (+ i 1)))) + result) + +(fn find_extern (name) + (let i 0) + (let result -1) + (loop (and (< i (. externs length)) (= result -1)) + (do + (if (= (get (get externs i) 0) name) + (mut! result i) 0) + (mut! i (+ i 1)))) + result) + +(fn find_param (func name) + (if (= func -1) -1 + (do + (let f (get functions func)) + (let params (get f 2)) + (let nparams (get f 1)) + (let i 0) + (let result -1) + (loop (and (< i nparams) (= result -1)) + (do + (if (= (get params i) name) + (mut! result (- nparams (+ i 1))) 0) + (mut! i (+ i 1)))) + result))) + +(fn parse_qname () + (if (!= cur_type T_IDENT) "" + (do + (let name cur_val) + (advance) + (loop (= cur_type T_DOT) + (do + (advance) + (if (= cur_type T_IDENT) + (do (mut! name (concat name "." cur_val)) + (advance)) 0))) + name))) + +(fn parse_expr (func) + (cond + (= cur_type T_NUM) (do + (emit CONST cur_num) + (advance)) + + (= cur_type T_STR) (do + (emit CONST cur_val) + (advance)) + + (= cur_type T_IDENT) (do + (let name (parse_qname)) + (if (= cur_type T_LPAREN) + (parse_call func name) + (do + (let offset (find_param func name)) + (emit LOAD offset))))) + + (if (= cur_type T_PLUS) + (do (advance) (parse_expr func) (emit ADD 0)) 0)) + +(fn parse_call (func name) + (advance) + (if (!= cur_type T_RPAREN) + (do + (parse_expr func) + (loop (= cur_type T_COMMA) + (do (advance) (parse_expr func)))) 0) + (advance) + + (let ext (find_extern name)) + (if (!= ext -1) + (emit EXTERN ext) + (emit CALL (find_function name)))) + +(fn parse_function () + (advance) + (let fname cur_val) + (advance) + (advance) + + (let params (array)) + (if (= cur_type T_IDENT) + (do + (push! params cur_val) + (advance) + (loop (= cur_type T_COMMA) + (do (advance) + (push! params cur_val) + (advance)))) 0) + (advance) + (advance) + + (let fidx (. functions length)) + (push! functions (array fname (. params length) params (. code length))) + + (loop (!= cur_type T_RBRACE) + (if (= cur_type T_RET) + (do + (advance) + (parse_expr fidx) + (emit RETURN 0) + (advance)) 0)) + (advance)) + +(fn parse_program () + (loop (= cur_type T_FN) + (parse_function)) + + (mut! entry (. code length)) + + (loop (!= cur_type T_EOF) + (if (= cur_type T_IDENT) + (do + (let name (parse_qname)) + (if (= cur_type T_LPAREN) + (do (parse_call -1 name) + (if (= cur_type T_SEMI) (advance) 0)) 0)) + (advance))) + + (emit HALT 0)) + +(fn vm_run () + (let pc entry) + (let fp 0) + + (loop (< pc (. code length)) + (do + (let instr (get code pc)) + (let op (get instr 0)) + (let operand (get instr 1)) + + (cond + (= op CONST) (do + (push! stack operand) + (mut! pc (+ pc 1))) + + (= op LOAD) (do + (push! stack (get stack (+ fp operand))) + (mut! pc (+ pc 1))) + + (= op ADD) (do + (let b (. stack pop)) + (let a (. stack pop)) + (push! stack (+ a b)) + (mut! pc (+ pc 1))) + + (= op CALL) (do + (let f (get functions operand)) + (push! callstack (+ pc 1)) + (push! callstack fp) + (mut! fp (- (. stack length) (get f 1))) + (mut! pc (get f 3))) + + (= op RETURN) (do + (let ret (. stack pop)) + (mut! stack (. stack slice 0 fp)) + (mut! fp (. callstack pop)) + (mut! pc (. callstack pop)) + (push! stack ret)) + + (= op EXTERN) (do + (let ext (get externs operand)) + (let eid (get ext 2)) + (cond + (= eid 0) (do + (let val (. stack pop)) + (push! output (concat val)) + (push! stack 0)) + (= eid 1) (do + (push! output "squeak") + (push! stack 0))) + (mut! pc (+ pc 1))) + + (= op HALT) + (mut! pc (. code length)))))) + +(fn print_bytecode () + (let names (array "CONST" "LOAD" "ADD" "CALL" "RETURN" "EXTERN" "HALT")) + (write "bytecode:\n") + (let i 0) + (loop (< i (. code length)) + (do + (let instr (get code i)) + (write " " i ": " (get names (get instr 0)) " " (get instr 1) "\n") + (mut! i (+ i 1))))) + +(let source "fn add(a, b) { + return a + b; +} +bunny.squeak(); +std.io.println(add(5, 10)); +std.io.println(\"hello world\");") + +(write "Source:\n\n" source "\n\n") +(write (concat (. "=" repeat 40) "\n\n")) + +(mut! src source) +(mut! pos 0) +(next_token) +(parse_program) +(print_bytecode) + +(write "\nOutput:\n") +(vm_run) + +(let i 0) +(loop (< i (. output length)) + (do + (write (get output i) "\n") + (mut! i (+ i 1)))) diff --git a/examples/demo/whisp/index.ts b/examples/whisp/tests/fib.wsp similarity index 84% rename from examples/demo/whisp/index.ts rename to examples/whisp/tests/fib.wsp index e7fd456..9966e5d 100644 --- a/examples/demo/whisp/index.ts +++ b/examples/whisp/tests/fib.wsp @@ -1,7 +1,4 @@ -import whisp from './whisp'; - -whisp` -(fn fib n a b +(fn fib (n a b) (if (= n 0) a (fib (- n 1) b (+ a b)))) @@ -12,4 +9,3 @@ whisp` (write "fibonacci(30) = " result "\n") (write "Time: " (. elapsed toFixed 4) " ms (" (. (* elapsed 1000) toFixed 2) " µs)\n")) -`; diff --git a/examples/demo/whisp/types.ts b/examples/whisp/types.ts similarity index 100% rename from examples/demo/whisp/types.ts rename to examples/whisp/types.ts diff --git a/examples/demo/whisp/whisp.ts b/examples/whisp/whisp.ts similarity index 80% rename from examples/demo/whisp/whisp.ts rename to examples/whisp/whisp.ts index c476e88..4731901 100644 --- a/examples/demo/whisp/whisp.ts +++ b/examples/whisp/whisp.ts @@ -141,7 +141,7 @@ function l_is_lambda(args: Types.Expr[], env: Types.Env): number { } function l_loop(args: Types.Expr[], env: Types.Env): number { - while (evaluate(args[0], env) === TRUE) evaluate(args[1], env); + while (evaluate(args[0], env)) evaluate(args[1], env); return -1; } @@ -151,6 +151,12 @@ function l_set(args: Types.Expr[], env: Types.Env): Types.LispValue[] { return array; } +function l_push(args: Types.Expr[], env: Types.Env): Types.LispValue[] { + const array = Eval.arr(args[0], env); + array.push(evaluate(args[1], env)); + return array; +} + function l_pop(args: Types.Expr[], env: Types.Env): Types.LispValue[] { const array = Eval.arr(args[0], env); array.pop(); @@ -162,15 +168,12 @@ function l_write(args: Types.Expr[], env: Types.Env): Types.LispValue { return FALSE; } -function l_dot(args: Types.Expr[], env: Types.Env): Types.LispValue { - const obj: any = evaluate(args[0], env); - const method = String(Eval.name(args[1])); - return obj[method](...args.slice(2).map(a => evaluate(a, env))); +function l_neq(args: Types.Expr[], env: Types.Env): number { + return +(evaluate(args[0], env) !== evaluate(args[1], env)); } -function l_fn(args: Types.Expr[], env: Types.Env): Types.LispValue { - const name = Eval.name(args[0]); - return (env[name] = l_lambda(args.slice(1), env)); +function l_concat(args: Types.Expr[], env: Types.Env): string { + return args.map(a => String(evaluate(a, env))).join(''); } function l_resolve(name: string, env: Types.Env) { @@ -179,6 +182,34 @@ function l_resolve(name: string, env: Types.Env) { return { target: env, value: env[name] }; } +function l_dot(args: Types.Expr[], env: Types.Env): Types.LispValue { + const obj: any = evaluate(args[0], env); + const member = String(Eval.name(args[1])); + const val = obj[member]; + if (typeof val === 'function') return val.call(obj, ...args.slice(2).map(a => evaluate(a, env))); + return val as Types.LispValue; +} + +function l_mut(args: Types.Expr[], env: Types.Env): Types.LispValue { + const name = String(Eval.name(args[0])); + const val = evaluate(args[1], env); + let scope: any = env; + while (scope && !Object.prototype.hasOwnProperty.call(scope, name)) { + scope = Object.getPrototypeOf(scope); + } + if (scope) scope[name] = val; + else env[name] = val; + return val; +} + +function l_cond(args: Types.Expr[], env: Types.Env): Types.LispValue { + for (let i = 0; i < args.length - 1; i += 2) { + if (evaluate(args[i], env)) return evaluate(args[i + 1], env); + } + if (args.length % 2 === 1) return evaluate(args.at(-1)!, env); + return FALSE; +} + function l_lambda(args: Types.Expr[], env: Types.Env): Types.LispFn { const params = args.slice(0, -1); return (props: Types.Expr[] = [], scope: Types.Env) => { @@ -188,6 +219,18 @@ function l_lambda(args: Types.Expr[], env: Types.Env): Types.LispFn { }; } +function l_fn(args: Types.Expr[], env: Types.Env): Types.LispValue { + const name = Eval.name(args[0]); + const paramList = args[1] as Types.Expr[]; + const bodyExprs = args.slice(2); + const fn: Types.LispFn = (props: Types.Expr[] = [], scope: Types.Env) => { + const localEnv: Types.Env = Object.create(env); + paramList.forEach((param, i) => (localEnv[Eval.name(param)] = evaluate(props[i], scope))); + return bodyExprs.reduce((_, x) => evaluate(x, localEnv), 0); + }; + return (env[name] = fn); +} + function jsResolve(path: string): { target: any; value: any } { const parts = path.split('.'); let target: any = globalThis; @@ -225,9 +268,14 @@ const keywords: Types.Env = { ['atom?']: l_is_atom, ['lambda?']: l_is_lambda, ['loop']: l_loop, + ['!=']: l_neq, ['set!']: l_set, + ['push!']: l_push, ['pop!']: l_pop, + ['mut!']: l_mut, ['write']: l_write, + ['concat']: l_concat, + ['cond']: l_cond, ['.']: l_dot, ['lambda']: l_lambda, ['fn']: l_fn diff --git a/include/silver/ast.h b/include/silver/ast.h index 7f58c0b..6e02ec1 100644 --- a/include/silver/ast.h +++ b/include/silver/ast.h @@ -78,7 +78,7 @@ typedef enum { typedef enum { SV_VAR_VAR, - SV_VAR_LET_ASN, + SV_VAR_LET, SV_VAR_CONST, } sv_var_kind_t; diff --git a/src/ant.c b/src/ant.c index 510fb94..0ba7580 100644 --- a/src/ant.c +++ b/src/ant.c @@ -12664,6 +12664,19 @@ static bool js_try_get(ant_t *js, jsval_t obj, const char *key, jsval_t *out) { *out = proxy_get(js, obj, key, key_len); return true; } + + if (t == T_STR || t == T_NUM || t == T_BOOL) { + if (t == T_STR && key_len == 6 && memcmp(key, "length", 6) == 0) { + jsoff_t byte_len = 0; jsoff_t str_off = vstr(js, obj, &byte_len); + const char *str_data = (const char *)&js->mem[str_off]; + *out = tov((double)utf16_strlen(str_data, byte_len)); + return true; + } + jsval_t boxed = mkobj(js, 0); + js_set_slot(js, js_as_obj(boxed), SLOT_PRIMITIVE, obj); + obj = boxed; t = T_OBJ; + } + if (is_promise) obj = js_as_obj(obj); else if (t != T_OBJ) return false; jsoff_t off = lkp(js, obj, key, key_len); diff --git a/src/types/modules/fs.d.ts b/src/types/modules/fs.d.ts index 4a0c751..8d81831 100644 --- a/src/types/modules/fs.d.ts +++ b/src/types/modules/fs.d.ts @@ -9,24 +9,35 @@ declare module 'fs' { isSymbolicLink(): boolean; } + type Encoding = 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'latin1' | 'binary' | 'base64' | 'base64url' | 'hex' | 'ascii'; + const constants: { F_OK: number; R_OK: number; W_OK: number; X_OK: number; + O_RDONLY: number; + O_WRONLY: number; + O_RDWR: number; + O_CREAT: number; + O_EXCL: number; + O_TRUNC: number; + O_APPEND: number; }; const promises: typeof import('fs/promises'); - function readFile(path: string): Promise; - function readFileSync(path: string): string; + function readFile(path: string, encoding: Encoding): Promise; + function readFile(path: string): Promise; + function readFileSync(path: string, encoding: Encoding | { encoding: Encoding }): string; + function readFileSync(path: string): Uint8Array; function readSync(fd: number, buffer: ArrayBufferView, offset?: number, length?: number, position?: number | null): number; function stream(path: string): Promise; function open(path: string, flags?: string, mode?: number): Promise; function openSync(path: string, flags?: string, mode?: number): number; function close(fd: number): Promise; function closeSync(fd: number): void; - function writeFile(path: string, data: string): Promise; - function writeFileSync(path: string, data: string): void; + function writeFile(path: string, data: string | ArrayBufferView): Promise; + function writeFileSync(path: string, data: string | ArrayBufferView): void; function write(fd: number, data: string | ArrayBufferView, offset?: number, length?: number, position?: number | null): Promise; function writeSync(fd: number, data: string | ArrayBufferView, offset?: number, length?: number, position?: number | null): number; function writev(fd: number, buffers: ArrayBufferView[], position?: number): Promise; @@ -36,8 +47,8 @@ declare module 'fs' { function renameSync(oldPath: string, newPath: string): void; function unlink(path: string): Promise; function unlinkSync(path: string): void; - function mkdir(path: string, options?: { recursive?: boolean }): Promise; - function mkdirSync(path: string, options?: { recursive?: boolean }): void; + function mkdir(path: string, options?: { recursive?: boolean; mode?: number }): Promise; + function mkdirSync(path: string, options?: number | { recursive?: boolean; mode?: number }): void; function rmdir(path: string): Promise; function rmdirSync(path: string): void; function stat(path: string): Promise; @@ -69,21 +80,31 @@ declare module 'fs/promises' { isSymbolicLink(): boolean; } + type Encoding = 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'latin1' | 'binary' | 'base64' | 'base64url' | 'hex' | 'ascii'; + const constants: { F_OK: number; R_OK: number; W_OK: number; X_OK: number; + O_RDONLY: number; + O_WRONLY: number; + O_RDWR: number; + O_CREAT: number; + O_EXCL: number; + O_TRUNC: number; + O_APPEND: number; }; - function readFile(path: string): Promise; + function readFile(path: string, encoding: Encoding): Promise; + function readFile(path: string): Promise; function open(path: string, flags?: string, mode?: number): Promise; function close(fd: number): Promise; - function writeFile(path: string, data: string): Promise; + function writeFile(path: string, data: string | ArrayBufferView): Promise; function write(fd: number, data: string | ArrayBufferView, offset?: number, length?: number, position?: number | null): Promise; function writev(fd: number, buffers: ArrayBufferView[], position?: number): Promise; function unlink(path: string): Promise; - function mkdir(path: string, options?: { recursive?: boolean }): Promise; + function mkdir(path: string, options?: { recursive?: boolean; mode?: number }): Promise; function rmdir(path: string): Promise; function stat(path: string): Promise; function exists(path: string): Promise;