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 @@
-
-
-
-
-
-
-
-
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"],
];