diff --git a/src/config.lsc b/src/config.lsc index 283df0f..fe3b7f4 100644 --- a/src/config.lsc +++ b/src/config.lsc @@ -19,6 +19,11 @@ export getMetadata() -> { defaultValue: "default" stage: "1" } + catchExpression: { + description: "Catch and transform errors while evaluating an expression. (HIGHLY EXPERIMENTAL. DO NOT USE IN PRODUCTION.)" + valueType: "boolean" + stage: "-50" + } bangCall: { description: "Call functions with paren-free syntax using `!`" valueType: "boolean" @@ -106,6 +111,7 @@ export getParserOpts(pluginOpts, initialParserOpts) -> if pluginOpts?.placeholderArgs: plugins.push("syntacticPlaceholder") if pluginOpts?.placeholder: parserOpts.placeholder = pluginOpts.placeholder + if pluginOpts.catchExpression: plugins.push("catchExpression") // TODO: watch upstream on pattern matching; default to their syntax when complete // patternMatchingVersion = pluginOpts?.patternMatching or "v4" diff --git a/src/helpers/tails.lsc b/src/helpers/tails.lsc index 98e91f5..ab548bc 100644 --- a/src/helpers/tails.lsc +++ b/src/helpers/tails.lsc @@ -29,6 +29,8 @@ export getTails(path, allowLoops) -> add(path.get("block")) add(path.get("handler")) add(path.get("finalizer")) + | ~isa("CatchClause"): + add(path.get("body")) | ~isa("MatchStatement"): for idx i in node.cases: add(path.get(`cases.${i}.consequent`)) diff --git a/src/lscNodeTypes.lsc b/src/lscNodeTypes.lsc index 021716c..dc6ddfd 100644 --- a/src/lscNodeTypes.lsc +++ b/src/lscNodeTypes.lsc @@ -286,3 +286,37 @@ export registerLightscriptNodeTypes(t): void -> }, }, }); + + if not t.hasType("CatchExpression"): + definePluginType("CatchExpression", { + builder: ["expression", "cases"], + visitor: ["expression", "cases"], + aliases: ["Expression"], + fields: { + expression: { + validate: assertNodeType("Expression") + }, + cases: { + validate: chain(assertValueType("array"), assertEach(assertNodeType("CatchCase"))) + } + } + }); + + if not t.hasType("CatchCase"): + definePluginType("CatchCase", { + builder: ["atoms", "binding", "consequent"], + visitor: ["atoms", "binding", "consequent"], + fields: { + atoms: { + validate: chain(assertValueType("array"), assertEach(assertNodeType("Expression"))) + optional: true + } + binding: { + validate: assertNodeType("Identifier", "ArrayPattern", "ObjectPattern") + optional: true + } + consequent: { + validate: assertNodeType("Expression", "Statement") + } + } + }); diff --git a/src/transforms/catchExpression.lsc b/src/transforms/catchExpression.lsc new file mode 100644 index 0000000..c39f0a4 --- /dev/null +++ b/src/transforms/catchExpression.lsc @@ -0,0 +1,80 @@ +import t, { isa } from '../types' +import { transformTails } from '../helpers/tails' + +import { + getLoc, placeAtLoc as atLoc, placeAtNode as atNode, + getSurroundingLoc, span, traverse, + placeTreeAtLocWhenUnplaced as allAtLoc +} from 'ast-loc-utils' + +import { getMatchInfo, transformMatchCases } from './match' + +transformVarDeclCatchExpression(path, catchExprPath, isLinter): void -> + { node } = catchExprPath + + resRef = path.scope.generateUidIdentifier("val") + errRef = path.scope.generateUidIdentifier("err")~atLoc(getLoc(node)~span(1)) + catchBody = getMatchInfo(catchExprPath, errRef, isLinter)~transformMatchCases(catchExprPath.get("cases")) + + path.insertBefore! t.variableDeclaration("let", [t.variableDeclarator(resRef)]) + path.insertBefore! t.tryStatement( + // try { _val = expr } + t.blockStatement([ + t.expressionStatement(t.assignmentExpression("=", resRef, node.expression)) + ]) + // catch (err) { ... } + t.catchClause( + errRef + t.blockStatement([catchBody]) + ) + ) + // x = _val + declaratorPath = path.get("declarations.0") + declaratorPath.node.init = resRef + // Transform tails in the catch-clause to assignments + tryPath = path.getPrevSibling() + transformTails(tryPath.get("handler.body"), false, false, (node) -> + t.assignmentExpression("=", resRef, node)~atNode(node) + ) + +transformPessimizedCatchExpression(path, isLinter): void -> + { node } = path + + errRef = path.scope.generateUidIdentifier("err")~atLoc(getLoc(node)~span(1)) + catchBody = getMatchInfo(path, errRef, isLinter)~transformMatchCases(path.get("cases")) + + iife = t.callExpression( + t.arrowFunctionExpression( + [] + t.blockStatement([ + t.tryStatement( + t.blockStatement([ + t.returnStatement(node.expression) + ]) + t.catchClause( + errRef + t.blockStatement([catchBody]) + ) + ) + ]) + node.expression~isa("AwaitExpression") // async + ) + [] + ) + + path.replaceWith(iife) + +isVarDeclCatchExpr(path) -> + path.parent~isa("VariableDeclarator") + and path.parentPath.parent.declarations.length == 1 + and path.parentPath.parentPath.listKey == "body" + +export transformCatchExpression(path, isLinter): void -> + console.log(path.parent) + console.log(path.parentPath.parent) + console.log(path.parentPath.parentPath.parent) + if path~isVarDeclCatchExpr!: + transformVarDeclCatchExpression(path.parentPath.parentPath, path, isLinter) + else: + transformPessimizedCatchExpression(path, isLinter) + diff --git a/src/transforms/match.lsc b/src/transforms/match.lsc index 9eeeb3f..a771b07 100644 --- a/src/transforms/match.lsc +++ b/src/transforms/match.lsc @@ -135,7 +135,7 @@ transformMatchCase(mtch, casePath) -> test: if c.outerGuard?.type != "MatchElse": mtch~transformMatchTest(casePath) } -transformMatchCases(mtch, [casePath, ...rest]) -> +export transformMatchCases(mtch, [casePath, ...rest]) -> c = casePath.node { test, consequent } = mtch~transformMatchCase(casePath) @@ -148,7 +148,7 @@ transformMatchCases(mtch, [casePath, ...rest]) -> consequent // Computed values needed in recursive descent. -getMatchInfo(path, discriminantRef, isLinter) -> +export getMatchInfo(path, discriminantRef, isLinter) -> { node } = path { path, node, discriminantRef diff --git a/src/visitors/main.lsc b/src/visitors/main.lsc index 1f2d931..0fe7790 100644 --- a/src/visitors/main.lsc +++ b/src/visitors/main.lsc @@ -15,6 +15,7 @@ import { maybeTransformArrayWithSpreadLoops, maybeTransformObjectWithSpreadLoops import { transformExistentialExpression, transformSafeSpreadElement } from "../transforms/safe" import { maybeReplaceWithInlinedOperator } from "../transforms/inlinedOperators" import { transformForInArrayStatement, transformForInObjectStatement, lintForInArrayStatement, lintForInObjectStatement } from "../transforms/for" +import { transformCatchExpression } from "../transforms/catchExpression" import { markIdentifier } from "../state/stdlib" @@ -221,6 +222,9 @@ export default mainPass(compilerState, programPath): void -> MatchStatement(path): void -> matching.transformMatchStatement(path, opts.__linter) + CatchExpression(path): void -> + transformCatchExpression(path, opts.__linter) + ForOfStatement(path): void -> // Auto-const { node } = path; { left } = node diff --git a/test/fixtures/catch-expression/basic/actual.js b/test/fixtures/catch-expression/basic/actual.js new file mode 100644 index 0000000..9ab2efb --- /dev/null +++ b/test/fixtures/catch-expression/basic/actual.js @@ -0,0 +1,2 @@ +a = b() + catch Error: panic() diff --git a/test/fixtures/catch-expression/basic/expected.js b/test/fixtures/catch-expression/basic/expected.js new file mode 100644 index 0000000..0f6d50c --- /dev/null +++ b/test/fixtures/catch-expression/basic/expected.js @@ -0,0 +1,12 @@ +import _isMatch from "@oigroup/lightscript-runtime/isMatch"; +let _val; + +try { + _val = b(); +} catch (_err) { + if (_isMatch(Error, _err)) { + _val = panic(); + } +} + +const a = _val; \ No newline at end of file diff --git a/test/fixtures/catch-expression/options.json b/test/fixtures/catch-expression/options.json new file mode 100644 index 0000000..672c3ec --- /dev/null +++ b/test/fixtures/catch-expression/options.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + ["lightscript", { + "isLightScript": true, + "catchExpression": true + }] + ] +} +