diff --git a/README.md b/README.md index ed30225..498f0c6 100644 --- a/README.md +++ b/README.md @@ -1299,6 +1299,20 @@ captured. → [0 1 3 6 10] ``` +One pit-fall is that, as Insitux only captures data just as a closure is being +created, nested closures can be tricky due to the change in scope. +This example will cause a reference error as `x` technically goes out-of-scope +before `(fn x)` is materialised: + +```clj +(let x 1 f (fn (fn x))) +(f) ;Reference Error: "x" did not exist + +(let x 1 f (fn (fn x))) +(var x 1) +(f) ;1 - this is fine as calling f captures x, which exists even within f itself +``` + **Destructuring** Destructuring is a syntax available as part of named function signatures, diff --git a/package.json b/package.json index 773506c..9e24ec4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "insitux", - "version": "23.11.3", + "version": "23.11.16", "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 8c87752..525ea27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export const insituxVersion = 231103; +export const insituxVersion = 231116; import { asBoo } from "./checks"; import { arityCheck, keyOpErr, numOpErr, typeCheck, typeErr } from "./checks"; import { isLetter, isDigit, isSpace, isPunc } from "./checks"; diff --git a/src/parse.ts b/src/parse.ts index 9c1cfac..6f1f14a 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -522,22 +522,33 @@ function parseForm( } else if (op === "#" || op === "@" || op === "fn") { const pins: ParserIns[] = []; const name = node2str([firstNode, ...nodes]); + /** Used to exclude from capturing in makeClosure. */ const cloParams: string[] = []; + /** Used within the closure, with shadowing of formParams as needed. */ + const cloFormParams: ParamsShape = []; + /** Informs capturing of child-closure references to parent params. */ const outerParams = formParams.map(p => p.name); + /** E.g. `(fn x)` */ let monoFnBody = false; if (op === "fn") { - const parsedParams = parseParams(nodes, false); - push( - cloParams, - parsedParams.shape.map(p => p.name), - ); - push(formParams, parsedParams.shape); - push(pins, parsedParams.errors); + const { shape, errors } = parseParams(nodes, false); + const paramNames = shape.map(p => p.name); + push(cloParams, paramNames); + push(pins, errors); + //Closures are allowed to see both parent parameters and their own as + // they then pick from them to capture appropriately. However, we need + // to shadow parent parameters with the same name as child parameters. + const shadowed = outerParams.filter(p => has(paramNames, p)); + push(cloFormParams, formParams.filter(p => !has(shadowed, p.name))); + push(cloFormParams, shape); + if (!len(nodes)) { return err("provide a body"); } monoFnBody = len(nodes) === 1; nodes.unshift({ typ: "sym", text: "do", errCtx }); + } else { + push(cloFormParams, formParams); } //Rewrite partial closure to #(... [body] args) if (op === "@") { @@ -555,7 +566,7 @@ function parseForm( { typ: "sym", text: "args", errCtx }, ]; } - push(pins, parseForm(nodes, formParams, op !== "@")); + push(pins, parseForm(nodes, cloFormParams, op !== "@")); const cins = pins.filter(i => i.typ !== "err"); const errors = pins.filter(i => i.typ === "err"); if (len(errors)) { diff --git a/src/test.ts b/src/test.ts index 519ed3b..fa78576 100644 --- a/src/test.ts +++ b/src/test.ts @@ -386,6 +386,11 @@ const tests: { code: `(function f x (fn y (fn z [x y z]))) (((f :a) :b) :c)`, out: `[:a :b :c]`, }, + { + name: "Clojure w/ shadow param", + code: `(function f y ((fn x y [x y]) 1 y)) (f 5)`, + out: `[1 5]`, + }, { name: "Destructure var", code: `(var [x [y]] [1 [2]]) [y x]`,