diff --git a/.gitignore b/.gitignore index eee0bde..789ec09 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ package-lock.json dist/ node/ *.ix +unvisited.txt diff --git a/README.md b/README.md index 05a8708..0afcf57 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,13 @@ $ ix file.ix #execute file.ix in the working directory $ ix -e "PI" #execute provided string $ ix -b #disable REPL budgets (loops, recur, etc) $ ix -nc #turn off "colour mode" for REPL errors, etc +$ ix -unv #generate unvisited.txt of unvisited code line:column $ ix [args] -r #… then open a REPL session $ ix [...] -- [...] #seperation between ix args and program args (e.g. %0) Most arguments/switches can be mixed with one another. $ ix i #installs dependencies listed in deps.txt -$ ix r #remove dependencies listed in deps +$ ix r #remove dependencies listed in deps.txt $ ix i user/repo #clone Github repository into the .ix directory $ ix r user/repo #… and subsequently remove $ ix i alias http… #download file via HTTP into the .ix directory as alias.ix @@ -193,7 +194,7 @@ built-in operations each within an example, with results after a `→`. ; when its condition is truthy (function f (return-when true 123) (print "hello")) (f) → 123 -(function f (return-when false) (print "hi")) +(function f (return-unless true) (print "hi")) (f) → null ;and prints "hi" ;Tests a condition and executes and returns either the second or third argument diff --git a/corpus/nearest-vowel-dist.ix b/corpus/nearest-vowel-dist.ix index 7573a49..c89d91c 100644 --- a/corpus/nearest-vowel-dist.ix +++ b/corpus/nearest-vowel-dist.ix @@ -8,3 +8,5 @@ (print [[0 0 0 0 0] [1 0 1 2 3] [0 1 2 1 0 1 2 3]]) (proj nearest-vowel-dist "aaaaa" "babbb" "abcdabcd") + +; Could be improved: https://codegolf.stackexchange.com/questions/233837/find-me-vowels-near-you diff --git a/media/insitux-text.svg b/media/insitux-text.svg new file mode 100644 index 0000000..171f6f1 --- /dev/null +++ b/media/insitux-text.svg @@ -0,0 +1,36 @@ + + + + + + + insitux + diff --git a/media/insitux.svg b/media/insitux.svg index 171f6f1..183493a 100644 --- a/media/insitux.svg +++ b/media/insitux.svg @@ -9,28 +9,31 @@ id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> - - - insitux + + + + + + + diff --git a/media/ix-text.svg b/media/ix-text.svg new file mode 100644 index 0000000..091a175 --- /dev/null +++ b/media/ix-text.svg @@ -0,0 +1,36 @@ + + + + + + + ix + diff --git a/media/ix.svg b/media/ix.svg index 091a175..87999c2 100644 --- a/media/ix.svg +++ b/media/ix.svg @@ -9,28 +9,31 @@ id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> - - - ix + + + + + + + diff --git a/media/logo.html b/media/logo.html deleted file mode 100644 index c7fc4dc..0000000 --- a/media/logo.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - -
-
insitu
- x -
-
-
-
-
i
- x -
diff --git a/package.json b/package.json index 51e4a6c..0a8ac99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "insitux", - "version": "23.10.1", + "version": "23.10.2", "description": "Extensible scripting language written in portable TypeScript.", "main": "dist/invoker.js", "types": "dist/invoker.d.ts", diff --git a/src/index.ts b/src/index.ts index b8e4614..449f4d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export const insituxVersion = 231001; +export const insituxVersion = 231002; import { asBoo } from "./checks"; import { arityCheck, keyOpErr, numOpErr, typeCheck, typeErr } from "./checks"; import { isLetter, isDigit, isSpace, isPunc } from "./checks"; @@ -23,6 +23,8 @@ import { _boo, _num, _str, _key, _vec, _dic, _nul, _fun, str } from "./val"; let lets: { [key: string]: Val } = {}; let recurArgs: undefined | Val[]; let forState: undefined | "for" | "brk" | "cnt" | "ret"; +/** Used for code coverage reporting. */ +let lineCols: { [key: string]: 1 } = {}; type _Exception = { errors: InvokeError[] }; function _throw(errors: InvokeError[]): Val { @@ -1414,7 +1416,7 @@ function exeFunc( const stack: Val[] = []; for (let i = 0, lim = len(func.ins); i < lim; ++i) { const ins = func.ins[i]; - const { errCtx } = func.ins[i]; + const { errCtx } = ins; const tooManyLoops = ctx.loopBudget < 1; if (tooManyLoops || ctx.callBudget < 1) { @@ -1422,6 +1424,12 @@ function exeFunc( _throw([{ e: "Budget", m, errCtx }]); } + if (ctx.coverageReport) { + delete lineCols[ + `${ins.errCtx.invokeId}\t${ins.errCtx.line}\t${ins.errCtx.col}` + ]; + } + switch (ins.typ) { case "val": stack.push(ins.value); @@ -1465,6 +1473,12 @@ function exeFunc( const name = ins.value; if (ops[name]) { stack.push(_fun(name)); + } else if (name in lets) { + stack.push(lets[name]); + } else if (name in ctx.env.vars) { + stack.push(ctx.env.vars[name]); + } else if (name in ctx.env.funcs) { + stack.push(_fun(name)); } else if (starts(name, "$")) { if (!ctx.get) { const m = `"get" feature not implemented on this platform`; @@ -1475,12 +1489,6 @@ function exeFunc( return _throw([{ e: "External", m: valAndErr.err, errCtx }]); } stack.push(valAndErr); - } else if (name in lets) { - stack.push(lets[name]); - } else if (name in ctx.env.vars) { - stack.push(ctx.env.vars[name]); - } else if (name in ctx.env.funcs) { - stack.push(_fun(name)); } else { _throw([{ e: "Reference", m: `"${name}" did not exist`, errCtx }]); } @@ -1577,12 +1585,12 @@ function exeFunc( break; case "clo": { //Ensure any in-scope declarations are captured here - const derefIns = slice(ins.value.derefs).map(ins => { + const derefIns: Ins[] = slice(ins.value.derefs).map(ins => { const decl = ins.typ === "val" && ins.value.t === "str" && (lets[ins.value.v] ?? ctx.env.vars[ins.value.v]); - return decl ? { typ: "val", value: decl } : ins; + return decl ? { typ: "val", value: decl, errCtx } : ins; }); //Dereference closure captures const captures = exeFunc(ctx, { ins: derefIns }, args, true); @@ -1712,10 +1720,17 @@ function parseAndExe( _throw(parsed.errors); } ctx.env.funcs = { ...ctx.env.funcs, ...parsed.funcs }; + lineCols = parsed.lineCols.reduce((acc, lineCol) => { + acc[lineCol] = 1; + return acc; + }, {} as { [key: string]: 1 }); if (!("entry" in ctx.env.funcs)) { + ctx.coverageReport?.(objKeys(lineCols), parsed.lineCols); return; } - return exeFunc(ctx, ctx.env.funcs["entry"], params, false); + const result = exeFunc(ctx, ctx.env.funcs["entry"], params, false); + ctx.coverageReport?.(objKeys(lineCols), parsed.lineCols); + return result; } function ingestExternalOperations(functions: ExternalFunctions) { diff --git a/src/invoker.ts b/src/invoker.ts index 8db8991..16031d9 100644 --- a/src/invoker.ts +++ b/src/invoker.ts @@ -9,7 +9,7 @@ export type InvokeOutput = { }[]; const invocations = new Map(); -export const parensRx = /[\[\]\(\) ,]/; +export const parensRx = /[[\]() ,]/; export function invoker( ctx: Ctx, diff --git a/src/parse.ts b/src/parse.ts index d9d6d93..839a7b9 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -346,14 +346,16 @@ function parseForm( const parsed = nodes.map(nodeParser); const [cond, body] = [parsed[0], slice(parsed, 1)]; const bodyIns = poppedBody(body); - return [ - ...cond, - ...(op === "unless" + const unlessIns: Ins[] = + op === "unless" ? [ - { typ: "val", value: { t: "func", v: "not" } }, - { typ: "exe", value: 1 }, + { typ: "val", value: { t: "func", v: "not" }, errCtx }, + { typ: "exe", value: 1, errCtx }, ] - : []), + : []; + return [ + ...cond, + ...unlessIns, { typ: "if", value: len(bodyIns) + 1, errCtx }, ...bodyIns, { typ: "jmp", value: 1, errCtx }, @@ -631,15 +633,23 @@ function parseForm( const args = nodes.map(nodeParser); const firstSym = symAt([firstNode]); - if (firstSym === "return-when") { + if (firstSym === "return-when" || firstSym === "return-unless") { if (len(args) < 1) { return [{ typ: "err", value: "provide a condition", errCtx }]; } const cond = args[0]; const params = slice(args, 1); const flatParams = flat(params); + const unlessIns: Ins[] = + firstSym === "return-unless" + ? [ + { typ: "val", value: { t: "func", v: "not" }, errCtx }, + { typ: "exe", value: 1, errCtx }, + ] + : []; return [ ...cond, + ...unlessIns, { typ: "if", value: len(flatParams) + 1, errCtx }, ...flatParams, { typ: "ret", value: !!(len(args) - 1), errCtx }, @@ -976,6 +986,10 @@ function insErrorDetect(fins: Ins[]): InvokeError[] | undefined { } break; case "for": + const errors = insErrorDetect(ins.body); + if (errors) { + return errors; + } stack.push({ types: ["vec"] }); break; default: @@ -984,20 +998,28 @@ function insErrorDetect(fins: Ins[]): InvokeError[] | undefined { } } +function extractLineCols(ins: Ins[]): string[] { + return ins.flatMap(i => + i.typ === "for" + ? extractLineCols(i.body) + : [`${i.errCtx.invokeId}\t${i.errCtx.line}\t${i.errCtx.col}`], + ); +} + export function parse( code: string, invokeId: string, -): { funcs: Funcs; errors: InvokeError[] } { +): { funcs: Funcs; errors: InvokeError[]; lineCols: string[] } { const { tokens, stringError } = tokenise(code, invokeId); const tokenErrors = tokenErrorDetect(stringError, tokens); if (len(tokenErrors)) { - return { errors: tokenErrors, funcs: {} }; + return { errors: tokenErrors, funcs: {}, lineCols: [] }; } const okFuncs: Func[] = [], errors: InvokeError[] = []; const tree = treeise(slice(tokens)); if (!len(tree)) { - return { funcs: {}, errors }; + return { funcs: {}, errors, lineCols: [] }; } const collected = collectFuncs(tree); const namedNodes: NamedNodes[] = []; @@ -1016,7 +1038,9 @@ export function parse( } }); push(errors, flat(okFuncs.map(f => insErrorDetect(f.ins) ?? []))); + const allLineCols = extractLineCols(okFuncs.flatMap(f => f.ins)); + const lineCols = [...new Set(allLineCols)]; const funcs: Funcs = {}; okFuncs.forEach(func => (funcs[func.name ?? ""] = func)); - return { errors, funcs }; + return { errors, funcs, lineCols }; } diff --git a/src/repl.ts b/src/repl.ts index c95b6a5..74e67ac 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -19,6 +19,7 @@ import { join as pathJoin, dirname } from "path"; const githubRegex = /^(?!https*:)[^\/]+?\/[^\/]+$/; let colourMode = true; +let coverages: { unvisited: string[]; all: string[] }[] = []; //#region External operations function invokeVal( @@ -492,6 +493,14 @@ const extractSwitch = (args: string[], arg: string) => { }; async function processCliArguments(args: string[]) { + if (extractSwitch(args, "-unv")) { + if (args.length) { + ctx.coverageReport = collectCoverages; + } else { + console.log("-unv was ignored."); + } + } + if (!args.length) { startRepl(); return; @@ -538,8 +547,9 @@ async function processCliArguments(args: string[]) { const invoke = (code: string, id = code) => printErrorOutput(invoker(ctx, code, id, params).output); - if (matchArgs([/^\.$/])) { - args[0] = "entry.ix"; + const entryDotIdx = args.indexOf("."); + if (entryDotIdx !== -1) { + args[entryDotIdx] = "entry.ix"; } const [arg0, arg1, arg2] = args; @@ -569,11 +579,18 @@ async function processCliArguments(args: string[]) { } else { //Execute files for (const path of args) { - if (!existsSync(path)) { - console.log(`${path} not found - ignored.`); - } else { - const code = readFileSync(path).toString(); - invoke(code, path); + try { + if (!existsSync(path)) { + console.log(`${path} not found - ignored.`); + } else { + const code = readFileSync(path).toString(); + invoke(code, path); + } + } catch (e) { + console.error( + `Error executing "${path}":`, + typeof e === "object" && e && "message" in e ? e.message : e, + ); } } //Execute optional inline @@ -582,6 +599,10 @@ async function processCliArguments(args: string[]) { } } + if (ctx.coverageReport) { + generateCoverageReport(); + } + if (openReplAfter) { startRepl(); } @@ -598,7 +619,10 @@ function readHistory() { } function startRepl() { + const coverageCallback = ctx.coverageReport; + ctx.coverageReport = undefined; printErrorOutput(invoker(ctx, `(str "Insitux " (version) " REPL")`).output); + ctx.coverageReport = coverageCallback; if (existsSync(".repl.ix")) { printErrorOutput(invoker(ctx, readFileSync(".repl.ix").toString()).output); @@ -684,4 +708,29 @@ function printErrorOutput(lines: InvokeOutput) { } }); } + +function collectCoverages(unvisited: string[], all: string[]) { + coverages.push({ unvisited, all }); +} + +function generateCoverageReport() { + const unvisited = coverages.flatMap(c => c.unvisited); + const all = coverages.reduce((n, c) => n + c.all.length, 0); + const collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: "base", + }); + unvisited.sort(collator.compare); + writeFileSync( + "unvisited.txt", + unvisited.map(u => (u.startsWith("-") ? u.substring(1) : u)).join("\n") + + "\n", + ); + const coverage = Math.round(((all - unvisited.length) / all) * 1000) / 10; + console.log( + "unvisited.txt generated:", + unvisited.length, + `unvisited (${coverage}% coverage)`, + ); +} //#endregion diff --git a/src/types.ts b/src/types.ts index 234febb..8869de4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,8 @@ export type Ctx = { /** Called when Insitux cannot find a function definition otherwise. * You should return an error if unknown externally too. */ exe?: (name: string, args: Val[]) => ValOrErr; + /** Callback for code coverage report */ + coverageReport?: (uncoveredLineCols: string[], allLineCols: string[]) => void; /** Function and variable definitions, retained by you for each invocation. */ env: Env; /** The number of loops an invocation is permitted. */ @@ -558,7 +560,8 @@ export const ops: { }; export const syntaxes = [ - ...["function", "fn", "var", "let", "var!", "let!", "return", "if", "if-not"], + ...["function", "fn", "var", "let", "var!", "let!", "if", "if-not"], + ...["return", "return-when", "return-unless"], ...["when", "unless", "while", "loop", "for", "match", "satisfy"], ...["catch", "args", "E", "PI"], ];